/** * Which social providers get registered, and when. * * The rule that matters is "both keys or neither". With one key set, * better-auth still registers the provider and /sign-in/social gets as far as * the OAuth URL builder before throwing — a 500 on an environment that is merely * unconfigured. Omitting it makes the same click a clean 404 PROVIDER_NOT_FOUND, * which the button turns into "not set up yet" rather than "try again". * * Env is swapped per case and the module re-imported, because `lib/auth.ts` * reads `process.env` once at module scope — which is exactly the behaviour * being pinned here. */ import { config } from 'dotenv'; import { afterEach, describe, expect, it, vi } from 'vitest'; config({ path: '../../.env' }); const KEYS = [ 'AUTH_GOOGLE_ID', 'AUTH_GOOGLE_SECRET', 'AUTH_MICROSOFT_ID', 'AUTH_MICROSOFT_SECRET', 'AUTH_MICROSOFT_TENANT_ID', 'AUTH_GITHUB_ID', 'AUTH_GITHUB_SECRET', ] as const; const original = Object.fromEntries(KEYS.map((k) => [k, process.env[k]])); async function providersWith(env: Partial>) { for (const key of KEYS) { const value = env[key]; if (value === undefined) delete process.env[key]; else process.env[key] = value; } vi.resetModules(); const { auth } = await import('@/lib/auth'); return (auth.options.socialProviders ?? {}) as Record; } afterEach(() => { for (const key of KEYS) { const value = original[key]; if (value === undefined) delete process.env[key]; else process.env[key] = value; } vi.resetModules(); }); describe('social provider registration', () => { it('registers each provider when both of its keys are present', async () => { const providers = await providersWith({ AUTH_GOOGLE_ID: 'g-id', AUTH_GOOGLE_SECRET: 'g-secret', AUTH_MICROSOFT_ID: 'm-id', AUTH_MICROSOFT_SECRET: 'm-secret', AUTH_GITHUB_ID: 'gh-id', AUTH_GITHUB_SECRET: 'gh-secret', }); expect(Object.keys(providers).sort()).toEqual(['github', 'google', 'microsoft']); }); it('treats a half-set pair as unset rather than half-registering it', async () => { const providers = await providersWith({ AUTH_GOOGLE_ID: 'g-id', AUTH_GOOGLE_SECRET: undefined, AUTH_MICROSOFT_ID: undefined, AUTH_MICROSOFT_SECRET: 'm-secret', AUTH_GITHUB_ID: 'gh-id', AUTH_GITHUB_SECRET: undefined, }); expect(providers.google).toBeUndefined(); expect(providers.microsoft).toBeUndefined(); expect(providers.github).toBeUndefined(); }); it('leaves phone OTP as the only route when nothing is configured', async () => { const providers = await providersWith({}); expect(Object.keys(providers)).toEqual([]); }); it('registers them independently — one missing does not take the others down', async () => { const providers = await providersWith({ AUTH_GOOGLE_ID: 'g-id', AUTH_GOOGLE_SECRET: 'g-secret', }); expect(providers.google).toBeDefined(); expect(providers.microsoft).toBeUndefined(); expect(providers.github).toBeUndefined(); }); it('defaults Microsoft to the multi-tenant endpoint', async () => { // `common` is work, school AND personal accounts. A consumer marketplace // that silently defaulted to a single tenant would turn away every customer // who is not in that organisation. const providers = await providersWith({ AUTH_MICROSOFT_ID: 'm-id', AUTH_MICROSOFT_SECRET: 'm-secret', AUTH_MICROSOFT_TENANT_ID: undefined, }); expect(providers.microsoft?.tenantId).toBe('common'); }); it('honours an explicit tenant so a single-organisation deploy can lock down', async () => { const providers = await providersWith({ AUTH_MICROSOFT_ID: 'm-id', AUTH_MICROSOFT_SECRET: 'm-secret', AUTH_MICROSOFT_TENANT_ID: '00000000-0000-0000-0000-000000000000', }); expect(providers.microsoft?.tenantId).toBe('00000000-0000-0000-0000-000000000000'); }); });