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.
127 lines
4.1 KiB
Python
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())
|