import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { buildOAuthProviders } from './auth.js'; describe('buildOAuthProviders', () => { const originalEnv = process.env; beforeEach(() => { process.env = { ...originalEnv }; delete process.env['AUTHENTIK_CLIENT_ID']; delete process.env['AUTHENTIK_CLIENT_SECRET']; delete process.env['AUTHENTIK_ISSUER']; delete process.env['WORKOS_CLIENT_ID']; delete process.env['WORKOS_CLIENT_SECRET']; delete process.env['WORKOS_ISSUER']; delete process.env['KEYCLOAK_CLIENT_ID']; delete process.env['KEYCLOAK_CLIENT_SECRET']; delete process.env['KEYCLOAK_ISSUER']; delete process.env['KEYCLOAK_URL']; delete process.env['KEYCLOAK_REALM']; }); afterEach(() => { process.env = originalEnv; }); it('returns empty array when no SSO env vars are set', () => { const providers = buildOAuthProviders(); expect(providers).toHaveLength(0); }); describe('WorkOS', () => { it('includes workos provider when all required env vars are set', () => { process.env['WORKOS_CLIENT_ID'] = 'client_test123'; process.env['WORKOS_CLIENT_SECRET'] = 'sk_live_test'; process.env['WORKOS_ISSUER'] = 'https://example.authkit.app/'; const providers = buildOAuthProviders(); const workos = providers.find((p) => p.providerId === 'workos'); expect(workos).toBeDefined(); expect(workos?.clientId).toBe('client_test123'); expect(workos?.issuer).toBe('https://example.authkit.app'); expect(workos?.discoveryUrl).toBe( 'https://example.authkit.app/.well-known/openid-configuration', ); expect(workos?.scopes).toEqual(['openid', 'email', 'profile']); }); it('throws when WorkOS is partially configured', () => { process.env['WORKOS_CLIENT_ID'] = 'client_test123'; expect(() => buildOAuthProviders()).toThrow( '@mosaicstack/auth: WorkOS SSO requires WORKOS_ISSUER, WORKOS_CLIENT_ID, WORKOS_CLIENT_SECRET.', ); }); it('excludes workos provider when WorkOS is not configured', () => { const providers = buildOAuthProviders(); const workos = providers.find((p) => p.providerId === 'workos'); expect(workos).toBeUndefined(); }); }); describe('Keycloak', () => { it('includes keycloak provider when KEYCLOAK_ISSUER is set', () => { process.env['KEYCLOAK_CLIENT_ID'] = 'mosaic'; process.env['KEYCLOAK_CLIENT_SECRET'] = 'secret123'; process.env['KEYCLOAK_ISSUER'] = 'https://auth.example.com/realms/myrealm/'; const providers = buildOAuthProviders(); const keycloakProvider = providers.find((p) => p.providerId === 'keycloak'); expect(keycloakProvider).toBeDefined(); expect(keycloakProvider?.clientId).toBe('mosaic'); expect(keycloakProvider?.discoveryUrl).toBe( 'https://auth.example.com/realms/myrealm/.well-known/openid-configuration', ); expect(keycloakProvider?.scopes).toEqual(['openid', 'email', 'profile']); }); it('supports deriving the Keycloak issuer from KEYCLOAK_URL and KEYCLOAK_REALM', () => { process.env['KEYCLOAK_CLIENT_ID'] = 'mosaic'; process.env['KEYCLOAK_CLIENT_SECRET'] = 'secret123'; process.env['KEYCLOAK_URL'] = 'https://auth.example.com/'; process.env['KEYCLOAK_REALM'] = 'myrealm'; const providers = buildOAuthProviders(); const keycloakProvider = providers.find((p) => p.providerId === 'keycloak'); expect(keycloakProvider?.discoveryUrl).toBe( 'https://auth.example.com/realms/myrealm/.well-known/openid-configuration', ); }); it('throws when Keycloak is partially configured', () => { process.env['KEYCLOAK_CLIENT_ID'] = 'mosaic'; process.env['KEYCLOAK_CLIENT_SECRET'] = 'secret123'; expect(() => buildOAuthProviders()).toThrow( '@mosaicstack/auth: Keycloak SSO requires KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET, KEYCLOAK_ISSUER.', ); }); it('excludes keycloak provider when Keycloak is not configured', () => { const providers = buildOAuthProviders(); const keycloakProvider = providers.find((p) => p.providerId === 'keycloak'); expect(keycloakProvider).toBeUndefined(); }); }); describe('Authentik', () => { it('includes authentik provider when all required env vars are set', () => { process.env['AUTHENTIK_CLIENT_ID'] = 'authentik-client'; process.env['AUTHENTIK_CLIENT_SECRET'] = 'authentik-secret'; process.env['AUTHENTIK_ISSUER'] = 'https://auth.example.com/application/o/mosaic/'; const providers = buildOAuthProviders(); const authentik = providers.find((p) => p.providerId === 'authentik'); expect(authentik).toBeDefined(); expect(authentik?.clientId).toBe('authentik-client'); expect(authentik?.issuer).toBe('https://auth.example.com/application/o/mosaic'); expect(authentik?.discoveryUrl).toBe( 'https://auth.example.com/application/o/mosaic/.well-known/openid-configuration', ); }); it('throws when Authentik is partially configured', () => { process.env['AUTHENTIK_CLIENT_ID'] = 'authentik-client'; expect(() => buildOAuthProviders()).toThrow( '@mosaicstack/auth: Authentik SSO requires AUTHENTIK_ISSUER, AUTHENTIK_CLIENT_ID, AUTHENTIK_CLIENT_SECRET.', ); }); it('excludes authentik provider when Authentik is not configured', () => { const providers = buildOAuthProviders(); const authentik = providers.find((p) => p.providerId === 'authentik'); expect(authentik).toBeUndefined(); }); }); it('registers all three providers when all env vars are set', () => { process.env['AUTHENTIK_CLIENT_ID'] = 'a-id'; process.env['AUTHENTIK_CLIENT_SECRET'] = 'a-secret'; process.env['AUTHENTIK_ISSUER'] = 'https://auth.example.com/application/o/mosaic'; process.env['WORKOS_CLIENT_ID'] = 'w-id'; process.env['WORKOS_CLIENT_SECRET'] = 'w-secret'; process.env['WORKOS_ISSUER'] = 'https://example.authkit.app'; process.env['KEYCLOAK_CLIENT_ID'] = 'k-id'; process.env['KEYCLOAK_CLIENT_SECRET'] = 'k-secret'; process.env['KEYCLOAK_ISSUER'] = 'https://kc.example.com/realms/test'; const providers = buildOAuthProviders(); expect(providers).toHaveLength(3); const ids = providers.map((p) => p.providerId); expect(ids).toContain('authentik'); expect(ids).toContain('workos'); expect(ids).toContain('keycloak'); }); });