test: Add credential helper and ndv helper (#18636)

This commit is contained in:
shortstacked
2025-08-21 17:40:30 +01:00
committed by GitHub
parent 944960bf43
commit 8ca75d6f51
10 changed files with 747 additions and 7 deletions

View File

@@ -0,0 +1,106 @@
import type { NodeDetailsViewPage } from '../pages/NodeDetailsViewPage';
/**
* Helper class for setting node parameters in the NDV
*/
export class NodeParameterHelper {
constructor(private ndv: NodeDetailsViewPage) {}
/**
* Detects parameter type by checking DOM structure
* Supports dropdown, text, and switch parameters
* @param parameterName - The parameter name to check
* @returns The detected parameter type
*/
async detectParameterType(parameterName: string): Promise<'dropdown' | 'text' | 'switch'> {
const parameterContainer = this.ndv.getParameterInput(parameterName);
const [hasSwitch, hasSelect, hasSelectCaret] = await Promise.all([
parameterContainer
.locator('.el-switch')
.count()
.then((count) => count > 0),
parameterContainer
.locator('.el-select')
.count()
.then((count) => count > 0),
parameterContainer
.locator('.el-select__caret')
.count()
.then((count) => count > 0),
]);
if (hasSwitch) return 'switch';
if (hasSelect && hasSelectCaret) return 'dropdown';
return 'text';
}
/**
* Sets a parameter value with automatic type detection or explicit type
* Supports dropdown, text, and switch parameters
* @param parameterName - Name of the parameter to set
* @param value - Value to set (string or boolean)
* @param type - Optional explicit type to skip detection for better performance
*/
async setParameter(
parameterName: string,
value: string | boolean,
type?: 'dropdown' | 'text' | 'switch',
): Promise<void> {
if (typeof value === 'boolean') {
await this.ndv.setParameterSwitch(parameterName, value);
return;
}
const parameterType = type ?? (await this.detectParameterType(parameterName));
switch (parameterType) {
case 'dropdown':
await this.ndv.setParameterDropdown(parameterName, value);
break;
case 'text':
await this.ndv.setParameterInput(parameterName, value);
break;
case 'switch':
await this.ndv.setParameterSwitch(parameterName, value === 'true');
break;
}
}
async webhook(config: {
httpMethod?: string;
path?: string;
authentication?: string;
responseMode?: string;
}): Promise<void> {
if (config.httpMethod !== undefined)
await this.setParameter('httpMethod', config.httpMethod, 'dropdown');
if (config.path !== undefined) await this.setParameter('path', config.path, 'text');
if (config.authentication !== undefined)
await this.setParameter('authentication', config.authentication, 'dropdown');
if (config.responseMode !== undefined)
await this.setParameter('responseMode', config.responseMode, 'dropdown');
}
/**
* Simplified HTTP Request node parameter configuration
* @param config - Configuration object with parameter values
*/
async httpRequest(config: {
method?: string;
url?: string;
authentication?: string;
sendQuery?: boolean;
sendHeaders?: boolean;
sendBody?: boolean;
}): Promise<void> {
if (config.method !== undefined) await this.setParameter('method', config.method, 'dropdown');
if (config.url !== undefined) await this.setParameter('url', config.url, 'text');
if (config.authentication !== undefined)
await this.setParameter('authentication', config.authentication, 'dropdown');
if (config.sendQuery !== undefined)
await this.setParameter('sendQuery', config.sendQuery, 'switch');
if (config.sendHeaders !== undefined)
await this.setParameter('sendHeaders', config.sendHeaders, 'switch');
if (config.sendBody !== undefined)
await this.setParameter('sendBody', config.sendBody, 'switch');
}
}

View File

@@ -77,6 +77,18 @@ export class CanvasPage extends BasePage {
await this.nodeCreatorSubItem(subItemText).click();
}
async addActionNode(searchText: string, subItemText: string): Promise<void> {
await this.addNode(searchText);
await this.page.getByText('Actions').click();
await this.nodeCreatorSubItem(subItemText).click();
}
async addTriggerNode(searchText: string, subItemText: string): Promise<void> {
await this.addNode(searchText);
await this.page.getByText('Triggers').click();
await this.nodeCreatorSubItem(subItemText).click();
}
async deleteNodeByName(nodeName: string): Promise<void> {
await this.nodeDeleteButton(nodeName).click();
}

View File

@@ -1,6 +1,17 @@
import { BasePage } from './BasePage';
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { BasePage } from './BasePage';
import { NodeParameterHelper } from '../helpers/NodeParameterHelper';
export class NodeDetailsViewPage extends BasePage {
readonly setupHelper: NodeParameterHelper;
constructor(page: Page) {
super(page);
this.setupHelper = new NodeParameterHelper(this);
}
export class NodeDisplayViewPage extends BasePage {
async clickBackToCanvasButton() {
await this.clickByTestId('back-to-canvas');
}
@@ -187,16 +198,82 @@ export class NodeDisplayViewPage extends BasePage {
}
/**
* Select option in parameter dropdown
* Get parameter input field
* @param parameterName - The name of the parameter
*/
getParameterInputField(parameterName: string) {
return this.getParameterInput(parameterName).getByTestId('parameter-input-field');
}
/**
* Select option in parameter dropdown (improved with Playwright best practices)
* @param parameterName - The parameter name
* @param optionText - The text of the option to select
*/
async selectOptionInParameterDropdown(parameterName: string, optionText: string) {
const dropdown = this.getParameterInput(parameterName);
await dropdown.click();
// Wait for dropdown to be visible and select option - following Playwright best practices
await this.page.getByRole('option', { name: optionText }).click();
}
/**
* Click parameter dropdown by name (test-id based selector)
* @param parameterName - The parameter name e.g 'httpMethod', 'authentication'
*/
async clickParameterDropdown(parameterName: string): Promise<void> {
await this.clickByTestId(`parameter-input-${parameterName}`);
}
/**
* Select option from visible dropdown using Playwright role-based selectors
* This follows the pattern used in working n8n tests
* @param optionText - The text of the option to select
*/
async selectFromVisibleDropdown(optionText: string): Promise<void> {
// Use Playwright's role-based selector - this is more reliable than CSS selectors
await this.page.getByRole('option', { name: optionText }).click();
}
/**
* Fill parameter input field by parameter name
* @param parameterName - The parameter name e.g 'path', 'url'
* @param value - The value to fill
*/
async fillParameterInputByName(parameterName: string, value: string): Promise<void> {
const input = this.getParameterInputField(parameterName);
await input.click();
await input.fill(value);
}
/**
* Click parameter options expansion (e.g. for Response Code)
*/
async clickParameterOptions(): Promise<void> {
await this.page.locator('.param-options').click();
}
/**
* Get visible Element UI popper (dropdown/popover)
* Ported from Cypress pattern with Playwright selectors
*/
getVisiblePopper() {
return this.page
.locator('.el-popper')
.filter({ hasNot: this.page.locator('[aria-hidden="true"]') });
}
/**
* Wait for parameter dropdown to be visible and ready for interaction
* @param parameterName - The parameter name
*/
async waitForParameterDropdown(parameterName: string): Promise<void> {
const dropdown = this.getParameterInput(parameterName);
await dropdown.waitFor({ state: 'visible' });
await expect(dropdown).toBeEnabled();
}
/**
* Click on a floating node in the NDV (for switching between connected nodes)
* @param nodeName - The name of the node to click
@@ -279,4 +356,91 @@ export class NodeDisplayViewPage extends BasePage {
getErrorMessageText(message: string) {
return this.page.locator(`text=${message}`);
}
async setParameterDropdown(parameterName: string, optionText: string): Promise<void> {
await this.getParameterInput(parameterName).click();
await this.page.getByRole('option', { name: optionText }).click();
}
async setParameterInput(parameterName: string, value: string): Promise<void> {
await this.fillParameterInputByName(parameterName, value);
}
async setParameterSwitch(parameterName: string, enabled: boolean): Promise<void> {
const switchElement = this.getParameterInput(parameterName).locator('.el-switch');
const isCurrentlyEnabled = (await switchElement.getAttribute('aria-checked')) === 'true';
if (isCurrentlyEnabled !== enabled) {
await switchElement.click();
}
}
async setMultipleParameters(
parameters: Record<string, string | number | boolean>,
): Promise<void> {
for (const [parameterName, value] of Object.entries(parameters)) {
if (typeof value === 'string') {
const parameterType = await this.setupHelper.detectParameterType(parameterName);
if (parameterType === 'dropdown') {
await this.setParameterDropdown(parameterName, value);
} else {
await this.setParameterInput(parameterName, value);
}
} else if (typeof value === 'boolean') {
await this.setParameterSwitch(parameterName, value);
} else if (typeof value === 'number') {
await this.setParameterInput(parameterName, value.toString());
}
}
}
async getParameterValue(parameterName: string): Promise<string> {
const parameterType = await this.setupHelper.detectParameterType(parameterName);
switch (parameterType) {
case 'text':
return await this.getTextParameterValue(parameterName);
case 'dropdown':
return await this.getDropdownParameterValue(parameterName);
case 'switch':
return await this.getSwitchParameterValue(parameterName);
default:
// Fallback for unknown types
return (await this.getParameterInput(parameterName).textContent()) ?? '';
}
}
/**
* Get value from a text parameter - simplified approach
*/
private async getTextParameterValue(parameterName: string): Promise<string> {
const parameterContainer = this.getParameterInput(parameterName);
const input = parameterContainer.locator('input').first();
return await input.inputValue();
}
/**
* Get value from a dropdown parameter
*/
private async getDropdownParameterValue(parameterName: string): Promise<string> {
const selectedOption = this.getParameterInput(parameterName).locator('.el-select__tags-text');
return (await selectedOption.textContent()) ?? '';
}
/**
* Get value from a switch parameter
*/
private async getSwitchParameterValue(parameterName: string): Promise<string> {
const switchElement = this.getParameterInput(parameterName).locator('.el-switch');
const isEnabled = (await switchElement.getAttribute('aria-checked')) === 'true';
return isEnabled ? 'true' : 'false';
}
async validateParameter(parameterName: string, expectedValue: string): Promise<void> {
const actualValue = await this.getParameterValue(parameterName);
if (actualValue !== expectedValue) {
throw new Error(
`Parameter ${parameterName} has value "${actualValue}", expected "${expectedValue}"`,
);
}
}
}

View File

@@ -6,7 +6,7 @@ import { CanvasPage } from './CanvasPage';
import { CredentialsPage } from './CredentialsPage';
import { ExecutionsPage } from './ExecutionsPage';
import { IframePage } from './IframePage';
import { NodeDisplayViewPage } from './NodeDisplayViewPage';
import { NodeDetailsViewPage } from './NodeDetailsViewPage';
import { NotificationsPage } from './NotificationsPage';
import { NpsSurveyPage } from './NpsSurveyPage';
import { ProjectSettingsPage } from './ProjectSettingsPage';
@@ -34,7 +34,7 @@ export class n8nPage {
readonly canvas: CanvasPage;
readonly iframe: IframePage;
readonly ndv: NodeDisplayViewPage;
readonly ndv: NodeDetailsViewPage;
readonly npsSurvey: NpsSurveyPage;
readonly projectSettings: ProjectSettingsPage;
readonly settings: SettingsPage;
@@ -66,7 +66,7 @@ export class n8nPage {
this.canvas = new CanvasPage(page);
this.iframe = new IframePage(page);
this.ndv = new NodeDisplayViewPage(page);
this.ndv = new NodeDetailsViewPage(page);
this.npsSurvey = new NpsSurveyPage(page);
this.projectSettings = new ProjectSettingsPage(page);
this.settings = new SettingsPage(page);

View File

@@ -9,6 +9,7 @@ import {
INSTANCE_ADMIN_CREDENTIALS,
} from '../config/test-users';
import { TestError } from '../Types';
import { CredentialApiHelper } from './credential-api-helper';
import { ProjectApiHelper } from './project-api-helper';
import { WorkflowApiHelper } from './workflow-api-helper';
@@ -35,11 +36,13 @@ export class ApiHelpers {
request: APIRequestContext;
workflowApi: WorkflowApiHelper;
projectApi: ProjectApiHelper;
credentialApi: CredentialApiHelper;
constructor(requestContext: APIRequestContext) {
this.request = requestContext;
this.workflowApi = new WorkflowApiHelper(this);
this.projectApi = new ProjectApiHelper(this);
this.credentialApi = new CredentialApiHelper(this);
}
// ===== MAIN SETUP METHODS =====

View File

@@ -0,0 +1,195 @@
import type {
CreateCredentialDto,
CredentialsGetManyRequestQuery,
CredentialsGetOneRequestQuery,
} from '@n8n/api-types';
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import { nanoid } from 'nanoid';
import type { ApiHelpers } from './api-helper';
import { TestError } from '../Types';
interface CredentialResponse {
id: string;
name: string;
type: string;
data?: ICredentialDataDecryptedObject;
scopes?: string[];
shared?: Array<{
id: string;
projectId: string;
role: string;
}>;
createdAt: string;
updatedAt: string;
}
type CredentialImportResult = {
credentialId: string;
createdCredential: CredentialResponse;
};
export class CredentialApiHelper {
constructor(private api: ApiHelpers) {}
/**
* Create a new credential
*/
async createCredential(credential: CreateCredentialDto): Promise<CredentialResponse> {
const response = await this.api.request.post('/rest/credentials', { data: credential });
if (!response.ok()) {
throw new TestError(`Failed to create credential: ${await response.text()}`);
}
const result = await response.json();
return result.data ?? result;
}
/**
* Get all credentials with optional query parameters
*/
async getCredentials(query?: CredentialsGetManyRequestQuery): Promise<CredentialResponse[]> {
const params = new URLSearchParams();
if (query?.includeScopes) params.set('includeScopes', String(query.includeScopes));
if (query?.includeData) params.set('includeData', String(query.includeData));
if (query?.onlySharedWithMe) params.set('onlySharedWithMe', String(query.onlySharedWithMe));
const response = await this.api.request.get('/rest/credentials', { params });
if (!response.ok()) {
throw new TestError(`Failed to get credentials: ${await response.text()}`);
}
const result = await response.json();
return Array.isArray(result) ? result : (result.data ?? []);
}
/**
* Get a specific credential by ID
*/
async getCredential(
credentialId: string,
query?: CredentialsGetOneRequestQuery,
): Promise<CredentialResponse> {
const params = new URLSearchParams();
if (query?.includeData) params.set('includeData', String(query.includeData));
const response = await this.api.request.get(`/rest/credentials/${credentialId}`, { params });
if (!response.ok()) {
throw new TestError(`Failed to get credential: ${await response.text()}`);
}
const result = await response.json();
return result.data ?? result;
}
/**
* Update an existing credential
*/
async updateCredential(
credentialId: string,
updates: Partial<CreateCredentialDto>,
): Promise<CredentialResponse> {
const existingCredential = await this.getCredential(credentialId);
const updateData = {
name: existingCredential.name,
type: existingCredential.type,
...updates,
};
const response = await this.api.request.patch(`/rest/credentials/${credentialId}`, {
data: updateData,
});
if (!response.ok()) {
throw new TestError(`Failed to update credential: ${await response.text()}`);
}
const result = await response.json();
return result.data ?? result;
}
/**
* Delete a credential
*/
async deleteCredential(credentialId: string): Promise<boolean> {
const response = await this.api.request.delete(`/rest/credentials/${credentialId}`);
if (!response.ok()) {
throw new TestError(`Failed to delete credential: ${await response.text()}`);
}
return true;
}
/**
* Get credentials available for a specific workflow or project
*/
async getCredentialsForWorkflow(options: {
workflowId?: string;
projectId?: string;
}): Promise<CredentialResponse[]> {
const params = new URLSearchParams();
if (options.workflowId) params.set('workflowId', options.workflowId);
if (options.projectId) params.set('projectId', options.projectId);
const response = await this.api.request.get('/rest/credentials/for-workflow', { params });
if (!response.ok()) {
throw new TestError(`Failed to get credentials for workflow: ${await response.text()}`);
}
const result = await response.json();
return Array.isArray(result) ? result : (result.data ?? []);
}
/**
* Transfer a credential to another project
*/
async transferCredential(credentialId: string, destinationProjectId: string): Promise<void> {
const response = await this.api.request.put(`/rest/credentials/${credentialId}/transfer`, {
data: { destinationProjectId },
});
if (!response.ok()) {
throw new TestError(`Failed to transfer credential: ${await response.text()}`);
}
}
/**
* Make credential unique by adding a unique suffix to avoid naming conflicts in tests.
*/
private makeCredentialUnique(
credential: CreateCredentialDto,
options?: { idLength?: number },
): CreateCredentialDto {
const idLength = options?.idLength ?? 8;
const uniqueSuffix = nanoid(idLength);
return {
...credential,
name: `${credential.name} (Test ${uniqueSuffix})`,
};
}
/**
* Create a credential from definition with automatic unique naming for testing.
* Returns detailed information about what was created.
*/
async createCredentialFromDefinition(
credential: CreateCredentialDto,
options?: { idLength?: number },
): Promise<CredentialImportResult> {
const uniqueCredential = this.makeCredentialUnique(credential, options);
const createdCredential = await this.createCredential(uniqueCredential);
const credentialId = createdCredential.id;
return {
credentialId,
createdCredential,
};
}
}

View File

@@ -27,4 +27,19 @@ export class ProjectApiHelper {
const result = await response.json();
return result.data ?? result;
}
/**
* Delete a project
* @param projectId The ID of the project to delete
* @returns True if deletion was successful
*/
async deleteProject(projectId: string): Promise<boolean> {
const response = await this.api.request.delete(`/rest/projects/${projectId}`);
if (!response.ok()) {
throw new TestError(`Failed to delete project: ${await response.text()}`);
}
return true;
}
}

View File

@@ -1,6 +1,6 @@
import { test, expect } from '../../../fixtures/base';
test.describe('Core UI Patterns - Building Blocks', () => {
test.describe('01 - UI Test Entry Points', () => {
test.describe('Entry Point: Home Page', () => {
test('should navigate from home', async ({ n8n }) => {
await n8n.start.fromHome();

View File

@@ -0,0 +1,50 @@
import { test, expect } from '../../../fixtures/base';
test.describe('03 - Node Details Configuration', () => {
test.beforeEach(async ({ n8n }) => {
await n8n.start.fromBlankCanvas();
});
test('should configure webhook node', async ({ n8n }) => {
await n8n.canvas.addNode('Webhook');
await n8n.ndv.setupHelper.webhook({
httpMethod: 'POST',
path: 'test-webhook',
authentication: 'Basic Auth',
});
await expect(n8n.ndv.getParameterInputField('path')).toHaveValue('test-webhook');
});
test('should configure HTTP Request node', async ({ n8n }) => {
await n8n.canvas.addNode('HTTP Request');
await n8n.ndv.setupHelper.httpRequest({
method: 'POST',
url: 'https://api.example.com/test',
sendQuery: true,
sendHeaders: false,
});
await expect(n8n.ndv.getParameterInputField('url')).toHaveValue('https://api.example.com/test');
});
test('should auto-detect parameter types', async ({ n8n }) => {
await n8n.canvas.addNode('Webhook');
await n8n.ndv.setupHelper.setParameter('httpMethod', 'PUT');
await n8n.ndv.setupHelper.setParameter('path', 'auto-detect-test');
await expect(n8n.ndv.getParameterInputField('path')).toHaveValue('auto-detect-test');
});
test('should use explicit types for better performance', async ({ n8n }) => {
await n8n.canvas.addNode('Webhook');
await n8n.ndv.setupHelper.setParameter('httpMethod', 'PATCH', 'dropdown');
await n8n.ndv.setupHelper.setParameter('path', 'explicit-types', 'text');
await expect(n8n.ndv.getParameterInputField('path')).toHaveValue('explicit-types');
});
});

View File

@@ -0,0 +1,195 @@
import type { CreateCredentialDto } from '@n8n/api-types';
import { test, expect } from '../../fixtures/base';
test.describe('Credential API Operations', () => {
test.describe('Basic CRUD Operations', () => {
test('should create, retrieve, update, and delete credential', async ({ api }) => {
const credentialData: CreateCredentialDto = {
name: 'Test HTTP Basic Auth',
type: 'httpBasicAuth',
data: {
user: 'test_user',
password: 'test_password',
},
};
const { credentialId, createdCredential } =
await api.credentialApi.createCredentialFromDefinition(credentialData);
expect(credentialId).toBeTruthy();
expect(createdCredential.type).toBe('httpBasicAuth');
expect(createdCredential.name).toContain('Test HTTP Basic Auth (Test');
const retrievedCredential = await api.credentialApi.getCredential(credentialId);
expect(retrievedCredential.id).toBe(credentialId);
expect(retrievedCredential.type).toBe('httpBasicAuth');
expect(retrievedCredential.name).toBe(createdCredential.name);
const credentialWithData = await api.credentialApi.getCredential(credentialId, {
includeData: true,
});
expect(credentialWithData.data).toBeDefined();
expect(credentialWithData.data?.user).toBe('test_user');
const updatedName = 'Updated HTTP Basic Auth';
const updatedCredential = await api.credentialApi.updateCredential(credentialId, {
name: updatedName,
data: {
user: 'updated_user',
password: 'updated_password',
},
});
expect(updatedCredential.name).toBe(updatedName);
const verifyUpdated = await api.credentialApi.getCredential(credentialId, {
includeData: true,
});
expect(verifyUpdated.name).toBe(updatedName);
expect(verifyUpdated.data?.user).toBe('updated_user');
const deleteResult = await api.credentialApi.deleteCredential(credentialId);
expect(deleteResult).toBe(true);
await expect(api.credentialApi.getCredential(credentialId)).rejects.toThrow();
});
});
test.describe('Credential Listing', () => {
test('should list credentials with different query options', async ({ api }) => {
const credential1 = await api.credentialApi.createCredentialFromDefinition({
name: 'First Test Credential',
type: 'httpBasicAuth',
data: { user: 'user1', password: 'pass1' },
});
const credential2 = await api.credentialApi.createCredentialFromDefinition({
name: 'Second Test Credential',
type: 'httpHeaderAuth',
data: { name: 'Authorization', value: 'Bearer token' },
});
const allCredentials = await api.credentialApi.getCredentials();
expect(allCredentials.length).toBeGreaterThanOrEqual(2);
const createdIds = [credential1.credentialId, credential2.credentialId];
const foundCredentials = allCredentials.filter((c) => createdIds.includes(c.id));
expect(foundCredentials).toHaveLength(2);
const credentialsWithScopes = await api.credentialApi.getCredentials({
includeScopes: true,
});
expect(credentialsWithScopes[0].scopes).toBeDefined();
expect(Array.isArray(credentialsWithScopes[0].scopes)).toBe(true);
const credentialsWithData = await api.credentialApi.getCredentials({
includeData: true,
});
const foundWithData = credentialsWithData.filter((c) => createdIds.includes(c.id));
expect(foundWithData.some((c) => c.data)).toBe(true);
});
});
test.describe('Project Integration', () => {
test('should handle credential-project associations', async ({ api }) => {
await api.enableFeature('projectRole:admin');
await api.enableFeature('projectRole:editor');
await api.setMaxTeamProjectsQuota(-1);
const project = await api.projectApi.createProject('Test Project for Credentials');
const credential = await api.credentialApi.createCredentialFromDefinition({
name: 'Project Credential',
type: 'httpBasicAuth',
data: { user: 'user', password: 'pass' },
projectId: project.id,
});
const projectCredentials = await api.credentialApi.getCredentialsForWorkflow({
projectId: project.id,
});
expect(projectCredentials).toBeDefined();
expect(Array.isArray(projectCredentials)).toBe(true);
const foundCredential = projectCredentials.find((c) => c.id === credential.credentialId);
expect(foundCredential).toBeDefined();
});
test('should transfer credential between projects', async ({ api }) => {
await api.enableFeature('projectRole:admin');
await api.enableFeature('projectRole:editor');
await api.setMaxTeamProjectsQuota(-1);
const sourceProject = await api.projectApi.createProject('Source Project');
const destinationProject = await api.projectApi.createProject('Destination Project');
const credential = await api.credentialApi.createCredentialFromDefinition({
name: 'Transfer Test Credential',
type: 'httpBasicAuth',
data: { user: 'user', password: 'pass' },
projectId: sourceProject.id,
});
const sourceCredentials = await api.credentialApi.getCredentialsForWorkflow({
projectId: sourceProject.id,
});
const foundInSource = sourceCredentials.find((c) => c.id === credential.credentialId);
expect(foundInSource).toBeDefined();
await api.credentialApi.transferCredential(credential.credentialId, destinationProject.id);
const destinationCredentials = await api.credentialApi.getCredentialsForWorkflow({
projectId: destinationProject.id,
});
const foundInDestination = destinationCredentials.find(
(c) => c.id === credential.credentialId,
);
expect(foundInDestination).toBeDefined();
const sourceCredentialsAfter = await api.credentialApi.getCredentialsForWorkflow({
projectId: sourceProject.id,
});
const stillInSource = sourceCredentialsAfter.find((c) => c.id === credential.credentialId);
expect(stillInSource).toBeUndefined();
});
});
test.describe('Data Persistence', () => {
test('should maintain credential data across operations', async ({ api }) => {
const originalData: CreateCredentialDto = {
name: 'Persistence Test Credential',
type: 'httpBasicAuth',
data: {
user: 'persistent_user',
password: 'persistent_password',
},
};
const { credentialId } = await api.credentialApi.createCredentialFromDefinition(originalData);
const afterCreate = await api.credentialApi.getCredential(credentialId, {
includeData: true,
});
expect(afterCreate.data?.user).toBe('persistent_user');
await api.credentialApi.updateCredential(credentialId, {
data: {
user: 'updated_persistent_user',
password: 'updated_persistent_password',
},
});
const afterUpdate = await api.credentialApi.getCredential(credentialId, {
includeData: true,
});
expect(afterUpdate.data?.user).toBe('updated_persistent_user');
expect(afterUpdate.data?.password).toBeDefined();
const allCredentials = await api.credentialApi.getCredentials();
const foundCredential = allCredentials.find((c) => c.id === credentialId);
expect(foundCredential).toBeDefined();
expect(foundCredential!.type).toBe('httpBasicAuth');
});
});
});