Files
brain-of-reese/tests/unit/test_tokens.py
T
ducoterra 7fce6572d0
Build and Push Containers / build-and-push-app (push) Successful in 1m45s
Build and Push Containers / build-and-push-db (push) Successful in 13s
feat: phases 77–80 — navbar view refresh, static background, API tokens, history suggestion chips
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
2026-09-07 12:39:01 -04:00

175 lines
6.2 KiB
Python

"""Unit: the API-token service (phase 79, task 02).
Covers ``app.core.tokens`` — the create/lookup/revoke service behind the
admin API (task 02's endpoints) and the future token-auth login (task
03):
* ``generate_token`` — the ``bor_`` + 32-hex shape, two calls differ;
* ``hash_token`` — deterministic 64-hex digest of the FULL string (a
stripped prefix can never collide);
* ``create_token`` / ``find_active_by_token`` — the round-trip (the
plaintext resolves to its active row) and the GENERIC-MISS contract:
revoked, unknown, empty, short and wrong-prefix candidates all return
the same ``None`` (hash matches no row — there is no "almost" path,
which is what makes the task-03 one-generic-401 safe);
* ``revoke`` — stamps ``revoked_at`` once (idempotent re-call keeps the
original stamp; False only for a missing id);
* ``mark_used`` — bumps ``last_used_at``.
House DB-test pattern (the ``test_sources_meta`` precedent): the service
is a thin session wrapper whose contract (server-default ``created_at``,
the unique hash index) only holds against a real database — runs against
the local compose Postgres, skips with clear instructions when the stack
is not up. The service flushes, never commits: the tests commit, as the
endpoints do.
"""
from __future__ import annotations
import re
import uuid
from collections.abc import Iterator
from datetime import UTC, datetime, timedelta
import pytest
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.core import tokens as tok
from app.models import ApiToken
TOKEN_SHAPE = re.compile(r"^bor_[0-9a-f]{32}$")
@pytest.fixture(autouse=True)
def clean_tokens(db: Session) -> Iterator[None]:
"""api_tokens is global state: reset around every test."""
db.execute(text("TRUNCATE api_tokens"))
db.commit()
yield
db.execute(text("TRUNCATE api_tokens"))
db.commit()
def _create_and_commit(db: Session, label: str = "alice") -> tuple[ApiToken, str]:
"""Service create + the endpoint's commit/refresh (the house split)."""
row, plaintext = tok.create_token(db, label)
db.commit()
db.refresh(row)
return row, plaintext
def test_generate_token_shape_and_uniqueness() -> None:
"""``bor_`` + exactly 32 lowercase hex chars (128-bit), no reuse."""
a = tok.generate_token()
b = tok.generate_token()
assert TOKEN_SHAPE.fullmatch(a), a
assert TOKEN_SHAPE.fullmatch(b), b
assert a != b
def test_hash_token_deterministic_64_hex_full_string() -> None:
"""Same token → same digest; 64 hex chars; the FULL string is hashed
(hashing ``bor_X`` ≠ hashing ``X`` — a stripped prefix can never
collide with another token's hash)."""
plain = tok.generate_token()
h1 = tok.hash_token(plain)
h2 = tok.hash_token(plain)
assert h1 == h2
assert re.fullmatch(r"[0-9a-f]{64}", h1), h1
assert tok.hash_token(plain.removeprefix("bor_")) != h1
def test_create_and_find_round_trip(db: Session) -> None:
"""create → the row carries ONLY the hash; the plaintext resolves
back to the same active row (the task-03 login path)."""
row, plaintext = _create_and_commit(db, " alice ")
# The service strips the label; the row only ever carries the hash.
assert row.label == "alice"
assert row.token_hash == tok.hash_token(plaintext)
assert row.token_hash != plaintext
assert row.last_used_at is None
assert row.revoked_at is None
hit = tok.find_active_by_token(db, plaintext)
assert hit is not None
assert hit.id == row.id
assert hit.label == "alice"
@pytest.mark.parametrize(
("candidate", "why"),
[
("", "empty string"),
("bor_ab", "short string"),
("bor_" + "f" * 31, "31 hex chars (one short)"),
(
"zzz_" + "0" * 32,
"wrong prefix",
),
],
ids=["empty", "short", "31-hex", "wrong-prefix"],
)
def test_find_active_by_token_generic_miss(db: Session, candidate: str, why: str) -> None:
"""Every malformed/unknown candidate is the SAME miss — the hash
simply matches no row (no "almost" path, no per-shape error)."""
_, plaintext = _create_and_commit(db) # the table is NOT empty
assert tok.find_active_by_token(db, candidate) is None, why
# …while the stored token still resolves (the miss is specific).
assert tok.find_active_by_token(db, plaintext) is not None
def test_find_active_by_token_unknown_well_formed(db: Session) -> None:
"""A well-formed token that was never stored (or belongs to another
admin) misses — no enumeration surface beyond the unique-index hit."""
unknown = tok.generate_token() # never persisted
assert tok.find_active_by_token(db, unknown) is None
def test_find_active_by_token_revoked_misses(db: Session) -> None:
"""A revoked row never resolves: the hash still matches the row, but
``revoked_at IS NULL`` is part of the contract (A4 — dead is dead,
enforced immediately)."""
row, plaintext = _create_and_commit(db)
assert tok.revoke(db, row.id) is True
db.commit()
assert tok.find_active_by_token(db, plaintext) is None
def test_revoke_stamps_once_and_false_only_for_missing(db: Session) -> None:
"""First revoke stamps ``revoked_at`` (True); the second call is
idempotent (True, the ORIGINAL stamp kept — no re-stamp); False only
when the row does not exist."""
row, _ = _create_and_commit(db)
assert tok.revoke(db, row.id) is True
db.commit()
db.refresh(row)
first_stamp = row.revoked_at
assert first_stamp is not None
# Idempotent: True again, and the first stamp survives.
assert tok.revoke(db, row.id) is True
db.commit()
db.refresh(row)
assert row.revoked_at == first_stamp
# A missing id is the only False.
assert tok.revoke(db, uuid.uuid4()) is False
def test_mark_used_stamps_last_used_at(db: Session) -> None:
"""NULL until first use, then "now" (UTC, tz-aware)."""
row, _ = _create_and_commit(db)
assert row.last_used_at is None
before = datetime.now(UTC)
tok.mark_used(row)
after = datetime.now(UTC)
assert row.last_used_at is not None
assert row.last_used_at.tzinfo is not None
assert before - timedelta(seconds=1) <= row.last_used_at <= after + timedelta(
seconds=1
)
db.commit()