QA plan: SPA API key intake and badge (#63)
Story: SPA: API key intake + always-visible badge (RAG009) (#63)
Capability: Web UI (#16)
Spec: specs/ui.md
ADR: RAG009 — key stored in localStorage, badge always visible
Test scope
Section titled “Test scope”| Type | Coverage |
|---|---|
| Unit (Vitest + RTL) | First-load with no key in localStorage: key intake prompt visible, question textarea disabled |
| Unit | Saving a key via the intake input stores it under the stable localStorage key name |
| Unit | After key is saved, question textarea is enabled; intake prompt is dismissed |
| Unit | Badge renders masked key (rks_a1b2…1234 format), remaining requests count, and expiry date |
| Unit | Badge is present in the DOM at all times (not inside a collapsible panel or modal) |
| Unit | Badge reflects values returned by the /query response or a separate badge-refresh call |
| Unit | Key in localStorage is attached as Authorization: Bearer on every /query POST |
| Security | localStorage key name is stable and documented; no other key names used |
| Security | Badge masks all but first 8 and last 4 chars of the key; full key never appears in visible text |
| e2e (Playwright) | First load: intake prompt appears; paste key; textarea enables; question submits with bearer token |
| e2e | Badge is visible after key entry; content shows remaining count and expiry |
Entry criteria
Section titled “Entry criteria”- Story #17 (React chat UI) scaffolding in place
- API key format
rks_<key_id>_<secret>documented
Exit criteria
Section titled “Exit criteria”- All unit tests pass
- First-load flow: textarea disabled until key saved (Vitest assertion)
- Badge always-visible:
document.querySelector('[data-testid="key-badge"]')is in the document at all times after key entry (never inside adisplay:noneparent) - XSS safety: badge renders the masked key as text, not as
innerHTMLwith the raw key value - TypeScript:
tsc --noEmitexits 0
Environments
Section titled “Environments”Unit: Vitest headless. e2e: Playwright against QA env. localStorage is reset between test cases.
Tooling
Section titled “Tooling”Vitest, React Testing Library, Playwright. localStorage manipulation via vi.stubGlobal or RTL’s localStorage API in unit tests.
Fixtures and data
Section titled “Fixtures and data”TEST_KEY = "rks_a1b2c3d4_AAABBBCCCDDDEEEFFFGGGHHHIIIJJJ00"(synthetic; matches format)localStorageseeded with and without the test key for the two initial-state tests
How tests are written
Section titled “How tests are written”test("question textarea is disabled without a stored key", () => { // Given localStorage has no API key localStorage.clear(); render(<App />); // When the component mounts // Then the question textarea is disabled expect(screen.getByRole("textbox", { name: /question/i })).toBeDisabled();});test("badge is always visible after key entry", async () => { // Given a key is stored in localStorage localStorage.setItem("rag_api_key", TEST_KEY); render(<App />); // When no interaction has occurred // Then the badge is in the document and not hidden const badge = screen.getByTestId("key-badge"); expect(badge).toBeVisible();});localStorageavailability: JSDOM supports it in Vitest but behavior differs from a real browser for quota errors. Keep key values short; no quota concern at this scale.- Badge masking logic: off-by-one on the mask boundary (
rks_a1b2…1234) could expose more of the key than intended. Add a unit test that verifies the masked output for a known input. - RAG009 notes XSS risk with
localStorage. Key is treated as public given the demo scale and per-key spend cap. Document this explicitly in the badge component.
Capability acceptance criteria link
Section titled “Capability acceptance criteria link”Spec: first-load with no key shows intake prompt and disables textarea; badge always visible with masked key, remaining requests, expiry. Both are direct exit criteria above.
© 2026 Benjamin Arunski