Files
ersatztv/web/src/api/auth.test.ts
T
timothyandClaude Opus 4.8 ab6d31309f feat(spa): API key entry, send key on all requests, 401 pointer (#197)
Bundle A SPA slice: the /api surface is now gated behind X-Api-Key on
every request (reads too, RequireKeyForReads defaults true), so a wrong/
missing key 401s everything.

- #282: send X-Api-Key on ALL requests when a key is stored, not only
  mutations (removed the mutatingMethods split in api/client.ts).
- #280: new keyless API Key screen (/app/api-key, System nav) that reads/
  writes only localStorage via auth.ts and never calls /api, so it works
  on a fresh install where every read 401s. Masked key state, Save/Clear,
  points at server-generated /config/api.key.
- 401 UX: client emits one app-wide unauthorized signal (auth.ts
  notify/subscribeUnauthorized); a shell-level UnauthorizedBanner points
  the user at the API Key screen. DRY, no per-screen 401 branches.
- Tests: inverted the GET header assertion (key now sent on reads), added
  no-key and 401-signal client tests, auth signal tests, and screen +
  banner tests. spa-conventions.md §5e documents the new seams.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:01:55 +02:00

57 lines
1.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { clearStoredApiKey, getStoredApiKey, notifyUnauthorized, setStoredApiKey, subscribeUnauthorized } from './auth';
describe('API key storage', () => {
beforeEach(() => {
window.localStorage.clear();
});
it('stores and reads the API key from local storage', () => {
setStoredApiKey('write-secret');
expect(getStoredApiKey()).toBe('write-secret');
expect(window.localStorage.getItem('ctv-api-key')).toBe('write-secret');
});
it('trims blank API keys and clears storage', () => {
setStoredApiKey(' ');
expect(getStoredApiKey()).toBeNull();
expect(window.localStorage.getItem('ctv-api-key')).toBeNull();
});
it('clears a stored API key', () => {
setStoredApiKey('write-secret');
clearStoredApiKey();
expect(getStoredApiKey()).toBeNull();
});
it('ignores unavailable local storage when reading the API key', () => {
const localStorageGetter = vi
.spyOn(window, 'localStorage', 'get')
.mockImplementation(() => {
throw new Error('localStorage unavailable');
});
expect(getStoredApiKey()).toBeNull();
localStorageGetter.mockRestore();
});
});
describe('unauthorized signal', () => {
it('notifies subscribers and stops after unsubscribe', () => {
const listener = vi.fn();
const unsubscribe = subscribeUnauthorized(listener);
notifyUnauthorized();
expect(listener).toHaveBeenCalledTimes(1);
unsubscribe();
notifyUnauthorized();
expect(listener).toHaveBeenCalledTimes(1);
});
});