@code-rag/api-server 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,620 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import request from 'supertest';
3
- import { ok, err } from 'neverthrow';
4
- import { ApiServer } from './server.js';
5
- import { parseApiKeys } from './middleware/auth.js';
6
- import { parseRateLimitConfig, createRateLimitMiddleware } from './middleware/rate-limit.js';
7
- import { createOpenAPISpec } from './openapi.js';
8
- import { EmbedError, StoreError } from '@coderag/core';
9
- import express from 'express';
10
- // --- Helpers ---
11
- function makeSearchResult(overrides = {}) {
12
- return {
13
- chunkId: 'chunk-1',
14
- content: 'function hello() {}',
15
- nlSummary: 'A greeting function',
16
- score: 0.95,
17
- method: 'hybrid',
18
- metadata: {
19
- chunkType: 'function',
20
- name: 'hello',
21
- declarations: [],
22
- imports: [],
23
- exports: [],
24
- },
25
- chunk: {
26
- id: 'chunk-1',
27
- content: 'function hello() {}',
28
- nlSummary: 'A greeting function',
29
- filePath: 'src/utils/hello.ts',
30
- startLine: 1,
31
- endLine: 3,
32
- language: 'typescript',
33
- metadata: {
34
- chunkType: 'function',
35
- name: 'hello',
36
- declarations: [],
37
- imports: [],
38
- exports: [],
39
- },
40
- },
41
- ...overrides,
42
- };
43
- }
44
- /**
45
- * Create an ApiServer with mocked core services injected.
46
- * We use a custom approach: create the server without initialize(),
47
- * then directly test the Express app with supertest.
48
- */
49
- function createTestServer(options) {
50
- const mockHybridSearch = {
51
- search: vi.fn().mockResolvedValue(ok([])),
52
- };
53
- const mockContextExpander = {
54
- expand: vi.fn().mockReturnValue({
55
- primaryResults: [],
56
- relatedChunks: [],
57
- graphExcerpt: { nodes: [], edges: [] },
58
- }),
59
- };
60
- const mockStore = {
61
- count: vi.fn().mockResolvedValue(ok(42)),
62
- };
63
- const mockConfig = {
64
- version: '1',
65
- project: { name: 'test-project', languages: ['typescript'] },
66
- ingestion: { maxTokensPerChunk: 512, exclude: [] },
67
- embedding: { provider: 'ollama', model: 'nomic-embed-text', dimensions: 768, autoStart: true, autoStop: false, docker: { image: 'ollama/ollama', gpu: 'auto' } },
68
- llm: { provider: 'ollama', model: 'qwen2.5-coder:7b' },
69
- search: { topK: 10, vectorWeight: 0.7, bm25Weight: 0.3 },
70
- storage: { path: '.coderag' },
71
- };
72
- const apiKeys = parseApiKeys(options?.apiKeys);
73
- const server = new ApiServer({
74
- rootDir: '/tmp/test',
75
- port: 0,
76
- apiKeys,
77
- onIndex: options?.onIndex ?? null,
78
- });
79
- // Inject mocked services via casting (they are private fields)
80
- const serverInternal = server;
81
- serverInternal.hybridSearch = mockHybridSearch;
82
- serverInternal.contextExpander = mockContextExpander;
83
- serverInternal.store = mockStore;
84
- serverInternal.config = mockConfig;
85
- return { server, mockHybridSearch, mockContextExpander, mockStore, mockConfig };
86
- }
87
- // --- Health Check Tests ---
88
- describe('GET /health', () => {
89
- it('should return 200 with ok status', async () => {
90
- const { server } = createTestServer();
91
- const res = await request(server.getApp()).get('/health');
92
- expect(res.status).toBe(200);
93
- expect(res.body).toHaveProperty('status', 'ok');
94
- expect(res.body).toHaveProperty('timestamp');
95
- });
96
- it('should not require authentication', async () => {
97
- const { server } = createTestServer({ apiKeys: 'secret-key' });
98
- const res = await request(server.getApp()).get('/health');
99
- expect(res.status).toBe(200);
100
- });
101
- });
102
- // --- OpenAPI Spec Tests ---
103
- describe('GET /api/openapi.json', () => {
104
- it('should return the OpenAPI spec', async () => {
105
- const { server } = createTestServer();
106
- const res = await request(server.getApp()).get('/api/openapi.json');
107
- expect(res.status).toBe(200);
108
- expect(res.body).toHaveProperty('openapi', '3.0.3');
109
- expect(res.body).toHaveProperty('info');
110
- expect(res.body.info).toHaveProperty('title', 'CodeRAG Cloud API');
111
- expect(res.body).toHaveProperty('paths');
112
- });
113
- it('should not require authentication', async () => {
114
- const { server } = createTestServer({ apiKeys: 'secret-key' });
115
- const res = await request(server.getApp()).get('/api/openapi.json');
116
- expect(res.status).toBe(200);
117
- });
118
- });
119
- // --- CORS Tests ---
120
- describe('CORS', () => {
121
- it('should return CORS headers', async () => {
122
- const { server } = createTestServer();
123
- const res = await request(server.getApp()).get('/health');
124
- expect(res.headers['access-control-allow-origin']).toBe('*');
125
- });
126
- it('should handle OPTIONS preflight', async () => {
127
- const { server } = createTestServer();
128
- const res = await request(server.getApp()).options('/api/v1/search');
129
- expect(res.status).toBe(204);
130
- expect(res.headers['access-control-allow-methods']).toContain('POST');
131
- expect(res.headers['access-control-allow-headers']).toContain('Authorization');
132
- });
133
- });
134
- // --- Auth Middleware Tests ---
135
- describe('Authentication', () => {
136
- it('should allow requests when no API keys configured (auth disabled)', async () => {
137
- const { server } = createTestServer();
138
- const res = await request(server.getApp())
139
- .get('/api/v1/status');
140
- expect(res.status).toBe(200);
141
- });
142
- it('should reject requests with missing API key when keys are configured', async () => {
143
- const { server } = createTestServer({ apiKeys: 'my-key' });
144
- const res = await request(server.getApp())
145
- .get('/api/v1/status');
146
- expect(res.status).toBe(401);
147
- expect(res.body).toHaveProperty('error', 'Unauthorized');
148
- });
149
- it('should reject requests with invalid API key', async () => {
150
- const { server } = createTestServer({ apiKeys: 'my-key' });
151
- const res = await request(server.getApp())
152
- .get('/api/v1/status')
153
- .set('Authorization', 'Bearer wrong-key');
154
- expect(res.status).toBe(401);
155
- expect(res.body.message).toContain('Invalid API key');
156
- });
157
- it('should accept valid API key via Authorization: Bearer', async () => {
158
- const { server } = createTestServer({ apiKeys: 'my-key' });
159
- const res = await request(server.getApp())
160
- .get('/api/v1/status')
161
- .set('Authorization', 'Bearer my-key');
162
- expect(res.status).toBe(200);
163
- });
164
- it('should accept valid API key via X-API-Key header', async () => {
165
- const { server } = createTestServer({ apiKeys: 'my-key' });
166
- const res = await request(server.getApp())
167
- .get('/api/v1/status')
168
- .set('X-API-Key', 'my-key');
169
- expect(res.status).toBe(200);
170
- });
171
- it('should support multiple API keys', async () => {
172
- const { server } = createTestServer({ apiKeys: 'key1,key2,key3' });
173
- const res1 = await request(server.getApp())
174
- .get('/api/v1/status')
175
- .set('Authorization', 'Bearer key1');
176
- expect(res1.status).toBe(200);
177
- const res2 = await request(server.getApp())
178
- .get('/api/v1/status')
179
- .set('Authorization', 'Bearer key2');
180
- expect(res2.status).toBe(200);
181
- const res3 = await request(server.getApp())
182
- .get('/api/v1/status')
183
- .set('Authorization', 'Bearer key3');
184
- expect(res3.status).toBe(200);
185
- });
186
- });
187
- // --- parseApiKeys Tests ---
188
- describe('parseApiKeys', () => {
189
- it('should return empty array for undefined', () => {
190
- expect(parseApiKeys(undefined)).toEqual([]);
191
- });
192
- it('should return empty array for empty string', () => {
193
- expect(parseApiKeys('')).toEqual([]);
194
- });
195
- it('should parse single key', () => {
196
- const keys = parseApiKeys('my-key');
197
- expect(keys).toEqual([{ key: 'my-key', admin: false }]);
198
- });
199
- it('should parse multiple comma-separated keys', () => {
200
- const keys = parseApiKeys('key1,key2,key3');
201
- expect(keys).toHaveLength(3);
202
- expect(keys.every((k) => !k.admin)).toBe(true);
203
- });
204
- it('should parse admin keys', () => {
205
- const keys = parseApiKeys('regular-key,admin-key:admin');
206
- expect(keys).toEqual([
207
- { key: 'regular-key', admin: false },
208
- { key: 'admin-key', admin: true },
209
- ]);
210
- });
211
- it('should trim whitespace', () => {
212
- const keys = parseApiKeys(' key1 , key2:admin ');
213
- expect(keys).toEqual([
214
- { key: 'key1', admin: false },
215
- { key: 'key2', admin: true },
216
- ]);
217
- });
218
- it('should skip empty entries', () => {
219
- const keys = parseApiKeys('key1,,key2,');
220
- expect(keys).toHaveLength(2);
221
- });
222
- });
223
- // --- Rate Limit Tests ---
224
- describe('Rate Limiting', () => {
225
- it('should allow requests under the limit', async () => {
226
- const { server } = createTestServer();
227
- const res = await request(server.getApp())
228
- .get('/api/v1/status');
229
- expect(res.status).toBe(200);
230
- expect(res.headers['x-ratelimit-limit']).toBeDefined();
231
- expect(res.headers['x-ratelimit-remaining']).toBeDefined();
232
- });
233
- it('should return 429 when rate limit exceeded', async () => {
234
- // Create a dedicated express app with a low rate limit for this test
235
- const app = express();
236
- app.use(express.json());
237
- app.use(createRateLimitMiddleware({ maxRequests: 2, windowMs: 60_000 }));
238
- app.get('/test', (_req, res) => { res.json({ ok: true }); });
239
- // First two requests should succeed
240
- const res1 = await request(app).get('/test');
241
- expect(res1.status).toBe(200);
242
- const res2 = await request(app).get('/test');
243
- expect(res2.status).toBe(200);
244
- // Third should be rate limited
245
- const res3 = await request(app).get('/test');
246
- expect(res3.status).toBe(429);
247
- expect(res3.body).toHaveProperty('error', 'Too Many Requests');
248
- expect(res3.headers['retry-after']).toBeDefined();
249
- });
250
- });
251
- // --- parseRateLimitConfig Tests ---
252
- describe('parseRateLimitConfig', () => {
253
- it('should return defaults for empty env', () => {
254
- const config = parseRateLimitConfig({});
255
- expect(config.maxRequests).toBe(60);
256
- expect(config.windowMs).toBe(60_000);
257
- });
258
- it('should parse CODERAG_RATE_LIMIT', () => {
259
- const config = parseRateLimitConfig({ CODERAG_RATE_LIMIT: '100' });
260
- expect(config.maxRequests).toBe(100);
261
- });
262
- it('should parse CODERAG_RATE_WINDOW_MS', () => {
263
- const config = parseRateLimitConfig({ CODERAG_RATE_WINDOW_MS: '30000' });
264
- expect(config.windowMs).toBe(30_000);
265
- });
266
- it('should use defaults for invalid values', () => {
267
- const config = parseRateLimitConfig({ CODERAG_RATE_LIMIT: 'invalid' });
268
- expect(config.maxRequests).toBe(60);
269
- });
270
- it('should use defaults for negative values', () => {
271
- const config = parseRateLimitConfig({ CODERAG_RATE_LIMIT: '-5' });
272
- expect(config.maxRequests).toBe(60);
273
- });
274
- });
275
- // --- Search Endpoint Tests ---
276
- describe('POST /api/v1/search', () => {
277
- it('should return search results for a valid query', async () => {
278
- const { server, mockHybridSearch } = createTestServer();
279
- const results = [makeSearchResult()];
280
- vi.mocked(mockHybridSearch.search).mockResolvedValue(ok(results));
281
- const res = await request(server.getApp())
282
- .post('/api/v1/search')
283
- .send({ query: 'hello function' });
284
- expect(res.status).toBe(200);
285
- expect(res.body.results).toHaveLength(1);
286
- expect(res.body.results[0]).toEqual({
287
- file_path: 'src/utils/hello.ts',
288
- chunk_type: 'function',
289
- name: 'hello',
290
- content: 'function hello() {}',
291
- nl_summary: 'A greeting function',
292
- score: 0.95,
293
- });
294
- expect(res.body.total).toBe(1);
295
- });
296
- it('should return 400 for missing query', async () => {
297
- const { server } = createTestServer();
298
- const res = await request(server.getApp())
299
- .post('/api/v1/search')
300
- .send({});
301
- expect(res.status).toBe(400);
302
- expect(res.body).toHaveProperty('error', 'Validation Error');
303
- });
304
- it('should return 400 for empty query', async () => {
305
- const { server } = createTestServer();
306
- const res = await request(server.getApp())
307
- .post('/api/v1/search')
308
- .send({ query: '' });
309
- expect(res.status).toBe(400);
310
- });
311
- it('should return 400 for path traversal in file_path', async () => {
312
- const { server } = createTestServer();
313
- const res = await request(server.getApp())
314
- .post('/api/v1/search')
315
- .send({ query: 'hello', file_path: '../../etc/passwd' });
316
- expect(res.status).toBe(400);
317
- });
318
- it('should return 400 for top_k above 100', async () => {
319
- const { server } = createTestServer();
320
- const res = await request(server.getApp())
321
- .post('/api/v1/search')
322
- .send({ query: 'hello', top_k: 200 });
323
- expect(res.status).toBe(400);
324
- });
325
- it('should filter by language', async () => {
326
- const { server, mockHybridSearch } = createTestServer();
327
- const results = [
328
- makeSearchResult(),
329
- makeSearchResult({
330
- chunkId: 'chunk-2',
331
- chunk: {
332
- id: 'chunk-2',
333
- content: 'def hello(): pass',
334
- nlSummary: 'Python greeting',
335
- filePath: 'src/hello.py',
336
- startLine: 1,
337
- endLine: 1,
338
- language: 'python',
339
- metadata: {
340
- chunkType: 'function',
341
- name: 'hello',
342
- declarations: [],
343
- imports: [],
344
- exports: [],
345
- },
346
- },
347
- }),
348
- ];
349
- vi.mocked(mockHybridSearch.search).mockResolvedValue(ok(results));
350
- const res = await request(server.getApp())
351
- .post('/api/v1/search')
352
- .send({ query: 'hello', language: 'typescript' });
353
- expect(res.status).toBe(200);
354
- expect(res.body.results).toHaveLength(1);
355
- });
356
- it('should handle search API errors', async () => {
357
- const { server, mockHybridSearch } = createTestServer();
358
- vi.mocked(mockHybridSearch.search).mockResolvedValue(err(new EmbedError('Connection refused')));
359
- const res = await request(server.getApp())
360
- .post('/api/v1/search')
361
- .send({ query: 'hello' });
362
- expect(res.status).toBe(500);
363
- expect(res.body.message).toContain('Connection refused');
364
- });
365
- it('should return 503 when search index is not initialized', async () => {
366
- const server = new ApiServer({ rootDir: '/tmp/test', port: 0 });
367
- // Don't inject mocks — services remain null
368
- const res = await request(server.getApp())
369
- .post('/api/v1/search')
370
- .send({ query: 'hello' });
371
- expect(res.status).toBe(503);
372
- expect(res.body.message).toContain('not initialized');
373
- });
374
- it('should use custom top_k', async () => {
375
- const { server, mockHybridSearch } = createTestServer();
376
- vi.mocked(mockHybridSearch.search).mockResolvedValue(ok([]));
377
- await request(server.getApp())
378
- .post('/api/v1/search')
379
- .send({ query: 'hello', top_k: 5 });
380
- expect(mockHybridSearch.search).toHaveBeenCalledWith('hello', { topK: 5 });
381
- });
382
- });
383
- // --- Context Endpoint Tests ---
384
- describe('POST /api/v1/context', () => {
385
- it('should return context for a valid file path', async () => {
386
- const { server, mockHybridSearch, mockContextExpander } = createTestServer();
387
- const results = [makeSearchResult()];
388
- vi.mocked(mockHybridSearch.search).mockResolvedValue(ok(results));
389
- const expandedContext = {
390
- primaryResults: results,
391
- relatedChunks: [],
392
- graphExcerpt: { nodes: [], edges: [] },
393
- };
394
- vi.mocked(mockContextExpander.expand).mockReturnValue(expandedContext);
395
- const res = await request(server.getApp())
396
- .post('/api/v1/context')
397
- .send({ file_path: 'src/utils/hello.ts' });
398
- expect(res.status).toBe(200);
399
- expect(res.body).toHaveProperty('context');
400
- expect(res.body).toHaveProperty('token_count');
401
- expect(res.body).toHaveProperty('truncated');
402
- expect(res.body).toHaveProperty('primary_chunks');
403
- expect(res.body).toHaveProperty('related_chunks');
404
- });
405
- it('should return 400 for missing file_path', async () => {
406
- const { server } = createTestServer();
407
- const res = await request(server.getApp())
408
- .post('/api/v1/context')
409
- .send({});
410
- expect(res.status).toBe(400);
411
- });
412
- it('should return 400 for path traversal', async () => {
413
- const { server } = createTestServer();
414
- const res = await request(server.getApp())
415
- .post('/api/v1/context')
416
- .send({ file_path: '../../etc/passwd' });
417
- expect(res.status).toBe(400);
418
- });
419
- it('should return 503 when services not initialized', async () => {
420
- const server = new ApiServer({ rootDir: '/tmp/test', port: 0 });
421
- const res = await request(server.getApp())
422
- .post('/api/v1/context')
423
- .send({ file_path: 'src/index.ts' });
424
- expect(res.status).toBe(503);
425
- });
426
- it('should return empty context when no chunks match', async () => {
427
- const { server, mockHybridSearch } = createTestServer();
428
- vi.mocked(mockHybridSearch.search).mockResolvedValue(ok([]));
429
- const res = await request(server.getApp())
430
- .post('/api/v1/context')
431
- .send({ file_path: 'nonexistent.ts' });
432
- expect(res.status).toBe(200);
433
- expect(res.body.context).toBe('');
434
- expect(res.body.message).toContain('No chunks found');
435
- });
436
- it('should handle search failures', async () => {
437
- const { server, mockHybridSearch } = createTestServer();
438
- vi.mocked(mockHybridSearch.search).mockResolvedValue(err(new EmbedError('Embedding unavailable')));
439
- const res = await request(server.getApp())
440
- .post('/api/v1/context')
441
- .send({ file_path: 'src/index.ts' });
442
- expect(res.status).toBe(500);
443
- expect(res.body.message).toContain('Embedding unavailable');
444
- });
445
- });
446
- // --- Status Endpoint Tests ---
447
- describe('GET /api/v1/status', () => {
448
- it('should return status with chunks and model info', async () => {
449
- const { server } = createTestServer();
450
- const res = await request(server.getApp())
451
- .get('/api/v1/status');
452
- expect(res.status).toBe(200);
453
- expect(res.body).toHaveProperty('total_chunks', 42);
454
- expect(res.body).toHaveProperty('health', 'ok');
455
- expect(res.body).toHaveProperty('model', 'nomic-embed-text');
456
- expect(res.body).toHaveProperty('languages');
457
- });
458
- it('should return not_initialized when store is null', async () => {
459
- const server = new ApiServer({ rootDir: '/tmp/test', port: 0 });
460
- const res = await request(server.getApp())
461
- .get('/api/v1/status');
462
- expect(res.status).toBe(200);
463
- expect(res.body.health).toBe('not_initialized');
464
- expect(res.body.total_chunks).toBe(0);
465
- });
466
- it('should return degraded when store count fails', async () => {
467
- const { server, mockStore } = createTestServer();
468
- vi.mocked(mockStore.count).mockResolvedValue(err(new StoreError('DB connection lost')));
469
- const res = await request(server.getApp())
470
- .get('/api/v1/status');
471
- expect(res.status).toBe(200);
472
- expect(res.body.health).toBe('degraded');
473
- });
474
- it('should return degraded when store is empty', async () => {
475
- const { server, mockStore } = createTestServer();
476
- vi.mocked(mockStore.count).mockResolvedValue(ok(0));
477
- const res = await request(server.getApp())
478
- .get('/api/v1/status');
479
- expect(res.status).toBe(200);
480
- expect(res.body.health).toBe('degraded');
481
- });
482
- });
483
- // --- Index Trigger Endpoint Tests ---
484
- describe('POST /api/v1/index', () => {
485
- it('should trigger indexing with admin key', async () => {
486
- const onIndex = vi.fn().mockResolvedValue({ indexed_files: 50, duration_ms: 1200 });
487
- const { server } = createTestServer({
488
- apiKeys: 'admin-key:admin',
489
- onIndex,
490
- });
491
- const res = await request(server.getApp())
492
- .post('/api/v1/index')
493
- .set('Authorization', 'Bearer admin-key')
494
- .send({ force: true });
495
- expect(res.status).toBe(200);
496
- expect(res.body).toEqual({
497
- status: 'completed',
498
- indexed_files: 50,
499
- duration_ms: 1200,
500
- });
501
- expect(onIndex).toHaveBeenCalledWith({ force: true, rootDir: undefined });
502
- });
503
- it('should return 403 for non-admin key', async () => {
504
- const onIndex = vi.fn().mockResolvedValue({ indexed_files: 0, duration_ms: 0 });
505
- const { server } = createTestServer({
506
- apiKeys: 'regular-key',
507
- onIndex,
508
- });
509
- const res = await request(server.getApp())
510
- .post('/api/v1/index')
511
- .set('Authorization', 'Bearer regular-key')
512
- .send({});
513
- expect(res.status).toBe(403);
514
- expect(res.body.error).toBe('Forbidden');
515
- });
516
- it('should return 401 for missing key when auth is enabled', async () => {
517
- const { server } = createTestServer({ apiKeys: 'admin-key:admin' });
518
- const res = await request(server.getApp())
519
- .post('/api/v1/index')
520
- .send({});
521
- expect(res.status).toBe(401);
522
- });
523
- it('should return 503 when index callback is not configured', async () => {
524
- const { server } = createTestServer({ apiKeys: 'admin-key:admin' });
525
- // No onIndex callback provided, and createTestServer uses null
526
- const serverInternal = server;
527
- const res = await request(serverInternal.app)
528
- .post('/api/v1/index')
529
- .set('Authorization', 'Bearer admin-key')
530
- .send({});
531
- expect(res.status).toBe(503);
532
- expect(res.body.message).toContain('not configured');
533
- });
534
- it('should allow index when auth is disabled (no keys configured)', async () => {
535
- const onIndex = vi.fn().mockResolvedValue({ indexed_files: 10, duration_ms: 500 });
536
- const { server } = createTestServer({ onIndex });
537
- const res = await request(server.getApp())
538
- .post('/api/v1/index')
539
- .send({});
540
- expect(res.status).toBe(200);
541
- expect(onIndex).toHaveBeenCalled();
542
- });
543
- it('should return 400 for path traversal in root_dir', async () => {
544
- const onIndex = vi.fn().mockResolvedValue({ indexed_files: 0, duration_ms: 0 });
545
- const { server } = createTestServer({ apiKeys: 'admin-key:admin', onIndex });
546
- const res = await request(server.getApp())
547
- .post('/api/v1/index')
548
- .set('Authorization', 'Bearer admin-key')
549
- .send({ root_dir: '../../etc' });
550
- expect(res.status).toBe(400);
551
- });
552
- it('should handle indexing errors', async () => {
553
- const onIndex = vi.fn().mockRejectedValue(new Error('Git not found'));
554
- const { server } = createTestServer({
555
- apiKeys: 'admin-key:admin',
556
- onIndex,
557
- });
558
- const res = await request(server.getApp())
559
- .post('/api/v1/index')
560
- .set('Authorization', 'Bearer admin-key')
561
- .send({});
562
- expect(res.status).toBe(500);
563
- expect(res.body.message).toContain('Git not found');
564
- });
565
- });
566
- // --- OpenAPI Spec Object Tests ---
567
- describe('OpenAPI Spec', () => {
568
- it('should have the correct structure', () => {
569
- const spec = createOpenAPISpec();
570
- expect(spec.openapi).toBe('3.0.3');
571
- expect(spec.info.title).toBe('CodeRAG Cloud API');
572
- expect(spec.info.version).toBe('0.1.0');
573
- });
574
- it('should define all API paths', () => {
575
- const spec = createOpenAPISpec();
576
- const paths = Object.keys(spec.paths);
577
- expect(paths).toContain('/api/v1/search');
578
- expect(paths).toContain('/api/v1/context');
579
- expect(paths).toContain('/api/v1/status');
580
- expect(paths).toContain('/api/v1/index');
581
- expect(paths).toContain('/health');
582
- });
583
- it('should define security schemes', () => {
584
- const spec = createOpenAPISpec();
585
- const components = spec.components;
586
- const schemes = components['securitySchemes'];
587
- expect(schemes).toHaveProperty('bearerAuth');
588
- expect(schemes).toHaveProperty('apiKeyAuth');
589
- expect(spec.components).toHaveProperty('securitySchemes');
590
- });
591
- it('should define response schemas', () => {
592
- const spec = createOpenAPISpec();
593
- const components = spec.components;
594
- expect(components).toHaveProperty('schemas');
595
- expect(components).toHaveProperty('responses');
596
- const schemas = components['schemas'];
597
- expect(schemas).toHaveProperty('SearchResult');
598
- expect(schemas).toHaveProperty('ContextResponse');
599
- expect(schemas).toHaveProperty('StatusResponse');
600
- expect(schemas).toHaveProperty('IndexResponse');
601
- });
602
- });
603
- // --- API Server Version Tests ---
604
- describe('ApiServer', () => {
605
- it('should export API_SERVER_VERSION', async () => {
606
- const mod = await import('./server.js');
607
- expect(mod.API_SERVER_VERSION).toBe('0.1.0');
608
- });
609
- it('should create a server instance', () => {
610
- const server = new ApiServer({ rootDir: '/tmp/test', port: 3100 });
611
- expect(server).toBeDefined();
612
- expect(server.getApp()).toBeDefined();
613
- });
614
- it('should start and stop cleanly', async () => {
615
- const server = new ApiServer({ rootDir: '/tmp/test', port: 0 });
616
- // Port 0 would need actual listen; test close without start
617
- await server.close(); // should not throw
618
- });
619
- });
620
- //# sourceMappingURL=server.test.js.map