Skip to content

Commit a2ad514

Browse files
committed
Finish Trigger Test Suite
1 parent 7e8450e commit a2ad514

File tree

6 files changed

+487
-67
lines changed

6 files changed

+487
-67
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
# n8n-nodes-http-forward-auth
1+
# n8n-nodes-gnr-stack
22

3-
This is an n8n community node. It can be used as a HTTP forward auth middleware with reverse proxies like Traefik and Caddy.
3+
This is a set of n8n community nodes. It may be used in GNR Stack.
4+
5+
## HTTP Forward Auth Trigger/Response Node
6+
It can be used as a HTTP forward auth middleware with reverse proxies like Traefik and Caddy.
7+
8+
## Redis Vector Store Node
9+
This is a node that use Redis Stack Vector database as a vector store for your agents.
410

511
[n8n](https://n8n.io/) is a [fair-code licensed](https://docs.n8n.io/reference/license/) workflow automation platform.
612

nodes/HttpForwardAuth/HttpForwardAuthTrigger.node.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class HttpForwardAuthTrigger implements INodeType {
4141
const logoutRedirectURL = this.getNodeParameter('logoutRedirectURL', '') as string;
4242
const enableHTTP = this.getNodeParameter('enableHTTP', false) as boolean;
4343
const rateLimit = this.getNodeParameter('rateLimit', false) as boolean;
44-
const remoteIp = rateLimit ? req.header(REMOTE_IP_HEADER) : undefined;
44+
const remoteIp = rateLimit ? req.headers[REMOTE_IP_HEADER] as string : undefined;
4545
const rateLimitErrorMessage = this.getNodeParameter('rateLimitErrorMessage', '') as string;
4646
const loginTemplate = this.getNodeParameter('loginTemplate', '') as string;
4747

@@ -83,7 +83,7 @@ export class HttpForwardAuthTrigger implements INodeType {
8383
}
8484
} else if (webhookName === 'default') {
8585
// CSRF protection
86-
const origin = req.header('Origin');
86+
const origin = req.headers.Origin;
8787
if (!origin || origin !== new URL(authURL).origin) {
8888
res.status(403).send('Error 403 - Forbidden').end();
8989
} else if (rateLimit && remoteIp && !(await rateLimitConsume(redis, remoteIp))) {
@@ -109,7 +109,7 @@ export class HttpForwardAuthTrigger implements INodeType {
109109
res.status(200).send(pageContent).end();
110110
} else if (webhookName === 'logout') {
111111
// CSRF protection
112-
const origin = req.header('Origin');
112+
const origin = req.headers.Origin;
113113
if (!origin || origin !== new URL(authURL).origin) {
114114
res.status(403).send('Error 403 - Forbidden').end();
115115
} else {

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"name": "n8n-nodes-http-forward-auth",
2+
"name": "n8n-nodes-http-gnr-stack",
33
"version": "0.1.0",
4-
"description": "This is an n8n community node. It can be used as a HTTP forward auth middleware with reverse proxies like Traefik and Caddy.",
4+
"description": "This is a set of n8n community nodes. It may be used in GNR Stack.",
55
"keywords": [
66
"n8n-community-node-package"
77
],
@@ -13,7 +13,7 @@
1313
},
1414
"repository": {
1515
"type": "git",
16-
"url": "https://github.com/pedrozadotdev/n8n-nodes-http-forward-auth.git"
16+
"url": "https://github.com/pedrozadotdev/n8n-nodes-gnr-stack.git"
1717
},
1818
"engines": {
1919
"node": ">=18.10",

test/helpers.ts

Lines changed: 207 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,48 @@
1+
import type { Request, Response } from 'express';
2+
import type { RedisCredential } from '../nodes/HttpForwardAuth/types';
3+
import type {
4+
IExecuteFunctions,
5+
INode,
6+
IWebhookFunctions,
7+
IWorkflowDataProxyData,
8+
NodeTypeAndVersion,
9+
} from 'n8n-workflow';
10+
111
import { createClient } from 'redis';
12+
import { mock } from 'jest-mock-extended';
213
import { getPoolManagerInstance } from '../nodes/HttpForwardAuth/transport';
3-
import type { RedisCredential } from '../nodes/HttpForwardAuth/types';
14+
import { TRIGGER_NAME } from '../nodes/HttpForwardAuth/constants';
15+
16+
type TriggerOpts = {
17+
request: {
18+
headers?: Record<string, unknown>;
19+
body?: Record<string, unknown>;
20+
};
21+
params: {
22+
authURL?: string;
23+
loginRedirectURL?: string;
24+
logoutRedirectURL?: string;
25+
rateLimit?: boolean;
26+
enableHTTP?: boolean;
27+
rateLimitErrorMessage?: string;
28+
loginTemplate?: string;
29+
};
30+
webhookName: 'setup' | 'logoutPage' | 'logout' | 'check' | 'default';
31+
redisStore: Record<string, string | null>;
32+
redisEvalShaFunc?: () => [0 | 1];
33+
};
34+
35+
type ResponseOpts = {
36+
params: {
37+
userID?: string;
38+
validationErrorMessage?: string;
39+
};
40+
parentNodes: NodeTypeAndVersion[];
41+
triggerData: {
42+
data?: Record<string, unknown>,
43+
params?: TriggerOpts['params']
44+
}
45+
};
446

547
export const setupRedis = (mockOverrides?: Record<string, unknown>) => {
648
(createClient as jest.Mock).mockImplementation(() => ({
@@ -24,6 +66,169 @@ export const resetRedis = async () => {
2466
export const resetJest = () => {
2567
jest.resetAllMocks();
2668
jest.useRealTimers();
27-
}
69+
};
2870

2971
export const credentialsMock: RedisCredential = { host: 'redis', port: 6379, database: 0 };
72+
73+
const defaultTriggerOpts: TriggerOpts = {
74+
request: {
75+
headers: {
76+
cookie: [],
77+
},
78+
body: {},
79+
},
80+
params: {
81+
authURL: 'http://localhost:8080',
82+
loginRedirectURL: 'http://localhost:8080/protected',
83+
logoutRedirectURL: 'http://localhost:8080/login',
84+
rateLimit: false,
85+
enableHTTP: false,
86+
rateLimitErrorMessage: 'Rate Limit Error',
87+
loginTemplate: '#LOGIN_URL#|#ERROR_MESSAGE#',
88+
},
89+
webhookName: 'setup',
90+
redisStore: {},
91+
redisEvalShaFunc: () => [1],
92+
};
93+
94+
export const setupTrigger = (opts?: Partial<TriggerOpts>) => {
95+
const store: TriggerOpts = {
96+
request: {
97+
headers: {
98+
...defaultTriggerOpts.request.headers,
99+
...(opts?.request?.headers ?? {}),
100+
},
101+
body: opts?.request?.body ?? {},
102+
},
103+
params: {
104+
...defaultTriggerOpts.params,
105+
...(opts?.params ?? {}),
106+
},
107+
webhookName: opts?.webhookName ?? defaultTriggerOpts.webhookName,
108+
redisStore: opts?.redisStore ?? {},
109+
};
110+
const redisEvalShaMock = jest.fn(opts?.redisEvalShaFunc ?? defaultTriggerOpts.redisEvalShaFunc);
111+
setupRedis({
112+
get: async (key: string) => store.redisStore[key],
113+
set: async (key: string, value: string) => {
114+
store.redisStore[key] = value;
115+
},
116+
evalSha: redisEvalShaMock,
117+
});
118+
119+
const resEndMock = jest.fn();
120+
const [resSetHeaderMock, resRedirectMock, resSendMock] = [
121+
jest.fn(),
122+
jest.fn(),
123+
jest.fn(() =>
124+
mock<Response>({
125+
end: resEndMock,
126+
}),
127+
),
128+
];
129+
const resStatusMock = jest.fn(() =>
130+
mock<Response>({
131+
redirect: resRedirectMock,
132+
end: resEndMock,
133+
send: resSendMock,
134+
}),
135+
);
136+
137+
const mocks = {
138+
redisEvalShaMock,
139+
resSetHeaderMock,
140+
resRedirectMock,
141+
resSendMock,
142+
resStatusMock,
143+
resEndMock,
144+
};
145+
146+
const context = mock<IWebhookFunctions>({
147+
getRequestObject: () =>
148+
mock<Request>({
149+
// biome-ignore lint/suspicious/noExplicitAny: remove error for test
150+
headers: store.request.headers as any,
151+
body: store.request.body,
152+
}),
153+
getResponseObject: () =>
154+
mock<Response>({
155+
setHeader: resSetHeaderMock,
156+
status: resStatusMock,
157+
}),
158+
getCredentials: async <ICredentialsDecrypted>() =>
159+
credentialsMock as unknown as ICredentialsDecrypted,
160+
// @ts-ignore
161+
getNodeParameter: (name: string) => store.params[name],
162+
getWebhookName: () => store.webhookName,
163+
});
164+
165+
return { context, mocks };
166+
};
167+
168+
const defaultResponseOpts: ResponseOpts = {
169+
params: {
170+
userID: '',
171+
validationErrorMessage: 'Validation Error',
172+
},
173+
parentNodes: [{
174+
type: `custom.${TRIGGER_NAME}`,
175+
name: 'Trigger Node',
176+
typeVersion: 1
177+
}],
178+
triggerData: {
179+
params: {
180+
authURL: 'http://localhost:8080',
181+
enableHTTP: false,
182+
loginRedirectURL: 'http://localhost:8080/protected',
183+
loginTemplate: '#LOGIN_URL#|#ERROR_MESSAGE#'
184+
},
185+
data: { remoteIp: 'REMOTE_IP' }
186+
}
187+
};
188+
189+
export const setupResponse = (opts?: Partial<ResponseOpts>) => {
190+
setupRedis();
191+
const sendResponseMock = jest.fn();
192+
const store: ResponseOpts = {
193+
params: {
194+
...defaultResponseOpts.params,
195+
...(opts?.params ?? {}),
196+
},
197+
parentNodes: [...(opts?.parentNodes ?? [])],
198+
triggerData: {
199+
params: {
200+
...defaultResponseOpts.triggerData.params,
201+
...(opts?.triggerData?.params ?? {})
202+
},
203+
data: {
204+
...defaultResponseOpts.triggerData.data,
205+
...(opts?.triggerData?.data ?? {})
206+
}
207+
}
208+
};
209+
210+
const mocks = {
211+
sendResponseMock,
212+
};
213+
214+
const context = mock<IExecuteFunctions>({
215+
getCredentials: async <ICredentialsDecrypted>() =>
216+
credentialsMock as unknown as ICredentialsDecrypted,
217+
// @ts-ignore
218+
getNodeParameter: jest.fn((name: string) => store.params[name]),
219+
getNode: () => mock<INode>(),
220+
getParentNodes: () => store.parentNodes,
221+
getWorkflowDataProxy: () => mock<IWorkflowDataProxyData>({
222+
$node: {
223+
'Trigger Node': {
224+
parameter: store.triggerData.params,
225+
data: store.triggerData.data
226+
}
227+
}
228+
}),
229+
sendResponse: sendResponseMock,
230+
continueOnFail: () => false,
231+
});
232+
233+
return { context, mocks };
234+
};

test/nodes/response.test.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { getRedisClient } from "../../nodes/HttpForwardAuth/transport";
2-
import { resetJest, resetRedis, setupRedis, credentialsMock } from "../helpers";
1+
import { resetJest, resetRedis, setupResponse } from '../helpers';
2+
import { HttpForwardAuth } from '../../nodes/HttpForwardAuth/HttpForwardAuth.node';
33

44
jest.mock('redis', () => ({
55
__esModule: true,
@@ -12,9 +12,11 @@ describe('Response Suite', () => {
1212
afterAll(resetJest);
1313
afterEach(resetRedis);
1414

15-
it('Test', async () => {
16-
setupRedis();
17-
await getRedisClient(credentialsMock);
18-
expect(true).toBe(true)
19-
})
20-
});
15+
it('Should fail if no HttpForwardAuthTrigger is found on workflow', async () => {
16+
const { context } = setupResponse();
17+
const node = new HttpForwardAuth();
18+
19+
const bindExecute = node.execute.bind(context);
20+
expect(() => bindExecute()).rejects.toThrow('No HttpForwardAuthTrigger node found in the workflow')
21+
});
22+
})

0 commit comments

Comments
 (0)