MotorSignal-App/website/tests/auth-recovery-verification....

197 lines
6.6 KiB
TypeScript

import React from 'react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { SegmentedOtpInput } from '../components/segmented-otp-input';
import { VerifyEmailForm } from '../components/verify-email-form';
import { RecoveryForm } from '../components/recovery-form';
describe('SegmentedOtpInput Component (WEB-22 / WEB-21)', () => {
it('renders 6 distinct numeric input elements', () => {
const onChange = vi.fn();
render(<SegmentedOtpInput value="" onChange={onChange} length={6} />);
const inputs = screen.getAllByRole('textbox');
expect(inputs).toHaveLength(6);
expect(inputs[0].getAttribute('inputmode')).toBe('numeric');
});
it('populates initial digits from value prop', () => {
render(<SegmentedOtpInput value="123456" onChange={vi.fn()} length={6} />);
const inputs = screen.getAllByRole('textbox') as HTMLInputElement[];
expect(inputs.map((i) => i.value)).toEqual(['1', '2', '3', '4', '5', '6']);
});
it('updates value and advances focus on typing', () => {
const onChange = vi.fn();
render(<SegmentedOtpInput value="" onChange={onChange} length={6} />);
const inputs = screen.getAllByRole('textbox') as HTMLInputElement[];
fireEvent.change(inputs[0], { target: { value: '9' } });
expect(onChange).toHaveBeenCalledWith('9');
});
it('handles paste event correctly across all 6 inputs', () => {
const onChange = vi.fn();
render(<SegmentedOtpInput value="" onChange={onChange} length={6} />);
const inputs = screen.getAllByRole('textbox');
fireEvent.paste(inputs[0], {
clipboardData: {
getData: () => '849201',
},
});
expect(onChange).toHaveBeenCalledWith('849201');
});
});
describe('VerifyEmailForm Component (WEB-22)', () => {
const originalFetch = global.fetch;
beforeEach(() => {
vi.restoreAllMocks();
});
afterEach(() => {
global.fetch = originalFetch;
});
it('renders tactical header with Verify Telemetry Signature and 6-digit OTP challenge', () => {
render(<VerifyEmailForm />);
expect(screen.getByRole('heading', { name: /verify telemetry signature/i })).toBeDefined();
const inputs = screen.getAllByRole('textbox');
expect(inputs).toHaveLength(6);
expect(screen.getByRole('button', { name: /confirm telemetry signature/i })).toBeDefined();
});
it('disables submit button until 6 digits are provided, then submits successfully', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({}),
});
render(<VerifyEmailForm />);
const submitBtn = screen.getByRole('button', { name: /confirm telemetry signature/i }) as HTMLButtonElement;
expect(submitBtn.disabled).toBe(true);
const inputs = screen.getAllByRole('textbox') as HTMLInputElement[];
fireEvent.paste(inputs[0], {
clipboardData: {
getData: () => '654321',
},
});
await waitFor(() => {
expect(submitBtn.disabled).toBe(false);
});
fireEvent.click(submitBtn);
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith('/api/auth/verify-email', expect.objectContaining({
method: 'POST',
body: JSON.stringify({ token: '654321' }),
}));
expect(screen.getByText(/telemetry signature verified successfully/i)).toBeDefined();
});
});
it('renders resend cooldown timer and handles resend verification code request', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ detail: 'Dispatched' }),
});
render(<VerifyEmailForm />);
expect(screen.getByText(/resend code in/i)).toBeDefined();
});
});
describe('RecoveryForm Component (WEB-21)', () => {
const originalFetch = global.fetch;
beforeEach(() => {
vi.restoreAllMocks();
});
afterEach(() => {
global.fetch = originalFetch;
});
it('renders step 1 with Recover Account Access and email input', () => {
render(<RecoveryForm />);
expect(screen.getByRole('heading', { name: /recover account access/i })).toBeDefined();
expect(screen.getByPlaceholderText('rider@example.com')).toBeDefined();
expect(screen.getByRole('button', { name: /send recovery code/i })).toBeDefined();
});
it('submits email in step 1 and advances to step 2 OTP challenge', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ detail: 'Sent' }),
});
render(<RecoveryForm />);
const emailInput = screen.getByPlaceholderText('rider@example.com');
fireEvent.change(emailInput, { target: { value: 'speed@motorsignal.app' } });
const sendBtn = screen.getByRole('button', { name: /send recovery code/i });
fireEvent.click(sendBtn);
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith('/api/auth/password-reset-request', expect.objectContaining({
method: 'POST',
body: JSON.stringify({ email: 'speed@motorsignal.app' }),
}));
expect(screen.getByText(/enter your 6-digit verification code/i)).toBeDefined();
expect(screen.getAllByPlaceholderText('••••••••••••')).toHaveLength(2);
});
});
it('validates password requirements and submits password reset', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({}),
});
render(<RecoveryForm initialToken="123456" />);
expect(screen.getByText(/6-digit recovery code/i)).toBeDefined();
const passwordInputs = screen.getAllByPlaceholderText('••••••••••••') as HTMLInputElement[];
expect(passwordInputs).toHaveLength(2);
// Enter short password
fireEvent.change(passwordInputs[0], { target: { value: 'short' } });
fireEvent.change(passwordInputs[1], { target: { value: 'short' } });
const submitBtn = screen.getByRole('button', { name: /set new password/i });
fireEvent.click(submitBtn);
await waitFor(() => {
expect(screen.getByText(/password must be at least 12 characters/i)).toBeDefined();
});
// Enter matching 12+ char password
fireEvent.change(passwordInputs[0], { target: { value: 'SuperSecret1234!' } });
fireEvent.change(passwordInputs[1], { target: { value: 'SuperSecret1234!' } });
fireEvent.click(submitBtn);
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith('/api/auth/password-reset', expect.objectContaining({
method: 'POST',
body: JSON.stringify({ token: '123456', password: 'SuperSecret1234!' }),
}));
expect(screen.getByText(/password reset complete/i)).toBeDefined();
});
});
});