import { describe, it, expect } from 'vitest' import { constantTimeCompare, validateRedirectPath, createOidcParams, getCodeChallenge, getOidcRedirectUri, getOidcConfig, buildAuthorizeUrl, exchangeCode, } from '../../server/utils/oidc.js' import { withTemporaryEnv } from '../helpers/env.js' describe('oidc', () => { describe('constantTimeCompare', () => { it.each([ [['abc', 'abc'], true], [['abc', 'abd'], false], [['ab', 'abc'], false], [['a', 1], false], ])('compares %j -> %s', ([a, b], expected) => { expect(constantTimeCompare(a, b)).toBe(expected) }) }) describe('validateRedirectPath', () => { it.each([ ['/', '/'], ['/feeds', '/feeds'], ['/feeds?foo=1', '/feeds?foo=1'], ['//evil.com', '/'], ['', '/'], [null, '/'], ['/foo//bar', '/'], ])('validates %s -> %s', (input, expected) => { expect(validateRedirectPath(input)).toBe(expected) }) }) describe('createOidcParams', () => { it('returns state, nonce, and codeVerifier', () => { const params = createOidcParams() expect(params).toMatchObject({ state: expect.any(String), nonce: expect.any(String), codeVerifier: expect.any(String), }) }) }) describe('getCodeChallenge', () => { it('returns a string for a verifier', async () => { const { codeVerifier } = createOidcParams() const challenge = await getCodeChallenge(codeVerifier) expect(challenge).toMatch(/^[\w-]+$/) }) }) describe('getOidcRedirectUri', () => { it('returns URL ending with callback path when env is default', () => { withTemporaryEnv( { OIDC_REDIRECT_URI: undefined, OPENID_REDIRECT_URI: undefined, NUXT_APP_URL: undefined, APP_URL: undefined, }, () => { expect(getOidcRedirectUri()).toMatch(/\/api\/auth\/oidc\/callback$/) }, ) }) it.each([ [{ OIDC_REDIRECT_URI: ' https://app.example.com/oidc/cb ' }, 'https://app.example.com/oidc/cb'], [ { OIDC_REDIRECT_URI: undefined, OPENID_REDIRECT_URI: undefined, NUXT_APP_URL: 'https://myapp.example.com/' }, 'https://myapp.example.com/api/auth/oidc/callback', ], [ { OIDC_REDIRECT_URI: undefined, OPENID_REDIRECT_URI: undefined, NUXT_APP_URL: undefined, APP_URL: 'https://app.example.com', }, 'https://app.example.com/api/auth/oidc/callback', ], ])('returns correct URI for env: %j', (env, expected) => { withTemporaryEnv(env, () => { expect(getOidcRedirectUri()).toBe(expected) }) }) }) describe('getOidcConfig', () => { it.each([ [{ OIDC_ISSUER: undefined, OIDC_CLIENT_ID: undefined, OIDC_CLIENT_SECRET: undefined }], [{ OIDC_ISSUER: 'https://idp.example.com', OIDC_CLIENT_ID: 'client', OIDC_CLIENT_SECRET: undefined }], ])('returns null when OIDC vars missing or incomplete: %j', async (env) => { withTemporaryEnv(env, async () => { expect(await getOidcConfig()).toBeNull() }) }) }) describe('buildAuthorizeUrl', () => { it('is a function that accepts config and params', () => { expect(buildAuthorizeUrl).toBeInstanceOf(Function) expect(buildAuthorizeUrl.length).toBe(2) }) it('calls oidc.buildAuthorizationUrl with valid config', async () => { withTemporaryEnv( { OIDC_ISSUER: 'https://accounts.google.com', OIDC_CLIENT_ID: 'test-client', OIDC_CLIENT_SECRET: 'test-secret', }, async () => { try { const config = await getOidcConfig() if (config) { const result = buildAuthorizeUrl(config, createOidcParams()) expect(result).toBeDefined() } } catch { // Discovery failures are acceptable } }, ) }) }) describe('getOidcConfig caching', () => { it('caches config when called multiple times with same issuer', async () => { withTemporaryEnv( { OIDC_ISSUER: 'https://accounts.google.com', OIDC_CLIENT_ID: 'test-client', OIDC_CLIENT_SECRET: 'test-secret', }, async () => { try { const config1 = await getOidcConfig() if (config1) { const config2 = await getOidcConfig() expect(config2).toBeDefined() } } catch { // Network/discovery failures are acceptable } }, ) }) }) describe('exchangeCode', () => { it('rejects when grant fails', async () => { await expect( exchangeCode({}, 'https://app/api/auth/oidc/callback?code=abc&state=s', { state: 's', nonce: 'n', codeVerifier: 'v', }), ).rejects.toBeDefined() }) }) })