Files
praxis/tests/test_create_operator.py
T
Praxis CI e8a05adcd1 feat(P01): SLICE-05 operator bootstrap CLI + secrets scope
- TASK-05-01 scripts/create-operator.py: CLI that reads
  PRAXIS_BOOTSTRAP_OPERATOR_USER/PASS + PRAXIS_PG_DSN from env, creates
  the pool, applies migrations, hashes the password with argon2id, and
  INSERTs with ON CONFLICT DO NOTHING (idempotent — D-052). --update
  flag forces rehash + ON CONFLICT DO UPDATE. Missing env → exit 1
  (R-BOOT-02). Connection failure → 3x retry with 5s backoff (R-BOOT-01).
- TASK-05-02 .ciagent/config.json: added "operator" secrets scope
  (PRAXIS_PG_PASSWORD, PRAXIS_COOKIE_SECRET,
  PRAXIS_BOOTSTRAP_OPERATOR_USER/PASS, PRAXIS_VC_ISSUER_KEY).
  .ciagent/.env.secrets.example: template (committed, no real secrets).
  .gitignore: added negations so .env.secrets.example is tracked while
  .env.secrets stays ignored.
- TASK-05-03 tests/test_create_operator.py: 7 tests (mocked PgStore) —
  create, already-exists (no update), --update rehashes, missing env →
  exit 1, password is argon2id (not plaintext).

---ci---
project: praxis
phase: 1
milestone: v0.4
status: execute
persona: devops-engineer
task: 05-01,05-02,05-03
requirements:
  covered: [REQ-AUTH-01]
---/ci---
2026-08-04 00:56:41 +00:00

217 lines
6.9 KiB
Python

"""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$")