diff --git a/src/tests/backend/common.ts b/src/tests/backend/common.ts index 73bc3f781c3..10d60337716 100644 --- a/src/tests/backend/common.ts +++ b/src/tests/backend/common.ts @@ -111,7 +111,7 @@ export const init = async function () { * @param {string} event - The socket.io Socket event to listen for. * @returns The argument(s) passed to the event handler. */ -export const waitForSocketEvent = async (socket: any, event:string) => { +export const waitForSocketEvent = async (socket: any, event:string, timeoutMs = 1000) => { const errorEvents = [ 'error', 'connect_error', @@ -126,7 +126,7 @@ export const waitForSocketEvent = async (socket: any, event:string) => { const timeout = setTimeout(() => { reject(new Error(`timed out waiting for ${event} event`)); cancelTimeout = () => {}; - }, 1000); + }, timeoutMs); cancelTimeout = () => { clearTimeout(timeout); resolve(); @@ -185,7 +185,9 @@ export const connect = async (res:any = null) => { query: {cookie: reqCookieHdr, padId}, }); try { - await waitForSocketEvent(socket, 'connect'); + // Connect is a known slow path on loaded CI runners — give it a longer budget than the + // default per-message wait used elsewhere. + await waitForSocketEvent(socket, 'connect', 5000); } catch (e) { socket.close(); throw e; @@ -213,7 +215,9 @@ export const handshake = async (socket: any, padId:string, token = padutils.gene token, }); logger.debug('waiting for CLIENT_VARS response...'); - const msg = await waitForSocketEvent(socket, 'message'); + // CLIENT_VARS is a known slow path on loaded CI runners (auth + pad load) — give it a longer + // budget than the default per-message wait used elsewhere. + const msg = await waitForSocketEvent(socket, 'message', 5000); logger.debug('received CLIENT_VARS message'); return msg; }; diff --git a/src/tests/backend/specs/SessionStore.ts b/src/tests/backend/specs/SessionStore.ts index 415ebc3c419..a92301f5da3 100644 --- a/src/tests/backend/specs/SessionStore.ts +++ b/src/tests/backend/specs/SessionStore.ts @@ -26,6 +26,19 @@ describe(__filename, function () { const destroy = async () => await util.promisify(ss!.destroy).call(ss, sid); const touch = async (sess: Session) => await util.promisify(ss!.touch).call(ss, sid, sess); + // Poll until `cond` is true. Used in place of fixed sleeps for "the cleanup timer should have + // fired by now" assertions — passes immediately when cleanup completes so tests stay fast, + // but tolerates slow CI runners where the event loop may be delayed by hundreds of ms. + const eventually = async (cond: () => Promise, maxMs = 2000, intervalMs = 25) => { + const deadline = Date.now() + maxMs; + // First check is immediate so the helper doesn't add a fixed delay. + while (true) { + if (await cond()) return; + if (Date.now() >= deadline) throw new Error(`condition not met within ${maxMs}ms`); + await new Promise((r) => setTimeout(r, intervalMs)); + } + }; + before(async function () { await common.init(); }); @@ -57,12 +70,11 @@ describe(__filename, function () { }); it('set of session that expires', async function () { - const sess:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; + const sess:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 300)}}; await set(sess); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess)); - await new Promise((resolve) => setTimeout(resolve, 110)); - // Writing should start a timeout. - assert(await db.get(`sessionstorage:${sid}`) == null); + // Writing should start a timeout that purges the record once expiry passes. + await eventually(async () => await db.get(`sessionstorage:${sid}`) == null); }); it('set of already expired session', async function () { @@ -75,18 +87,17 @@ describe(__filename, function () { it('switch from non-expiring to expiring', async function () { const sess:any = {foo: 'bar'}; await set(sess); - const sess2:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; + const sess2:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 300)}}; await set(sess2); - await new Promise((resolve) => setTimeout(resolve, 110)); - assert(await db.get(`sessionstorage:${sid}`) == null); + await eventually(async () => await db.get(`sessionstorage:${sid}`) == null); }); it('switch from expiring to non-expiring', async function () { - const sess:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; + const sess:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 300)}}; await set(sess); const sess2:any = {foo: 'bar'}; await set(sess2); - await new Promise((resolve) => setTimeout(resolve, 110)); + await new Promise((resolve) => setTimeout(resolve, 330)); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess2)); }); }); @@ -109,12 +120,11 @@ describe(__filename, function () { }); it('get of record from previous run (not yet expired)', async function () { - const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; + const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 300)}}; await db.set(`sessionstorage:${sid}`, sess); assert.equal(JSON.stringify(await get()), JSON.stringify(sess)); - await new Promise((resolve) => setTimeout(resolve, 110)); - // Reading should start a timeout. - assert(await db.get(`sessionstorage:${sid}`) == null); + // Reading should start a timeout that purges the record once expiry passes. + await eventually(async () => await db.get(`sessionstorage:${sid}`) == null); }); it('get of record from previous run (already expired)', async function () { @@ -125,13 +135,13 @@ describe(__filename, function () { }); it('external expiration update is picked up', async function () { - const sess:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; + const sess:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 300)}}; await set(sess); assert.equal(JSON.stringify(await get()), JSON.stringify(sess)); - const sess2 = {...sess, cookie: {expires: new Date(Date.now() + 200)}}; + const sess2 = {...sess, cookie: {expires: new Date(Date.now() + 600)}}; await db.set(`sessionstorage:${sid}`, sess2); assert.equal(JSON.stringify(await get()), JSON.stringify(sess2)); - await new Promise((resolve) => setTimeout(resolve, 110)); + await new Promise((resolve) => setTimeout(resolve, 330)); // The original timeout should not have fired. assert.equal(JSON.stringify(await get()), JSON.stringify(sess2)); }); @@ -139,11 +149,14 @@ describe(__filename, function () { describe('shutdown', function () { it('shutdown cancels timeouts', async function () { - const sess:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; + // expires=500 / wait=700 keeps comfortable headroom on slow CI: setup + // (set+get+shutdown) must finish before the timer would fire (500ms is plenty), and the + // 700ms wait is past the original expiry so a cancelled timer would have fired by then. + const sess:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 500)}}; await set(sess); assert.equal(JSON.stringify(await get()), JSON.stringify(sess)); ss!.shutdown(); - await new Promise((resolve) => setTimeout(resolve, 110)); + await new Promise((resolve) => setTimeout(resolve, 700)); // The record should not have been automatically purged. assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess)); }); @@ -151,18 +164,18 @@ describe(__filename, function () { describe('destroy', function () { it('destroy deletes the database record', async function () { - const sess:any = {cookie: {expires: new Date(Date.now() + 100)}}; + const sess:any = {cookie: {expires: new Date(Date.now() + 300)}}; await set(sess); await destroy(); assert(await db.get(`sessionstorage:${sid}`) == null); }); it('destroy cancels the timeout', async function () { - const sess:any = {cookie: {expires: new Date(Date.now() + 100)}}; + const sess:any = {cookie: {expires: new Date(Date.now() + 300)}}; await set(sess); await destroy(); await db.set(`sessionstorage:${sid}`, sess); - await new Promise((resolve) => setTimeout(resolve, 110)); + await new Promise((resolve) => setTimeout(resolve, 330)); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess)); });