Files
simple-nanoshare/tests/test_link_composer.py
T
daniel156161 ed62837f6d
Build and Push Docker Container / build-and-push (push) Successful in 1m36s
feat: add self-contained link composer
- 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.
2026-08-14 08:56:42 +02:00

127 lines
4.1 KiB
Python

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())