feat: add self-contained link composer
Build and Push Docker Container / build-and-push (push) Successful in 1m36s
Build and Push Docker Container / build-and-push (push) Successful in 1m36s
- Add structured URL codec with compact alphabets and dictionary support. - Add public decode, redirect, and QR endpoints for composed links. - Add authenticated encode API and link composer UI. - Generate PNG QR codes via qrcode with Pillow support. - Register the new blueprint and navigation entry. - Cover public decode, auth gating, redirect URL parsing, and QR output. - Bump NanoShare to 1.27.0 and lock new dependencies.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
from quart import Quart, g, session
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
class _NoOpLimiter:
|
||||
def limit(self, *_args, **_kwargs):
|
||||
return lambda fn: fn
|
||||
|
||||
def _load_module(module_name, module_path):
|
||||
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
def _install_stubs(monkeypatch):
|
||||
setup_stub = types.ModuleType("my_modules.app.setup")
|
||||
setup_stub.LIMITER = _NoOpLimiter()
|
||||
monkeypatch.setitem(sys.modules, "my_modules.app.setup", setup_stub)
|
||||
|
||||
header_stub = types.ModuleType("my_modules.decoratory.header")
|
||||
|
||||
def login_required(func):
|
||||
async def wrapper(*args, **kwargs):
|
||||
user = session.get("user")
|
||||
if user is None:
|
||||
return "unauthorized", 401
|
||||
return await func(user=user, *args, **kwargs)
|
||||
wrapper.__name__ = func.__name__
|
||||
return wrapper
|
||||
|
||||
header_stub.login_required = login_required
|
||||
monkeypatch.setitem(sys.modules, "my_modules.decoratory.header", header_stub)
|
||||
|
||||
def _client(monkeypatch):
|
||||
_install_stubs(monkeypatch)
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
module = _load_module("link_composer_route_under_test", root / "routes" / "side" / "link_composer.py")
|
||||
app = Quart(__name__, template_folder=str(root / "templates" / "side"))
|
||||
app.secret_key = "test-secret"
|
||||
app.register_blueprint(module.link_composer_bp)
|
||||
|
||||
@app.before_request
|
||||
async def wide_event_context():
|
||||
g.wide_event = {}
|
||||
|
||||
return app, app.test_client()
|
||||
|
||||
def test_decode_api_is_public(monkeypatch):
|
||||
async def run_test():
|
||||
_app, client = _client(monkeypatch)
|
||||
from link_composer.codec import encode_url
|
||||
|
||||
code = encode_url("https://example.com/target").code
|
||||
response = await client.post("/api/links/decode", json={"code": code})
|
||||
payload = await response.get_json()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert payload["ok"] is True
|
||||
assert payload["url"] == "https://example.com/target"
|
||||
|
||||
asyncio.run(run_test())
|
||||
|
||||
def test_decode_api_accepts_redirect_url(monkeypatch):
|
||||
async def run_test():
|
||||
_app, client = _client(monkeypatch)
|
||||
from link_composer.codec import encode_url
|
||||
|
||||
code = encode_url("https://example.com/target").code
|
||||
response = await client.post("/api/links/decode", json={"code": f"http://localhost/l/{code}"})
|
||||
payload = await response.get_json()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert payload["ok"] is True
|
||||
assert payload["url"] == "https://example.com/target"
|
||||
|
||||
asyncio.run(run_test())
|
||||
|
||||
def test_encode_api_requires_login(monkeypatch):
|
||||
async def run_test():
|
||||
_app, client = _client(monkeypatch)
|
||||
|
||||
response = await client.post("/api/links/encode", json={"url": "https://example.com/target"})
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
asyncio.run(run_test())
|
||||
|
||||
def test_encode_api_returns_qr_for_logged_in_user(monkeypatch):
|
||||
async def run_test():
|
||||
_app, client = _client(monkeypatch)
|
||||
async with client.session_transaction() as sess:
|
||||
sess["user"] = {"sub": "user_123", "name": "Demo"}
|
||||
|
||||
response = await client.post("/api/links/encode", json={"url": "https://example.com/target"})
|
||||
payload = await response.get_json()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert payload["ok"] is True
|
||||
assert payload["code"].startswith("u")
|
||||
encoded_code = quote(payload["code"], safe="")
|
||||
assert payload["redirect_url"].endswith(f"/l/{encoded_code}")
|
||||
assert f"/links/qr/{encoded_code}" in payload["qr_url"]
|
||||
|
||||
asyncio.run(run_test())
|
||||
|
||||
def test_qr_endpoint_returns_png_after_decoding(monkeypatch):
|
||||
async def run_test():
|
||||
_app, client = _client(monkeypatch)
|
||||
from link_composer.codec import encode_url
|
||||
|
||||
code = encode_url("https://example.com/target").code
|
||||
response = await client.get(f"/links/qr/{code}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["Content-Type"] == "image/png"
|
||||
assert (await response.get_data()).startswith(b"\x89PNG")
|
||||
|
||||
asyncio.run(run_test())
|
||||
Reference in New Issue
Block a user