workato-dev-api 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/.env.example +1 -0
- package/README.md +104 -0
- package/cli.js +171 -0
- package/lib.js +323 -0
- package/package.json +22 -0
- package/test/cli.test.js +989 -0
package/test/cli.test.js
ADDED
|
@@ -0,0 +1,989 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, test, beforeEach } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
// Set token before requiring lib so the module loads cleanly
|
|
10
|
+
process.env.WORKATO_API_TOKEN = 'test-token';
|
|
11
|
+
|
|
12
|
+
const lib = require('../lib');
|
|
13
|
+
|
|
14
|
+
// Reset config before each test to avoid cross-test bleed
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
lib.setConfig({ baseUrl: 'https://example.com/api', token: 'tok' });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Silence console output during tests
|
|
20
|
+
const origLog = console.log;
|
|
21
|
+
const origError = console.error;
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
console.log = () => {};
|
|
24
|
+
console.error = () => {};
|
|
25
|
+
});
|
|
26
|
+
// Restore after each suite naturally via test isolation — tests clean up their own mocks
|
|
27
|
+
|
|
28
|
+
// ── findStep ─────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
describe('findStep', () => {
|
|
31
|
+
test('finds step at top level of block', () => {
|
|
32
|
+
const block = [
|
|
33
|
+
{ as: 'aaa', keyword: 'action' },
|
|
34
|
+
{ as: 'bbb', keyword: 'action' },
|
|
35
|
+
];
|
|
36
|
+
assert.deepEqual(lib.findStep(block, 'bbb'), { as: 'bbb', keyword: 'action' });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('finds step nested inside a block', () => {
|
|
40
|
+
const block = [
|
|
41
|
+
{
|
|
42
|
+
as: 'aaa', keyword: 'if', block: [
|
|
43
|
+
{ as: 'ccc', keyword: 'action' },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
assert.deepEqual(lib.findStep(block, 'ccc'), { as: 'ccc', keyword: 'action' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('finds deeply nested step', () => {
|
|
51
|
+
const block = [
|
|
52
|
+
{
|
|
53
|
+
as: 'a1', keyword: 'foreach', block: [
|
|
54
|
+
{
|
|
55
|
+
as: 'a2', keyword: 'if', block: [
|
|
56
|
+
{ as: 'a3', keyword: 'action' },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
assert.deepEqual(lib.findStep(block, 'a3'), { as: 'a3', keyword: 'action' });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('returns null when step not found', () => {
|
|
66
|
+
const block = [{ as: 'aaa', keyword: 'action' }];
|
|
67
|
+
assert.equal(lib.findStep(block, 'zzz'), null);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('returns null on empty block', () => {
|
|
71
|
+
assert.equal(lib.findStep([], 'aaa'), null);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── deepMerge ─────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
describe('deepMerge', () => {
|
|
78
|
+
test('merges flat objects', () => {
|
|
79
|
+
const target = { a: 1, b: 2 };
|
|
80
|
+
lib.deepMerge(target, { b: 3, c: 4 });
|
|
81
|
+
assert.deepEqual(target, { a: 1, b: 3, c: 4 });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('deep merges nested objects', () => {
|
|
85
|
+
const target = { input: { lang: 'en', size: 10 } };
|
|
86
|
+
lib.deepMerge(target, { input: { size: 20 } });
|
|
87
|
+
assert.deepEqual(target, { input: { lang: 'en', size: 20 } });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('replaces arrays entirely (does not merge elements)', () => {
|
|
91
|
+
const target = { block: [1, 2, 3] };
|
|
92
|
+
lib.deepMerge(target, { block: [4, 5] });
|
|
93
|
+
assert.deepEqual(target, { block: [4, 5] });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('adds new top-level keys', () => {
|
|
97
|
+
const target = { a: 1 };
|
|
98
|
+
lib.deepMerge(target, { b: { c: 3 } });
|
|
99
|
+
assert.deepEqual(target, { a: 1, b: { c: 3 } });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('overwrites primitive with a new value', () => {
|
|
103
|
+
const target = { lang: 'en' };
|
|
104
|
+
lib.deepMerge(target, { lang: 'fr' });
|
|
105
|
+
assert.deepEqual(target, { lang: 'fr' });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('merges multiple levels deep', () => {
|
|
109
|
+
const target = { a: { b: { c: 1, d: 2 } } };
|
|
110
|
+
lib.deepMerge(target, { a: { b: { d: 99 } } });
|
|
111
|
+
assert.deepEqual(target, { a: { b: { c: 1, d: 99 } } });
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ── extractCode ───────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
describe('extractCode', () => {
|
|
118
|
+
test('extracts from data.recipe.code', () => {
|
|
119
|
+
const code = { number: 0, as: 'abc', block: [] };
|
|
120
|
+
const data = { recipe: { code: JSON.stringify(code) } };
|
|
121
|
+
assert.deepEqual(lib.extractCode(data), code);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('extracts from flat data.code (no .recipe wrapper)', () => {
|
|
125
|
+
const code = { number: 0, as: 'xyz', block: [] };
|
|
126
|
+
const data = { code: JSON.stringify(code) };
|
|
127
|
+
assert.deepEqual(lib.extractCode(data), code);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── apiGet ────────────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
describe('apiGet', () => {
|
|
134
|
+
test('calls fetch with correct URL and Authorization header', async () => {
|
|
135
|
+
lib.setConfig({ baseUrl: 'https://example.com/api', token: 'tok123' });
|
|
136
|
+
let capturedUrl, capturedOpts;
|
|
137
|
+
global.fetch = async (url, opts) => {
|
|
138
|
+
capturedUrl = url;
|
|
139
|
+
capturedOpts = opts;
|
|
140
|
+
return { ok: true, json: async () => ({ result: 'ok' }) };
|
|
141
|
+
};
|
|
142
|
+
await lib.apiGet('/recipes/1');
|
|
143
|
+
assert.equal(capturedUrl, 'https://example.com/api/recipes/1');
|
|
144
|
+
assert.equal(capturedOpts.headers['Authorization'], 'Bearer tok123');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('throws with status code on non-ok response', async () => {
|
|
148
|
+
global.fetch = async () => ({ ok: false, status: 404, text: async () => 'Not Found' });
|
|
149
|
+
await assert.rejects(() => lib.apiGet('/recipes/999'), /404/);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ── apiPost ───────────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
describe('apiPost', () => {
|
|
156
|
+
test('sends POST with JSON body and correct headers', async () => {
|
|
157
|
+
let capturedUrl, capturedOpts;
|
|
158
|
+
global.fetch = async (url, opts) => {
|
|
159
|
+
capturedUrl = url;
|
|
160
|
+
capturedOpts = opts;
|
|
161
|
+
return { ok: true, json: async () => ({ id: 1 }) };
|
|
162
|
+
};
|
|
163
|
+
const result = await lib.apiPost('/recipes', { recipe: { name: 'Test' } });
|
|
164
|
+
assert.equal(capturedOpts.method, 'POST');
|
|
165
|
+
assert.equal(capturedOpts.headers['Content-Type'], 'application/json');
|
|
166
|
+
assert.deepEqual(JSON.parse(capturedOpts.body), { recipe: { name: 'Test' } });
|
|
167
|
+
assert.deepEqual(result, { id: 1 });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('throws on non-ok response', async () => {
|
|
171
|
+
global.fetch = async () => ({ ok: false, status: 422, text: async () => 'Unprocessable' });
|
|
172
|
+
await assert.rejects(() => lib.apiPost('/recipes', {}), /422/);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── apiPut ────────────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
describe('apiPut', () => {
|
|
179
|
+
test('sends PUT with JSON body', async () => {
|
|
180
|
+
let capturedOpts;
|
|
181
|
+
global.fetch = async (url, opts) => {
|
|
182
|
+
capturedOpts = opts;
|
|
183
|
+
return { ok: true, json: async () => ({}) };
|
|
184
|
+
};
|
|
185
|
+
await lib.apiPut('/recipes/1', { recipe: { code: '{}' } });
|
|
186
|
+
assert.equal(capturedOpts.method, 'PUT');
|
|
187
|
+
assert.equal(capturedOpts.headers['Content-Type'], 'application/json');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ── apiDelete ─────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
describe('apiDelete', () => {
|
|
194
|
+
test('sends DELETE request', async () => {
|
|
195
|
+
let capturedMethod;
|
|
196
|
+
global.fetch = async (url, opts) => {
|
|
197
|
+
capturedMethod = opts.method;
|
|
198
|
+
return { ok: true, text: async () => '' };
|
|
199
|
+
};
|
|
200
|
+
await lib.apiDelete('/recipes/1');
|
|
201
|
+
assert.equal(capturedMethod, 'DELETE');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('returns empty object when response body is empty', async () => {
|
|
205
|
+
global.fetch = async () => ({ ok: true, text: async () => '' });
|
|
206
|
+
const result = await lib.apiDelete('/recipes/1');
|
|
207
|
+
assert.deepEqual(result, {});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('parses and returns JSON body when present', async () => {
|
|
211
|
+
global.fetch = async () => ({ ok: true, text: async () => JSON.stringify({ success: true }) });
|
|
212
|
+
const result = await lib.apiDelete('/recipes/1');
|
|
213
|
+
assert.deepEqual(result, { success: true });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('throws on non-ok response', async () => {
|
|
217
|
+
global.fetch = async () => ({ ok: false, status: 403, text: async () => 'Forbidden' });
|
|
218
|
+
await assert.rejects(() => lib.apiDelete('/recipes/1'), /403/);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ── cmdGet ────────────────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
describe('cmdGet', () => {
|
|
225
|
+
test('fetches recipe, writes file, and returns parsed code', async () => {
|
|
226
|
+
const code = { number: 0, as: 'abc', block: [] };
|
|
227
|
+
global.fetch = async () => ({ ok: true, json: async () => ({ recipe: { code: JSON.stringify(code) } }) });
|
|
228
|
+
|
|
229
|
+
const written = {};
|
|
230
|
+
const origWriteFile = require('fs').writeFileSync;
|
|
231
|
+
try {
|
|
232
|
+
require('fs').writeFileSync = (p, content) => { written[p] = content; };
|
|
233
|
+
const result = await lib.cmdGet('123');
|
|
234
|
+
assert.deepEqual(result, code);
|
|
235
|
+
assert.ok(Object.keys(written).some(k => k.includes('recipe_123_code.json')), 'should write recipe_123_code.json');
|
|
236
|
+
} finally {
|
|
237
|
+
require('fs').writeFileSync = origWriteFile;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ── cmdListRecipes ────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
describe('cmdListRecipes', () => {
|
|
245
|
+
test('calls /recipes with no query string by default', async () => {
|
|
246
|
+
let calledUrl;
|
|
247
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({ items: [] }) }; };
|
|
248
|
+
await lib.cmdListRecipes();
|
|
249
|
+
assert.equal(calledUrl, 'https://example.com/api/recipes');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('appends folder_id when provided', async () => {
|
|
253
|
+
let calledUrl;
|
|
254
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
255
|
+
await lib.cmdListRecipes({ folder_id: '42' });
|
|
256
|
+
assert.ok(calledUrl.includes('folder_id=42'), `URL was: ${calledUrl}`);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('appends project_id when provided', async () => {
|
|
260
|
+
let calledUrl;
|
|
261
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
262
|
+
await lib.cmdListRecipes({ project_id: '14318' });
|
|
263
|
+
assert.ok(calledUrl.includes('project_id=14318'), `URL was: ${calledUrl}`);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('appends page when provided', async () => {
|
|
267
|
+
let calledUrl;
|
|
268
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
269
|
+
await lib.cmdListRecipes({ page: '2' });
|
|
270
|
+
assert.ok(calledUrl.includes('page=2'), `URL was: ${calledUrl}`);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ── cmdListProjects ───────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
describe('cmdListProjects', () => {
|
|
277
|
+
test('calls /projects', async () => {
|
|
278
|
+
let calledUrl;
|
|
279
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
280
|
+
await lib.cmdListProjects();
|
|
281
|
+
assert.ok(calledUrl.endsWith('/projects'));
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ── cmdListFolders ────────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
describe('cmdListFolders', () => {
|
|
288
|
+
test('calls /folders with no params by default', async () => {
|
|
289
|
+
let calledUrl;
|
|
290
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
291
|
+
await lib.cmdListFolders();
|
|
292
|
+
assert.equal(calledUrl, 'https://example.com/api/folders');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('appends parent_id when provided', async () => {
|
|
296
|
+
let calledUrl;
|
|
297
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
298
|
+
await lib.cmdListFolders({ parent_id: '5' });
|
|
299
|
+
assert.ok(calledUrl.includes('parent_id=5'), `URL was: ${calledUrl}`);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ── cmdListConnections ────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
describe('cmdListConnections', () => {
|
|
306
|
+
test('calls /connections', async () => {
|
|
307
|
+
let calledUrl;
|
|
308
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
309
|
+
await lib.cmdListConnections();
|
|
310
|
+
assert.ok(calledUrl.endsWith('/connections'));
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('appends folder_id when provided', async () => {
|
|
314
|
+
let calledUrl;
|
|
315
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
316
|
+
await lib.cmdListConnections({ folder_id: '20245' });
|
|
317
|
+
assert.ok(calledUrl.includes('folder_id=20245'), `URL was: ${calledUrl}`);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// ── cmdListDataTables ─────────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
describe('cmdListDataTables', () => {
|
|
324
|
+
test('calls /data_tables', async () => {
|
|
325
|
+
let calledUrl;
|
|
326
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
327
|
+
await lib.cmdListDataTables();
|
|
328
|
+
assert.ok(calledUrl.endsWith('/data_tables'));
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('appends project_id when provided', async () => {
|
|
332
|
+
let calledUrl;
|
|
333
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
334
|
+
await lib.cmdListDataTables({ project_id: '14318' });
|
|
335
|
+
assert.ok(calledUrl.includes('project_id=14318'), `URL was: ${calledUrl}`);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// ── cmdGetDataTable ───────────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
describe('cmdGetDataTable', () => {
|
|
342
|
+
test('calls /data_tables/:id', async () => {
|
|
343
|
+
let calledUrl;
|
|
344
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
345
|
+
await lib.cmdGetDataTable('3512');
|
|
346
|
+
assert.ok(calledUrl.endsWith('/data_tables/3512'));
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// ── cmdGetJobs ────────────────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
describe('cmdGetJobs', () => {
|
|
353
|
+
test('calls /recipes/:id/jobs', async () => {
|
|
354
|
+
let calledUrl;
|
|
355
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
356
|
+
await lib.cmdGetJobs('167603');
|
|
357
|
+
assert.ok(calledUrl.includes('/recipes/167603/jobs'));
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('appends per_page from limit option', async () => {
|
|
361
|
+
let calledUrl;
|
|
362
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
363
|
+
await lib.cmdGetJobs('167603', { limit: '10' });
|
|
364
|
+
assert.ok(calledUrl.includes('per_page=10'), `URL was: ${calledUrl}`);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test('appends status filter', async () => {
|
|
368
|
+
let calledUrl;
|
|
369
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
370
|
+
await lib.cmdGetJobs('167603', { status: 'failed' });
|
|
371
|
+
assert.ok(calledUrl.includes('status=failed'), `URL was: ${calledUrl}`);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// ── cmdGetJob ─────────────────────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
describe('cmdGetJob', () => {
|
|
378
|
+
test('calls /recipes/:id/jobs/:job_id', async () => {
|
|
379
|
+
let calledUrl;
|
|
380
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
381
|
+
await lib.cmdGetJob('167603', 'job-abc');
|
|
382
|
+
assert.ok(calledUrl.endsWith('/recipes/167603/jobs/job-abc'));
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ── cmdCreate ─────────────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
describe('cmdCreate', () => {
|
|
389
|
+
test('reads code file and POSTs to /recipes with name and code', async () => {
|
|
390
|
+
const code = { number: 0, as: 'abc', block: [] };
|
|
391
|
+
const origRead = require('fs').readFileSync;
|
|
392
|
+
try {
|
|
393
|
+
require('fs').readFileSync = () => JSON.stringify(code);
|
|
394
|
+
let postedBody;
|
|
395
|
+
global.fetch = async (url, opts) => {
|
|
396
|
+
postedBody = JSON.parse(opts.body);
|
|
397
|
+
return { ok: true, json: async () => ({ recipe: { id: 99 } }) };
|
|
398
|
+
};
|
|
399
|
+
await lib.cmdCreate('My Recipe', 'code.json');
|
|
400
|
+
assert.equal(postedBody.recipe.name, 'My Recipe');
|
|
401
|
+
assert.deepEqual(JSON.parse(postedBody.recipe.code), code);
|
|
402
|
+
} finally {
|
|
403
|
+
require('fs').readFileSync = origRead;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ── cmdCreateApiTrigger ───────────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
describe('cmdCreateApiTrigger', () => {
|
|
411
|
+
test('POSTs recipe with workato_api_platform trigger code', async () => {
|
|
412
|
+
let postedBody;
|
|
413
|
+
global.fetch = async (url, opts) => {
|
|
414
|
+
postedBody = JSON.parse(opts.body);
|
|
415
|
+
return { ok: true, json: async () => ({ recipe: { id: 100 } }) };
|
|
416
|
+
};
|
|
417
|
+
await lib.cmdCreateApiTrigger('Test Trigger Recipe');
|
|
418
|
+
const code = JSON.parse(postedBody.recipe.code);
|
|
419
|
+
assert.equal(code.provider, 'workato_api_platform');
|
|
420
|
+
assert.equal(code.keyword, 'trigger');
|
|
421
|
+
assert.equal(postedBody.recipe.name, 'Test Trigger Recipe');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test('sets config with workato_api_platform application', async () => {
|
|
425
|
+
let postedBody;
|
|
426
|
+
global.fetch = async (url, opts) => {
|
|
427
|
+
postedBody = JSON.parse(opts.body);
|
|
428
|
+
return { ok: true, json: async () => ({ recipe: { id: 100 } }) };
|
|
429
|
+
};
|
|
430
|
+
await lib.cmdCreateApiTrigger('Trigger Recipe');
|
|
431
|
+
const config = JSON.parse(postedBody.recipe.config);
|
|
432
|
+
assert.equal(config[0].provider, 'workato_api_platform');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test('trigger code has as, uuid, and empty block', async () => {
|
|
436
|
+
let postedBody;
|
|
437
|
+
global.fetch = async (url, opts) => {
|
|
438
|
+
postedBody = JSON.parse(opts.body);
|
|
439
|
+
return { ok: true, json: async () => ({ recipe: { id: 100 } }) };
|
|
440
|
+
};
|
|
441
|
+
await lib.cmdCreateApiTrigger('Trigger Recipe');
|
|
442
|
+
const code = JSON.parse(postedBody.recipe.code);
|
|
443
|
+
assert.ok(code.as, 'should have an as field');
|
|
444
|
+
assert.ok(code.uuid, 'should have a uuid field');
|
|
445
|
+
assert.deepEqual(code.block, []);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// ── cmdUpdateStep ─────────────────────────────────────────────────────────────
|
|
450
|
+
|
|
451
|
+
describe('cmdUpdateStep', () => {
|
|
452
|
+
test('updates the trigger step (top-level, matched by as)', async () => {
|
|
453
|
+
const code = {
|
|
454
|
+
as: 'trig0001', keyword: 'trigger', provider: 'workato_api_platform',
|
|
455
|
+
input: { request: { content_type: 'json' } },
|
|
456
|
+
block: [],
|
|
457
|
+
};
|
|
458
|
+
const patch = { input: { request: { content_type: 'multipart' } } };
|
|
459
|
+
const origRead = require('fs').readFileSync;
|
|
460
|
+
try {
|
|
461
|
+
require('fs').readFileSync = () => JSON.stringify(patch);
|
|
462
|
+
let putBody;
|
|
463
|
+
global.fetch = async (url, opts) => {
|
|
464
|
+
if (opts?.method === 'PUT') {
|
|
465
|
+
putBody = JSON.parse(opts.body);
|
|
466
|
+
return { ok: true, json: async () => ({}) };
|
|
467
|
+
}
|
|
468
|
+
return { ok: true, json: async () => ({ recipe: { code: JSON.stringify(code) } }) };
|
|
469
|
+
};
|
|
470
|
+
await lib.cmdUpdateStep('123', 'trig0001', 'patch.json');
|
|
471
|
+
const updated = JSON.parse(putBody.recipe.code);
|
|
472
|
+
assert.equal(updated.input.request.content_type, 'multipart');
|
|
473
|
+
} finally {
|
|
474
|
+
require('fs').readFileSync = origRead;
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test('updates a nested action step', async () => {
|
|
479
|
+
const code = {
|
|
480
|
+
as: 'trig0001', keyword: 'trigger', input: {},
|
|
481
|
+
block: [
|
|
482
|
+
{ as: 'act00001', keyword: 'action', input: { lang: 'en' } },
|
|
483
|
+
],
|
|
484
|
+
};
|
|
485
|
+
const patch = { input: { lang: 'fr' } };
|
|
486
|
+
const origRead = require('fs').readFileSync;
|
|
487
|
+
try {
|
|
488
|
+
require('fs').readFileSync = () => JSON.stringify(patch);
|
|
489
|
+
let putBody;
|
|
490
|
+
global.fetch = async (url, opts) => {
|
|
491
|
+
if (opts?.method === 'PUT') {
|
|
492
|
+
putBody = JSON.parse(opts.body);
|
|
493
|
+
return { ok: true, json: async () => ({}) };
|
|
494
|
+
}
|
|
495
|
+
return { ok: true, json: async () => ({ recipe: { code: JSON.stringify(code) } }) };
|
|
496
|
+
};
|
|
497
|
+
await lib.cmdUpdateStep('123', 'act00001', 'patch.json');
|
|
498
|
+
const updated = JSON.parse(putBody.recipe.code);
|
|
499
|
+
assert.equal(updated.block[0].input.lang, 'fr');
|
|
500
|
+
} finally {
|
|
501
|
+
require('fs').readFileSync = origRead;
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test('throws when step as-id is not found', async () => {
|
|
506
|
+
const code = { as: 'trig0001', keyword: 'trigger', block: [] };
|
|
507
|
+
const origRead = require('fs').readFileSync;
|
|
508
|
+
try {
|
|
509
|
+
require('fs').readFileSync = () => JSON.stringify({});
|
|
510
|
+
global.fetch = async () => ({ ok: true, json: async () => ({ recipe: { code: JSON.stringify(code) } }) });
|
|
511
|
+
await assert.rejects(
|
|
512
|
+
() => lib.cmdUpdateStep('123', 'notfound', 'patch.json'),
|
|
513
|
+
/not found/,
|
|
514
|
+
);
|
|
515
|
+
} finally {
|
|
516
|
+
require('fs').readFileSync = origRead;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test('deep-merges patch without clobbering sibling keys', async () => {
|
|
521
|
+
const code = {
|
|
522
|
+
as: 'trig0001', keyword: 'trigger',
|
|
523
|
+
input: { a: 1, b: 2 },
|
|
524
|
+
block: [],
|
|
525
|
+
};
|
|
526
|
+
const patch = { input: { b: 99 } };
|
|
527
|
+
const origRead = require('fs').readFileSync;
|
|
528
|
+
try {
|
|
529
|
+
require('fs').readFileSync = () => JSON.stringify(patch);
|
|
530
|
+
let putBody;
|
|
531
|
+
global.fetch = async (url, opts) => {
|
|
532
|
+
if (opts?.method === 'PUT') {
|
|
533
|
+
putBody = JSON.parse(opts.body);
|
|
534
|
+
return { ok: true, json: async () => ({}) };
|
|
535
|
+
}
|
|
536
|
+
return { ok: true, json: async () => ({ recipe: { code: JSON.stringify(code) } }) };
|
|
537
|
+
};
|
|
538
|
+
await lib.cmdUpdateStep('123', 'trig0001', 'patch.json');
|
|
539
|
+
const updated = JSON.parse(putBody.recipe.code);
|
|
540
|
+
assert.equal(updated.input.a, 1, 'sibling key a should be preserved');
|
|
541
|
+
assert.equal(updated.input.b, 99, 'key b should be updated');
|
|
542
|
+
} finally {
|
|
543
|
+
require('fs').readFileSync = origRead;
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// ── cmdPutCode ────────────────────────────────────────────────────────────────
|
|
549
|
+
|
|
550
|
+
describe('cmdPutCode', () => {
|
|
551
|
+
test('reads code file and PUTs full code to /recipes/:id', async () => {
|
|
552
|
+
const code = { number: 0, as: 'abc', block: [] };
|
|
553
|
+
const origRead = require('fs').readFileSync;
|
|
554
|
+
try {
|
|
555
|
+
require('fs').readFileSync = () => JSON.stringify(code);
|
|
556
|
+
let putUrl, putBody;
|
|
557
|
+
global.fetch = async (url, opts) => {
|
|
558
|
+
putUrl = url;
|
|
559
|
+
putBody = JSON.parse(opts.body);
|
|
560
|
+
return { ok: true, json: async () => ({}) };
|
|
561
|
+
};
|
|
562
|
+
await lib.cmdPutCode('123', 'code.json');
|
|
563
|
+
assert.ok(putUrl.endsWith('/recipes/123'));
|
|
564
|
+
assert.deepEqual(JSON.parse(putBody.recipe.code), code);
|
|
565
|
+
} finally {
|
|
566
|
+
require('fs').readFileSync = origRead;
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// ── cmdStart ──────────────────────────────────────────────────────────────────
|
|
572
|
+
|
|
573
|
+
describe('cmdStart', () => {
|
|
574
|
+
test('PUTs to /recipes/:id/start', async () => {
|
|
575
|
+
let calledUrl, calledMethod;
|
|
576
|
+
global.fetch = async (url, opts) => {
|
|
577
|
+
calledUrl = url;
|
|
578
|
+
calledMethod = opts.method;
|
|
579
|
+
return { ok: true, json: async () => ({}) };
|
|
580
|
+
};
|
|
581
|
+
await lib.cmdStart('123');
|
|
582
|
+
assert.ok(calledUrl.endsWith('/recipes/123/start'));
|
|
583
|
+
assert.equal(calledMethod, 'PUT');
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// ── cmdStop ───────────────────────────────────────────────────────────────────
|
|
588
|
+
|
|
589
|
+
describe('cmdStop', () => {
|
|
590
|
+
test('PUTs to /recipes/:id/stop', async () => {
|
|
591
|
+
let calledUrl, calledMethod;
|
|
592
|
+
global.fetch = async (url, opts) => {
|
|
593
|
+
calledUrl = url;
|
|
594
|
+
calledMethod = opts.method;
|
|
595
|
+
return { ok: true, json: async () => ({}) };
|
|
596
|
+
};
|
|
597
|
+
await lib.cmdStop('123');
|
|
598
|
+
assert.ok(calledUrl.endsWith('/recipes/123/stop'));
|
|
599
|
+
assert.equal(calledMethod, 'PUT');
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// ── cmdDelete ─────────────────────────────────────────────────────────────────
|
|
604
|
+
|
|
605
|
+
describe('cmdDelete', () => {
|
|
606
|
+
test('DELETEs /recipes/:id', async () => {
|
|
607
|
+
let calledMethod, calledUrl;
|
|
608
|
+
global.fetch = async (url, opts) => {
|
|
609
|
+
calledMethod = opts.method;
|
|
610
|
+
calledUrl = url;
|
|
611
|
+
return { ok: true, text: async () => '' };
|
|
612
|
+
};
|
|
613
|
+
await lib.cmdDelete('123');
|
|
614
|
+
assert.equal(calledMethod, 'DELETE');
|
|
615
|
+
assert.ok(calledUrl.endsWith('/recipes/123'));
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test('returns parsed response body', async () => {
|
|
619
|
+
global.fetch = async () => ({ ok: true, text: async () => JSON.stringify({ success: true }) });
|
|
620
|
+
const result = await lib.cmdDelete('123');
|
|
621
|
+
assert.deepEqual(result, { success: true });
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// ── loadEnv ───────────────────────────────────────────────────────────────────
|
|
626
|
+
|
|
627
|
+
describe('loadEnv', () => {
|
|
628
|
+
function tmpEnvFile(content) {
|
|
629
|
+
const p = path.join(os.tmpdir(), `.workato-test-${Date.now()}-${Math.random()}`);
|
|
630
|
+
fs.writeFileSync(p, content);
|
|
631
|
+
return p;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
test('parses key=value and sets process.env', () => {
|
|
635
|
+
const f = tmpEnvFile('WTEST_SIMPLE=hello_world\n');
|
|
636
|
+
try {
|
|
637
|
+
delete process.env.WTEST_SIMPLE;
|
|
638
|
+
lib.loadEnv(f);
|
|
639
|
+
assert.equal(process.env.WTEST_SIMPLE, 'hello_world');
|
|
640
|
+
} finally {
|
|
641
|
+
fs.unlinkSync(f);
|
|
642
|
+
delete process.env.WTEST_SIMPLE;
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test('parses multiple key=value pairs', () => {
|
|
647
|
+
const f = tmpEnvFile('WTEST_A=foo\nWTEST_B=bar\n');
|
|
648
|
+
try {
|
|
649
|
+
delete process.env.WTEST_A;
|
|
650
|
+
delete process.env.WTEST_B;
|
|
651
|
+
lib.loadEnv(f);
|
|
652
|
+
assert.equal(process.env.WTEST_A, 'foo');
|
|
653
|
+
assert.equal(process.env.WTEST_B, 'bar');
|
|
654
|
+
} finally {
|
|
655
|
+
fs.unlinkSync(f);
|
|
656
|
+
delete process.env.WTEST_A;
|
|
657
|
+
delete process.env.WTEST_B;
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test('ignores comment lines starting with #', () => {
|
|
662
|
+
const f = tmpEnvFile('# WTEST_COMMENT=should_not_be_set\n');
|
|
663
|
+
try {
|
|
664
|
+
delete process.env.WTEST_COMMENT;
|
|
665
|
+
lib.loadEnv(f);
|
|
666
|
+
assert.equal(process.env.WTEST_COMMENT, undefined);
|
|
667
|
+
} finally {
|
|
668
|
+
fs.unlinkSync(f);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test('ignores blank lines without throwing', () => {
|
|
673
|
+
const f = tmpEnvFile('\nWTEST_BLANK=set\n\n');
|
|
674
|
+
try {
|
|
675
|
+
delete process.env.WTEST_BLANK;
|
|
676
|
+
lib.loadEnv(f);
|
|
677
|
+
assert.equal(process.env.WTEST_BLANK, 'set');
|
|
678
|
+
} finally {
|
|
679
|
+
fs.unlinkSync(f);
|
|
680
|
+
delete process.env.WTEST_BLANK;
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test('handles value containing = sign', () => {
|
|
685
|
+
const f = tmpEnvFile('WTEST_EQUALS=a=b=c\n');
|
|
686
|
+
try {
|
|
687
|
+
delete process.env.WTEST_EQUALS;
|
|
688
|
+
lib.loadEnv(f);
|
|
689
|
+
assert.equal(process.env.WTEST_EQUALS, 'a=b=c');
|
|
690
|
+
} finally {
|
|
691
|
+
fs.unlinkSync(f);
|
|
692
|
+
delete process.env.WTEST_EQUALS;
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test('does not throw when file does not exist', () => {
|
|
697
|
+
assert.doesNotThrow(() => lib.loadEnv('/nonexistent/path/that/does/not/exist/.env'));
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
test('defaults to cwd/.env when no path argument given', () => {
|
|
701
|
+
// Should not throw even if cwd/.env doesn't exist
|
|
702
|
+
assert.doesNotThrow(() => lib.loadEnv());
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test('later calls overwrite earlier values (last writer wins)', () => {
|
|
706
|
+
const f1 = tmpEnvFile('WTEST_OVERWRITE=first\n');
|
|
707
|
+
const f2 = tmpEnvFile('WTEST_OVERWRITE=second\n');
|
|
708
|
+
try {
|
|
709
|
+
delete process.env.WTEST_OVERWRITE;
|
|
710
|
+
lib.loadEnv(f1);
|
|
711
|
+
lib.loadEnv(f2);
|
|
712
|
+
assert.equal(process.env.WTEST_OVERWRITE, 'second');
|
|
713
|
+
} finally {
|
|
714
|
+
fs.unlinkSync(f1);
|
|
715
|
+
fs.unlinkSync(f2);
|
|
716
|
+
delete process.env.WTEST_OVERWRITE;
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// ── getToken / setConfig ──────────────────────────────────────────────────────
|
|
722
|
+
|
|
723
|
+
describe('getToken', () => {
|
|
724
|
+
test('returns config.token when explicitly set', () => {
|
|
725
|
+
lib.setConfig({ token: 'config-tok' });
|
|
726
|
+
process.env.WORKATO_API_TOKEN = 'env-tok';
|
|
727
|
+
assert.equal(lib.getToken(), 'config-tok');
|
|
728
|
+
process.env.WORKATO_API_TOKEN = 'test-token';
|
|
729
|
+
lib.setConfig({ token: 'tok' });
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
test('falls back to WORKATO_API_TOKEN env var when config token is null', () => {
|
|
733
|
+
lib.setConfig({ token: null });
|
|
734
|
+
process.env.WORKATO_API_TOKEN = 'env-fallback';
|
|
735
|
+
assert.equal(lib.getToken(), 'env-fallback');
|
|
736
|
+
process.env.WORKATO_API_TOKEN = 'test-token';
|
|
737
|
+
lib.setConfig({ token: 'tok' });
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
describe('setConfig', () => {
|
|
742
|
+
test('updates baseUrl used by API calls', async () => {
|
|
743
|
+
lib.setConfig({ baseUrl: 'https://custom.example.com/api', token: 'tok' });
|
|
744
|
+
let calledUrl;
|
|
745
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
746
|
+
await lib.apiGet('/test');
|
|
747
|
+
assert.ok(calledUrl.startsWith('https://custom.example.com/api'));
|
|
748
|
+
lib.setConfig({ baseUrl: 'https://example.com/api' });
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// ── apiTriggerCode ────────────────────────────────────────────────────────────
|
|
753
|
+
|
|
754
|
+
describe('apiTriggerCode', () => {
|
|
755
|
+
test('returns correct provider, keyword, and name', () => {
|
|
756
|
+
const code = lib.apiTriggerCode();
|
|
757
|
+
assert.equal(code.provider, 'workato_api_platform');
|
|
758
|
+
assert.equal(code.keyword, 'trigger');
|
|
759
|
+
assert.equal(code.name, 'receive_request');
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test('as field is a non-empty string', () => {
|
|
763
|
+
const code = lib.apiTriggerCode();
|
|
764
|
+
assert.ok(typeof code.as === 'string' && code.as.length > 0);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test('uuid field matches UUID format', () => {
|
|
768
|
+
const code = lib.apiTriggerCode();
|
|
769
|
+
assert.match(code.uuid, /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
test('generates unique as values on successive calls', () => {
|
|
773
|
+
const codes = Array.from({ length: 5 }, () => lib.apiTriggerCode());
|
|
774
|
+
const asValues = new Set(codes.map(c => c.as));
|
|
775
|
+
assert.equal(asValues.size, 5, 'all as values should be unique');
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
test('has empty block array and empty schemas', () => {
|
|
779
|
+
const code = lib.apiTriggerCode();
|
|
780
|
+
assert.deepEqual(code.block, []);
|
|
781
|
+
assert.deepEqual(code.extended_output_schema, []);
|
|
782
|
+
assert.deepEqual(code.extended_input_schema, []);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
test('input includes request and response sub-objects', () => {
|
|
786
|
+
const code = lib.apiTriggerCode();
|
|
787
|
+
assert.ok(code.input.request);
|
|
788
|
+
assert.ok(code.input.response);
|
|
789
|
+
assert.equal(code.input.request.content_type, 'json');
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// ── apiTriggerConfig ──────────────────────────────────────────────────────────
|
|
794
|
+
|
|
795
|
+
describe('apiTriggerConfig', () => {
|
|
796
|
+
test('returns array with exactly one entry', () => {
|
|
797
|
+
assert.equal(lib.apiTriggerConfig().length, 1);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
test('entry has correct provider and keyword', () => {
|
|
801
|
+
const [entry] = lib.apiTriggerConfig();
|
|
802
|
+
assert.equal(entry.provider, 'workato_api_platform');
|
|
803
|
+
assert.equal(entry.keyword, 'application');
|
|
804
|
+
assert.equal(entry.account_id, null);
|
|
805
|
+
assert.equal(entry.skip_validation, false);
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// ── findStep — edge cases ─────────────────────────────────────────────────────
|
|
810
|
+
|
|
811
|
+
describe('findStep — edge cases', () => {
|
|
812
|
+
test('does not throw when a step has no block property', () => {
|
|
813
|
+
const block = [
|
|
814
|
+
{ as: 'aaa', keyword: 'action' }, // no .block
|
|
815
|
+
{ as: 'bbb', keyword: 'action' },
|
|
816
|
+
];
|
|
817
|
+
assert.doesNotThrow(() => lib.findStep(block, 'zzz'));
|
|
818
|
+
assert.equal(lib.findStep(block, 'zzz'), null);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test('finds first match when same as appears multiple times (degenerate)', () => {
|
|
822
|
+
const block = [
|
|
823
|
+
{ as: 'dup', keyword: 'action', value: 1 },
|
|
824
|
+
{ as: 'dup', keyword: 'action', value: 2 },
|
|
825
|
+
];
|
|
826
|
+
assert.equal(lib.findStep(block, 'dup').value, 1);
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// ── deepMerge — edge cases ────────────────────────────────────────────────────
|
|
831
|
+
|
|
832
|
+
describe('deepMerge — edge cases', () => {
|
|
833
|
+
test('null source value replaces target value', () => {
|
|
834
|
+
const target = { a: 'something' };
|
|
835
|
+
lib.deepMerge(target, { a: null });
|
|
836
|
+
assert.equal(target.a, null);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
test('empty source object leaves target unchanged', () => {
|
|
840
|
+
const target = { a: 1, b: 2 };
|
|
841
|
+
lib.deepMerge(target, {});
|
|
842
|
+
assert.deepEqual(target, { a: 1, b: 2 });
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
test('array in target is fully replaced when source has same key as array', () => {
|
|
846
|
+
const target = { steps: [{ as: 'x' }] };
|
|
847
|
+
lib.deepMerge(target, { steps: [] });
|
|
848
|
+
assert.deepEqual(target.steps, []);
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// ── apiPut error path ─────────────────────────────────────────────────────────
|
|
853
|
+
|
|
854
|
+
describe('apiPut — error handling', () => {
|
|
855
|
+
test('throws with status code on non-ok response', async () => {
|
|
856
|
+
global.fetch = async () => ({ ok: false, status: 409, text: async () => 'Conflict' });
|
|
857
|
+
await assert.rejects(() => lib.apiPut('/recipes/1', {}), /409/);
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// ── cmdListRecipes — multiple filters ────────────────────────────────────────
|
|
862
|
+
|
|
863
|
+
describe('cmdListRecipes — multiple filters', () => {
|
|
864
|
+
test('appends folder_id and page together', async () => {
|
|
865
|
+
let calledUrl;
|
|
866
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
867
|
+
await lib.cmdListRecipes({ folder_id: '42', page: '3' });
|
|
868
|
+
assert.ok(calledUrl.includes('folder_id=42'), `URL: ${calledUrl}`);
|
|
869
|
+
assert.ok(calledUrl.includes('page=3'), `URL: ${calledUrl}`);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
test('appends project_id and per_page together', async () => {
|
|
873
|
+
let calledUrl;
|
|
874
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
875
|
+
await lib.cmdListRecipes({ project_id: '14318', per_page: '50' });
|
|
876
|
+
assert.ok(calledUrl.includes('project_id=14318'), `URL: ${calledUrl}`);
|
|
877
|
+
assert.ok(calledUrl.includes('per_page=50'), `URL: ${calledUrl}`);
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
// ── cmdGetJobs — combined filters ────────────────────────────────────────────
|
|
882
|
+
|
|
883
|
+
describe('cmdGetJobs — combined filters', () => {
|
|
884
|
+
test('appends both per_page and status', async () => {
|
|
885
|
+
let calledUrl;
|
|
886
|
+
global.fetch = async (url) => { calledUrl = url; return { ok: true, json: async () => ({}) }; };
|
|
887
|
+
await lib.cmdGetJobs('167603', { limit: '5', status: 'succeeded' });
|
|
888
|
+
assert.ok(calledUrl.includes('per_page=5'), `URL: ${calledUrl}`);
|
|
889
|
+
assert.ok(calledUrl.includes('status=succeeded'), `URL: ${calledUrl}`);
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// ── error propagation from write commands ─────────────────────────────────────
|
|
894
|
+
|
|
895
|
+
describe('error propagation', () => {
|
|
896
|
+
test('cmdStart propagates API error', async () => {
|
|
897
|
+
global.fetch = async () => ({ ok: false, status: 500, text: async () => 'Internal Server Error' });
|
|
898
|
+
await assert.rejects(() => lib.cmdStart('123'), /500/);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
test('cmdStop propagates API error', async () => {
|
|
902
|
+
global.fetch = async () => ({ ok: false, status: 422, text: async () => 'Unprocessable' });
|
|
903
|
+
await assert.rejects(() => lib.cmdStop('123'), /422/);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
test('cmdDelete propagates API error', async () => {
|
|
907
|
+
global.fetch = async () => ({ ok: false, status: 403, text: async () => 'Forbidden' });
|
|
908
|
+
await assert.rejects(() => lib.cmdDelete('123'), /403/);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
test('cmdPutCode propagates API error', async () => {
|
|
912
|
+
const origRead = require('fs').readFileSync;
|
|
913
|
+
try {
|
|
914
|
+
require('fs').readFileSync = () => JSON.stringify({ as: 'x', block: [] });
|
|
915
|
+
global.fetch = async () => ({ ok: false, status: 400, text: async () => 'Bad Request' });
|
|
916
|
+
await assert.rejects(() => lib.cmdPutCode('123', 'code.json'), /400/);
|
|
917
|
+
} finally {
|
|
918
|
+
require('fs').readFileSync = origRead;
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
test('cmdCreate propagates API error', async () => {
|
|
923
|
+
const origRead = require('fs').readFileSync;
|
|
924
|
+
try {
|
|
925
|
+
require('fs').readFileSync = () => JSON.stringify({ as: 'x' });
|
|
926
|
+
global.fetch = async () => ({ ok: false, status: 422, text: async () => 'Unprocessable' });
|
|
927
|
+
await assert.rejects(() => lib.cmdCreate('Bad Recipe', 'code.json'), /422/);
|
|
928
|
+
} finally {
|
|
929
|
+
require('fs').readFileSync = origRead;
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
test('cmdGet propagates API error', async () => {
|
|
934
|
+
global.fetch = async () => ({ ok: false, status: 404, text: async () => 'Not Found' });
|
|
935
|
+
await assert.rejects(() => lib.cmdGet('9999999'), /404/);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test('cmdListRecipes propagates API error', async () => {
|
|
939
|
+
global.fetch = async () => ({ ok: false, status: 401, text: async () => 'Unauthorized' });
|
|
940
|
+
await assert.rejects(() => lib.cmdListRecipes(), /401/);
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
// ── cmdUpdateStep — deeply nested step ────────────────────────────────────────
|
|
945
|
+
|
|
946
|
+
describe('cmdUpdateStep — deeply nested step', () => {
|
|
947
|
+
test('updates a step nested three levels deep', async () => {
|
|
948
|
+
const code = {
|
|
949
|
+
as: 'trig', keyword: 'trigger', input: {},
|
|
950
|
+
block: [
|
|
951
|
+
{
|
|
952
|
+
as: 'foreach1', keyword: 'foreach', input: {},
|
|
953
|
+
block: [
|
|
954
|
+
{
|
|
955
|
+
as: 'if1', keyword: 'if', input: {},
|
|
956
|
+
block: [
|
|
957
|
+
{ as: 'deepact', keyword: 'action', input: { x: 1 } },
|
|
958
|
+
],
|
|
959
|
+
},
|
|
960
|
+
],
|
|
961
|
+
},
|
|
962
|
+
],
|
|
963
|
+
};
|
|
964
|
+
const patch = { input: { x: 99 } };
|
|
965
|
+
const origRead = require('fs').readFileSync;
|
|
966
|
+
try {
|
|
967
|
+
require('fs').readFileSync = () => JSON.stringify(patch);
|
|
968
|
+
let putBody;
|
|
969
|
+
global.fetch = async (url, opts) => {
|
|
970
|
+
if (opts?.method === 'PUT') {
|
|
971
|
+
putBody = JSON.parse(opts.body);
|
|
972
|
+
return { ok: true, json: async () => ({}) };
|
|
973
|
+
}
|
|
974
|
+
return { ok: true, json: async () => ({ recipe: { code: JSON.stringify(code) } }) };
|
|
975
|
+
};
|
|
976
|
+
await lib.cmdUpdateStep('123', 'deepact', 'patch.json');
|
|
977
|
+
const updated = JSON.parse(putBody.recipe.code);
|
|
978
|
+
assert.equal(updated.block[0].block[0].block[0].input.x, 99);
|
|
979
|
+
} finally {
|
|
980
|
+
require('fs').readFileSync = origRead;
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
// Restore console at end
|
|
986
|
+
process.on('exit', () => {
|
|
987
|
+
console.log = origLog;
|
|
988
|
+
console.error = origError;
|
|
989
|
+
});
|