import { readFileSync } from "node:fs"; import { describe, expect, it, vi } from "vitest"; import { createAuth } from "../src/auth"; import { booleanEnv, csvEnv, isTrustedOrigin, optionalEnv, requiredEnv, trustedOrigins, type Env, } from "../src/env"; import { buildAuthEmail, sendEmail } from "../src/email"; import { createGenericOAuthProviders, createSocialProviders } from "../src/oauth"; import { authPerformanceConfig } from "../src/performance"; import { createApiKeyConfigurations, createAuthPlugins, getApiKeyFromRequestHeaders, } from "../src/plugins"; import { buildAuthSms, sendSms } from "../src/sms"; const env: Env = { BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: " http://localhost:8787, https://app.example.com ", }; describe("auth env helpers", () => { it("parses trusted origins from comma-separated env", () => { expect(trustedOrigins(env)).toEqual(["http://localhost:8787", "https://app.example.com"]); }); it("allows loopback trusted origins to match any port", () => { const trusted = ["http://localhost", "http://127.0.0.1", "https://app.example.com"]; expect(isTrustedOrigin("http://localhost:5173", trusted)).toBe(true); expect(isTrustedOrigin("http://localhost:9999", trusted)).toBe(true); expect(isTrustedOrigin("http://127.0.0.1:5173", trusted)).toBe(true); expect(isTrustedOrigin("https://app.example.com", trusted)).toBe(true); }); it("does not allow non-loopback trusted origins to match arbitrary ports", () => { const trusted = ["https://app.example.com"]; expect(isTrustedOrigin("https://app.example.com:8443", trusted)).toBe(false); expect(isTrustedOrigin("https://evil.example.com", trusted)).toBe(false); }); it("adds a trusted dynamic loopback origin for request-scoped auth config", () => { expect( trustedOrigins( { ...env, TRUSTED_ORIGINS: "http://localhost,https://app.example.com", }, "http://localhost:5174", ), ).toContain("http://localhost:5174"); }); it("adds Expo app schemes to trusted origins", () => { expect(trustedOrigins({ ...env, EXPO_SCHEME: "cfwauth" })).toEqual([ "http://localhost:8787", "https://app.example.com", "cfwauth://", ]); }); it("parses optional csv values", () => { expect(csvEnv(" google, github ,, ")).toEqual(["google", "github"]); }); it("parses boolean env values", () => { expect(booleanEnv("true")).toBe(true); expect(booleanEnv("1")).toBe(true); expect(booleanEnv("false")).toBe(false); expect(booleanEnv(undefined)).toBe(false); }); it("throws for missing required env", () => { expect(() => requiredEnv({}, "MAIL_FROM")).toThrow("Missing required env: MAIL_FROM"); }); it("returns optional env values", () => { expect(optionalEnv({ MAIL_FROM: "noreply@example.com" }, "MAIL_FROM")).toBe( "noreply@example.com", ); expect(optionalEnv({}, "MAIL_FROM")).toBeUndefined(); }); }); describe("Autumn registration hook", () => { it("schedules Autumn sync after Better Auth creates a user", async () => { const waitUntil = vi.fn(); const auth = createAuth( { ...env, AUTUMN_SECRET_KEY: "autumn-secret", AUTUMN_FREE_PLAN_ID: "free", }, { waitUntil, }, ); const hook = ( auth as unknown as { options: { databaseHooks?: { user?: { create?: { after?: ( user: { id: string; email?: string | null; name?: string | null }, context?: unknown, ) => Promise; }; }; }; }; } ).options.databaseHooks?.user?.create?.after; expect(hook).toEqual(expect.any(Function)); await hook?.( { id: "user_123", email: "user@example.com", name: "Example User", }, undefined, ); expect(waitUntil).toHaveBeenCalledTimes(1); expect(waitUntil.mock.calls[0]?.[0]).toBeInstanceOf(Promise); }); }); describe("production auth config", () => { it("allows the Web Shell origin in Wrangler trusted origins", () => { const config = JSON.parse(readFileSync("wrangler.jsonc", "utf8")) as { vars: { TRUSTED_ORIGINS: string }; }; expect(csvEnv(config.vars.TRUSTED_ORIGINS)).toContain("https://web.bowong.cc"); }); it("allows gateway and local Web Shell origins in Wrangler trusted origins", () => { const config = JSON.parse(readFileSync("wrangler.jsonc", "utf8")) as { vars: { TRUSTED_ORIGINS: string }; }; expect(csvEnv(config.vars.TRUSTED_ORIGINS)).toEqual( expect.arrayContaining([ "https://cfw-gateway.bowong.cc", "http://localhost", "http://127.0.0.1", ]), ); }); }); describe("auth performance config", () => { it("uses conservative defaults", () => { expect(authPerformanceConfig(env)).toEqual({ sessionCookieCacheMaxAge: 300, apiKeyDeferUpdates: true, }); }); it("parses session cookie cache max age from env", () => { expect( authPerformanceConfig({ ...env, BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE: "60", }).sessionCookieCacheMaxAge, ).toBe(60); }); it("falls back for invalid session cookie cache max age", () => { expect( authPerformanceConfig({ ...env, BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE: "0", }).sessionCookieCacheMaxAge, ).toBe(300); expect( authPerformanceConfig({ ...env, BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE: "not-a-number", }).sessionCookieCacheMaxAge, ).toBe(300); }); it("allows disabling api key deferred updates", () => { expect(authPerformanceConfig({ ...env, API_KEY_DEFER_UPDATES: "false" })).toMatchObject({ apiKeyDeferUpdates: false, }); expect(authPerformanceConfig({ ...env, API_KEY_DEFER_UPDATES: "true" })).toMatchObject({ apiKeyDeferUpdates: true, }); }); }); describe("auth email adapter", () => { it("builds a verification email", () => { const message = buildAuthEmail({ kind: "verify-email", to: "user@example.com", url: "https://auth.example.com/verify", }); expect(message.subject).toBe("Verify your email"); expect(message.to).toBe("user@example.com"); expect(message.text).toContain("https://auth.example.com/verify"); }); it("fails clearly when no mail provider is configured", async () => { await expect( sendEmail( { BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", }, { to: "user@example.com", subject: "Test", text: "Test", }, ), ).rejects.toThrow("MAIL_PROVIDER is not configured"); }); }); describe("auth sms adapter", () => { it("builds a phone verification sms", () => { const message = buildAuthSms({ kind: "phone-otp", to: "+15555550100", code: "123456", }); expect(message.to).toBe("+15555550100"); expect(message.text).toContain("123456"); }); it("fails clearly when no sms provider is configured", async () => { await expect(sendSms(env, { to: "+15555550100", text: "123456" })).rejects.toThrow( "SMS_PROVIDER is not configured", ); }); }); describe("auth plugins", () => { it("does not enable captcha without provider config", () => { const plugins = createAuthPlugins({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", }); expect(plugins.map((plugin) => plugin.id)).not.toContain("captcha"); }); it("enables captcha when Cloudflare Turnstile config is present", () => { const plugins = createAuthPlugins({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", CAPTCHA_PROVIDER: "cloudflare-turnstile", CAPTCHA_SECRET_KEY: "secret", }); expect(plugins.map((plugin) => plugin.id)).toContain("captcha"); }); it("enables email otp plugin by default", () => { const plugins = createAuthPlugins({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", }); expect(plugins.map((plugin) => plugin.id)).toContain("email-otp"); }); it("enables i18n plugin by default", () => { const plugins = createAuthPlugins({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", }); expect(plugins.map((plugin) => plugin.id)).toContain("i18n"); }); it("enables account security and admin plugins", () => { const plugins = createAuthPlugins({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", }); expect(plugins.map((plugin) => plugin.id)).toEqual( expect.arrayContaining(["two-factor", "multi-session", "last-login-method", "admin"]), ); }); it("enables account center plugins by default", () => { const plugins = createAuthPlugins({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", EXPO_SCHEME: "cfwauth", }); expect(plugins.map((plugin) => plugin.id)).toEqual( expect.arrayContaining(["username", "phone-number", "organization", "api-key", "expo"]), ); }); it("enables jwt and bearer only when requested", () => { const disabled = createAuthPlugins({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", }); expect(disabled.map((plugin) => plugin.id)).not.toContain("jwt"); expect(disabled.map((plugin) => plugin.id)).not.toContain("bearer"); const enabled = createAuthPlugins({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", ENABLE_JWT: "true", ENABLE_BEARER: "true", }); expect(enabled.map((plugin) => plugin.id)).toEqual(expect.arrayContaining(["jwt", "bearer"])); }); it("enables api key deferred updates by default", () => { const configurations = createApiKeyConfigurations({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", }); expect(configurations).toEqual( expect.arrayContaining([ expect.objectContaining({ configId: "default", deferUpdates: true, }), expect.objectContaining({ configId: "organization", deferUpdates: true, }), ]), ); }); it("enables API keys as get-session credentials only from x-api-key", () => { const configurations = createApiKeyConfigurations({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", }); expect(configurations).toEqual( expect.arrayContaining([ expect.objectContaining({ configId: "default", enableSessionForAPIKeys: true, customAPIKeyGetter: expect.any(Function), }), ]), ); expect(getApiKeyFromRequestHeaders(new Headers({ "x-api-key": "cfw_secret" }))).toBe( "cfw_secret", ); expect( getApiKeyFromRequestHeaders(new Headers({ authorization: "Bearer cfw_secret" })), ).toBeNull(); expect( getApiKeyFromRequestHeaders(new Headers({ authorization: "Bearer better-auth-session" })), ).toBeNull(); }); it("allows disabling api key deferred updates", () => { const configurations = createApiKeyConfigurations({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", API_KEY_DEFER_UPDATES: "false", }); expect(configurations).toEqual( expect.arrayContaining([ expect.objectContaining({ configId: "default", deferUpdates: false, }), expect.objectContaining({ configId: "organization", deferUpdates: false, }), ]), ); }); }); describe("auth runtime performance options", () => { it("enables session cookie cache with configured max age", () => { const auth = createAuth({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", BETTER_AUTH_SESSION_COOKIE_CACHE_MAX_AGE: "60", }); expect(auth.options.session?.cookieCache).toEqual({ enabled: true, maxAge: 60, }); }); }); describe("oauth config", () => { it("creates built-in social providers from env", () => { const providers = createSocialProviders({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", GOOGLE_CLIENT_ID: "google-id", GOOGLE_CLIENT_SECRET: "google-secret", GITHUB_CLIENT_ID: "github-id", GITHUB_CLIENT_SECRET: "github-secret", }); expect(Object.keys(providers)).toEqual(["google", "github"]); }); it("creates generic oauth providers from env", () => { const providers = createGenericOAuthProviders({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", GENERIC_OAUTH_PROVIDER_ID: "workos", GENERIC_OAUTH_DISCOVERY_URL: "https://example.com/.well-known/openid-configuration", GENERIC_OAUTH_CLIENT_ID: "client-id", GENERIC_OAUTH_CLIENT_SECRET: "client-secret", }); expect(providers).toEqual([ { providerId: "workos", discoveryUrl: "https://example.com/.well-known/openid-configuration", clientId: "client-id", clientSecret: "client-secret", scopes: ["openid", "email", "profile"], }, ]); }); }); describe("passkey config", () => { it("enables passkey when rp config is present", () => { const plugins = createAuthPlugins({ BETTER_AUTH_URL: "http://localhost:8788", TRUSTED_ORIGINS: "http://localhost:8787", PASSKEY_RP_ID: "localhost", PASSKEY_RP_NAME: "cfw-auth", PASSKEY_ORIGIN: "http://localhost:8788", }); expect(plugins.map((plugin) => plugin.id)).toContain("passkey"); }); }); describe("Better Auth migration config", () => { it("exports a static auth instance for the Better Auth CLI", async () => { const module = await import("../src/auth.migration"); expect(module.auth).toBeDefined(); expect(module.default).toBe(module.auth); expect(typeof module.auth.handler).toBe("function"); }); it("keeps schema-affecting plugins enabled for generated migrations", async () => { const { migrationPlugins } = await import("../src/auth.migration"); const pluginIds = migrationPlugins.map((plugin) => plugin.id); expect(pluginIds).toEqual( expect.arrayContaining([ "open-api", "i18n", "email-otp", "two-factor", "multi-session", "last-login-method", "admin", "captcha", "generic-oauth", "passkey", "username", "phone-number", "organization", "api-key", "expo", "jwt", "bearer", ]), ); }); });