n8n-nodes-dominusnode 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/credentials/DominusNodeApi.credentials.d.ts +7 -0
- package/dist/credentials/DominusNodeApi.credentials.js +42 -0
- package/dist/credentials/DominusNodeApi.credentials.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes/DominusNodeProxy/DominusNodeProxy.node.d.ts +24 -0
- package/dist/nodes/DominusNodeProxy/DominusNodeProxy.node.js +436 -0
- package/dist/nodes/DominusNodeProxy/DominusNodeProxy.node.js.map +1 -0
- package/dist/nodes/DominusNodeUsage/DominusNodeUsage.node.d.ts +13 -0
- package/dist/nodes/DominusNodeUsage/DominusNodeUsage.node.js +105 -0
- package/dist/nodes/DominusNodeUsage/DominusNodeUsage.node.js.map +1 -0
- package/dist/nodes/DominusNodeWallet/DominusNodeWallet.node.d.ts +33 -0
- package/dist/nodes/DominusNodeWallet/DominusNodeWallet.node.js +656 -0
- package/dist/nodes/DominusNodeWallet/DominusNodeWallet.node.js.map +1 -0
- package/dist/shared/auth.d.ts +74 -0
- package/dist/shared/auth.js +264 -0
- package/dist/shared/auth.js.map +1 -0
- package/dist/shared/constants.d.ts +9 -0
- package/dist/shared/constants.js +13 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/ssrf.d.ts +42 -0
- package/dist/shared/ssrf.js +252 -0
- package/dist/shared/ssrf.js.map +1 -0
- package/package.json +41 -0
- package/src/credentials/DominusNodeApi.credentials.ts +39 -0
- package/src/index.ts +4 -0
- package/src/nodes/DominusNodeProxy/DominusNodeProxy.node.ts +459 -0
- package/src/nodes/DominusNodeUsage/DominusNodeUsage.node.ts +130 -0
- package/src/nodes/DominusNodeWallet/DominusNodeWallet.node.ts +898 -0
- package/src/shared/auth.ts +272 -0
- package/src/shared/constants.ts +11 -0
- package/src/shared/ssrf.ts +257 -0
- package/tests/DominusNodeProxy.test.ts +281 -0
- package/tests/DominusNodeUsage.test.ts +250 -0
- package/tests/DominusNodeWallet.test.ts +591 -0
- package/tests/ssrf.test.ts +238 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mock n8n-workflow before importing the node
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
vi.mock("n8n-workflow", () => ({
|
|
8
|
+
NodeOperationError: class NodeOperationError extends Error {
|
|
9
|
+
constructor(node: unknown, message: string, extra?: unknown) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "NodeOperationError";
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Mock dns/promises for DNS rebinding tests
|
|
17
|
+
vi.mock("dns/promises", () => ({
|
|
18
|
+
default: {
|
|
19
|
+
resolve4: vi.fn().mockResolvedValue(["93.184.216.34"]),
|
|
20
|
+
resolve6: vi.fn().mockRejectedValue(new Error("no AAAA")),
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { DominusNodeProxy } from "../src/nodes/DominusNodeProxy/DominusNodeProxy.node";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Helpers — mock IExecuteFunctions
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function createMockExecuteFunctions(overrides: {
|
|
31
|
+
operation: string;
|
|
32
|
+
params?: Record<string, unknown>;
|
|
33
|
+
credentials?: Record<string, unknown>;
|
|
34
|
+
continueOnFail?: boolean;
|
|
35
|
+
}) {
|
|
36
|
+
const params: Record<string, unknown> = {
|
|
37
|
+
operation: overrides.operation,
|
|
38
|
+
method: "GET",
|
|
39
|
+
proxyType: "dc",
|
|
40
|
+
country: "",
|
|
41
|
+
headers: {},
|
|
42
|
+
...overrides.params,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const creds = {
|
|
46
|
+
apiKey: "dn_test_abc123",
|
|
47
|
+
baseUrl: "https://api.dominusnode.com",
|
|
48
|
+
proxyHost: "proxy.dominusnode.com",
|
|
49
|
+
proxyPort: 8080,
|
|
50
|
+
...overrides.credentials,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
getInputData: () => [{ json: {} }],
|
|
55
|
+
getNodeParameter: (name: string, _index: number, fallback?: unknown) => {
|
|
56
|
+
return params[name] ?? fallback;
|
|
57
|
+
},
|
|
58
|
+
getCredentials: vi.fn().mockResolvedValue(creds),
|
|
59
|
+
getNode: () => ({ name: "DomiNode Proxy" }),
|
|
60
|
+
continueOnFail: () => overrides.continueOnFail ?? false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Tests
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
describe("DominusNodeProxy", () => {
|
|
69
|
+
const node = new DominusNodeProxy();
|
|
70
|
+
|
|
71
|
+
it("has correct description metadata", () => {
|
|
72
|
+
expect(node.description.name).toBe("dominusNodeProxy");
|
|
73
|
+
expect(node.description.displayName).toBe("DomiNode Proxy");
|
|
74
|
+
expect(node.description.credentials).toEqual([
|
|
75
|
+
{ name: "dominusNodeApi", required: true },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("has three operations", () => {
|
|
80
|
+
const opProp = node.description.properties.find((p) => p.name === "operation");
|
|
81
|
+
expect(opProp).toBeDefined();
|
|
82
|
+
expect((opProp as any).options).toHaveLength(3);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("DominusNodeProxy.execute - proxied fetch validation", () => {
|
|
87
|
+
let node: DominusNodeProxy;
|
|
88
|
+
let originalFetch: typeof globalThis.fetch;
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
node = new DominusNodeProxy();
|
|
92
|
+
originalFetch = globalThis.fetch;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
globalThis.fetch = originalFetch;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("rejects empty URL", async () => {
|
|
100
|
+
const mockFns = createMockExecuteFunctions({
|
|
101
|
+
operation: "proxiedFetch",
|
|
102
|
+
params: { url: "" },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/URL is required/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("rejects localhost URL (SSRF)", async () => {
|
|
109
|
+
const mockFns = createMockExecuteFunctions({
|
|
110
|
+
operation: "proxiedFetch",
|
|
111
|
+
params: { url: "http://localhost/admin" },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/localhost/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("rejects private IP (192.168.x.x)", async () => {
|
|
118
|
+
const mockFns = createMockExecuteFunctions({
|
|
119
|
+
operation: "proxiedFetch",
|
|
120
|
+
params: { url: "http://192.168.1.1/admin" },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/private/i);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("rejects cloud metadata endpoint (169.254.169.254)", async () => {
|
|
127
|
+
const mockFns = createMockExecuteFunctions({
|
|
128
|
+
operation: "proxiedFetch",
|
|
129
|
+
params: { url: "http://169.254.169.254/latest/meta-data/" },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/blocked/i);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("rejects hex-encoded loopback (0x7f000001)", async () => {
|
|
136
|
+
const mockFns = createMockExecuteFunctions({
|
|
137
|
+
operation: "proxiedFetch",
|
|
138
|
+
params: { url: "http://0x7f000001/" },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/private/i);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("rejects decimal-encoded loopback (2130706433)", async () => {
|
|
145
|
+
const mockFns = createMockExecuteFunctions({
|
|
146
|
+
operation: "proxiedFetch",
|
|
147
|
+
params: { url: "http://2130706433/" },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/private/i);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("rejects .localhost TLD", async () => {
|
|
154
|
+
const mockFns = createMockExecuteFunctions({
|
|
155
|
+
operation: "proxiedFetch",
|
|
156
|
+
params: { url: "http://evil.localhost/" },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/localhost/);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("rejects .internal hostname", async () => {
|
|
163
|
+
const mockFns = createMockExecuteFunctions({
|
|
164
|
+
operation: "proxiedFetch",
|
|
165
|
+
params: { url: "http://db.internal/" },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/internal/);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("rejects embedded credentials in URL", async () => {
|
|
172
|
+
const mockFns = createMockExecuteFunctions({
|
|
173
|
+
operation: "proxiedFetch",
|
|
174
|
+
params: { url: "http://user:pass@example.com/" },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/credentials/);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("rejects file:// protocol", async () => {
|
|
181
|
+
const mockFns = createMockExecuteFunctions({
|
|
182
|
+
operation: "proxiedFetch",
|
|
183
|
+
params: { url: "file:///etc/passwd" },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/protocols/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("rejects OFAC sanctioned country (IR)", async () => {
|
|
190
|
+
const mockFns = createMockExecuteFunctions({
|
|
191
|
+
operation: "proxiedFetch",
|
|
192
|
+
params: { url: "https://example.com", country: "IR" },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/OFAC/);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("rejects OFAC sanctioned country (KP)", async () => {
|
|
199
|
+
const mockFns = createMockExecuteFunctions({
|
|
200
|
+
operation: "proxiedFetch",
|
|
201
|
+
params: { url: "https://example.com", country: "KP" },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/OFAC/);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("rejects OFAC sanctioned country (RU)", async () => {
|
|
208
|
+
const mockFns = createMockExecuteFunctions({
|
|
209
|
+
operation: "proxiedFetch",
|
|
210
|
+
params: { url: "https://example.com", country: "RU" },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/OFAC/);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("sanitizes credentials in error messages with continueOnFail", async () => {
|
|
217
|
+
const mockFns = createMockExecuteFunctions({
|
|
218
|
+
operation: "proxiedFetch",
|
|
219
|
+
params: { url: "" },
|
|
220
|
+
continueOnFail: true,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const result = await node.execute.call(mockFns as any);
|
|
224
|
+
// Should not throw, but return error in json
|
|
225
|
+
expect(result[0][0].json).toHaveProperty("error");
|
|
226
|
+
// Make sure no raw API key appears
|
|
227
|
+
const errorMsg = result[0][0].json.error as string;
|
|
228
|
+
expect(errorMsg).not.toContain("dn_test_abc123");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("rejects missing API key", async () => {
|
|
232
|
+
const mockFns = createMockExecuteFunctions({
|
|
233
|
+
operation: "proxiedFetch",
|
|
234
|
+
params: { url: "https://example.com" },
|
|
235
|
+
credentials: { apiKey: "" },
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/API Key is required/);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("DominusNodeProxy.execute - DNS rebinding protection", () => {
|
|
243
|
+
let node: DominusNodeProxy;
|
|
244
|
+
let originalFetch: typeof globalThis.fetch;
|
|
245
|
+
|
|
246
|
+
beforeEach(() => {
|
|
247
|
+
node = new DominusNodeProxy();
|
|
248
|
+
originalFetch = globalThis.fetch;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
afterEach(() => {
|
|
252
|
+
globalThis.fetch = originalFetch;
|
|
253
|
+
vi.restoreAllMocks();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("blocks hostname resolving to private IP", async () => {
|
|
257
|
+
// Override the dns mock for this test
|
|
258
|
+
const dnsModule = await import("dns/promises");
|
|
259
|
+
vi.mocked(dnsModule.default.resolve4).mockResolvedValueOnce(["127.0.0.1"]);
|
|
260
|
+
|
|
261
|
+
const mockFns = createMockExecuteFunctions({
|
|
262
|
+
operation: "proxiedFetch",
|
|
263
|
+
params: { url: "https://evil-rebind.example.com/" },
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/private IP/);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("blocks hostname resolving to private IPv6", async () => {
|
|
270
|
+
const dnsModule = await import("dns/promises");
|
|
271
|
+
vi.mocked(dnsModule.default.resolve4).mockResolvedValueOnce(["93.184.216.34"]);
|
|
272
|
+
vi.mocked(dnsModule.default.resolve6).mockResolvedValueOnce(["::1"]);
|
|
273
|
+
|
|
274
|
+
const mockFns = createMockExecuteFunctions({
|
|
275
|
+
operation: "proxiedFetch",
|
|
276
|
+
params: { url: "https://evil-rebind6.example.com/" },
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/private IPv6/);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mock n8n-workflow
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
vi.mock("n8n-workflow", () => ({
|
|
8
|
+
NodeOperationError: class NodeOperationError extends Error {
|
|
9
|
+
constructor(node: unknown, message: string, extra?: unknown) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "NodeOperationError";
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import { DominusNodeUsage } from "../src/nodes/DominusNodeUsage/DominusNodeUsage.node";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Helpers — mock IExecuteFunctions
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
function createMockExecuteFunctions(overrides: {
|
|
23
|
+
operation: string;
|
|
24
|
+
params?: Record<string, unknown>;
|
|
25
|
+
credentials?: Record<string, unknown>;
|
|
26
|
+
continueOnFail?: boolean;
|
|
27
|
+
}) {
|
|
28
|
+
const params: Record<string, unknown> = {
|
|
29
|
+
operation: overrides.operation,
|
|
30
|
+
...overrides.params,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const creds = {
|
|
34
|
+
apiKey: "dn_test_abc123",
|
|
35
|
+
baseUrl: "http://localhost:3000",
|
|
36
|
+
...overrides.credentials,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
getInputData: () => [{ json: {} }],
|
|
41
|
+
getNodeParameter: (name: string, _index: number, fallback?: unknown) => {
|
|
42
|
+
return params[name] ?? fallback;
|
|
43
|
+
},
|
|
44
|
+
getCredentials: vi.fn().mockResolvedValue(creds),
|
|
45
|
+
getNode: () => ({ name: "DomiNode Usage" }),
|
|
46
|
+
continueOnFail: () => overrides.continueOnFail ?? false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Mocked fetch for API calls
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
let originalFetch: typeof globalThis.fetch;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
originalFetch = globalThis.fetch;
|
|
58
|
+
globalThis.fetch = vi.fn().mockImplementation((url: string) => {
|
|
59
|
+
if (typeof url === "string" && url.includes("/api/auth/verify-key")) {
|
|
60
|
+
return Promise.resolve({
|
|
61
|
+
ok: true,
|
|
62
|
+
status: 200,
|
|
63
|
+
text: () => Promise.resolve('{"token": "jwt-mock-token"}'),
|
|
64
|
+
headers: new Headers({ "content-length": "30" }),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// Default: mock API call success with usage data
|
|
68
|
+
return Promise.resolve({
|
|
69
|
+
ok: true,
|
|
70
|
+
status: 200,
|
|
71
|
+
text: () =>
|
|
72
|
+
Promise.resolve(
|
|
73
|
+
'{"totalBytes": 1073741824, "totalCostCents": 300, "records": []}',
|
|
74
|
+
),
|
|
75
|
+
headers: new Headers({ "content-length": "60" }),
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
globalThis.fetch = originalFetch;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Tests
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
describe("DominusNodeUsage - metadata", () => {
|
|
89
|
+
it("has correct description metadata", () => {
|
|
90
|
+
const node = new DominusNodeUsage();
|
|
91
|
+
expect(node.description.name).toBe("dominusNodeUsage");
|
|
92
|
+
expect(node.description.displayName).toBe("DomiNode Usage");
|
|
93
|
+
expect(node.description.credentials).toEqual([
|
|
94
|
+
{ name: "dominusNodeApi", required: true },
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("has one operation", () => {
|
|
99
|
+
const node = new DominusNodeUsage();
|
|
100
|
+
const opProp = node.description.properties.find((p) => p.name === "operation");
|
|
101
|
+
expect(opProp).toBeDefined();
|
|
102
|
+
expect((opProp as any).options).toHaveLength(1);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("has period options: day, week, month", () => {
|
|
106
|
+
const node = new DominusNodeUsage();
|
|
107
|
+
const periodProp = node.description.properties.find((p) => p.name === "period");
|
|
108
|
+
expect(periodProp).toBeDefined();
|
|
109
|
+
const values = (periodProp as any).options.map((o: any) => o.value);
|
|
110
|
+
expect(values).toEqual(["day", "week", "month"]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("DominusNodeUsage.execute - checkUsage", () => {
|
|
115
|
+
it("returns usage data for month period", async () => {
|
|
116
|
+
const node = new DominusNodeUsage();
|
|
117
|
+
const mockFns = createMockExecuteFunctions({
|
|
118
|
+
operation: "checkUsage",
|
|
119
|
+
params: { period: "month" },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = await node.execute.call(mockFns as any);
|
|
123
|
+
expect(result[0]).toHaveLength(1);
|
|
124
|
+
expect(result[0][0].json).toHaveProperty("totalBytes", 1073741824);
|
|
125
|
+
expect(result[0][0].json).toHaveProperty("totalCostCents", 300);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("returns usage data for day period", async () => {
|
|
129
|
+
const node = new DominusNodeUsage();
|
|
130
|
+
const mockFns = createMockExecuteFunctions({
|
|
131
|
+
operation: "checkUsage",
|
|
132
|
+
params: { period: "day" },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const result = await node.execute.call(mockFns as any);
|
|
136
|
+
expect(result[0]).toHaveLength(1);
|
|
137
|
+
expect(result[0][0].json).toHaveProperty("totalBytes");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("returns usage data for week period", async () => {
|
|
141
|
+
const node = new DominusNodeUsage();
|
|
142
|
+
const mockFns = createMockExecuteFunctions({
|
|
143
|
+
operation: "checkUsage",
|
|
144
|
+
params: { period: "week" },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const result = await node.execute.call(mockFns as any);
|
|
148
|
+
expect(result[0]).toHaveLength(1);
|
|
149
|
+
expect(result[0][0].json).toHaveProperty("totalBytes");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("passes since/until ISO dates (not days integer)", async () => {
|
|
153
|
+
const node = new DominusNodeUsage();
|
|
154
|
+
const mockFns = createMockExecuteFunctions({
|
|
155
|
+
operation: "checkUsage",
|
|
156
|
+
params: { period: "week" },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await node.execute.call(mockFns as any);
|
|
160
|
+
|
|
161
|
+
// Verify that the fetch call included since/until params
|
|
162
|
+
const fetchCalls = (globalThis.fetch as any).mock.calls;
|
|
163
|
+
const usageCall = fetchCalls.find(
|
|
164
|
+
(call: any) => typeof call[0] === "string" && call[0].includes("/api/usage"),
|
|
165
|
+
);
|
|
166
|
+
expect(usageCall).toBeDefined();
|
|
167
|
+
const url = usageCall[0] as string;
|
|
168
|
+
expect(url).toContain("since=");
|
|
169
|
+
expect(url).toContain("until=");
|
|
170
|
+
// Verify ISO date format
|
|
171
|
+
const params = new URLSearchParams(url.split("?")[1]);
|
|
172
|
+
const since = params.get("since");
|
|
173
|
+
expect(since).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("rejects missing API key", async () => {
|
|
177
|
+
const node = new DominusNodeUsage();
|
|
178
|
+
const mockFns = createMockExecuteFunctions({
|
|
179
|
+
operation: "checkUsage",
|
|
180
|
+
params: { period: "month" },
|
|
181
|
+
credentials: { apiKey: "" },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await expect(node.execute.call(mockFns as any)).rejects.toThrow(/API Key is required/);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("returns error in json when continueOnFail is true", async () => {
|
|
188
|
+
// Make the API call fail
|
|
189
|
+
globalThis.fetch = vi.fn().mockImplementation((url: string) => {
|
|
190
|
+
if (typeof url === "string" && url.includes("/api/auth/verify-key")) {
|
|
191
|
+
return Promise.resolve({
|
|
192
|
+
ok: false,
|
|
193
|
+
status: 401,
|
|
194
|
+
text: () => Promise.resolve('{"error": "Invalid API key dn_live_secret123"}'),
|
|
195
|
+
headers: new Headers({ "content-length": "50" }),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return Promise.resolve({
|
|
199
|
+
ok: false,
|
|
200
|
+
status: 500,
|
|
201
|
+
text: () => Promise.resolve('{"error": "Internal error"}'),
|
|
202
|
+
headers: new Headers({ "content-length": "30" }),
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const node = new DominusNodeUsage();
|
|
207
|
+
const mockFns = createMockExecuteFunctions({
|
|
208
|
+
operation: "checkUsage",
|
|
209
|
+
params: { period: "month" },
|
|
210
|
+
continueOnFail: true,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const result = await node.execute.call(mockFns as any);
|
|
214
|
+
expect(result[0][0].json).toHaveProperty("error");
|
|
215
|
+
// Ensure credentials are sanitized
|
|
216
|
+
const errorMsg = result[0][0].json.error as string;
|
|
217
|
+
expect(errorMsg).not.toContain("dn_live_secret123");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("handles API error with credential sanitization", async () => {
|
|
221
|
+
globalThis.fetch = vi.fn().mockImplementation((url: string) => {
|
|
222
|
+
if (typeof url === "string" && url.includes("/api/auth/verify-key")) {
|
|
223
|
+
return Promise.resolve({
|
|
224
|
+
ok: true,
|
|
225
|
+
status: 200,
|
|
226
|
+
text: () => Promise.resolve('{"token": "jwt-mock-token"}'),
|
|
227
|
+
headers: new Headers({ "content-length": "30" }),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return Promise.resolve({
|
|
231
|
+
ok: false,
|
|
232
|
+
status: 500,
|
|
233
|
+
text: () => Promise.resolve('{"error": "dn_live_abc123 unauthorized"}'),
|
|
234
|
+
headers: new Headers({ "content-length": "50" }),
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const node = new DominusNodeUsage();
|
|
239
|
+
const mockFns = createMockExecuteFunctions({
|
|
240
|
+
operation: "checkUsage",
|
|
241
|
+
params: { period: "month" },
|
|
242
|
+
continueOnFail: true,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const result = await node.execute.call(mockFns as any);
|
|
246
|
+
const errorMsg = result[0][0].json.error as string;
|
|
247
|
+
expect(errorMsg).not.toContain("dn_live_abc123");
|
|
248
|
+
expect(errorMsg).toContain("***");
|
|
249
|
+
});
|
|
250
|
+
});
|