@rooguys/js 0.1.0 → 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/README.md +342 -141
- package/package.json +1 -1
- package/src/__tests__/fixtures/responses.js +249 -0
- package/src/__tests__/property/batch-event-validation.property.test.js +225 -0
- package/src/__tests__/property/email-validation.property.test.js +272 -0
- package/src/__tests__/property/error-mapping.property.test.js +506 -0
- package/src/__tests__/property/field-selection.property.test.js +297 -0
- package/src/__tests__/property/idempotency-key.property.test.js +350 -0
- package/src/__tests__/property/leaderboard-filter.property.test.js +585 -0
- package/src/__tests__/property/partial-update.property.test.js +251 -0
- package/src/__tests__/property/rate-limit-error.property.test.js +276 -0
- package/src/__tests__/property/rate-limit-extraction.property.test.js +193 -0
- package/src/__tests__/property/request-construction.property.test.js +20 -28
- package/src/__tests__/property/response-format.property.test.js +418 -0
- package/src/__tests__/property/response-parsing.property.test.js +16 -21
- package/src/__tests__/property/timestamp-validation.property.test.js +345 -0
- package/src/__tests__/unit/aha.test.js +57 -26
- package/src/__tests__/unit/config.test.js +7 -1
- package/src/__tests__/unit/errors.test.js +6 -8
- package/src/__tests__/unit/events.test.js +253 -14
- package/src/__tests__/unit/leaderboards.test.js +249 -0
- package/src/__tests__/unit/questionnaires.test.js +6 -6
- package/src/__tests__/unit/users.test.js +275 -12
- package/src/__tests__/utils/generators.js +87 -0
- package/src/__tests__/utils/mockClient.js +71 -5
- package/src/errors.js +156 -0
- package/src/http-client.js +276 -0
- package/src/index.js +856 -66
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import Rooguys, { ValidationError } from '../../index';
|
|
3
|
+
import { mockStandardizedSuccess, mockErrorResponse, mockTimeoutError } from '../utils/mockClient';
|
|
3
4
|
import { mockResponses, mockErrors } from '../fixtures/responses';
|
|
4
5
|
|
|
5
6
|
describe('Events Resource', () => {
|
|
@@ -14,7 +15,7 @@ describe('Events Resource', () => {
|
|
|
14
15
|
describe('track', () => {
|
|
15
16
|
it('should track an event with valid inputs', async () => {
|
|
16
17
|
global.fetch.mockResolvedValue(
|
|
17
|
-
|
|
18
|
+
mockStandardizedSuccess(mockResponses.trackEventResponse)
|
|
18
19
|
);
|
|
19
20
|
|
|
20
21
|
const result = await client.events.track('purchase_completed', 'user_123', {
|
|
@@ -22,7 +23,7 @@ describe('Events Resource', () => {
|
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
25
|
-
expect.stringContaining('/
|
|
26
|
+
expect.stringContaining('/events'),
|
|
26
27
|
expect.objectContaining({
|
|
27
28
|
method: 'POST',
|
|
28
29
|
headers: expect.objectContaining({
|
|
@@ -41,7 +42,7 @@ describe('Events Resource', () => {
|
|
|
41
42
|
|
|
42
43
|
it('should track an event with empty properties', async () => {
|
|
43
44
|
global.fetch.mockResolvedValue(
|
|
44
|
-
|
|
45
|
+
mockStandardizedSuccess(mockResponses.trackEventResponse)
|
|
45
46
|
);
|
|
46
47
|
|
|
47
48
|
const result = await client.events.track('user_login', 'user_456');
|
|
@@ -52,7 +53,7 @@ describe('Events Resource', () => {
|
|
|
52
53
|
|
|
53
54
|
it('should include profile when includeProfile is true', async () => {
|
|
54
55
|
global.fetch.mockResolvedValue(
|
|
55
|
-
|
|
56
|
+
mockStandardizedSuccess(mockResponses.trackEventWithProfileResponse)
|
|
56
57
|
);
|
|
57
58
|
|
|
58
59
|
const result = await client.events.track(
|
|
@@ -70,7 +71,7 @@ describe('Events Resource', () => {
|
|
|
70
71
|
|
|
71
72
|
it('should handle special characters in event name', async () => {
|
|
72
73
|
global.fetch.mockResolvedValue(
|
|
73
|
-
|
|
74
|
+
mockStandardizedSuccess(mockResponses.trackEventResponse)
|
|
74
75
|
);
|
|
75
76
|
|
|
76
77
|
await client.events.track('user-signup_v2', 'user_123');
|
|
@@ -81,7 +82,7 @@ describe('Events Resource', () => {
|
|
|
81
82
|
|
|
82
83
|
it('should handle special characters in user ID', async () => {
|
|
83
84
|
global.fetch.mockResolvedValue(
|
|
84
|
-
|
|
85
|
+
mockStandardizedSuccess(mockResponses.trackEventResponse)
|
|
85
86
|
);
|
|
86
87
|
|
|
87
88
|
await client.events.track('user_login', 'user@example.com');
|
|
@@ -92,7 +93,7 @@ describe('Events Resource', () => {
|
|
|
92
93
|
|
|
93
94
|
it('should handle complex nested properties', async () => {
|
|
94
95
|
global.fetch.mockResolvedValue(
|
|
95
|
-
|
|
96
|
+
mockStandardizedSuccess(mockResponses.trackEventResponse)
|
|
96
97
|
);
|
|
97
98
|
|
|
98
99
|
const complexProperties = {
|
|
@@ -151,12 +152,12 @@ describe('Events Resource', () => {
|
|
|
151
152
|
|
|
152
153
|
await expect(
|
|
153
154
|
client.events.track('user_login', 'user_123')
|
|
154
|
-
).rejects.toThrow('
|
|
155
|
+
).rejects.toThrow('Request timeout');
|
|
155
156
|
});
|
|
156
157
|
|
|
157
158
|
it('should handle properties with null values', async () => {
|
|
158
159
|
global.fetch.mockResolvedValue(
|
|
159
|
-
|
|
160
|
+
mockStandardizedSuccess(mockResponses.trackEventResponse)
|
|
160
161
|
);
|
|
161
162
|
|
|
162
163
|
await client.events.track('user_updated', 'user_123', {
|
|
@@ -175,7 +176,7 @@ describe('Events Resource', () => {
|
|
|
175
176
|
|
|
176
177
|
it('should handle properties with boolean values', async () => {
|
|
177
178
|
global.fetch.mockResolvedValue(
|
|
178
|
-
|
|
179
|
+
mockStandardizedSuccess(mockResponses.trackEventResponse)
|
|
179
180
|
);
|
|
180
181
|
|
|
181
182
|
await client.events.track('feature_toggled', 'user_123', {
|
|
@@ -189,7 +190,7 @@ describe('Events Resource', () => {
|
|
|
189
190
|
|
|
190
191
|
it('should handle properties with numeric values', async () => {
|
|
191
192
|
global.fetch.mockResolvedValue(
|
|
192
|
-
|
|
193
|
+
mockStandardizedSuccess(mockResponses.trackEventResponse)
|
|
193
194
|
);
|
|
194
195
|
|
|
195
196
|
await client.events.track('score_updated', 'user_123', {
|
|
@@ -208,7 +209,7 @@ describe('Events Resource', () => {
|
|
|
208
209
|
|
|
209
210
|
it('should handle empty string properties', async () => {
|
|
210
211
|
global.fetch.mockResolvedValue(
|
|
211
|
-
|
|
212
|
+
mockStandardizedSuccess(mockResponses.trackEventResponse)
|
|
212
213
|
);
|
|
213
214
|
|
|
214
215
|
await client.events.track('form_submitted', 'user_123', {
|
|
@@ -219,5 +220,243 @@ describe('Events Resource', () => {
|
|
|
219
220
|
const callBody = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
220
221
|
expect(callBody.properties.comment).toBe('');
|
|
221
222
|
});
|
|
223
|
+
|
|
224
|
+
it('should include idempotency key header when provided', async () => {
|
|
225
|
+
global.fetch.mockResolvedValue(
|
|
226
|
+
mockStandardizedSuccess(mockResponses.trackEventResponse)
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await client.events.track('purchase_completed', 'user_123', { amount: 50 }, {
|
|
230
|
+
idempotencyKey: 'unique-key-123',
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const callHeaders = global.fetch.mock.calls[0][1].headers;
|
|
234
|
+
expect(callHeaders['X-Idempotency-Key']).toBe('unique-key-123');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should include custom timestamp when provided', async () => {
|
|
238
|
+
global.fetch.mockResolvedValue(
|
|
239
|
+
mockStandardizedSuccess(mockResponses.trackEventResponse)
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const timestamp = new Date(Date.now() - 24 * 60 * 60 * 1000); // 1 day ago
|
|
243
|
+
await client.events.track('historical_event', 'user_123', {}, {
|
|
244
|
+
timestamp,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const callBody = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
248
|
+
expect(callBody.timestamp).toBe(timestamp.toISOString());
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should throw ValidationError when timestamp is more than 7 days old', async () => {
|
|
252
|
+
const oldTimestamp = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000); // 8 days ago
|
|
253
|
+
|
|
254
|
+
await expect(
|
|
255
|
+
client.events.track('old_event', 'user_123', {}, { timestamp: oldTimestamp })
|
|
256
|
+
).rejects.toThrow(ValidationError);
|
|
257
|
+
|
|
258
|
+
await expect(
|
|
259
|
+
client.events.track('old_event', 'user_123', {}, { timestamp: oldTimestamp })
|
|
260
|
+
).rejects.toThrow('Custom timestamp cannot be more than 7 days in the past');
|
|
261
|
+
});
|
|
222
262
|
});
|
|
263
|
+
|
|
264
|
+
describe('trackBatch', () => {
|
|
265
|
+
it('should track multiple events in a single request', async () => {
|
|
266
|
+
global.fetch.mockResolvedValue(
|
|
267
|
+
mockStandardizedSuccess(mockResponses.batchTrackEventResponse)
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const events = [
|
|
271
|
+
{ eventName: 'event_1', userId: 'user_1', properties: { key: 'value1' } },
|
|
272
|
+
{ eventName: 'event_2', userId: 'user_2', properties: { key: 'value2' } },
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
const result = await client.events.trackBatch(events);
|
|
276
|
+
|
|
277
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
278
|
+
expect.stringContaining('/events/batch'),
|
|
279
|
+
expect.objectContaining({
|
|
280
|
+
method: 'POST',
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
events: [
|
|
283
|
+
{ event_name: 'event_1', user_id: 'user_1', properties: { key: 'value1' } },
|
|
284
|
+
{ event_name: 'event_2', user_id: 'user_2', properties: { key: 'value2' } },
|
|
285
|
+
],
|
|
286
|
+
}),
|
|
287
|
+
})
|
|
288
|
+
);
|
|
289
|
+
expect(result).toEqual(mockResponses.batchTrackEventResponse);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should include idempotency key header when provided', async () => {
|
|
293
|
+
global.fetch.mockResolvedValue(
|
|
294
|
+
mockStandardizedSuccess(mockResponses.batchTrackEventResponse)
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const events = [
|
|
298
|
+
{ eventName: 'event_1', userId: 'user_1' },
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
await client.events.trackBatch(events, { idempotencyKey: 'batch-key-123' });
|
|
302
|
+
|
|
303
|
+
const callHeaders = global.fetch.mock.calls[0][1].headers;
|
|
304
|
+
expect(callHeaders['X-Idempotency-Key']).toBe('batch-key-123');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should throw ValidationError when events is not an array', async () => {
|
|
308
|
+
await expect(
|
|
309
|
+
client.events.trackBatch('not-an-array')
|
|
310
|
+
).rejects.toThrow(ValidationError);
|
|
311
|
+
|
|
312
|
+
await expect(
|
|
313
|
+
client.events.trackBatch('not-an-array')
|
|
314
|
+
).rejects.toThrow('Events must be an array');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should throw ValidationError when events array is empty', async () => {
|
|
318
|
+
await expect(
|
|
319
|
+
client.events.trackBatch([])
|
|
320
|
+
).rejects.toThrow(ValidationError);
|
|
321
|
+
|
|
322
|
+
await expect(
|
|
323
|
+
client.events.trackBatch([])
|
|
324
|
+
).rejects.toThrow('Events array cannot be empty');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should throw ValidationError when batch exceeds 100 events', async () => {
|
|
328
|
+
const events = Array.from({ length: 101 }, (_, i) => ({
|
|
329
|
+
eventName: `event_${i}`,
|
|
330
|
+
userId: `user_${i}`,
|
|
331
|
+
}));
|
|
332
|
+
|
|
333
|
+
await expect(
|
|
334
|
+
client.events.trackBatch(events)
|
|
335
|
+
).rejects.toThrow(ValidationError);
|
|
336
|
+
|
|
337
|
+
await expect(
|
|
338
|
+
client.events.trackBatch(events)
|
|
339
|
+
).rejects.toThrow('Batch size exceeds maximum of 100 events');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should accept exactly 100 events', async () => {
|
|
343
|
+
global.fetch.mockResolvedValue(
|
|
344
|
+
mockStandardizedSuccess({ results: Array.from({ length: 100 }, (_, i) => ({ index: i, status: 'queued' })) })
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const events = Array.from({ length: 100 }, (_, i) => ({
|
|
348
|
+
eventName: `event_${i}`,
|
|
349
|
+
userId: `user_${i}`,
|
|
350
|
+
}));
|
|
351
|
+
|
|
352
|
+
await expect(client.events.trackBatch(events)).resolves.toBeDefined();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should include custom timestamps in batch events', async () => {
|
|
356
|
+
global.fetch.mockResolvedValue(
|
|
357
|
+
mockStandardizedSuccess(mockResponses.batchTrackEventResponse)
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const timestamp = new Date(Date.now() - 24 * 60 * 60 * 1000); // 1 day ago
|
|
361
|
+
const events = [
|
|
362
|
+
{ eventName: 'event_1', userId: 'user_1', timestamp },
|
|
363
|
+
{ eventName: 'event_2', userId: 'user_2' },
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
await client.events.trackBatch(events);
|
|
367
|
+
|
|
368
|
+
const callBody = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
369
|
+
expect(callBody.events[0].timestamp).toBe(timestamp.toISOString());
|
|
370
|
+
expect(callBody.events[1].timestamp).toBeUndefined();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should throw ValidationError when any batch event has timestamp older than 7 days', async () => {
|
|
374
|
+
const oldTimestamp = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000); // 8 days ago
|
|
375
|
+
const events = [
|
|
376
|
+
{ eventName: 'event_1', userId: 'user_1' },
|
|
377
|
+
{ eventName: 'event_2', userId: 'user_2', timestamp: oldTimestamp },
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
await expect(
|
|
381
|
+
client.events.trackBatch(events)
|
|
382
|
+
).rejects.toThrow(ValidationError);
|
|
383
|
+
|
|
384
|
+
await expect(
|
|
385
|
+
client.events.trackBatch(events)
|
|
386
|
+
).rejects.toThrow('Event at index 1: Custom timestamp cannot be more than 7 days in the past');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should handle batch response with mixed results', async () => {
|
|
390
|
+
global.fetch.mockResolvedValue(
|
|
391
|
+
mockStandardizedSuccess(mockResponses.batchTrackEventWithErrorResponse)
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const events = [
|
|
395
|
+
{ eventName: 'event_1', userId: 'user_1' },
|
|
396
|
+
{ eventName: '', userId: 'user_2' },
|
|
397
|
+
{ eventName: 'event_3', userId: 'user_3' },
|
|
398
|
+
];
|
|
399
|
+
|
|
400
|
+
const result = await client.events.trackBatch(events);
|
|
401
|
+
|
|
402
|
+
expect(result.results[0].status).toBe('queued');
|
|
403
|
+
expect(result.results[1].status).toBe('error');
|
|
404
|
+
expect(result.results[1].error).toBe('Invalid event name');
|
|
405
|
+
expect(result.results[2].status).toBe('queued');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should handle events with empty properties', async () => {
|
|
409
|
+
global.fetch.mockResolvedValue(
|
|
410
|
+
mockStandardizedSuccess(mockResponses.batchTrackEventResponse)
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const events = [
|
|
414
|
+
{ eventName: 'event_1', userId: 'user_1' },
|
|
415
|
+
{ eventName: 'event_2', userId: 'user_2', properties: {} },
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
await client.events.trackBatch(events);
|
|
419
|
+
|
|
420
|
+
const callBody = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
421
|
+
expect(callBody.events[0].properties).toEqual({});
|
|
422
|
+
expect(callBody.events[1].properties).toEqual({});
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
describe('trackLegacy (deprecated)', () => {
|
|
427
|
+
it('should log deprecation warning when called', async () => {
|
|
428
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
429
|
+
global.fetch.mockResolvedValue(
|
|
430
|
+
mockStandardizedSuccess(mockResponses.trackEventResponse)
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
await client.events.trackLegacy('user_login', 'user_123');
|
|
434
|
+
|
|
435
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
436
|
+
expect.stringContaining('DEPRECATION WARNING')
|
|
437
|
+
);
|
|
438
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
439
|
+
expect.stringContaining('/v1/event')
|
|
440
|
+
);
|
|
441
|
+
warnSpy.mockRestore();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('should use the legacy /event endpoint', async () => {
|
|
445
|
+
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
446
|
+
global.fetch.mockResolvedValue(
|
|
447
|
+
mockStandardizedSuccess(mockResponses.trackEventResponse)
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
await client.events.trackLegacy('user_login', 'user_123');
|
|
451
|
+
|
|
452
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
453
|
+
expect.stringContaining('/event'),
|
|
454
|
+
expect.any(Object)
|
|
455
|
+
);
|
|
456
|
+
// Should NOT contain /events (plural)
|
|
457
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
458
|
+
expect(callUrl).not.toContain('/events');
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
223
462
|
});
|
|
@@ -43,6 +43,93 @@ describe('Leaderboards Resource', () => {
|
|
|
43
43
|
expect(callUrl).toContain('limit=25');
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
it('should support options object signature', async () => {
|
|
47
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardWithFiltersResponse));
|
|
48
|
+
|
|
49
|
+
await client.leaderboards.getGlobal({
|
|
50
|
+
timeframe: 'weekly',
|
|
51
|
+
page: 2,
|
|
52
|
+
limit: 25,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
56
|
+
expect(callUrl).toContain('timeframe=weekly');
|
|
57
|
+
expect(callUrl).toContain('page=2');
|
|
58
|
+
expect(callUrl).toContain('limit=25');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should filter by persona', async () => {
|
|
62
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardWithFiltersResponse));
|
|
63
|
+
|
|
64
|
+
await client.leaderboards.getGlobal({
|
|
65
|
+
persona: 'Achiever',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
69
|
+
expect(callUrl).toContain('persona=Achiever');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should filter by level range', async () => {
|
|
73
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardWithFiltersResponse));
|
|
74
|
+
|
|
75
|
+
await client.leaderboards.getGlobal({
|
|
76
|
+
minLevel: 2,
|
|
77
|
+
maxLevel: 5,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
81
|
+
expect(callUrl).toContain('min_level=2');
|
|
82
|
+
expect(callUrl).toContain('max_level=5');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should filter by date range with Date objects', async () => {
|
|
86
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardWithFiltersResponse));
|
|
87
|
+
|
|
88
|
+
const startDate = new Date('2024-01-01T00:00:00Z');
|
|
89
|
+
const endDate = new Date('2024-01-31T23:59:59Z');
|
|
90
|
+
|
|
91
|
+
await client.leaderboards.getGlobal({
|
|
92
|
+
startDate,
|
|
93
|
+
endDate,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
97
|
+
expect(callUrl).toContain('start_date=2024-01-01T00%3A00%3A00.000Z');
|
|
98
|
+
expect(callUrl).toContain('end_date=2024-01-31T23%3A59%3A59.000Z');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should filter by date range with ISO strings', async () => {
|
|
102
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardWithFiltersResponse));
|
|
103
|
+
|
|
104
|
+
await client.leaderboards.getGlobal({
|
|
105
|
+
startDate: '2024-01-01T00:00:00Z',
|
|
106
|
+
endDate: '2024-01-31T23:59:59Z',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
110
|
+
expect(callUrl).toContain('start_date=');
|
|
111
|
+
expect(callUrl).toContain('end_date=');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should parse cache metadata in response', async () => {
|
|
115
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardWithFiltersResponse));
|
|
116
|
+
|
|
117
|
+
const result = await client.leaderboards.getGlobal();
|
|
118
|
+
|
|
119
|
+
expect(result.cacheMetadata).toBeDefined();
|
|
120
|
+
expect(result.cacheMetadata.cachedAt).toBeInstanceOf(Date);
|
|
121
|
+
expect(result.cacheMetadata.ttl).toBe(300);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should include percentile in rankings', async () => {
|
|
125
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardWithFiltersResponse));
|
|
126
|
+
|
|
127
|
+
const result = await client.leaderboards.getGlobal();
|
|
128
|
+
|
|
129
|
+
expect(result.rankings[0].percentile).toBe(99.5);
|
|
130
|
+
expect(result.rankings[1].percentile).toBe(98.0);
|
|
131
|
+
});
|
|
132
|
+
|
|
46
133
|
it('should throw error for invalid timeframe', async () => {
|
|
47
134
|
global.fetch.mockResolvedValue(
|
|
48
135
|
mockErrorResponse(400, mockErrors.invalidTimeframeError.message)
|
|
@@ -52,6 +139,20 @@ describe('Leaderboards Resource', () => {
|
|
|
52
139
|
'Timeframe must be one of'
|
|
53
140
|
);
|
|
54
141
|
});
|
|
142
|
+
|
|
143
|
+
it('should support combined filters with legacy signature', async () => {
|
|
144
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardWithFiltersResponse));
|
|
145
|
+
|
|
146
|
+
await client.leaderboards.getGlobal('weekly', 1, 50, {
|
|
147
|
+
persona: 'Explorer',
|
|
148
|
+
minLevel: 3,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
152
|
+
expect(callUrl).toContain('timeframe=weekly');
|
|
153
|
+
expect(callUrl).toContain('persona=Explorer');
|
|
154
|
+
expect(callUrl).toContain('min_level=3');
|
|
155
|
+
});
|
|
55
156
|
});
|
|
56
157
|
|
|
57
158
|
describe('list', () => {
|
|
@@ -72,6 +173,21 @@ describe('Leaderboards Resource', () => {
|
|
|
72
173
|
const callUrl = global.fetch.mock.calls[0][0];
|
|
73
174
|
expect(callUrl).toContain('search=top');
|
|
74
175
|
});
|
|
176
|
+
|
|
177
|
+
it('should support options object signature', async () => {
|
|
178
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardsListResponse));
|
|
179
|
+
|
|
180
|
+
await client.leaderboards.list({
|
|
181
|
+
page: 2,
|
|
182
|
+
limit: 25,
|
|
183
|
+
search: 'weekly',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
187
|
+
expect(callUrl).toContain('page=2');
|
|
188
|
+
expect(callUrl).toContain('limit=25');
|
|
189
|
+
expect(callUrl).toContain('search=weekly');
|
|
190
|
+
});
|
|
75
191
|
});
|
|
76
192
|
|
|
77
193
|
describe('getCustom', () => {
|
|
@@ -84,6 +200,48 @@ describe('Leaderboards Resource', () => {
|
|
|
84
200
|
expect(result.rankings).toBeDefined();
|
|
85
201
|
});
|
|
86
202
|
|
|
203
|
+
it('should support options object signature with filters', async () => {
|
|
204
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardWithFiltersResponse));
|
|
205
|
+
|
|
206
|
+
await client.leaderboards.getCustom('lb1', {
|
|
207
|
+
page: 2,
|
|
208
|
+
limit: 25,
|
|
209
|
+
persona: 'Competitor',
|
|
210
|
+
minLevel: 1,
|
|
211
|
+
maxLevel: 10,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
215
|
+
expect(callUrl).toContain('/leaderboards/lb1');
|
|
216
|
+
expect(callUrl).toContain('page=2');
|
|
217
|
+
expect(callUrl).toContain('limit=25');
|
|
218
|
+
expect(callUrl).toContain('persona=Competitor');
|
|
219
|
+
expect(callUrl).toContain('min_level=1');
|
|
220
|
+
expect(callUrl).toContain('max_level=10');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should filter by date range', async () => {
|
|
224
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardWithFiltersResponse));
|
|
225
|
+
|
|
226
|
+
await client.leaderboards.getCustom('lb1', {
|
|
227
|
+
startDate: new Date('2024-01-01'),
|
|
228
|
+
endDate: new Date('2024-01-31'),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
232
|
+
expect(callUrl).toContain('start_date=');
|
|
233
|
+
expect(callUrl).toContain('end_date=');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should parse cache metadata', async () => {
|
|
237
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardWithFiltersResponse));
|
|
238
|
+
|
|
239
|
+
const result = await client.leaderboards.getCustom('lb1');
|
|
240
|
+
|
|
241
|
+
expect(result.cacheMetadata).toBeDefined();
|
|
242
|
+
expect(result.cacheMetadata.cachedAt).toBeInstanceOf(Date);
|
|
243
|
+
});
|
|
244
|
+
|
|
87
245
|
it('should throw 404 for non-existent leaderboard', async () => {
|
|
88
246
|
global.fetch.mockResolvedValue(mockErrorResponse(404, 'Leaderboard not found'));
|
|
89
247
|
|
|
@@ -91,6 +249,18 @@ describe('Leaderboards Resource', () => {
|
|
|
91
249
|
'Leaderboard not found'
|
|
92
250
|
);
|
|
93
251
|
});
|
|
252
|
+
|
|
253
|
+
it('should support legacy signature with additional options', async () => {
|
|
254
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardWithFiltersResponse));
|
|
255
|
+
|
|
256
|
+
await client.leaderboards.getCustom('lb1', 1, 50, 'search', {
|
|
257
|
+
persona: 'Achiever',
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
261
|
+
expect(callUrl).toContain('search=search');
|
|
262
|
+
expect(callUrl).toContain('persona=Achiever');
|
|
263
|
+
});
|
|
94
264
|
});
|
|
95
265
|
|
|
96
266
|
describe('getUserRank', () => {
|
|
@@ -102,6 +272,23 @@ describe('Leaderboards Resource', () => {
|
|
|
102
272
|
expect(result.rank).toBe(42);
|
|
103
273
|
});
|
|
104
274
|
|
|
275
|
+
it('should include percentile in response', async () => {
|
|
276
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.userRankWithPercentileResponse));
|
|
277
|
+
|
|
278
|
+
const result = await client.leaderboards.getUserRank('lb1', 'user_123');
|
|
279
|
+
|
|
280
|
+
expect(result.rank).toBe(42);
|
|
281
|
+
expect(result.percentile).toBe(95.8);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should return null percentile when not available', async () => {
|
|
285
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.userRankResponse));
|
|
286
|
+
|
|
287
|
+
const result = await client.leaderboards.getUserRank('lb1', 'user_123');
|
|
288
|
+
|
|
289
|
+
expect(result.percentile).toBeNull();
|
|
290
|
+
});
|
|
291
|
+
|
|
105
292
|
it('should throw 404 for non-existent user or leaderboard', async () => {
|
|
106
293
|
global.fetch.mockResolvedValue(mockErrorResponse(404, 'Not found'));
|
|
107
294
|
|
|
@@ -110,4 +297,66 @@ describe('Leaderboards Resource', () => {
|
|
|
110
297
|
).rejects.toThrow('Not found');
|
|
111
298
|
});
|
|
112
299
|
});
|
|
300
|
+
|
|
301
|
+
describe('getAroundUser', () => {
|
|
302
|
+
it('should get leaderboard entries around user with default range', async () => {
|
|
303
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.aroundUserResponse));
|
|
304
|
+
|
|
305
|
+
const result = await client.leaderboards.getAroundUser('lb1', 'user123');
|
|
306
|
+
|
|
307
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
308
|
+
expect(callUrl).toContain('/leaderboards/lb1/users/user123/around');
|
|
309
|
+
expect(callUrl).toContain('range=5');
|
|
310
|
+
expect(result.rankings).toHaveLength(5);
|
|
311
|
+
expect(result.user).toBeDefined();
|
|
312
|
+
expect(result.user.rank).toBe(42);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should get leaderboard entries around user with custom range', async () => {
|
|
316
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.aroundUserResponse));
|
|
317
|
+
|
|
318
|
+
await client.leaderboards.getAroundUser('lb1', 'user123', 10);
|
|
319
|
+
|
|
320
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
321
|
+
expect(callUrl).toContain('range=10');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should include percentile for all entries', async () => {
|
|
325
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.aroundUserResponse));
|
|
326
|
+
|
|
327
|
+
const result = await client.leaderboards.getAroundUser('lb1', 'user123');
|
|
328
|
+
|
|
329
|
+
result.rankings.forEach(entry => {
|
|
330
|
+
expect(entry.percentile).toBeDefined();
|
|
331
|
+
expect(typeof entry.percentile).toBe('number');
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should parse cache metadata', async () => {
|
|
336
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.aroundUserResponse));
|
|
337
|
+
|
|
338
|
+
const result = await client.leaderboards.getAroundUser('lb1', 'user123');
|
|
339
|
+
|
|
340
|
+
expect(result.cacheMetadata).toBeDefined();
|
|
341
|
+
expect(result.cacheMetadata.cachedAt).toBeInstanceOf(Date);
|
|
342
|
+
expect(result.cacheMetadata.ttl).toBe(60);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should URL encode user ID', async () => {
|
|
346
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.aroundUserResponse));
|
|
347
|
+
|
|
348
|
+
await client.leaderboards.getAroundUser('lb1', 'user@example.com');
|
|
349
|
+
|
|
350
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
351
|
+
expect(callUrl).toContain('user%40example.com');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should throw 404 for non-existent user', async () => {
|
|
355
|
+
global.fetch.mockResolvedValue(mockErrorResponse(404, 'User not found'));
|
|
356
|
+
|
|
357
|
+
await expect(
|
|
358
|
+
client.leaderboards.getAroundUser('lb1', 'invalid_user')
|
|
359
|
+
).rejects.toThrow('User not found');
|
|
360
|
+
});
|
|
361
|
+
});
|
|
113
362
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Rooguys from '../../index';
|
|
2
|
-
import {
|
|
2
|
+
import { mockStandardizedSuccess, mockErrorResponse } from '../utils/mockClient';
|
|
3
3
|
import { mockResponses } from '../fixtures/responses';
|
|
4
4
|
|
|
5
5
|
describe('Questionnaires Resource', () => {
|
|
@@ -13,12 +13,12 @@ describe('Questionnaires Resource', () => {
|
|
|
13
13
|
|
|
14
14
|
describe('get', () => {
|
|
15
15
|
it('should get questionnaire by slug', async () => {
|
|
16
|
-
global.fetch.mockResolvedValue(
|
|
16
|
+
global.fetch.mockResolvedValue(mockStandardizedSuccess(mockResponses.questionnaireResponse));
|
|
17
17
|
|
|
18
18
|
const result = await client.questionnaires.get('user-persona');
|
|
19
19
|
|
|
20
20
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
21
|
-
expect.stringContaining('/
|
|
21
|
+
expect.stringContaining('/questionnaires/user-persona'),
|
|
22
22
|
expect.any(Object)
|
|
23
23
|
);
|
|
24
24
|
expect(result.slug).toBe('user-persona');
|
|
@@ -33,7 +33,7 @@ describe('Questionnaires Resource', () => {
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
it('should handle questionnaire with multiple questions', async () => {
|
|
36
|
-
global.fetch.mockResolvedValue(
|
|
36
|
+
global.fetch.mockResolvedValue(mockStandardizedSuccess(mockResponses.questionnaireResponse));
|
|
37
37
|
|
|
38
38
|
const result = await client.questionnaires.get('user-persona');
|
|
39
39
|
|
|
@@ -44,12 +44,12 @@ describe('Questionnaires Resource', () => {
|
|
|
44
44
|
|
|
45
45
|
describe('getActive', () => {
|
|
46
46
|
it('should get active questionnaire', async () => {
|
|
47
|
-
global.fetch.mockResolvedValue(
|
|
47
|
+
global.fetch.mockResolvedValue(mockStandardizedSuccess(mockResponses.questionnaireResponse));
|
|
48
48
|
|
|
49
49
|
const result = await client.questionnaires.getActive();
|
|
50
50
|
|
|
51
51
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
52
|
-
expect.stringContaining('/
|
|
52
|
+
expect.stringContaining('/questionnaires/active'),
|
|
53
53
|
expect.any(Object)
|
|
54
54
|
);
|
|
55
55
|
expect(result.is_active).toBe(true);
|