ti2 1.0.94 → 1.0.96

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.
@@ -0,0 +1,394 @@
1
+ /* globals beforeAll describe it expect */
2
+ const yaml = require('js-yaml');
3
+ const fs = require('fs');
4
+
5
+ // Load environment variables and API schema
6
+ const { env: { adminKey } } = process;
7
+
8
+ describe('cronjobs', () => {
9
+ const testUtils = require('../../test/utils');
10
+ // API helper functions
11
+ let doApiPost;
12
+ let doApiGet;
13
+ let doApiDelete;
14
+
15
+ // Test data
16
+ let appName;
17
+ let appKey;
18
+ let userId;
19
+ let userToken;
20
+ let userSetup;
21
+ let otherUserId;
22
+ let otherUserToken;
23
+ let otherUserSetup;
24
+ let createdAdminJobId; // To store the ID of the job created by admin
25
+ let createdUserJobId; // To store the ID of the job created by user
26
+
27
+ // Constants
28
+ const cron = '0 0 * * *';
29
+ const testApiPayload = ({ appKey, userId, hint }, bodyParam = {}) => ({
30
+ url: `/products/${appKey}/${userId}/${hint}/search`,
31
+ method: 'post',
32
+ body: { ...bodyParam },
33
+ cron,
34
+ });
35
+
36
+ beforeAll(async () => {
37
+ const {
38
+ doApiPost: post,
39
+ doApiGet: get,
40
+ doApiDelete: del,
41
+ createUserToken,
42
+ appSetup
43
+ } = await testUtils();
44
+
45
+ // Use appSetup to create app and user
46
+ userSetup = await appSetup();
47
+ appName = userSetup.newApp.name;
48
+ appKey = userSetup.appKey;
49
+ userId = userSetup.userId;
50
+
51
+ // Create user token
52
+ userToken = createUserToken(userId);
53
+
54
+ // Create another user for cross-user permission tests under the same app
55
+ otherUserSetup = await appSetup({ appName: appName });
56
+ otherUserId = otherUserSetup.userId;
57
+
58
+ // Create token for other user
59
+ otherUserToken = createUserToken(otherUserId);
60
+
61
+ doApiPost = post;
62
+ doApiGet = get;
63
+ doApiDelete = del;
64
+ });
65
+
66
+ it('should create a new cronjob as admin', async () => {
67
+ const newJobPayload = testApiPayload(userSetup);
68
+ const response = await doApiPost({
69
+ url: `/cronjobs/${userId}`,
70
+ token: adminKey,
71
+ payload: {...newJobPayload},
72
+ });
73
+
74
+ expect(response).toEqual(
75
+ expect.objectContaining({...newJobPayload}),
76
+ );
77
+ createdAdminJobId = response.id; // Changed from bullJobId to id
78
+ expect(createdAdminJobId).toBeTruthy(); // Check the new id variable
79
+ });
80
+
81
+ it('should create a new cronjob as user', async () => {
82
+ const response = await doApiPost({
83
+ url: `/cronjobs/${userId}`,
84
+ token: userToken,
85
+ payload: {...testApiPayload(userSetup)},
86
+ });
87
+
88
+ expect(response).toEqual(
89
+ expect.objectContaining({...testApiPayload(userSetup)}),
90
+ );
91
+ createdUserJobId = response.id; // Changed from bullJobId to id
92
+ expect(createdUserJobId).toBeTruthy(); // Check the new id variable
93
+ });
94
+
95
+ it('should list cronjobs as admin', async () => {
96
+ const { jobs } = await doApiGet({
97
+ url: `/cronjobs/${userId}`,
98
+ token: adminKey,
99
+ });
100
+
101
+ expect(Array.isArray(jobs)).toBe(true);
102
+ // Check for admin job
103
+ expect(jobs).toEqual(
104
+ expect.arrayContaining([
105
+ expect.objectContaining({
106
+ ...testApiPayload(userSetup),
107
+ userId,
108
+ id: createdAdminJobId,
109
+ }),
110
+ ]),
111
+ );
112
+ // Check for user job
113
+ expect(jobs).toEqual(
114
+ expect.arrayContaining([
115
+ expect.objectContaining({
116
+ ...testApiPayload(userSetup),
117
+ userId,
118
+ id: createdUserJobId,
119
+ }),
120
+ ]),
121
+ );
122
+ });
123
+
124
+ it('should list cronjobs as user', async () => {
125
+ const { jobs } = await doApiGet({
126
+ url: `/cronjobs/${userId}`,
127
+ token: userToken,
128
+ });
129
+
130
+ expect(Array.isArray(jobs)).toBe(true);
131
+ // Check for admin job
132
+ expect(jobs).toEqual(
133
+ expect.arrayContaining([
134
+ expect.objectContaining({
135
+ ...testApiPayload(userSetup),
136
+ userId,
137
+ id: createdAdminJobId,
138
+ }),
139
+ ]),
140
+ );
141
+ // Check for user job
142
+ expect(jobs).toEqual(
143
+ expect.arrayContaining([
144
+ expect.objectContaining({
145
+ ...testApiPayload(userSetup),
146
+ userId,
147
+ id: createdUserJobId,
148
+ }),
149
+ ]),
150
+ );
151
+ });
152
+
153
+ it('should delete a cronjob as admin', async () => {
154
+ const response = await doApiDelete({
155
+ url: `/cronjobs/${userId}/${createdAdminJobId}`, // Use createdAdminJobId
156
+ token: adminKey,
157
+ });
158
+
159
+ expect(response).toEqual({ success: true });
160
+
161
+ // Wait for the database to refresh
162
+ await new Promise(resolve => setTimeout(resolve, 2e3));
163
+
164
+ // Verify it's deleted
165
+ const { jobs } = await doApiGet({
166
+ url: `/cronjobs/${userId}`,
167
+ token: adminKey,
168
+ });
169
+
170
+ // Verify the job is no longer in the list
171
+ const deletedJob = jobs.find(job => job.id === createdAdminJobId); // Check against id and createdAdminJobId
172
+ expect(deletedJob).toBeUndefined();
173
+ });
174
+
175
+ it('should delete a cronjob as user', async () => {
176
+ // First create a new cronjob ( new one since we used prev for other test)
177
+ const { id: newJobId } = await doApiPost({
178
+ url: `/cronjobs/${userId}`,
179
+ token: userToken,
180
+ payload: {...testApiPayload(userSetup)},
181
+ });
182
+
183
+ const response = await doApiDelete({
184
+ url: `/cronjobs/${userId}/${newJobId}`,
185
+ token: userToken,
186
+ });
187
+
188
+ expect(response).toEqual({ success: true });
189
+
190
+ // Wait for the database to refresh
191
+ await new Promise(resolve => setTimeout(resolve, 2e3));
192
+
193
+ // Verify it's deleted
194
+ const { jobs } = await doApiGet({
195
+ url: `/cronjobs/${userId}`,
196
+ token: adminKey,
197
+ });
198
+
199
+ // Check if the job is still in the list
200
+ const deletedJob = jobs.find(job => job.bullJobId === newJobId);
201
+ expect(deletedJob).toBeUndefined();
202
+ });
203
+
204
+ it('should fail when user tries to delete another users cronjob', async () => {
205
+ // Use the properly seeded otherUserId and otherUserToken
206
+ let otherJobId;
207
+
208
+ // First create a cronjob for the other user as an admin
209
+ const createResponse = await doApiPost({
210
+ url: `/cronjobs/${otherUserId}`,
211
+ token: adminKey,
212
+ payload: {...testApiPayload(otherUserSetup)},
213
+ });
214
+
215
+ otherJobId = createResponse.id;
216
+ expect(otherJobId).toBeTruthy();
217
+
218
+ // Now try to delete the other user's job with the first user's token
219
+ // This should fail with an error
220
+ try {
221
+ await doApiDelete({
222
+ url: `/cronjobs/${otherUserId}/${otherJobId}`,
223
+ token: userToken,
224
+ });
225
+ // If we get here, the test should fail
226
+ throw new Error('Should have failed with permission error');
227
+ } catch (error) {
228
+ // Verify we got an error response
229
+ expect(error.response).toBeDefined();
230
+ // The exact status code might be 401 or 403 depending on implementation
231
+ expect([401, 403]).toContain(error.response.status);
232
+ }
233
+
234
+ // Verify the job still exists (using admin token for consistency)
235
+ const { jobs } = await doApiGet({
236
+ url: `/cronjobs/${otherUserId}`,
237
+ token: adminKey,
238
+ });
239
+
240
+ const otherJob = jobs.find(job => job.id === otherJobId);
241
+ expect(otherJob).toBeDefined();
242
+ });
243
+
244
+ it('should fail to create cronjob with invalid url', async () => {
245
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
246
+
247
+ try {
248
+ await doApiPost({
249
+ url: `/cronjobs/${userId}`,
250
+ token: adminKey,
251
+ payload: {
252
+ method: 'GET',
253
+ url: '/invalid/path',
254
+ cron,
255
+ },
256
+ });
257
+ throw new Error('Should have failed');
258
+ } catch (error) {
259
+ expect(error.response.status).toBe(400);
260
+ expect(error.response.data.message).toContain('Invalid URL');
261
+ } finally {
262
+ consoleErrorSpy.mockRestore();
263
+ }
264
+ });
265
+
266
+ it('should fail to delete non-existent cronjob', async () => {
267
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
268
+ try {
269
+ await doApiDelete({
270
+ url: `/cronjobs/${userId}/nonexistent`,
271
+ token: adminKey,
272
+ });
273
+ throw new Error('Should have failed');
274
+ } catch (error) {
275
+ expect(error.response.status).toBe(404);
276
+ expect(error.response.data.message).toBe('Cronjob not found');
277
+ } finally {
278
+ consoleErrorSpy.mockRestore();
279
+ }
280
+ });
281
+
282
+ describe('scheduled cronjob execution', () => {
283
+ let bullJobId;
284
+ describe('create and remove job (via api)', () => {
285
+ let cronJobIdToCleanup; // To store the ID of the job for cleanup in afterAll
286
+ let server; // To hold the HTTP server instance for manual cleanup if needed
287
+
288
+ // Define port here to be accessible in afterAll for server closing
289
+ const testPort = 44294;
290
+
291
+ afterAll(async () => {
292
+ if (cronJobIdToCleanup) {
293
+ try {
294
+ await doApiDelete({
295
+ url: `/cronjobs/${userId}/${cronJobIdToCleanup}`,
296
+ token: adminKey,
297
+ });
298
+ } catch (e) {
299
+ console.error(`Failed to cleanup cronjob ${cronJobIdToCleanup} in afterAll: ${e.message}`);
300
+ }
301
+ }
302
+ // Ensure server is closed
303
+ if (server && server.listening) {
304
+ await new Promise(resolve => server.close(resolve));
305
+ }
306
+ });
307
+
308
+ it('should create, execute, and verify a scheduled cronjob', async () => {
309
+ const cronExpression = '* * * * *'; // Every minute
310
+ const uniqueId = `${(new Date()).getTime()}`;
311
+ let response; // To store the API response for creating the cronjob
312
+
313
+ const callBackServerPromise = new Promise((resolve, reject) => {
314
+ const http = require('http');
315
+ const url = require('url');
316
+
317
+ server = http.createServer((req, res) => {
318
+ let body = '';
319
+ req.on('data', chunk => body += chunk);
320
+ req.on('end', () => {
321
+ const requestUrl = url.parse(req.url, true);
322
+ const receivedUniqueId = requestUrl.query.date;
323
+
324
+ if (receivedUniqueId === uniqueId) {
325
+ // Check if response and response.bullJobId are available
326
+ if (response && response.bullJobId) {
327
+ res.writeHead(200, { 'Content-Type': 'application/json' });
328
+ res.end(JSON.stringify({ success: true }));
329
+ // server.close() will be handled in afterAll or if explicitly resolved/rejected
330
+ resolve({ id: response.bullJobId });
331
+ } else {
332
+ // This case handles if callback arrives but job creation might have failed or response not processed
333
+ res.writeHead(500, { 'Content-Type': 'application/json' });
334
+ res.end(JSON.stringify({ success: false, message: 'Callback received but job details unavailable.' }));
335
+ reject(new Error('Callback processed but original job details (response.bullJobId) were missing.'));
336
+ }
337
+ } else {
338
+ res.writeHead(400, { 'Content-Type': 'application/json' });
339
+ res.end(JSON.stringify({ success: false, message: 'Callback ID mismatch' }));
340
+ // Do not resolve or reject here, let the test timeout if correct callback never comes.
341
+ // Or, optionally, reject:
342
+ // reject(new Error('Callback ID mismatch.'));
343
+ }
344
+ });
345
+ });
346
+
347
+ server.on('error', (err) => {
348
+ reject(err); // Reject promise if server fails to start (e.g. port in use)
349
+ });
350
+
351
+ server.listen(testPort, '0.0.0.0', () => {
352
+ // console.log(`Test callback server listening on port ${testPort}`);
353
+ });
354
+ });
355
+ response = await doApiPost({
356
+ url: `/cronjobs/${userId}`,
357
+ token: adminKey,
358
+ payload: {
359
+ method: 'POST',
360
+ url: `/products/${appName}/${userId}/search`,
361
+ cron: cronExpression,
362
+ payload: {},
363
+ callbackUrl: `http://localhost:${testPort}/callback?date=${uniqueId}`,
364
+ removeOnComplete: true,
365
+ },
366
+ });
367
+
368
+ expect(response.bullJobId).toBeTruthy();
369
+ expect(response.id).toBeTruthy();
370
+ cronJobIdToCleanup = response.id;
371
+
372
+ const callBackInstance = await callBackServerPromise;
373
+ expect(callBackInstance.id).toBe(response.bullJobId);
374
+ bullJobId = response.bullJobId;
375
+
376
+ }, 120e3); // Timeout for the cronjob to be executed
377
+
378
+ })
379
+ it ('wait for queue job to be automatically removed', async() => {
380
+ expect(bullJobId).toBeTruthy();
381
+ const { queue } = require('../../worker/queue');
382
+ let jobInQueue;
383
+ const maxRetries = 60;
384
+ const retryInterval = 1e3;
385
+
386
+ for (let i = 0; i < maxRetries; i++) {
387
+ jobInQueue = await queue.getJob(bullJobId);
388
+ if (!jobInQueue) break;
389
+ await new Promise(resolveDelay => setTimeout(resolveDelay, retryInterval));
390
+ }
391
+ expect(jobInQueue).toBeNull();
392
+ }, 60e3) // timeout for bull to remove the queue Job once executed (removeOnComplete)
393
+ });
394
+ });
@@ -1,7 +1,7 @@
1
1
  /* global beforeAll, describe, it, expect */
2
- const testUtils = require('../../test/utils');
3
2
 
4
3
  describe('ping', () => {
4
+ const testUtils = require('../../test/utils');
5
5
  let doApiGet;
6
6
  beforeAll(async () => {
7
7
  ({
@@ -1,9 +1,9 @@
1
1
  /* globals beforeAll describe it expect jest beforeEach */
2
2
 
3
3
  const chance = require('chance').Chance();
4
- const testUtils = require('../../test/utils');
5
4
 
6
5
  describe('user: bookings controller - productSearch background job', () => {
6
+ const testUtils = require('../../test/utils');
7
7
  const newApp = {
8
8
  name: `travelgate-${chance.guid()}`,
9
9
  packageName: 'ti2-travelgate',
@@ -2,12 +2,12 @@
2
2
 
3
3
  const R = require('ramda');
4
4
  const chance = require('chance').Chance();
5
- const testUtils = require('../../test/utils');
6
5
  const slugify = require('../../test/slugify');
7
6
 
8
7
  const { env: { adminKey } } = process;
9
8
 
10
9
  describe('user', () => {
10
+ const testUtils = require('../../test/utils');
11
11
  const appName = slugify(
12
12
  chance.company(),
13
13
  ).toLowerCase();