From 206d90320a845ed0b94ea4055e0283e3f1fc85a6 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 12 Oct 2026 15:16:30 +0200 Subject: [PATCH] test(api): restore spies so call history cannot leak between files bun restores a spied method when its test file ends but keeps the spy's call history, so a later file's spyOn() on the same method starts with calls it never made. Under some file orders this failed supabaseAuth.test.ts after vortexAdminAuth.test.ts. The invariant test keeps every spying file restoring its spies. --- .../api/middlewares/vortexAdminAuth.test.ts | 3 ++- .../admin-console/admin-console.route.test.ts | 3 ++- .../api/services/impersonation.service.test.ts | 3 ++- .../src/tests/spy-restore.invariants.test.ts | 18 ++++++++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 apps/api/src/tests/spy-restore.invariants.test.ts diff --git a/apps/api/src/api/middlewares/vortexAdminAuth.test.ts b/apps/api/src/api/middlewares/vortexAdminAuth.test.ts index 3dd092385..ddb7f50ec 100644 --- a/apps/api/src/api/middlewares/vortexAdminAuth.test.ts +++ b/apps/api/src/api/middlewares/vortexAdminAuth.test.ts @@ -1,4 +1,4 @@ -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, spyOn } from "bun:test"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; import express, { Request, Response } from "express"; import { config } from "../../config/vars"; import ProfileRole from "../../models/profileRole.model"; @@ -39,6 +39,7 @@ describe("requireVortexAdmin", () => { }); afterEach(() => { + mock.restore(); config.impersonationEnabled = originalImpersonationEnabled; }); diff --git a/apps/api/src/api/routes/v1/admin-console/admin-console.route.test.ts b/apps/api/src/api/routes/v1/admin-console/admin-console.route.test.ts index 27a59901d..3a82d92ba 100644 --- a/apps/api/src/api/routes/v1/admin-console/admin-console.route.test.ts +++ b/apps/api/src/api/routes/v1/admin-console/admin-console.route.test.ts @@ -1,4 +1,4 @@ -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, spyOn } from "bun:test"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; import express from "express"; import { config } from "../../../../config/vars"; import AdminImpersonationSession from "../../../../models/adminImpersonationSession.model"; @@ -42,6 +42,7 @@ describe("admin-console routes", () => { }); afterEach(() => { + mock.restore(); config.impersonationEnabled = originalImpersonationEnabled; }); diff --git a/apps/api/src/api/services/impersonation.service.test.ts b/apps/api/src/api/services/impersonation.service.test.ts index f7cce1fc3..34deb4e82 100644 --- a/apps/api/src/api/services/impersonation.service.test.ts +++ b/apps/api/src/api/services/impersonation.service.test.ts @@ -1,4 +1,4 @@ -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, spyOn } from "bun:test"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; import crypto from "crypto"; import { config } from "../../config/vars"; import AdminImpersonationSession from "../../models/adminImpersonationSession.model"; @@ -28,6 +28,7 @@ describe("impersonation.service", () => { }); afterEach(() => { + mock.restore(); config.impersonationEnabled = originalImpersonationEnabled; }); diff --git a/apps/api/src/tests/spy-restore.invariants.test.ts b/apps/api/src/tests/spy-restore.invariants.test.ts new file mode 100644 index 000000000..86adfaf03 --- /dev/null +++ b/apps/api/src/tests/spy-restore.invariants.test.ts @@ -0,0 +1,18 @@ +import { expect, it } from "bun:test"; +import { join } from "path"; + +// bun restores a spied method when its test file ends but keeps the spy's call history: a +// later file's spyOn() on the same method then starts with calls that file never made. +it("every test file that installs spyOn() spies restores them", async () => { + const root = join(import.meta.dir, ".."); + const offenders: string[] = []; + for await (const path of new Bun.Glob("**/*.test.ts").scan(root)) { + const file = join(root, path); + if (file === import.meta.path) continue; + const source = await Bun.file(file).text(); + if (source.includes("spyOn(") && !/mock\.restore\(\)|\.mockRestore\(\)/.test(source)) { + offenders.push(path); + } + } + expect(offenders).toEqual([]); +});