Ctrl K
ComunidadGuías…

server/http/auth-management-api.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { createAuth } from '@server/auth'
import type {
  EmailOtpPluginOptions,
  OneTapPluginOptions,
  PhonePluginOptions,
  SiwePluginOptions,
} from '@server/auth-test-plugin-types'
import type { Database } from '@server/db/client'
import { createIdentifierGeneratorFake } from '@server/usecases/identifier-generator.fake'
import type { ManagementSignInSettingsResponse } from '@shared/api/management'
import { describe, expect, it, vi } from 'vitest'

describe('auth.test 4', () => {
  it('configures built-in email, phone, One Tap, and SIWE provider plugins', async () => {
    vi.spyOn(globalThis, 'fetch').mockResolvedValue(jsonResponse({ messages: [{ status: '0' }] }))
    const auth = createAuth(
      {} as Database,
      createIdentifierGeneratorFake(),
      '01234567890123456789012345678901',
      'https://auth.example.com',
      ['https://auth.example.com'],
      createEmailSenderMock(),
      createSecurityPolicy(),
      undefined,
      { builtInProviders: createBuiltInProviders() },
    )

    expect(findPlugin<EmailOtpPluginOptions>(auth, 'email-otp').options).toMatchObject({
      otpLength: 8,
      expiresIn: 900,
      changeEmail: {
        enabled: true,
        verifyCurrentEmail: false,
      },
    })
    expect(findPlugin<PhonePluginOptions>(auth, 'phone-number').options).toMatchObject({
      otpLength: 6,
      expiresIn: 300,
      requireVerification: true,
      signUpOnVerification: undefined,
    })
    expect(findPlugin<OneTapPluginOptions>(auth, 'one-tap').options).toMatchObject({
      clientId: 'google-client-id',
      disableSignup: false,
    })

    const siwe = findPlugin<SiwePluginOptions>(auth, 'siwe').options
    expect(siwe).toMatchObject({
      domain: 'wallet.example.com',
      emailDomainName: 'wallet.example.com',
      anonymous: true,
    })
    await expect(siwe.getNonce()).resolves.toMatch(/^[a-f0-9]{32}$/)

    const phone = findPlugin<PhonePluginOptions>(auth, 'phone-number').options
    await phone.sendOTP({ phoneNumber: '+15555550123', code: '123456' })
    expect(fetch).toHaveBeenCalledWith(
      'https://api.twilio.com/2010-04-01/Accounts/twilio-sid/Messages.json',
      expect.objectContaining({
        method: 'POST',
        headers: expect.objectContaining({
          Authorization: expect.stringContaining('Basic '),
          'Content-Type': 'application/x-www-form-urlencoded',
        }),
      }),
    )
  })

  it('sends phone OTPs through each configured SMS provider and fails invalid provider config', async () => {
    const fetchMock = vi.spyOn(globalThis, 'fetch')
    fetchMock.mockResolvedValue(jsonResponse({ messages: [{ status: '0' }] }))

    const vonage = findPlugin<PhonePluginOptions>(
      createAuth(
        {} as Database,
        createIdentifierGeneratorFake(),
        '01234567890123456789012345678901',
        'https://auth.example.com',
        ['https://auth.example.com'],
        createEmailSenderMock(),
        createSecurityPolicy(),
        undefined,
        {
          builtInProviders: createBuiltInProviders({
            smsProvider: 'vonage',
            vonageApiKey: 'vonage-key',
            vonageApiSecret: 'vonage-secret',
            vonageFrom: 'Realmroot',
          }),
        },
      ),
      'phone-number',
    ).options
    await vonage.sendPasswordResetOTP?.({ phoneNumber: '+15555550123', code: '123456' })
    expect(fetch).toHaveBeenCalledWith('https://rest.nexmo.com/sms/json', expect.objectContaining({ method: 'POST' }))

    const messageBird = findPlugin<PhonePluginOptions>(
      createAuth(
        {} as Database,
        createIdentifierGeneratorFake(),
        '01234567890123456789012345678901',
        'https://auth.example.com',
        ['https://auth.example.com'],
        createEmailSenderMock(),
        createSecurityPolicy(),
        undefined,
        {
          builtInProviders: createBuiltInProviders({
            smsProvider: 'messagebird',
            messageBirdAccessKey: 'messagebird-key',
            messageBirdOriginator: 'Realmroot',
          }),
        },
      ),
      'phone-number',
    ).options
    await messageBird.sendOTP({ phoneNumber: '+15555550123', code: '123456' })
    expect(fetch).toHaveBeenCalledWith(
      'https://rest.messagebird.com/messages',
      expect.objectContaining({ method: 'POST' }),
    )

    const brokenTwilio = findPlugin<PhonePluginOptions>(
      createAuth(
        {} as Database,
        createIdentifierGeneratorFake(),
        '01234567890123456789012345678901',
        'https://auth.example.com',
        ['https://auth.example.com'],
        createEmailSenderMock(),
        createSecurityPolicy(),
        undefined,
        { builtInProviders: createBuiltInProviders({ twilioAuthToken: '' }) },
      ),
      'phone-number',
    ).options
    await expect(brokenTwilio.sendOTP({ phoneNumber: '+15555550123', code: '123456' })).rejects.toThrow(
      'Twilio SMS provider is not configured.',
    )

    const unsupported = findPlugin<PhonePluginOptions>(
      createAuth(
        {} as Database,
        createIdentifierGeneratorFake(),
        '01234567890123456789012345678901',
        'https://auth.example.com',
        ['https://auth.example.com'],
        createEmailSenderMock(),
        createSecurityPolicy(),
        undefined,
        { builtInProviders: createBuiltInProviders({ smsProvider: 'unsupported' as 'twilio' }) },
      ),
      'phone-number',
    ).options
    await expect(unsupported.sendOTP({ phoneNumber: '+15555550123', code: '123456' })).rejects.toThrow(
      'Unsupported SMS provider: unsupported',
    )
  })

  it('disables direct passkey auth endpoints when passkeys are disabled by deployment policy', () => {
    const auth = createAuth(
      {} as Database,
      createIdentifierGeneratorFake(),
      '01234567890123456789012345678901',
      'https://auth.example.com',
      ['https://auth.example.com'],
      createEmailSenderMock(),
      createSecurityPolicy({
        passkeys: {
          enabled: false,
          rpId: 'auth.example.com',
          rpName: 'Realmroot',
          origins: ['https://auth.example.com'],
        },
      }),
    )

    expect(auth.options.disabledPaths).toEqual(
      expect.arrayContaining([
        '/passkey/generate-register-options',
        '/passkey/generate-authenticate-options',
        '/passkey/verify-registration',
        '/passkey/verify-authentication',
        '/passkey/list-user-passkeys',
        '/passkey/delete-passkey',
        '/passkey/update-passkey',
      ]),
    )
  })
})

