This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/__tests__/auth-resilience.test.js
Jeffrey Smith 3d4228f868
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-sqlite (push) Successful in 2m46s
CI/CD / test-go-pg (push) Successful in 2m47s
CI/CD / build-and-deploy (push) Successful in 26s
Feat v0.6.3 dead code sweep (#38)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-31 12:37:47 +00:00

322 lines
11 KiB
JavaScript

// ==========================================
// Auth Resilience Tests
// ==========================================
// Validates that auth failure during startup
// results in a redirect to login, NOT a broken
// main UI with 401 errors everywhere.
//
// Bug: Profile returned 200 (token on edge of
// expiry), startApp() proceeded, then every
// subsequent call 401'd. User saw broken UI
// instead of login screen.
//
// Run: node --test src/js/__tests__/auth-resilience.test.js
// ==========================================
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
// ── Auth state model ────────────────────────
describe('Auth state transitions', () => {
// Simulates the API client's auth lifecycle
function makeAuthState() {
return {
accessToken: null,
refreshToken: null,
get isAuthed() { return !!this.accessToken; },
clearTokens() { this.accessToken = null; this.refreshToken = null; },
setTokens(access, refresh) { this.accessToken = access; this.refreshToken = refresh; },
};
}
it('isAuthed reflects token presence', () => {
const auth = makeAuthState();
assert.equal(auth.isAuthed, false);
auth.setTokens('tok_access', 'tok_refresh');
assert.equal(auth.isAuthed, true);
});
it('clearTokens makes isAuthed false', () => {
const auth = makeAuthState();
auth.setTokens('tok_access', 'tok_refresh');
assert.equal(auth.isAuthed, true);
auth.clearTokens();
assert.equal(auth.isAuthed, false);
});
it('expired token string is still truthy (the edge case)', () => {
const auth = makeAuthState();
// An expired JWT is still a non-empty string — isAuthed returns true
// This is BY DESIGN: the server validates, not the client
const expiredJWT = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjB9.fake';
auth.setTokens(expiredJWT, 'tok_refresh');
assert.equal(auth.isAuthed, true, 'expired JWT is still truthy');
});
});
// ── Startup auth guard ──────────────────────
describe('startApp auth guard', () => {
// This models the critical startup sequence:
// init():
// if (isAuthed) → getProfile() → if ok → startApp()
// startApp():
// loadSettings() → loadChats() → fetchModels()
// *** AUTH GUARD CHECK HERE ***
// if (!isAuthed) → showSplash, return
// else → render UI
it('proceeds when auth is valid throughout startup', () => {
let splashShown = false;
let uiRendered = false;
const auth = { isAuthed: true };
// Simulate: loadChats succeeds, auth stays valid
const loadChats = () => { /* no 401 */ };
const fetchModels = () => { /* no 401 */ };
// startApp logic
loadChats();
fetchModels();
if (!auth.isAuthed) {
splashShown = true;
} else {
uiRendered = true;
}
assert.equal(uiRendered, true, 'UI should render when auth is valid');
assert.equal(splashShown, false);
});
it('returns to login when auth is lost during startup', () => {
let splashShown = false;
let uiRendered = false;
const auth = {
accessToken: 'tok',
get isAuthed() { return !!this.accessToken; },
clearTokens() { this.accessToken = null; },
};
// Simulate: loadChats gets 401, refresh fails, clearTokens called
const loadChats = () => {
// _authed → 401 → refresh() → fails → clearTokens()
auth.clearTokens();
};
const fetchModels = () => { /* also 401 but tokens already cleared */ };
loadChats();
fetchModels();
// THE FIX: guard check after critical loads
if (!auth.isAuthed) {
splashShown = true;
} else {
uiRendered = true;
}
assert.equal(splashShown, true, 'must return to login when auth lost');
assert.equal(uiRendered, false, 'must NOT render UI when auth lost');
});
it('detects auth loss even when only one call triggers it', () => {
const auth = {
accessToken: 'tok',
get isAuthed() { return !!this.accessToken; },
clearTokens() { this.accessToken = null; },
};
let splashShown = false;
// loadSettings succeeds (token barely valid)
// loadChats triggers 401 → clearTokens
const loadSettings = () => { /* ok */ };
const loadChats = () => { auth.clearTokens(); };
const fetchModels = () => { /* tokens already gone, silently fails */ };
loadSettings();
loadChats();
fetchModels();
if (!auth.isAuthed) splashShown = true;
assert.equal(splashShown, true, 'single 401 during startup must trigger login redirect');
});
});
// ── 401 retry + refresh behavior ─────────────
describe('401 retry with refresh', () => {
it('successful refresh preserves auth', () => {
const auth = {
accessToken: 'expired',
refreshToken: 'valid_refresh',
get isAuthed() { return !!this.accessToken; },
clearTokens() { this.accessToken = null; this.refreshToken = null; },
refresh() {
// Simulate successful refresh
this.accessToken = 'new_valid_token';
return true;
},
};
// _authed: first call 401, refresh succeeds, retry succeeds
const result401 = { status: 401 };
if (result401.status === 401 && auth.refreshToken) {
auth.refresh();
}
assert.equal(auth.isAuthed, true, 'auth preserved after successful refresh');
assert.equal(auth.accessToken, 'new_valid_token');
});
it('failed refresh clears auth', () => {
const auth = {
accessToken: 'expired',
refreshToken: 'also_expired',
get isAuthed() { return !!this.accessToken; },
clearTokens() { this.accessToken = null; this.refreshToken = null; },
refresh() {
// Simulate failed refresh (401 on refresh endpoint)
this.clearTokens();
return false;
},
};
const result401 = { status: 401 };
if (result401.status === 401 && auth.refreshToken) {
auth.refresh();
}
assert.equal(auth.isAuthed, false, 'auth cleared after failed refresh');
});
});
// ── Init flow: profile check ─────────────────
describe('init() profile gate', () => {
it('clears tokens when profile returns 401', () => {
const auth = {
accessToken: 'expired',
refreshToken: 'expired_too',
get isAuthed() { return !!this.accessToken; },
clearTokens() { this.accessToken = null; this.refreshToken = null; },
};
// Simulate getProfile throwing (as _authed does after failed refresh)
const profileFailed = true;
if (profileFailed) auth.clearTokens();
assert.equal(auth.isAuthed, false, 'tokens cleared on profile failure');
});
it('profile success + subsequent 401 = the edge case bug', () => {
// This is the exact scenario from the bug report:
// 1. Profile returns 200 (token barely valid, 5ms)
// 2. init() proceeds to startApp()
// 3. loadChats() returns 401 (token expired in the 200ms gap)
// 4. Without the guard, user sees broken UI
const auth = {
accessToken: 'barely_valid',
get isAuthed() { return !!this.accessToken; },
clearTokens() { this.accessToken = null; },
};
// Profile succeeds (token still valid)
const profileOk = true;
assert.equal(profileOk, true);
assert.equal(auth.isAuthed, true, 'auth valid after profile');
// startApp: loadChats fails, clears tokens
auth.clearTokens();
// THE GUARD: must catch this
assert.equal(auth.isAuthed, false, 'auth gone after loadChats 401');
// Without guard: UI renders. With guard: splash shown.
});
});
// ── Boot-time 401 redirect suppression ──────
// When the login page boots the SDK with stale localStorage tokens,
// the REST client's 401 handler must NOT redirect to /login (we're
// already there). boot() handles 401 gracefully on its own.
describe('on401Failure redirect suppression', () => {
// Model of the _on401Failure guard logic
function make401Handler() {
let _booting = false;
let redirectedTo = null;
return {
set booting(v) { _booting = v; },
get booting() { return _booting; },
get redirectedTo() { return redirectedTo; },
reset() { redirectedTo = null; },
on401Failure(pathname, basePath) {
// Mirrors the real _on401Failure logic
if (_booting) return;
const loginPath = basePath + '/login';
const here = pathname.replace(/\/+$/, '');
if (here === loginPath) return;
redirectedTo = loginPath;
},
};
}
it('suppresses redirect during boot', () => {
const h = make401Handler();
h.booting = true;
h.on401Failure('/chat', '');
assert.equal(h.redirectedTo, null, 'must NOT redirect during boot');
});
it('allows redirect after boot completes', () => {
const h = make401Handler();
h.booting = true;
h.on401Failure('/chat', '');
assert.equal(h.redirectedTo, null);
h.booting = false;
h.on401Failure('/chat', '');
assert.equal(h.redirectedTo, '/login', 'must redirect after boot');
});
it('suppresses redirect when already on /login', () => {
const h = make401Handler();
h.on401Failure('/login', '');
assert.equal(h.redirectedTo, null, 'must NOT redirect to /login from /login');
});
it('suppresses redirect when on /login with trailing slash', () => {
const h = make401Handler();
h.on401Failure('/login/', '');
assert.equal(h.redirectedTo, null, 'must NOT redirect from /login/');
});
it('handles basePath correctly', () => {
const h = make401Handler();
h.on401Failure('/app/login', '/app');
assert.equal(h.redirectedTo, null, 'must NOT redirect from basePath + /login');
});
it('redirects from non-login paths normally', () => {
const h = make401Handler();
h.on401Failure('/chat', '');
assert.equal(h.redirectedTo, '/login');
});
it('redirects from non-login paths with basePath', () => {
const h = make401Handler();
h.on401Failure('/app/chat', '/app');
assert.equal(h.redirectedTo, '/app/login');
});
});