const assert = require('node:assert/strict') const { execFileSync } = require('node:child_process') const fs = require('node:fs') const os = require('node:os') const path = require('node:path') const test = require('node:test') const { setTimeout: delay } = require('node:timers/promises') const frontend = path.resolve(__dirname, '../frontend') const dockerContext = process.env.NGINX_TEST_DOCKER_CONTEXT const docker = (...args) => execFileSync('docker', [ ...(dockerContext ? ['--context', dockerContext] : []), ...args, ], { encoding: 'utf8', timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'] }).trim() // Runs only a disposable nginx with static fixtures, never the application stack. test('nginx serves runtime config and versioned assets with distinct cache policies', { timeout: 90000 }, async t => { const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'weknora-cache-test-')) const container = `weknora-cache-test-${process.pid}-${Date.now()}` const image = process.env.NGINX_TEST_IMAGE || fs.readFileSync(path.join(frontend, 'Dockerfile'), 'utf8').match(/^FROM (\S+)/m)[1] let created = false try { const site = path.join(temp, 'html') const fixtures = { 'index.html': 'main cache fixture', 'embed.html': 'embed cache fixture', 'config.js': 'window.__RUNTIME_CONFIG__ = { MAX_FILE_SIZE_MB: 50 };', 'tdesign-icons/0.4.1/fonts/index.js': 'window.cacheTestIcon = true;', 'assets/cache-test-012345.js': 'window.cacheTestAsset = true;', } for (const [name, body] of Object.entries(fixtures)) { const filename = path.join(site, name) fs.mkdirSync(path.dirname(filename), { recursive: true }) fs.writeFileSync(filename, body) } const replacements = { APP_SCHEME: 'http', APP_HOST: '127.0.0.1', APP_PORT: '9', MAX_FILE_SIZE: '50M', MAX_SKILL_BUNDLE_SIZE: '256M' } const config = fs.readFileSync(path.join(frontend, 'nginx.conf'), 'utf8') .replace(/\$\{(APP_SCHEME|APP_HOST|APP_PORT|MAX_FILE_SIZE|MAX_SKILL_BUNDLE_SIZE)\}/g, (_, key) => replacements[key]) fs.writeFileSync(path.join(temp, 'default.conf'), config) docker('create', '--rm', '--pull', 'never', '--name', container, '--publish', '127.0.0.1::80', '--entrypoint', 'nginx', image, '-g', 'daemon off;') created = true // Copying also supports remote Docker contexts without host-directory mounts. docker('cp', `${site}/.`, `${container}:/usr/share/nginx/html`) docker('cp', path.join(temp, 'default.conf'), `${container}:/etc/nginx/conf.d/default.conf`) docker('cp', path.join(frontend, 'nginx-api-proxy.conf'), `${container}:/etc/nginx/api-proxy.conf`) docker('start', container) docker('exec', container, 'nginx', '-t') const address = docker('port', container, '80/tcp').split('\n')[0] assert.match(address, /^127\.0\.0\.1:\d+$/) const base = `http://${address}` const request = async (url, options) => { const response = await fetch(`${base}${url}`, { ...options, signal: AbortSignal.timeout(3000) }) return { status: response.status, headers: response.headers, body: await response.text() } } for (let attempt = 0; ; attempt += 1) { try { await request('/login'); break } catch (error) { if (attempt >= 19) throw error await delay(250) } } await t.test('runtime config is never stored, including conditional responses', async () => { const response = await request('/config.js?deployment=next') assert.equal(response.status, 200) assert.equal(response.body, fixtures['config.js']) assert.equal(response.headers.get('cache-control'), 'no-store') assert.equal(response.headers.get('x-content-type-options'), 'nosniff') assert.ok(response.headers.get('etag')) const conditional = await request('/config.js', { headers: { 'If-None-Match': response.headers.get('etag') } }) assert.equal(conditional.status, 304) assert.equal(conditional.headers.get('cache-control'), 'no-store') }) await t.test('versioned icons and hashed build assets retain immutable caching', async () => { for (const name of ['tdesign-icons/0.4.1/fonts/index.js', 'assets/cache-test-012345.js']) { const response = await request(`/${name}`) assert.equal(response.status, 200) assert.equal(response.body, fixtures[name]) assert.equal(response.headers.get('cache-control'), 'public, max-age=31536000, immutable') assert.equal(response.headers.get('x-content-type-options'), 'nosniff') assert.ok(response.headers.get('etag')) const conditional = await request(`/${name}`, { headers: { 'If-None-Match': response.headers.get('etag') } }) assert.equal(conditional.status, 304) assert.equal(conditional.headers.get('cache-control'), 'public, max-age=31536000, immutable') } }) await t.test('missing versioned assets return an uncacheable 404, never the SPA', async () => { for (const url of ['/tdesign-icons/0.4.1/fonts/missing.js', '/tdesign-icons/99.0.0/fonts/index.js', '/assets/missing.js']) { const response = await request(url) assert.equal(response.status, 404) assert.equal(response.headers.get('cache-control'), 'no-store') assert.ok(!response.body.includes('main cache fixture')) } }) await t.test('SPA and embed entry points still require revalidation', async () => { for (const [url, fixture] of [['/login', 'index.html'], ['/embed/cache-test', 'embed.html']]) { const response = await request(url) assert.equal(response.status, 200) assert.equal(response.body, fixtures[fixture]) assert.equal(response.headers.get('cache-control'), 'no-cache, must-revalidate') } }) await t.test('missing runtime configuration is an uncacheable 404', async () => { docker('exec', container, 'rm', '/usr/share/nginx/html/config.js') const response = await request('/config.js') assert.equal(response.status, 404) assert.equal(response.headers.get('cache-control'), 'no-store') assert.ok(!response.body.includes('main cache fixture')) }) } finally { try { if (created) docker('rm', '--force', container) } finally { fs.rmSync(temp, { recursive: true, force: true }) } } })