Test suite for the realm architecture
- conftest: bootstrap seeds a localhost realm Config; realm_registry fixture builds the runtime registry; avatar storage redirected to a per-test tmp dir; credentials/sessions stamped with the test realm. - test_cli rewritten for the init/serve split, incl. legacy adoption. - TestServerConfig replaced by TestRealms covering the realm CRUD API, cross-realm validation, delete guards and effective-auth-host fallback. - Avatar/OIDC tests updated for per-realm providers and realm-derived URLs; obsolete PASKIA_DB path tests removed.
This commit is contained in:
+164
-98
@@ -1,4 +1,9 @@
|
||||
"""Tests for the CLI entry point in paskia/__main__.py."""
|
||||
"""Tests for the CLI entry point in paskia/__main__.py.
|
||||
|
||||
The CLI is split into ``paskia init`` (create the combined paskia.kantadb
|
||||
with the initial realm(s)) and bare ``paskia`` (serve the stored realms,
|
||||
adopting a lone legacy ``<rp-id>.paskiadb`` database if present).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -6,83 +11,88 @@ import asyncio
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import msgspec
|
||||
import pytest
|
||||
from kanta import Kanta
|
||||
|
||||
from paskia.__main__ import main
|
||||
from paskia.db.structs import DB, Config
|
||||
from paskia.util.runtime import clear_config_cache
|
||||
from paskia.util.runtime import config as runtime_config
|
||||
from paskia.__main__ import _load_stored_config, main
|
||||
from paskia.db import legacy
|
||||
from paskia.db.structs import Config
|
||||
from paskia.util.runtime import ServeConfig, clear_cache
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cli_run(monkeypatch):
|
||||
"""Run the CLI main() with the given args and return the RuntimeConfig."""
|
||||
def run_cli(monkeypatch, tmp_path):
|
||||
"""Run the CLI main() in a temporary working directory.
|
||||
|
||||
def _run(*args: str, db_root: str | None = None) -> Any:
|
||||
env = os.environ.copy()
|
||||
if db_root is not None:
|
||||
env["PASKIA_DB"] = db_root
|
||||
monkeypatch.setattr(os, "environ", env)
|
||||
Returns a callable; server.run and the startup box are stubbed out.
|
||||
The returned dict records the server.run invocation (if any).
|
||||
"""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
calls: dict = {}
|
||||
monkeypatch.setattr(
|
||||
"fastapi_vue.server.run",
|
||||
lambda app, **kw: calls.update({"app": app, **kw}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"paskia.util.startupbox.print_startup_config", lambda *a, **kw: None
|
||||
)
|
||||
monkeypatch.setattr("logging.basicConfig", lambda **kw: None)
|
||||
# Isolate environment mutations (PASKIA_CONFIG) from other tests
|
||||
env = os.environ.copy()
|
||||
env.pop("PASKIA_CONFIG", None)
|
||||
env.pop("PASKIA_VITE_URL", None)
|
||||
monkeypatch.setattr(os, "environ", env)
|
||||
|
||||
def _run(*args: str) -> dict:
|
||||
monkeypatch.setattr(sys, "argv", ["paskia", *args])
|
||||
monkeypatch.setattr("fastapi_vue.server.run", lambda *_args, **_kw: None)
|
||||
monkeypatch.setattr(
|
||||
"paskia.util.startupbox.print_startup_config", lambda _rt: None
|
||||
)
|
||||
monkeypatch.setattr("logging.basicConfig", lambda **_kw: None)
|
||||
|
||||
clear_config_cache()
|
||||
main()
|
||||
runtime = runtime_config()
|
||||
clear_config_cache()
|
||||
return runtime
|
||||
clear_cache()
|
||||
try:
|
||||
main()
|
||||
finally:
|
||||
clear_cache()
|
||||
return calls
|
||||
|
||||
return _run
|
||||
|
||||
|
||||
async def _write_config(db_path: Path, config: Config) -> None:
|
||||
"""Write a Config into a JSONL database file using Kanta.
|
||||
|
||||
The initial root uses a different rp_id so the stored diff includes the
|
||||
target rp_id (required because Config omits defaults when diffing).
|
||||
"""
|
||||
kanta = Kanta(
|
||||
str(db_path),
|
||||
DB(config=Config(rp_id="uninitialized.invalid")),
|
||||
migrations="paskia.db.migrations",
|
||||
)
|
||||
kanta.ctx.rp_id = config.rp_id
|
||||
await kanta.open()
|
||||
with kanta.transaction("test:write_config"):
|
||||
kanta.data.config = config
|
||||
await kanta.close()
|
||||
def stored_config(tmp_path: Path) -> Config:
|
||||
"""Read back the stored combined configuration."""
|
||||
return _load_stored_config(tmp_path / "paskia.kantadb")
|
||||
|
||||
|
||||
def write_config(db_path: Path, config: Config) -> None:
|
||||
"""Synchronous wrapper for _write_config."""
|
||||
asyncio.run(_write_config(db_path, config))
|
||||
def write_legacy_db(root: Path, config: legacy.LegacyConfig) -> Path:
|
||||
"""Create a legacy-format database directory <rp-id>.paskiadb/main.db."""
|
||||
src_dir = root / f"{config.rp_id}.paskiadb"
|
||||
src_dir.mkdir()
|
||||
db_file = src_dir / "main.db"
|
||||
|
||||
async def _write() -> None:
|
||||
kanta = Kanta(str(db_file), legacy.LegacyDB())
|
||||
await kanta.open()
|
||||
with kanta.transaction("test:seed"):
|
||||
kanta.data.config = config
|
||||
await kanta.close()
|
||||
|
||||
asyncio.run(_write())
|
||||
return src_dir
|
||||
|
||||
|
||||
def test_cli_defaults(cli_run):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
runtime = cli_run("--rp-id", "localhost", db_root=tmp)
|
||||
def test_init_defaults(run_cli, tmp_path):
|
||||
run_cli("init")
|
||||
|
||||
assert runtime.config.rp_id == "localhost"
|
||||
assert runtime.config.rp_name is None
|
||||
assert runtime.config.auth_host is None
|
||||
assert runtime.config.origins is None
|
||||
assert runtime.site_url == "http://localhost:4401"
|
||||
assert runtime.site_path == "/auth/"
|
||||
assert runtime.save is False
|
||||
config = stored_config(tmp_path)
|
||||
assert [r.rp_id for r in config.realms] == ["localhost"]
|
||||
assert config.realms[0].rp_name is None
|
||||
assert config.realms[0].auth_host is None
|
||||
assert config.listen is None
|
||||
|
||||
|
||||
def test_cli_explicit_options(cli_run):
|
||||
runtime = cli_run(
|
||||
def test_init_full_options(run_cli, tmp_path):
|
||||
run_cli(
|
||||
"init",
|
||||
"--rp-id",
|
||||
"example.com",
|
||||
"--rp-name",
|
||||
@@ -91,56 +101,101 @@ def test_cli_explicit_options(cli_run):
|
||||
"auth.example.com",
|
||||
"--origin",
|
||||
"https://app.example.com",
|
||||
"--listen",
|
||||
"4402",
|
||||
)
|
||||
|
||||
assert runtime.config.rp_id == "example.com"
|
||||
assert runtime.config.rp_name == "Example Corp"
|
||||
assert runtime.config.auth_host == "https://auth.example.com"
|
||||
assert runtime.config.origins == [
|
||||
"https://auth.example.com",
|
||||
"https://app.example.com",
|
||||
]
|
||||
assert runtime.site_url == "https://auth.example.com"
|
||||
assert runtime.site_path == "/"
|
||||
config = stored_config(tmp_path)
|
||||
realm = config.realms[0]
|
||||
assert realm.rp_id == "example.com"
|
||||
assert realm.rp_name == "Example Corp"
|
||||
assert realm.auth_host == "https://auth.example.com"
|
||||
assert realm.origins == ["https://auth.example.com", "https://app.example.com"]
|
||||
assert config.listen == ["4402"]
|
||||
|
||||
|
||||
def test_cli_loads_stored_config(cli_run):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
db_path = Path(tmp) / "main.db"
|
||||
write_config(
|
||||
db_path,
|
||||
Config(
|
||||
rp_id="example.com",
|
||||
rp_name="Stored Name",
|
||||
origins=["https://stored.example.com"],
|
||||
),
|
||||
)
|
||||
runtime = cli_run("--rp-id", "example.com", db_root=tmp)
|
||||
def test_init_multiple_rp_ids(run_cli, tmp_path):
|
||||
run_cli("init", "--rp-id", "company.com,app.com", "--rp-id", "pro.com")
|
||||
|
||||
assert runtime.config.rp_name == "Stored Name"
|
||||
assert runtime.config.origins == ["https://stored.example.com"]
|
||||
assert runtime.site_url == "https://stored.example.com"
|
||||
config = stored_config(tmp_path)
|
||||
assert [r.rp_id for r in config.realms] == ["company.com", "app.com", "pro.com"]
|
||||
assert config.default_realm.rp_id == "company.com"
|
||||
|
||||
|
||||
def test_cli_overrides_stored_config(cli_run):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
db_path = Path(tmp) / "main.db"
|
||||
write_config(db_path, Config(rp_id="example.com", rp_name="Stored Name"))
|
||||
runtime = cli_run(
|
||||
"--rp-id", "example.com", "--rp-name", "Overridden", db_root=tmp
|
||||
)
|
||||
|
||||
assert runtime.config.rp_name == "Overridden"
|
||||
|
||||
|
||||
def test_cli_save_flag(cli_run):
|
||||
runtime = cli_run("--save")
|
||||
assert runtime.save is True
|
||||
|
||||
|
||||
def test_cli_invalid_auth_host(cli_run):
|
||||
def test_init_refuses_existing_database(run_cli):
|
||||
run_cli("init")
|
||||
with pytest.raises(SystemExit):
|
||||
cli_run("--rp-id", "example.com", "--auth-host", "notsub.example.org")
|
||||
run_cli("init")
|
||||
|
||||
|
||||
def test_init_refuses_legacy_database(run_cli, tmp_path):
|
||||
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="example.com"))
|
||||
with pytest.raises(SystemExit):
|
||||
run_cli("init")
|
||||
|
||||
|
||||
def test_init_invalid_auth_host(run_cli):
|
||||
with pytest.raises(SystemExit):
|
||||
run_cli("init", "--rp-id", "example.com", "--auth-host", "notsub.example.org")
|
||||
|
||||
|
||||
def test_serve_requires_database(run_cli):
|
||||
with pytest.raises(SystemExit, match="paskia init"):
|
||||
run_cli()
|
||||
|
||||
|
||||
def test_serve_uses_stored_config(run_cli, tmp_path):
|
||||
run_cli("init", "--rp-id", "example.com", "--rp-name", "Stored Name")
|
||||
calls = run_cli()
|
||||
|
||||
assert calls["app"] == "paskia.fastapi.mainapp:app"
|
||||
assert calls["listen"] is None # stored listen (None) used
|
||||
serve = msgspec.json.decode(os.environ["PASKIA_CONFIG"].encode(), type=ServeConfig)
|
||||
assert serve.listen is None
|
||||
|
||||
|
||||
def test_serve_listen_override_not_persisted(run_cli, tmp_path):
|
||||
run_cli("init", "--listen", "4402")
|
||||
calls = run_cli("--listen", "4403")
|
||||
|
||||
assert calls["listen"] == ["4403"]
|
||||
serve = msgspec.json.decode(os.environ["PASKIA_CONFIG"].encode(), type=ServeConfig)
|
||||
assert serve.listen == ["4403"]
|
||||
# Stored config keeps the original listen value
|
||||
assert stored_config(tmp_path).listen == ["4402"]
|
||||
|
||||
|
||||
def test_serve_adopts_legacy_database(run_cli, tmp_path):
|
||||
src_dir = write_legacy_db(
|
||||
tmp_path, legacy.LegacyConfig(rp_id="example.com", rp_name="Legacy Name")
|
||||
)
|
||||
# Persisted user files move to the new data root
|
||||
avatar = src_dir / "users" / "019c6831-84cf-7b88-b66c-c8165890b7c5"
|
||||
avatar.mkdir(parents=True)
|
||||
(avatar / "profile.webp").write_bytes(b"RIFF1234WEBP")
|
||||
|
||||
run_cli()
|
||||
|
||||
config = stored_config(tmp_path)
|
||||
assert [r.rp_id for r in config.realms] == ["example.com"]
|
||||
assert config.realms[0].rp_name == "Legacy Name"
|
||||
# Legacy directory renamed aside, user files adopted
|
||||
assert not src_dir.exists()
|
||||
assert (tmp_path / "example.com.paskiadb.converted-bak").is_dir()
|
||||
assert (
|
||||
tmp_path
|
||||
/ "paskia.data"
|
||||
/ "users"
|
||||
/ "019c6831-84cf-7b88-b66c-c8165890b7c5"
|
||||
/ "profile.webp"
|
||||
).read_bytes() == b"RIFF1234WEBP"
|
||||
|
||||
|
||||
def test_serve_multiple_legacy_databases_abort(run_cli, tmp_path):
|
||||
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="one.com"))
|
||||
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="two.com"))
|
||||
with pytest.raises(SystemExit, match="Multiple legacy"):
|
||||
run_cli()
|
||||
|
||||
|
||||
def test_cli_help():
|
||||
@@ -152,3 +207,14 @@ def test_cli_help():
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "Paskia authentication server" in result.stdout
|
||||
|
||||
|
||||
def test_cli_init_help():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "paskia", "init", "--help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "Bootstrap" in result.stdout
|
||||
|
||||
Reference in New Issue
Block a user