Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions web_app/frontend-react/src/pages/VerifyEmailReminder.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import VerifyEmailReminder from './VerifyEmailReminder';
import { authAPI } from '../utils/api';
import toast from 'react-hot-toast';

jest.mock('react-hot-toast');
jest.mock('../utils/api', () => ({
authAPI: {
post: jest.fn(),
},
}));

describe('VerifyEmailReminder Component', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders reminder content and resend button', () => {
render(<VerifyEmailReminder />);

expect(screen.getByRole('heading', { name: /verify your email/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /resend verification email/i })).toBeInTheDocument();
});

it('resends verification email successfully', async () => {
authAPI.post.mockResolvedValueOnce({ data: { success: true } });

render(<VerifyEmailReminder />);

fireEvent.click(screen.getByRole('button', { name: /resend verification email/i }));

await waitFor(() => {
expect(authAPI.post).toHaveBeenCalledWith('/auth/resend-verification');
expect(toast.success).toHaveBeenCalledWith('Verification email sent! Check your inbox.');
});
});

it('shows API error message on resend failure', async () => {
authAPI.post.mockRejectedValueOnce({
response: { data: { message: 'Too many requests' } },
});

render(<VerifyEmailReminder />);

fireEvent.click(screen.getByRole('button', { name: /resend verification email/i }));

await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Too many requests');
});
});

it('shows fallback error message on resend failure without API message', async () => {
authAPI.post.mockRejectedValueOnce(new Error('Network down'));

render(<VerifyEmailReminder />);

fireEvent.click(screen.getByRole('button', { name: /resend verification email/i }));

await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Failed to resend email');
});
});
});
145 changes: 145 additions & 0 deletions web_app/frontend-react/src/pages/VerifyOTP.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import VerifyOTP from './VerifyOTP';
import toast from 'react-hot-toast';
import useAuthStore from '../store/authStore';
import { authAPI } from '../utils/api';

const mockNavigate = jest.fn();
const mockSetAuth = jest.fn();

jest.mock('react-hot-toast');
jest.mock('../store/authStore');
jest.mock('../utils/api', () => ({
authAPI: {
post: jest.fn(),
},
}));

jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return {
...actual,
useNavigate: () => mockNavigate,
useLocation: jest.fn(),
};
});

const { useLocation } = require('react-router-dom');

describe('VerifyOTP Component', () => {
beforeEach(() => {
jest.clearAllMocks();

useAuthStore.mockImplementation((selector) => {
const state = { setAuth: mockSetAuth };
return selector(state);
});

useLocation.mockReturnValue({
state: {
userId: 'user-123',
email: 'test@example.com',
},
});
});

it('renders OTP screen with six input boxes', () => {
render(<VerifyOTP />);

expect(screen.getByText(/verify your email/i)).toBeInTheDocument();
expect(screen.getByText(/test@example.com/i)).toBeInTheDocument();
expect(screen.getAllByRole('textbox')).toHaveLength(6);
});

it('shows error for incomplete OTP and does not call API', async () => {
render(<VerifyOTP />);

const inputs = screen.getAllByRole('textbox');
fireEvent.change(inputs[0], { target: { value: '1' } });
fireEvent.change(inputs[1], { target: { value: '2' } });

fireEvent.click(screen.getByRole('button', { name: /verify email/i }));

await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Please enter complete OTP');
expect(authAPI.post).not.toHaveBeenCalled();
});
});

it('verifies OTP successfully and navigates to dashboard', async () => {
authAPI.post.mockResolvedValueOnce({
data: {
success: true,
user: { id: 'user-123', email: 'test@example.com' },
token: 'token-123',
},
});

render(<VerifyOTP />);

const inputs = screen.getAllByRole('textbox');
['1', '2', '3', '4', '5', '6'].forEach((digit, idx) => {
fireEvent.change(inputs[idx], { target: { value: digit } });
});

fireEvent.click(screen.getByRole('button', { name: /verify email/i }));

await waitFor(() => {
expect(authAPI.post).toHaveBeenCalledWith('/auth/verify-otp', {
userId: 'user-123',
otp: '123456',
});
expect(mockSetAuth).toHaveBeenCalledWith(
{ id: 'user-123', email: 'test@example.com' },
'token-123',
);
expect(toast.success).toHaveBeenCalledWith('Email verified successfully!');
expect(mockNavigate).toHaveBeenCalledWith('/dashboard');
});
});

it('handles verification failure and resets OTP', async () => {
authAPI.post.mockRejectedValueOnce({
response: { data: { message: 'Invalid OTP' } },
});

render(<VerifyOTP />);

const inputs = screen.getAllByRole('textbox');
['1', '2', '3', '4', '5', '6'].forEach((digit, idx) => {
fireEvent.change(inputs[idx], { target: { value: digit } });
});

fireEvent.click(screen.getByRole('button', { name: /verify email/i }));

await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Invalid OTP');
expect(inputs[0]).toHaveValue('');
expect(inputs[5]).toHaveValue('');
});
Comment on lines +116 to +120
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Strengthen OTP reset assertion to all six fields.

On failure, the component resets every OTP slot, but the test only checks Line 118 and Line 119. This can let partial-reset regressions slip through.

✅ Suggested test assertion update
     await waitFor(() => {
       expect(toast.error).toHaveBeenCalledWith('Invalid OTP');
-      expect(inputs[0]).toHaveValue('');
-      expect(inputs[5]).toHaveValue('');
+      inputs.forEach((input) => expect(input).toHaveValue(''));
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Invalid OTP');
expect(inputs[0]).toHaveValue('');
expect(inputs[5]).toHaveValue('');
});
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Invalid OTP');
inputs.forEach((input) => expect(input).toHaveValue(''));
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web_app/frontend-react/src/pages/VerifyOTP.test.jsx` around lines 116 - 120,
The test currently only asserts that inputs[0] and inputs[5] are cleared after
an invalid OTP; update the assertion inside the waitFor in VerifyOTP.test.jsx to
verify that all six OTP input fields (inputs[0] through inputs[5]) have been
reset to '' so partial-reset regressions are caught—locate the waitFor block
that checks toast.error and the inputs array and add expectations for inputs[1],
inputs[2], inputs[3], and inputs[4] toHaveValue('') alongside the existing
checks.

});

it('resends OTP successfully', async () => {
authAPI.post.mockResolvedValueOnce({ data: { success: true } });

render(<VerifyOTP />);

fireEvent.click(screen.getByRole('button', { name: /resend otp/i }));

await waitFor(() => {
expect(authAPI.post).toHaveBeenCalledWith('/auth/resend-otp', { userId: 'user-123' });
expect(toast.success).toHaveBeenCalledWith('OTP resent successfully!');
});
});

it('redirects to register when userId is missing', async () => {
useLocation.mockReturnValueOnce({ state: {} });

render(<VerifyOTP />);

await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith('/register');
});
});
});
Loading