Files
api/src/__tests__/auth.test.ts
T
Chris Farhood abac9dfe6c Extract groombook/api from monorepo with CI workflow
- Add source code from apps/api
- Add packages/db and packages/types workspace dependencies
- Add GitHub Actions CI workflow (lint, typecheck, test, docker)
- Generate pnpm-lock.yaml
- Add .gitignore

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-11 01:26:56 +00:00

153 lines
4.1 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();
});
afterEach(() => {
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();
});
});