import assert from 'node:assert/strict' import { createServer } from 'node:http' import { spawn } from 'node:child_process' import { after, before, test } from 'node:test' import { createServer as createViteServer } from 'vite' import { render } from './dist/server/entry-server.js' const origin = 'https://ssr.example.invalid' const date = '2026-09-15T00:00:00+09:00' let api, apiURL, routes, vite, node, httpOrigin let failAPI = false const requests = [] // Rails owns encryption and verification; this upstream fixture tests Node's forwarding contract. const recoveryCode = 'test-recovery-secret' const users = new Map ([ [recoveryCode, { id: 1, name: 'SSR account', role: 'admin' }], ['second-recovery-secret', { id: 2, name: 'Second account', role: 'member' }]]) const sessions = new Map () let sessionSequence = 0 const cookieName = '__Host-btrc-session' const cookieFor = value => `${cookieName}=${value}` const sessionValue = request => request.headers.cookie?.split (';') .map (part => part.trim ()).find (part => part.startsWith (`${cookieName}=`)) ?.slice (cookieName.length + 1) const issueSession = (response, code = null) => { const value = `opaque-rails-session-${++sessionSequence}` sessions.set (value, { code, token: `csrf-${sessionSequence}` }) refreshCookie (response, value) return value } const refreshCookie = (response, value) => response.setHeader ('Set-Cookie', `${cookieFor (value)}; Path=/; Max-Age=630720000; HttpOnly; Secure; SameSite=Lax`) const tag = locale => ({ id: 1, name: locale === 'ja' ? 'SSR タグ' : 'SSR シロシ', category: 'general', deprecated_at: null, aliases: [], parents: [], children: [], post_count: 1, created_at: date, updated_at: date, has_wiki: true, material_id: null, has_deerjikists: false, sections: [] }) const post = locale => ({ id: 1, version_no: 1, title: `SSR post ${locale}`, url: 'https://example.invalid/post', thumbnail: null, thumbnail_base: null, video_ms: null, tags: [tag (locale)], parent_posts: [], child_posts: [], sibling_posts: {}, viewed: false, related: [], original_created_from: date, original_created_before: date, created_at: date, updated_at: date, uploaded_user: null }) const wiki = locale => ({ id: 1, tag_id: 1, resource_type: 'tag_description', language_code: locale === 'ja' ? 'ja' : 'ain', title: tag (locale).name, body: `SSR wiki body ${locale}`, created_at: date, updated_at: date, revision_id: 1, pred: null, succ: null, deprecated_at: null }) const material = locale => ({ id: 1, version_no: 1, tag: tag (locale), file: null, url: 'https://example.invalid', thumbnail: null, thumbnail_fallback_text: 'SSR material', thumbnail_fallback_kind: 'tag_name', media_kind: 'url_only', content_type: null, file_byte_size: null, export_paths: {}, export_items: [], created_at: date, updated_at: date, created_by_user: { id: 1, name: 'SSR' }, updated_by_user: { id: 1, name: 'SSR' } }) const responseFor = url => { const locale = url.searchParams.get ('locale') ?? 'ja' const path = url.pathname if (path === '/locales') return { locales: [{ code: 'ja', name: '日本語' }, { code: 'ain-Kana', name: 'アイヌ' }] } if (path === '/users/settings') return { theme: 'system', wiki_editor_mode: 'split' } if (path === '/users/theme_slots') return [] if (path === '/posts') return { posts: [post (locale)], count: 1 } if (path === '/gekanator/posts') return { posts: [post (locale)] } if (path === '/gekanator/questions') return { questions: [] } if (path === '/posts/1') return post (locale) if (path === '/tags') return { tags: [tag (locale)], count: 1 } if (path === '/tags/1' || path.startsWith ('/tags/name/')) return tag (locale) if (path === '/tags/1/deerjikists') return { tag: tag (locale), deerjikists: [] } if (path === '/tags/with-depth') return [] if (path === '/tags/nico' || path === '/nico/tags') return { tags: [], count: 0 } if (path.endsWith ('/versions')) return { versions: [], count: 0 } if (path === '/wiki') return [wiki (locale)] if (path === '/wiki/changes') return [] if (path.includes ('autolink')) return [] if (path === '/wiki/1/diff') return { title: 'SSR wiki diff', diff: [] } if (path.startsWith ('/wiki/')) return wiki (locale) if (path === '/materials') return { materials: [material (locale)], count: 1 } if (path === '/materials/1') return material (locale) if (path.includes ('suppressions')) return { suppressions: [] } if (path === '/theatres/1') return { id: 1, name: 'SSR theatre', created_at: date } if (path === '/theatres/1/programmes') return [] if (path === '/theatres/1/comments') return [] if (path === '/theatres/1/post_selection_weights') return { tag_penalties: [], lightest_posts: [], heaviest_posts: [] } if (path === '/theatres/1/watching') return { host_flg: false, post_id: null, post_started_at: null, post_elapsed_ms: null, watching_users: [], skip_vote: { votes_count: 0, required_count: 1, watching_users_count: 0, voted: false } } return null } // Give startup failures a bounded, visible error instead of waiting indefinitely. const withTimeout = async (label, promise, milliseconds) => { let timer try { return await Promise.race ([promise, new Promise ((_, reject) => { timer = setTimeout (() => reject (new Error ( `[SSR TEST] ${label} timed out after ${milliseconds}ms`)), milliseconds) })]) } finally { clearTimeout (timer) } } before (async () => { console.error('[SSR TEST] setup started') api = createServer (async (request, response) => { requests.push ({ method: request.method, url: request.url, cookie: request.headers.cookie, csrf: request.headers['x-csrf-token'], transferCode: request.headers['x-transfer-code'] }) const url = new URL (request.url, 'http://localhost') response.setHeader ('Content-Type', 'application/json') response.setHeader ('Cache-Control', 'private, no-store') if (failAPI) { response.writeHead (503).end () return } if (url.pathname.startsWith ('/users')) { const value = sessionValue (request) const session = sessions.get (value) const user = users.get (session?.code) if (request.method === 'GET') { if (url.pathname === '/users/csrf') { const id = session != null ? value : issueSession (response) response.end (JSON.stringify ({ token: sessions.get (id).token })) return } if (user == null) { response.writeHead (401).end () return } refreshCookie (response, value) const data = url.pathname === '/users/session' ? user : (url.pathname === '/users/me' ? { ...user, inheritance_code: session.code } : responseFor (url)) response.writeHead (data == null ? 404 : 200) response.end (JSON.stringify (data)) return } if (![origin, httpOrigin].includes (request.headers.origin) || session == null || request.headers['x-csrf-token'] !== session.token) { response.writeHead (403, { 'X-CSRF-Invalid': '1' }).end () return } let body = '' for await (const chunk of request) body += chunk const params = JSON.parse (body || '{}') if (request.method === 'POST' && url.pathname === '/users/verify') { const code = params.restore && user != null ? session.code : params.code const verified = users.get (code) if (verified == null) { response.end (JSON.stringify ({ valid: false })) return } sessions.delete (value) issueSession (response, code) response.end (JSON.stringify ({ valid: true, user: { ...verified, inheritance_code: code } })) return } if (request.method === 'DELETE' && url.pathname === '/users/session') { sessions.delete (value) response.writeHead (204, { 'Set-Cookie': `${cookieName}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax` }).end () return } if (request.method === 'POST' && url.pathname === '/users/code/renew' && user != null) { const code = `rotated-recovery-${sessionSequence}` users.delete (session.code) users.set (code, user) sessions.delete (value) issueSession (response, code) response.end (JSON.stringify ({ code })) return } response.writeHead (404).end () return } const data = responseFor (url) response.writeHead (data == null ? 404 : 200) response.end (JSON.stringify (data)) }) console.error ('[SSR TEST] API created') await withTimeout ('mock API listen', new Promise ((resolve, reject) => { api.once ('error', reject) api.listen (0, '127.0.0.1', () => { api.off ('error', reject) resolve () }) }), 15_000) apiURL = `http://127.0.0.1:${api.address ().port}` console.error ('[SSR TEST] API ready:', apiURL) node = spawn (process.execPath, ['server.mjs'], { env: { ...process.env, NODE_ENV: 'production', PORT: '0', HOST: '127.0.0.1', PUBLIC_ORIGIN: origin, SSR_API_BASE_URL: apiURL }, stdio: ['ignore', 'pipe', 'pipe'] }) console.error ('[SSR TEST] SSR process spawned:', node.pid ?? '(no PID)') let childOutput = '' let childError = '' node.stderr.on ('data', data => { const text = data.toString () childError = (childError + text).slice (-8_192) process.stderr.write (`[SSR CHILD] ${text}`) }) httpOrigin = await withTimeout ('SSR server readiness', new Promise ((resolve, reject) => { const onData = data => { childOutput = (childOutput + data.toString()).slice(-8192) // ANSI カラーコードを取り除いてから解析する const output = childOutput.replace(/\x1b\[[0-9;]*m/g, '') const match = output.match(/\[SSR\]\s+listening on\s+(\d+)/) if (!match) return console.error('[SSR TEST] detected SSR port:', match[1]) resolve(`http://127.0.0.1:${match[1]}`) } node.stdout.on ('data', onData) node.once ('error', reject) node.once ('exit', (code, signal) => reject (new Error ( `SSR process exited before readiness (code=${code}, signal=${signal})` + `\nstdout: ${childOutput}\nstderr: ${childError}`))) }), 30_000).catch (error => { throw new Error (`${error.message}\nSSR stdout: ${childOutput}\nSSR stderr: ${childError}`, { cause: error }) }) console.error ('[SSR TEST] SSR server ready:', httpOrigin) console.error ('[SSR TEST] starting Vite') vite = await withTimeout ('Vite initialization', createViteServer ({ server: { middlewareMode: true }, appType: 'custom' }), 90_000) console.error ('[SSR TEST] Vite ready') const { pageRoutes } = await withTimeout ('App.tsx module loading', vite.ssrLoadModule ('/src/App.tsx'), 90_000) console.error ('[SSR TEST] App loaded') const flatten = (entries, parent = '') => entries.flatMap (route => { const path = [parent, route.path].filter (Boolean).join ('/') return route.children ? flatten (route.children, path) : [{ ...route, path }] }) routes = flatten (pageRoutes (null, () => {})) console.error (`[SSR TEST] setup complete (${routes.length} routes)`) }) // Report the route on which server-side rendering stalls or fails. const renderPage = async (url, ...args) => { console.error (`[SSR TEST] rendering: ${url}`) const result = await withTimeout (`render ${url}`, render (url, ...args), 60_000) console.error (`[SSR TEST] rendered: ${url}`) return result } const authenticatedCookie = async (code = recoveryCode) => { const csrf = await fetch (`${httpOrigin}/api/users/csrf`) assert.equal (csrf.status, 200) const { token } = await csrf.json () const verified = await fetch (`${httpOrigin}/api/users/verify`, { method: 'POST', headers: { Origin: origin, 'Content-Type': 'application/json', Cookie: csrf.headers.get ('set-cookie').split (';')[0], 'X-CSRF-Token': token }, body: JSON.stringify ({ code, restore: true }) }) assert.equal (verified.status, 200) assert.equal ((await verified.json ()).valid, true) const cookie = verified.headers.get ('set-cookie') assert.match (cookie, /HttpOnly; Secure; SameSite=Lax/) assert.doesNotMatch (cookie, new RegExp (code)) return cookie.split (';')[0] } after (async () => { console.error ('[SSR TEST] teardown started') const errors = [] if (vite) { try { await withTimeout ('Vite shutdown', vite.close (), 30_000) } catch (error) { errors.push (error) } } if (node && node.exitCode == null && node.signalCode == null) node.kill () if (api?.listening) { api.closeAllConnections () try { await withTimeout ('mock API shutdown', new Promise ((resolve, reject) => { api.close (error => error ? reject (error) : resolve ()) }), 10_000) } catch (error) { errors.push (error) } } console.error ('[SSR TEST] teardown finished') if (errors.length) throw new AggregateError (errors, 'SSR teardown failed') }) test ('every canonical screen route returns page HTML without JavaScript', async t => { const cookie = await authenticatedCookie () for (const route of routes) { for (const locale of ['ja', 'ain-Kana']) { const path = route.path.replace (':identifier', 'home').replace (/:id|:title/g, '1') .replace ('*', 'unknown-screen') const url = `/${locale}/${path}?from=2026-09-15&to=2026-09-15` await t.test (`${locale} ${route.path}`, async () => { const result = await renderPage (url, origin, apiURL, { cookie }) assert.ok (result != null) if (route.handle?.redirectTo) { assert.equal (result.status, 308) return } assert.ok (result.html?.length > 300, 'actual page HTML is required') assert.equal (result.status, route.path === '*' ? 404 : 200) assert.match (result.head, /