"""Tests for extension distribution endpoints."""
import hashlib
import json
import zipfile
from unittest.mock import MagicMock, patch
import pytest
from httpx import ASGITransport, AsyncClient
from gnexus_creds import api as api_module
def _make_zip(path) -> None:
with zipfile.ZipFile(path, "w") as zf:
zf.writestr("manifest.json", "{}")
def _sha256(path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
@pytest.fixture
def extension_dir(tmp_path):
ext = tmp_path / "extensions"
dist = ext / "dist"
dist.mkdir(parents=True)
chrome = dist / "gnexus-creds-extension-chrome-0.1.0.zip"
firefox = dist / "gnexus-creds-extension-firefox-0.1.0.zip"
for path in (chrome, firefox):
_make_zip(path)
manifest = {
"version": "0.1.0",
"released_at": "2026-08-24T00:00:00Z",
"notes": "test build",
"builds": [
{
"browser": "chrome",
"filename": chrome.name,
"sha256": "a" * 64,
"size": chrome.stat().st_size,
},
{
"browser": "firefox",
"filename": firefox.name,
"sha256": "b" * 64,
"size": firefox.stat().st_size,
},
],
}
(ext / "manifest.json").write_text(json.dumps(manifest))
return ext
@pytest.fixture
def patched_settings(extension_dir):
settings = MagicMock()
settings.extension_dir = str(extension_dir)
with patch.object(api_module, "get_settings", return_value=settings):
yield
@pytest.mark.anyio
async def test_extension_info_lists_builds(app, patched_settings):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/api/v1/extension")
assert response.status_code == 200, response.text
data = response.json()
assert data["version"] == "0.1.0"
assert data["notes"] == "test build"
browsers = {build["browser"] for build in data["builds"]}
assert browsers == {"chrome", "firefox"}
for build in data["builds"]:
assert build["download_url"].startswith("/api/v1/extension/download/")
assert build["size"] > 0
assert len(build["sha256"]) == 64
@pytest.mark.anyio
async def test_extension_download_returns_file(app, patched_settings, extension_dir):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
filename = "gnexus-creds-extension-chrome-0.1.0.zip"
response = await client.get(f"/api/v1/extension/download/{filename}")
assert response.status_code == 200, response.text
assert "attachment" in response.headers.get("content-disposition", "")
assert filename in response.headers["content-disposition"]
assert len(response.content) > 0
@pytest.mark.anyio
async def test_extension_download_unknown_returns_404(app, patched_settings):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/api/v1/extension/download/does-not-exist.zip")
assert response.status_code == 404
@pytest.mark.anyio
async def test_extension_info_requires_auth(auth_app, patched_settings):
async with AsyncClient(transport=ASGITransport(app=auth_app), base_url="http://test") as client:
response = await client.get("/api/v1/extension")
assert response.status_code == 401
def _write_pack(ext, version=1):
pack_dir = ext / "pack"
version_dir = pack_dir / f"v{version}"
version_dir.mkdir(parents=True, exist_ok=True)
files = {}
contents = {
"content.css": ".gnexus-creds-autofill-card { border: 1px solid #aaa }\n",
"config.json": json.dumps({"timings": {"scanDebounceMs": 300, "cardExitMs": 280}}),
}
for name, text in contents.items():
(version_dir / name).write_text(text)
files[name] = {
"sha256": hashlib.sha256(text.encode()).hexdigest(),
"size": len(text.encode()),
}
pack_json = {
"packVersion": version,
"minExtensionVersion": "0.1.0",
"maxExtensionVersion": None,
"publishedAt": "2026-10-02T00:00:00Z",
"files": files,
}
(pack_dir / "pack.json").write_text(json.dumps(pack_json))
return pack_json
@pytest.mark.anyio
async def test_pack_json_is_public(app, patched_settings, extension_dir):
_write_pack(extension_dir)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/api/v1/extension/pack")
assert response.status_code == 200, response.text
data = response.json()
assert data["packVersion"] == 1
assert data["minExtensionVersion"] == "0.1.0"
assert set(data["files"]) == {"content.css", "config.json"}
assert response.headers["cache-control"] == "no-cache, must-revalidate"
@pytest.mark.anyio
async def test_pack_json_public_without_token(auth_app, patched_settings, extension_dir):
_write_pack(extension_dir)
async with AsyncClient(transport=ASGITransport(app=auth_app), base_url="http://test") as client:
response = await client.get("/api/v1/extension/pack")
assert response.status_code == 200, response.text
@pytest.mark.anyio
async def test_pack_file_served_immutable(app, patched_settings, extension_dir):
_write_pack(extension_dir)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/api/v1/extension/pack/file/1/content.css")
assert response.status_code == 200, response.text
assert "immutable" in response.headers["cache-control"]
assert response.text == (extension_dir / "pack/v1/content.css").read_text()
@pytest.mark.anyio
async def test_pack_file_wrong_version_returns_404(app, patched_settings, extension_dir):
_write_pack(extension_dir)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/api/v1/extension/pack/file/2/content.css")
assert response.status_code == 404
@pytest.mark.anyio
async def test_pack_file_unknown_filename_returns_404(app, patched_settings, extension_dir):
_write_pack(extension_dir)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/api/v1/extension/pack/file/1/nope.css")
assert response.status_code == 404
@pytest.mark.anyio
async def test_pack_file_traversal_returns_404(app, patched_settings, extension_dir):
_write_pack(extension_dir)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/api/v1/extension/pack/file/1/..%2F..%2Fdist%2Fgnexus-creds-extension-chrome-0.1.0.zip")
assert response.status_code == 404
@pytest.mark.anyio
async def test_pack_missing_returns_404(app, patched_settings):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/api/v1/extension/pack")
assert response.status_code == 404
assert response.json()["error"]["code"] == "pack_not_found"
@pytest.mark.anyio
async def test_extension_info_includes_pack(app, patched_settings, extension_dir):
_write_pack(extension_dir)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/api/v1/extension")
assert response.status_code == 200, response.text
assert response.json()["pack"]["pack_version"] == 1
@pytest.mark.anyio
async def test_extension_info_pack_null_when_absent(app, patched_settings):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/api/v1/extension")
assert response.status_code == 200, response.text
assert response.json()["pack"] is None