| title | Integration Testing Guide |
|---|---|
| description | Comprehensive guide to integration testing in Altus 4 covering API testing, database integration, and service interaction testing. |
Comprehensive Integration Testing for Altus 4
Integration tests verify that different components of Altus 4 work correctly together, including API endpoints, database interactions, and service integrations. This guide covers strategies, patterns, and best practices for effective integration testing.
Integration tests focus on:
- API Endpoints: Complete request/response cycles
- Database Operations: Real database interactions
- Service Integration: How services work together
- External Dependencies: Third-party API integrations
- Authentication Flow: End-to-end auth processes
- Error Handling: System behavior under failure conditions
| Aspect | Unit Tests | Integration Tests |
|---|---|---|
| Scope | Single component | Multiple components |
| Speed | Very fast (<1ms) | Moderate (10-100ms) |
| Dependencies | Mocked | Real or test doubles |
| Environment | Isolated | Test environment |
| Purpose | Logic verification | Component interaction |
// tests/config/database.ts
import mysql from 'mysql2/promise';
export class TestDatabaseManager {
private connection: mysql.Connection | null = null;
async setupTestDatabase(): Promise<void> {
this.connection = await mysql.createConnection({
host: process.env.TEST_DB_HOST || 'localhost',
port: parseInt(process.env.TEST_DB_PORT || '3306'),
user: process.env.TEST_DB_USER || 'altus4_test',
password: process.env.TEST_DB_PASSWORD || 'test_password',
database: process.env.TEST_DB_NAME || 'altus4_test',
multipleStatements: true,
});
// Create test schema
await this.createTestSchema();
// Seed initial data
await this.seedTestData();
}
async cleanupTestDatabase(): Promise<void> {
if (this.connection) {
await this.clearTestData();
await this.connection.end();
this.connection = null;
}
}
private async createTestSchema(): Promise<void> {
const schema = `
CREATE TABLE IF NOT EXISTS test_articles (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
author VARCHAR(100),
published_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FULLTEXT KEY ft_title_content (title, content),
FULLTEXT KEY ft_title (title)
);
CREATE TABLE IF NOT EXISTS test_products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
category VARCHAR(100),
price DECIMAL(10,2),
FULLTEXT KEY ft_name_description (name, description)
);
`;
await this.connection!.execute(schema);
}
private async seedTestData(): Promise<void> {
const articles = [
{
title: 'MySQL Performance Optimization Guide',
content:
'Comprehensive guide to optimizing MySQL database performance including indexing strategies, query optimization, and server configuration.',
author: 'Database Expert',
},
{
title: 'Advanced Database Indexing',
content:
'Learn about B-tree indexes, composite indexes, and full-text search optimization techniques for better query performance.',
author: 'Index Specialist',
},
{
title: 'Query Performance Tuning',
content:
'Best practices for writing efficient SQL queries, avoiding common pitfalls, and using EXPLAIN to analyze query execution plans.',
author: 'SQL Expert',
},
];
for (const article of articles) {
await this.connection!.execute(
'INSERT INTO test_articles (title, content, author) VALUES (?, ?, ?)',
[article.title, article.content, article.author]
);
}
const products = [
{
name: 'Gaming Laptop Pro',
description:
'High-performance gaming laptop with RTX graphics and fast SSD storage',
category: 'Electronics',
price: 1299.99,
},
{
name: 'Wireless Gaming Mouse',
description: 'Precision wireless mouse designed for competitive gaming',
category: 'Accessories',
price: 79.99,
},
];
for (const product of products) {
await this.connection!.execute(
'INSERT INTO test_products (name, description, category, price) VALUES (?, ?, ?, ?)',
[product.name, product.description, product.category, product.price]
);
}
}
private async clearTestData(): Promise<void> {
await this.connection!.execute('DELETE FROM test_articles');
await this.connection!.execute('DELETE FROM test_products');
}
async getConnection(): Promise<mysql.Connection> {
if (!this.connection) {
throw new Error('Test database not initialized');
}
return this.connection;
}
}
// Global test database instance
export const testDb = new TestDatabaseManager();// tests/helpers/test-server.ts
import { app } from '@/app';
import { Server } from 'http';
import { testDb } from '../config/database';
export class TestServer {
private server: Server | null = null;
private port: number;
constructor(port: number = 0) {
this.port = port;
}
async start(): Promise<void> {
// Setup test database
await testDb.setupTestDatabase();
// Start server
return new Promise((resolve, reject) => {
this.server = app.listen(this.port, (err?: Error) => {
if (err) {
reject(err);
} else {
const address = this.server!.address();
if (typeof address === 'object' && address) {
this.port = address.port;
}
resolve();
}
});
});
}
async stop(): Promise<void> {
// Cleanup database
await testDb.cleanupTestDatabase();
// Stop server
if (this.server) {
return new Promise(resolve => {
this.server!.close(() => {
this.server = null;
resolve();
});
});
}
}
getPort(): number {
return this.port;
}
getBaseUrl(): string {
return `http://localhost:${this.port}`;
}
}// tests/integration/auth/authentication.test.ts
import request from 'supertest';
import { TestServer } from '../../helpers/test-server';
import { createTestUser, createTestApiKey } from '../../helpers/auth-helpers';
describe('Authentication Integration', () => {
let testServer: TestServer;
let baseUrl: string;
beforeAll(async () => {
testServer = new TestServer();
await testServer.start();
baseUrl = testServer.getBaseUrl();
});
afterAll(async () => {
await testServer.stop();
});
describe('User Registration', () => {
it('should register a new user successfully', async () => {
const userData = {
name: 'Test User',
email: 'test@example.com',
password: 'securePassword123',
};
const response = await request(baseUrl)
.post('/api/v1/auth/register')
.send(userData)
.expect(201);
expect(response.body).toMatchObject({
success: true,
data: {
user: {
id: expect.any(String),
name: userData.name,
email: userData.email,
role: 'user',
},
},
});
// Verify user was created in database
const connection = await testDb.getConnection();
const [users] = await connection.execute(
'SELECT * FROM users WHERE email = ?',
[userData.email]
);
expect(users).toHaveLength(1);
expect((users as any[])[0].email).toBe(userData.email);
});
it('should reject duplicate email registration', async () => {
const userData = {
name: 'Test User',
email: 'duplicate@example.com',
password: 'securePassword123',
};
// First registration
await request(baseUrl)
.post('/api/v1/auth/register')
.send(userData)
.expect(201);
// Duplicate registration
const response = await request(baseUrl)
.post('/api/v1/auth/register')
.send(userData)
.expect(400);
expect(response.body).toMatchObject({
success: false,
error: {
code: 'EMAIL_EXISTS',
message: expect.stringContaining('email'),
},
});
});
it('should validate required fields', async () => {
const invalidData = {
name: 'Test User',
// Missing email and password
};
const response = await request(baseUrl)
.post('/api/v1/auth/register')
.send(invalidData)
.expect(400);
expect(response.body.error.code).toBe('VALIDATION_ERROR');
});
});
describe('User Login', () => {
let testUser: any;
beforeEach(async () => {
testUser = await createTestUser({
email: 'login-test@example.com',
password: 'testPassword123',
name: 'Login Test User',
});
});
it('should login with valid credentials', async () => {
const response = await request(baseUrl)
.post('/api/v1/auth/login')
.send({
email: testUser.email,
password: 'testPassword123',
})
.expect(200);
expect(response.body).toMatchObject({
success: true,
data: {
token: expect.any(String),
user: {
id: testUser.id,
email: testUser.email,
name: testUser.name,
},
},
});
// Verify JWT token structure
const token = response.body.data.token;
expect(token).toMatch(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/);
});
it('should reject invalid credentials', async () => {
const response = await request(baseUrl)
.post('/api/v1/auth/login')
.send({
email: testUser.email,
password: 'wrongPassword',
})
.expect(401);
expect(response.body).toMatchObject({
success: false,
error: {
code: 'INVALID_CREDENTIALS',
message: expect.any(String),
},
});
});
});
describe('API Key Management', () => {
let testUser: any;
let authToken: string;
beforeEach(async () => {
testUser = await createTestUser();
// Get auth token
const loginResponse = await request(baseUrl)
.post('/api/v1/auth/login')
.send({
email: testUser.email,
password: 'testPassword123',
});
authToken = loginResponse.body.data.token;
});
it('should create first API key via setup endpoint', async () => {
const response = await request(baseUrl)
.post('/api/v1/management/setup')
.set('Authorization', `Bearer ${authToken}`)
.expect(201);
expect(response.body).toMatchObject({
success: true,
data: {
apiKey: {
id: expect.any(String),
key: expect.stringMatching(/^altus4_sk_test_/),
name: 'Default API Key',
environment: 'test',
permissions: ['search'],
},
},
});
});
it('should create additional API keys', async () => {
// First create initial API key
const setupResponse = await request(baseUrl)
.post('/api/v1/management/setup')
.set('Authorization', `Bearer ${authToken}`);
const firstApiKey = setupResponse.body.data.apiKey.key;
// Create additional API key
const response = await request(baseUrl)
.post('/api/v1/keys')
.set('Authorization', `Bearer ${firstApiKey}`)
.send({
name: 'Production API Key',
environment: 'live',
permissions: ['search', 'analytics'],
})
.expect(201);
expect(response.body.data.apiKey.key).toMatch(/^altus4_sk_live_/);
expect(response.body.data.apiKey.permissions).toEqual([
'search',
'analytics',
]);
});
});
});// tests/integration/api/search.test.ts
import request from 'supertest';
import { TestServer } from '../../helpers/test-server';
import {
createTestUser,
createTestApiKey,
createTestDatabase,
} from '../../helpers/auth-helpers';
describe('Search API Integration', () => {
let testServer: TestServer;
let baseUrl: string;
let testUser: any;
let testApiKey: string;
let testDatabaseId: string;
beforeAll(async () => {
testServer = new TestServer();
await testServer.start();
baseUrl = testServer.getBaseUrl();
// Setup test user and API key
testUser = await createTestUser();
testApiKey = await createTestApiKey(testUser.id, {
permissions: ['search', 'analytics'],
environment: 'test',
});
// Create test database connection
testDatabaseId = await createTestDatabase(testUser.id, {
name: 'Test Database',
host: 'localhost',
database: 'altus4_test',
tables: ['test_articles', 'test_products'],
});
});
afterAll(async () => {
await testServer.stop();
});
describe('POST /api/v1/search', () => {
it('should execute basic search successfully', async () => {
const response = await request(baseUrl)
.post('/api/v1/search')
.set('Authorization', `Bearer ${testApiKey}`)
.send({
query: 'mysql performance',
databases: [testDatabaseId],
searchMode: 'natural',
limit: 10,
})
.expect(200);
expect(response.body).toMatchObject({
success: true,
data: {
results: expect.any(Array),
totalCount: expect.any(Number),
executionTime: expect.any(Number),
categories: expect.any(Array),
suggestions: expect.any(Array),
},
meta: {
timestamp: expect.any(String),
requestId: expect.any(String),
version: expect.any(String),
},
});
// Verify search results structure
if (response.body.data.results.length > 0) {
const result = response.body.data.results[0];
expect(result).toMatchObject({
id: expect.any(String),
database: testDatabaseId,
table: expect.any(String),
data: expect.any(Object),
relevanceScore: expect.any(Number),
snippet: expect.any(String),
matchedColumns: expect.any(Array),
});
}
});
it('should handle semantic search mode', async () => {
const response = await request(baseUrl)
.post('/api/v1/search')
.set('Authorization', `Bearer ${testApiKey}`)
.send({
query: 'database optimization techniques',
databases: [testDatabaseId],
searchMode: 'semantic',
limit: 15,
})
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.results).toBeInstanceOf(Array);
});
it('should handle boolean search mode', async () => {
const response = await request(baseUrl)
.post('/api/v1/search')
.set('Authorization', `Bearer ${testApiKey}`)
.send({
query: '+mysql +performance -slow',
databases: [testDatabaseId],
searchMode: 'boolean',
limit: 20,
})
.expect(200);
expect(response.body.success).toBe(true);
});
it('should support table filtering', async () => {
const response = await request(baseUrl)
.post('/api/v1/search')
.set('Authorization', `Bearer ${testApiKey}`)
.send({
query: 'performance',
databases: [testDatabaseId],
tables: ['test_articles'],
limit: 10,
})
.expect(200);
expect(response.body.success).toBe(true);
// All results should be from the specified table
response.body.data.results.forEach((result: any) => {
expect(result.table).toBe('test_articles');
});
});
it('should support column filtering', async () => {
const response = await request(baseUrl)
.post('/api/v1/search')
.set('Authorization', `Bearer ${testApiKey}`)
.send({
query: 'mysql',
databases: [testDatabaseId],
columns: ['title'],
limit: 10,
})
.expect(200);
expect(response.body.success).toBe(true);
});
it('should handle pagination correctly', async () => {
// First page
const page1Response = await request(baseUrl)
.post('/api/v1/search')
.set('Authorization', `Bearer ${testApiKey}`)
.send({
query: 'database',
databases: [testDatabaseId],
limit: 2,
offset: 0,
})
.expect(200);
// Second page
const page2Response = await request(baseUrl)
.post('/api/v1/search')
.set('Authorization', `Bearer ${testApiKey}`)
.send({
query: 'database',
databases: [testDatabaseId],
limit: 2,
offset: 2,
})
.expect(200);
expect(page1Response.body.success).toBe(true);
expect(page2Response.body.success).toBe(true);
// Results should be different (assuming more than 2 total results)
if (page1Response.body.data.totalCount > 2) {
const page1Ids = page1Response.body.data.results.map((r: any) => r.id);
const page2Ids = page2Response.body.data.results.map((r: any) => r.id);
expect(page1Ids).not.toEqual(page2Ids);
}
});
it('should validate request parameters', async () => {
// Empty query
const emptyQueryResponse = await request(baseUrl)
.post('/api/v1/search')
.set('Authorization', `Bearer ${testApiKey}`)
.send({
query: '',
databases: [testDatabaseId],
})
.expect(400);
expect(emptyQueryResponse.body.error.code).toBe('VALIDATION_ERROR');
// No databases
const noDatabasesResponse = await request(baseUrl)
.post('/api/v1/search')
.set('Authorization', `Bearer ${testApiKey}`)
.send({
query: 'test query',
databases: [],
})
.expect(400);
expect(noDatabasesResponse.body.error.code).toBe('VALIDATION_ERROR');
// Invalid limit
const invalidLimitResponse = await request(baseUrl)
.post('/api/v1/search')
.set('Authorization', `Bearer ${testApiKey}`)
.send({
query: 'test query',
databases: [testDatabaseId],
limit: 500, // Exceeds maximum
})
.expect(400);
expect(invalidLimitResponse.body.error.code).toBe('VALIDATION_ERROR');
});
it('should handle authentication errors', async () => {
// No API key
await request(baseUrl)
.post('/api/v1/search')
.send({
query: 'test query',
databases: [testDatabaseId],
})
.expect(401);
// Invalid API key
await request(baseUrl)
.post('/api/v1/search')
.set('Authorization', 'Bearer invalid_key')
.send({
query: 'test query',
databases: [testDatabaseId],
})
.expect(401);
});
it('should handle permission errors', async () => {
// Create API key without search permission
const limitedApiKey = await createTestApiKey(testUser.id, {
permissions: ['analytics'], // No search permission
environment: 'test',
});
const response = await request(baseUrl)
.post('/api/v1/search')
.set('Authorization', `Bearer ${limitedApiKey}`)
.send({
query: 'test query',
databases: [testDatabaseId],
})
.expect(403);
expect(response.body.error.code).toBe('INSUFFICIENT_PERMISSIONS');
});
});
describe('GET /api/v1/search/suggestions', () => {
it('should return search suggestions', async () => {
const response = await request(baseUrl)
.get('/api/v1/search/suggestions')
.set('Authorization', `Bearer ${testApiKey}`)
.query({
query: 'mysql perf',
databases: testDatabaseId,
limit: 5,
})
.expect(200);
expect(response.body).toMatchObject({
success: true,
data: {
suggestions: expect.any(Array),
},
});
// Verify suggestion structure
if (response.body.data.suggestions.length > 0) {
const suggestion = response.body.data.suggestions[0];
expect(suggestion).toMatchObject({
text: expect.any(String),
score: expect.any(Number),
type: expect.any(String),
});
}
});
});
describe('GET /api/v1/search/history', () => {
it('should return user search history', async () => {
// Perform a search first to create history
await request(baseUrl)
.post('/api/v1/search')
.set('Authorization', `Bearer ${testApiKey}`)
.send({
query: 'mysql optimization',
databases: [testDatabaseId],
});
// Get search history
const response = await request(baseUrl)
.get('/api/v1/search/history')
.set('Authorization', `Bearer ${testApiKey}`)
.query({ limit: 10 })
.expect(200);
expect(response.body).toMatchObject({
success: true,
data: {
searches: expect.any(Array),
totalCount: expect.any(Number),
},
});
// Verify history entry structure
if (response.body.data.searches.length > 0) {
const historyEntry = response.body.data.searches[0];
expect(historyEntry).toMatchObject({
id: expect.any(String),
query: expect.any(String),
searchMode: expect.any(String),
resultCount: expect.any(Number),
executionTime: expect.any(Number),
createdAt: expect.any(String),
});
}
});
});
});// tests/integration/database/database-operations.test.ts
import { DatabaseService } from '@/services/DatabaseService';
import { testDb } from '../../config/database';
import { createTestUser, createTestDatabase } from '../../helpers/auth-helpers';
describe('Database Operations Integration', () => {
let databaseService: DatabaseService;
let testUser: any;
let testDatabaseId: string;
beforeAll(async () => {
await testDb.setupTestDatabase();
databaseService = new DatabaseService();
testUser = await createTestUser();
testDatabaseId = await createTestDatabase(testUser.id);
});
afterAll(async () => {
await testDb.cleanupTestDatabase();
});
describe('Full-text Search Operations', () => {
it('should execute natural language search', async () => {
const results = await databaseService.executeFullTextSearch(
testDatabaseId,
'mysql performance optimization',
['test_articles'],
['title', 'content'],
10,
0
);
expect(results).toBeInstanceOf(Array);
expect(results.length).toBeGreaterThan(0);
// Verify result structure
const result = results[0];
expect(result).toHaveProperty('id');
expect(result).toHaveProperty('title');
expect(result).toHaveProperty('content');
expect(result).toHaveProperty('table', 'test_articles');
});
it('should execute boolean search', async () => {
const results = await databaseService.executeFullTextSearch(
testDatabaseId,
'+mysql +performance -slow',
['test_articles'],
['title', 'content'],
10,
0,
'boolean'
);
expect(results).toBeInstanceOf(Array);
// All results should contain 'mysql' and 'performance' but not 'slow'
results.forEach(result => {
const text = `${result.title} ${result.content}`.toLowerCase();
expect(text).toMatch(/mysql/);
expect(text).toMatch(/performance/);
expect(text).not.toMatch(/slow/);
});
});
it('should handle empty search results', async () => {
const results = await databaseService.executeFullTextSearch(
testDatabaseId,
'nonexistent query terms xyz123',
['test_articles'],
['title', 'content'],
10,
0
);
expect(results).toBeInstanceOf(Array);
expect(results).toHaveLength(0);
});
it('should support pagination', async () => {
// Get first page
const page1 = await databaseService.executeFullTextSearch(
testDatabaseId,
'database',
['test_articles'],
['title', 'content'],
2,
0
);
// Get second page
const page2 = await databaseService.executeFullTextSearch(
testDatabaseId,
'database',
['test_articles'],
['title', 'content'],
2,
2
);
expect(page1).toBeInstanceOf(Array);
expect(page2).toBeInstanceOf(Array);
// Results should be different (if there are enough total results)
if (page1.length === 2 && page2.length > 0) {
const page1Ids = page1.map(r => r.id);
const page2Ids = page2.map(r => r.id);
expect(page1Ids).not.toEqual(page2Ids);
}
});
it('should handle multiple tables', async () => {
const results = await databaseService.executeFullTextSearch(
testDatabaseId,
'gaming',
['test_articles', 'test_products'],
['title', 'content', 'name', 'description'],
10,
0
);
expect(results).toBeInstanceOf(Array);
// Results should include entries from both tables
const tables = [...new Set(results.map(r => r.table))];
expect(tables.length).toBeGreaterThan(0);
});
});
describe('Connection Management', () => {
it('should test database connection', async () => {
const connectionConfig = {
host: 'localhost',
port: 3306,
database: 'altus4_test',
username: process.env.TEST_DB_USER || 'altus4_test',
password: process.env.TEST_DB_PASSWORD || 'test_password',
};
const result = await databaseService.testConnection(connectionConfig);
expect(result).toMatchObject({
success: true,
message: expect.any(String),
responseTime: expect.any(Number),
});
});
it('should handle connection failures', async () => {
const invalidConfig = {
host: 'invalid-host',
port: 9999,
database: 'nonexistent',
username: 'invalid',
password: 'invalid',
};
const result = await databaseService.testConnection(invalidConfig);
expect(result).toMatchObject({
success: false,
error: expect.any(String),
});
});
});
describe('Schema Discovery', () => {
it('should discover database schema', async () => {
const schema = await databaseService.discoverSchema(testDatabaseId);
expect(schema).toMatchObject({
database: expect.any(String),
tables: expect.any(Array),
});
// Should include our test tables
const tableNames = schema.tables.map(t => t.name);
expect(tableNames).toContain('test_articles');
expect(tableNames).toContain('test_products');
// Verify table structure
const articlesTable = schema.tables.find(t => t.name === 'test_articles');
expect(articlesTable).toMatchObject({
name: 'test_articles',
columns: expect.any(Array),
fulltextIndexes: expect.any(Array),
estimatedRows: expect.any(Number),
});
// Should have FULLTEXT indexes
expect(articlesTable.fulltextIndexes.length).toBeGreaterThan(0);
});
});
});// tests/integration/services/search-integration.test.ts
import { SearchService } from '@/services/SearchService';
import { DatabaseService } from '@/services/DatabaseService';
import { AIService } from '@/services/AIService';
import { CacheService } from '@/services/CacheService';
import { testDb } from '../../config/database';
import { createTestUser, createTestDatabase } from '../../helpers/auth-helpers';
describe('Search Service Integration', () => {
let searchService: SearchService;
let databaseService: DatabaseService;
let aiService: AIService;
let cacheService: CacheService;
let testUser: any;
let testDatabaseId: string;
beforeAll(async () => {
await testDb.setupTestDatabase();
// Initialize real services
databaseService = new DatabaseService();
aiService = new AIService();
cacheService = new CacheService();
searchService = new SearchService(databaseService, aiService, cacheService);
testUser = await createTestUser();
testDatabaseId = await createTestDatabase(testUser.id);
});
afterAll(async () => {
await testDb.cleanupTestDatabase();
});
describe('End-to-End Search Flow', () => {
it('should execute complete search workflow', async () => {
const searchRequest = {
query: 'mysql performance optimization',
databases: [testDatabaseId],
userId: testUser.id,
searchMode: 'natural' as const,
limit: 10,
};
const result = await searchService.search(searchRequest);
expect(result).toMatchObject({
results: expect.any(Array),
totalCount: expect.any(Number),
executionTime: expect.any(Number),
categories: expect.any(Array),
suggestions: expect.any(Array),
});
// Verify search results structure
if (result.results.length > 0) {
const searchResult = result.results[0];
expect(searchResult).toMatchObject({
id: expect.any(String),
database: testDatabaseId,
table: expect.any(String),
data: expect.any(Object),
relevanceScore: expect.any(Number),
snippet: expect.any(String),
matchedColumns: expect.any(Array),
});
}
});
it('should cache search results', async () => {
const searchRequest = {
query: 'database indexing strategies',
databases: [testDatabaseId],
userId: testUser.id,
searchMode: 'natural' as const,
limit: 10,
};
// First search - should hit database
const startTime1 = Date.now();
const result1 = await searchService.search(searchRequest);
const executionTime1 = Date.now() - startTime1;
// Second search - should hit cache
const startTime2 = Date.now();
const result2 = await searchService.search(searchRequest);
const executionTime2 = Date.now() - startTime2;
// Results should be identical
expect(result1.results).toEqual(result2.results);
expect(result1.totalCount).toBe(result2.totalCount);
// Second search should be faster (cached)
expect(executionTime2).toBeLessThan(executionTime1);
});
it('should handle AI enhancement when available', async () => {
// Skip if AI service is not available
if (!aiService.isAvailable()) {
console.log('Skipping AI test - service not available');
return;
}
const searchRequest = {
query: 'improve database speed',
databases: [testDatabaseId],
userId: testUser.id,
searchMode: 'semantic' as const,
limit: 10,
};
const result = await searchService.search(searchRequest);
expect(result).toMatchObject({
results: expect.any(Array),
totalCount: expect.any(Number),
executionTime: expect.any(Number),
});
// AI-enhanced search might have categories
if (result.categories && result.categories.length > 0) {
expect(result.categories[0]).toMatchObject({
name: expect.any(String),
count: expect.any(Number),
});
}
});
it('should handle multiple database search', async () => {
// Create second test database
const testDatabase2Id = await createTestDatabase(testUser.id, {
name: 'Test Database 2',
});
const searchRequest = {
query: 'database performance',
databases: [testDatabaseId, testDatabase2Id],
userId: testUser.id,
searchMode: 'natural' as const,
limit: 20,
};
const result = await searchService.search(searchRequest);
expect(result).toMatchObject({
results: expect.any(Array),
totalCount: expect.any(Number),
executionTime: expect.any(Number),
});
// Results should potentially come from both databases
if (result.results.length > 0) {
const databases = [...new Set(result.results.map(r => r.database))];
expect(databases.length).toBeGreaterThan(0);
}
});
it('should handle database failures gracefully', async () => {
// Create database with invalid configuration
const invalidDatabaseId = await createTestDatabase(testUser.id, {
name: 'Invalid Database',
host: 'invalid-host',
port: 9999,
});
const searchRequest = {
query: 'test query',
databases: [testDatabaseId, invalidDatabaseId],
userId: testUser.id,
searchMode: 'natural' as const,
limit: 10,
};
// Should not throw error, but handle failure gracefully
const result = await searchService.search(searchRequest);
expect(result).toMatchObject({
results: expect.any(Array),
totalCount: expect.any(Number),
executionTime: expect.any(Number),
});
// Should still have results from valid database
if (result.results.length > 0) {
result.results.forEach(r => {
expect(r.database).toBe(testDatabaseId);
});
}
});
});
describe('Search Analytics Integration', () => {
it('should log search analytics', async () => {
const searchRequest = {
query: 'analytics test query',
databases: [testDatabaseId],
userId: testUser.id,
searchMode: 'natural' as const,
limit: 10,
};
await searchService.search(searchRequest);
// Verify analytics were logged
const connection = await testDb.getConnection();
const [searches] = await connection.execute(
'SELECT * FROM searches WHERE user_id = ? AND query = ?',
[testUser.id, searchRequest.query]
);
expect(searches).toHaveLength(1);
const searchLog = (searches as any[])[0];
expect(searchLog).toMatchObject({
user_id: testUser.id,
query: searchRequest.query,
search_mode: searchRequest.searchMode,
result_count: expect.any(Number),
execution_time_ms: expect.any(Number),
});
});
});
});// tests/integration/performance/api-performance.test.ts
import request from 'supertest';
import { TestServer } from '../../helpers/test-server';
import {
createTestUser,
createTestApiKey,
createTestDatabase,
} from '../../helpers/auth-helpers';
import { performance } from 'perf_hooks';
describe('API Performance Integration', () => {
let testServer: TestServer;
let baseUrl: string;
let testApiKey: string;
let testDatabaseId: string;
beforeAll(async () => {
testServer = new TestServer();
await testServer.start();
baseUrl = testServer.getBaseUrl();
const testUser = await createTestUser();
testApiKey = await createTestApiKey(testUser.id);
testDatabaseId = await createTestDatabase(testUser.id);
});
afterAll(async () => {
await testServer.stop();
});
describe('Search Performance', () => {
it('should handle concurrent search requests', async () => {
const concurrentRequests = 10;
const searchPromises = Array.from(
{ length: concurrentRequests },
(_, i) =>
request(baseUrl)
.post('/api/v1/search')
.set('Authorization', `Bearer ${testApiKey}`)
.send({
query: `performance test query ${i}`,
databases: [testDatabaseId],
limit: 10,
})
);
const startTime = performance.now();
const results = await Promise.all(searchPromises);
const endTime = performance.now();
const totalTime = endTime - startTime;
const averageTime = totalTime / concurrentRequests;
// All requests should succeed
results.forEach(response => {
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
});
// Performance assertions
expect(averageTime).toBeLessThan(1000); // Average < 1 second
expect(totalTime).toBeLessThan(5000); // Total < 5 seconds
});
it('should maintain performance under load', async () => {
const requestCount = 50;
const batchSize = 10;
const batches = Math.ceil(requestCount / batchSize);
const allResults = [];
const executionTimes = [];
for (let batch = 0; batch < batches; batch++) {
const batchPromises = Array.from({ length: batchSize }, (_, i) => {
const startTime = performance.now();
return request(baseUrl)
.post('/api/v1/search')
.set('Authorization', `Bearer ${testApiKey}`)
.send({
query: 'load test query',
databases: [testDatabaseId],
limit: 10,
})
.then(response => {
const endTime = performance.now();
executionTimes.push(endTime - startTime);
return response;
});
});
const batchResults = await Promise.all(batchPromises);
allResults.push(...batchResults);
// Small delay between batches
await new Promise(resolve => setTimeout(resolve, 100));
}
// All requests should succeed
const successfulRequests = allResults.filter(
r => r.status === 200
).length;
const successRate = (successfulRequests / requestCount) * 100;
expect(successRate).toBeGreaterThan(95); // 95% success rate
// Performance metrics
const averageTime =
executionTimes.reduce((a, b) => a + b, 0) / executionTimes.length;
const p95Time = executionTimes.sort((a, b) => a - b)[
Math.floor(executionTimes.length * 0.95)
];
expect(averageTime).toBeLessThan(500); // Average < 500ms
expect(p95Time).toBeLessThan(1000); // P95 < 1 second
});
});
});// jest.integration.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests/integration'],
testMatch: ['<rootDir>/tests/integration/**/*.test.ts'],
setupFilesAfterEnv: ['<rootDir>/tests/integration-setup.ts'],
testTimeout: 30000, // 30 seconds for integration tests
maxWorkers: 1, // Run tests sequentially to avoid database conflicts
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/__tests__/**',
],
coverageDirectory: 'coverage/integration',
globalSetup: '<rootDir>/tests/global-setup.ts',
globalTeardown: '<rootDir>/tests/global-teardown.ts',
};// tests/integration-setup.ts
import { testDb } from './config/database';
// Global setup for integration tests
beforeAll(async () => {
// Setup test database
await testDb.setupTestDatabase();
// Set environment variables
process.env.NODE_ENV = 'test';
process.env.LOG_LEVEL = 'error';
process.env.CACHE_ENABLED = 'true';
process.env.AI_ENABLED = 'false'; // Disable AI for most tests
}, 60000); // 60 second timeout
afterAll(async () => {
// Cleanup test database
await testDb.cleanupTestDatabase();
}, 30000);
// Clean up between tests
afterEach(async () => {
// Clear cache
if (global.cacheService) {
await global.cacheService.flushAll();
}
});- Use Real Dependencies: Test with actual database, cache, and external services
- Isolate Tests: Each test should be independent and not affect others
- Test Happy Path: Verify normal operation flows
- Test Error Conditions: Verify error handling and recovery
- Performance Aware: Monitor test execution time and system resources
- Data Management: Use fixtures and cleanup between tests
- Environment Consistency: Use consistent test environment setup
// Pattern: Test setup and cleanup
describe('Feature Integration', () => {
let testData: any;
beforeEach(async () => {
testData = await setupTestData();
});
afterEach(async () => {
await cleanupTestData(testData);
});
it('should work correctly', async () => {
// Test implementation
});
});
// Pattern: Error handling verification
it('should handle service failures gracefully', async () => {
// Simulate service failure
jest
.spyOn(externalService, 'method')
.mockRejectedValue(new Error('Service down'));
// Verify graceful handling
const result = await serviceUnderTest.operation();
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
// Pattern: Performance assertions
it('should meet performance requirements', async () => {
const startTime = performance.now();
await operationUnderTest();
const executionTime = performance.now() - startTime;
expect(executionTime).toBeLessThan(1000); // < 1 second
});- Unit Testing Guide - Unit testing strategies and patterns
- Performance Testing Guide - Performance testing implementation
- Testing Overview - Complete testing strategy
- Development Standards - Code quality standards
Integration tests ensure that Altus 4 components work correctly together in realistic scenarios. Follow these patterns to build comprehensive integration test suites that verify system behavior and catch integration issues early.