164 lines
5.7 KiB
Python
164 lines
5.7 KiB
Python
"""Tests for the assembled app (issue #38): one ``create_app()`` + entrypoints.
|
|
|
|
Hermetic: temp SQLite DBs, Flask test client, no network / Stripe / Gitea.
|
|
|
|
Acceptance contract exercised here:
|
|
1. ``create_app()``'s URL map contains the billing routes, the
|
|
``/api/factory/*`` routes, ``/dashboard`` and ``/healthz`` (asserted
|
|
on the URL map, not by requesting each one).
|
|
2. Starting the app with NO ``SAAS_API_KEY`` still starts, and every
|
|
authed route returns 503 -- not 500 and not 200; the liveness routes
|
|
stay public.
|
|
3. ``python -m gitea_saas`` serves ``/health`` locally (verified live as
|
|
part of the PR's test evidence; not a subprocess test here).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from gitea_saas import app as app_module
|
|
from gitea_saas.app import create_app
|
|
from gitea_saas.auth import API_KEY_HEADER, UNAUTHORIZED_BODY
|
|
from gitea_saas.config import ServerConfig
|
|
|
|
API_KEY = "test-api-key-secret"
|
|
AUTH = {API_KEY_HEADER: API_KEY}
|
|
|
|
|
|
def _cfg(tmp_path: Path, api_key: str | None = API_KEY) -> ServerConfig:
|
|
return ServerConfig(api_key=api_key, port=8080, db_path=tmp_path / "saas.db")
|
|
|
|
|
|
@pytest.fixture()
|
|
def app(tmp_path: Path):
|
|
application = create_app(_cfg(tmp_path))
|
|
application.config.update(TESTING=True)
|
|
return application
|
|
|
|
|
|
@pytest.fixture()
|
|
def client(app):
|
|
return app.test_client()
|
|
|
|
|
|
def _rules(app) -> set[str]:
|
|
return {str(r.rule) for r in app.url_map.iter_rules()}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Acceptance 1: the URL map composes every front-end
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_url_map_contains_billing_routes(app):
|
|
rules = _rules(app)
|
|
for expected in (
|
|
"/api/clients",
|
|
"/api/clients/<int:client_id>",
|
|
"/api/clients/<int:client_id>/credits",
|
|
"/api/clients/<int:client_id>/debits",
|
|
"/api/clients/<int:client_id>/holds",
|
|
"/api/holds/<int:hold_id>/release",
|
|
"/api/clients/<int:client_id>/ledger",
|
|
):
|
|
assert expected in rules, f"missing billing route {expected}"
|
|
|
|
|
|
def test_url_map_contains_factory_routes(app):
|
|
rules = _rules(app)
|
|
for expected in (
|
|
"/api/factory/health",
|
|
"/api/factory/provision",
|
|
"/api/factory/<int:client_id>/issues",
|
|
"/api/factory/<int:client_id>/unlock",
|
|
"/api/factory/<int:client_id>/lock",
|
|
):
|
|
assert expected in rules, f"missing factory route {expected}"
|
|
|
|
|
|
def test_url_map_contains_dashboard_and_liveness_routes(app):
|
|
rules = _rules(app)
|
|
for expected in ("/dashboard", "/api/dashboard/summary",
|
|
"/healthz", "/health"):
|
|
assert expected in rules, f"missing route {expected}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Acceptance 2: no SAAS_API_KEY -> app starts, authed routes fail closed 503
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_unset_api_key_fails_closed_503(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("SAAS_API_KEY", raising=False)
|
|
application = create_app(_cfg(tmp_path, api_key=None))
|
|
application.config.update(TESTING=True)
|
|
c = application.test_client()
|
|
|
|
# authed routes: 503 -- not 500, not 200
|
|
assert c.get("/api/clients/1").status_code == 503
|
|
assert c.post("/api/clients", json={"name": "X"}).status_code == 503
|
|
assert c.get("/dashboard").status_code == 503
|
|
assert c.get("/api/dashboard/summary").status_code == 503
|
|
assert c.post("/api/factory/provision",
|
|
json={"client_id": 1}).status_code == 503
|
|
|
|
# liveness routes in auth.PUBLIC_PATHS stay reachable without a key
|
|
for path in ("/health", "/healthz", "/api/factory/health"):
|
|
assert c.get(path).status_code == 200, path
|
|
|
|
|
|
def test_default_create_app_with_no_env_starts(monkeypatch):
|
|
monkeypatch.delenv("SAAS_API_KEY", raising=False)
|
|
monkeypatch.delenv("SAAS_DB_PATH", raising=False)
|
|
application = create_app() # nothing set: config loader defaults
|
|
application.config.update(TESTING=True)
|
|
c = application.test_client()
|
|
|
|
assert c.get("/health").status_code == 200
|
|
assert c.get("/healthz").status_code == 200
|
|
assert c.get("/api/clients/1").status_code == 503
|
|
rules = _rules(application)
|
|
assert "/dashboard" in rules
|
|
assert "/api/factory/provision" in rules
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configured key: the front-ends' own auth.key_is_valid guards are untouched
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_configured_key_serves_billing(client):
|
|
created = client.post("/api/clients", json={"name": "Assembled"},
|
|
headers=AUTH)
|
|
assert created.status_code == 201
|
|
cid = created.get_json()["id"]
|
|
resp = client.get(f"/api/clients/{cid}", headers=AUTH)
|
|
assert resp.status_code == 200
|
|
assert resp.get_json()["name"] == "Assembled"
|
|
|
|
|
|
def test_wrong_key_is_canonical_401(client):
|
|
resp = client.post("/api/clients", json={"name": "X"},
|
|
headers={API_KEY_HEADER: "wrong"})
|
|
assert resp.status_code == 401
|
|
assert resp.get_json() == UNAUTHORIZED_BODY
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entrypoints exist and are wired to the same main()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_console_entrypoint_target_exists():
|
|
# [project.scripts] gitea-saas = "gitea_saas.app:main"
|
|
assert callable(app_module.main)
|
|
|
|
|
|
def test_python_m_gitea_saas_uses_app_main():
|
|
from gitea_saas import __main__ as main_module
|
|
|
|
assert main_module.main is app_module.main
|