All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 13m21s
227 lines
8.4 KiB
TypeScript
227 lines
8.4 KiB
TypeScript
// src/tests/integration/ai.integration.test.ts
|
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
import supertest from 'supertest';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'path';
|
|
import { createAndLoginUser } from '../utils/testHelpers';
|
|
import { cleanupDb } from '../utils/cleanup';
|
|
import { cleanupFiles } from '../utils/cleanupFiles';
|
|
|
|
/**
|
|
* @vitest-environment node
|
|
*/
|
|
|
|
interface TestGeolocationCoordinates {
|
|
latitude: number;
|
|
longitude: number;
|
|
accuracy: number;
|
|
altitude: number | null;
|
|
altitudeAccuracy: number | null;
|
|
heading: number | null;
|
|
speed: number | null;
|
|
toJSON(): object;
|
|
}
|
|
|
|
describe('AI API Routes Integration Tests', () => {
|
|
let request: ReturnType<typeof supertest>;
|
|
let authToken: string;
|
|
let testUserId: string;
|
|
|
|
beforeAll(async () => {
|
|
vi.stubEnv('FRONTEND_URL', 'https://example.com');
|
|
const app = (await import('../../../server')).default;
|
|
request = supertest(app);
|
|
|
|
// Create and log in as a new user for authenticated tests.
|
|
const { token, user } = await createAndLoginUser({ fullName: 'AI Tester', request });
|
|
authToken = token;
|
|
testUserId = user.user.user_id;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
vi.unstubAllEnvs();
|
|
// 1. Clean up database records
|
|
await cleanupDb({ userIds: [testUserId] });
|
|
|
|
// 2. Safeguard: Clean up any leftover files from failed tests.
|
|
// The routes themselves should clean up on success, but this handles interruptions.
|
|
const uploadDir = path.resolve(__dirname, '../../../flyer-images');
|
|
try {
|
|
const allFiles = await fs.readdir(uploadDir);
|
|
const testFiles = allFiles
|
|
.filter((f) => f.startsWith('image-') || f.startsWith('images-'))
|
|
.map((f) => path.join(uploadDir, f));
|
|
|
|
if (testFiles.length > 0) {
|
|
await cleanupFiles(testFiles);
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof Error && (error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
console.error('Error during AI integration test file cleanup:', error);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('POST /api/ai/check-flyer should return a boolean', async () => {
|
|
const response = await request
|
|
.post('/api/ai/check-flyer')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.attach('image', Buffer.from('content'), 'test.jpg');
|
|
const result = response.body.data;
|
|
expect(response.status).toBe(200);
|
|
// The backend is stubbed to always return true for this check
|
|
expect(result.is_flyer).toBe(true);
|
|
});
|
|
|
|
it('POST /api/ai/extract-address should return a stubbed address', async () => {
|
|
const response = await request
|
|
.post('/api/ai/extract-address')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.attach('image', Buffer.from('content'), 'test.jpg');
|
|
const result = response.body.data;
|
|
expect(response.status).toBe(200);
|
|
expect(result.address).toBe('not identified');
|
|
});
|
|
|
|
it('POST /api/ai/extract-logo should return a stubbed response', async () => {
|
|
const response = await request
|
|
.post('/api/ai/extract-logo')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.attach('images', Buffer.from('content'), 'test.jpg');
|
|
const result = response.body.data;
|
|
expect(response.status).toBe(200);
|
|
expect(result).toEqual({ store_logo_base_64: null });
|
|
});
|
|
|
|
it('POST /api/ai/quick-insights should return a stubbed insight', async () => {
|
|
const response = await request
|
|
.post('/api/ai/quick-insights')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({ items: [{ item: 'test' }] });
|
|
const result = response.body.data;
|
|
// DEBUG: Log response if it fails expectation
|
|
if (response.status !== 200 || !result.text) {
|
|
console.log('[DEBUG] POST /api/ai/quick-insights response:', response.status, response.body);
|
|
}
|
|
expect(response.status).toBe(200);
|
|
expect(result.text).toBe('This is a server-generated quick insight: buy the cheap stuff!');
|
|
});
|
|
|
|
it('POST /api/ai/deep-dive should return a stubbed analysis', async () => {
|
|
const response = await request
|
|
.post('/api/ai/deep-dive')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({ items: [{ item: 'test' }] });
|
|
const result = response.body.data;
|
|
// DEBUG: Log response if it fails expectation
|
|
if (response.status !== 200 || !result.text) {
|
|
console.log('[DEBUG] POST /api/ai/deep-dive response:', response.status, response.body);
|
|
}
|
|
expect(response.status).toBe(200);
|
|
expect(result.text).toBe('This is a server-generated deep dive analysis. It is very detailed.');
|
|
});
|
|
|
|
it('POST /api/ai/search-web should return a stubbed search result', async () => {
|
|
const response = await request
|
|
.post('/api/ai/search-web')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({ query: 'test query' });
|
|
const result = response.body.data;
|
|
// DEBUG: Log response if it fails expectation
|
|
if (response.status !== 200 || !result.text) {
|
|
console.log('[DEBUG] POST /api/ai/search-web response:', response.status, response.body);
|
|
}
|
|
expect(response.status).toBe(200);
|
|
expect(result).toEqual({ text: 'The web says this is good.', sources: [] });
|
|
});
|
|
|
|
it('POST /api/ai/plan-trip should return an error as the feature is disabled', async () => {
|
|
// The GeolocationCoordinates type requires more than just lat/lng.
|
|
// We create a complete mock object to satisfy the type.
|
|
const mockLocation: TestGeolocationCoordinates = {
|
|
latitude: 48.4284,
|
|
longitude: -123.3656,
|
|
accuracy: 100,
|
|
altitude: null,
|
|
altitudeAccuracy: null,
|
|
heading: null,
|
|
speed: null,
|
|
toJSON: function () {
|
|
return {
|
|
latitude: this.latitude,
|
|
longitude: this.longitude,
|
|
accuracy: this.accuracy,
|
|
altitude: this.altitude,
|
|
altitudeAccuracy: this.altitudeAccuracy,
|
|
heading: this.heading,
|
|
speed: this.speed,
|
|
};
|
|
},
|
|
};
|
|
const mockStore = {
|
|
name: 'Test Store for Trip',
|
|
store_id: 1,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
};
|
|
const response = await request
|
|
.post('/api/ai/plan-trip')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({ items: [], store: mockStore, userLocation: mockLocation });
|
|
// The service for this endpoint is disabled and throws an error, which results in a 500.
|
|
// DEBUG: Log response if it fails expectation
|
|
if (response.status !== 500) {
|
|
console.log('[DEBUG] POST /api/ai/plan-trip response:', response.status, response.body);
|
|
}
|
|
expect(response.status).toBe(500);
|
|
const errorResult = response.body.error;
|
|
expect(errorResult.message).toContain('planTripWithMaps');
|
|
});
|
|
|
|
it('POST /api/ai/generate-image should reject because it is not implemented', async () => {
|
|
// The backend for this is not stubbed and will throw an error.
|
|
// This test confirms that the endpoint is protected and responds as expected to a failure.
|
|
const response = await request
|
|
.post('/api/ai/generate-image')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({ prompt: 'a test prompt' });
|
|
expect(response.status).toBe(501);
|
|
});
|
|
|
|
it('POST /api/ai/generate-speech should reject because it is not implemented', async () => {
|
|
// The backend for this is not stubbed and will throw an error.
|
|
const response = await request
|
|
.post('/api/ai/generate-speech')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({ text: 'a test prompt' });
|
|
expect(response.status).toBe(501);
|
|
});
|
|
|
|
describe('Rate Limiting', () => {
|
|
it('should block requests to /api/ai/quick-insights after exceeding the limit', async () => {
|
|
const limit = 20; // Matches aiGenerationLimiter config
|
|
const items = [{ item: 'test' }];
|
|
|
|
// Send requests up to the limit
|
|
for (let i = 0; i < limit; i++) {
|
|
const response = await request
|
|
.post('/api/ai/quick-insights')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
|
.send({ items });
|
|
expect(response.status).toBe(200);
|
|
}
|
|
|
|
// The next request should be blocked
|
|
const blockedResponse = await request
|
|
.post('/api/ai/quick-insights')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
|
.send({ items });
|
|
|
|
expect(blockedResponse.status).toBe(429);
|
|
expect(blockedResponse.text).toContain('Too many AI generation requests');
|
|
});
|
|
});
|
|
});
|