@apogeelabs/the-agency 0.1.1
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/README.md +93 -0
- package/bin/cli.js +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +20 -0
- package/dist/manifest.d.ts +12 -0
- package/dist/manifest.js +24 -0
- package/dist/sync.d.ts +14 -0
- package/dist/sync.js +97 -0
- package/package.json +45 -0
- package/src/templates/.ai/UnitTestExamples.md +1148 -0
- package/src/templates/.ai/UnitTestGeneration.md +477 -0
- package/src/templates/.ai/workflow.md +59 -0
- package/src/templates/.claude/agents/architect.md +90 -0
- package/src/templates/.claude/agents/dev.md +79 -0
- package/src/templates/.claude/agents/explorer.md +93 -0
- package/src/templates/.claude/agents/pm.md +74 -0
- package/src/templates/.claude/agents/reviewer.md +93 -0
- package/src/templates/.claude/agents/test-hardener.md +121 -0
- package/src/templates/.claude/commands/architect.md +108 -0
- package/src/templates/.claude/commands/build.md +125 -0
- package/src/templates/.claude/commands/pm.md +72 -0
- package/src/templates/.claude/commands/review-pr.md +279 -0
- package/src/templates/.claude/settings.local.json +44 -0
|
@@ -0,0 +1,1148 @@
|
|
|
1
|
+
# Unit Test Examples
|
|
2
|
+
|
|
3
|
+
Reference examples for the style guide in `.ai/UnitTestGeneration.md`. Each example demonstrates the conventions and critical rules described there.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Example: SearchSessionRepository
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// SearchSessionRepository.test.ts
|
|
11
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
12
|
+
const collectionMockFn = jest.fn();
|
|
13
|
+
const getMongoDb = jest.fn().mockResolvedValue({
|
|
14
|
+
collection: collectionMockFn,
|
|
15
|
+
} as unknown as Db);
|
|
16
|
+
jest.mock("@apogee-travel/common-db", () => {
|
|
17
|
+
return {
|
|
18
|
+
getMongoDb,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
const mockObjectId = jest.fn();
|
|
22
|
+
jest.mock("mongodb", () => {
|
|
23
|
+
const originalModule = jest.requireActual("mongodb");
|
|
24
|
+
return {
|
|
25
|
+
__esModule: true,
|
|
26
|
+
...originalModule,
|
|
27
|
+
ObjectId: mockObjectId,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
const mockGetServicesFeatureFlagClient = jest.fn();
|
|
31
|
+
jest.mock("@apogee-travel/common-feature-flag", () => {
|
|
32
|
+
return { getServicesFeatureFlagClient: mockGetServicesFeatureFlagClient };
|
|
33
|
+
});
|
|
34
|
+
const mockAsyncLockWithTelemetry = jest.fn();
|
|
35
|
+
jest.mock("../../messaging/handlers/common/asyncLockWithMetrics", () => {
|
|
36
|
+
return {
|
|
37
|
+
AsyncLockWithTelemetry: mockAsyncLockWithTelemetry,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
jest.mock("../SearchTelemetry/searchTelemetryApi", () => {
|
|
41
|
+
return {
|
|
42
|
+
traceEvent: jest.fn(),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
import { Collection, Db } from "mongodb";
|
|
46
|
+
import { mock_logger } from "@apogee-travel/common-test";
|
|
47
|
+
import { SearchSessionStatus } from "@apogee-travel/common-types";
|
|
48
|
+
let repoInstance: any;
|
|
49
|
+
describe("services > hotel-availability > data > SearchSessionRepository", () => {
|
|
50
|
+
let fakeCollection: any,
|
|
51
|
+
fakeMongoInventoryItemId: any,
|
|
52
|
+
replaceOneResult: any,
|
|
53
|
+
flagClient: any,
|
|
54
|
+
findOneResult: any;
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
jest.clearAllMocks();
|
|
57
|
+
jest.resetModules();
|
|
58
|
+
flagClient = {
|
|
59
|
+
getNumberValue: jest.fn().mockResolvedValue(3600),
|
|
60
|
+
addHandler: jest.fn(),
|
|
61
|
+
};
|
|
62
|
+
mockGetServicesFeatureFlagClient.mockResolvedValue(flagClient);
|
|
63
|
+
mockObjectId.mockImplementation(() => {
|
|
64
|
+
return {
|
|
65
|
+
toHexString() {
|
|
66
|
+
return fakeMongoInventoryItemId;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
replaceOneResult = {
|
|
71
|
+
matchedCount: 1,
|
|
72
|
+
};
|
|
73
|
+
findOneResult = {
|
|
74
|
+
_id: "MONGO_ID",
|
|
75
|
+
asid: "a38c287c-15c3-42f0-863f-872af171982d",
|
|
76
|
+
status: "COMPLETED",
|
|
77
|
+
payload: {
|
|
78
|
+
asid: "a38c287c-15c3-42f0-863f-872af171982d",
|
|
79
|
+
stay: {
|
|
80
|
+
adults: 2,
|
|
81
|
+
children: 0,
|
|
82
|
+
rooms: 1,
|
|
83
|
+
checkIn: "2024-06-12",
|
|
84
|
+
checkOut: "2024-06-14",
|
|
85
|
+
},
|
|
86
|
+
place: {
|
|
87
|
+
coords: {
|
|
88
|
+
latitude: 28.5383832,
|
|
89
|
+
longitude: -81.3789269,
|
|
90
|
+
},
|
|
91
|
+
text: "Orlando, FL, USA",
|
|
92
|
+
},
|
|
93
|
+
radiusMiles: 20,
|
|
94
|
+
pagination: {
|
|
95
|
+
offset: 0,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
fakeCollection = {
|
|
100
|
+
findOne: jest.fn().mockResolvedValue(findOneResult),
|
|
101
|
+
replaceOne: jest.fn().mockResolvedValue(replaceOneResult),
|
|
102
|
+
createIndex: jest.fn().mockResolvedValue(undefined),
|
|
103
|
+
dropIndex: jest.fn().mockResolvedValue(undefined),
|
|
104
|
+
updateOne: jest.fn().mockResolvedValue(undefined),
|
|
105
|
+
};
|
|
106
|
+
collectionMockFn.mockImplementation(
|
|
107
|
+
(): Collection => fakeCollection as unknown as Collection
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
describe("with getSearchSessionRepository", () => {
|
|
111
|
+
describe("when the !MONGO_URL env var is missing", () => {
|
|
112
|
+
let expectedErr any;
|
|
113
|
+
beforeEach(async () => {
|
|
114
|
+
try {
|
|
115
|
+
const mod = await import("./SearchSessionRepository");
|
|
116
|
+
await mod.getSearchSessionRepository();
|
|
117
|
+
} catch (err) {
|
|
118
|
+
expectedErr = err;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
it("should throw an error", async () => {
|
|
122
|
+
expect((expectedErr).toString()).toMatch(
|
|
123
|
+
"Search Session Service error: missing MONGO_URL env var value"
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe("when things go splendiferously", () => {
|
|
128
|
+
beforeEach(async () => {
|
|
129
|
+
const mod = await import("./SearchSessionRepository");
|
|
130
|
+
process.env.MONGO_URL = "MONGO_URL";
|
|
131
|
+
repoInstance = await mod.getSearchSessionRepository();
|
|
132
|
+
});
|
|
133
|
+
it("should call through to getMongoDb on the first call", () => {
|
|
134
|
+
expect(getMongoDb).toHaveBeenCalledTimes(1);
|
|
135
|
+
expect(getMongoDb).toHaveBeenCalledWith("MONGO_URL", "hotel_availability");
|
|
136
|
+
});
|
|
137
|
+
it("should drop the session doc index", () => {
|
|
138
|
+
expect(fakeCollection.dropIndex).toHaveBeenCalledWith(
|
|
139
|
+
"search_session_doc_expiration"
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
it("should create the expected indexes", () => {
|
|
143
|
+
expect(fakeCollection.createIndex).toHaveBeenNthCalledWith(
|
|
144
|
+
1,
|
|
145
|
+
{ asid: 1 },
|
|
146
|
+
{ unique: true }
|
|
147
|
+
);
|
|
148
|
+
expect(fakeCollection.createIndex).toHaveBeenNthCalledWith(
|
|
149
|
+
2,
|
|
150
|
+
{ updatedAt: 1 },
|
|
151
|
+
{ expireAfterSeconds: 3600, name: "search_session_doc_expiration" }
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe("with the SearchSessionRepository class", () => {
|
|
157
|
+
beforeEach(async () => {
|
|
158
|
+
process.env.MONGO_URL = "MONGO_URL";
|
|
159
|
+
const mod = await import("./SearchSessionRepository");
|
|
160
|
+
repoInstance = await mod.getSearchSessionRepository();
|
|
161
|
+
});
|
|
162
|
+
it("should initialize the instance collection", () => {
|
|
163
|
+
expect(collectionMockFn).toHaveBeenCalledWith("search_sessions");
|
|
164
|
+
});
|
|
165
|
+
it("should assign collection to the repo instance", () => {
|
|
166
|
+
expect(repoInstance.collection).toBeTruthy();
|
|
167
|
+
});
|
|
168
|
+
describe("when calling upsertSearchSession", () => {
|
|
169
|
+
let upserted: any, upsertResult: any;
|
|
170
|
+
beforeEach(async () => {
|
|
171
|
+
upserted = {
|
|
172
|
+
asid: "a38c287c-15c3-42f0-863f-872af171982d",
|
|
173
|
+
status: "COMPLETED",
|
|
174
|
+
payload: {
|
|
175
|
+
asid: "a38c287c-15c3-42f0-863f-872af171982d",
|
|
176
|
+
stay: {
|
|
177
|
+
adults: 2,
|
|
178
|
+
children: 0,
|
|
179
|
+
rooms: 1,
|
|
180
|
+
checkIn: "2024-06-12",
|
|
181
|
+
checkOut: "2024-06-14",
|
|
182
|
+
},
|
|
183
|
+
place: {
|
|
184
|
+
coords: {
|
|
185
|
+
latitude: 28.5383832,
|
|
186
|
+
longitude: -81.3789269,
|
|
187
|
+
},
|
|
188
|
+
text: "Orlando, FL, USA",
|
|
189
|
+
},
|
|
190
|
+
radiusMiles: 20,
|
|
191
|
+
pagination: {
|
|
192
|
+
offset: 0,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
const mod = await import("./SearchSessionRepository");
|
|
197
|
+
repoInstance = await mod.getSearchSessionRepository();
|
|
198
|
+
upsertResult = await repoInstance.upsertSearchSession(upserted);
|
|
199
|
+
});
|
|
200
|
+
it("should call replaceOne on the instance collection", () => {
|
|
201
|
+
expect(fakeCollection.replaceOne).toHaveBeenCalledTimes(1);
|
|
202
|
+
expect(fakeCollection.replaceOne).toHaveBeenCalledWith(
|
|
203
|
+
{
|
|
204
|
+
asid: upserted.asid,
|
|
205
|
+
},
|
|
206
|
+
upserted,
|
|
207
|
+
{ upsert: true }
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
it("should return the session data that was passed to upsertSearchSession", () => {
|
|
211
|
+
expect(upsertResult).toEqual(upserted);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
describe("when calling getSearchSession", () => {
|
|
215
|
+
let getSearchSessionResult: any;
|
|
216
|
+
beforeEach(async () => {
|
|
217
|
+
const mod = await import("./SearchSessionRepository");
|
|
218
|
+
repoInstance = await mod.getSearchSessionRepository();
|
|
219
|
+
getSearchSessionResult = await repoInstance.getSearchSession(
|
|
220
|
+
"a38c287c-15c3-42f0-863f-872af171982d"
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
it("should call findOne on the collection", () => {
|
|
224
|
+
expect(fakeCollection.findOne).toHaveBeenCalledTimes(1);
|
|
225
|
+
expect(fakeCollection.findOne).toHaveBeenCalledWith({
|
|
226
|
+
asid: "a38c287c-15c3-42f0-863f-872af171982d",
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
it("should return the document without the MongoDB _id property", () => {
|
|
230
|
+
expect(getSearchSessionResult).toEqual({
|
|
231
|
+
asid: "a38c287c-15c3-42f0-863f-872af171982d",
|
|
232
|
+
status: "COMPLETED",
|
|
233
|
+
payload: {
|
|
234
|
+
asid: "a38c287c-15c3-42f0-863f-872af171982d",
|
|
235
|
+
stay: {
|
|
236
|
+
adults: 2,
|
|
237
|
+
children: 0,
|
|
238
|
+
rooms: 1,
|
|
239
|
+
checkIn: "2024-06-12",
|
|
240
|
+
checkOut: "2024-06-14",
|
|
241
|
+
},
|
|
242
|
+
place: {
|
|
243
|
+
coords: {
|
|
244
|
+
latitude: 28.5383832,
|
|
245
|
+
longitude: -81.3789269,
|
|
246
|
+
},
|
|
247
|
+
text: "Orlando, FL, USA",
|
|
248
|
+
},
|
|
249
|
+
radiusMiles: 20,
|
|
250
|
+
pagination: {
|
|
251
|
+
offset: 0,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
describe("when calling cancelSearch", () => {
|
|
258
|
+
beforeEach(async () => {
|
|
259
|
+
const mod = await import("./SearchSessionRepository");
|
|
260
|
+
repoInstance = await mod.getSearchSessionRepository();
|
|
261
|
+
await repoInstance.cancelSearch("ASID", "SEARCH_ID");
|
|
262
|
+
});
|
|
263
|
+
it("should call updateOne on the instance collection", () => {
|
|
264
|
+
expect(fakeCollection.updateOne).toHaveBeenCalledTimes(1);
|
|
265
|
+
expect(fakeCollection.updateOne).toHaveBeenCalledWith(
|
|
266
|
+
{ asid: "ASID", "criteria.searchId": "SEARCH_ID" },
|
|
267
|
+
{
|
|
268
|
+
$set: {
|
|
269
|
+
status: SearchSessionStatus.CANCELLED,
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
describe("when calling forceSessionStatusToCompleted", () => {
|
|
276
|
+
beforeEach(async () => {
|
|
277
|
+
const mod = await import("./SearchSessionRepository");
|
|
278
|
+
repoInstance = await mod.getSearchSessionRepository();
|
|
279
|
+
await repoInstance.forceSessionStatusToCompleted("ASID");
|
|
280
|
+
});
|
|
281
|
+
it("should call updateOne on the instance collection", () => {
|
|
282
|
+
expect(fakeCollection.updateOne).toHaveBeenCalledTimes(1);
|
|
283
|
+
expect(fakeCollection.updateOne).toHaveBeenCalledWith(
|
|
284
|
+
{
|
|
285
|
+
asid: "ASID",
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
$set: {
|
|
289
|
+
status: SearchSessionStatus.COMPLETED,
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
describe("when invoking the feature flag client's addHandler callback", () => {
|
|
297
|
+
beforeEach(async () => {
|
|
298
|
+
flagClient.addHandler.mockReset();
|
|
299
|
+
flagClient.addHandler.mockImplementationOnce((_event: any, cb: any) => {
|
|
300
|
+
cb();
|
|
301
|
+
});
|
|
302
|
+
const mod = await import("./SearchSessionRepository");
|
|
303
|
+
process.env.MONGO_URL = "MONGO_URL";
|
|
304
|
+
repoInstance = await mod.getSearchSessionRepository();
|
|
305
|
+
});
|
|
306
|
+
it("should drop the session doc index (in addition to the calls from init)", () => {
|
|
307
|
+
expect(fakeCollection.dropIndex).toHaveBeenNthCalledWith(
|
|
308
|
+
2,
|
|
309
|
+
"search_session_doc_expiration"
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
it("should create the expected indexes (in addition to the calls from init)", () => {
|
|
313
|
+
expect(fakeCollection.createIndex).toHaveBeenNthCalledWith(
|
|
314
|
+
3,
|
|
315
|
+
{ updatedAt: 1 },
|
|
316
|
+
{ expireAfterSeconds: 3600, name: "search_session_doc_expiration" }
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
describe("when dropping the index fails", () => {
|
|
321
|
+
let err: any;
|
|
322
|
+
beforeEach(async () => {
|
|
323
|
+
err = new Error("E_CLEANHOUSE_SANITY_KIDS_PICK_TWO"); // The parental CAP theorem
|
|
324
|
+
fakeCollection.dropIndex.mockReset();
|
|
325
|
+
fakeCollection.dropIndex.mockRejectedValueOnce(err);
|
|
326
|
+
const mod = await import("./SearchSessionRepository");
|
|
327
|
+
process.env.MONGO_URL = "MONGO_URL";
|
|
328
|
+
repoInstance = await mod.getSearchSessionRepository();
|
|
329
|
+
});
|
|
330
|
+
it("should log a warning", () => {
|
|
331
|
+
expect(mock_logger.warn).toHaveBeenCalledWith(
|
|
332
|
+
`Unable to drop index search_session_doc_expiration: ${err}`
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Example: useAnimationClassesV3
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// useAnimationClassesV3.test.ts
|
|
345
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
346
|
+
/* eslint-disable import/no-anonymous-default-export */
|
|
347
|
+
import { squelchConsole, unsquelchConsole } from "@apogee-travel/common-test";
|
|
348
|
+
export default {};
|
|
349
|
+
const mockUseSearchParamsStore = jest.fn();
|
|
350
|
+
jest.mock("@/contexts/RootStoreContext", () => {
|
|
351
|
+
return { useSearchParamsStore: mockUseSearchParamsStore };
|
|
352
|
+
});
|
|
353
|
+
const mockReact = {
|
|
354
|
+
useCallback: jest.fn(),
|
|
355
|
+
useMemo: jest.fn(),
|
|
356
|
+
useState: jest.fn(),
|
|
357
|
+
};
|
|
358
|
+
jest.mock("react", () => {
|
|
359
|
+
return mockReact;
|
|
360
|
+
});
|
|
361
|
+
describe("clients > customers-web > src > pages > search > useAnimationClassesV3", () => {
|
|
362
|
+
let mockLogger: any,
|
|
363
|
+
origSetTimeout: any,
|
|
364
|
+
origClearTimeout: any,
|
|
365
|
+
mockSearchParamsStore: any,
|
|
366
|
+
mapTransitionStatus: any,
|
|
367
|
+
setMapTransitionStatus: any,
|
|
368
|
+
result: any;
|
|
369
|
+
beforeEach(() => {
|
|
370
|
+
jest.clearAllMocks();
|
|
371
|
+
jest.resetModules();
|
|
372
|
+
squelchConsole();
|
|
373
|
+
origSetTimeout = global.setTimeout;
|
|
374
|
+
origClearTimeout = global.clearTimeout;
|
|
375
|
+
// @ts-ignore
|
|
376
|
+
global.setTimeout = jest.fn().mockImplementation((cb) => {
|
|
377
|
+
cb();
|
|
378
|
+
return "timeoutId";
|
|
379
|
+
});
|
|
380
|
+
global.clearTimeout = jest.fn();
|
|
381
|
+
mockSearchParamsStore = {
|
|
382
|
+
actions: {
|
|
383
|
+
resetRadiusMilesToDefault: jest.fn(),
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
mapTransitionStatus = "collapsed";
|
|
387
|
+
setMapTransitionStatus = jest.fn();
|
|
388
|
+
mockReact.useState.mockReturnValueOnce([mapTransitionStatus, setMapTransitionStatus]);
|
|
389
|
+
mockUseSearchParamsStore.mockReturnValue(mockSearchParamsStore);
|
|
390
|
+
mockReact.useMemo.mockImplementationOnce((cb) => {
|
|
391
|
+
return cb();
|
|
392
|
+
});
|
|
393
|
+
mockReact.useMemo.mockImplementationOnce((cb) => {
|
|
394
|
+
return cb();
|
|
395
|
+
});
|
|
396
|
+
mockReact.useMemo.mockImplementationOnce((cb) => {
|
|
397
|
+
return cb();
|
|
398
|
+
});
|
|
399
|
+
mockReact.useCallback.mockImplementationOnce(() => {
|
|
400
|
+
return "expandMap";
|
|
401
|
+
});
|
|
402
|
+
mockReact.useCallback.mockImplementationOnce(() => {
|
|
403
|
+
return "collapseMap";
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
afterEach(() => {
|
|
407
|
+
global.setTimeout = origSetTimeout;
|
|
408
|
+
global.clearTimeout = origClearTimeout;
|
|
409
|
+
unsquelchConsole();
|
|
410
|
+
});
|
|
411
|
+
describe("with hook init", () => {
|
|
412
|
+
beforeEach(async () => {
|
|
413
|
+
const mod = await import("./useAnimationClassesV3");
|
|
414
|
+
result = mod.useAnimationClassesV3();
|
|
415
|
+
});
|
|
416
|
+
it("should track state for mapTransitionStatus", () => {
|
|
417
|
+
expect(mockReact.useState).toHaveBeenNthCalledWith(1, null);
|
|
418
|
+
});
|
|
419
|
+
it("should create a memo for isMapExpanded", () => {
|
|
420
|
+
expect(mockReact.useMemo).toHaveBeenNthCalledWith(1, expect.any(Function), [
|
|
421
|
+
mapTransitionStatus,
|
|
422
|
+
]);
|
|
423
|
+
});
|
|
424
|
+
it("should create an expandMap callback", () => {
|
|
425
|
+
expect(mockReact.useCallback).toHaveBeenNthCalledWith(1, expect.any(Function), []);
|
|
426
|
+
});
|
|
427
|
+
it("should create an collapseMap callback", () => {
|
|
428
|
+
expect(mockReact.useCallback).toHaveBeenNthCalledWith(2, expect.any(Function), []);
|
|
429
|
+
});
|
|
430
|
+
it("should create a memo for mapClasses", () => {
|
|
431
|
+
expect(mockReact.useMemo).toHaveBeenNthCalledWith(2, expect.any(Function), [
|
|
432
|
+
mapTransitionStatus,
|
|
433
|
+
]);
|
|
434
|
+
});
|
|
435
|
+
it("should create a memo for resultsClasses", () => {
|
|
436
|
+
expect(mockReact.useMemo).toHaveBeenNthCalledWith(3, expect.any(Function), [
|
|
437
|
+
mapTransitionStatus,
|
|
438
|
+
]);
|
|
439
|
+
});
|
|
440
|
+
it("should return the expected value", () => {
|
|
441
|
+
expect(result).toEqual({
|
|
442
|
+
mapClasses: "search-page__map search-page__map--up",
|
|
443
|
+
resultsClasses: "search-page__results-list search-page__results-list--up",
|
|
444
|
+
isMapExpanded: false,
|
|
445
|
+
expandMap: "expandMap",
|
|
446
|
+
collapseMap: "collapseMap",
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
describe("with the isExpanded memo", () => {
|
|
451
|
+
describe("when mapTransitionStatus is null", () => {
|
|
452
|
+
beforeEach(async () => {
|
|
453
|
+
mockReact.useState.mockReset();
|
|
454
|
+
mapTransitionStatus = null;
|
|
455
|
+
mockReact.useState.mockReturnValueOnce([
|
|
456
|
+
mapTransitionStatus,
|
|
457
|
+
setMapTransitionStatus,
|
|
458
|
+
]);
|
|
459
|
+
const mod = await import("./useAnimationClassesV3");
|
|
460
|
+
result = mod.useAnimationClassesV3();
|
|
461
|
+
});
|
|
462
|
+
it("should return false", () => {
|
|
463
|
+
expect(result.isMapExpanded).toBe(false);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
describe("when mapTransitionStatus is 'collapsed'", () => {
|
|
467
|
+
beforeEach(async () => {
|
|
468
|
+
mockReact.useState.mockReset();
|
|
469
|
+
mapTransitionStatus = "collapsed";
|
|
470
|
+
mockReact.useState.mockReturnValueOnce([
|
|
471
|
+
mapTransitionStatus,
|
|
472
|
+
setMapTransitionStatus,
|
|
473
|
+
]);
|
|
474
|
+
const mod = await import("./useAnimationClassesV3");
|
|
475
|
+
result = mod.useAnimationClassesV3();
|
|
476
|
+
});
|
|
477
|
+
it("should return false", () => {
|
|
478
|
+
expect(result.isMapExpanded).toBe(false);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
describe("when mapTransitionStatus is not 'collapsed'", () => {
|
|
482
|
+
beforeEach(async () => {
|
|
483
|
+
mockReact.useState.mockReset();
|
|
484
|
+
mapTransitionStatus = "expanded";
|
|
485
|
+
mockReact.useState.mockReturnValueOnce([
|
|
486
|
+
mapTransitionStatus,
|
|
487
|
+
setMapTransitionStatus,
|
|
488
|
+
]);
|
|
489
|
+
const mod = await import("./useAnimationClassesV3");
|
|
490
|
+
result = mod.useAnimationClassesV3();
|
|
491
|
+
});
|
|
492
|
+
it("should return true", () => {
|
|
493
|
+
expect(result.isMapExpanded).toBe(true);
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
describe("with the expandMap callback", () => {
|
|
498
|
+
beforeEach(async () => {
|
|
499
|
+
mockReact.useCallback.mockReset();
|
|
500
|
+
mockReact.useCallback.mockImplementationOnce((cb) => {
|
|
501
|
+
cb()();
|
|
502
|
+
return "expandMap";
|
|
503
|
+
});
|
|
504
|
+
mockReact.useCallback.mockImplementationOnce(() => {
|
|
505
|
+
// no-op
|
|
506
|
+
return "collapseMap";
|
|
507
|
+
});
|
|
508
|
+
const mod = await import("./useAnimationClassesV3");
|
|
509
|
+
result = mod.useAnimationClassesV3();
|
|
510
|
+
});
|
|
511
|
+
it("should first set mapTransitionStatus to 'expanding'", () => {
|
|
512
|
+
expect(setMapTransitionStatus).toHaveBeenNthCalledWith(1, "expanding");
|
|
513
|
+
});
|
|
514
|
+
it("should call setTimeout with a duration of MAP_ANIM_DURATION_MS", () => {
|
|
515
|
+
expect(global.setTimeout).toHaveBeenNthCalledWith(1, expect.any(Function), 750);
|
|
516
|
+
});
|
|
517
|
+
it("should next call setMapTransitionStatus with 'expanded'", () => {
|
|
518
|
+
expect(setMapTransitionStatus).toHaveBeenNthCalledWith(2, "expanded");
|
|
519
|
+
});
|
|
520
|
+
it("should return a function that clears the timeout", () => {
|
|
521
|
+
expect(global.clearTimeout).toHaveBeenNthCalledWith(1, "timeoutId");
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
describe("with the collapseMap callback", () => {
|
|
525
|
+
beforeEach(async () => {
|
|
526
|
+
mockReact.useCallback.mockReset();
|
|
527
|
+
mockReact.useCallback.mockImplementationOnce(() => {
|
|
528
|
+
// no-op
|
|
529
|
+
return "expandMap";
|
|
530
|
+
});
|
|
531
|
+
mockReact.useCallback.mockImplementationOnce((cb) => {
|
|
532
|
+
cb()();
|
|
533
|
+
return "collapseMap";
|
|
534
|
+
});
|
|
535
|
+
const mod = await import("./useAnimationClassesV3");
|
|
536
|
+
result = mod.useAnimationClassesV3();
|
|
537
|
+
});
|
|
538
|
+
it("should first set mapTransitionStatus to 'collapsing'", () => {
|
|
539
|
+
expect(setMapTransitionStatus).toHaveBeenNthCalledWith(1, "collapsing");
|
|
540
|
+
});
|
|
541
|
+
it("should call setTimeout with a duration of MAP_ANIM_DURATION_MS", () => {
|
|
542
|
+
expect(global.setTimeout).toHaveBeenNthCalledWith(1, expect.any(Function), 750);
|
|
543
|
+
});
|
|
544
|
+
it("should next call setMapTransitionStatus with 'collapsed'", () => {
|
|
545
|
+
expect(setMapTransitionStatus).toHaveBeenNthCalledWith(2, "collapsed");
|
|
546
|
+
});
|
|
547
|
+
it("should return a function that clears the timeout", () => {
|
|
548
|
+
expect(global.clearTimeout).toHaveBeenNthCalledWith(1, "timeoutId");
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
describe("with the mapClasses memo", () => {
|
|
552
|
+
describe("when mapTransitionStatus is 'collapsing'", () => {
|
|
553
|
+
beforeEach(async () => {
|
|
554
|
+
mockReact.useState.mockReset();
|
|
555
|
+
mapTransitionStatus = "collapsing";
|
|
556
|
+
mockReact.useState.mockReturnValueOnce([
|
|
557
|
+
mapTransitionStatus,
|
|
558
|
+
setMapTransitionStatus,
|
|
559
|
+
]);
|
|
560
|
+
const mod = await import("./useAnimationClassesV3");
|
|
561
|
+
result = mod.useAnimationClassesV3();
|
|
562
|
+
});
|
|
563
|
+
it("should return the expected classnames value", () => {
|
|
564
|
+
expect(result.mapClasses).toBe("search-page__map search-page__map--up");
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
describe("when mapTransitionStatus is 'collapsed'", () => {
|
|
568
|
+
beforeEach(async () => {
|
|
569
|
+
mockReact.useState.mockReset();
|
|
570
|
+
mapTransitionStatus = "collapsed";
|
|
571
|
+
mockReact.useState.mockReturnValueOnce([
|
|
572
|
+
mapTransitionStatus,
|
|
573
|
+
setMapTransitionStatus,
|
|
574
|
+
]);
|
|
575
|
+
const mod = await import("./useAnimationClassesV3");
|
|
576
|
+
result = mod.useAnimationClassesV3();
|
|
577
|
+
});
|
|
578
|
+
it("should return the expected classnames value", () => {
|
|
579
|
+
expect(result.mapClasses).toBe("search-page__map search-page__map--up");
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
describe("when mapTransitionStatus is 'expanding'", () => {
|
|
583
|
+
beforeEach(async () => {
|
|
584
|
+
mockReact.useState.mockReset();
|
|
585
|
+
mapTransitionStatus = "expanding";
|
|
586
|
+
mockReact.useState.mockReturnValueOnce([
|
|
587
|
+
mapTransitionStatus,
|
|
588
|
+
setMapTransitionStatus,
|
|
589
|
+
]);
|
|
590
|
+
const mod = await import("./useAnimationClassesV3");
|
|
591
|
+
result = mod.useAnimationClassesV3();
|
|
592
|
+
});
|
|
593
|
+
it("should return the expected classnames value", () => {
|
|
594
|
+
expect(result.mapClasses).toBe("search-page__map search-page__map--down");
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
describe("when mapTransitionStatus is 'expanded'", () => {
|
|
598
|
+
beforeEach(async () => {
|
|
599
|
+
mockReact.useState.mockReset();
|
|
600
|
+
mapTransitionStatus = "expanded";
|
|
601
|
+
mockReact.useState.mockReturnValueOnce([
|
|
602
|
+
mapTransitionStatus,
|
|
603
|
+
setMapTransitionStatus,
|
|
604
|
+
]);
|
|
605
|
+
const mod = await import("./useAnimationClassesV3");
|
|
606
|
+
result = mod.useAnimationClassesV3();
|
|
607
|
+
});
|
|
608
|
+
it("should return the expected classnames value", () => {
|
|
609
|
+
expect(result.mapClasses).toBe("search-page__map search-page__map--down");
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
describe("with the resultsClasses memo", () => {
|
|
614
|
+
describe("when mapTransitionStatus is 'collapsing'", () => {
|
|
615
|
+
beforeEach(async () => {
|
|
616
|
+
mockReact.useState.mockReset();
|
|
617
|
+
mapTransitionStatus = "collapsing";
|
|
618
|
+
mockReact.useState.mockReturnValueOnce([
|
|
619
|
+
mapTransitionStatus,
|
|
620
|
+
setMapTransitionStatus,
|
|
621
|
+
]);
|
|
622
|
+
const mod = await import("./useAnimationClassesV3");
|
|
623
|
+
result = mod.useAnimationClassesV3();
|
|
624
|
+
});
|
|
625
|
+
it("should return the expected classnames value", () => {
|
|
626
|
+
expect(result.resultsClasses).toBe(
|
|
627
|
+
"search-page__results-list search-page__results-list--up"
|
|
628
|
+
);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
describe("when mapTransitionStatus is 'collapsed'", () => {
|
|
632
|
+
beforeEach(async () => {
|
|
633
|
+
mockReact.useState.mockReset();
|
|
634
|
+
mapTransitionStatus = "collapsed";
|
|
635
|
+
mockReact.useState.mockReturnValueOnce([
|
|
636
|
+
mapTransitionStatus,
|
|
637
|
+
setMapTransitionStatus,
|
|
638
|
+
]);
|
|
639
|
+
const mod = await import("./useAnimationClassesV3");
|
|
640
|
+
result = mod.useAnimationClassesV3();
|
|
641
|
+
});
|
|
642
|
+
it("should return the expected classnames value", () => {
|
|
643
|
+
expect(result.resultsClasses).toBe(
|
|
644
|
+
"search-page__results-list search-page__results-list--up"
|
|
645
|
+
);
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
describe("when mapTransitionStatus is 'expanding'", () => {
|
|
649
|
+
beforeEach(async () => {
|
|
650
|
+
mockReact.useState.mockReset();
|
|
651
|
+
mapTransitionStatus = "expanding";
|
|
652
|
+
mockReact.useState.mockReturnValueOnce([
|
|
653
|
+
mapTransitionStatus,
|
|
654
|
+
setMapTransitionStatus,
|
|
655
|
+
]);
|
|
656
|
+
const mod = await import("./useAnimationClassesV3");
|
|
657
|
+
result = mod.useAnimationClassesV3();
|
|
658
|
+
});
|
|
659
|
+
it("should return the expected classnames value", () => {
|
|
660
|
+
expect(result.resultsClasses).toBe(
|
|
661
|
+
"search-page__results-list search-page__results-list--down"
|
|
662
|
+
);
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
describe("when mapTransitionStatus is 'expanded'", () => {
|
|
666
|
+
beforeEach(async () => {
|
|
667
|
+
mockReact.useState.mockReset();
|
|
668
|
+
mapTransitionStatus = "expanded";
|
|
669
|
+
mockReact.useState.mockReturnValueOnce([
|
|
670
|
+
mapTransitionStatus,
|
|
671
|
+
setMapTransitionStatus,
|
|
672
|
+
]);
|
|
673
|
+
const mod = await import("./useAnimationClassesV3");
|
|
674
|
+
result = mod.useAnimationClassesV3();
|
|
675
|
+
});
|
|
676
|
+
it("should return the expected classnames value", () => {
|
|
677
|
+
expect(result.resultsClasses).toBe(
|
|
678
|
+
"search-page__results-list search-page__results-list--down"
|
|
679
|
+
);
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
## Example: search-operation-batch
|
|
689
|
+
|
|
690
|
+
```typescript
|
|
691
|
+
// search-operation-batch.test.ts
|
|
692
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
693
|
+
const mockLuxon = {
|
|
694
|
+
DateTime: {
|
|
695
|
+
utc: jest.fn().mockReturnThis(),
|
|
696
|
+
toISO: jest.fn(),
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
jest.mock("luxon", () => {
|
|
700
|
+
return mockLuxon;
|
|
701
|
+
});
|
|
702
|
+
const mockSearchTelemetryApi = {
|
|
703
|
+
traceEvent: jest.fn(),
|
|
704
|
+
};
|
|
705
|
+
jest.mock("../../../data/SearchTelemetry/searchTelemetryApi", () => {
|
|
706
|
+
return mockSearchTelemetryApi;
|
|
707
|
+
});
|
|
708
|
+
const mockShouldContinueSearchExecution = jest.fn();
|
|
709
|
+
jest.mock("../common/shouldContinueSearchExecution", () => {
|
|
710
|
+
return { shouldContinueSearchExecution: mockShouldContinueSearchExecution };
|
|
711
|
+
});
|
|
712
|
+
const mockGetProfilesAndAvailabilitiesForBatch = jest.fn();
|
|
713
|
+
jest.mock("./getProfilesAndAvailabilitiesForBatch", () => {
|
|
714
|
+
return {
|
|
715
|
+
getProfilesAndAvailabilitiesForBatch: mockGetProfilesAndAvailabilitiesForBatch,
|
|
716
|
+
};
|
|
717
|
+
});
|
|
718
|
+
const mockPublishSearchResultSuccessMessages = jest.fn();
|
|
719
|
+
jest.mock("./publishSearchResultSuccessMessages", () => {
|
|
720
|
+
return {
|
|
721
|
+
publishSearchResultSuccessMessages: mockPublishSearchResultSuccessMessages,
|
|
722
|
+
};
|
|
723
|
+
});
|
|
724
|
+
const mockPublishSearchResultFailureMessages = jest.fn();
|
|
725
|
+
jest.mock("./publishSearchResultFailureMessages", () => {
|
|
726
|
+
return {
|
|
727
|
+
publishSearchResultFailureMessages: mockPublishSearchResultFailureMessages,
|
|
728
|
+
};
|
|
729
|
+
});
|
|
730
|
+
import { mock_logger } from "@apogee-travel/common-test";
|
|
731
|
+
import { V3SearchTraceEventType } from "../../../routes/search/availability/v3/types";
|
|
732
|
+
import { Message } from "amqplib";
|
|
733
|
+
describe("hotel-availability > src > messaging > handlers > search-operation-batch > index", () => {
|
|
734
|
+
let content: any, ackOrNack: any;
|
|
735
|
+
beforeEach(() => {
|
|
736
|
+
jest.clearAllMocks();
|
|
737
|
+
jest.resetModules();
|
|
738
|
+
mockLuxon.DateTime.toISO.mockReturnValue("2025-12-31T00:00:00.000Z");
|
|
739
|
+
mockGetProfilesAndAvailabilitiesForBatch.mockResolvedValueOnce("RESULTS");
|
|
740
|
+
mockPublishSearchResultSuccessMessages.mockResolvedValueOnce(undefined);
|
|
741
|
+
mockPublishSearchResultFailureMessages.mockResolvedValueOnce(undefined);
|
|
742
|
+
content = {
|
|
743
|
+
asid: "ASID",
|
|
744
|
+
searchId: "SEARCH_ID",
|
|
745
|
+
executionId: "EXECUTION_ID",
|
|
746
|
+
channel: "amadeus",
|
|
747
|
+
batch: "amadeus-1",
|
|
748
|
+
stay: "STAY",
|
|
749
|
+
suppliers: ["SUPPLIER1", "SUPPLIER2"],
|
|
750
|
+
donatedInventory: "DONATEDINVENTORY",
|
|
751
|
+
};
|
|
752
|
+
ackOrNack = jest.fn();
|
|
753
|
+
});
|
|
754
|
+
describe("when content is an empty object", () => {
|
|
755
|
+
beforeEach(async () => {
|
|
756
|
+
const mod = await import("./index");
|
|
757
|
+
await mod.searchOperationBatchHandler({} as Message, {}, ackOrNack);
|
|
758
|
+
});
|
|
759
|
+
it("should not call shouldContinueSearchExecution", () => {
|
|
760
|
+
expect(mockShouldContinueSearchExecution).not.toHaveBeenCalled();
|
|
761
|
+
});
|
|
762
|
+
it("should call ackOrNack", () => {
|
|
763
|
+
expect(ackOrNack).toHaveBeenCalledTimes(1);
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
describe("when everything goes splendiferously", () => {
|
|
767
|
+
beforeEach(async () => {
|
|
768
|
+
mockShouldContinueSearchExecution.mockResolvedValueOnce(true);
|
|
769
|
+
const mod = await import("./index");
|
|
770
|
+
await mod.searchOperationBatchHandler({} as Message, content, ackOrNack);
|
|
771
|
+
});
|
|
772
|
+
it("should call traceEvent with a SEARCH_BATCH_RECEIVED event", () => {
|
|
773
|
+
expect(mockSearchTelemetryApi.traceEvent).toHaveBeenCalledTimes(4);
|
|
774
|
+
expect(mockSearchTelemetryApi.traceEvent).toHaveBeenNthCalledWith(1, {
|
|
775
|
+
asid: "ASID",
|
|
776
|
+
searchId: "SEARCH_ID",
|
|
777
|
+
executionId: "EXECUTION_ID",
|
|
778
|
+
eventType: V3SearchTraceEventType.SEARCH_BATCH_RECEIVED,
|
|
779
|
+
metadata: {
|
|
780
|
+
channel: "amadeus",
|
|
781
|
+
batch: "amadeus-1",
|
|
782
|
+
supplierCount: 2,
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
it("should call traceEvent with a GDS_REQUEST_START event", () => {
|
|
787
|
+
expect(mockSearchTelemetryApi.traceEvent).toHaveBeenNthCalledWith(2, {
|
|
788
|
+
asid: "ASID",
|
|
789
|
+
searchId: "SEARCH_ID",
|
|
790
|
+
executionId: "EXECUTION_ID",
|
|
791
|
+
eventType: V3SearchTraceEventType.GDS_REQUEST_START,
|
|
792
|
+
metadata: {
|
|
793
|
+
channel: "amadeus",
|
|
794
|
+
batch: "amadeus-1",
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
it("should call getProfilesAndAvailabilitiesForBatch with the expected args", () => {
|
|
799
|
+
expect(mockGetProfilesAndAvailabilitiesForBatch).toHaveBeenCalledTimes(1);
|
|
800
|
+
expect(mockGetProfilesAndAvailabilitiesForBatch).toHaveBeenCalledWith({
|
|
801
|
+
asid: "ASID",
|
|
802
|
+
searchId: "SEARCH_ID",
|
|
803
|
+
executionId: "EXECUTION_ID",
|
|
804
|
+
channel: "amadeus",
|
|
805
|
+
stay: "STAY",
|
|
806
|
+
suppliers: ["SUPPLIER1", "SUPPLIER2"],
|
|
807
|
+
donatedInventory: "DONATEDINVENTORY",
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
it("should call traceEvent with a GDS_REQUEST_START event", () => {
|
|
811
|
+
expect(mockSearchTelemetryApi.traceEvent).toHaveBeenNthCalledWith(3, {
|
|
812
|
+
asid: "ASID",
|
|
813
|
+
searchId: "SEARCH_ID",
|
|
814
|
+
executionId: "EXECUTION_ID",
|
|
815
|
+
eventType: V3SearchTraceEventType.GDS_REQUEST_END,
|
|
816
|
+
metadata: {
|
|
817
|
+
channel: "amadeus",
|
|
818
|
+
batch: "amadeus-1",
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
it("should call publishSearchResultSuccessMessages with the expected args", () => {
|
|
823
|
+
expect(mockPublishSearchResultSuccessMessages).toHaveBeenCalledTimes(1);
|
|
824
|
+
expect(mockPublishSearchResultSuccessMessages).toHaveBeenCalledWith({
|
|
825
|
+
asid: "ASID",
|
|
826
|
+
searchId: "SEARCH_ID",
|
|
827
|
+
executionId: "EXECUTION_ID",
|
|
828
|
+
channel: "amadeus",
|
|
829
|
+
batch: "amadeus-1",
|
|
830
|
+
results: "RESULTS",
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
it("should call traceEvent with a SEARCH_BATCH_RESULTS_PUBLISHED event", () => {
|
|
834
|
+
expect(mockSearchTelemetryApi.traceEvent).toHaveBeenNthCalledWith(4, {
|
|
835
|
+
asid: "ASID",
|
|
836
|
+
searchId: "SEARCH_ID",
|
|
837
|
+
executionId: "EXECUTION_ID",
|
|
838
|
+
eventType: V3SearchTraceEventType.SEARCH_BATCH_RESULTS_PUBLISHED,
|
|
839
|
+
metadata: {
|
|
840
|
+
channel: "amadeus",
|
|
841
|
+
batch: "amadeus-1",
|
|
842
|
+
isError: false,
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
it("should call ackOrNack", () => {
|
|
847
|
+
expect(ackOrNack).toHaveBeenCalledTimes(1);
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
describe("when the search should not continue executing", () => {
|
|
851
|
+
beforeEach(async () => {
|
|
852
|
+
mockShouldContinueSearchExecution.mockResolvedValueOnce(false);
|
|
853
|
+
const mod = await import("./index");
|
|
854
|
+
await mod.searchOperationBatchHandler({} as Message, content, ackOrNack);
|
|
855
|
+
});
|
|
856
|
+
it("should not call getProfilesAndAvailabilitiesForBatch", () => {
|
|
857
|
+
expect(mockGetProfilesAndAvailabilitiesForBatch).not.toHaveBeenCalled();
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
describe("when an exception is thrown", () => {
|
|
861
|
+
let err: any;
|
|
862
|
+
beforeEach(async () => {
|
|
863
|
+
mockShouldContinueSearchExecution.mockResolvedValueOnce(true);
|
|
864
|
+
err = new Error("E_DEEP_STATE");
|
|
865
|
+
mockGetProfilesAndAvailabilitiesForBatch.mockReset();
|
|
866
|
+
mockGetProfilesAndAvailabilitiesForBatch.mockRejectedValueOnce(err);
|
|
867
|
+
const mod = await import("./index");
|
|
868
|
+
await mod.searchOperationBatchHandler({} as Message, content, ackOrNack);
|
|
869
|
+
});
|
|
870
|
+
it("should log an error message", () => {
|
|
871
|
+
expect(mock_logger.error).toHaveBeenCalledWith(
|
|
872
|
+
"V3 Search (ASID|SEARCH_ID):search operation batch handler error:",
|
|
873
|
+
{ error: err }
|
|
874
|
+
);
|
|
875
|
+
});
|
|
876
|
+
it("should call publishSearchResultFailureMessages with the expected args", () => {
|
|
877
|
+
expect(mockPublishSearchResultFailureMessages).toHaveBeenCalledTimes(1);
|
|
878
|
+
expect(mockPublishSearchResultFailureMessages).toHaveBeenCalledWith({
|
|
879
|
+
asid: "ASID",
|
|
880
|
+
searchId: "SEARCH_ID",
|
|
881
|
+
executionId: "EXECUTION_ID",
|
|
882
|
+
channel: "amadeus",
|
|
883
|
+
batch: "amadeus-1",
|
|
884
|
+
suppliers: content.suppliers,
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
it("should call ackOrNack with the expected args", () => {
|
|
888
|
+
expect(ackOrNack).toHaveBeenCalledTimes(1);
|
|
889
|
+
expect(ackOrNack).toHaveBeenCalledWith(err as Error, {
|
|
890
|
+
strategy: "nack",
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
## Example: userStore
|
|
900
|
+
|
|
901
|
+
```typescript
|
|
902
|
+
// userStore/index.test.ts
|
|
903
|
+
import { UserRewardsMemberType } from "@/types";
|
|
904
|
+
export default {};
|
|
905
|
+
const mockCreateStore = jest.fn();
|
|
906
|
+
jest.mock("@apogeelabs/beacon", () => {
|
|
907
|
+
return {
|
|
908
|
+
createStore: mockCreateStore,
|
|
909
|
+
};
|
|
910
|
+
});
|
|
911
|
+
describe("src > store > userStore > index", () => {
|
|
912
|
+
let storeCfg: any;
|
|
913
|
+
beforeEach(async () => {
|
|
914
|
+
jest.resetAllMocks();
|
|
915
|
+
jest.resetModules();
|
|
916
|
+
mockCreateStore.mockImplementationOnce((cfg: any) => {
|
|
917
|
+
return cfg; // returning the store config so we can invoke derived and actions
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
describe("when calling createUserStore", () => {
|
|
921
|
+
beforeEach(async () => {
|
|
922
|
+
const mod = await import("./index");
|
|
923
|
+
storeCfg = mod.createUserStore();
|
|
924
|
+
});
|
|
925
|
+
it("should call createStore with the expected configuration", () => {
|
|
926
|
+
expect(mockCreateStore).toHaveBeenCalledWith({
|
|
927
|
+
initialState: {
|
|
928
|
+
isAuthenticated: false,
|
|
929
|
+
isAuthStatusLoading: false,
|
|
930
|
+
isInitialLoading: false,
|
|
931
|
+
isUserMetadataLoading: false,
|
|
932
|
+
user: null,
|
|
933
|
+
userMetadataError: null,
|
|
934
|
+
defaultCharity: null,
|
|
935
|
+
rawRewardsMetadata: null,
|
|
936
|
+
},
|
|
937
|
+
derived: {
|
|
938
|
+
hasKnownAuthStatus: expect.any(Function),
|
|
939
|
+
isInitializing: expect.any(Function),
|
|
940
|
+
notificationPreferences: expect.any(Function),
|
|
941
|
+
rewardsMetadata: expect.any(Function),
|
|
942
|
+
},
|
|
943
|
+
actions: {
|
|
944
|
+
setIsAuthenticated: expect.any(Function),
|
|
945
|
+
setIsAuthLoading: expect.any(Function),
|
|
946
|
+
setIsUserMetadataLoading: expect.any(Function),
|
|
947
|
+
setUser: expect.any(Function),
|
|
948
|
+
setDefaultCharity: expect.any(Function),
|
|
949
|
+
clearUser: expect.any(Function),
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
describe("with the user store derived state configuration", () => {
|
|
955
|
+
beforeEach(async () => {
|
|
956
|
+
const mod = await import("./index");
|
|
957
|
+
storeCfg = mod.createUserStore();
|
|
958
|
+
});
|
|
959
|
+
describe("with hasKnownAuthStatus", () => {
|
|
960
|
+
it("should return true for hasKnownAuthStatus when isAuthenticated is true", () => {
|
|
961
|
+
expect(storeCfg.derived.hasKnownAuthStatus({ isAuthenticated: true })).toBe(true);
|
|
962
|
+
});
|
|
963
|
+
it("should return true for hasKnownAuthStatus when isAuthenticated is false", () => {
|
|
964
|
+
expect(storeCfg.derived.hasKnownAuthStatus({ isAuthenticated: false })).toBe(true);
|
|
965
|
+
});
|
|
966
|
+
it("should return false for hasKnownAuthStatus when isAuthenticated is null", () => {
|
|
967
|
+
expect(storeCfg.derived.hasKnownAuthStatus({ isAuthenticated: null })).toBe(false);
|
|
968
|
+
});
|
|
969
|
+
});
|
|
970
|
+
describe("with isInitializing", () => {
|
|
971
|
+
it("should return true for isInitializing when isAuthStatusLoading is true", () => {
|
|
972
|
+
expect(storeCfg.derived.isInitializing({ isAuthStatusLoading: true })).toBe(true);
|
|
973
|
+
});
|
|
974
|
+
it("should return true for isInitializing when isInitialLoading is true", () => {
|
|
975
|
+
expect(storeCfg.derived.isInitializing({ isInitialLoading: true })).toBe(true);
|
|
976
|
+
});
|
|
977
|
+
it("should return true for isInitializing when isUserMetadataLoading is true", () => {
|
|
978
|
+
expect(storeCfg.derived.isInitializing({ isUserMetadataLoading: true })).toBe(true);
|
|
979
|
+
});
|
|
980
|
+
it("should return false for isInitializing when all loading states are false", () => {
|
|
981
|
+
expect(
|
|
982
|
+
storeCfg.derived.isInitializing({
|
|
983
|
+
isAuthStatusLoading: false,
|
|
984
|
+
isInitialLoading: false,
|
|
985
|
+
isUserMetadataLoading: false,
|
|
986
|
+
})
|
|
987
|
+
).toBe(false);
|
|
988
|
+
});
|
|
989
|
+
});
|
|
990
|
+
describe("with notificationPreferences", () => {
|
|
991
|
+
describe("when user is null", () => {
|
|
992
|
+
it("should return default notification preferences", () => {
|
|
993
|
+
expect(
|
|
994
|
+
storeCfg.derived.notificationPreferences({
|
|
995
|
+
user: null,
|
|
996
|
+
})
|
|
997
|
+
).toEqual({ email: false, sms: false });
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
describe("when user is not null", () => {
|
|
1001
|
+
it("should return default notification preferences", () => {
|
|
1002
|
+
expect(
|
|
1003
|
+
storeCfg.derived.notificationPreferences({
|
|
1004
|
+
user: {},
|
|
1005
|
+
})
|
|
1006
|
+
).toEqual({ email: false, sms: false });
|
|
1007
|
+
});
|
|
1008
|
+
it("should return notification preferences when present", () => {
|
|
1009
|
+
const user = {
|
|
1010
|
+
notification_preferences: {
|
|
1011
|
+
prefs: [
|
|
1012
|
+
{ channel: "email", type: "marketing", enabled: true },
|
|
1013
|
+
{ channel: "sms", type: "system", enabled: false },
|
|
1014
|
+
],
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1017
|
+
expect(storeCfg.derived.notificationPreferences({ user })).toEqual({
|
|
1018
|
+
email: true,
|
|
1019
|
+
sms: false,
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
describe("with rewardsMetadata", () => {
|
|
1025
|
+
describe("when rawRewardsMetadata is null", () => {
|
|
1026
|
+
it("should return default rewards metadata", () => {
|
|
1027
|
+
expect(
|
|
1028
|
+
storeCfg.derived.rewardsMetadata({
|
|
1029
|
+
rawRewardsMetadata: null,
|
|
1030
|
+
})
|
|
1031
|
+
).toEqual({
|
|
1032
|
+
balance: 0,
|
|
1033
|
+
bookingCount: 0,
|
|
1034
|
+
earningRate: "1x",
|
|
1035
|
+
memberType: UserRewardsMemberType.MEMBER,
|
|
1036
|
+
});
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
describe("when rawRewardsMetadata is not null", () => {
|
|
1040
|
+
it("should return the actual rewards metadata", () => {
|
|
1041
|
+
expect(
|
|
1042
|
+
storeCfg.derived.rewardsMetadata({
|
|
1043
|
+
rawRewardsMetadata: {
|
|
1044
|
+
balance: 42,
|
|
1045
|
+
bookingCount: 12,
|
|
1046
|
+
earningRate: "2x",
|
|
1047
|
+
memberType: UserRewardsMemberType.ELITE,
|
|
1048
|
+
},
|
|
1049
|
+
})
|
|
1050
|
+
).toEqual({
|
|
1051
|
+
balance: 42,
|
|
1052
|
+
bookingCount: 12,
|
|
1053
|
+
earningRate: "2x",
|
|
1054
|
+
memberType: UserRewardsMemberType.ELITE,
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
});
|
|
1060
|
+
describe("with the user store actions configuration", () => {
|
|
1061
|
+
let targetState: any;
|
|
1062
|
+
beforeEach(async () => {
|
|
1063
|
+
const mod = await import("./index");
|
|
1064
|
+
storeCfg = mod.createUserStore();
|
|
1065
|
+
targetState = {};
|
|
1066
|
+
});
|
|
1067
|
+
it("should set state to expected values when calling setIsAuthenticated", () => {
|
|
1068
|
+
storeCfg.actions.setIsAuthenticated(targetState, true);
|
|
1069
|
+
expect(targetState).toEqual({
|
|
1070
|
+
isAuthenticated: true,
|
|
1071
|
+
isAuthStatusLoading: false,
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
it("should set state to expected values when calling setIsAuthLoading", () => {
|
|
1075
|
+
storeCfg.actions.setIsAuthLoading(targetState, true);
|
|
1076
|
+
expect(targetState).toEqual({
|
|
1077
|
+
isAuthLoading: true,
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
it("should set state to expected values when calling setIsUserMetadataLoading", () => {
|
|
1081
|
+
storeCfg.actions.setIsUserMetadataLoading(targetState, true);
|
|
1082
|
+
expect(targetState).toEqual({
|
|
1083
|
+
isUserMetadataLoading: true,
|
|
1084
|
+
});
|
|
1085
|
+
});
|
|
1086
|
+
it("should set state to expected values when calling setUser with a user that has rewards metadata", () => {
|
|
1087
|
+
const user = {
|
|
1088
|
+
id: "123",
|
|
1089
|
+
name: "Cal Zone",
|
|
1090
|
+
rewardsMetadata: {
|
|
1091
|
+
balance: 42,
|
|
1092
|
+
bookingCount: 12,
|
|
1093
|
+
earningRate: "2x",
|
|
1094
|
+
memberType: UserRewardsMemberType.ELITE,
|
|
1095
|
+
},
|
|
1096
|
+
};
|
|
1097
|
+
storeCfg.actions.setUser(targetState, user);
|
|
1098
|
+
expect(targetState).toEqual({
|
|
1099
|
+
user: {
|
|
1100
|
+
id: "123",
|
|
1101
|
+
name: "Cal Zone",
|
|
1102
|
+
},
|
|
1103
|
+
isUserMetadataLoading: false,
|
|
1104
|
+
rawRewardsMetadata: {
|
|
1105
|
+
balance: 42,
|
|
1106
|
+
bookingCount: 12,
|
|
1107
|
+
earningRate: "2x",
|
|
1108
|
+
memberType: UserRewardsMemberType.ELITE,
|
|
1109
|
+
},
|
|
1110
|
+
});
|
|
1111
|
+
});
|
|
1112
|
+
it("should set state to expected values when calling setUser with a user that has no rewards metadata", () => {
|
|
1113
|
+
const user = {
|
|
1114
|
+
id: "123",
|
|
1115
|
+
name: "Cal Zone",
|
|
1116
|
+
rewardsMetadata: undefined,
|
|
1117
|
+
};
|
|
1118
|
+
storeCfg.actions.setUser(targetState, user);
|
|
1119
|
+
expect(targetState).toEqual({
|
|
1120
|
+
user: {
|
|
1121
|
+
id: "123",
|
|
1122
|
+
name: "Cal Zone",
|
|
1123
|
+
},
|
|
1124
|
+
isUserMetadataLoading: false,
|
|
1125
|
+
rawRewardsMetadata: null,
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
it("should set state to expected values when calling setDefaultCharity", () => {
|
|
1129
|
+
const charity = {
|
|
1130
|
+
ein: "123456789",
|
|
1131
|
+
name: "Calzone Research Institute",
|
|
1132
|
+
};
|
|
1133
|
+
storeCfg.actions.setDefaultCharity(targetState, charity);
|
|
1134
|
+
expect(targetState).toEqual({
|
|
1135
|
+
defaultCharity: charity,
|
|
1136
|
+
});
|
|
1137
|
+
});
|
|
1138
|
+
it("should set state to expected values when calling clearUser", () => {
|
|
1139
|
+
storeCfg.actions.clearUser(targetState);
|
|
1140
|
+
expect(targetState).toEqual({
|
|
1141
|
+
isAuthenticated: false,
|
|
1142
|
+
user: null,
|
|
1143
|
+
defaultCharity: null,
|
|
1144
|
+
});
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
});
|
|
1148
|
+
```
|