function createEmailSenderMock() {
  return {
    send: vi.fn().mockResolvedValue({ messageId: 'email-1' }),
  }
}

function createSecurityPolicy(overrides: Partial<SecurityPolicyInput> = {}) {
  return {
    mfa: {
      mode: 'optional',
      authenticatorAppEnabled: true,
      emailOtpEnabled: false,
      backupCodesEnabled: true,
      ...overrides.mfa,
    },
    passkeys: {
      enabled: true,
      rpId: 'auth.example.com',
      rpName: 'Realmroot',
      origins: ['https://auth.example.com'],
      ...overrides.passkeys,
    },
    sessions: {
      expiresInSeconds: 60 * 60 * 24 * 7,
      updateAgeSeconds: 60 * 60 * 24,
      freshAgeSeconds: 60 * 60 * 24,
      cookieCacheSeconds: 60 * 5,
      ...overrides.sessions,
    },
    password: {
      minLength: 8,
      requiredCharacterTypes: 1,
      customWords: [],
      rejectUserInfo: true,
      rejectSequential: true,
      rejectCustomWords: false,
      ...overrides.password,
    },
    captcha: {
      enabled: false,
      provider: 'turnstile',
      siteKey: '',
      projectId: null,
      secretKey: '',
      ...overrides.captcha,
    },
    blocklist: {
      blockSubaddressing: false,
      entries: [],
      ...overrides.blocklist,
    },
  } satisfies SecurityPolicyInput
}

function createBuiltInProviders(
  phoneOverrides: Partial<ManagementSignInSettingsResponse['builtInProviders']['phone']> = {},
): ManagementSignInSettingsResponse['builtInProviders'] {
  return {
    email: {
      enabled: true,
      otpLength: 8,
      expiresInSeconds: 900,
    },
    phone: {
      enabled: true,
      otpLength: 6,
      expiresInSeconds: 300,
      signUpOnVerification: false,
      requireVerification: true,
      smsProvider: 'twilio',
      twilioAccountSid: 'twilio-sid',
      twilioAuthToken: 'twilio-token',
      twilioFromNumber: '+15555550100',
      vonageApiKey: '',
      vonageApiSecret: '',
      vonageFrom: '',
      messageBirdAccessKey: '',
      messageBirdOriginator: '',
      ...phoneOverrides,
    },
    web3Wallet: {
      enabled: true,
      chains: [1],
      domain: 'wallet.example.com',
      emailDomainName: 'wallet.example.com',
      ensLookupEnabled: false,
      allowSignUp: true,
    },
    passkey: {
      allowSignUp: true,
    },
    oneTap: {
      enabled: true,
      clientId: 'google-client-id',
      autoSelect: false,
      cancelOnTapOutside: true,
      uxMode: 'popup',
      context: 'signin',
      promptBaseDelayMs: 1000,
      promptMaxAttempts: 5,
      disableSignUp: false,
    },
  }
}

interface SecurityPolicyInput {
  mfa: {
    mode: 'optional' | 'required'
    authenticatorAppEnabled?: boolean
    emailOtpEnabled?: boolean
    backupCodesEnabled?: boolean
  }
  passkeys: {
    enabled: boolean
    rpId: string
    rpName: string
    origins: string[]
  }
  sessions: {
    expiresInSeconds: number
    updateAgeSeconds: number
    freshAgeSeconds: number
    cookieCacheSeconds: number
  }
  password: {
    minLength: number
    requiredCharacterTypes: number
    customWords: string[]
    rejectUserInfo: boolean
    rejectSequential: boolean
    rejectCustomWords: boolean
  }
  captcha: {
    enabled: boolean
    provider: 'turnstile'
    siteKey: string
    projectId: string | null
    secretKey: string
  }
  blocklist: {
    blockSubaddressing: boolean
    entries: string[]
  }
}

function jsonResponse(body: unknown, status = 200) {
  return new Response(JSON.stringify(body), {
    status,
    headers: { 'Content-Type': 'application/json' },
  })
}

function findPlugin<TOptions extends object>(auth: ReturnType<typeof createAuth>, id: string) {
  const plugin = auth.options.plugins?.find((candidate) => candidate.id === id)

  if (!plugin) {
    throw new Error(`Plugin not found: ${id}`)
  }

  return plugin as unknown as { options: TOptions }
}