267 lines
9.7 KiB
Python
267 lines
9.7 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from collections.abc import Callable
|
|
|
|
import httpx
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from m2_market_web import http as web_http
|
|
from m2_market_web.main import create_app
|
|
|
|
SECRET_VALUES = {
|
|
"fleet-passcode-secret",
|
|
"session-secret-value",
|
|
"mem-secret-key",
|
|
"ledger-secret-key",
|
|
"forgejo-secret-token",
|
|
}
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("FLEET_PASSCODE", "fleet-passcode-secret")
|
|
monkeypatch.setenv("SESSION_SECRET", "session-secret-value")
|
|
monkeypatch.setenv("MEMORY_API_URL", "https://memory.example")
|
|
monkeypatch.setenv("MEMORY_API_KEY", "mem-secret-key")
|
|
monkeypatch.setenv("LEDGER_URL", "https://ledger.example")
|
|
monkeypatch.setenv("LEDGER_SERVICE_KEY", "ledger-secret-key")
|
|
monkeypatch.setenv("FORGEJO_URL", "https://git.example")
|
|
monkeypatch.setenv("FORGEJO_TOKEN", "forgejo-secret-token")
|
|
monkeypatch.setenv("REGISTRY_REPO", "m2/market-registry")
|
|
monkeypatch.setenv("TENANT", "m2-core")
|
|
|
|
|
|
@pytest.fixture
|
|
def client(monkeypatch: pytest.MonkeyPatch) -> Callable[[httpx.MockTransport], TestClient]:
|
|
def build(transport: httpx.MockTransport) -> TestClient:
|
|
monkeypatch.setattr(web_http, "async_transport", lambda: transport)
|
|
return TestClient(create_app(), base_url="https://testserver")
|
|
|
|
return build
|
|
|
|
|
|
def _authed(test_client: TestClient) -> None:
|
|
response = test_client.post("/api/login", json={"passcode": "fleet-passcode-secret"})
|
|
assert response.status_code == 204
|
|
|
|
|
|
def _listing() -> dict:
|
|
return {
|
|
"name": "PDF Export",
|
|
"summary": "Branded reports",
|
|
"category": "reporting",
|
|
"price": {"amount": 7, "currency": "credits", "model": "fixed"},
|
|
"seller": "m2bd",
|
|
"stats": {"installs": 3},
|
|
"tenant_visibility": ["*"],
|
|
"evidence_summary": "Used in one production workflow",
|
|
"permissions": ["filesystem:write"],
|
|
"install_ref": "lst_pdf-v1.0.0",
|
|
}
|
|
|
|
|
|
def _ok_transport(request: httpx.Request) -> httpx.Response:
|
|
if request.url.host == "memory.example":
|
|
assert request.headers["X-API-Key"] == "mem-secret-key"
|
|
body = json.loads(request.content)
|
|
assert body["agent_id"] == "market:catalog"
|
|
assert body["limit"] == 6 or body["limit"] == 30
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"results": [
|
|
{
|
|
"score": 0.9,
|
|
"metadata": {"listing_id": "lst_pdf", "listing": _listing()},
|
|
},
|
|
{
|
|
"score": 0.8,
|
|
"metadata": {
|
|
"listing_id": "lst_hidden",
|
|
"listing": {"name": "Hidden", "tenant_visibility": ["other"]},
|
|
},
|
|
},
|
|
]
|
|
},
|
|
)
|
|
if request.url.host == "ledger.example":
|
|
assert request.headers["X-API-Key"] == "ledger-secret-key"
|
|
if request.url.path == "/balance/m2bd":
|
|
return httpx.Response(
|
|
200,
|
|
json={"operator_id": "m2bd", "balance": 42, "as_of": "2026-07-02T10:00:00Z"},
|
|
)
|
|
if request.url.path == "/balance/platform":
|
|
return httpx.Response(200, json={"operator_id": "platform", "balance": 8})
|
|
if request.url.path == "/tx" and request.url.params.get("operator_id") == "m2bd":
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"transactions": [
|
|
{
|
|
"ts": "2026-07-02T09:00:00Z",
|
|
"from_id": "mint",
|
|
"to": "m2bd",
|
|
"amount": 50,
|
|
"reason": "grant",
|
|
"ref": "starter",
|
|
"private": "ignored",
|
|
}
|
|
]
|
|
},
|
|
)
|
|
if request.url.path == "/tx":
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"transactions": [
|
|
{"from_id": "mint", "to_id": "m2bd", "amount": 50},
|
|
{"from_id": "m2bd", "to_id": "platform", "amount": 8},
|
|
]
|
|
},
|
|
)
|
|
if request.url.host == "git.example":
|
|
assert request.headers["Authorization"] == "Basic bTI6Zm9yZ2Vqby1zZWNyZXQtdG9rZW4="
|
|
if request.url.path == "/api/v1/repos/m2/market-registry/contents/audit":
|
|
return httpx.Response(200, json=[{"name": "2026-07-01.json"}])
|
|
if request.url.path == "/api/v1/repos/m2/market-registry/pulls":
|
|
return httpx.Response(
|
|
200,
|
|
json=[
|
|
{
|
|
"number": 12,
|
|
"title": "Publish PDF Export",
|
|
"created_at": "2026-07-01T10:00:00Z",
|
|
"labels": [{"name": "approved"}],
|
|
"html_url": "https://git.example/m2/market-registry/pulls/12",
|
|
}
|
|
],
|
|
)
|
|
if request.url.path == "/api/v1/repos/m2/market-registry/contents/listings":
|
|
return httpx.Response(200, json=[{"name": "lst_pdf", "type": "dir"}])
|
|
if (
|
|
request.url.path
|
|
== "/api/v1/repos/m2/market-registry/contents/listings/lst_pdf/listing.json"
|
|
):
|
|
return httpx.Response(200, json={"content_json": {"status": "published"}})
|
|
return httpx.Response(404, json={"unexpected": str(request.url)})
|
|
|
|
|
|
def test_login_and_auth_gate(client: Callable[[httpx.MockTransport], TestClient]) -> None:
|
|
test_client = client(httpx.MockTransport(_ok_transport))
|
|
assert test_client.get("/api/search?q=pdf").status_code == 401
|
|
assert test_client.post("/api/login", json={"passcode": "wrong"}).status_code == 401
|
|
_authed(test_client)
|
|
assert test_client.get("/api/search?q=pdf&limit=2").status_code == 200
|
|
|
|
|
|
def test_catalog_wallet_economy_governance_happy_paths(
|
|
client: Callable[[httpx.MockTransport], TestClient],
|
|
) -> None:
|
|
test_client = client(httpx.MockTransport(_ok_transport))
|
|
_authed(test_client)
|
|
|
|
search = test_client.get("/api/search?q=pdf&limit=2")
|
|
assert search.status_code == 200
|
|
assert search.json() == [
|
|
{
|
|
"listing_id": "lst_pdf",
|
|
"name": "PDF Export",
|
|
"summary": "Branded reports",
|
|
"category": "reporting",
|
|
"price": {"amount": 7, "currency": "credits", "model": "fixed"},
|
|
"seller": "m2bd",
|
|
"stats": {"installs": 3},
|
|
"score": 0.9,
|
|
}
|
|
]
|
|
|
|
listing = test_client.get("/api/listing/lst_pdf")
|
|
assert listing.status_code == 200
|
|
assert listing.json()["install_command"] == "m2-market install lst_pdf --yes"
|
|
assert listing.json()["permissions"] == ["filesystem:write"]
|
|
|
|
wallet = test_client.get("/api/wallet/m2bd")
|
|
assert wallet.status_code == 200
|
|
assert wallet.json()["balance"] == 42
|
|
assert wallet.json()["transactions"][0] == {
|
|
"ts": "2026-07-02T09:00:00Z",
|
|
"from": "mint",
|
|
"to": "m2bd",
|
|
"amount": 50,
|
|
"reason": "grant",
|
|
"ref": "starter",
|
|
}
|
|
|
|
economy = test_client.get("/api/economy")
|
|
assert economy.status_code == 200
|
|
assert economy.json()["operators"] == [
|
|
{"operator_id": "m2bd", "balance": 42},
|
|
{"operator_id": "platform", "balance": 8},
|
|
]
|
|
assert economy.json()["audit"] == {
|
|
"date": "2026-07-01",
|
|
"url": "https://git.example/m2/market-registry/raw/branch/main/audit/2026-07-01.json",
|
|
}
|
|
|
|
governance = test_client.get("/api/governance")
|
|
assert governance.status_code == 200
|
|
assert governance.json()["open_prs"][0]["labels"] == ["approved"]
|
|
assert governance.json()["listings"] == [
|
|
{
|
|
"listing_id": "lst_pdf",
|
|
"status": "published",
|
|
"url": "https://git.example/m2/market-registry/src/branch/main/listings/lst_pdf",
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("path", "failed_host", "panel"),
|
|
[
|
|
("/api/search?q=pdf", "memory.example", "catalog"),
|
|
("/api/wallet/m2bd", "ledger.example", "wallet"),
|
|
("/api/economy", "ledger.example", "economy"),
|
|
("/api/governance", "git.example", "governance"),
|
|
],
|
|
)
|
|
def test_upstream_failures_are_panel_502(
|
|
client: Callable[[httpx.MockTransport], TestClient],
|
|
path: str,
|
|
failed_host: str,
|
|
panel: str,
|
|
) -> None:
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
if request.url.host == failed_host:
|
|
return httpx.Response(503, json={"secret": "upstream body ignored"})
|
|
return _ok_transport(request)
|
|
|
|
test_client = client(httpx.MockTransport(handler))
|
|
_authed(test_client)
|
|
response = test_client.get(path)
|
|
assert response.status_code == 502
|
|
assert response.json() == {"error": "upstream_failure", "panel": panel}
|
|
|
|
|
|
def test_secret_values_never_appear_in_response_bodies(
|
|
client: Callable[[httpx.MockTransport], TestClient],
|
|
) -> None:
|
|
test_client = client(httpx.MockTransport(_ok_transport))
|
|
responses = [
|
|
test_client.post("/api/login", json={"passcode": "fleet-passcode-secret"}),
|
|
]
|
|
responses.extend(
|
|
[
|
|
test_client.get("/health"),
|
|
test_client.get("/api/search?q=pdf"),
|
|
test_client.get("/api/listing/lst_pdf"),
|
|
test_client.get("/api/wallet/m2bd"),
|
|
test_client.get("/api/economy"),
|
|
test_client.get("/api/governance"),
|
|
]
|
|
)
|
|
for response in responses:
|
|
body = response.text
|
|
for secret in SECRET_VALUES:
|
|
assert secret not in body
|