mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
chore: Add mockserver for e2e testing (#19104)
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
setupRedis,
|
||||
setupCaddyLoadBalancer,
|
||||
pollContainerHttpEndpoint,
|
||||
setupProxyServer,
|
||||
} from './n8n-test-container-dependencies';
|
||||
import { createSilentLogConsumer } from './n8n-test-container-utils';
|
||||
|
||||
@@ -31,6 +32,7 @@ const POSTGRES_IMAGE = 'postgres:16-alpine';
|
||||
const REDIS_IMAGE = 'redis:7-alpine';
|
||||
const CADDY_IMAGE = 'caddy:2-alpine';
|
||||
const N8N_E2E_IMAGE = 'n8nio/n8n:local';
|
||||
const MOCKSERVER_IMAGE = 'mockserver/mockserver:5.15.0';
|
||||
|
||||
// Default n8n image (can be overridden via N8N_DOCKER_IMAGE env var)
|
||||
const N8N_IMAGE = process.env.N8N_DOCKER_IMAGE ?? N8N_E2E_IMAGE;
|
||||
@@ -78,6 +80,7 @@ export interface N8NConfig {
|
||||
memory?: number; // in GB
|
||||
cpu?: number; // in cores
|
||||
};
|
||||
proxyServerEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface N8NStack {
|
||||
@@ -109,7 +112,14 @@ export interface N8NStack {
|
||||
* });
|
||||
*/
|
||||
export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack> {
|
||||
const { postgres = false, queueMode = false, env = {}, projectName, resourceQuota } = config;
|
||||
const {
|
||||
postgres = false,
|
||||
queueMode = false,
|
||||
env = {},
|
||||
proxyServerEnabled = false,
|
||||
projectName,
|
||||
resourceQuota,
|
||||
} = config;
|
||||
const queueConfig = normalizeQueueConfig(queueMode);
|
||||
const usePostgres = postgres || !!queueConfig;
|
||||
const uniqueProjectName = projectName ?? `n8n-stack-${Math.random().toString(36).substring(7)}`;
|
||||
@@ -117,7 +127,7 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
||||
|
||||
const mainCount = queueConfig?.mains ?? 1;
|
||||
const needsLoadBalancer = mainCount > 1;
|
||||
const needsNetwork = usePostgres || !!queueConfig || needsLoadBalancer;
|
||||
const needsNetwork = usePostgres || !!queueConfig || needsLoadBalancer || proxyServerEnabled;
|
||||
|
||||
let network: StartedNetwork | undefined;
|
||||
if (needsNetwork) {
|
||||
@@ -182,6 +192,31 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
||||
}
|
||||
}
|
||||
|
||||
if (proxyServerEnabled) {
|
||||
assert(network, 'Network should be created for ProxyServer');
|
||||
const hostname = 'proxyserver';
|
||||
const port = 1080;
|
||||
const url = `http://${hostname}:${port}`;
|
||||
const proxyServerContainer: StartedTestContainer = await setupProxyServer({
|
||||
proxyServerImage: MOCKSERVER_IMAGE,
|
||||
projectName: uniqueProjectName,
|
||||
network,
|
||||
hostname,
|
||||
port,
|
||||
});
|
||||
|
||||
containers.push(proxyServerContainer);
|
||||
|
||||
environment = {
|
||||
...environment,
|
||||
// Configure n8n to proxy all HTTP requests through ProxyServer
|
||||
HTTP_PROXY: url,
|
||||
HTTPS_PROXY: url,
|
||||
// Ensure https requests can be proxied without SSL issues
|
||||
...(proxyServerEnabled ? { NODE_TLS_REJECT_UNAUTHORIZED: '0' } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
let baseUrl: string;
|
||||
|
||||
if (needsLoadBalancer) {
|
||||
|
||||
@@ -316,6 +316,40 @@ export async function pollContainerHttpEndpoint(
|
||||
);
|
||||
}
|
||||
|
||||
export async function setupProxyServer({
|
||||
proxyServerImage,
|
||||
projectName,
|
||||
network,
|
||||
hostname,
|
||||
port,
|
||||
}: {
|
||||
proxyServerImage: string;
|
||||
projectName: string;
|
||||
network: StartedNetwork;
|
||||
hostname: string;
|
||||
port: number;
|
||||
}): Promise<StartedTestContainer> {
|
||||
const { consumer, throwWithLogs } = createSilentLogConsumer();
|
||||
|
||||
try {
|
||||
return await new GenericContainer(proxyServerImage)
|
||||
.withNetwork(network)
|
||||
.withNetworkAliases(hostname)
|
||||
.withExposedPorts(port)
|
||||
// Wait.forListeningPorts strategy did not work here for some reason
|
||||
.withWaitStrategy(Wait.forLogMessage(`INFO ${port} started on port: ${port}`))
|
||||
.withLabels({
|
||||
'com.docker.compose.project': projectName,
|
||||
'com.docker.compose.service': 'proxyserver',
|
||||
})
|
||||
.withName(`${projectName}-proxyserver`)
|
||||
.withReuse()
|
||||
.withLogConsumer(consumer)
|
||||
.start();
|
||||
} catch (error) {
|
||||
return throwWithLogs(error);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Look at Ollama container?
|
||||
// TODO: Look at MariaDB container?
|
||||
// TODO: Look at MockServer container, could we use this for mocking out external services?
|
||||
|
||||
@@ -37,6 +37,7 @@ test('postgres only @mode:postgres', ...) // Mode-specific
|
||||
test('needs clean db @db:reset', ...) // Sequential per worker
|
||||
test('chaos test @mode:multi-main @chaostest', ...) // Isolated per worker
|
||||
test('cloud resource test @cloud:trial', ...) // Cloud resource constraints
|
||||
test('proxy test @capability:proxy', ...) // Requires proxy server capability
|
||||
```
|
||||
|
||||
## Fixture Selection
|
||||
@@ -73,5 +74,48 @@ test('Performance under constraints @cloud:trial', async ({ n8n, api }) => {
|
||||
- **utils**: Utility functions (string manipulation, helpers, etc.)
|
||||
- **workflows**: Test workflow JSON files for import/reuse
|
||||
|
||||
## Writing Tests with Proxy
|
||||
|
||||
You can use ProxyServer to mock API requests.
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '../fixtures/base';
|
||||
|
||||
// The `@capability:proxy` tag ensures tests only run when proxy infrastructure is available.
|
||||
test.describe('Proxy tests @capability:proxy', () => {
|
||||
test('should mock HTTP requests', async ({ proxyServer, n8n }) => {
|
||||
// Create mock expectations
|
||||
await proxyServer.createGetExpectation('/api/data', { result: 'mocked' });
|
||||
|
||||
// Execute workflow that makes HTTP requests
|
||||
await n8n.canvas.openNewWorkflow();
|
||||
// ... test implementation
|
||||
|
||||
// Verify requests were proxied
|
||||
expect(await proxyServer.wasGetRequestMade('/api/data')).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Recording and replaying requests
|
||||
|
||||
The ProxyServer service supports recording HTTP requests for test mocking and replay. All proxied requests are automatically recorded by the mock server as described in the [Mock Server documentation](https://www.mock-server.com/proxy/record_and_replay.html).
|
||||
|
||||
```typescript
|
||||
// Record all requests
|
||||
await proxyServer.recordExpectations();
|
||||
|
||||
// Record requests with matching criteria
|
||||
await proxyServer.recordExpectations({
|
||||
method: 'POST',
|
||||
path: '/api/workflows',
|
||||
queryStringParameters: {
|
||||
'userId': ['123']
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Recorded expectations are saved as JSON files in the `expectations/` directory with unique names based on the request details. When the ProxyServer fixture initializes, all saved expectations are automatically loaded and mocked for subsequent test runs.
|
||||
|
||||
## Writing Tests
|
||||
For guidelines on writing new tests, see [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"httpRequest": {
|
||||
"method": "GET",
|
||||
"path": "/mock-endpoint"
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"headers": {
|
||||
"Content-Type": ["application/json"]
|
||||
},
|
||||
"body": {
|
||||
"userId": 1,
|
||||
"id": 1,
|
||||
"title": "delectus aut autem",
|
||||
"completed": false
|
||||
}
|
||||
},
|
||||
"id": "511d9c87-9ee3-4af8-8729-ca13b0a70e89",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"remainingTimes": 1
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { ContainerTestHelpers } from 'n8n-containers/n8n-test-container-helpers'
|
||||
import { setupDefaultInterceptors } from '../config/intercepts';
|
||||
import { n8nPage } from '../pages/n8nPage';
|
||||
import { ApiHelpers } from '../services/api-helper';
|
||||
import { ProxyServer } from '../services/proxy-server';
|
||||
import { TestError, type TestRequirements } from '../Types';
|
||||
import { setupTestRequirements } from '../utils/requirements';
|
||||
|
||||
@@ -14,6 +15,7 @@ type TestFixtures = {
|
||||
api: ApiHelpers;
|
||||
baseURL: string;
|
||||
setupRequirements: (requirements: TestRequirements) => Promise<void>;
|
||||
proxyServer: ProxyServer;
|
||||
};
|
||||
|
||||
type WorkerFixtures = {
|
||||
@@ -31,6 +33,7 @@ interface ContainerConfig {
|
||||
workers: number;
|
||||
};
|
||||
env?: Record<string, string>;
|
||||
proxyServerEnabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,6 +161,31 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
||||
|
||||
await use(setupFunction);
|
||||
},
|
||||
|
||||
proxyServer: async ({ n8nContainer }, use) => {
|
||||
// n8nContainer is "null" if running tests in "local" mode
|
||||
if (!n8nContainer) {
|
||||
throw new TestError(
|
||||
'Testing with Proxy server is not supported when using N8N_BASE_URL environment variable. Remove N8N_BASE_URL to use containerized testing.',
|
||||
);
|
||||
}
|
||||
|
||||
const proxyServerContainer = n8nContainer.containers.find((container) =>
|
||||
container.getName().endsWith('proxyserver'),
|
||||
);
|
||||
|
||||
// proxy server is not initialized in local mode (it be only supported in container modes)
|
||||
// tests that require proxy server should have "@capability:proxy" so that they are skipped in local mode
|
||||
if (!proxyServerContainer) {
|
||||
throw new TestError('Proxy server container not initialized. Cannot initialize client.');
|
||||
}
|
||||
|
||||
const serverUrl = `http://${proxyServerContainer?.getHost()}:${proxyServerContainer?.getFirstMappedPort()}`;
|
||||
const proxyServer = new ProxyServer(serverUrl);
|
||||
await proxyServer.loadExpectations();
|
||||
|
||||
await use(proxyServer);
|
||||
},
|
||||
});
|
||||
|
||||
export { expect };
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"n8n-workflow": "workspace:*",
|
||||
"nanoid": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"mockserver-client": "^5.15.0",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Locator } from '@playwright/test';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
import { ROUTES } from '../config/constants';
|
||||
import { resolveFromRoot } from '../utils/path-helper';
|
||||
|
||||
export class CanvasPage extends BasePage {
|
||||
@@ -494,7 +495,7 @@ export class CanvasPage extends BasePage {
|
||||
// Set fixed time using Playwright's clock API
|
||||
await this.page.clock.setFixedTime(timestamp);
|
||||
|
||||
await this.page.goto('/workflow/new');
|
||||
await this.openNewWorkflow();
|
||||
}
|
||||
|
||||
async addNodeWithSubItem(searchText: string, subItemText: string): Promise<void> {
|
||||
@@ -502,6 +503,10 @@ export class CanvasPage extends BasePage {
|
||||
await this.nodeCreatorSubItem(subItemText).click();
|
||||
}
|
||||
|
||||
async openNewWorkflow() {
|
||||
await this.page.goto(ROUTES.NEW_WORKFLOW_PAGE);
|
||||
}
|
||||
|
||||
getRagCalloutTip(): Locator {
|
||||
return this.page.getByText('Tip: Get a feel for vector stores in n8n with our');
|
||||
}
|
||||
|
||||
@@ -11,8 +11,11 @@ const CONTAINER_ONLY = new RegExp(`@capability:(${CONTAINER_ONLY_TAGS.join('|')}
|
||||
// In local run they are a "dependency" which means they will be skipped if earlier tests fail, not ideal but needed for isolation
|
||||
const SERIAL_EXECUTION = /@db:reset/;
|
||||
|
||||
// Tags that require proxy server
|
||||
const REQUIRES_PROXY_SERVER = /@capability:proxy/;
|
||||
|
||||
const CONTAINER_CONFIGS: Array<{ name: string; config: N8NConfig }> = [
|
||||
{ name: 'standard', config: {} },
|
||||
{ name: 'standard', config: { proxyServerEnabled: true } },
|
||||
{ name: 'postgres', config: { postgres: true } },
|
||||
{ name: 'queue', config: { queueMode: true } },
|
||||
{ name: 'multi-main', config: { queueMode: { mains: 2, workers: 1 } } },
|
||||
@@ -35,17 +38,23 @@ export function getProjects(): Project[] {
|
||||
name: 'ui:isolated',
|
||||
testDir: './tests/ui',
|
||||
grep: SERIAL_EXECUTION,
|
||||
grepInvert: REQUIRES_PROXY_SERVER,
|
||||
workers: 1,
|
||||
use: { baseURL: process.env.N8N_BASE_URL },
|
||||
},
|
||||
);
|
||||
} else {
|
||||
for (const { name, config } of CONTAINER_CONFIGS) {
|
||||
const grepInvertPatterns = [SERIAL_EXECUTION.source];
|
||||
if (!config.proxyServerEnabled) {
|
||||
grepInvertPatterns.push(REQUIRES_PROXY_SERVER.source);
|
||||
}
|
||||
|
||||
projects.push(
|
||||
{
|
||||
name: `${name}:ui`,
|
||||
testDir: './tests/ui',
|
||||
grepInvert: SERIAL_EXECUTION,
|
||||
grepInvert: new RegExp(grepInvertPatterns.join('|')),
|
||||
timeout: name === 'standard' ? 60000 : 180000, // 60 seconds for standard container test, 180 for containers to allow startup etc
|
||||
fullyParallel: true,
|
||||
use: { containerConfig: config },
|
||||
@@ -54,6 +63,7 @@ export function getProjects(): Project[] {
|
||||
name: `${name}:ui:isolated`,
|
||||
testDir: './tests/ui',
|
||||
grep: SERIAL_EXECUTION,
|
||||
grepInvert: !config.proxyServerEnabled ? REQUIRES_PROXY_SERVER : undefined,
|
||||
workers: 1,
|
||||
use: { containerConfig: config },
|
||||
},
|
||||
|
||||
239
packages/testing/playwright/services/proxy-server.ts
Normal file
239
packages/testing/playwright/services/proxy-server.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* ProxyServer service helper functions for Playwright tests
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
import { promises as fs } from 'fs';
|
||||
import type { Expectation, HttpRequest } from 'mockserver-client';
|
||||
import { mockServerClient as proxyServerClient } from 'mockserver-client';
|
||||
import type { MockServerClient, RequestResponse } from 'mockserver-client/mockServerClient';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface ProxyServerRequest {
|
||||
method: string;
|
||||
path: string;
|
||||
queryStringParameters?: Record<string, string[]>;
|
||||
headers?: Record<string, string[]>;
|
||||
body?: string | { type?: string; [key: string]: unknown };
|
||||
}
|
||||
|
||||
export interface ProxyServerResponse {
|
||||
statusCode: number;
|
||||
headers?: Record<string, string[]>;
|
||||
body?: string;
|
||||
delay?: {
|
||||
timeUnit: 'MICROSECONDS' | 'MILLISECONDS' | 'SECONDS' | 'MINUTES';
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProxyServerExpectation {
|
||||
httpRequest: ProxyServerRequest;
|
||||
httpResponse: ProxyServerResponse;
|
||||
times?: {
|
||||
remainingTimes?: number;
|
||||
unlimited?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RequestLog {
|
||||
method: string;
|
||||
path: string;
|
||||
headers: Record<string, string[]>;
|
||||
queryStringParameters?: Record<string, string[]>;
|
||||
body?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export class ProxyServer {
|
||||
private client: MockServerClient;
|
||||
url: string;
|
||||
private expectationsDir = './expectations';
|
||||
|
||||
/**
|
||||
* Create a ProxyServer client instance from a URL
|
||||
*/
|
||||
constructor(proxyServerUrl: string) {
|
||||
this.url = proxyServerUrl;
|
||||
const parsedURL = new URL(proxyServerUrl);
|
||||
this.client = proxyServerClient(parsedURL.hostname, parseInt(parsedURL.port, 10));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all expectations from the expectations directory and mock them
|
||||
*/
|
||||
async loadExpectations(): Promise<void> {
|
||||
try {
|
||||
const files = await fs.readdir(this.expectationsDir);
|
||||
const jsonFiles = files.filter((file) => file.endsWith('.json'));
|
||||
const expectations: Expectation[] = [];
|
||||
|
||||
for (const file of jsonFiles) {
|
||||
try {
|
||||
const filePath = join(this.expectationsDir, file);
|
||||
const fileContent = await fs.readFile(filePath, 'utf8');
|
||||
const expectation = JSON.parse(fileContent);
|
||||
expectations.push(expectation);
|
||||
} catch (parseError) {
|
||||
console.log(`Error parsing expectation from ${file}:`, parseError);
|
||||
}
|
||||
}
|
||||
|
||||
if (expectations.length > 0) {
|
||||
console.log('Loading expectations:', expectations.length);
|
||||
await this.client.mockAnyResponse(expectations);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error loading expectations:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an expectation in ProxyServer
|
||||
*/
|
||||
async createExpectation(expectation: ProxyServerExpectation): Promise<RequestResponse> {
|
||||
try {
|
||||
return await this.client.mockAnyResponse({
|
||||
httpRequest: expectation.httpRequest,
|
||||
httpResponse: expectation.httpResponse,
|
||||
times: expectation.times,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to create expectation: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that a request was received by ProxyServer
|
||||
*/
|
||||
async verifyRequest(request: ProxyServerRequest, numberOfRequests: number): Promise<boolean> {
|
||||
try {
|
||||
await this.client.verify(request, numberOfRequests);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all expectations and logs from ProxyServer
|
||||
*/
|
||||
async clearProxyServer(): Promise<void> {
|
||||
try {
|
||||
await this.client.clear(null, 'ALL');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to clear ProxyServer: ${JSON.stringify(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a simple GET request expectation with JSON response
|
||||
*/
|
||||
async createGetExpectation(
|
||||
path: string,
|
||||
responseBody: unknown,
|
||||
queryParams?: Record<string, string>,
|
||||
statusCode: number = 200,
|
||||
): Promise<RequestResponse> {
|
||||
const queryStringParameters = queryParams
|
||||
? Object.entries(queryParams).reduce<Record<string, string[]>>((acc, [key, value]) => {
|
||||
acc[key] = [value];
|
||||
return acc;
|
||||
}, {})
|
||||
: undefined;
|
||||
|
||||
return await this.createExpectation({
|
||||
httpRequest: {
|
||||
method: 'GET',
|
||||
path,
|
||||
...(queryStringParameters && { queryStringParameters }),
|
||||
},
|
||||
httpResponse: {
|
||||
statusCode,
|
||||
headers: {
|
||||
'Content-Type': ['application/json'],
|
||||
},
|
||||
body: JSON.stringify(responseBody),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a GET request was made to ProxyServer
|
||||
*/
|
||||
async wasGetRequestMade(
|
||||
path: string,
|
||||
queryParams?: Record<string, string>,
|
||||
numberOfRequests = 1,
|
||||
): Promise<boolean> {
|
||||
const queryStringParameters = queryParams
|
||||
? Object.entries(queryParams).reduce<Record<string, string[]>>((acc, [key, value]) => {
|
||||
acc[key] = [value];
|
||||
return acc;
|
||||
}, {})
|
||||
: undefined;
|
||||
|
||||
return await this.verifyRequest(
|
||||
{
|
||||
method: 'GET',
|
||||
path,
|
||||
...(queryStringParameters && { queryStringParameters }),
|
||||
},
|
||||
numberOfRequests,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve recorded expectations and write to files
|
||||
*/
|
||||
async recordExpectations(request?: HttpRequest): Promise<void> {
|
||||
try {
|
||||
// Retrieve recorded expectations from the mock server
|
||||
const recordedExpectations = await this.client.retrieveRecordedExpectations(request);
|
||||
|
||||
// Ensure expectations directory exists
|
||||
await fs.mkdir(this.expectationsDir, { recursive: true });
|
||||
|
||||
for (const expectation of recordedExpectations) {
|
||||
if (
|
||||
!expectation.httpRequest ||
|
||||
!(
|
||||
'method' in expectation.httpRequest &&
|
||||
typeof expectation.httpRequest.method === 'string' &&
|
||||
typeof expectation.httpRequest.path === 'string'
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate unique filename based on request details
|
||||
const requestData = {
|
||||
method: expectation.httpRequest?.method,
|
||||
path: expectation.httpRequest?.path,
|
||||
queryStringParameters: expectation.httpRequest?.queryStringParameters,
|
||||
headers: expectation.httpRequest?.headers,
|
||||
};
|
||||
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.update(JSON.stringify(requestData))
|
||||
.digest('hex')
|
||||
.substring(0, 8);
|
||||
|
||||
const filename = `${expectation.httpRequest?.method?.toString()}-${expectation.httpRequest?.path?.replace(/[^a-zA-Z0-9]/g, '_')}-${hash}.json`;
|
||||
const filePath = join(this.expectationsDir, filename);
|
||||
|
||||
// Write expectation to JSON file
|
||||
await fs.writeFile(filePath, JSON.stringify(expectation, null, 2));
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to record expectations: ${JSON.stringify(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getActiveExpectations() {
|
||||
return await this.client.retrieveActiveExpectations({ method: 'GET' });
|
||||
}
|
||||
}
|
||||
67
packages/testing/playwright/tests/ui/env-mock-server.spec.ts
Normal file
67
packages/testing/playwright/tests/ui/env-mock-server.spec.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
|
||||
// @capability:proxy tag ensures that test suite is only run when proxy is available
|
||||
test.describe('Proxy server @capability:proxy', () => {
|
||||
test('should verify ProxyServer container is running', async ({ proxyServer }) => {
|
||||
const mockResponse = await proxyServer.createGetExpectation('/health', {
|
||||
status: 'healthy',
|
||||
});
|
||||
|
||||
assert(typeof mockResponse !== 'string');
|
||||
expect(mockResponse.statusCode).toBe(201);
|
||||
|
||||
expect(await proxyServer.wasGetRequestMade('/health')).toBe(false);
|
||||
|
||||
// Verify the mock endpoint works
|
||||
const healthResponse = await fetch(`${proxyServer.url}/health`);
|
||||
expect(healthResponse.ok).toBe(true);
|
||||
const healthData = await healthResponse.json();
|
||||
expect(healthData.status).toBe('healthy');
|
||||
|
||||
expect(await proxyServer.wasGetRequestMade('/health')).toBe(true);
|
||||
});
|
||||
|
||||
test('should run a simple workflow calling http endpoint', async ({ n8n, proxyServer }) => {
|
||||
const mockResponse = { data: 'Hello from ProxyServer!', test: '1' };
|
||||
|
||||
// Create expectation in mockserver to handle the request
|
||||
await proxyServer.createGetExpectation('/data', mockResponse, { test: '1' });
|
||||
|
||||
await n8n.canvas.openNewWorkflow();
|
||||
|
||||
// This is calling a random endpoint http://mock-api.com
|
||||
await n8n.canvas.importWorkflow('Simple_workflow_with_http_node.json', 'Test');
|
||||
|
||||
// Execute workflow - this should now proxy through mockserver
|
||||
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful');
|
||||
await n8n.canvas.openNode('HTTP Request');
|
||||
await expect(n8n.ndv.getOutputTbodyCell(0, 0)).toContainText('Hello from ProxyServer!');
|
||||
|
||||
// Verify the request was handled by mockserver
|
||||
expect(
|
||||
await proxyServer.wasGetRequestMade('/data', {
|
||||
test: '1',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('should use stored expectations respond to api request', async ({ proxyServer }) => {
|
||||
const response = await fetch(`${proxyServer.url}/mock-endpoint`);
|
||||
expect(response.ok).toBe(true);
|
||||
const data = await response.json();
|
||||
expect(data.title).toBe('delectus aut autem');
|
||||
expect(await proxyServer.wasGetRequestMade('/mock-endpoint')).toBe(true);
|
||||
});
|
||||
|
||||
test('should run a simple workflow proxying HTTPS request', async ({ n8n }) => {
|
||||
await n8n.canvas.openNewWorkflow();
|
||||
await n8n.canvas.importWorkflow('Simple_workflow_with_http_node.json', 'Test');
|
||||
|
||||
await n8n.canvas.openNode('HTTP Request');
|
||||
await n8n.ndv.setParameterInput('url', 'https://jsonplaceholder.typicode.com/todos/1');
|
||||
await n8n.ndv.execute();
|
||||
await expect(n8n.ndv.getOutputTbodyCell(0, 0)).toContainText('1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-688, 272],
|
||||
"id": "2edde9d2-09a3-46dd-8d56-19a0c92654fc",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "http://mock-api.com/data",
|
||||
"sendQuery": true,
|
||||
"queryParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "test",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"allowUnauthorizedCerts": true
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [-480, 272],
|
||||
"id": "99de0e7f-20d1-441e-8e8b-2f3cabcacb0c",
|
||||
"name": "HTTP Request"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTTP Request",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {},
|
||||
"meta": {
|
||||
"instanceId": "cb484ba7b742928a2048bf8829668bed5b5ad9787579adea888f05980292a4a7"
|
||||
}
|
||||
}
|
||||
885
pnpm-lock.yaml
generated
885
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user