Model fail-closed cross-replica session renewal and implement owner-fenced refresh, transient observer handling, logout fencing, and graceful shutdown. Remove chart-owned Ingress while retaining qualification-owned external routing.
237 lines
7.1 KiB
JavaScript
237 lines
7.1 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { connect } from 'node:net'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { after, before, test } from 'node:test'
|
|
|
|
import { createWebServer } from './server.mjs'
|
|
import { SessionRenewalPendingError } from './server/oauth.mjs'
|
|
|
|
let base
|
|
let root
|
|
let server
|
|
|
|
before(async () => {
|
|
root = await mkdtemp(join(tmpdir(), 'remora-web-server-'))
|
|
await writeFile(join(root, 'index.html'), '<main>Remora</main>')
|
|
await writeFile(join(root, 'application.js'), 'application()')
|
|
await writeFile(join(root, 'application.js.br'), 'compressed')
|
|
const config = {
|
|
root,
|
|
publicOrigin: 'http://127.0.0.1',
|
|
}
|
|
const created = createWebServer(config, {
|
|
store: { ready: true },
|
|
oauth: { route: async () => false },
|
|
proxy: {
|
|
route: async () => false,
|
|
upgrade: async () => false,
|
|
},
|
|
})
|
|
server = created.server
|
|
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
|
const address = server.address()
|
|
if (typeof address !== 'object' || address === null)
|
|
throw new Error('server address unavailable')
|
|
base = `http://127.0.0.1:${address.port}`
|
|
})
|
|
|
|
after(async () => {
|
|
server.closeIdleConnections()
|
|
server.closeAllConnections()
|
|
await new Promise((resolve, reject) =>
|
|
server.close((error) => (error ? reject(error) : resolve())),
|
|
)
|
|
await rm(root, { recursive: true })
|
|
})
|
|
|
|
test('serves health, metrics, runtime configuration, and security headers', async () => {
|
|
const ready = await fetch(`${base}/health/ready`)
|
|
assert.equal(ready.status, 200)
|
|
assert.deepEqual(await ready.json(), { status: 'ready' })
|
|
assert.equal(ready.headers.get('x-content-type-options'), 'nosniff')
|
|
assert.equal(ready.headers.get('x-frame-options'), 'DENY')
|
|
assert.match(
|
|
ready.headers.get('content-security-policy'),
|
|
/connect-src 'self'/,
|
|
)
|
|
|
|
const metrics = await fetch(`${base}/metrics`)
|
|
assert.match(await metrics.text(), /remora_web_requests_total/)
|
|
|
|
const runtime = await fetch(`${base}/runtime-config.js`)
|
|
const script = await runtime.text()
|
|
assert.match(script, /"apiUrl":"\/bff\/api\/"/)
|
|
assert.doesNotMatch(script, /oidc|client|secret/i)
|
|
assert.equal(runtime.headers.get('cache-control'), 'no-store')
|
|
})
|
|
|
|
test('serves immutable precompressed assets and correct HEAD responses', async () => {
|
|
const asset = await fetch(`${base}/application.js`, {
|
|
headers: { 'Accept-Encoding': 'br' },
|
|
})
|
|
assert.equal(asset.status, 200)
|
|
assert.equal(asset.headers.get('content-encoding'), 'br')
|
|
assert.equal(asset.headers.get('vary'), 'Accept-Encoding')
|
|
assert.match(asset.headers.get('cache-control'), /immutable/)
|
|
|
|
const head = await fetch(`${base}/application.js`, { method: 'HEAD' })
|
|
assert.equal(head.status, 200)
|
|
assert.equal(await head.text(), '')
|
|
assert.equal(
|
|
head.headers.get('content-type'),
|
|
'text/javascript; charset=utf-8',
|
|
)
|
|
})
|
|
|
|
test('limits the SPA fallback to extensionless HTML navigation', async () => {
|
|
const navigation = await fetch(`${base}/jobs/current`, {
|
|
headers: { Accept: 'text/html' },
|
|
})
|
|
assert.equal(navigation.status, 200)
|
|
assert.equal(await navigation.text(), '<main>Remora</main>')
|
|
|
|
const data = await fetch(`${base}/jobs/current`, {
|
|
headers: { Accept: 'application/json' },
|
|
})
|
|
assert.equal(data.status, 404)
|
|
const missingAsset = await fetch(`${base}/missing.js`, {
|
|
headers: { Accept: 'text/html' },
|
|
})
|
|
assert.equal(missingAsset.status, 404)
|
|
const traversal = await fetch(`${base}/%2e%2e/package.json`, {
|
|
headers: { Accept: 'text/html' },
|
|
})
|
|
assert.equal(traversal.status, 404)
|
|
})
|
|
|
|
test('graceful shutdown drains active requests and closes dependencies', async () => {
|
|
const requestStarted = deferred()
|
|
const requestReleased = deferred()
|
|
let storeClosed = false
|
|
let proxyClosed = false
|
|
const created = createWebServer(
|
|
{
|
|
root,
|
|
publicOrigin: 'http://127.0.0.1',
|
|
},
|
|
{
|
|
store: {
|
|
ready: true,
|
|
close: async () => {
|
|
storeClosed = true
|
|
},
|
|
},
|
|
oauth: { route: async () => false },
|
|
proxy: {
|
|
route: async (_request, response, url) => {
|
|
if (url.pathname !== '/bff/api/blocked') return false
|
|
requestStarted.resolve()
|
|
await requestReleased.promise
|
|
response.writeHead(204)
|
|
response.end()
|
|
return true
|
|
},
|
|
upgrade: async () => false,
|
|
close: () => {
|
|
proxyClosed = true
|
|
},
|
|
},
|
|
shutdownTimeoutMilliseconds: 1_000,
|
|
},
|
|
)
|
|
await new Promise((resolve) => created.server.listen(0, '127.0.0.1', resolve))
|
|
const address = created.server.address()
|
|
const request = fetch(`http://127.0.0.1:${address.port}/bff/api/blocked`)
|
|
await requestStarted.promise
|
|
|
|
let stopped = false
|
|
const stopping = created.shutdown().then(() => {
|
|
stopped = true
|
|
})
|
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
assert.equal(stopped, false)
|
|
assert.equal(proxyClosed, true)
|
|
requestReleased.resolve()
|
|
|
|
assert.equal((await request).status, 204)
|
|
await stopping
|
|
assert.equal(storeClosed, true)
|
|
})
|
|
|
|
test('preserves the session cookie while renewal is pending', async () => {
|
|
const created = createWebServer(
|
|
{
|
|
root,
|
|
publicOrigin: 'http://127.0.0.1',
|
|
},
|
|
{
|
|
store: { ready: true },
|
|
oauth: {
|
|
route: async () => {
|
|
throw new SessionRenewalPendingError()
|
|
},
|
|
},
|
|
proxy: {
|
|
route: async () => false,
|
|
upgrade: async () => false,
|
|
},
|
|
},
|
|
)
|
|
await new Promise((resolve) => created.server.listen(0, '127.0.0.1', resolve))
|
|
const address = created.server.address()
|
|
|
|
const response = await fetch(`http://127.0.0.1:${address.port}/auth/session`)
|
|
|
|
assert.equal(response.status, 503)
|
|
assert.equal(response.headers.get('set-cookie'), null)
|
|
await created.shutdown()
|
|
})
|
|
|
|
test('returns transient unavailability for a pending WebSocket renewal', async () => {
|
|
const created = createWebServer(
|
|
{
|
|
root,
|
|
publicOrigin: 'http://127.0.0.1',
|
|
},
|
|
{
|
|
store: { ready: true },
|
|
oauth: { route: async () => false },
|
|
proxy: {
|
|
route: async () => false,
|
|
upgrade: async () => {
|
|
throw new SessionRenewalPendingError()
|
|
},
|
|
},
|
|
},
|
|
)
|
|
await new Promise((resolve) => created.server.listen(0, '127.0.0.1', resolve))
|
|
const address = created.server.address()
|
|
const response = await new Promise((resolve, reject) => {
|
|
const socket = connect(address.port, '127.0.0.1')
|
|
let received = ''
|
|
socket.setEncoding('utf8')
|
|
socket.once('connect', () => {
|
|
socket.write(
|
|
'GET /bff/events HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n',
|
|
)
|
|
})
|
|
socket.on('data', (chunk) => {
|
|
received += chunk
|
|
})
|
|
socket.once('end', () => resolve(received))
|
|
socket.once('error', reject)
|
|
})
|
|
|
|
assert.match(response, /^HTTP\/1\.1 503 Service Unavailable\r\n/)
|
|
await created.shutdown()
|
|
})
|
|
|
|
function deferred() {
|
|
let resolve
|
|
const promise = new Promise((complete) => {
|
|
resolve = complete
|
|
})
|
|
return { promise, resolve }
|
|
}
|