Skip to content
Merged
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
300 changes: 147 additions & 153 deletions __tests__/api/assessments.test.ts

Large diffs are not rendered by default.

415 changes: 189 additions & 226 deletions __tests__/api/auth-login.test.ts

Large diffs are not rendered by default.

584 changes: 262 additions & 322 deletions __tests__/api/auth-register.test.ts

Large diffs are not rendered by default.

448 changes: 325 additions & 123 deletions __tests__/api/interview-session.test.ts

Large diffs are not rendered by default.

421 changes: 190 additions & 231 deletions __tests__/api/profile-dashboard.test.ts

Large diffs are not rendered by default.

367 changes: 120 additions & 247 deletions __tests__/api/questions.test.ts

Large diffs are not rendered by default.

417 changes: 229 additions & 188 deletions __tests__/api/resume-coverletter.test.ts

Large diffs are not rendered by default.

272 changes: 272 additions & 0 deletions __tests__/components/admin-panel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { AdminPanel } from '@/components/interview-lab/AdminPanel';

const mockUseAuth = vi.fn();
vi.mock('@/lib/auth-context', () => ({
useAuth: () => mockUseAuth(),
}));

function jsonResponse(body: unknown, ok = true) {
return Promise.resolve({ ok, json: () => Promise.resolve(body) });
}

const adminUser = { id: 'admin1', email: 'admin@test.com', isAdmin: true, subscriptionTier: 'free' };
const regularUser = { id: 'u1', email: 'user@test.com', isAdmin: false, subscriptionTier: 'free' };

function defaultFetchMock() {
return vi.fn((url: string) => {
if (url.startsWith('/api/admin/questions')) return jsonResponse({ questions: [], total: 0 });
if (url === '/api/guides') return jsonResponse({ guides: [] });
if (url === '/api/downloads') return jsonResponse({ downloads: [] });
if (url === '/api/admin/analytics') {
return jsonResponse({
stats: { totalUsers: 12, totalSessions: 34, totalAttempts: 56, avgScore: 7.2, totalQuestions: 100, totalGuides: 5, totalDownloads: 8, sessionsLast30Days: 9 },
breakdowns: { usersByTier: { free: 12 } },
});
}
return jsonResponse({});
});
}

describe('AdminPanel — access control', () => {
it('shows an access-denied message and does not fetch admin data for a non-admin user', async () => {
mockUseAuth.mockReturnValue({ user: regularUser });
const fetchMock = defaultFetchMock();
global.fetch = fetchMock;

render(<AdminPanel />);
expect(screen.getByText("You don't have admin access.")).toBeInTheDocument();
expect(fetchMock).not.toHaveBeenCalled();
});

it('shows an access-denied message when there is no user at all', () => {
mockUseAuth.mockReturnValue({ user: null });
global.fetch = defaultFetchMock();
render(<AdminPanel />);
expect(screen.getByText("You don't have admin access.")).toBeInTheDocument();
});
});

describe('AdminPanel — questions tab (admin)', () => {
beforeEach(() => {
mockUseAuth.mockReturnValue({ user: adminUser });
});

it('fetches questions, guides, downloads, and analytics on mount for an admin', async () => {
const fetchMock = defaultFetchMock();
global.fetch = fetchMock;
render(<AdminPanel />);
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/admin/questions'));
expect(fetchMock).toHaveBeenCalledWith('/api/guides');
expect(fetchMock).toHaveBeenCalledWith('/api/downloads');
expect(fetchMock).toHaveBeenCalledWith('/api/admin/analytics');
});
});

it('renders fetched questions with role/difficulty/type badges', async () => {
const fetchMock = vi.fn((url: string) => {
if (url.startsWith('/api/admin/questions')) {
return jsonResponse({
questions: [{ id: 'q1', question: 'What is ACoS?', role: 'PPC VA', difficulty: 'beginner', type: 'technical', status: 'published' }],
total: 1,
});
}
if (url === '/api/guides') return jsonResponse({ guides: [] });
if (url === '/api/downloads') return jsonResponse({ downloads: [] });
if (url === '/api/admin/analytics') return jsonResponse({ stats: {}, breakdowns: {} });
return jsonResponse({});
});
global.fetch = fetchMock;

render(<AdminPanel />);
await waitFor(() => expect(screen.getByText('What is ACoS?')).toBeInTheDocument());
expect(screen.getByText('Question Database (1 total)')).toBeInTheDocument();
});

