const test = require('node:test'); const assert = require('node:assert/strict'); require('../src/common'); const registerAuthRoutes = require('../src/web/routes/auth'); const renderLoginPage = require('../src/web/routes/auth/login'); const { createSessionService, normalizeReturnToPath } = require('../src/web/lib/auth/session'); test('login page preserves the return target in a hidden field', () => { const html = renderLoginPage('', '/slides/6/edit', 'demo'); assert.match(html, /name="returnTo"/); assert.match(html, /value="\/slides\/6\/edit"/); assert.match(html, /name="remember_me"/); assert.match(html, /name="username"[^>]*value="demo"/); }); test('requireAuth preserves the original request path when redirecting to login', () => { const sessionService = createSessionService({ sessionCookieName: 'pulse_session', sessionMaxAgeMs: 60 * 60 * 1000, hashSessionToken: (value) => value, createSessionToken: () => 'token' }); const req = { originalUrl: '/slides/6/edit?tab=content' }; const res = { headers: {}, getHeader(name) { return this.headers[name]; }, setHeader(name, value) { this.headers[name] = value; }, redirect(url) { this.redirectedTo = url; } }; sessionService.requireAuth(req, res, () => {}); assert.equal(res.redirectedTo, '/login?returnTo=%2Fslides%2F6%2Fedit%3Ftab%3Dcontent'); }); test('normalizeReturnToPath keeps same-origin absolute urls and strips the origin', () => { assert.equal( normalizeReturnToPath('https://signage.example.com/slides/6/edit?tab=content', 'https://signage.example.com'), '/slides/6/edit?tab=content' ); assert.equal( normalizeReturnToPath('https://evil.example.com/slides/6/edit', 'https://signage.example.com'), '' ); }); test('successful login returns to the requested link instead of the dashboard', async () => { const handlers = {}; const app = { get(path, ...routeHandlers) { handlers['GET ' + path] = routeHandlers; }, post(path, ...routeHandlers) { handlers['POST ' + path] = routeHandlers; } }; registerAuthRoutes(app, { pool: { async query(sql) { if (sql.includes('FROM a_users WHERE username = ? LIMIT 1')) { return [[{ id: 7, name: 'Demo User', username: 'demo', password_hash: 'hash', password_salt: 'salt', password_iterations: 1 }]]; } return [[]]; } }, pages: { renderLoginPage(message, returnTo) { return { message, returnTo }; } }, createUserSession: async () => 'session-token', getSessionMaxAgeMs: async () => 7 * 24 * 60 * 60 * 1000, setSessionCookie(res, token, maxAgeMs) { res.sessionToken = token; res.sessionMaxAgeMs = maxAgeMs; }, clearSessionCookie() {}, parseCookies() { return {}; }, consumeAuthMessageCookie() { return ''; }, setAuthMessageCookie() {}, hashSessionToken(value) { return value; }, verifyPassword() { return true; }, sessionCookieName: 'pulse_session' }); const req = { body: { username: 'demo', password: 'secret', returnTo: '/slides/6/edit?tab=content' }, query: { returnTo: '/slides/6/edit?tab=content' } }; const res = { redirect(url) { this.redirectedTo = url; }, status() { return this; }, send(value) { this.sentValue = value; return this; } }; await handlers['POST /login'][0](req, res, () => {}); assert.equal(res.sessionToken, 'session-token'); assert.equal(res.sessionMaxAgeMs, null); assert.equal(res.redirectedTo, '/slides/6/edit?tab=content'); const rememberedRequest = { body: { username: 'demo', password: 'secret', remember_me: '1' }, query: {} }; const rememberedResponse = { redirect(url) { this.redirectedTo = url; }, status() { return this; }, send(value) { this.sentValue = value; return this; } }; await handlers['POST /login'][0](rememberedRequest, rememberedResponse, () => {}); assert.equal(rememberedResponse.sessionMaxAgeMs, 7 * 24 * 60 * 60 * 1000); });