fix: replace hardcoded localhost:3001 with relative API URL + add canvas mock and integration test
- Fix frontend Network error: API_BASE now defaults to empty string (same-origin) - Fix 3 files: api.ts, AuthContext.tsx, Dashboard.tsx - Add Canvas 2D context mock in tests/setup.ts for jsdom - Fix -0 vs +0 in ZoomPanController.getViewport() - Add IntegrationWorkflow.test.ts (35 tests, full CAD workflow)
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user