mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat: Add testcontainers and Playwright (no-changelog) (#16662)
Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
This commit is contained in:
238
packages/testing/playwright/services/api-helper.ts
Normal file
238
packages/testing/playwright/services/api-helper.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
// services/api-helper.ts
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
|
||||
import type { UserCredentials } from '../config/test-users';
|
||||
import {
|
||||
INSTANCE_OWNER_CREDENTIALS,
|
||||
INSTANCE_MEMBER_CREDENTIALS,
|
||||
INSTANCE_ADMIN_CREDENTIALS,
|
||||
} from '../config/test-users';
|
||||
import { TestError } from '../Types';
|
||||
|
||||
export interface LoginResponseData {
|
||||
id: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type UserRole = 'owner' | 'admin' | 'member';
|
||||
export type TestState = 'fresh' | 'reset' | 'signin-only';
|
||||
|
||||
const AUTH_TAGS = {
|
||||
ADMIN: '@auth:admin',
|
||||
OWNER: '@auth:owner',
|
||||
MEMBER: '@auth:member',
|
||||
NONE: '@auth:none',
|
||||
} as const;
|
||||
|
||||
const DB_TAGS = {
|
||||
RESET: '@db:reset',
|
||||
} as const;
|
||||
|
||||
export class ApiHelpers {
|
||||
private request: APIRequestContext;
|
||||
|
||||
constructor(requestContext: APIRequestContext) {
|
||||
this.request = requestContext;
|
||||
}
|
||||
|
||||
// ===== MAIN SETUP METHODS =====
|
||||
|
||||
/**
|
||||
* Setup test environment based on test tags (recommended approach)
|
||||
* @param tags - Array of test tags (e.g., ['@db:reset', '@auth:owner'])
|
||||
* @param memberIndex - Which member to use (if auth role is 'member')
|
||||
*
|
||||
* Examples:
|
||||
* - ['@db:reset'] = reset DB, manual signin required
|
||||
* - ['@db:reset', '@auth:owner'] = reset DB + signin as owner
|
||||
* - ['@auth:admin'] = signin as admin (no reset)
|
||||
*/
|
||||
async setupFromTags(tags: string[], memberIndex: number = 0): Promise<LoginResponseData | null> {
|
||||
const shouldReset = this.shouldResetDatabase(tags);
|
||||
const role = this.getRoleFromTags(tags);
|
||||
|
||||
if (shouldReset && role) {
|
||||
// Reset + signin
|
||||
await this.resetDatabase();
|
||||
return await this.signin(role, memberIndex);
|
||||
} else if (shouldReset) {
|
||||
// Reset only, manual signin required
|
||||
await this.resetDatabase();
|
||||
return null;
|
||||
} else if (role) {
|
||||
// Signin only
|
||||
return await this.signin(role, memberIndex);
|
||||
}
|
||||
|
||||
// No setup required
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup test environment based on desired state (programmatic approach)
|
||||
* @param state - 'fresh': new container, 'reset': reset DB + signin, 'signin-only': just signin
|
||||
* @param role - User role to sign in as
|
||||
* @param memberIndex - Which member to use (if role is 'member')
|
||||
*/
|
||||
async setupTest(
|
||||
state: TestState,
|
||||
role: UserRole = 'owner',
|
||||
memberIndex: number = 0,
|
||||
): Promise<LoginResponseData | null> {
|
||||
switch (state) {
|
||||
case 'fresh':
|
||||
// For fresh docker container - just reset, no signin needed yet
|
||||
await this.resetDatabase();
|
||||
return null;
|
||||
|
||||
case 'reset':
|
||||
// Reset database then sign in
|
||||
await this.resetDatabase();
|
||||
return await this.signin(role, memberIndex);
|
||||
|
||||
case 'signin-only':
|
||||
// Just sign in without reset
|
||||
return await this.signin(role, memberIndex);
|
||||
|
||||
default:
|
||||
throw new TestError('Unknown test state');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CORE METHODS =====
|
||||
|
||||
async resetDatabase(): Promise<void> {
|
||||
const response = await this.request.post('/rest/e2e/reset', {
|
||||
data: {
|
||||
owner: INSTANCE_OWNER_CREDENTIALS,
|
||||
members: INSTANCE_MEMBER_CREDENTIALS,
|
||||
admin: INSTANCE_ADMIN_CREDENTIALS,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
const errorText = await response.text();
|
||||
throw new TestError(errorText);
|
||||
}
|
||||
// Adding small delay to ensure database is reset
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
async signin(role: UserRole, memberIndex: number = 0): Promise<LoginResponseData> {
|
||||
const credentials = this.getCredentials(role, memberIndex);
|
||||
return await this.loginAndSetCookies(credentials);
|
||||
}
|
||||
|
||||
// ===== CONFIGURATION METHODS =====
|
||||
|
||||
async setFeature(feature: string, enabled: boolean): Promise<void> {
|
||||
await this.request.patch('/rest/e2e/feature', {
|
||||
data: { feature: `feat:${feature}`, enabled },
|
||||
});
|
||||
}
|
||||
|
||||
async setQuota(quotaName: string, value: number | string): Promise<void> {
|
||||
await this.request.patch('/rest/e2e/quota', {
|
||||
data: { feature: `quota:${quotaName}`, value },
|
||||
});
|
||||
}
|
||||
|
||||
async setQueueMode(enabled: boolean): Promise<void> {
|
||||
await this.request.patch('/rest/e2e/queue-mode', {
|
||||
data: { enabled },
|
||||
});
|
||||
}
|
||||
|
||||
// ===== CONVENIENCE METHODS =====
|
||||
|
||||
async enableFeature(feature: string): Promise<void> {
|
||||
await this.setFeature(feature, true);
|
||||
}
|
||||
|
||||
async disableFeature(feature: string): Promise<void> {
|
||||
await this.setFeature(feature, false);
|
||||
}
|
||||
|
||||
async setMaxTeamProjectsQuota(value: number | string): Promise<void> {
|
||||
await this.setQuota('maxTeamProjects', value);
|
||||
}
|
||||
|
||||
async get(path: string, params?: URLSearchParams) {
|
||||
const response = await this.request.get(path, { params });
|
||||
|
||||
const { data } = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
// ===== PRIVATE METHODS =====
|
||||
|
||||
private async loginAndSetCookies(
|
||||
credentials: Pick<UserCredentials, 'email' | 'password'>,
|
||||
): Promise<LoginResponseData> {
|
||||
const response = await this.request.post('/rest/login', {
|
||||
data: {
|
||||
emailOrLdapLoginId: credentials.email,
|
||||
password: credentials.password,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
const errorText = await response.text();
|
||||
throw new TestError(errorText);
|
||||
}
|
||||
|
||||
let responseData: any;
|
||||
try {
|
||||
responseData = await response.json();
|
||||
} catch (error) {
|
||||
const errorText = await response.text();
|
||||
throw new TestError(errorText);
|
||||
}
|
||||
|
||||
const loginData: LoginResponseData = responseData.data;
|
||||
|
||||
if (!loginData?.id) {
|
||||
throw new TestError('Login did not return expected user data (missing user ID)');
|
||||
}
|
||||
|
||||
return loginData;
|
||||
}
|
||||
|
||||
private getCredentials(role: UserRole, memberIndex: number = 0): UserCredentials {
|
||||
switch (role) {
|
||||
case 'owner':
|
||||
return INSTANCE_OWNER_CREDENTIALS;
|
||||
case 'admin':
|
||||
return INSTANCE_ADMIN_CREDENTIALS;
|
||||
case 'member':
|
||||
if (!INSTANCE_MEMBER_CREDENTIALS || memberIndex >= INSTANCE_MEMBER_CREDENTIALS.length) {
|
||||
throw new TestError(`No member credentials found for index ${memberIndex}`);
|
||||
}
|
||||
return INSTANCE_MEMBER_CREDENTIALS[memberIndex];
|
||||
default:
|
||||
throw new TestError(`Unknown role: ${role as string}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== TAG PARSING METHODS =====
|
||||
|
||||
private shouldResetDatabase(tags: string[]): boolean {
|
||||
const lowerTags = tags.map((tag) => tag.toLowerCase());
|
||||
return lowerTags.includes(DB_TAGS.RESET.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role from the tags
|
||||
* @param tags - Array of test tags (e.g., ['@db:reset', '@auth:owner'])
|
||||
* @returns The role from the tags, or 'owner' if no role is found
|
||||
*/
|
||||
getRoleFromTags(tags: string[]): UserRole | null {
|
||||
const lowerTags = tags.map((tag) => tag.toLowerCase());
|
||||
|
||||
if (lowerTags.includes(AUTH_TAGS.ADMIN.toLowerCase())) return 'admin';
|
||||
if (lowerTags.includes(AUTH_TAGS.OWNER.toLowerCase())) return 'owner';
|
||||
if (lowerTags.includes(AUTH_TAGS.MEMBER.toLowerCase())) return 'member';
|
||||
if (lowerTags.includes(AUTH_TAGS.NONE.toLowerCase())) return null;
|
||||
return 'owner';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user