ti2 1.0.95 → 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.
- package/controllers/README.md +70 -1
- package/controllers/__tests__/bookings-searchProducts.js +533 -0
- package/controllers/__tests__/bookings.js +28 -276
- package/controllers/app.js +11 -8
- package/controllers/bookings.js +188 -108
- package/package.json +1 -1
- package/test/utils.js +36 -35
- package/worker/__tests__/job-plugin.js +207 -0
- package/worker/index.js +193 -121
- package/worker/queue.js +1 -0
|
@@ -30,12 +30,17 @@ describe('user: bookings controller', () => {
|
|
|
30
30
|
let doApiPost;
|
|
31
31
|
let plugins;
|
|
32
32
|
let userToken;
|
|
33
|
+
let appSetup;
|
|
34
|
+
let createUserToken;
|
|
35
|
+
|
|
33
36
|
beforeAll(async () => {
|
|
34
37
|
({
|
|
35
38
|
doApiDelete,
|
|
36
39
|
doApiGet,
|
|
37
40
|
doApiPost,
|
|
38
41
|
plugins,
|
|
42
|
+
appSetup,
|
|
43
|
+
createUserToken,
|
|
39
44
|
} = await testUtils({
|
|
40
45
|
plugins: [newApp.name],
|
|
41
46
|
}));
|
|
@@ -55,40 +60,25 @@ describe('user: bookings controller', () => {
|
|
|
55
60
|
pluginName: appKey,
|
|
56
61
|
key: `${cacheKey}:lock`,
|
|
57
62
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
token: adminKey,
|
|
62
|
-
payload: newApp,
|
|
63
|
-
});
|
|
64
|
-
expect(apiKey).toBeTruthy();
|
|
65
|
-
|
|
66
|
-
// Create the user token
|
|
67
|
-
({ value: userToken } = await doApiPost({
|
|
68
|
-
url: '/user',
|
|
69
|
-
token: adminKey,
|
|
70
|
-
payload: { userId },
|
|
71
|
-
}));
|
|
63
|
+
|
|
64
|
+
// Create user token
|
|
65
|
+
userToken = createUserToken(userId);
|
|
72
66
|
expect(userToken).toBeTruthy();
|
|
73
|
-
|
|
67
|
+
|
|
68
|
+
// Create the user
|
|
74
69
|
await doApiPost({
|
|
75
70
|
url: '/user',
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
({ value: userToken } = await doApiPost({
|
|
81
|
-
url: '/user',
|
|
82
|
-
token: adminKey,
|
|
83
|
-
payload: { userId },
|
|
84
|
-
}));
|
|
85
|
-
expect(userToken).toBeTruthy();
|
|
71
|
+
token: adminKey,
|
|
72
|
+
payload: { userId, email: `${userId}@example.com` },
|
|
73
|
+
});
|
|
74
|
+
|
|
86
75
|
// create the travelGage+User setup
|
|
87
76
|
let userAppKeys = [];
|
|
88
77
|
({ userAppKeys } = await doApiGet({
|
|
89
78
|
url: `/user/${userId}/apps`,
|
|
90
79
|
token: userToken,
|
|
91
80
|
}));
|
|
81
|
+
|
|
92
82
|
// Check if we need to create the integration
|
|
93
83
|
if (!userAppKeys.find(e => e.hint === newIntegration.tokenHint
|
|
94
84
|
&& e.integrationId === newApp.name)) {
|
|
@@ -96,268 +86,30 @@ describe('user: bookings controller', () => {
|
|
|
96
86
|
const { value } = await doApiPost({
|
|
97
87
|
url: `/${appKey}/${userId}`,
|
|
98
88
|
token: userToken,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
89
|
+
payload: newIntegration,
|
|
90
|
+
});
|
|
91
|
+
expect(value).toBeTruthy();
|
|
92
|
+
}
|
|
103
93
|
});
|
|
104
94
|
beforeEach(async () => {
|
|
105
95
|
jest.clearAllMocks();
|
|
106
96
|
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
bookingId: '', supplierBookingId: '', name: '',
|
|
110
|
-
};
|
|
111
|
-
const { bookings } = await doApiPost({
|
|
112
|
-
url: `/bookings/${appKey}/${userId}/testingToken/search`,
|
|
113
|
-
token: userToken,
|
|
114
|
-
payload,
|
|
115
|
-
});
|
|
116
|
-
expect(plugins[0].searchHotelBooking).toHaveBeenCalled();
|
|
117
|
-
expect(Array.isArray(bookings)).toBeTruthy();
|
|
118
|
-
expect(plugins[0].searchHotelBooking.mock.calls[0][0].payload).toEqual(payload);
|
|
119
|
-
expect(plugins[0].searchHotelBooking.mock.calls[0][0].token).toEqual(token);
|
|
120
|
-
|
|
121
|
-
// expect(Array.isArray(retVal.bookings)).toBeTruthy();
|
|
122
|
-
// expect(retVal.bookings.length > 0).toBeTruthy();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe('searchProducts', () => {
|
|
126
|
-
it('should be able to get bookings products for most users without special setup: no cache', async () => {
|
|
127
|
-
const payload = {};
|
|
128
|
-
const { products } = await doApiPost({
|
|
129
|
-
url: `/products/${appKey}/${userId}/testingToken/search`,
|
|
130
|
-
token: userToken,
|
|
131
|
-
payload,
|
|
132
|
-
});
|
|
133
|
-
expect(plugins[0].searchProducts).toHaveBeenCalled();
|
|
134
|
-
expect(Array.isArray(products)).toBeTruthy();
|
|
135
|
-
expect(products.length).toBe(2);
|
|
136
|
-
expect(products[0].options.length).toBe(1);
|
|
137
|
-
expect(products[1].options.length).toBe(2);
|
|
138
|
-
});
|
|
139
|
-
it('should be able to get booking products: no cache, forceRefresh', async () => {
|
|
140
|
-
// NOTE: we need to remove the cache first
|
|
141
|
-
const cacheKey = hash({
|
|
142
|
-
appKey,
|
|
143
|
-
userId,
|
|
144
|
-
hint: 'testingToken',
|
|
145
|
-
operationId: 'bookingsProductSearch',
|
|
146
|
-
});
|
|
147
|
-
await cache.drop({
|
|
148
|
-
pluginName: appKey,
|
|
149
|
-
key: cacheKey,
|
|
150
|
-
});
|
|
97
|
+
describe('bookings', ()=> {
|
|
98
|
+
it('should be able to search a for a booking', async () => {
|
|
151
99
|
const payload = {
|
|
152
|
-
|
|
100
|
+
bookingId: '', supplierBookingId: '', name: '',
|
|
153
101
|
};
|
|
154
|
-
const {
|
|
155
|
-
url: `/
|
|
102
|
+
const { bookings } = await doApiPost({
|
|
103
|
+
url: `/bookings/${appKey}/${userId}/testingToken/search`,
|
|
156
104
|
token: userToken,
|
|
157
105
|
payload,
|
|
158
106
|
});
|
|
159
|
-
expect(plugins[0].
|
|
160
|
-
expect(Array.isArray(
|
|
161
|
-
expect(
|
|
162
|
-
expect(
|
|
163
|
-
expect(products[1].options.length).toBe(2);
|
|
164
|
-
expect(plugins[0].searchProducts.mock.calls[0][0].token).toEqual(token);
|
|
107
|
+
expect(plugins[0].searchHotelBooking).toHaveBeenCalled();
|
|
108
|
+
expect(Array.isArray(bookings)).toBeTruthy();
|
|
109
|
+
expect(plugins[0].searchHotelBooking.mock.calls[0][0].payload).toEqual(payload);
|
|
110
|
+
expect(plugins[0].searchHotelBooking.mock.calls[0][0].token).toEqual(token);
|
|
165
111
|
});
|
|
166
|
-
it('should be able to get booking products: using cache', async () => {
|
|
167
|
-
const payload = {};
|
|
168
|
-
const { products } = await doApiPost({
|
|
169
|
-
url: `/products/${appKey}/${userId}/testingToken/search`,
|
|
170
|
-
token: userToken,
|
|
171
|
-
payload,
|
|
172
|
-
});
|
|
173
|
-
expect(plugins[0].searchProducts).not.toHaveBeenCalled();
|
|
174
|
-
expect(Array.isArray(products)).toBeTruthy();
|
|
175
|
-
expect(products.length).toBe(2);
|
|
176
|
-
expect(products[0].options.length).toBe(1);
|
|
177
|
-
expect(products[1].options.length).toBe(2);
|
|
178
|
-
});
|
|
179
|
-
it('should be able to get booking products with searchInput', async () => {
|
|
180
112
|
|
|
181
|
-
const payload = {
|
|
182
|
-
searchInput: 'Transfer from Sydney Harbor to Hilton Hotel',
|
|
183
|
-
};
|
|
184
|
-
const { products } = await doApiPost({
|
|
185
|
-
url: `/products/${appKey}/${userId}/testingToken/search`,
|
|
186
|
-
token: userToken,
|
|
187
|
-
payload,
|
|
188
|
-
});
|
|
189
|
-
expect(plugins[0].searchProducts).not.toHaveBeenCalled();
|
|
190
|
-
expect(Array.isArray(products)).toBeTruthy();
|
|
191
|
-
expect(products.length).toBe(1);
|
|
192
|
-
expect(products[0].productName).toBe('Davids');
|
|
193
|
-
expect(products[0].options.length).toBe(1);
|
|
194
|
-
expect(products[0].options[0].optionName).toBe('Transfer from Sydney Harbor Bridge to Hilton Hotel');
|
|
195
|
-
});
|
|
196
|
-
it('should be able to process doNotCallPluginForProducts in token', async () => {
|
|
197
|
-
const { userAppKeys } = await doApiGet({
|
|
198
|
-
url: `/user/${userId}/apps`,
|
|
199
|
-
token: userToken,
|
|
200
|
-
});
|
|
201
|
-
let newnewIntegration = userAppKeys.find(e => e.hint === 'hint_for_doNotCallPluginForProducts'
|
|
202
|
-
&& e.integrationId === newApp.name);
|
|
203
|
-
if (!newnewIntegration) {
|
|
204
|
-
const newIntegrationContent = {
|
|
205
|
-
endpoint: 'https://api.travelgatex.com',
|
|
206
|
-
apiKey: chance.guid(),
|
|
207
|
-
client: 'tourconnect',
|
|
208
|
-
doNotCallPluginForProducts: true,
|
|
209
|
-
};
|
|
210
|
-
// create a new user token with the new content
|
|
211
|
-
newnewIntegration = await doApiPost({
|
|
212
|
-
url: `/travelgate/${userId}`,
|
|
213
|
-
token: adminKey,
|
|
214
|
-
payload: {
|
|
215
|
-
token: newIntegrationContent,
|
|
216
|
-
tokenHint: 'hint_for_doNotCallPluginForProducts',
|
|
217
|
-
},
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
expect(newnewIntegration).toBeTruthy();
|
|
221
|
-
// first time call, no cache expect no plugin call and empty products
|
|
222
|
-
let products;
|
|
223
|
-
await doApiPost({
|
|
224
|
-
url: `/products/${appKey}/${userId}/hint_for_doNotCallPluginForProducts/search`,
|
|
225
|
-
token: userToken,
|
|
226
|
-
payload: {},
|
|
227
|
-
}).then(({ products: p }) => {
|
|
228
|
-
products = p;
|
|
229
|
-
});
|
|
230
|
-
expect(plugins[0].searchProducts).not.toHaveBeenCalled();
|
|
231
|
-
expect(Array.isArray(products)).toBeTruthy();
|
|
232
|
-
expect(products.length).toBe(0);
|
|
233
|
-
// second time call, forceRefresh and still expect plugin call
|
|
234
|
-
await doApiPost({
|
|
235
|
-
url: `/products/${appKey}/${userId}/hint_for_doNotCallPluginForProducts/search`,
|
|
236
|
-
token: userToken,
|
|
237
|
-
payload: { forceRefresh: true },
|
|
238
|
-
}).then(({ products: p }) => {
|
|
239
|
-
products = p;
|
|
240
|
-
});
|
|
241
|
-
expect(plugins[0].searchProducts).toHaveBeenCalled();
|
|
242
|
-
expect(Array.isArray(products)).toBeTruthy();
|
|
243
|
-
expect(products.length).toBe(2);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
describe('cache TTR and lock mechanism', () => {
|
|
247
|
-
const shortTTRToken = {
|
|
248
|
-
endpoint: 'https://api.travelgatex.com',
|
|
249
|
-
apiKey: chance.guid(),
|
|
250
|
-
client: 'tourconnect',
|
|
251
|
-
ttlForProducts: 2, // 2 seconds TTR
|
|
252
|
-
};
|
|
253
|
-
beforeAll(async () => {
|
|
254
|
-
await doApiPost({
|
|
255
|
-
url: `/${appKey}/${userId}`,
|
|
256
|
-
token: userToken,
|
|
257
|
-
payload: {
|
|
258
|
-
tokenHint: 'ttr-test',
|
|
259
|
-
token: shortTTRToken,
|
|
260
|
-
},
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
beforeEach(async () => {
|
|
264
|
-
jest.clearAllMocks();
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it('should use cache within TTR period', async () => {
|
|
268
|
-
await doApiPost({
|
|
269
|
-
url: `/products/${appKey}/${userId}/ttr-test/search`,
|
|
270
|
-
token: userToken,
|
|
271
|
-
payload: {},
|
|
272
|
-
});
|
|
273
|
-
expect(plugins[0].searchProducts).toHaveBeenCalledTimes(1);
|
|
274
|
-
|
|
275
|
-
// Second immediate call should use cache
|
|
276
|
-
await doApiPost({
|
|
277
|
-
url: `/products/${appKey}/${userId}/ttr-test/search`,
|
|
278
|
-
token: userToken,
|
|
279
|
-
payload: {},
|
|
280
|
-
});
|
|
281
|
-
expect(plugins[0].searchProducts).toHaveBeenCalledTimes(1); // Still 1, used cache
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('should refresh cache after TTR expires', async () => {
|
|
285
|
-
// Wait for TTR to expire (2 seconds + buffer) so the cache from previous test is stale
|
|
286
|
-
await new Promise(resolve => {
|
|
287
|
-
setTimeout(resolve, 2100);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
// First call to populate cache
|
|
291
|
-
await doApiPost({
|
|
292
|
-
url: `/products/${appKey}/${userId}/ttr-test/search`,
|
|
293
|
-
token: userToken,
|
|
294
|
-
payload: {},
|
|
295
|
-
});
|
|
296
|
-
expect(plugins[0].searchProducts).toHaveBeenCalledTimes(1);
|
|
297
|
-
|
|
298
|
-
// Wait for TTR to expire (2 seconds + buffer)
|
|
299
|
-
await new Promise(resolve => {
|
|
300
|
-
setTimeout(resolve, 2100);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// Call after TTR expired should hit the plugin again
|
|
304
|
-
await doApiPost({
|
|
305
|
-
url: `/products/${appKey}/${userId}/ttr-test/search`,
|
|
306
|
-
token: userToken,
|
|
307
|
-
payload: {},
|
|
308
|
-
});
|
|
309
|
-
expect(plugins[0].searchProducts).toHaveBeenCalledTimes(2);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it('should use lock mechanism to prevent concurrent calls', async () => {
|
|
313
|
-
// Wait for TTR to expire (2 seconds + buffer) so the cache from previous test is stale
|
|
314
|
-
await new Promise(resolve => {
|
|
315
|
-
setTimeout(resolve, 2100);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Make multiple requests with slight delays to simulate real-world concurrent requests
|
|
319
|
-
const makeRequest = () => doApiPost({
|
|
320
|
-
url: `/products/${appKey}/${userId}/ttr-test/search`,
|
|
321
|
-
token: userToken,
|
|
322
|
-
payload: {},
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
// Start first request
|
|
326
|
-
const request1 = makeRequest();
|
|
327
|
-
// Start second request after 10ms
|
|
328
|
-
await new Promise(resolve => {
|
|
329
|
-
setTimeout(resolve, 10);
|
|
330
|
-
});
|
|
331
|
-
const request2 = makeRequest();
|
|
332
|
-
// Start third request after another 10ms
|
|
333
|
-
await new Promise(resolve => {
|
|
334
|
-
setTimeout(resolve, 10);
|
|
335
|
-
});
|
|
336
|
-
const request3 = makeRequest();
|
|
337
|
-
|
|
338
|
-
// Wait for all requests to complete
|
|
339
|
-
const results = await Promise.all([request1, request2, request3]);
|
|
340
|
-
|
|
341
|
-
// All requests should return valid products
|
|
342
|
-
results.forEach(({ products }) => {
|
|
343
|
-
expect(Array.isArray(products)).toBeTruthy();
|
|
344
|
-
expect(products.length).toBe(2); // Based on existing test expectations
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
// Plugin should only be called once due to lock mechanism, and with correct context
|
|
348
|
-
expect(plugins[0].searchProducts).toHaveBeenCalledWith(
|
|
349
|
-
expect.objectContaining({
|
|
350
|
-
token: expect.objectContaining({
|
|
351
|
-
client: 'tourconnect',
|
|
352
|
-
endpoint: expect.stringContaining('https://api.travelgatex.com'),
|
|
353
|
-
}),
|
|
354
|
-
userId: expect.stringContaining(userId),
|
|
355
|
-
payload: {},
|
|
356
|
-
typeDefsAndQueries: expect.any(Object)
|
|
357
|
-
})
|
|
358
|
-
);
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
113
|
});
|
|
362
114
|
|
|
363
115
|
it('should be able to search for availabiility', async () => {
|
package/controllers/app.js
CHANGED
|
@@ -142,25 +142,28 @@ const createAppToken = async (req, res, next) => {
|
|
|
142
142
|
if (existing) {
|
|
143
143
|
const bullJob = await queue.getJob(job.bullJobId);
|
|
144
144
|
if (!bullJob) {
|
|
145
|
-
|
|
146
|
-
existing.bullJobId =
|
|
145
|
+
let rawBullJobId = await addJob(jobPayload, jobParams);
|
|
146
|
+
existing.bullJobId = (rawBullJobId && typeof rawBullJobId === 'object' && rawBullJobId.id) ? rawBullJobId.id : rawBullJobId;
|
|
147
147
|
await existing.save();
|
|
148
148
|
} else {
|
|
149
149
|
// make sure the cron is the same
|
|
150
|
+
// bullJobId here is from existing.bullJobId, which should be a string.
|
|
151
|
+
// The queue.getJob expects a string ID.
|
|
150
152
|
const bullCron = R.path(
|
|
151
153
|
['opts', 'repeat', 'cron'],
|
|
152
|
-
await queue.getJob(bullJobId),
|
|
154
|
+
await queue.getJob(existing.bullJobId), // Use existing.bullJobId
|
|
153
155
|
);
|
|
154
156
|
if (bullCron !== job.cron) {
|
|
155
|
-
await queue.removeJobs(bullJobId);
|
|
156
|
-
|
|
157
|
-
existing.bullJobId =
|
|
157
|
+
await queue.removeJobs(existing.bullJobId); // Use existing.bullJobId
|
|
158
|
+
let rawBullJobId = await addJob(jobPayload, jobParams);
|
|
159
|
+
existing.bullJobId = (rawBullJobId && typeof rawBullJobId === 'object' && rawBullJobId.id) ? rawBullJobId.id : rawBullJobId;
|
|
158
160
|
await existing.save();
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
} else {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
+
let rawBullJobId = await addJob(jobPayload, jobParams);
|
|
165
|
+
const actualBullJobId = (rawBullJobId && typeof rawBullJobId === 'object' && rawBullJobId.id) ? rawBullJobId.id : rawBullJobId;
|
|
166
|
+
await sqldb.CronJobs.create({ ...where, bullJobId: actualBullJobId });
|
|
164
167
|
}
|
|
165
168
|
});
|
|
166
169
|
}
|