ext: pairing-restart test + AMO permission justification
tests/transport/storage.test.ts covers the storage half of "the pairing token survives a browser restart" (round-trip, unpair-clears, corrupted override falls back to auto). websocket.test.ts adds the transport half: a fresh WebSocketTransport instance over the same backing store reuses the persisted token with no re-pairing, plus pairWithCode/unpair coverage. "Wrong token rejected and rate-limited" was already covered (websocket.test.ts's NotPaired/RateLimited cases). docs/amo-permissions.md is the submission-ready permission justification for AMO's Notes to Reviewer field, covering every permission in manifest.json plus what was deliberately not requested and how cookie/ header data is handled. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Ed8KEmAW48v4YHdxLtqsMB
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// browser.storage.local is backed by the real add-on profile on disk, so anything
|
||||
// written through transport/storage.ts is what "survives a browser restart" means in
|
||||
// practice — this covers the storage half of that DoD item; websocket.test.ts's
|
||||
// "survives a browser restart" case covers the transport half (a fresh transport
|
||||
// instance reusing a persisted token with no re-pairing).
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import * as storage from '../../src/background/transport/storage.js';
|
||||
|
||||
describe('transport/storage', () => {
|
||||
beforeEach(async () => {
|
||||
await browser.storage.local.clear();
|
||||
});
|
||||
|
||||
it('round-trips the pairing token', async () => {
|
||||
expect(await storage.getToken()).toBeNull();
|
||||
await storage.setToken('tok-abc');
|
||||
expect(await storage.getToken()).toBe('tok-abc');
|
||||
});
|
||||
|
||||
it('clears the token on unpair (null)', async () => {
|
||||
await storage.setToken('tok-abc');
|
||||
await storage.setToken(null);
|
||||
expect(await storage.getToken()).toBeNull();
|
||||
});
|
||||
|
||||
it('a fresh read after a simulated restart still sees the persisted token', async () => {
|
||||
await storage.setToken('tok-survives');
|
||||
// Nothing here recreates browser.storage.local — that's the point: it is the one
|
||||
// thing in the extension that outlives the background page's lifetime, restart
|
||||
// included. A second, independent read call stands in for "the page reloaded".
|
||||
expect(await storage.getToken()).toBe('tok-survives');
|
||||
expect(await storage.getToken()).toBe('tok-survives');
|
||||
});
|
||||
|
||||
it('round-trips the transport override, defaulting to auto', async () => {
|
||||
expect(await storage.getOverride()).toBe('auto');
|
||||
await storage.setOverride('uds');
|
||||
expect(await storage.getOverride()).toBe('uds');
|
||||
await storage.setOverride('ws');
|
||||
expect(await storage.getOverride()).toBe('ws');
|
||||
});
|
||||
|
||||
it('ignores a corrupted override value and falls back to auto', async () => {
|
||||
await browser.storage.local.set({ 'velox.transportOverride': 'not-a-transport' });
|
||||
expect(await storage.getOverride()).toBe('auto');
|
||||
});
|
||||
|
||||
it('round-trips the cached WebSocket port', async () => {
|
||||
expect(await storage.getCachedWsPort()).toBeNull();
|
||||
await storage.setCachedWsPort(52003);
|
||||
expect(await storage.getCachedWsPort()).toBe(52003);
|
||||
await storage.setCachedWsPort(null);
|
||||
expect(await storage.getCachedWsPort()).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -93,6 +93,59 @@ describe('WebSocketTransport', () => {
|
||||
expect(lastHello?.token).toBe('tok-issued-1');
|
||||
});
|
||||
|
||||
it('the pairing token survives a browser restart: a fresh transport instance over the same storage reuses it, no re-pairing', async () => {
|
||||
const port = nextPort();
|
||||
daemon = await FakeDaemon.start({ port, acceptToken: null });
|
||||
const h = memDeps(); // stands in for browser.storage.local, which outlives the page
|
||||
transport = makeTransport(port, h);
|
||||
|
||||
await transport.connect();
|
||||
expect(daemon.pairCount).toBe(1);
|
||||
expect(h.store.token).toBe('tok-issued-1');
|
||||
transport.disconnect();
|
||||
|
||||
// Simulate "the browser restarted": a brand new transport instance, same backing
|
||||
// store (in reality, the same on-disk profile), no in-memory state carried over.
|
||||
const restarted = makeTransport(port, h);
|
||||
await restarted.connect();
|
||||
try {
|
||||
expect(restarted.state).toBe('connected');
|
||||
expect(daemon.pairCount).toBe(1); // still just the one pairing, ever
|
||||
const lastHello = [...daemon.seen].reverse().find((s) => s.method === 'session.hello');
|
||||
expect(lastHello?.token).toBe('tok-issued-1');
|
||||
} finally {
|
||||
restarted.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
it('pairWithCode drops any stale token and pairs fresh with the typed code', async () => {
|
||||
const port = nextPort();
|
||||
daemon = await FakeDaemon.start({ port, acceptToken: 'stale' });
|
||||
const h = memDeps({ token: 'stale' });
|
||||
transport = makeTransport(port, h);
|
||||
|
||||
await transport.pairWithCode('4821');
|
||||
|
||||
expect(transport.state).toBe('connected');
|
||||
expect(daemon.pairCount).toBe(1);
|
||||
const pairCall = daemon.seen.find((s) => s.method === 'session.pair');
|
||||
expect((pairCall?.params as { code?: string }).code).toBe('4821');
|
||||
expect(h.store.token).toBe('tok-issued-1');
|
||||
});
|
||||
|
||||
it('unpair clears the stored token and disconnects', async () => {
|
||||
const port = nextPort();
|
||||
daemon = await FakeDaemon.start({ port, acceptToken: 'good-token' });
|
||||
const h = memDeps({ token: 'good-token' });
|
||||
transport = makeTransport(port, h);
|
||||
await transport.connect();
|
||||
|
||||
await transport.unpair();
|
||||
|
||||
expect(h.store.token).toBeNull();
|
||||
expect(transport.state).toBe('disconnected');
|
||||
});
|
||||
|
||||
it('with autoPair off, a wrong token surfaces needsPairing and does NOT retry', async () => {
|
||||
const port = nextPort();
|
||||
daemon = await FakeDaemon.start({ port, acceptToken: 'the-real-one' });
|
||||
|
||||
Reference in New Issue
Block a user