gitea-saas-core/tests/test_app.py

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