/** * AI API Tests – Auth guard, validation, and service availability checks * Does NOT test actual OpenRouter API calls. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import type { FastifyInstance } from 'fastify'; import { SqliteAdapter } from '../src/database/SqliteAdapter.js'; import { createServer } from '../src/server.js'; describe('AI API', () => { let app: FastifyInstance; let db: SqliteAdapter; let authToken: string; beforeAll(async () => { db = new SqliteAdapter(':memory:'); await db.init(); app = await createServer({ db, port: 0 }); await app.ready(); // Register a user to get an auth token const registerRes = await app.inject({ method: 'POST', url: '/api/auth/register', payload: { email: 'ai-test@example.com', password: 'TestPass123!', name: 'AI Test User', role: 'planer', }, }); const body = JSON.parse(registerRes.body); authToken = body.session.token; }); afterAll(async () => { await app.close(); db.close(); }); // ─── Auth Guard ───────────────────────────────────── describe('POST /api/ai/chat – Authentication', () => { it('should return 401 without authorization header', async () => { const response = await app.inject({ method: 'POST', url: '/api/ai/chat', payload: { messages: [{ role: 'user', content: 'hello' }] }, }); expect(response.statusCode).toBe(401); const body = JSON.parse(response.body); expect(body.error).toContain('Authentication required'); }); it('should return 401 with invalid token', async () => { const response = await app.inject({ method: 'POST', url: '/api/ai/chat', headers: { authorization: 'Bearer invalid-token-xxx' }, payload: { messages: [{ role: 'user', content: 'hello' }] }, }); expect(response.statusCode).toBe(401); const body = JSON.parse(response.body); expect(body.error).toContain('Invalid or expired session'); }); it('should return 401 with malformed authorization header', async () => { const response = await app.inject({ method: 'POST', url: '/api/ai/chat', headers: { authorization: 'NotBearer sometoken' }, payload: { messages: [{ role: 'user', content: 'hello' }] }, }); expect(response.statusCode).toBe(401); }); }); // ─── Validation ───────────────────────────────────── describe('POST /api/ai/chat – Validation', () => { it('should return 400 when messages array is missing', async () => { const response = await app.inject({ method: 'POST', url: '/api/ai/chat', headers: { authorization: `Bearer ${authToken}` }, payload: {}, }); expect(response.statusCode).toBe(400); const body = JSON.parse(response.body); expect(body.error).toContain('Messages array is required'); }); it('should return 400 when messages array is empty', async () => { const response = await app.inject({ method: 'POST', url: '/api/ai/chat', headers: { authorization: `Bearer ${authToken}` }, payload: { messages: [] }, }); expect(response.statusCode).toBe(400); const body = JSON.parse(response.body); expect(body.error).toContain('Messages array is required'); }); it('should return 400 when messages is not an array', async () => { const response = await app.inject({ method: 'POST', url: '/api/ai/chat', headers: { authorization: `Bearer ${authToken}` }, payload: { messages: 'not an array' }, }); expect(response.statusCode).toBe(400); }); }); // ─── Service Availability ─────────────────────────── describe('POST /api/ai/chat – Service availability', () => { it('should return 503 when API_KEY_OPENROUTER is not set', async () => { // Ensure env var is not set for this test const originalValue = process.env.API_KEY_OPENROUTER; delete process.env.API_KEY_OPENROUTER; const response = await app.inject({ method: 'POST', url: '/api/ai/chat', headers: { authorization: `Bearer ${authToken}` }, payload: { messages: [{ role: 'user', content: 'hello' }] }, }); expect(response.statusCode).toBe(503); const body = JSON.parse(response.body); expect(body.error).toContain('AI service not configured'); // Restore original value if it existed if (originalValue !== undefined) { process.env.API_KEY_OPENROUTER = originalValue; } }); }); });