Add comprehensive test suite: 219 new tests across backend and frontend
Backend (103 tests): - Unit tests for listing_service, export_service, district_service - Regression tests for API response contracts and query parameter validation - Integration tests for API workflows, Redis listing cache, listing processor pipeline, and repository advanced queries - E2E tests for streaming with filters, batching, caching, and task management Frontend (116 tests): - Service tests for apiClient, streamingService, taskService, listingService, healthService - Hook tests for useTaskProgress (WebSocket + polling) - Component tests for PropertyCard, FilterPanel, Header, ListView, TaskProgressDrawer, TaskIndicator, StreamingProgressBar, HealthIndicator - E2E tests for filter-stream-display flow Infrastructure: - Add pytest-xdist and test markers (regression, integration, e2e) - Add conftest fixtures: fake_redis, rent_listing_factory, seeded_repository - Add vitest + testing-library + MSW for frontend testing
This commit is contained in:
parent
a3ac9cc060
commit
8d22c97320
36 changed files with 5447 additions and 19 deletions
145
frontend/src/services/__tests__/apiClient.test.ts
Normal file
145
frontend/src/services/__tests__/apiClient.test.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
|
||||
import { server } from '@/__tests__/mocks/server';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { mockUser } from '@/__tests__/helpers';
|
||||
import { apiRequest } from '@/services/apiClient';
|
||||
import { ApiError } from '@/types';
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
describe('apiClient', () => {
|
||||
it('makes GET requests and returns JSON', async () => {
|
||||
server.use(
|
||||
http.get('/api/status', () => {
|
||||
return HttpResponse.json({ status: 'OK' });
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await apiRequest<{ status: string }>(mockUser(), '/api/status');
|
||||
expect(result).toEqual({ status: 'OK' });
|
||||
});
|
||||
|
||||
it('includes Authorization header', async () => {
|
||||
let receivedAuth: string | null = null;
|
||||
|
||||
server.use(
|
||||
http.get('/api/test-auth', ({ request }) => {
|
||||
receivedAuth = request.headers.get('Authorization');
|
||||
return HttpResponse.json({ ok: true });
|
||||
}),
|
||||
);
|
||||
|
||||
const user = mockUser({ accessToken: 'my-secret-token' });
|
||||
await apiRequest(user, '/api/test-auth');
|
||||
|
||||
expect(receivedAuth).toBe('Bearer my-secret-token');
|
||||
});
|
||||
|
||||
it('builds URL with query params', async () => {
|
||||
let requestUrl = '';
|
||||
|
||||
server.use(
|
||||
http.get('/api/search', ({ request }) => {
|
||||
requestUrl = request.url;
|
||||
return HttpResponse.json({ results: [] });
|
||||
}),
|
||||
);
|
||||
|
||||
await apiRequest(mockUser(), '/api/search', {
|
||||
params: { limit: 5, active: true },
|
||||
});
|
||||
|
||||
const url = new URL(requestUrl);
|
||||
expect(url.searchParams.get('limit')).toBe('5');
|
||||
expect(url.searchParams.get('active')).toBe('true');
|
||||
});
|
||||
|
||||
it('sends POST requests with JSON body', async () => {
|
||||
let receivedMethod = '';
|
||||
let receivedBody: unknown = null;
|
||||
|
||||
server.use(
|
||||
http.post('/api/create', async ({ request }) => {
|
||||
receivedMethod = request.method;
|
||||
receivedBody = await request.json();
|
||||
return HttpResponse.json({ id: 1 });
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await apiRequest(mockUser(), '/api/create', {
|
||||
method: 'POST',
|
||||
body: { key: 'value' },
|
||||
});
|
||||
|
||||
expect(receivedMethod).toBe('POST');
|
||||
expect(receivedBody).toEqual({ key: 'value' });
|
||||
expect(result).toEqual({ id: 1 });
|
||||
});
|
||||
|
||||
it('throws ApiError on 404', async () => {
|
||||
server.use(
|
||||
http.get('/api/missing', () => {
|
||||
return new HttpResponse(null, { status: 404 });
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(apiRequest(mockUser(), '/api/missing')).rejects.toThrow(ApiError);
|
||||
await expect(apiRequest(mockUser(), '/api/missing')).rejects.toMatchObject({
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws ApiError on 500', async () => {
|
||||
server.use(
|
||||
http.get('/api/error', () => {
|
||||
return new HttpResponse(null, { status: 500 });
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(apiRequest(mockUser(), '/api/error')).rejects.toThrow(ApiError);
|
||||
await expect(apiRequest(mockUser(), '/api/error')).rejects.toMatchObject({
|
||||
statusCode: 500,
|
||||
});
|
||||
});
|
||||
|
||||
it('excludes undefined params from URL', async () => {
|
||||
let requestUrl = '';
|
||||
|
||||
server.use(
|
||||
http.get('/api/filter', ({ request }) => {
|
||||
requestUrl = request.url;
|
||||
return HttpResponse.json({});
|
||||
}),
|
||||
);
|
||||
|
||||
await apiRequest(mockUser(), '/api/filter', {
|
||||
params: { limit: 10, offset: undefined, name: 'test' },
|
||||
});
|
||||
|
||||
const url = new URL(requestUrl);
|
||||
expect(url.searchParams.get('limit')).toBe('10');
|
||||
expect(url.searchParams.get('name')).toBe('test');
|
||||
expect(url.searchParams.has('offset')).toBe(false);
|
||||
});
|
||||
|
||||
it('serializes Date params as ISO strings', async () => {
|
||||
let requestUrl = '';
|
||||
|
||||
server.use(
|
||||
http.get('/api/dated', ({ request }) => {
|
||||
requestUrl = request.url;
|
||||
return HttpResponse.json({});
|
||||
}),
|
||||
);
|
||||
|
||||
const date = new Date('2024-06-15T12:00:00.000Z');
|
||||
await apiRequest(mockUser(), '/api/dated', {
|
||||
params: { since: date },
|
||||
});
|
||||
|
||||
const url = new URL(requestUrl);
|
||||
expect(url.searchParams.get('since')).toBe('2024-06-15T12:00:00.000Z');
|
||||
});
|
||||
});
|
||||
81
frontend/src/services/__tests__/healthService.test.ts
Normal file
81
frontend/src/services/__tests__/healthService.test.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { checkBackendHealth } from '@/services/healthService';
|
||||
|
||||
// In jsdom, AbortSignal.timeout() may not exist or may produce signals that
|
||||
// internal fetch rejects. We mock fetch directly for all health service tests.
|
||||
describe('healthService', () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('returns healthy when backend responds with OK', async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ status: 'OK' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await checkBackendHealth();
|
||||
expect(result.status).toBe('healthy');
|
||||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns unhealthy on non-ok HTTP response', async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
new Response(null, { status: 500 }),
|
||||
);
|
||||
|
||||
const result = await checkBackendHealth();
|
||||
expect(result.status).toBe('unhealthy');
|
||||
expect(result.error).toBe('HTTP 500');
|
||||
});
|
||||
|
||||
it('returns unhealthy with unexpected response body', async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ status: 'FAIL' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await checkBackendHealth();
|
||||
expect(result.status).toBe('unhealthy');
|
||||
expect(result.error).toBe('Unexpected response');
|
||||
});
|
||||
|
||||
it('measures latency as a non-negative number', async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ status: 'OK' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await checkBackendHealth();
|
||||
expect(result.latencyMs).toBeDefined();
|
||||
expect(typeof result.latencyMs).toBe('number');
|
||||
expect(result.latencyMs!).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('returns unhealthy on network error', async () => {
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new TypeError('Failed to fetch'));
|
||||
|
||||
const result = await checkBackendHealth();
|
||||
expect(result.status).toBe('unhealthy');
|
||||
expect(result.error).toBe('Failed to fetch');
|
||||
expect(result.latencyMs).toBeDefined();
|
||||
});
|
||||
|
||||
it('handles AbortError as timeout', async () => {
|
||||
const abortError = new Error('The operation was aborted');
|
||||
abortError.name = 'AbortError';
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(abortError);
|
||||
|
||||
const result = await checkBackendHealth();
|
||||
expect(result.status).toBe('unhealthy');
|
||||
expect(result.error).toBe('Request timeout');
|
||||
});
|
||||
});
|
||||
85
frontend/src/services/__tests__/listingService.test.ts
Normal file
85
frontend/src/services/__tests__/listingService.test.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
|
||||
import { server } from '@/__tests__/mocks/server';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { mockUser, createMockFeatureCollection } from '@/__tests__/helpers';
|
||||
import { fetchListingGeoJSON, refreshListings } from '@/services/listingService';
|
||||
import type { ParameterValues } from '@/components/FilterPanel';
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
const defaultParams: ParameterValues = {
|
||||
metric: 'qmprice',
|
||||
listing_type: 'RENT',
|
||||
min_bedrooms: 1,
|
||||
max_bedrooms: 3,
|
||||
min_price: 1000,
|
||||
max_price: 5000,
|
||||
district: '',
|
||||
};
|
||||
|
||||
describe('listingService', () => {
|
||||
it('fetchListingGeoJSON returns a FeatureCollection', async () => {
|
||||
const collection = createMockFeatureCollection(3);
|
||||
|
||||
server.use(
|
||||
http.get('/api/listing_geojson', () => {
|
||||
return HttpResponse.json(collection);
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await fetchListingGeoJSON(mockUser(), defaultParams);
|
||||
expect(result.type).toBe('FeatureCollection');
|
||||
expect(result.features).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('fetchListingGeoJSON sends query parameters', async () => {
|
||||
let requestUrl = '';
|
||||
|
||||
server.use(
|
||||
http.get('/api/listing_geojson', ({ request }) => {
|
||||
requestUrl = request.url;
|
||||
return HttpResponse.json({ type: 'FeatureCollection', features: [] });
|
||||
}),
|
||||
);
|
||||
|
||||
await fetchListingGeoJSON(mockUser(), defaultParams);
|
||||
|
||||
const url = new URL(requestUrl);
|
||||
expect(url.searchParams.get('listing_type')).toBe('RENT');
|
||||
expect(url.searchParams.get('min_bedrooms')).toBe('1');
|
||||
expect(url.searchParams.get('max_bedrooms')).toBe('3');
|
||||
expect(url.searchParams.get('min_price')).toBe('1000');
|
||||
expect(url.searchParams.get('max_price')).toBe('5000');
|
||||
});
|
||||
|
||||
it('refreshListings returns task_id', async () => {
|
||||
server.use(
|
||||
http.post('/api/refresh_listings', () => {
|
||||
return HttpResponse.json({ task_id: 'abc-123' });
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await refreshListings(mockUser(), defaultParams);
|
||||
expect(result.task_id).toBe('abc-123');
|
||||
});
|
||||
|
||||
it('refreshListings sends query parameters', async () => {
|
||||
let requestUrl = '';
|
||||
|
||||
server.use(
|
||||
http.post('/api/refresh_listings', ({ request }) => {
|
||||
requestUrl = request.url;
|
||||
return HttpResponse.json({ task_id: 'xyz' });
|
||||
}),
|
||||
);
|
||||
|
||||
await refreshListings(mockUser(), defaultParams);
|
||||
|
||||
const url = new URL(requestUrl);
|
||||
expect(url.searchParams.get('listing_type')).toBe('RENT');
|
||||
expect(url.searchParams.get('min_bedrooms')).toBe('1');
|
||||
expect(url.searchParams.get('min_price')).toBe('1000');
|
||||
});
|
||||
});
|
||||
214
frontend/src/services/__tests__/streamingService.test.ts
Normal file
214
frontend/src/services/__tests__/streamingService.test.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { mockUser, createMockFeature } from '@/__tests__/helpers';
|
||||
import { streamListingGeoJSON } from '@/services/streamingService';
|
||||
import { ApiError } from '@/types';
|
||||
import type { ParameterValues } from '@/components/FilterPanel';
|
||||
|
||||
const defaultParams: ParameterValues = {
|
||||
metric: 'qmprice',
|
||||
listing_type: 'RENT',
|
||||
min_bedrooms: 1,
|
||||
max_bedrooms: 3,
|
||||
min_price: 1000,
|
||||
max_price: 5000,
|
||||
district: '',
|
||||
};
|
||||
|
||||
function createMockResponse(lines: string[]) {
|
||||
const body = lines.join('\n') + '\n';
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(body));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/x-ndjson' },
|
||||
});
|
||||
}
|
||||
|
||||
describe('streamingService', () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('yields feature batches', async () => {
|
||||
const features = [createMockFeature(), createMockFeature({ rooms: 3 })];
|
||||
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
createMockResponse([
|
||||
JSON.stringify({ type: 'metadata', total_expected: 2 }),
|
||||
JSON.stringify({ type: 'batch', features }),
|
||||
JSON.stringify({ type: 'complete', total: 2 }),
|
||||
]),
|
||||
);
|
||||
|
||||
const batches: unknown[][] = [];
|
||||
for await (const batch of streamListingGeoJSON(mockUser(), defaultParams)) {
|
||||
batches.push(batch);
|
||||
}
|
||||
|
||||
expect(batches).toHaveLength(1);
|
||||
expect(batches[0]).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('calls onProgress with metadata', async () => {
|
||||
const onProgress = vi.fn();
|
||||
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
createMockResponse([
|
||||
JSON.stringify({ type: 'metadata', total_expected: 42 }),
|
||||
JSON.stringify({ type: 'complete', total: 0 }),
|
||||
]),
|
||||
);
|
||||
|
||||
const gen = streamListingGeoJSON(mockUser(), defaultParams, onProgress);
|
||||
// Consume the generator
|
||||
for await (const _ of gen) { /* drain */ }
|
||||
|
||||
expect(onProgress).toHaveBeenCalledWith({ count: 0, total: 42 });
|
||||
});
|
||||
|
||||
it('calls onProgress with incrementing count on batches', async () => {
|
||||
const features1 = [createMockFeature()];
|
||||
const features2 = [createMockFeature(), createMockFeature()];
|
||||
const onProgress = vi.fn();
|
||||
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
createMockResponse([
|
||||
JSON.stringify({ type: 'metadata', total_expected: 3 }),
|
||||
JSON.stringify({ type: 'batch', features: features1 }),
|
||||
JSON.stringify({ type: 'batch', features: features2 }),
|
||||
JSON.stringify({ type: 'complete', total: 3 }),
|
||||
]),
|
||||
);
|
||||
|
||||
for await (const _ of streamListingGeoJSON(mockUser(), defaultParams, onProgress)) { /* drain */ }
|
||||
|
||||
// metadata: count=0, batch1: count=1, batch2: count=3, complete: count=3
|
||||
expect(onProgress).toHaveBeenCalledWith({ count: 1 });
|
||||
expect(onProgress).toHaveBeenCalledWith({ count: 3 });
|
||||
});
|
||||
|
||||
it('calls onProgress on complete message', async () => {
|
||||
const onProgress = vi.fn();
|
||||
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
createMockResponse([
|
||||
JSON.stringify({ type: 'metadata', total_expected: 5 }),
|
||||
JSON.stringify({ type: 'complete', total: 5 }),
|
||||
]),
|
||||
);
|
||||
|
||||
for await (const _ of streamListingGeoJSON(mockUser(), defaultParams, onProgress)) { /* drain */ }
|
||||
|
||||
expect(onProgress).toHaveBeenCalledWith({ count: 5, total: 5 });
|
||||
});
|
||||
|
||||
it('handles multiple batches and yields each separately', async () => {
|
||||
const batch1 = [createMockFeature({ rooms: 1 })];
|
||||
const batch2 = [createMockFeature({ rooms: 2 })];
|
||||
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
createMockResponse([
|
||||
JSON.stringify({ type: 'metadata', total_expected: 2 }),
|
||||
JSON.stringify({ type: 'batch', features: batch1 }),
|
||||
JSON.stringify({ type: 'batch', features: batch2 }),
|
||||
JSON.stringify({ type: 'complete', total: 2 }),
|
||||
]),
|
||||
);
|
||||
|
||||
const batches: unknown[][] = [];
|
||||
for await (const batch of streamListingGeoJSON(mockUser(), defaultParams)) {
|
||||
batches.push(batch);
|
||||
}
|
||||
|
||||
expect(batches).toHaveLength(2);
|
||||
expect(batches[0]).toHaveLength(1);
|
||||
expect(batches[1]).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('handles abort signal', async () => {
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new DOMException('Aborted', 'AbortError'));
|
||||
|
||||
const batches: unknown[][] = [];
|
||||
try {
|
||||
for await (const batch of streamListingGeoJSON(mockUser(), defaultParams, undefined, { signal: controller.signal })) {
|
||||
batches.push(batch);
|
||||
}
|
||||
} catch {
|
||||
// Expected: fetch throws on aborted signal
|
||||
}
|
||||
|
||||
expect(batches).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('handles malformed JSON lines without crashing', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const features = [createMockFeature()];
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
createMockResponse([
|
||||
JSON.stringify({ type: 'metadata', total_expected: 1 }),
|
||||
'this is not valid json{{{',
|
||||
JSON.stringify({ type: 'batch', features }),
|
||||
JSON.stringify({ type: 'complete', total: 1 }),
|
||||
]),
|
||||
);
|
||||
|
||||
const batches: unknown[][] = [];
|
||||
for await (const batch of streamListingGeoJSON(mockUser(), defaultParams)) {
|
||||
batches.push(batch);
|
||||
}
|
||||
|
||||
// Should still yield the valid batch
|
||||
expect(batches).toHaveLength(1);
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('handles empty stream with no batches', async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
createMockResponse([
|
||||
JSON.stringify({ type: 'metadata', total_expected: 0 }),
|
||||
JSON.stringify({ type: 'complete', total: 0 }),
|
||||
]),
|
||||
);
|
||||
|
||||
const batches: unknown[][] = [];
|
||||
for await (const batch of streamListingGeoJSON(mockUser(), defaultParams)) {
|
||||
batches.push(batch);
|
||||
}
|
||||
|
||||
expect(batches).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('throws ApiError on HTTP error response', async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
new Response(null, { status: 401 }),
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
for await (const _ of streamListingGeoJSON(mockUser(), defaultParams)) { /* drain */ }
|
||||
}).rejects.toThrow(ApiError);
|
||||
});
|
||||
|
||||
it('throws when response body is null', async () => {
|
||||
const response = new Response(null, { status: 200 });
|
||||
// Force body to null by creating a response without a body that reports ok
|
||||
Object.defineProperty(response, 'body', { value: null });
|
||||
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(response);
|
||||
|
||||
await expect(async () => {
|
||||
for await (const _ of streamListingGeoJSON(mockUser(), defaultParams)) { /* drain */ }
|
||||
}).rejects.toThrow('No response body');
|
||||
});
|
||||
});
|
||||
90
frontend/src/services/__tests__/taskService.test.ts
Normal file
90
frontend/src/services/__tests__/taskService.test.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
|
||||
import { server } from '@/__tests__/mocks/server';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { mockUser } from '@/__tests__/helpers';
|
||||
import { fetchTasksForUser, fetchTaskStatus, cancelTask, clearAllTasks } from '@/services/taskService';
|
||||
import { ApiError } from '@/types';
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
describe('taskService', () => {
|
||||
it('fetchTasksForUser returns task IDs', async () => {
|
||||
server.use(
|
||||
http.get('/api/tasks_for_user', () => {
|
||||
return HttpResponse.json(['task-1', 'task-2']);
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await fetchTasksForUser(mockUser());
|
||||
expect(result).toEqual(['task-1', 'task-2']);
|
||||
});
|
||||
|
||||
it('fetchTaskStatus returns task status response', async () => {
|
||||
server.use(
|
||||
http.get('/api/task_status', () => {
|
||||
return HttpResponse.json({
|
||||
task_id: 'task-42',
|
||||
status: 'STARTED',
|
||||
result: null,
|
||||
progress: 0.5,
|
||||
processed: 25,
|
||||
total: 50,
|
||||
message: 'Processing...',
|
||||
error: null,
|
||||
traceback: null,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await fetchTaskStatus(mockUser(), 'task-42');
|
||||
expect(result.task_id).toBe('task-42');
|
||||
expect(result.status).toBe('STARTED');
|
||||
expect(result.progress).toBe(0.5);
|
||||
expect(result.processed).toBe(25);
|
||||
expect(result.total).toBe(50);
|
||||
expect(result.message).toBe('Processing...');
|
||||
});
|
||||
|
||||
it('cancelTask sends POST and returns success', async () => {
|
||||
let requestUrl = '';
|
||||
|
||||
server.use(
|
||||
http.post('/api/cancel_task', ({ request }) => {
|
||||
requestUrl = request.url;
|
||||
return HttpResponse.json({ success: true, message: 'Task cancelled' });
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await cancelTask(mockUser(), 'task-99');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('Task cancelled');
|
||||
expect(requestUrl).toContain('task_id=task-99');
|
||||
});
|
||||
|
||||
it('clearAllTasks sends POST and returns count', async () => {
|
||||
server.use(
|
||||
http.post('/api/clear_all_tasks', () => {
|
||||
return HttpResponse.json({ success: true, count: 5, message: 'Cleared 5 tasks' });
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await clearAllTasks(mockUser());
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.count).toBe(5);
|
||||
});
|
||||
|
||||
it('fetchTaskStatus throws ApiError on server error', async () => {
|
||||
server.use(
|
||||
http.get('/api/task_status', () => {
|
||||
return new HttpResponse(null, { status: 500 });
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(fetchTaskStatus(mockUser(), 'task-fail')).rejects.toThrow(ApiError);
|
||||
await expect(fetchTaskStatus(mockUser(), 'task-fail')).rejects.toMatchObject({
|
||||
statusCode: 500,
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue