/** * Integration tests for Queue API endpoints * * Tests the HTTP API routes for queue operations by directly invoking the handlers. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { queueManager } from '$lib/server/queue/QueueManager'; import { POST as queuePOST, GET as queueGET } from '../routes/api/queue/+server.js'; import { GET as itemGET, DELETE as itemDELETE } from '../routes/api/queue/[id]/+server.js'; import { POST as retryPOST } from '../routes/api/queue/[id]/retry/+server.js'; describe('Queue API Endpoints', () => { beforeEach(() => { // Clear queue between tests queueManager.getAll().forEach(item => queueManager.remove(item.id)); }); afterEach(() => { // Clean up after tests queueManager.getAll().forEach(item => queueManager.remove(item.id)); }); describe('POST /api/queue', () => { it('should enqueue valid Instagram URL', async () => { const request = new Request('http://localhost/api/queue', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ url: 'https://instagram.com/p/ABC123' }) }); const response = await queuePOST({ request } as any); expect(response.status).toBe(200); const data = await response.json(); expect(data.id).toBeTruthy(); expect(data.url).toBe('https://instagram.com/p/ABC123'); expect(data.status).toBe('pending'); expect(data.enqueuedAt).toBeTruthy(); // Verify item exists in queue const item = queueManager.get(data.id); expect(item).toBeTruthy(); expect(item?.url).toBe('https://instagram.com/p/ABC123'); }); it('should accept Instagram URLs with www', async () => { const request = new Request('http://localhost/api/queue', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ url: 'https://www.instagram.com/p/XYZ789' }) }); const response = await queuePOST({ request } as any); expect(response.status).toBe(200); const data = await response.json(); expect(data.url).toBe('https://www.instagram.com/p/XYZ789'); // Verify item exists in queue const item = queueManager.get(data.id); expect(item).toBeTruthy(); expect(item?.url).toBe('https://www.instagram.com/p/XYZ789'); }); it('should accept Instagram reel URLs', async () => { const request = new Request('http://localhost/api/queue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://instagram.com/reel/ABC123' }) }); const response = await queuePOST({ request } as any); expect(response.status).toBe(200); const data = await response.json(); expect(data.url).toBe('https://instagram.com/reel/ABC123'); }); it('should accept Instagram URLs with query parameters', async () => { const request = new Request('http://localhost/api/queue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://www.instagram.com/reel/DSevV5CDcNm/?utm_source=ig_web_copy_link' }) }); const response = await queuePOST({ request } as any); expect(response.status).toBe(200); const data = await response.json(); expect(data.url).toBe('https://www.instagram.com/reel/DSevV5CDcNm/?utm_source=ig_web_copy_link'); }); it('should accept Instagram IGTV URLs', async () => { const request = new Request('http://localhost/api/queue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://instagram.com/tv/XYZ789' }) }); const response = await queuePOST({ request } as any); expect(response.status).toBe(200); }); it('should reject HTTP (non-HTTPS) URLs', async () => { const request = new Request('http://localhost/api/queue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'http://instagram.com/p/ABC123' }) }); try { const response = await queuePOST({ request } as any); expect(response.status).toBe(400); const data = await response.json(); expect(data.message).toContain('HTTPS'); } catch (err: any) { expect(err.status).toBe(400); expect(err.body.message).toContain('HTTPS'); } }); it('should reject invalid Instagram URL formats', async () => { const invalidUrls = [ 'https://facebook.com/post/123', 'not-a-url', 'https://other-site.com' ]; for (const url of invalidUrls) { const request = new Request('http://localhost/api/queue', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ url }) }); try { const response = await queuePOST({ request } as any); // If we get here, check the response status expect(response.status).toBe(400); const data = await response.json(); // Updated to check for new error messages expect(data.message).toBeTruthy(); } catch (err: any) { // SvelteKit's error() throws - check the error expect(err.status).toBe(400); expect(err.body.message).toBeTruthy(); } } // Verify no items were added to queue expect(queueManager.getAll()).toHaveLength(0); }); it('should reject non-Instagram domains', async () => { const invalidUrls = [ 'https://facebook.com/post/123', 'https://twitter.com/status/456', 'https://example.com', 'https://instagram.com.evil.com/p/123' ]; for (const url of invalidUrls) { const request = new Request('http://localhost/api/queue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }); try { const response = await queuePOST({ request } as any); expect(response.status).toBe(400); const data = await response.json(); expect(data.message).toContain('instagram.com'); } catch (err: any) { expect(err.status).toBe(400); expect(err.body.message).toContain('instagram.com'); } } }); it('should reject missing URL', async () => { const request = new Request('http://localhost/api/queue', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({}) }); try { const response = await queuePOST({ request } as any); expect(response.status).toBe(400); const data = await response.json(); expect(data.message).toBe('URL is required and must be a string'); } catch (err: any) { expect(err.status).toBe(400); expect(err.body.message).toBe('URL is required and must be a string'); } }); it('should reject non-JSON body', async () => { const request = new Request('http://localhost/api/queue', { method: 'POST', headers: { 'Content-Type': 'text/plain', }, body: 'not json' }); try { const response = await queuePOST({ request } as any); expect(response.status).toBe(400); const data = await response.json(); expect(data.message).toBe('Invalid JSON in request body'); } catch (err: any) { expect(err.status).toBe(400); expect(err.body.message).toBe('Invalid JSON in request body'); } }); }); describe('GET /api/queue', () => { it('should return empty list when no items', async () => { const url = new URL('http://localhost/api/queue'); const request = new Request(url); const response = await queueGET({ request, url } as any); expect(response.status).toBe(200); const data = await response.json(); expect(data.items).toEqual([]); expect(data.total).toBe(0); expect(data.pagination.offset).toBe(0); expect(data.pagination.limit).toBe(50); }); it('should return queued items', async () => { // Add test items const item1 = queueManager.enqueue('https://instagram.com/p/TEST1'); const item2 = queueManager.enqueue('https://instagram.com/p/TEST2'); const url = new URL('http://localhost/api/queue'); const request = new Request(url); const response = await queueGET({ request, url } as any); expect(response.status).toBe(200); const data = await response.json(); expect(data.total).toBe(2); expect(data.items).toHaveLength(2); expect(data.items[0].url).toBe('https://instagram.com/p/TEST1'); expect(data.items[1].url).toBe('https://instagram.com/p/TEST2'); }); it('should filter by status', async () => { // Add test items with different statuses const item1 = queueManager.enqueue('https://instagram.com/p/PENDING'); const item2 = queueManager.enqueue('https://instagram.com/p/ERROR'); // Set one to error status queueManager.updateStatus(item2.id, 'error', { message: 'Test error' }); const url = new URL('http://localhost/api/queue?status=error'); const request = new Request(url); const response = await queueGET({ request, url } as any); expect(response.status).toBe(200); const data = await response.json(); expect(data.total).toBe(1); expect(data.items).toHaveLength(1); expect(data.items[0].status).toBe('error'); expect(data.items[0].url).toBe('https://instagram.com/p/ERROR'); }); it('should handle pagination', async () => { // Add multiple test items for (let i = 1; i <= 5; i++) { queueManager.enqueue(`https://instagram.com/p/TEST${i}`); } const url = new URL('http://localhost/api/queue?limit=2&offset=1'); const request = new Request(url); const response = await queueGET({ request, url } as any); expect(response.status).toBe(200); const data = await response.json(); expect(data.total).toBe(5); expect(data.items).toHaveLength(2); expect(data.pagination.offset).toBe(1); expect(data.pagination.limit).toBe(2); // Items are sorted by enqueued time (newest first), so with offset=1, limit=2 we get items 2-3 from the sorted list }); it('should validate query parameters', async () => { // Invalid status try { let url = new URL('http://localhost/api/queue?status=invalid'); let request = new Request(url); let response = await queueGET({ request, url } as any); expect(response.status).toBe(400); let data = await response.json(); expect(data.message).toBe('Invalid status filter. Must be one of: pending, in_progress, success, unhealthy, error'); } catch (err: any) { expect(err.status).toBe(400); expect(err.body.message).toBe('Invalid status filter. Must be one of: pending, in_progress, success, unhealthy, error'); } // Invalid limit (negative) try { let url = new URL('http://localhost/api/queue?limit=-1'); let request = new Request(url); let response = await queueGET({ request, url } as any); expect(response.status).toBe(400); let data = await response.json(); expect(data.message).toBe('Limit must be a positive integer'); } catch (err: any) { expect(err.status).toBe(400); expect(err.body.message).toBe('Limit must be a positive integer'); } // Invalid offset (negative) try { let url = new URL('http://localhost/api/queue?offset=-1'); let request = new Request(url); let response = await queueGET({ request, url } as any); expect(response.status).toBe(400); let data = await response.json(); expect(data.message).toBe('Offset must be a non-negative integer'); } catch (err: any) { expect(err.status).toBe(400); expect(err.body.message).toBe('Offset must be a non-negative integer'); } // Limit too large try { let url = new URL('http://localhost/api/queue?limit=999'); let request = new Request(url); let response = await queueGET({ request, url } as any); expect(response.status).toBe(400); let data = await response.json(); expect(data.message).toBe('Limit cannot exceed 200'); } catch (err: any) { expect(err.status).toBe(400); expect(err.body.message).toBe('Limit cannot exceed 200'); } }); }); describe('GET /api/queue/[id]', () => { it('should return queue item by ID', async () => { // Add test item const item = queueManager.enqueue('https://instagram.com/p/DETAIL'); const response = await itemGET({ params: { id: item.id } } as any); expect(response.status).toBe(200); const data = await response.json(); expect(data.id).toBe(item.id); expect(data.url).toBe('https://instagram.com/p/DETAIL'); expect(data.status).toBe('pending'); }); it('should return 404 for non-existent ID', async () => { const fakeId = '550e8400-e29b-41d4-a716-446655440000'; // Valid v4 UUID format but non-existent try { const response = await itemGET({ params: { id: fakeId } } as any); expect(response.status).toBe(404); const data = await response.json(); expect(data.message).toBe('Queue item not found'); } catch (err: any) { expect(err.status).toBe(404); expect(err.body.message).toBe('Queue item not found'); } }); it('should validate ID format', async () => { try { const response = await itemGET({ params: { id: 'invalid-id' } } as any); expect(response.status).toBe(400); const data = await response.json(); expect(data.message).toBe('Invalid queue item ID format'); } catch (err: any) { expect(err.status).toBe(400); expect(err.body.message).toBe('Invalid queue item ID format'); } }); }); describe('POST /api/queue/[id]/retry', () => { it('should retry error item', async () => { // Add test item and set to error const item = queueManager.enqueue('https://instagram.com/p/RETRY'); queueManager.updateStatus(item.id, 'error', { message: 'Test error' }); const request = new Request(`http://localhost/api/queue/${item.id}/retry`, { method: 'POST' }); const response = await retryPOST({ request, params: { id: item.id } } as any); expect(response.status).toBe(200); const data = await response.json(); expect(data.message).toBe('Queue item has been reset and will be reprocessed'); expect(data.success).toBe(true); // Verify item status was reset const updatedItem = queueManager.get(item.id); expect(updatedItem?.status).toBe('pending'); expect(updatedItem?.error).toBeUndefined(); // error field is cleared (undefined, not null) }); it('should retry unhealthy item', async () => { // Add test item and set to unhealthy const item = queueManager.enqueue('https://instagram.com/p/UNHEALTHY'); queueManager.updateStatus(item.id, 'unhealthy', { phase: 'extraction', attempts: 3, lastAttempt: new Date(), message: 'Max retries exceeded' }); const request = new Request(`http://localhost/api/queue/${item.id}/retry`, { method: 'POST' }); const response = await retryPOST({ request, params: { id: item.id } } as any); expect(response.status).toBe(200); const data = await response.json(); expect(data.message).toBe('Queue item has been reset and will be reprocessed'); expect(data.success).toBe(true); // Verify item status was reset const updatedItem = queueManager.get(item.id); expect(updatedItem?.status).toBe('pending'); }); it('should reject retry for non-retryable statuses', async () => { // Add test item (default status is 'pending') const item = queueManager.enqueue('https://instagram.com/p/PENDING'); const request = new Request(`http://localhost/api/queue/${item.id}/retry`, { method: 'POST' }); // Item is pending (cannot retry) try { const response = await retryPOST({ request, params: { id: item.id } } as any); expect(response.status).toBe(409); const data = await response.json(); expect(data.message).toBe("Cannot retry item with status 'pending'. Only 'error' and 'unhealthy' items can be retried."); } catch (err: any) { expect(err.status).toBe(409); expect(err.body.message).toBe("Cannot retry item with status 'pending'. Only 'error' and 'unhealthy' items can be retried."); } }); it('should return 404 for non-existent item', async () => { const fakeId = '550e8400-e29b-41d4-a716-446655440000'; // Valid v4 UUID format but non-existent const request = new Request(`http://localhost/api/queue/${fakeId}/retry`, { method: 'POST' }); try { const response = await retryPOST({ request, params: { id: fakeId } } as any); expect(response.status).toBe(404); const data = await response.json(); expect(data.message).toBe('Queue item not found'); } catch (err: any) { expect(err.status).toBe(404); expect(err.body.message).toBe('Queue item not found'); } }); }); describe('DELETE /api/queue/[id]', () => { it('should delete queue item successfully', async () => { // Create an item const item = queueManager.enqueue('https://instagram.com/p/DELETE123'); // Mark it as success (completed) queueManager.updateStatus(item.id, 'success'); const request = new Request(`http://localhost/api/queue/${item.id}`, { method: 'DELETE' }); const response = await itemDELETE({ request, params: { id: item.id } } as any); expect(response.status).toBe(200); const data = await response.json(); expect(data.success).toBe(true); expect(data.message).toBe('Queue item removed successfully'); // Verify item no longer exists expect(queueManager.get(item.id)).toBeUndefined(); }); it('should return 404 for non-existent item', async () => { const fakeId = '550e8400-e29b-41d4-a716-446655440000'; const request = new Request(`http://localhost/api/queue/${fakeId}`, { method: 'DELETE' }); try { const response = await itemDELETE({ request, params: { id: fakeId } } as any); expect(response.status).toBe(404); const data = await response.json(); expect(data.message).toBe('Queue item not found'); } catch (err: any) { expect(err.status).toBe(404); expect(err.body.message).toBe('Queue item not found'); } }); it('should return 409 for in-progress items', async () => { // Create an item and mark it as in progress const item = queueManager.enqueue('https://instagram.com/p/PROCESSING'); queueManager.updateStatus(item.id, 'in_progress', { phase: 'extraction' }); const request = new Request(`http://localhost/api/queue/${item.id}`, { method: 'DELETE' }); try { const response = await itemDELETE({ request, params: { id: item.id } } as any); expect(response.status).toBe(409); const data = await response.json(); expect(data.message).toBe('Cannot delete item that is currently being processed'); } catch (err: any) { expect(err.status).toBe(409); expect(err.body.message).toBe('Cannot delete item that is currently being processed'); } // Verify item still exists expect(queueManager.get(item.id)).toBeTruthy(); }); it('should validate ID format', async () => { const invalidIds = ['not-a-uuid', '12345', 'abc-def-ghi']; for (const invalidId of invalidIds) { const request = new Request(`http://localhost/api/queue/${invalidId}`, { method: 'DELETE' }); try { const response = await itemDELETE({ request, params: { id: invalidId } } as any); expect(response.status).toBe(400); const data = await response.json(); expect(data.message).toBe('Invalid queue item ID format'); } catch (err: any) { expect(err.status).toBe(400); expect(err.body.message).toBe('Invalid queue item ID format'); } } }); }); });