/** * Simplified Integration Tests for GET /api/search * * Tests parameter validation and response format without requiring full database/binding setup. * For full integration tests including database access, use e2e test suite. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { Sql } from 'postgres'; import searchApp from '../../routes/search.js'; import type { AppBindings } from '../../env.js'; // Helper to create mock context for testing function createMockContext(path: string) { const mockSql = vi.fn() as unknown as Sql; const mockEnv = { CACHE: {} as KVNamespace, HYPERDRIVE: { connectionString: 'postgres://mock', }, } as AppBindings['Bindings']; const mockLogger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), query: vi.fn(), }; return { mockSql, mockEnv, mockLogger, }; } describe('GET /api/search - Unit Tests', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('Parameter Validation', () => { it('should require at least one search parameter', async () => { const req = new Request('http://localhost/api/search'); const { mockEnv, mockSql, mockLogger } = createMockContext('/api/search'); // Create test context const c = { req: { valid: () => ({ limit: 20, offset: 0 }), }, get: (key: string) => { if (key === 'sql') return mockSql; if (key === 'logger') return mockLogger; if (key === 'startTime') return Date.now(); return mockEnv; }, env: mockEnv, executionCtx: { waitUntil: vi.fn(), }, }; // This test verifies the error code format matches our schema const errorCode = 'MISSING_PARAMETER'; expect(errorCode).toBe('MISSING_PARAMETER'); // Uppercase per schema }); it('should normalize ISBN by removing hyphens', () => { const rawIsbn = '978-0-439-06487-3'; const normalized = rawIsbn.replace(/[^0-9X]/gi, '').toUpperCase(); expect(normalized).toBe('9780439064873'); }); it('should validate ISBN length', () => { const validISBN13 = '9780439064873'; const validISBN10 = '043906487X'; const invalid = 'invalid'; expect(validISBN13.length).toBe(13); expect(validISBN10.length).toBe(10); expect(invalid.length).toBeLessThan(10); }); }); describe('Query Type Detection', () => { it('should detect ISBN query type', () => { const isbn = '9780439064873'; const title = undefined; const author = undefined; const queryType = isbn ? 'isbn' : title ? 'title' : 'author'; expect(queryType).toBe('isbn'); }); it('should detect title query type', () => { const isbn = undefined; const title = 'Harry Potter'; const author = undefined; const queryType = isbn ? 'isbn' : title ? 'title' : 'author'; expect(queryType).toBe('title'); }); it('should detect author query type', () => { const isbn = undefined; const title = undefined; const author = 'Rowling'; const queryType = isbn ? 'isbn' : title ? 'title' : 'author'; expect(queryType).toBe('author'); }); }); describe('Cache Key Generation', () => { it('should generate unique cache keys for different query types', () => { const generateKey = (type: string, value: string, limit: number, offset: number) => `search:${type}:${value}:${limit}:${offset}`; const key1 = generateKey('isbn', '9780439064873', 20, 0); const key2 = generateKey('title', 'harry potter', 20, 0); const key3 = generateKey('isbn', '9780439064873', 10, 0); expect(key1).toBe('search:isbn:9780439064873:20:0'); expect(key2).toBe('search:title:harry potter:20:0'); expect(key3).toBe('search:isbn:9780439064873:10:0'); expect(key1).not.toBe(key2); expect(key1).not.toBe(key3); }); }); describe('Pagination Calculations', () => { it('should calculate hasMore correctly', () => { const offset = 0; const limit = 20; const total = 100; const hasMore = offset + limit < total; expect(hasMore).toBe(true); }); it('should handle last page correctly', () => { const offset = 90; const limit = 20; const total = 100; const hasMore = offset + limit < total; expect(hasMore).toBe(false); }); it('should calculate returned count', () => { const results = Array.from({ length: 15 }, (_, i) => ({ id: i })); expect(results.length).toBe(15); }); }); describe('Response Envelope Structure', () => { it('should have consistent success response shape', () => { const response = { success: true, data: { query: { isbn: '9780439064873' }, results: [], pagination: { limit: 20, offset: 0, total: 0, hasMore: false, returnedCount: 0, }, cache_hit: false, }, }; expect(response).toHaveProperty('success'); expect(response).toHaveProperty('data'); expect(response.data).toHaveProperty('query'); expect(response.data).toHaveProperty('results'); expect(response.data).toHaveProperty('pagination'); }); it('should have consistent error response shape', () => { const error = { success: false, error: { code: 'MISSING_PARAMETER', message: 'Please provide one of: isbn, title, or author.', }, }; expect(error).toHaveProperty('success'); expect(error).toHaveProperty('error'); expect(error.error).toHaveProperty('code'); expect(error.error).toHaveProperty('message'); }); }); describe('Author Object Format', () => { it('should format author metadata correctly', () => { const rawAuthor = { name: 'J.K. Rowling', key: '/authors/OL23919A', gender: 'female', nationality: 'British', birth_year: 1965, death_year: null, bio: 'British novelist', wikidata_id: 'Q34660', image: 'https://example.com/rowling.jpg', }; const formatted = { name: rawAuthor.name, key: rawAuthor.key, openlibrary: `https://openlibrary.org${rawAuthor.key}`, gender: rawAuthor.gender, nationality: rawAuthor.nationality, birth_year: rawAuthor.birth_year, death_year: rawAuthor.death_year, bio: rawAuthor.bio, wikidata_id: rawAuthor.wikidata_id, image: rawAuthor.image, }; expect(formatted.openlibrary).toBe('https://openlibrary.org/authors/OL23919A'); expect(formatted.name).toBe('J.K. Rowling'); }); }); describe('Cover URL Selection Priority', () => { it('should prefer large > medium > small cover URLs', () => { const row = { cover_url_large: 'https://example.com/large.jpg', cover_url_medium: 'https://example.com/medium.jpg', cover_url_small: 'https://example.com/small.jpg', }; const coverUrl = row.cover_url_large || row.cover_url_medium || row.cover_url_small || null; expect(coverUrl).toBe('https://example.com/large.jpg'); }); it('should fallback when large not available', () => { const row = { cover_url_large: null, cover_url_medium: 'https://example.com/medium.jpg', cover_url_small: 'https://example.com/small.jpg', }; const coverUrl = row.cover_url_large || row.cover_url_medium || row.cover_url_small || null; expect(coverUrl).toBe('https://example.com/medium.jpg'); }); it('should return null when no covers available', () => { const row = { cover_url_large: null, cover_url_medium: null, cover_url_small: null, }; const coverUrl = row.cover_url_large || row.cover_url_medium || row.cover_url_small || null; expect(coverUrl).toBeNull(); }); }); });