Co-authored-by: Michael Chihlas <michael@resolutionflow.com> Co-committed-by: Michael Chihlas <michael@resolutionflow.com>
124 lines
3.7 KiB
TypeScript
124 lines
3.7 KiB
TypeScript
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(
|
|
<HelmetProvider>
|
|
<MemoryRouter initialEntries={[initialPath]}>
|
|
<AcceptInvitePage />
|
|
</MemoryRouter>
|
|
</HelmetProvider>,
|
|
)
|
|
}
|
|
|
|
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()
|
|
})
|
|
})
|