it('re-fetches with role/status query params when filters change', async () => {
const fetchMock = defaultFetchMock();
global.fetch = fetchMock;
render(<AdminPanel />);
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/admin/questions')));

fetchMock.mockClear();
fireEvent.click(screen.getByText('All Status'));
fireEvent.click(await screen.findByText('Published'));

await waitFor(() => {
const called = fetchMock.mock.calls.some(([url]) => typeof url === 'string' && url.includes('status=published'));
expect(called).toBe(true);
});
});

it('creates a new question via POST and resets the form', async () => {
const fetchMock = defaultFetchMock();
global.fetch = fetchMock;
render(<AdminPanel />);
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/admin/questions')));

fireEvent.click(screen.getByRole('button', { name: 'Add Question' }));
const questionField = screen.getByText('Question').nextElementSibling as HTMLElement;
fireEvent.change(questionField, { target: { value: 'How do you calculate ACoS?' } });

fetchMock.mockClear();
fireEvent.click(screen.getByRole('button', { name: 'Create Question' }));

await waitFor(() => {
const postCall = fetchMock.mock.calls.find(([url, opts]) => url === '/api/admin/questions' && (opts as RequestInit)?.method === 'POST');
expect(postCall).toBeDefined();
const body = JSON.parse((postCall![1] as RequestInit).body as string);
expect(body.question).toBe('How do you calculate ACoS?');
});

// Form resets/hides after save
await waitFor(() => expect(screen.queryByText('Question')).not.toBeInTheDocument());
});

it('edits an existing question via PUT with its id', async () => {
const fetchMock = vi.fn((url: string) => {
if (url.startsWith('/api/admin/questions')) {
return jsonResponse({
questions: [{ id: 'q1', question: 'Old question text', role: 'PPC VA', difficulty: 'beginner', type: 'technical', status: 'published', answerFormat: 'bullet' }],
total: 1,
});
}
if (url === '/api/guides') return jsonResponse({ guides: [] });
if (url === '/api/downloads') return jsonResponse({ downloads: [] });
if (url === '/api/admin/analytics') return jsonResponse({ stats: {}, breakdowns: {} });
return jsonResponse({});
});
global.fetch = fetchMock;

render(<AdminPanel />);
await waitFor(() => expect(screen.getByText('Old question text')).toBeInTheDocument());

fireEvent.click(screen.getByRole('button', { name: 'Edit' }));
const questionField = screen.getByText('Question').nextElementSibling as HTMLElement;
expect(questionField).toHaveValue('Old question text');

fetchMock.mockClear();
fireEvent.click(screen.getByRole('button', { name: 'Update Question' }));

await waitFor(() => {
const putCall = fetchMock.mock.calls.find(([url, opts]) => url === '/api/admin/questions' && (opts as RequestInit)?.method === 'PUT');
expect(putCall).toBeDefined();
const body = JSON.parse((putCall![1] as RequestInit).body as string);
expect(body.id).toBe('q1');
});
});

it('archives a question after confirmation, and skips the request when confirmation is declined', async () => {
const fetchMock = vi.fn((url: string) => {
if (url.startsWith('/api/admin/questions')) {
return jsonResponse({
questions: [{ id: 'q1', question: 'A question', role: 'PPC VA', difficulty: 'beginner', type: 'technical', status: 'published' }],
total: 1,
});
}
if (url === '/api/guides') return jsonResponse({ guides: [] });
if (url === '/api/downloads') return jsonResponse({ downloads: [] });
if (url === '/api/admin/analytics') return jsonResponse({ stats: {}, breakdowns: {} });
return jsonResponse({});
});
global.fetch = fetchMock;
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false);

render(<AdminPanel />);
await waitFor(() => expect(screen.getByText('A question')).toBeInTheDocument());

fetchMock.mockClear();
fireEvent.click(screen.getByRole('button', { name: 'Archive' }));
expect(confirmSpy).toHaveBeenCalled();
expect(fetchMock).not.toHaveBeenCalled();

confirmSpy.mockReturnValue(true);
fireEvent.click(screen.getByRole('button', { name: 'Archive' }));
await waitFor(() => {
const putCall = fetchMock.mock.calls.find(([url, opts]) => url === '/api/admin/questions' && (opts as RequestInit)?.method === 'PUT');
expect(putCall).toBeDefined();
const body = JSON.parse((putCall![1] as RequestInit).body as string);
expect(body).toEqual({ id: 'q1', status: 'archived' });
});

confirmSpy.mockRestore();
});
});

describe('AdminPanel — guides and downloads tabs', () => {
beforeEach(() => {
mockUseAuth.mockReturnValue({ user: adminUser });
});

it('creates a guide via POST from the Guides tab', async () => {
const fetchMock = defaultFetchMock();
global.fetch = fetchMock;
render(<AdminPanel />);
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith('/api/guides'));

fireEvent.mouseDown(screen.getByRole('tab', { name: 'Guides' }));
fireEvent.click(screen.getByRole('button', { name: 'Add Guide' }));
fireEvent.change(screen.getByText('Title').nextElementSibling as HTMLElement, { target: { value: 'PPC Basics' } });
fireEvent.change(screen.getByText('Content (Markdown)').nextElementSibling as HTMLElement, { target: { value: '# Intro' } });

fetchMock.mockClear();
fireEvent.click(screen.getByRole('button', { name: 'Create Guide' }));

await waitFor(() => {
const postCall = fetchMock.mock.calls.find(([url, opts]) => url === '/api/guides' && (opts as RequestInit)?.method === 'POST');
expect(postCall).toBeDefined();
const body = JSON.parse((postCall![1] as RequestInit).body as string);
expect(body.title).toBe('PPC Basics');
expect(body.slug).toBe('ppc-basics');
});
});

it('creates a download resource via POST from the Downloads tab', async () => {
const fetchMock = defaultFetchMock();
global.fetch = fetchMock;
render(<AdminPanel />);
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith('/api/downloads'));

fireEvent.mouseDown(screen.getByRole('tab', { name: 'Downloads' }));
fireEvent.click(screen.getByRole('button', { name: 'Add Download' }));
fireEvent.change(screen.getByText('Title').nextElementSibling as HTMLElement, { target: { value: 'ACoS Cheat Sheet' } });

fetchMock.mockClear();
fireEvent.click(screen.getByRole('button', { name: 'Add Download' }));

await waitFor(() => {
const postCall = fetchMock.mock.calls.find(([url, opts]) => url === '/api/downloads' && (opts as RequestInit)?.method === 'POST');
expect(postCall).toBeDefined();
const body = JSON.parse((postCall![1] as RequestInit).body as string);
expect(body.title).toBe('ACoS Cheat Sheet');
});
});
});

describe('AdminPanel — analytics tab', () => {
beforeEach(() => {
mockUseAuth.mockReturnValue({ user: adminUser });
});

it('renders platform stats from the analytics API', async () => {
global.fetch = defaultFetchMock();
render(<AdminPanel />);
fireEvent.mouseDown(screen.getByRole('tab', { name: 'Analytics' }));

await waitFor(() => expect(screen.getByText('12')).toBeInTheDocument());
expect(screen.getByText('Total Users')).toBeInTheDocument();
expect(screen.getByText('34')).toBeInTheDocument();
});

it('renders a breakdown bar for each tier in usersByTier', async () => {
global.fetch = defaultFetchMock();
render(<AdminPanel />);
fireEvent.mouseDown(screen.getByRole('tab', { name: 'Analytics' }));

await waitFor(() => expect(screen.getByText('Users by Subscription Tier')).toBeInTheDocument());
expect(screen.getByText('free', { selector: 'span.capitalize' })).toBeInTheDocument();
expect(screen.getByText('12 (100%)')).toBeInTheDocument();
});
});
123 changes: 123 additions & 0 deletions __tests__/components/cover-letter-studio.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { CoverLetterStudio } from '@/components/interview-lab/CoverLetterStudio';

vi.mock('@/lib/auth-context', () => ({
useAuth: () => ({
user: { id: 'u1', email: 'demo@interviewlab.com', name: 'Demo User', subscriptionTier: 'free', isAdmin: false },
}),
}));

vi.mock('@/lib/use-subscription', () => ({
useSubscription: () => ({
usage: { coverLettersThisMonth: 0 },
currentTier: 'free',
loading: false,
}),
}));

vi.mock('next/image', () => ({
default: (props: Record<string, unknown>) => <img alt={props.alt as string} />,
}));

function jsonResponse(body: unknown, ok = true) {
return Promise.resolve({ ok, json: () => Promise.resolve(body) });
}

