Skip to content

Commit f48dd15

Browse files
authored
chore(core): Add credential support for benchmark scenarios (#18229)
1 parent 57590e5 commit f48dd15

File tree

11 files changed

+429
-8
lines changed

11 files changed

+429
-8
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"type": "httpBearerAuth",
3+
"name": "Dummy HTTP credential",
4+
"data": {
5+
"token": "dummy token"
6+
},
7+
"id": "0fqzOReozl2aQvtl"
8+
}
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
{
2+
"name": "Credential HTTP Request",
3+
"nodes": [
4+
{
5+
"parameters": {
6+
"httpMethod": "POST",
7+
"path": "benchmark-credential-http-node",
8+
"responseMode": "responseNode",
9+
"options": {}
10+
},
11+
"type": "n8n-nodes-base.webhook",
12+
"typeVersion": 2,
13+
"position": [-64, 32],
14+
"id": "7dd66dcc-03c7-4898-ab3d-d2765730e3f3",
15+
"name": "Webhook",
16+
"webhookId": "92b141cd-6e59-4425-9c0a-e2ee0f4faad2"
17+
},
18+
{
19+
"parameters": {
20+
"respondWith": "allIncomingItems",
21+
"options": {}
22+
},
23+
"type": "n8n-nodes-base.respondToWebhook",
24+
"typeVersion": 1.1,
25+
"position": [1072, 32],
26+
"id": "e074e6a7-2417-4fde-8b6b-bc68069e833b",
27+
"name": "Respond to Webhook"
28+
},
29+
{
30+
"parameters": {
31+
"url": "http://mockapi:8080/users/clair.bahringer/received_events/public",
32+
"authentication": "genericCredentialType",
33+
"genericAuthType": "httpBearerAuth",
34+
"options": {
35+
"response": {
36+
"response": {
37+
"fullResponse": true
38+
}
39+
}
40+
}
41+
},
42+
"type": "n8n-nodes-base.httpRequest",
43+
"typeVersion": 4.2,
44+
"position": [304, -176],
45+
"id": "28ddbbf9-56a5-431e-afe9-3c44b21aa676",
46+
"name": "Mock public received events",
47+
"credentials": {
48+
"httpBearerAuth": {
49+
"id": "0fqzOReozl2aQvtl",
50+
"name": "Dummy HTTP credential"
51+
}
52+
}
53+
},
54+
{
55+
"parameters": {
56+
"url": "http://mockapi:8080/repos/udke6pujoywnagxkcvab2riw23khzn2tibo2vincws32qexb50ey7h97d42vnzyol0rxypgsg4pomsf7sgnmdaihstljw8edcijrwmy7mfi76yif19c4/47i31dh737el215j62ts2f2782nw3ss26rul3s8jw13u3vu0xm349a5hyay5asmwnlnf7nx8p9h4g62so6s1cis7xv9puj5j98t4m980sbe2455fn1obccjl/events",
57+
"authentication": "genericCredentialType",
58+
"genericAuthType": "httpBearerAuth",
59+
"options": {
60+
"response": {
61+
"response": {
62+
"fullResponse": true
63+
}
64+
}
65+
}
66+
},
67+
"type": "n8n-nodes-base.httpRequest",
68+
"typeVersion": 4.2,
69+
"position": [304, 32],
70+
"id": "3ce8827c-6226-467e-a4da-9891c1acd863",
71+
"name": "Mock repository events",
72+
"credentials": {
73+
"httpBearerAuth": {
74+
"id": "0fqzOReozl2aQvtl",
75+
"name": "Dummy HTTP credential"
76+
}
77+
}
78+
},
79+
{
80+
"parameters": {
81+
"url": "http://mockapi:8080/orgs/g02pp066qoyithcjevhd6m1wfii3c4x51k39n9apybljhx69/events",
82+
"authentication": "genericCredentialType",
83+
"genericAuthType": "httpBearerAuth",
84+
"options": {
85+
"response": {
86+
"response": {
87+
"fullResponse": true
88+
}
89+
}
90+
}
91+
},
92+
"type": "n8n-nodes-base.httpRequest",
93+
"typeVersion": 4.2,
94+
"position": [304, 224],
95+
"id": "a8e416ab-50cc-4e50-8d9a-9fcf5d4bbdc8",
96+
"name": "Mock organization events",
97+
"credentials": {
98+
"httpBearerAuth": {
99+
"id": "0fqzOReozl2aQvtl",
100+
"name": "Dummy HTTP credential"
101+
}
102+
}
103+
},
104+
{
105+
"parameters": {
106+
"numberInputs": 3
107+
},
108+
"type": "n8n-nodes-base.merge",
109+
"typeVersion": 3,
110+
"position": [608, 32],
111+
"id": "542a27d4-3a03-4c22-a79a-7266050c519e",
112+
"name": "Merge"
113+
},
114+
{
115+
"parameters": {
116+
"assignments": {
117+
"assignments": [
118+
{
119+
"id": "89608adb-f487-416f-a7d8-3ebb1f7b50e5",
120+
"name": "statusCode",
121+
"value": "={{ $json.statusCode }}",
122+
"type": "number"
123+
}
124+
]
125+
},
126+
"options": {}
127+
},
128+
"id": "35d4bfbb-d4be-49f4-a5dd-bd5c67a48200",
129+
"name": "Select statusCode",
130+
"type": "n8n-nodes-base.set",
131+
"typeVersion": 3.4,
132+
"position": [832, 32]
133+
}
134+
],
135+
"pinData": {
136+
"Webhook": [
137+
{
138+
"json": {
139+
"headers": {
140+
"host": "localhost:5678",
141+
"user-agent": "curl/8.6.0",
142+
"accept": "*/*"
143+
},
144+
"params": {},
145+
"query": {},
146+
"body": {},
147+
"webhookUrl": "http://localhost:5678/webhook-test/benchmark-credential-http-node",
148+
"executionMode": "test"
149+
}
150+
}
151+
]
152+
},
153+
"connections": {
154+
"Webhook": {
155+
"main": [
156+
[
157+
{
158+
"node": "Mock public received events",
159+
"type": "main",
160+
"index": 0
161+
},
162+
{
163+
"node": "Mock repository events",
164+
"type": "main",
165+
"index": 0
166+
},
167+
{
168+
"node": "Mock organization events",
169+
"type": "main",
170+
"index": 0
171+
}
172+
]
173+
]
174+
},
175+
"Mock public received events": {
176+
"main": [
177+
[
178+
{
179+
"node": "Merge",
180+
"type": "main",
181+
"index": 0
182+
}
183+
]
184+
]
185+
},
186+
"Mock repository events": {
187+
"main": [
188+
[
189+
{
190+
"node": "Merge",
191+
"type": "main",
192+
"index": 1
193+
}
194+
]
195+
]
196+
},
197+
"Mock organization events": {
198+
"main": [
199+
[
200+
{
201+
"node": "Merge",
202+
"type": "main",
203+
"index": 2
204+
}
205+
]
206+
]
207+
},
208+
"Merge": {
209+
"main": [
210+
[
211+
{
212+
"node": "Select statusCode",
213+
"type": "main",
214+
"index": 0
215+
}
216+
]
217+
]
218+
},
219+
"Select statusCode": {
220+
"main": [
221+
[
222+
{
223+
"node": "Respond to Webhook",
224+
"type": "main",
225+
"index": 0
226+
}
227+
]
228+
]
229+
}
230+
},
231+
"active": true,
232+
"settings": {
233+
"executionOrder": "v1"
234+
},
235+
"versionId": "96bfc5ef-9421-47f5-9fdd-432dbd4bc4ca",
236+
"meta": {
237+
"instanceId": "4141065f11bd5ed73fef4f9b1d91842ded0ec4058e6640a98aa14384e269204b"
238+
},
239+
"id": "6V8rTiIDqZOniAs1",
240+
"tags": []
241+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"$schema": "../scenario.schema.json",
3+
"name": "CredentialHttpNode",
4+
"description": "Webhook -> 3x HTTP request to a mock API -> Merge -> Respond to Webhook. Requires a mock API running at http://mockapi:8080",
5+
"scenarioData": {
6+
"workflowFiles": ["credential-http-node.json"],
7+
"credentialFiles": ["credential-bearer.json"]
8+
},
9+
"scriptPath": "credential-http-node.script.js"
10+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import http from 'k6/http';
2+
import { check } from 'k6';
3+
4+
const apiBaseUrl = __ENV.API_BASE_URL;
5+
6+
export default function () {
7+
const res = http.post(`${apiBaseUrl}/webhook/benchmark-http-node`);
8+
9+
if (res.status !== 200) {
10+
console.error(
11+
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
12+
);
13+
}
14+
15+
check(res, {
16+
'is status 200': (r) => r.status === 200,
17+
'http requests were OK': (r) => {
18+
if (r.status !== 200) return false;
19+
20+
try {
21+
// Response body is an array of the request status codes made with HttpNodes
22+
const body = JSON.parse(r.body);
23+
return Array.isArray(body) ? body.every((request) => request.statusCode === 200) : false;
24+
} catch (error) {
25+
console.error('Error parsing response body: ', error);
26+
return false;
27+
}
28+
},
29+
});
30+
}

packages/@n8n/benchmark/scenarios/scenario.schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
"items": {
99
"type": "string"
1010
}
11+
},
12+
"credentialFiles": {
13+
"type": "array",
14+
"items": {
15+
"type": "string"
16+
}
1117
}
1218
},
1319
"required": [],
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { Credential } from '@/n8n-api-client/n8n-api-client.types';
2+
3+
import type { AuthenticatedN8nApiClient } from './authenticated-n8n-api-client';
4+
5+
export class CredentialApiClient {
6+
constructor(private readonly apiClient: AuthenticatedN8nApiClient) {}
7+
8+
async getAllCredentials(): Promise<Credential[]> {
9+
const response = await this.apiClient.get<{ count: number; data: Credential[] }>(
10+
'/credentials',
11+
);
12+
13+
return response.data.data;
14+
}
15+
16+
async createCredential(credential: Credential): Promise<Credential> {
17+
const response = await this.apiClient.post<{ data: Credential }>('/credentials', {
18+
...credential,
19+
id: undefined,
20+
});
21+
22+
return response.data.data;
23+
}
24+
25+
async deleteCredential(credentialId: Credential['id']): Promise<void> {
26+
await this.apiClient.delete(`/credentials/${credentialId}`);
27+
}
28+
}

