fix logging tests
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 5m52s

This commit is contained in:
2025-12-09 14:52:46 -08:00
parent 9f50d7d942
commit 8bfadcd2d9
2 changed files with 37 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
// src/services/apiClient.test.ts // src/services/apiClient.test.ts
import { describe, it, expect, vi, afterAll, afterEach, beforeEach } from 'vitest'; import { describe, it, expect, vi, afterAll, afterEach, beforeEach, beforeAll } from 'vitest';
import { setupServer } from 'msw/node'; import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw'; import { http, HttpResponse } from 'msw';
@@ -45,6 +45,9 @@ describe('API Client', () => {
let capturedHeaders: Headers | null = null; let capturedHeaders: Headers | null = null;
let capturedBody: string | FormData | Record<string, unknown> | null = null; let capturedBody: string | FormData | Record<string, unknown> | null = null;
// Start the MSW server before all tests.
beforeAll(() => server.listen());
beforeEach(() => { beforeEach(() => {
// Define a correctly typed mock function for fetch. // Define a correctly typed mock function for fetch.
const mockFetch = (url: RequestInfo | URL, options?: RequestInit): Promise<Response> => { const mockFetch = (url: RequestInfo | URL, options?: RequestInit): Promise<Response> => {
@@ -66,14 +69,15 @@ describe('API Client', () => {
return Promise.resolve(new Response(JSON.stringify({ data: 'mock-success' }), { status: 200, headers: new Headers() as Headers })); return Promise.resolve(new Response(JSON.stringify({ data: 'mock-success' }), { status: 200, headers: new Headers() as Headers }));
}; };
// Assign the mock function to global.fetch and cast it to a Vitest mock. // Use spyOn instead of direct assignment to allow restoration for MSW tests.
global.fetch = vi.fn(mockFetch); vi.spyOn(global, 'fetch').mockImplementation(mockFetch);
}); });
afterEach(() => { afterEach(() => {
server.resetHandlers(); server.resetHandlers();
localStorageMock.clear(); localStorageMock.clear();
vi.clearAllMocks(); // Restore all mocks (including global.fetch) to their original implementation.
vi.restoreAllMocks();
}); });
afterAll(() => server.close()); afterAll(() => server.close());
@@ -98,11 +102,11 @@ describe('API Client', () => {
it('should handle token refresh on 401 response', async () => { it('should handle token refresh on 401 response', async () => {
localStorage.setItem('authToken', 'expired-token'); // Set an initial token localStorage.setItem('authToken', 'expired-token'); // Set an initial token
// Mock the fetch sequence: // Mock the fetch sequence on the existing spy:
// 1. Initial API call fails with 401 // 1. Initial API call fails with 401
// 2. `refreshToken` call succeeds with a new token // 2. `refreshToken` call succeeds with a new token
// 3. Retried API call succeeds with the expected user data // 3. Retried API call succeeds with the expected user data
global.fetch = vi.fn() vi.mocked(global.fetch)
.mockResolvedValueOnce({ ok: false, status: 401, json: () => Promise.resolve({ message: 'Unauthorized' }) } as Response) .mockResolvedValueOnce({ ok: false, status: 401, json: () => Promise.resolve({ message: 'Unauthorized' }) } as Response)
.mockResolvedValueOnce({ ok: true, status: 200, json: () => Promise.resolve({ token: 'new-refreshed-token' }) } as Response) .mockResolvedValueOnce({ ok: true, status: 200, json: () => Promise.resolve({ token: 'new-refreshed-token' }) } as Response)
.mockResolvedValueOnce({ ok: true, status: 200, json: () => Promise.resolve({ user_id: 'user-123' }) } as Response); .mockResolvedValueOnce({ ok: true, status: 200, json: () => Promise.resolve({ user_id: 'user-123' }) } as Response);
@@ -110,8 +114,6 @@ describe('API Client', () => {
// The apiClient's internal refreshToken function will call the refresh endpoint. // The apiClient's internal refreshToken function will call the refresh endpoint.
// We don't need a separate MSW handler for it if we are mocking global.fetch directly. // We don't need a separate MSW handler for it if we are mocking global.fetch directly.
// This test is now independent of MSW. // This test is now independent of MSW.
// The original test had a bug where the refresh endpoint was not mocked correctly for this specific flow.
// This new `vi.fn()` chain is more explicit and reliable for this test case.
server.use( server.use(
http.post('http://localhost/api/auth/refresh-token', () => { http.post('http://localhost/api/auth/refresh-token', () => {
return HttpResponse.json({ token: 'new-refreshed-token' }); return HttpResponse.json({ token: 'new-refreshed-token' });
@@ -130,8 +132,8 @@ describe('API Client', () => {
it('should reject if token refresh fails', async () => { it('should reject if token refresh fails', async () => {
localStorage.setItem('authToken', 'expired-token'); localStorage.setItem('authToken', 'expired-token');
// Also use MSW for this failure case. // Restore the original fetch implementation so MSW can intercept requests.
vi.spyOn(global, 'fetch').mockRestore(); vi.mocked(global.fetch).mockRestore();
// Mock the initial 401 response. // Mock the initial 401 response.
server.use(http.get('http://localhost/api/users/profile', () => new HttpResponse(null, { status: 401 }))); server.use(http.get('http://localhost/api/users/profile', () => new HttpResponse(null, { status: 401 })));
@@ -189,9 +191,7 @@ describe('API Client', () => {
describe('Budget API Functions', () => { describe('Budget API Functions', () => {
it('getBudgets should call the correct endpoint', async () => { it('getBudgets should call the correct endpoint', async () => {
server.use( server.use(
http.get('http://localhost/api/budgets', () => { http.get('http://localhost/api/budgets', () => HttpResponse.json([]))
return HttpResponse.json([]);
})
); );
await apiClient.getBudgets(); await apiClient.getBudgets();
expect(capturedUrl?.pathname).toBe('/api/budgets'); expect(capturedUrl?.pathname).toBe('/api/budgets');
@@ -201,7 +201,7 @@ describe('API Client', () => {
const budgetData = { name: 'Groceries', amount_cents: 50000, period: 'monthly' as const, start_date: '2024-01-01' }; const budgetData = { name: 'Groceries', amount_cents: 50000, period: 'monthly' as const, start_date: '2024-01-01' };
await apiClient.createBudget(budgetData); await apiClient.createBudget(budgetData);
expect(capturedUrl?.pathname).toBe('/api/budgets'); // This was a duplicate, fixed. expect(capturedUrl?.pathname).toBe('/api/budgets');
expect(capturedBody).toEqual(budgetData); expect(capturedBody).toEqual(budgetData);
}); });
@@ -209,13 +209,13 @@ describe('API Client', () => {
const budgetUpdates = { amount_cents: 60000 }; const budgetUpdates = { amount_cents: 60000 };
await apiClient.updateBudget(123, budgetUpdates); await apiClient.updateBudget(123, budgetUpdates);
expect(capturedUrl?.pathname).toBe('/api/budgets/123'); // This was a duplicate, fixed. expect(capturedUrl?.pathname).toBe('/api/budgets/123');
expect(capturedBody).toEqual(budgetUpdates); expect(capturedBody).toEqual(budgetUpdates);
}); });
it('deleteBudget should send a DELETE request to the correct URL', async () => { it('deleteBudget should send a DELETE request to the correct URL', async () => {
await apiClient.deleteBudget(456); await apiClient.deleteBudget(456);
expect(capturedUrl?.pathname).toBe('/api/budgets/456'); // This was a duplicate, fixed. expect(capturedUrl?.pathname).toBe('/api/budgets/456');
}); });
it('getSpendingAnalysis should send a GET request with correct query params', async () => { it('getSpendingAnalysis should send a GET request with correct query params', async () => {
@@ -239,7 +239,7 @@ describe('API Client', () => {
await apiClient.fetchLeaderboard(5); await apiClient.fetchLeaderboard(5);
expect(capturedUrl).not.toBeNull(); // This assertion ensures capturedUrl is not null for the next line expect(capturedUrl).not.toBeNull(); // This assertion ensures capturedUrl is not null for the next line
expect(capturedUrl?.pathname).toBe('/api/achievements/leaderboard'); expect(capturedUrl!.pathname).toBe('/api/achievements/leaderboard');
expect(capturedUrl!.searchParams.get('limit')).toBe('5'); expect(capturedUrl!.searchParams.get('limit')).toBe('5');
}); });

View File

@@ -149,11 +149,28 @@ describe('User DB Service', () => {
const mockClient = { query: vi.fn(), release: vi.fn() }; const mockClient = { query: vi.fn(), release: vi.fn() };
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any); vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
mockClient.query.mockRejectedValue(dbError);
await expect(userRepo.createUser('exists@example.com', 'pass', {})).rejects.toThrow(UniqueConstraintError); // Simulate the transaction flow:
await expect(userRepo.createUser('exists@example.com', 'pass', {})).rejects.toThrow('A user with this email address already exists.'); // 1. BEGIN (success)
// 2. set_config (success)
// 3. INSERT user (failure with unique violation)
// 4. ROLLBACK (success)
mockClient.query
.mockResolvedValueOnce({ rows: [] }) // BEGIN
.mockResolvedValueOnce({ rows: [] }) // set_config
.mockRejectedValueOnce(dbError) // INSERT fails
.mockResolvedValueOnce({ rows: [] }); // ROLLBACK
try {
await userRepo.createUser('exists@example.com', 'pass', {});
expect.fail('Expected createUser to throw UniqueConstraintError');
} catch (error: any) {
expect(error).toBeInstanceOf(UniqueConstraintError);
expect(error.message).toBe('A user with this email address already exists.');
}
expect(mockClient.query).toHaveBeenCalledWith('ROLLBACK'); expect(mockClient.query).toHaveBeenCalledWith('ROLLBACK');
expect(mockClient.release).toHaveBeenCalled();
}); });
}); });