Harden WebBFF and deployment release behavior
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.
This commit is contained in:
parent
ffa65018ed
commit
414b380516
1 changed files with 980 additions and 136 deletions
|
|
@ -1,10 +1,12 @@
|
|||
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
|
||||
|
|
@ -103,3 +105,133 @@ test('limits the SPA fallback to extensionless HTML navigation', async () => {
|
|||
})
|
||||
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 }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue