remora/web/server/sessions.test.mjs
ms2k 414b380516 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.
2026-07-20 17:09:26 +09:00

186 lines
4.8 KiB
JavaScript

import assert from 'node:assert/strict'
import { test } from 'node:test'
import { SESSION_RENEWAL_PENDING, SessionStore } from './sessions.mjs'
const identifier = 'a'.repeat(43)
const config = {
sessionNamespace: 'remora:test:',
sessionTtlSeconds: 300,
}
test('serializes renewal across session store users', async () => {
const client = new FakeRedis()
const store = new SessionStore(config, client)
const original = { kind: 'authenticated', accessToken: 'old' }
await client.set(store.key(identifier), JSON.stringify(original))
const blocked = deferred()
let renewals = 0
const renewal = async () => {
renewals += 1
await blocked.promise
return { kind: 'authenticated', accessToken: 'new' }
}
const shouldRenew = (session) => session.accessToken === 'old'
const first = store.renew(identifier, shouldRenew, renewal)
await waitFor(() => renewals === 1)
const second = await store.renew(identifier, shouldRenew, renewal)
assert.equal(second, SESSION_RENEWAL_PENDING)
blocked.resolve()
const left = await first
assert.deepEqual(left, { kind: 'authenticated', accessToken: 'new' })
assert.equal(renewals, 1)
assert.deepEqual(await store.read(identifier), left)
assert.deepEqual(await store.renew(identifier, shouldRenew, renewal), left)
assert.equal(renewals, 1)
})
test('logout fences an in-flight renewal from restoring the session', async () => {
const client = new FakeRedis()
const store = new SessionStore(config, client)
await client.set(
store.key(identifier),
JSON.stringify({ kind: 'authenticated', accessToken: 'old' }),
)
const blocked = deferred()
let started = false
const renewing = store.renew(
identifier,
() => true,
async () => {
started = true
await blocked.promise
return { kind: 'authenticated', accessToken: 'new' }
},
)
await waitFor(() => started)
await store.destroy(identifier)
blocked.resolve()
assert.equal(await renewing, null)
assert.equal(await store.read(identifier), null)
})
test('ambiguous commit retains renewal ownership', async () => {
const client = new FakeRedis()
const store = new SessionStore(config, client)
await client.set(
store.key(identifier),
JSON.stringify({ accessToken: 'old' }),
)
client.failCommit = true
await assert.rejects(
store.renew(
identifier,
() => true,
async () => ({ accessToken: 'new' }),
),
/commit unavailable/,
)
assert.equal(client.values.has(store.lockKey(identifier)), true)
assert.equal(
await store.renew(
identifier,
() => true,
async () => {
throw new Error('duplicate provider refresh')
},
),
SESSION_RENEWAL_PENDING,
)
})
test('ambiguous destruction retains renewal ownership', async () => {
const client = new FakeRedis()
const store = new SessionStore(config, client)
await client.set(
store.key(identifier),
JSON.stringify({ accessToken: 'old' }),
)
client.failDeleteKey = store.key(identifier)
await assert.rejects(
store.renew(
identifier,
() => true,
async () => null,
),
/deletion unavailable/,
)
assert.equal(client.values.has(store.lockKey(identifier)), true)
assert.equal(
await store.renew(
identifier,
() => true,
async () => {
throw new Error('duplicate provider refresh')
},
),
SESSION_RENEWAL_PENDING,
)
})
class FakeRedis {
values = new Map()
isOpen = true
failCommit = false
failDeleteKey = null
async get(key) {
return this.values.get(key) ?? null
}
async set(key, value, options = {}) {
if (options.NX && this.values.has(key)) return null
this.values.set(key, value)
return 'OK'
}
async del(...keys) {
if (keys.includes(this.failDeleteKey))
throw new Error('deletion unavailable')
let deleted = 0
for (const key of keys) deleted += Number(this.values.delete(key))
return deleted
}
async eval(_script, { keys, arguments: values }) {
if (keys.length === 2 && values.length === 1) {
if (!this.values.has(keys[1]) || this.values.has(keys[0])) return 0
this.values.set(keys[0], values[0])
return 1
}
if (keys.length === 2) {
if (this.failCommit) throw new Error('commit unavailable')
if (this.values.get(keys[0]) !== values[0] || !this.values.has(keys[1]))
return 0
this.values.set(keys[1], values[1])
return 1
}
if (this.values.get(keys[0]) !== values[0]) return 0
this.values.delete(keys[0])
return 1
}
async quit() {
this.isOpen = false
}
}
function deferred() {
let resolve
const promise = new Promise((complete) => {
resolve = complete
})
return { promise, resolve }
}
async function waitFor(predicate) {
while (!predicate()) await new Promise((resolve) => setTimeout(resolve, 1))
}