import { describe, it, expect, beforeEach, vi } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { HelmetProvider } from 'react-helmet-async' import { AcceptInvitePage } from '../AcceptInvitePage' import { inviteApi } from '@/api/invite' import { __resetAppConfigCache, __setAppConfigCache, } from '@/hooks/useAppConfig' vi.mock('@/api/invite', () => ({ inviteApi: { lookupAccountInvite: vi.fn(), validateCode: vi.fn(), }, })) vi.mock('@/store/authStore', () => ({ useAuthStore: () => ({ register: vi.fn().mockResolvedValue(undefined), isLoading: false, error: null, clearError: vi.fn(), }), })) function renderPage(initialPath: string) { return render( , ) } describe('AcceptInvitePage', () => { beforeEach(() => { __resetAppConfigCache() __setAppConfigCache({ self_serve_enabled: true, oauth_providers: ['google', 'microsoft'], }) vi.clearAllMocks() }) it('shows account name + locked email + accept buttons for a valid code', async () => { vi.mocked(inviteApi.lookupAccountInvite).mockResolvedValue({ account_name: 'Acme MSP', inviter_name: 'Alice Owner', invited_email: 'bob@acme.example', role: 'engineer', }) renderPage('/accept-invite?code=VALIDINVITECODE0011223344556677') // Inviter context (also confirms the lookup completed and rendered) await waitFor(() => { expect( screen.getByText(/Alice Owner invited you as engineer/), ).toBeInTheDocument() }) // Account name surfaces in the heading line. expect( screen.getByText((_content, node) => { return ( node?.tagName.toLowerCase() === 'span' && /Acme MSP/.test(node.textContent || '') ) }), ).toBeInTheDocument() // Locked email — not an editable input const emailDisplay = screen.getByTestId('invited-email') expect(emailDisplay.tagName.toLowerCase()).not.toBe('input') expect(emailDisplay).toHaveTextContent('bob@acme.example') expect(screen.queryByLabelText(/email address/i)).not.toBeInTheDocument() // OAuth buttons + password submit all rendered expect(screen.getByTestId('oauth-google')).toBeInTheDocument() expect(screen.getByTestId('oauth-microsoft')).toBeInTheDocument() expect(screen.getByTestId('accept-submit')).toBeInTheDocument() expect(screen.getByTestId('accept-submit')).toHaveTextContent(/Join Acme MSP/) expect(inviteApi.lookupAccountInvite).toHaveBeenCalledWith( 'VALIDINVITECODE0011223344556677', ) }) it('shows resend message + mailto link for an invalid invite code', async () => { vi.mocked(inviteApi.lookupAccountInvite).mockRejectedValue( Object.assign(new Error('not found'), { response: { status: 404, data: { detail: { error: 'invite_invalid_or_expired_or_revoked' } }, }, }), ) renderPage('/accept-invite?code=BADCODE') await waitFor(() => { expect( screen.getByText(/This invite is no longer valid/i), ).toBeInTheDocument() }) expect( screen.getByText(/Ask the person who invited you to resend it/i), ).toBeInTheDocument() const resendLink = screen.getByRole('link', { name: /Email your inviter/i }) expect(resendLink).toHaveAttribute( 'href', expect.stringMatching(/^mailto:/), ) // No accept form rendered when invite is invalid. expect(screen.queryByTestId('accept-submit')).not.toBeInTheDocument() expect(screen.queryByTestId('oauth-google')).not.toBeInTheDocument() }) })