import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ sqlite: null as { exec: (sql: string) => void; prepare: (sql: string) => { all: () => Array>; run: (...values: unknown[]) => unknown; }; } | null, saveDatabase: vi.fn().mockResolvedValue(undefined), })); vi.mock("../../database/db/index.js", async () => { const { default: Database } = await import("better-sqlite3"); const { drizzle } = await import("drizzle-orm/better-sqlite3"); const sqlite = new Database(":memory:"); mocks.sqlite = sqlite; const db = drizzle(sqlite); return { db, getDb: () => db, saveMemoryDatabaseToFile: mocks.saveDatabase, }; }); vi.mock("../../utils/user-keys.js", () => ({ UserKeyManager: { getInstance: () => ({ invalidate: vi.fn(), tryGetUserDEK: vi.fn(() => null), hasUserDEK: vi.fn(() => true), }), }, })); vi.mock("../../utils/system-crypto.js", () => ({ SystemCrypto: { getInstance: () => ({}) }, })); vi.mock("../../utils/data-crypto.js", () => ({ DataCrypto: { getInstance: () => ({}) }, })); vi.mock("../../utils/logger.js", () => ({ authLogger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), success: vi.fn(), }, databaseLogger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), success: vi.fn(), }, })); const { AuthManager } = await import("../../utils/auth-manager.js"); const authManager = AuthManager.getInstance(); type SessionInput = { id: string; userId: string; sub: string; sid: string | null; providerId: number | null; }; function insertSession({ id, userId, sub, sid, providerId }: SessionInput) { mocks .sqlite!.prepare( `INSERT INTO sessions ( id, user_id, jwt_token, device_type, device_info, oidc_sub, oidc_sid, sso_provider_id, created_at, expires_at, last_active_at ) VALUES (?, ?, ?, 'browser', 'test', ?, ?, ?, ?, ?, ?)`, ) .run( id, userId, `token-${id}`, sub, sid, providerId, "2026-07-10T00:00:00.000Z", "2026-07-11T00:00:00.000Z", "2026-07-10T00:00:00.000Z", ); } function sessionIds(): string[] { return mocks .sqlite!.prepare("SELECT id FROM sessions ORDER BY id") .all() .map((row) => String(row.id)); } describe("AuthManager.revokeSessionsByOidc", () => { beforeAll(() => { mocks.sqlite!.exec(` CREATE TABLE sessions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, jwt_token TEXT NOT NULL, device_type TEXT NOT NULL, device_info TEXT NOT NULL, oidc_sub TEXT, oidc_sid TEXT, sso_provider_id INTEGER, created_at TEXT NOT NULL, expires_at TEXT NOT NULL, last_active_at TEXT NOT NULL ) `); }); beforeEach(() => { mocks.sqlite!.exec("DELETE FROM sessions"); mocks.saveDatabase.mockReset().mockResolvedValue(undefined); }); it("revokes only the matching provider session when sid is present", async () => { insertSession({ id: "matching", userId: "user-1", sub: "subject-1", sid: "session-1", providerId: 7, }); insertSession({ id: "other-provider", userId: "user-1", sub: "subject-1", sid: "session-1", providerId: 8, }); insertSession({ id: "other-session", userId: "user-1", sub: "subject-1", sid: "session-2", providerId: 7, }); await expect( authManager.revokeSessionsByOidc({ ssoProviderId: 7, sub: "subject-1", sid: "session-1", }), ).resolves.toBe(1); expect(sessionIds()).toEqual(["other-provider", "other-session"]); }); it("revokes all provider sessions for a subject when sid is absent", async () => { insertSession({ id: "first", userId: "user-1", sub: "subject-1", sid: "session-1", providerId: 7, }); insertSession({ id: "second", userId: "user-1", sub: "subject-1", sid: "session-2", providerId: 7, }); insertSession({ id: "other-subject", userId: "user-2", sub: "subject-2", sid: null, providerId: 7, }); await expect( authManager.revokeSessionsByOidc({ ssoProviderId: 7, sub: "subject-1", }), ).resolves.toBe(2); expect(sessionIds()).toEqual(["other-subject"]); }); it("does not persist when no session matches", async () => { await expect( authManager.revokeSessionsByOidc({ ssoProviderId: 7, sid: "missing", }), ).resolves.toBe(0); expect(mocks.saveDatabase).not.toHaveBeenCalled(); }); it("propagates persistence failures so the provider can retry", async () => { insertSession({ id: "matching", userId: "user-1", sub: "subject-1", sid: "session-1", providerId: 7, }); mocks.saveDatabase.mockRejectedValueOnce(new Error("disk full")); await expect( authManager.revokeSessionsByOidc({ ssoProviderId: 7, sid: "session-1", }), ).rejects.toThrow("disk full"); }); });