test: add Phase 4 database integration tests (partial)
- Add comprehensive test utilities for database testing - Implement connection management tests for in-memory and file databases - Add transaction tests including nested transactions and savepoints - Test database lifecycle, error handling, and performance - Include tests for WAL mode, connection pooling, and constraints Part of Phase 4: Integration Testing
This commit is contained in:
179
tests/mocks/README.md
Normal file
179
tests/mocks/README.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# MSW (Mock Service Worker) Setup for n8n API
|
||||
|
||||
This directory contains the MSW infrastructure for mocking n8n API responses in tests.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
mocks/
|
||||
├── n8n-api/
|
||||
│ ├── handlers.ts # Default MSW handlers for n8n API endpoints
|
||||
│ ├── data/ # Mock data for responses
|
||||
│ │ ├── workflows.ts # Mock workflow data and factories
|
||||
│ │ ├── executions.ts # Mock execution data and factories
|
||||
│ │ └── credentials.ts # Mock credential data
|
||||
│ └── index.ts # Central exports
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage (Automatic)
|
||||
|
||||
MSW is automatically initialized for all tests via `vitest.config.ts`. The default handlers will intercept all n8n API requests.
|
||||
|
||||
```typescript
|
||||
// Your test file
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { N8nApiClient } from '@/services/n8n-api-client';
|
||||
|
||||
describe('My Integration Test', () => {
|
||||
it('should work with mocked n8n API', async () => {
|
||||
const client = new N8nApiClient({ baseUrl: 'http://localhost:5678' });
|
||||
|
||||
// This will hit the MSW mock, not the real API
|
||||
const workflows = await client.getWorkflows();
|
||||
|
||||
expect(workflows).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Handlers for Specific Tests
|
||||
|
||||
```typescript
|
||||
import { useHandlers, http, HttpResponse } from '@tests/setup/msw-setup';
|
||||
|
||||
it('should handle custom response', async () => {
|
||||
// Add custom handler for this test only
|
||||
useHandlers(
|
||||
http.get('*/api/v1/workflows', () => {
|
||||
return HttpResponse.json({
|
||||
data: [{ id: 'custom-workflow', name: 'Custom' }]
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Your test code here
|
||||
});
|
||||
```
|
||||
|
||||
### Using Factory Functions
|
||||
|
||||
```typescript
|
||||
import { workflowFactory, executionFactory } from '@tests/mocks/n8n-api';
|
||||
|
||||
it('should test with factory data', async () => {
|
||||
const workflow = workflowFactory.simple('n8n-nodes-base.httpRequest', {
|
||||
method: 'POST',
|
||||
url: 'https://example.com/api'
|
||||
});
|
||||
|
||||
useHandlers(
|
||||
http.get('*/api/v1/workflows/test-id', () => {
|
||||
return HttpResponse.json({ data: workflow });
|
||||
})
|
||||
);
|
||||
|
||||
// Your test code here
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Test Server
|
||||
|
||||
For integration tests that need more control:
|
||||
|
||||
```typescript
|
||||
import { mswTestServer, n8nApiMock } from '@tests/integration/setup/msw-test-server';
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
beforeAll(() => {
|
||||
mswTestServer.start({ onUnhandledRequest: 'error' });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mswTestServer.stop();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mswTestServer.reset();
|
||||
});
|
||||
|
||||
it('should test workflow creation', async () => {
|
||||
// Use helper to mock workflow creation
|
||||
mswTestServer.use(
|
||||
n8nApiMock.mockWorkflowCreate({
|
||||
id: 'new-workflow',
|
||||
name: 'Created Workflow'
|
||||
})
|
||||
);
|
||||
|
||||
// Your test code here
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
Enable MSW debug logging:
|
||||
|
||||
```bash
|
||||
MSW_DEBUG=true npm test
|
||||
```
|
||||
|
||||
This will log all intercepted requests and responses.
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Use factories for test data**: Don't hardcode test data, use the provided factories
|
||||
2. **Reset handlers between tests**: This is done automatically, but be aware of it
|
||||
3. **Be specific with handlers**: Use specific URLs/patterns to avoid conflicts
|
||||
4. **Test error scenarios**: Use the error helpers to test error handling
|
||||
5. **Verify unhandled requests**: In integration tests, verify no unexpected requests were made
|
||||
|
||||
### Common Patterns
|
||||
|
||||
#### Testing Success Scenarios
|
||||
```typescript
|
||||
useHandlers(
|
||||
http.get('*/api/v1/workflows/:id', ({ params }) => {
|
||||
return HttpResponse.json({
|
||||
data: workflowFactory.custom({ id: params.id as string })
|
||||
});
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
#### Testing Error Scenarios
|
||||
```typescript
|
||||
useHandlers(
|
||||
http.get('*/api/v1/workflows/:id', () => {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Not found', code: 'NOT_FOUND' },
|
||||
{ status: 404 }
|
||||
);
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
#### Testing Pagination
|
||||
```typescript
|
||||
const workflows = Array.from({ length: 150 }, (_, i) =>
|
||||
workflowFactory.custom({ id: `workflow_${i}` })
|
||||
);
|
||||
|
||||
useHandlers(
|
||||
http.get('*/api/v1/workflows', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const limit = parseInt(url.searchParams.get('limit') || '100');
|
||||
const cursor = url.searchParams.get('cursor');
|
||||
|
||||
const start = cursor ? parseInt(cursor) : 0;
|
||||
const data = workflows.slice(start, start + limit);
|
||||
|
||||
return HttpResponse.json({
|
||||
data,
|
||||
nextCursor: start + limit < workflows.length ? String(start + limit) : null
|
||||
});
|
||||
})
|
||||
);
|
||||
```
|
||||
49
tests/mocks/n8n-api/data/credentials.ts
Normal file
49
tests/mocks/n8n-api/data/credentials.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Mock credential data for MSW handlers
|
||||
*/
|
||||
|
||||
export interface MockCredential {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
data?: Record<string, any>; // Usually encrypted in real n8n
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export const mockCredentials: MockCredential[] = [
|
||||
{
|
||||
id: 'cred_1',
|
||||
name: 'Slack Account',
|
||||
type: 'slackApi',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z'
|
||||
},
|
||||
{
|
||||
id: 'cred_2',
|
||||
name: 'HTTP Header Auth',
|
||||
type: 'httpHeaderAuth',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z'
|
||||
},
|
||||
{
|
||||
id: 'cred_3',
|
||||
name: 'OpenAI API',
|
||||
type: 'openAiApi',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z'
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Factory for creating mock credentials
|
||||
*/
|
||||
export const credentialFactory = {
|
||||
create: (type: string, name?: string): MockCredential => ({
|
||||
id: `cred_${Date.now()}`,
|
||||
name: name || `${type} Credential`,
|
||||
type,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
})
|
||||
};
|
||||
159
tests/mocks/n8n-api/data/executions.ts
Normal file
159
tests/mocks/n8n-api/data/executions.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Mock execution data for MSW handlers
|
||||
*/
|
||||
|
||||
export interface MockExecution {
|
||||
id: string;
|
||||
workflowId: string;
|
||||
status: 'success' | 'error' | 'waiting' | 'running';
|
||||
mode: 'manual' | 'trigger' | 'webhook' | 'internal';
|
||||
startedAt: string;
|
||||
stoppedAt?: string;
|
||||
data?: any;
|
||||
error?: any;
|
||||
}
|
||||
|
||||
export const mockExecutions: MockExecution[] = [
|
||||
{
|
||||
id: 'exec_1',
|
||||
workflowId: 'workflow_1',
|
||||
status: 'success',
|
||||
mode: 'manual',
|
||||
startedAt: '2024-01-01T10:00:00.000Z',
|
||||
stoppedAt: '2024-01-01T10:00:05.000Z',
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
'node_2': [
|
||||
{
|
||||
startTime: 1704106800000,
|
||||
executionTime: 234,
|
||||
data: {
|
||||
main: [[{
|
||||
json: {
|
||||
status: 200,
|
||||
data: { message: 'Success' }
|
||||
}
|
||||
}]]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'exec_2',
|
||||
workflowId: 'workflow_2',
|
||||
status: 'error',
|
||||
mode: 'webhook',
|
||||
startedAt: '2024-01-01T11:00:00.000Z',
|
||||
stoppedAt: '2024-01-01T11:00:02.000Z',
|
||||
error: {
|
||||
message: 'Could not send message to Slack',
|
||||
stack: 'Error: Could not send message to Slack\n at SlackNode.execute',
|
||||
node: 'slack_1'
|
||||
},
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
'webhook_1': [
|
||||
{
|
||||
startTime: 1704110400000,
|
||||
executionTime: 10,
|
||||
data: {
|
||||
main: [[{
|
||||
json: {
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: { message: 'Test webhook' }
|
||||
}
|
||||
}]]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'exec_3',
|
||||
workflowId: 'workflow_3',
|
||||
status: 'waiting',
|
||||
mode: 'trigger',
|
||||
startedAt: '2024-01-01T12:00:00.000Z',
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {}
|
||||
},
|
||||
waitingExecutions: {
|
||||
'agent_1': {
|
||||
reason: 'Waiting for user input'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Factory functions for creating mock executions
|
||||
*/
|
||||
export const executionFactory = {
|
||||
/**
|
||||
* Create a successful execution
|
||||
*/
|
||||
success: (workflowId: string, data?: any): MockExecution => ({
|
||||
id: `exec_${Date.now()}`,
|
||||
workflowId,
|
||||
status: 'success',
|
||||
mode: 'manual',
|
||||
startedAt: new Date().toISOString(),
|
||||
stoppedAt: new Date(Date.now() + 5000).toISOString(),
|
||||
data: data || {
|
||||
resultData: {
|
||||
runData: {
|
||||
'node_1': [{
|
||||
startTime: Date.now(),
|
||||
executionTime: 100,
|
||||
data: {
|
||||
main: [[{ json: { success: true } }]]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create a failed execution
|
||||
*/
|
||||
error: (workflowId: string, error: { message: string; node?: string }): MockExecution => ({
|
||||
id: `exec_${Date.now()}`,
|
||||
workflowId,
|
||||
status: 'error',
|
||||
mode: 'manual',
|
||||
startedAt: new Date().toISOString(),
|
||||
stoppedAt: new Date(Date.now() + 2000).toISOString(),
|
||||
error: {
|
||||
message: error.message,
|
||||
stack: `Error: ${error.message}\n at Node.execute`,
|
||||
node: error.node
|
||||
},
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create a custom execution
|
||||
*/
|
||||
custom: (config: Partial<MockExecution>): MockExecution => ({
|
||||
id: `exec_${Date.now()}`,
|
||||
workflowId: 'workflow_1',
|
||||
status: 'success',
|
||||
mode: 'manual',
|
||||
startedAt: new Date().toISOString(),
|
||||
...config
|
||||
})
|
||||
};
|
||||
219
tests/mocks/n8n-api/data/workflows.ts
Normal file
219
tests/mocks/n8n-api/data/workflows.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Mock workflow data for MSW handlers
|
||||
* These represent typical n8n workflows used in tests
|
||||
*/
|
||||
|
||||
export interface MockWorkflow {
|
||||
id: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
nodes: any[];
|
||||
connections: any;
|
||||
settings?: any;
|
||||
tags?: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
versionId: string;
|
||||
}
|
||||
|
||||
export const mockWorkflows: MockWorkflow[] = [
|
||||
{
|
||||
id: 'workflow_1',
|
||||
name: 'Test HTTP Workflow',
|
||||
active: true,
|
||||
nodes: [
|
||||
{
|
||||
id: 'node_1',
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'node_2',
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [450, 300],
|
||||
parameters: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/data',
|
||||
authentication: 'none',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'node_1': {
|
||||
main: [[{ node: 'node_2', type: 'main', index: 0 }]]
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
executionOrder: 'v1',
|
||||
timezone: 'UTC'
|
||||
},
|
||||
tags: ['http', 'api'],
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
versionId: '1'
|
||||
},
|
||||
{
|
||||
id: 'workflow_2',
|
||||
name: 'Webhook to Slack',
|
||||
active: false,
|
||||
nodes: [
|
||||
{
|
||||
id: 'webhook_1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [250, 300],
|
||||
parameters: {
|
||||
httpMethod: 'POST',
|
||||
path: 'test-webhook',
|
||||
responseMode: 'onReceived',
|
||||
responseData: 'firstEntryJson'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'slack_1',
|
||||
name: 'Slack',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
typeVersion: 2.2,
|
||||
position: [450, 300],
|
||||
parameters: {
|
||||
resource: 'message',
|
||||
operation: 'post',
|
||||
channel: '#general',
|
||||
text: '={{ $json.message }}',
|
||||
authentication: 'accessToken'
|
||||
},
|
||||
credentials: {
|
||||
slackApi: {
|
||||
id: 'cred_1',
|
||||
name: 'Slack Account'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'webhook_1': {
|
||||
main: [[{ node: 'slack_1', type: 'main', index: 0 }]]
|
||||
}
|
||||
},
|
||||
settings: {},
|
||||
tags: ['webhook', 'slack', 'notification'],
|
||||
createdAt: '2024-01-02T00:00:00.000Z',
|
||||
updatedAt: '2024-01-02T00:00:00.000Z',
|
||||
versionId: '1'
|
||||
},
|
||||
{
|
||||
id: 'workflow_3',
|
||||
name: 'AI Agent Workflow',
|
||||
active: true,
|
||||
nodes: [
|
||||
{
|
||||
id: 'agent_1',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1.7,
|
||||
position: [250, 300],
|
||||
parameters: {
|
||||
agent: 'openAiFunctionsAgent',
|
||||
prompt: 'You are a helpful assistant',
|
||||
temperature: 0.7
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'tool_1',
|
||||
name: 'HTTP Tool',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [450, 200],
|
||||
parameters: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/search',
|
||||
sendQuery: true,
|
||||
queryParameters: {
|
||||
parameters: [
|
||||
{
|
||||
name: 'q',
|
||||
value: '={{ $json.query }}'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'tool_1': {
|
||||
ai_tool: [[{ node: 'agent_1', type: 'ai_tool', index: 0 }]]
|
||||
}
|
||||
},
|
||||
settings: {},
|
||||
tags: ['ai', 'agent', 'langchain'],
|
||||
createdAt: '2024-01-03T00:00:00.000Z',
|
||||
updatedAt: '2024-01-03T00:00:00.000Z',
|
||||
versionId: '1'
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Factory functions for creating mock workflows
|
||||
*/
|
||||
export const workflowFactory = {
|
||||
/**
|
||||
* Create a simple workflow with Start and one other node
|
||||
*/
|
||||
simple: (nodeType: string, nodeParams: any = {}): MockWorkflow => ({
|
||||
id: `workflow_${Date.now()}`,
|
||||
name: `Test ${nodeType} Workflow`,
|
||||
active: true,
|
||||
nodes: [
|
||||
{
|
||||
id: 'start_1',
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'node_1',
|
||||
name: nodeType.split('.').pop() || nodeType,
|
||||
type: nodeType,
|
||||
typeVersion: 1,
|
||||
position: [450, 300],
|
||||
parameters: nodeParams
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'start_1': {
|
||||
main: [[{ node: 'node_1', type: 'main', index: 0 }]]
|
||||
}
|
||||
},
|
||||
settings: {},
|
||||
tags: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
versionId: '1'
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create a workflow with specific nodes and connections
|
||||
*/
|
||||
custom: (config: Partial<MockWorkflow>): MockWorkflow => ({
|
||||
id: `workflow_${Date.now()}`,
|
||||
name: 'Custom Workflow',
|
||||
active: false,
|
||||
nodes: [],
|
||||
connections: {},
|
||||
settings: {},
|
||||
tags: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
versionId: '1',
|
||||
...config
|
||||
})
|
||||
};
|
||||
287
tests/mocks/n8n-api/handlers.ts
Normal file
287
tests/mocks/n8n-api/handlers.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import { http, HttpResponse, RequestHandler } from 'msw';
|
||||
import { mockWorkflows } from './data/workflows';
|
||||
import { mockExecutions } from './data/executions';
|
||||
import { mockCredentials } from './data/credentials';
|
||||
|
||||
// Base URL for n8n API (will be overridden by actual URL in tests)
|
||||
const API_BASE = process.env.N8N_API_URL || 'http://localhost:5678';
|
||||
|
||||
/**
|
||||
* Default handlers for n8n API endpoints
|
||||
* These can be overridden in specific tests using server.use()
|
||||
*/
|
||||
export const handlers: RequestHandler[] = [
|
||||
// Health check endpoint
|
||||
http.get('*/api/v1/health', () => {
|
||||
return HttpResponse.json({
|
||||
status: 'ok',
|
||||
version: '1.103.2',
|
||||
features: {
|
||||
workflows: true,
|
||||
executions: true,
|
||||
credentials: true,
|
||||
webhooks: true,
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
// Workflow endpoints
|
||||
http.get('*/api/v1/workflows', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const limit = parseInt(url.searchParams.get('limit') || '100');
|
||||
const cursor = url.searchParams.get('cursor');
|
||||
const active = url.searchParams.get('active');
|
||||
|
||||
let filtered = mockWorkflows;
|
||||
|
||||
// Filter by active status if provided
|
||||
if (active !== null) {
|
||||
filtered = filtered.filter(w => w.active === (active === 'true'));
|
||||
}
|
||||
|
||||
// Simple pagination simulation
|
||||
const startIndex = cursor ? parseInt(cursor) : 0;
|
||||
const paginatedData = filtered.slice(startIndex, startIndex + limit);
|
||||
const hasMore = startIndex + limit < filtered.length;
|
||||
const nextCursor = hasMore ? String(startIndex + limit) : null;
|
||||
|
||||
return HttpResponse.json({
|
||||
data: paginatedData,
|
||||
nextCursor,
|
||||
hasMore
|
||||
});
|
||||
}),
|
||||
|
||||
http.get('*/api/v1/workflows/:id', ({ params }) => {
|
||||
const workflow = mockWorkflows.find(w => w.id === params.id);
|
||||
|
||||
if (!workflow) {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Workflow not found', code: 'NOT_FOUND' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return HttpResponse.json({ data: workflow });
|
||||
}),
|
||||
|
||||
http.post('*/api/v1/workflows', async ({ request }) => {
|
||||
const body = await request.json() as any;
|
||||
|
||||
// Validate required fields
|
||||
if (!body.name || !body.nodes || !body.connections) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
message: 'Validation failed',
|
||||
errors: {
|
||||
name: !body.name ? 'Name is required' : undefined,
|
||||
nodes: !body.nodes ? 'Nodes are required' : undefined,
|
||||
connections: !body.connections ? 'Connections are required' : undefined,
|
||||
},
|
||||
code: 'VALIDATION_ERROR'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const newWorkflow = {
|
||||
id: `workflow_${Date.now()}`,
|
||||
name: body.name,
|
||||
active: body.active || false,
|
||||
nodes: body.nodes,
|
||||
connections: body.connections,
|
||||
settings: body.settings || {},
|
||||
tags: body.tags || [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
versionId: '1'
|
||||
};
|
||||
|
||||
mockWorkflows.push(newWorkflow);
|
||||
|
||||
return HttpResponse.json({ data: newWorkflow }, { status: 201 });
|
||||
}),
|
||||
|
||||
http.patch('*/api/v1/workflows/:id', async ({ params, request }) => {
|
||||
const workflowIndex = mockWorkflows.findIndex(w => w.id === params.id);
|
||||
|
||||
if (workflowIndex === -1) {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Workflow not found', code: 'NOT_FOUND' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json() as any;
|
||||
const updatedWorkflow = {
|
||||
...mockWorkflows[workflowIndex],
|
||||
...body,
|
||||
id: params.id, // Ensure ID doesn't change
|
||||
updatedAt: new Date().toISOString(),
|
||||
versionId: String(parseInt(mockWorkflows[workflowIndex].versionId) + 1)
|
||||
};
|
||||
|
||||
mockWorkflows[workflowIndex] = updatedWorkflow;
|
||||
|
||||
return HttpResponse.json({ data: updatedWorkflow });
|
||||
}),
|
||||
|
||||
http.delete('*/api/v1/workflows/:id', ({ params }) => {
|
||||
const workflowIndex = mockWorkflows.findIndex(w => w.id === params.id);
|
||||
|
||||
if (workflowIndex === -1) {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Workflow not found', code: 'NOT_FOUND' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
mockWorkflows.splice(workflowIndex, 1);
|
||||
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
// Execution endpoints
|
||||
http.get('*/api/v1/executions', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const limit = parseInt(url.searchParams.get('limit') || '100');
|
||||
const cursor = url.searchParams.get('cursor');
|
||||
const workflowId = url.searchParams.get('workflowId');
|
||||
const status = url.searchParams.get('status');
|
||||
|
||||
let filtered = mockExecutions;
|
||||
|
||||
// Filter by workflow ID if provided
|
||||
if (workflowId) {
|
||||
filtered = filtered.filter(e => e.workflowId === workflowId);
|
||||
}
|
||||
|
||||
// Filter by status if provided
|
||||
if (status) {
|
||||
filtered = filtered.filter(e => e.status === status);
|
||||
}
|
||||
|
||||
// Simple pagination simulation
|
||||
const startIndex = cursor ? parseInt(cursor) : 0;
|
||||
const paginatedData = filtered.slice(startIndex, startIndex + limit);
|
||||
const hasMore = startIndex + limit < filtered.length;
|
||||
const nextCursor = hasMore ? String(startIndex + limit) : null;
|
||||
|
||||
return HttpResponse.json({
|
||||
data: paginatedData,
|
||||
nextCursor,
|
||||
hasMore
|
||||
});
|
||||
}),
|
||||
|
||||
http.get('*/api/v1/executions/:id', ({ params }) => {
|
||||
const execution = mockExecutions.find(e => e.id === params.id);
|
||||
|
||||
if (!execution) {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Execution not found', code: 'NOT_FOUND' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return HttpResponse.json({ data: execution });
|
||||
}),
|
||||
|
||||
http.delete('*/api/v1/executions/:id', ({ params }) => {
|
||||
const executionIndex = mockExecutions.findIndex(e => e.id === params.id);
|
||||
|
||||
if (executionIndex === -1) {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Execution not found', code: 'NOT_FOUND' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
mockExecutions.splice(executionIndex, 1);
|
||||
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
// Webhook endpoints (dynamic handling)
|
||||
http.all('*/webhook/*', async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const method = request.method;
|
||||
const body = request.body ? await request.json() : undefined;
|
||||
|
||||
// Log webhook trigger in debug mode
|
||||
if (process.env.MSW_DEBUG === 'true') {
|
||||
console.log('[MSW] Webhook triggered:', {
|
||||
url: url.pathname,
|
||||
method,
|
||||
body
|
||||
});
|
||||
}
|
||||
|
||||
// Return success response by default
|
||||
return HttpResponse.json({
|
||||
success: true,
|
||||
webhookUrl: url.pathname,
|
||||
method,
|
||||
timestamp: new Date().toISOString(),
|
||||
data: body
|
||||
});
|
||||
}),
|
||||
|
||||
// Catch-all for unhandled API routes (helps identify missing handlers)
|
||||
http.all('*/api/*', ({ request }) => {
|
||||
console.warn('[MSW] Unhandled API request:', request.method, request.url);
|
||||
|
||||
return HttpResponse.json(
|
||||
{
|
||||
message: 'Not implemented in mock',
|
||||
code: 'NOT_IMPLEMENTED',
|
||||
path: new URL(request.url).pathname,
|
||||
method: request.method
|
||||
},
|
||||
{ status: 501 }
|
||||
);
|
||||
}),
|
||||
];
|
||||
|
||||
/**
|
||||
* Dynamic handler registration helpers
|
||||
*/
|
||||
export const dynamicHandlers = {
|
||||
/**
|
||||
* Add a workflow that will be returned by GET requests
|
||||
*/
|
||||
addWorkflow: (workflow: any) => {
|
||||
mockWorkflows.push(workflow);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all mock workflows
|
||||
*/
|
||||
clearWorkflows: () => {
|
||||
mockWorkflows.length = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add an execution that will be returned by GET requests
|
||||
*/
|
||||
addExecution: (execution: any) => {
|
||||
mockExecutions.push(execution);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all mock executions
|
||||
*/
|
||||
clearExecutions: () => {
|
||||
mockExecutions.length = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset all mock data to initial state
|
||||
*/
|
||||
resetAll: () => {
|
||||
// Reset arrays to initial state (implementation depends on data modules)
|
||||
mockWorkflows.length = 0;
|
||||
mockExecutions.length = 0;
|
||||
mockCredentials.length = 0;
|
||||
}
|
||||
};
|
||||
19
tests/mocks/n8n-api/index.ts
Normal file
19
tests/mocks/n8n-api/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Central export for all n8n API mocks
|
||||
*/
|
||||
|
||||
export * from './handlers';
|
||||
export * from './data/workflows';
|
||||
export * from './data/executions';
|
||||
export * from './data/credentials';
|
||||
|
||||
// Re-export MSW utilities for convenience
|
||||
export { http, HttpResponse } from 'msw';
|
||||
|
||||
// Export factory utilities
|
||||
export { n8nHandlerFactory } from '../setup/msw-setup';
|
||||
export {
|
||||
n8nApiMock,
|
||||
testDataBuilders,
|
||||
mswTestServer
|
||||
} from '../../integration/setup/msw-test-server';
|
||||
Reference in New Issue
Block a user