tests/confirmation-url.unit.test.ts
Rama main · réplica de lectura del repositorio original.
import { describe, expect, it } from 'vitest'
import { parseQuery } from 'vue-router'
import { resolveConfirmationUrl } from '../src/utils/safeRedirect.ts'
const options = { allowedHosts: ['console.capgo.app', 'sb.capgo.app'] }
const verificationUrl = 'https://sb.capgo.app/auth/v1/verify?token=recovery-regression'
const redirectTo = 'https://console.capgo.app/forgot_password?step=2'
describe('resolveConfirmationUrl', () => {
it.concurrent('restores the misplaced parameters in the delivered recovery email', () => {
const query = parseQuery(`confirmation_url=${encodeURIComponent(verificationUrl)}&type=recovery&redirect_to=${encodeURIComponent(redirectTo)}&utm_source=bento&bento_uuid=tracking-regression`)
const resolved = resolveConfirmationUrl(query, options)
const target = new URL(resolved!)
expect(Object.fromEntries(target.searchParams)).toEqual({
token: 'recovery-regression',
type: 'recovery',
redirect_to: redirectTo,
})
})
it.concurrent('preserves encoding inside a complete verification URL', () => {
const nestedRedirect = `${redirectTo}&next=${encodeURIComponent('/settings?tab=security&source=email')}`
const completeUrl = `${verificationUrl}&type=recovery&redirect_to=${encodeURIComponent(nestedRedirect)}`
const query = parseQuery(`confirmation_url=${encodeURIComponent(completeUrl)}`)
expect(resolveConfirmationUrl(query, options)).toBe(completeUrl)
expect(new URL(resolveConfirmationUrl(query, options)!).searchParams.get('redirect_to')).toBe(nestedRedirect)
})
it.concurrent('does not override parameters already in the verification URL', () => {
const completeUrl = `${verificationUrl}&type=recovery&redirect_to=${encodeURIComponent(redirectTo)}`
expect(resolveConfirmationUrl({
confirmation_url: completeUrl,
type: 'signup',
redirect_to: 'https://console.capgo.app/dashboard',
}, options)).toBe(completeUrl)
})
it.concurrent('supports legacy links with an extra layer of URL encoding', () => {
const completeUrl = `${verificationUrl}&type=recovery&redirect_to=${encodeURIComponent(redirectTo)}`
expect(resolveConfirmationUrl({ confirmation_url: encodeURIComponent(completeUrl) }, options)).toBe(completeUrl)
})
it.concurrent.each(['signup', 'invite', 'magiclink', 'email_change'])('restores misplaced parameters for %s emails', (type) => {
const resolved = resolveConfirmationUrl({ confirmation_url: verificationUrl, type }, options)
expect(new URL(resolved!).searchParams.get('type')).toBe(type)
})
it.concurrent('does not forward auth parameters to other allowed URL paths', () => {
const confirmationUrl = 'https://console.capgo.app/confirm'
expect(resolveConfirmationUrl({ confirmation_url: confirmationUrl, type: 'recovery', redirect_to: redirectTo }, options)).toBe(confirmationUrl)
})
it.concurrent.each([
'https://evil.example/auth/v1/verify?token=regression',
'https://sb.capgo.app.evil.example/auth/v1/verify?token=regression',
'http://sb.capgo.app/auth/v1/verify?token=regression',
'javascript:alert(1)',
'//sb.capgo.app/auth/v1/verify?token=regression',
'https%3A%2F%2Fsb.capgo.app%2Fauth%2Fv1%2Fverify%3Ftoken%3D%',
])('rejects an invalid confirmation URL: %s', (confirmationUrl) => {
expect(resolveConfirmationUrl({ confirmation_url: confirmationUrl, type: 'recovery', redirect_to: redirectTo }, options)).toBeNull()
})
it.concurrent('rejects missing and duplicate confirmation URLs', () => {
expect(resolveConfirmationUrl({}, options)).toBeNull()
expect(resolveConfirmationUrl({ confirmation_url: null }, options)).toBeNull()
expect(resolveConfirmationUrl({ confirmation_url: [verificationUrl, verificationUrl] }, options)).toBeNull()
})
it.concurrent('supports local HTTP verification endpoints only in development', () => {
const query = { confirmation_url: 'http://127.0.0.1:54321/auth/v1/verify?token=regression', type: 'recovery' }
expect(resolveConfirmationUrl(query, options)).toBeNull()
expect(new URL(resolveConfirmationUrl(query, { ...options, allowLocalDev: true })!).searchParams.get('type')).toBe('recovery')
})
it.concurrent('relays verification links through the Capgo API when an API host is configured', () => {
const relayOptions = { ...options, apiHost: 'https://api.capgo.app/' }
const relayed = new URL(resolveConfirmationUrl({ confirmation_url: verificationUrl, type: 'recovery', redirect_to: redirectTo }, relayOptions)!)
expect(relayed.origin).toBe('https://api.capgo.app')
expect(relayed.pathname).toBe('/private/console/auth/verify')
expect(relayed.searchParams.get('token')).toBe('recovery-regression')
expect(relayed.searchParams.get('type')).toBe('recovery')
expect(relayed.searchParams.get('redirect_to')).toBe(redirectTo)
// The email host is irrelevant: only our own auth provider ever sees the token.
expect(new URL(resolveConfirmationUrl({ confirmation_url: 'https://evil.example/auth/v1/verify?token=regression' }, relayOptions)!).origin).toBe('https://api.capgo.app')
// Other links still need an approved host.
expect(resolveConfirmationUrl({ confirmation_url: 'https://evil.example/login' }, relayOptions)).toBeNull()
})
})