Single consolidated commit for four completed, validated phases (77, 78, 79, 80). The pipeline run left all work uncommitted because the harness commits only with PHASE_COMMIT=1 while child executors are forbidden from committing; the phases themselves all passed validation and moved to .agents/phases/complete/. Phase 77 — navbar view refresh - router.js dispatches bor:view-refresh on re-show / active re-click / popstate (gated on wasMounted; first show and boot exempt) - History / RAG / Sources / Tuning re-fetch on refresh (admin branch); Chat deliberately excluded (stream survival) - History "Refresh" button (admin-only, in-flight disable + status line) - New story suite tests/e2e/test_navbar_refresh.py (7 tests) Phase 78 — static background - Removed the animated glow layers; static 44px grid over the flat --bg canvas; default and reduced-motion renders byte-identical - Updated background/theme E2E suites; removed bg-glow test pins Phase 79 — API tokens - api_tokens model + migration 0012; hash-only token service - Admin tokens API + Tokens admin view; POST /api/token-auth; live-revoking require_user on chat / suggestions / document content - Frontend token gate with localStorage cache; anonymous E2E suites migrated to token login - New story suite tests/e2e/test_api_tokens.py (9 tests) Phase 80 — history suggestion chips - last_questions() endpoint with SEED fallback; startNewChat() refetch - Seed-semantics docs (config.py, .env.example, README) - Integration state matrix + E2E suite rewritten to the 4 chip states Also included: phase-76 report artifacts and the repo restore-test-db skill (previously untracked), scripts/* ruff fixes from phase 77. Final gate state (phase 80 final pass, covers everything above): - uv run pytest --cov=app → 1637 passed, 0 failed, app/ coverage 99% - uv run ruff check . && uv run pyright → clean, 0 errors - Per-phase story E2E suites green in isolation
95 lines
3.6 KiB
Python
95 lines
3.6 KiB
Python
"""Unit tests: the ApiToken model registers the api_tokens contract
|
|
(phase 79, task 01).
|
|
|
|
Schema-level assertions without a live DB (the
|
|
``tests/unit/test_models.py`` doc_drafts precedent): the table name, the
|
|
column set + nullability, the UNIQUE ``token_hash`` (two tokens with the
|
|
same hash must collide — the unique constraint backing
|
|
``ix_api_tokens_token_hash``), and a display-only, non-unique
|
|
``label``. The real duplicate-rejection behaviour is pinned against the
|
|
dev DB in ``tests/integration/test_migration_0012.py``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from sqlalchemy import PrimaryKeyConstraint, String, UniqueConstraint
|
|
from sqlalchemy.schema import Table
|
|
|
|
import app.models # noqa: F401 (import registers all tables on Base.metadata)
|
|
from app.db import Base
|
|
|
|
|
|
def _table() -> Table:
|
|
return Base.metadata.tables["api_tokens"]
|
|
|
|
|
|
def test_api_tokens_table_registered() -> None:
|
|
assert "api_tokens" in Base.metadata.tables, "ApiToken must register api_tokens"
|
|
assert _table().name == "api_tokens"
|
|
|
|
|
|
def test_api_tokens_column_contract() -> None:
|
|
"""The column set + nullability: ``id`` UUID PK; ``label`` /
|
|
``token_hash`` NOT NULL; ``created_at`` NOT NULL with a server
|
|
default (now()); ``last_used_at`` / ``revoked_at`` NULL until the
|
|
service (tasks 02/03) sets them."""
|
|
tok = _table()
|
|
assert set(tok.c.keys()) == {
|
|
"id", "label", "token_hash", "created_at", "last_used_at", "revoked_at",
|
|
}
|
|
|
|
assert tok.c["id"].primary_key is True, "api_tokens.id must be the PK"
|
|
assert tok.c["id"].nullable is False, "api_tokens.id must be NOT NULL"
|
|
|
|
assert tok.c["label"].nullable is False, "label must be NOT NULL"
|
|
label_type = tok.c["label"].type
|
|
assert isinstance(label_type, String), "label must be String(120)"
|
|
assert label_type.length == 120, "label must be String(120)"
|
|
|
|
assert tok.c["token_hash"].nullable is False, "token_hash must be NOT NULL"
|
|
hash_type = tok.c["token_hash"].type
|
|
assert isinstance(hash_type, String), "token_hash must be String(64)"
|
|
assert hash_type.length == 64, (
|
|
"token_hash must be String(64) — a sha256 hex digest"
|
|
)
|
|
|
|
assert tok.c["created_at"].nullable is False, "created_at must be NOT NULL"
|
|
assert tok.c["created_at"].server_default is not None, (
|
|
"created_at needs a server default (now())"
|
|
)
|
|
|
|
for name in ("last_used_at", "revoked_at"):
|
|
assert tok.c[name].nullable is True, (
|
|
f"{name} must be NULL until first use / revocation"
|
|
)
|
|
|
|
|
|
def test_api_tokens_token_hash_is_unique() -> None:
|
|
"""Two tokens with the same hash must collide: a UNIQUE constraint
|
|
covers exactly ``token_hash`` (the backing constraint of the
|
|
``ix_api_tokens_token_hash`` unique index — the stored credential
|
|
is the lookup key)."""
|
|
tok = _table()
|
|
uq = [
|
|
c
|
|
for c in tok.constraints
|
|
if isinstance(c, UniqueConstraint)
|
|
and not isinstance(c, PrimaryKeyConstraint)
|
|
and {col.name for col in c.columns} == {"token_hash"}
|
|
]
|
|
assert uq, "api_tokens must be unique on (token_hash) — the stored credential"
|
|
|
|
|
|
def test_api_tokens_label_is_not_unique() -> None:
|
|
"""``label`` is the hand-out name — display-only: the column itself
|
|
is not unique and no unique constraint may cover it (two tokens can
|
|
share a label, e.g. two "alice" tokens issued at different times)."""
|
|
tok = _table()
|
|
assert not tok.c["label"].unique, "label must not be unique"
|
|
covering = [
|
|
c
|
|
for c in tok.constraints
|
|
if isinstance(c, UniqueConstraint)
|
|
and any(col.name == "label" for col in c.columns)
|
|
]
|
|
assert not covering, "no unique constraint may cover api_tokens.label"
|