"""Bootstrap CLI test (TASK-05-03) — mocked PgStore, no real Postgres. Covers: create operator → exists; re-run → "already exists" (no update); --update → password updated; missing env → exit 1; password is argon2id (not plaintext). """ from __future__ import annotations import importlib import os import sys from unittest.mock import AsyncMock, MagicMock import pytest def _load_cli_module(monkeypatch, env: dict, update: bool = False): """Load scripts/create-operator.py as a module with a mocked asyncpg pool.""" for k in ("PRAXIS_BOOTSTRAP_OPERATOR_USER", "PRAXIS_BOOTSTRAP_OPERATOR_PASS", "PRAXIS_PG_DSN"): monkeypatch.delenv(k, raising=False) for k, v in env.items(): if v is None: monkeypatch.delenv(k, raising=False) else: monkeypatch.setenv(k, v) # Import the script as a module by path. import importlib.util spec = importlib.util.spec_from_file_location( "create_operator", "scripts/create-operator.py" ) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def _make_mock_pool_store(operators: dict[str, dict] | None = None): operators = operators if operators is not None else {} pool = MagicMock() pool.close = AsyncMock() conn = MagicMock() async def acquire_ctx(): return conn cm = MagicMock() cm.__aenter__ = AsyncMock(return_value=conn) cm.__aexit__ = AsyncMock(return_value=None) pool.acquire = MagicMock(return_value=cm) store = MagicMock() async def insert_operator(username, password_hash, display_name, *, on_conflict_update=False): if on_conflict_update: operators[username] = { "id": "11111111-1111-1111-1111-111111111111", "username": username, "password_hash": password_hash, } return operators[username]["id"] if username in operators: return None # already exists operators[username] = { "id": "11111111-1111-1111-1111-111111111111", "username": username, "password_hash": password_hash, } return operators[username]["id"] store.insert_operator = insert_operator return pool, store, operators @pytest.mark.asyncio async def test_create_operator_creates(monkeypatch, capsys): env = { "PRAXIS_BOOTSTRAP_OPERATOR_USER": "admin", "PRAXIS_BOOTSTRAP_OPERATOR_PASS": "secret-pw", "PRAXIS_PG_DSN": "postgresql://praxis:x@localhost/praxis", } mod = _load_cli_module(monkeypatch, env) pool, store, operators = _make_mock_pool_store() import asyncpg async def fake_create_pool(**kw): return pool monkeypatch.setattr(asyncpg, "create_pool", fake_create_pool) from db.pg_migrate import apply_pg_migrations as _apm import db.pg_migrate async def fake_apply_migrations(p): return ["0001_operator_tier"] monkeypatch.setattr(db.pg_migrate, "apply_pg_migrations", fake_apply_migrations) import db.pg_store monkeypatch.setattr(db.pg_store, "PgStore", lambda p: store) rc = await mod.create_operator(update=False) assert rc == 0 out = capsys.readouterr().out assert "created" in out assert "admin" in operators h = operators["admin"]["password_hash"] assert h.startswith("$argon2id$") assert "secret-pw" not in h # not plaintext @pytest.mark.asyncio async def test_create_operator_already_exists(monkeypatch, capsys): env = { "PRAXIS_BOOTSTRAP_OPERATOR_USER": "admin", "PRAXIS_BOOTSTRAP_OPERATOR_PASS": "secret-pw", "PRAXIS_PG_DSN": "postgresql://praxis:x@localhost/praxis", } mod = _load_cli_module(monkeypatch, env) pool, store, operators = _make_mock_pool_store({"admin": { "id": "id1", "username": "admin", "password_hash": "$argon2id$old" }}) import asyncpg async def fake_create_pool(**kw): return pool monkeypatch.setattr(asyncpg, "create_pool", fake_create_pool) import db.pg_migrate async def fake_apply_migrations(p): return [] monkeypatch.setattr(db.pg_migrate, "apply_pg_migrations", fake_apply_migrations) import db.pg_store monkeypatch.setattr(db.pg_store, "PgStore", lambda p: store) rc = await mod.create_operator(update=False) assert rc == 0 out = capsys.readouterr().out assert "already exists" in out # password NOT updated assert operators["admin"]["password_hash"] == "$argon2id$old" @pytest.mark.asyncio async def test_create_operator_update_rehashes(monkeypatch, capsys): env = { "PRAXIS_BOOTSTRAP_OPERATOR_USER": "admin", "PRAXIS_BOOTSTRAP_OPERATOR_PASS": "new-pw", "PRAXIS_PG_DSN": "postgresql://praxis:x@localhost/praxis", } mod = _load_cli_module(monkeypatch, env) pool, store, operators = _make_mock_pool_store({"admin": { "id": "id1", "username": "admin", "password_hash": "$argon2id$old" }}) import asyncpg async def fake_create_pool(**kw): return pool monkeypatch.setattr(asyncpg, "create_pool", fake_create_pool) import db.pg_migrate async def fake_apply_migrations(p): return [] monkeypatch.setattr(db.pg_migrate, "apply_pg_migrations", fake_apply_migrations) import db.pg_store monkeypatch.setattr(db.pg_store, "PgStore", lambda p: store) rc = await mod.create_operator(update=True) assert rc == 0 out = capsys.readouterr().out assert "updated" in out assert operators["admin"]["password_hash"].startswith("$argon2id$") assert operators["admin"]["password_hash"] != "$argon2id$old" @pytest.mark.asyncio async def test_create_operator_missing_user_env(monkeypatch, capsys): env = { "PRAXIS_BOOTSTRAP_OPERATOR_PASS": "x", "PRAXIS_PG_DSN": "postgresql://praxis:x@localhost/praxis", } mod = _load_cli_module(monkeypatch, env) rc = await mod.create_operator(update=False) assert rc == 1 err = capsys.readouterr().err assert "PRAXIS_BOOTSTRAP_OPERATOR_USER" in err @pytest.mark.asyncio async def test_create_operator_missing_pass_env(monkeypatch, capsys): env = { "PRAXIS_BOOTSTRAP_OPERATOR_USER": "admin", "PRAXIS_PG_DSN": "postgresql://praxis:x@localhost/praxis", } mod = _load_cli_module(monkeypatch, env) rc = await mod.create_operator(update=False) assert rc == 1 @pytest.mark.asyncio async def test_create_operator_missing_dsn(monkeypatch, capsys): env = { "PRAXIS_BOOTSTRAP_OPERATOR_USER": "admin", "PRAXIS_BOOTSTRAP_OPERATOR_PASS": "x", } mod = _load_cli_module(monkeypatch, env) rc = await mod.create_operator(update=False) assert rc == 1 err = capsys.readouterr().err assert "PRAXIS_PG_DSN" in err def test_password_hash_is_argon2id(): from argon2 import PasswordHasher h = PasswordHasher().hash("test") assert h.startswith("$argon2id$")