describe('CoverLetterStudio', () => {
beforeEach(() => {
(global.fetch as ReturnType<typeof vi.fn>).mockImplementation((url: string) => {
if (url === '/api/cover-letter') return jsonResponse({ coverLetters: [] });
return jsonResponse({});
});
});

it('renders the job description form', () => {
render(<CoverLetterStudio />);
expect(screen.getByText('Cover Letter Studio')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Paste the job description here...')).toBeInTheDocument();
});

it('fetches cover letter history for the logged-in user on mount', async () => {
render(<CoverLetterStudio />);
await waitFor(() => expect(global.fetch).toHaveBeenCalledWith('/api/cover-letter'));
});

it('disables the generate button until a job description is entered', () => {
render(<CoverLetterStudio />);
expect(screen.getByRole('button', { name: 'Generate Letter' })).toBeDisabled();
});

it('enables the generate button once a job description is entered', () => {
render(<CoverLetterStudio />);
const textarea = screen.getByPlaceholderText('Paste the job description here...');
fireEvent.change(textarea, { target: { value: 'We need an Amazon PPC VA' } });
expect(screen.getByRole('button', { name: 'Generate Letter' })).not.toBeDisabled();
});

it('generates a letter, persists it, and displays the draft plus claims to verify', async () => {
const fetchMock = vi.fn((url: string, opts?: RequestInit) => {
if (url === '/api/ai/cover-letter') {
return jsonResponse({
draftLetter: 'Dear Hiring Manager, I am excited to apply...',
claimsToVerify: ['3+ years of PPC experience'],
customizationTips: ['Mention specific tools'],
});
}
if (url === '/api/cover-letter' && opts?.method === 'POST') {
return jsonResponse({ id: 'cl-1' });
}
if (url === '/api/cover-letter') {
return jsonResponse({ coverLetters: [] });
}
return jsonResponse({});
});
global.fetch = fetchMock;

render(<CoverLetterStudio />);
const textarea = screen.getByPlaceholderText('Paste the job description here...');
fireEvent.change(textarea, { target: { value: 'We need an Amazon PPC VA' } });
fireEvent.click(screen.getByRole('button', { name: 'Generate Letter' }));

await waitFor(() => expect(screen.getByText(/Dear Hiring Manager/)).toBeInTheDocument());
expect(screen.getByText('3+ years of PPC experience')).toBeInTheDocument();
expect(screen.getByText('Claims to Verify Before Sending')).toBeInTheDocument();
expect(fetchMock).toHaveBeenCalledWith('/api/ai/cover-letter', expect.objectContaining({ method: 'POST' }));

const persistCall = fetchMock.mock.calls.find(([url, o]) => url === '/api/cover-letter' && (o as RequestInit)?.method === 'POST');
expect(persistCall).toBeDefined();
const body = JSON.parse((persistCall![1] as RequestInit).body as string);
expect(body.generatedLetter).toContain('Dear Hiring Manager');
});

it('does not call the AI endpoint when the job description is empty', () => {
const fetchMock = vi.fn(() => jsonResponse({ coverLetters: [] }));
global.fetch = fetchMock;
render(<CoverLetterStudio />);
// Button is disabled, but guard against a direct click bypassing the UI too.
fireEvent.click(screen.getByRole('button', { name: 'Generate Letter' }));
expect(fetchMock).not.toHaveBeenCalledWith('/api/ai/cover-letter', expect.anything());
});

it('loads a previous cover letter from history', async () => {
const fetchMock = vi.fn((url: string) => {
if (url === '/api/cover-letter') {
return jsonResponse({
coverLetters: [{ id: 'cl-1', tone: 'upwork', generatedLetter: 'Previously generated letter text', jobDescription: 'Old job', truthFlags: JSON.stringify(['flag']), createdAt: '2026-01-01' }],
});
}
return jsonResponse({});
});
global.fetch = fetchMock;

render(<CoverLetterStudio />);
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith('/api/cover-letter'));

// The button carries an explicit aria-label ("Show history"/"Hide history")
// that overrides its visible "History (n)" text for accessible-name purposes.
fireEvent.click(screen.getByRole('button', { name: 'Show history' }));
fireEvent.click(await screen.findByRole('button', { name: 'Load' }));

expect(screen.getByText('Previously generated letter text')).toBeInTheDocument();
});
});
Loading
Loading