packages/@n8n/benchmark/src/n8n-api-client/n8n-api-client.types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ export type Workflow = {
66
name: string;
77
tags?: string[];
88
};
9+
10+
export type Credential = {
11+
id: string;
12+
name: string;
13+
type: string;
14+
};

packages/@n8n/benchmark/src/scenario/scenario-data-loader.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,48 @@
11
import * as fs from 'node:fs';
22
import * as path from 'node:path';
33

4-
import type { Workflow } from '@/n8n-api-client/n8n-api-client.types';
4+
import type { Workflow, Credential } from '@/n8n-api-client/n8n-api-client.types';
55
import type { Scenario } from '@/types/scenario';
66

7+
export type LoadableScenarioData = {
8+
workflows: Workflow[];
9+
credentials: Credential[];
10+
};
11+
712
/**
813
* Loads scenario data files from FS
914
*/
1015
export class ScenarioDataFileLoader {
11-
async loadDataForScenario(scenario: Scenario): Promise<{
12-
workflows: Workflow[];
13-
}> {
16+
async loadDataForScenario(scenario: Scenario): Promise<LoadableScenarioData> {
1417
const workflows = await Promise.all(
1518
scenario.scenarioData.workflowFiles?.map((workflowFilePath) =>
1619
this.loadSingleWorkflowFromFile(path.join(scenario.scenarioDirPath, workflowFilePath)),
1720
) ?? [],
1821
);
1922

23+
const credentials = await Promise.all(
24+
scenario.scenarioData.credentialFiles?.map((credentialFilePath) =>
25+
this.loadSingleCredentialFromFile(path.join(scenario.scenarioDirPath, credentialFilePath)),
26+
) ?? [],
27+
);
28+
2029
return {
2130
workflows,
31+
credentials,
2232
};
2333
}
2434

35+
private loadSingleCredentialFromFile(credentialFilePath: string): Credential {
36+
const fileContent = fs.readFileSync(credentialFilePath, 'utf8');
37+
38+
try {
39+
return JSON.parse(fileContent) as Credential;
40+
} catch (error) {
41+
const e = error as Error;
42+
throw new Error(`Failed to parse credential file ${credentialFilePath}: ${e.message}`);
43+
}
44+
}
45+
2546
private loadSingleWorkflowFromFile(workflowFilePath: string): Workflow {
2647
const fileContent = fs.readFileSync(workflowFilePath, 'utf8');
2748

0 commit comments

Comments
 (0)