import assert from 'node:assert/strict'; import { test } from 'node:test'; import { install, server, nodes, find, field, emit, settle } from './flow-helpers.mjs'; const ui = install(); const { api, setUnauthorizedHandler } = await import('../../bindery/static/js/lib.js'); const { stopPolling } = await import('../../bindery/static/js/jobs.js'); test('login retry, navigation, session expiry and logout preserve the authentication lifecycle', async (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }); const backend = server(t); let auth = { authenticated: false, mode: 'cwa', user: null }; backend.on('GET', 'auth/check', () => auth); backend.on('GET', 'config', { metadata_fields: {}, mode_labels: {}, library_available: false, download_fields: {} }); backend.on('GET', 'jobs', []); backend.on('GET', 'stories', []); backend.on('GET', 'rules', { rules: [], paragraphs: false }); backend.on('GET', 'settings', { recent_chapters: 3, personal_ini: '' }); backend.on('GET', 'stories/1', { error: 'Story unavailable' }, 404); backend.on('GET', 'stories/1/rules', { rules: [], paragraphs: null }); backend.on('GET', 'chapters/2/diff?', { error: 'Chapter unavailable' }, 404); backend.on('GET', 'expired', { error: 'Session ended' }, 401); backend.on('POST', 'auth/logout', () => { auth = { ...auth, authenticated: false }; return {}; }); t.after(() => { stopPolling(); setUnauthorizedHandler(() => {}); }); await import('../../bindery/static/js/main.js'); await settle(); assert.equal(document.activeElement, field(ui.app, 'username')); assert.equal(backend.sent('GET', 'config').length, 0); ui.windowEvents.hashchange(); assert.equal(backend.sent('GET', 'stories').length, 0); const form = () => nodes(ui.app).find((el) => el.tagName === 'form'); field(ui.app, 'username').value = 'reader'; field(ui.app, 'password').value = 'wrong'; nodes(form()).find((el) => el.attributes.type === 'checkbox').checked = false; backend.on('POST', 'auth/login', { error: 'Wrong password' }, 401); await emit(form(), 'submit'); assert.match(ui.app.textContent, /Wrong password/); assert.equal(find(ui.app, 'button', 'Log in').disabled, false); assert.deepEqual(backend.sent('POST', 'auth/login')[0].body, { username: 'reader', password: 'wrong', remember: false }); field(ui.app, 'password').value = 'correct'; backend.on('POST', 'auth/login', () => { auth = { authenticated: true, mode: 'cwa', user: 'reader' }; return {}; }); await emit(form(), 'submit'); await settle(); assert.ok(find(ui.app, 'button', 'Log out')); assert.equal(backend.sent('GET', 'config').length, 1); for (const path of ['', '#/settings', '#/story/1', '#/story/1/chapter/2', '#/missing']) { window.location.hash = path; ui.windowEvents.hashchange(); await settle(); } assert.match(ui.app.textContent, /Page not found/); assert.equal(backend.sent('GET', 'stories/1/rules').length, 1); assert.equal(backend.sent('GET', 'chapters/2/diff?').length, 1); auth = { authenticated: false, mode: 'cwa' }; await assert.rejects(api('expired'), /Session ended/); await settle(); assert.ok(find(ui.app, 'button', 'Log in')); const polls = backend.sent('GET', 'jobs').length; t.mock.timers.tick(60000); await settle(); assert.equal(backend.sent('GET', 'jobs').length, polls); const checks = backend.sent('GET', 'auth/check').length; await assert.rejects(api('expired'), /Session ended/); assert.equal(backend.sent('GET', 'auth/check').length, checks); await emit(form(), 'submit'); await settle(); await emit(find(ui.app, 'button', 'Log out'), 'click'); await settle(); assert.ok(find(ui.app, 'button', 'Log in')); assert.equal(backend.sent('POST', 'auth/logout').length, 1); backend.on('POST', 'auth/login', () => { auth = { authenticated: true, mode: 'proxy', user: 'reader', logout_url: 'https://login.example/logout' }; return {}; }); await emit(form(), 'submit'); await settle(); await emit(find(ui.app, 'button', 'Log out'), 'click'); assert.equal(window.location.href, 'https://login.example/logout'); });