Ctrl K
ComunidadGuías…

lib/auth/better-auth/__tests__/invitations.test.ts

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

Archivo original ↗Preguntar a KiBot
import { beforeEach, describe, expect, it, vi } from 'vitest'

import {
  consumeInvitation,
  createInvitation,
  hashInvitationToken,
  hasRecentBootstrapInvitation,
  releaseInvitation,
  revokeInvitation,
  validateInvitation
} from '@/lib/auth/better-auth/invitations'
import { db } from '@/lib/db'

const { state } = vi.hoisted(() => ({
  state: { rows: [] as unknown[] }
}))

vi.mock('@/lib/db', () => ({
  db: {
    select: vi.fn(() => ({
      from: vi.fn(() => ({
        where: vi.fn(() => Promise.resolve(state.rows))
      }))
    })),
    insert: vi.fn(() => ({
      values: vi.fn(() => ({
        returning: vi.fn(() => Promise.resolve(state.rows))
      }))
    })),
    update: vi.fn(() => ({
      set: vi.fn(() => ({
        where: vi.fn(() => ({
          returning: vi.fn(() => Promise.resolve(state.rows))
        }))
      }))
    }))
  }
}))

function activeInvitation(overrides: Record<string, unknown> = {}) {
  return {
    id: 'inv-1',
    email: '[email protected]',
    token: 'tok',
    invitedBy: 'admin-1',
    revokedAt: null,
    usedAt: null,
    expiresAt: new Date(Date.now() + 60_000),
    createdAt: new Date(),
    ...overrides
  }
}

describe('invitations', () => {
  beforeEach(() => {
    state.rows = []
    vi.clearAllMocks()
  })

  describe('validateInvitation', () => {
    it('returns null for a missing token', async () => {
      await expect(validateInvitation(undefined)).resolves.toBeNull()
      await expect(validateInvitation('')).resolves.toBeNull()
      expect(db.select).not.toHaveBeenCalled()
    })

    it('returns null when no active row matches (missing or revoked)', async () => {
      // Revoked tokens are filtered out by the query (isNull(revokedAt))
      state.rows = []

      await expect(validateInvitation('revoked-or-missing')).resolves.toBeNull()
    })

    it('returns the invitation when valid', async () => {
      state.rows = [activeInvitation()]

      const invitation = await validateInvitation('tok')

      expect(invitation).toMatchObject({ id: 'inv-1', token: 'tok' })
    })

    it('returns null for an expired invitation', async () => {
      state.rows = [
        activeInvitation({ expiresAt: new Date(Date.now() - 1000) })
      ]

      await expect(validateInvitation('tok')).resolves.toBeNull()
    })

    it('returns null for an already used invitation', async () => {
      state.rows = [activeInvitation({ usedAt: new Date() })]

      await expect(validateInvitation('tok')).resolves.toBeNull()
    })
  })

  describe('hashInvitationToken', () => {
    it('returns the SHA-256 hex digest of the token', () => {
      expect(hashInvitationToken('')).toBe(
        'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
      )
    })
  })

  describe('createInvitation', () => {
    it('inserts a hashed token and returns the plaintext once', async () => {
      state.rows = [activeInvitation()]

      const { invitation, token } = await createInvitation({
        invitedBy: 'admin-1',
        email: '[email protected]'
      })

      expect(invitation.id).toBe('inv-1')
      expect(token).toMatch(/^[0-9a-f]{48}$/)
      expect(db.insert).toHaveBeenCalled()
    })
  })

  describe('revokeInvitation', () => {
    it('reports success when a row was revoked', async () => {
      state.rows = [{ id: 'inv-1' }]
      await expect(revokeInvitation('inv-1')).resolves.toBe(true)
    })

    it('reports failure when nothing matched', async () => {
      state.rows = []
      await expect(revokeInvitation('missing')).resolves.toBe(false)
    })
  })

  describe('consumeInvitation', () => {
    it('consumes an unconsumed invitation', async () => {
      state.rows = [{ id: 'inv-1' }]
      await expect(consumeInvitation('inv-1')).resolves.toBe(true)
    })

    it('fails when already consumed', async () => {
      state.rows = []
      await expect(consumeInvitation('inv-1')).resolves.toBe(false)
    })
  })

  describe('releaseInvitation', () => {
    it('clears the used marker so the same link can be retried', async () => {
      state.rows = [{ id: 'inv-1' }]
      await expect(releaseInvitation('inv-1')).resolves.toBeUndefined()
      expect(db.update).toHaveBeenCalled()
    })
  })
})

describe('hasRecentBootstrapInvitation', () => {
  it('is true when a live bootstrap invitation row exists', async () => {
    state.rows = [{ id: 'inv-1' }]

    await expect(
      hasRecentBootstrapInvitation('[email protected]', 15 * 60 * 1000)
    ).resolves.toBe(true)
  })

  it('is false when no row matches', async () => {
    state.rows = []

    await expect(
      hasRecentBootstrapInvitation('[email protected]', 15 * 60 * 1000)
    ).resolves.toBe(false)
  })
})