import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { constantTimeCompare, validateRedirectPath, createOidcParams, getCodeChallenge, getOidcRedirectUri, getOidcConfig, buildAuthorizeUrl, exchangeCode, } from '../../server/utils/oidc.js' describe('oidc', () => { describe('constantTimeCompare', () => { it('returns true for equal strings', () => { expect(constantTimeCompare('abc', 'abc')).toBe(true) }) it('returns false for different strings', () => { expect(constantTimeCompare('abc', 'abd')).toBe(false) }) it('returns false for different length', () => { expect(constantTimeCompare('ab', 'abc')).toBe(false) }) it('returns false for non-strings', () => { expect(constantTimeCompare('a', 1)).toBe(false) }) }) describe('validateRedirectPath', () => { it('returns path for valid same-origin path', () => { expect(validateRedirectPath('/')).toBe('/') expect(validateRedirectPath('/feeds')).toBe('/feeds') expect(validateRedirectPath('/feeds?foo=1')).toBe('/feeds?foo=1') }) it('returns / for path starting with //', () => { expect(validateRedirectPath('//evil.com')).toBe('/') }) it('returns / for non-string or empty', () => { expect(validateRedirectPath('')).toBe('/') expect(validateRedirectPath(null)).toBe('/') }) it('returns / for path containing //', () => { expect(validateRedirectPath('/foo//bar')).toBe('/') }) }) describe('createOidcParams', () => { it('returns state, nonce, and codeVerifier', () => { const p = createOidcParams() expect(p).toHaveProperty('state') expect(p).toHaveProperty('nonce') expect(p).toHaveProperty('codeVerifier') expect(typeof p.state).toBe('string') expect(typeof p.nonce).toBe('string') expect(typeof p.codeVerifier).toBe('string') }) }) describe('getCodeChallenge', () => { it('returns a string for a verifier', async () => { const p = createOidcParams() const challenge = await getCodeChallenge(p.codeVerifier) expect(typeof challenge).toBe('string') expect(challenge.length).toBeGreaterThan(0) }) }) describe('getOidcRedirectUri', () => { const origEnv = process.env afterEach(() => { process.env = origEnv }) it('returns a URL ending with callback path when env is default', () => { delete process.env.OIDC_REDIRECT_URI delete process.env.OPENID_REDIRECT_URI delete process.env.NUXT_APP_URL delete process.env.APP_URL const uri = getOidcRedirectUri() expect(uri).toMatch(/\/api\/auth\/oidc\/callback$/) }) it('returns explicit OIDC_REDIRECT_URI when set', () => { process.env.OIDC_REDIRECT_URI = ' https://app.example.com/oidc/cb ' const uri = getOidcRedirectUri() expect(uri).toBe('https://app.example.com/oidc/cb') }) it('returns URL from NUXT_APP_URL when set and no explicit redirect', () => { delete process.env.OIDC_REDIRECT_URI delete process.env.OPENID_REDIRECT_URI process.env.NUXT_APP_URL = 'https://myapp.example.com/' const uri = getOidcRedirectUri() expect(uri).toBe('https://myapp.example.com/api/auth/oidc/callback') }) it('returns URL from APP_URL when set and no NUXT_APP_URL', () => { delete process.env.OIDC_REDIRECT_URI delete process.env.OPENID_REDIRECT_URI delete process.env.NUXT_APP_URL process.env.APP_URL = 'https://app.example.com' const uri = getOidcRedirectUri() expect(uri).toBe('https://app.example.com/api/auth/oidc/callback') }) }) describe('getOidcConfig', () => { const origEnv = process.env beforeEach(() => { process.env = { ...origEnv } }) afterEach(() => { process.env = origEnv }) it('returns null when OIDC env vars missing', async () => { delete process.env.OIDC_ISSUER delete process.env.OIDC_CLIENT_ID delete process.env.OIDC_CLIENT_SECRET const config = await getOidcConfig() expect(config).toBeNull() }) it('returns null when only some OIDC env vars set', async () => { process.env.OIDC_ISSUER = 'https://idp.example.com' process.env.OIDC_CLIENT_ID = 'client' delete process.env.OIDC_CLIENT_SECRET const config = await getOidcConfig() expect(config).toBeNull() delete process.env.OIDC_ISSUER delete process.env.OIDC_CLIENT_ID }) }) describe('buildAuthorizeUrl', () => { it('is a function that accepts config and params', () => { expect(typeof buildAuthorizeUrl).toBe('function') expect(buildAuthorizeUrl.length).toBe(2) }) }) describe('exchangeCode', () => { it('rejects when grant fails', async () => { const config = {} const currentUrl = 'https://app/api/auth/oidc/callback?code=abc&state=s' const checks = { state: 's', nonce: 'n', codeVerifier: 'v' } await expect(exchangeCode(config, currentUrl, checks)).rejects.toBeDefined() }) }) })