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)) }