f1b0a53520
CI / Lint & Typecheck (push) Successful in 19s
CI / Test (push) Successful in 22s
CI / Lint & Typecheck (pull_request) Successful in 19s
CI / Test (pull_request) Successful in 20s
CI / Build & Push Docker Images (pull_request) Successful in 52s
CI / Build & Push Docker Images (push) Successful in 1m45s
AbortSignal.timeout(5000) in initAuth's discovery fetch races with vitest's 5000ms default test timeout, causing intermittent failures on CI push runs. Stub fetch to return ok:false instantly so tests complete in <1s. Co-Authored-By: Paperclip <noreply@paperclip.ing>
158 lines
4.4 KiB
TypeScript
158 lines
4.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
|
// Mutable state to control mock behavior per test
|
|
let dbSelectResult: unknown[] = [];
|
|
const mockEq = vi.fn((_col: unknown, _val: unknown) => ({ col: _col, val: _val }));
|
|
const mockDecryptSecret = vi.fn((s: string) => `decrypted:${s}`);
|
|
|
|
vi.mock("@groombook/db", () => {
|
|
const authProviderConfig = new Proxy(
|
|
{ _name: "auth_provider_config" },
|
|
{
|
|
get(target, prop) {
|
|
if (prop === "_name") return "auth_provider_config";
|
|
if (prop === "$inferSelect") return {};
|
|
return { table: "auth_provider_config", column: prop };
|
|
},
|
|
}
|
|
);
|
|
|
|
return {
|
|
getDb: () => ({
|
|
select: () => ({
|
|
from: () => ({
|
|
where: () => ({
|
|
limit: () => dbSelectResult,
|
|
[Symbol.iterator]: function* () {
|
|
for (const item of dbSelectResult) yield item;
|
|
},
|
|
0: dbSelectResult[0],
|
|
length: dbSelectResult.length,
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
authProviderConfig,
|
|
eq: mockEq,
|
|
decryptSecret: mockDecryptSecret,
|
|
};
|
|
});
|
|
|
|
async function reimportAuth() {
|
|
vi.resetModules();
|
|
vi.doMock("@groombook/db", () => ({
|
|
getDb: () => ({
|
|
select: () => ({
|
|
from: () => ({
|
|
where: () => ({
|
|
limit: () => dbSelectResult,
|
|
[Symbol.iterator]: function* () {
|
|
for (const item of dbSelectResult) yield item;
|
|
},
|
|
0: dbSelectResult[0],
|
|
length: dbSelectResult.length,
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
authProviderConfig: {},
|
|
eq: mockEq,
|
|
decryptSecret: mockDecryptSecret,
|
|
}));
|
|
const mod = await import("../lib/auth.js");
|
|
return mod;
|
|
}
|
|
|
|
describe("auth init", () => {
|
|
const originalEnv = { ...process.env };
|
|
|
|
beforeEach(() => {
|
|
dbSelectResult = [];
|
|
vi.clearAllMocks();
|
|
// Stub fetch so OIDC discovery requests resolve instantly during tests.
|
|
// Without this, AbortSignal.timeout(5000) in auth.ts races with vitest's
|
|
// 5000ms default test timeout and causes flaky failures.
|
|
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 503 }));
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
process.env = { ...originalEnv };
|
|
});
|
|
|
|
it("falls back to env vars when DB returns empty", async () => {
|
|
process.env = {
|
|
...originalEnv,
|
|
OIDC_ISSUER: "https://issuer.example.com",
|
|
OIDC_CLIENT_ID: "test-client-id",
|
|
OIDC_CLIENT_SECRET: "test-client-secret",
|
|
BETTER_AUTH_SECRET: "test-secret",
|
|
BETTER_AUTH_URL: "http://localhost:3000",
|
|
NODE_ENV: "test",
|
|
};
|
|
|
|
const { initAuth, getAuth } = await reimportAuth();
|
|
await initAuth();
|
|
expect(getAuth()).toBeDefined();
|
|
});
|
|
|
|
it("uses DB config and decrypts clientSecret when DB has enabled provider", async () => {
|
|
const dbConfig = {
|
|
id: "config-id",
|
|
providerId: "okta",
|
|
displayName: "Okta",
|
|
issuerUrl: "https://okta.example.com",
|
|
internalBaseUrl: null,
|
|
clientId: "okta-client-id",
|
|
clientSecret: "encrypted:okta-secret",
|
|
scopes: "openid profile email",
|
|
enabled: true,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
dbSelectResult = [dbConfig];
|
|
|
|
process.env = {
|
|
...originalEnv,
|
|
BETTER_AUTH_SECRET: "test-secret",
|
|
BETTER_AUTH_URL: "http://localhost:3000",
|
|
NODE_ENV: "test",
|
|
};
|
|
|
|
const { initAuth, getAuth } = await reimportAuth();
|
|
await initAuth();
|
|
expect(getAuth()).toBeDefined();
|
|
expect(mockDecryptSecret).toHaveBeenCalledWith("encrypted:okta-secret");
|
|
});
|
|
|
|
it("throws when BETTER_AUTH_SECRET is missing and AUTH_DISABLED is not set", async () => {
|
|
process.env = {
|
|
...originalEnv,
|
|
OIDC_ISSUER: "",
|
|
OIDC_CLIENT_ID: "",
|
|
OIDC_CLIENT_SECRET: "",
|
|
NODE_ENV: "test",
|
|
};
|
|
delete process.env.BETTER_AUTH_SECRET;
|
|
delete process.env.AUTH_DISABLED;
|
|
|
|
const { initAuth } = await reimportAuth();
|
|
await expect(initAuth()).rejects.toThrow(
|
|
"[FATAL] BETTER_AUTH_SECRET environment variable is required when auth is enabled"
|
|
);
|
|
});
|
|
|
|
it("builds placeholder auth when AUTH_DISABLED=true without throwing", async () => {
|
|
process.env = {
|
|
...originalEnv,
|
|
AUTH_DISABLED: "true",
|
|
NODE_ENV: "test",
|
|
};
|
|
delete process.env.BETTER_AUTH_SECRET;
|
|
|
|
const { initAuth, getAuth } = await reimportAuth();
|
|
await expect(initAuth()).resolves.toBeUndefined();
|
|
expect(getAuth()).toBeDefined();
|
|
});
|
|
});
|