diff --git a/src/auth/session.test.ts b/src/auth/session.test.ts new file mode 100644 index 0000000..02d3529 --- /dev/null +++ b/src/auth/session.test.ts @@ -0,0 +1,78 @@ +import { strict as assert } from 'node:assert'; +import { test, beforeEach } from 'node:test'; +import { persist, restore, clear } from './session'; +import { SESSION_STORAGE_KEY } from './token'; +import type { SessionUser } from './roles'; + +/* +A session without a token is not a session. + +On 2026-09-25 a PUT to `users/update` came back 401 and the request carried no +`authorization` header at all. The console was signed in by every visible +measure — name in the corner, nav rendered, pages mounted — and could not make +one authenticated request, because `restore()` accepted a stored blob that had a +`userid` and a `role` and no token. + +Two ways in. A session stored before the token existed; or a login against a +server that could not mint one — `attachWebSession` logs that failure and +returns the user record anyway, which was right while WEB_AUTH_REQUIRED was off +and is a broken console now that it defaults on. + +Either way the state is the same and it is the worst one available: authorised +enough to render, not enough to load anything, and nothing on screen saying so. +*/ + +// A minimal sessionStorage, since node has none. +const store = new Map(); +(globalThis as { sessionStorage?: unknown }).sessionStorage = { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => void store.set(key, value), + removeItem: (key: string) => void store.delete(key), +}; + +const signedIn: SessionUser = { + userid: 904, + role: 'store-admin', + token: 'w1.payload.signature', +} as SessionUser; + +beforeEach(() => store.clear()); + +test('a stored session with a token comes back', () => { + persist(signedIn); + assert.equal(restore()?.userid, 904); + assert.equal(restore()?.token, 'w1.payload.signature'); +}); + +test('a session with no token is refused', () => { + // The bug. Restoring this renders a console that cannot load anything. + store.set(SESSION_STORAGE_KEY, JSON.stringify({ userid: 904, role: 'store-admin' })); + assert.equal(restore(), null, 'a tokenless session was restored'); +}); + +test('a session with an empty token is refused', () => { + // `token: ""` is what a server that failed to mint would produce if the field + // were assigned rather than spread. Same broken state, different shape. + store.set(SESSION_STORAGE_KEY, JSON.stringify({ userid: 904, role: 'store-admin', token: ' ' })); + assert.equal(restore(), null, 'an empty token was accepted'); +}); + +test('the checks that were already there still hold', () => { + // A corrupted blob must not crash the shell on boot. + for (const bad of [ + '{"role":"store-admin","token":"t"}', + '{"userid":904,"token":"t"}', + '{"userid":"904","role":"store-admin","token":"t"}', + 'not json at all', + '', + ]) { + store.set(SESSION_STORAGE_KEY, bad); + assert.equal(restore(), null, `restored from ${bad}`); + } +}); + +test('signing out leaves nothing behind', () => { + persist(signedIn); + clear(); + assert.equal(restore(), null); +}); diff --git a/src/auth/session.ts b/src/auth/session.ts index 1411523..408794a 100644 --- a/src/auth/session.ts +++ b/src/auth/session.ts @@ -82,10 +82,25 @@ export async function login(email: string, password: string): Promise