@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.
Files changed (28) hide show
  1. package/README.md +342 -141
  2. package/package.json +1 -1
  3. package/src/__tests__/fixtures/responses.js +249 -0
  4. package/src/__tests__/property/batch-event-validation.property.test.js +225 -0
  5. package/src/__tests__/property/email-validation.property.test.js +272 -0
  6. package/src/__tests__/property/error-mapping.property.test.js +506 -0
  7. package/src/__tests__/property/field-selection.property.test.js +297 -0
  8. package/src/__tests__/property/idempotency-key.property.test.js +350 -0
  9. package/src/__tests__/property/leaderboard-filter.property.test.js +585 -0
  10. package/src/__tests__/property/partial-update.property.test.js +251 -0
  11. package/src/__tests__/property/rate-limit-error.property.test.js +276 -0
  12. package/src/__tests__/property/rate-limit-extraction.property.test.js +193 -0
  13. package/src/__tests__/property/request-construction.property.test.js +20 -28
  14. package/src/__tests__/property/response-format.property.test.js +418 -0
  15. package/src/__tests__/property/response-parsing.property.test.js +16 -21
  16. package/src/__tests__/property/timestamp-validation.property.test.js +345 -0
  17. package/src/__tests__/unit/aha.test.js +57 -26
  18. package/src/__tests__/unit/config.test.js +7 -1
  19. package/src/__tests__/unit/errors.test.js +6 -8
  20. package/src/__tests__/unit/events.test.js +253 -14
  21. package/src/__tests__/unit/leaderboards.test.js +249 -0
  22. package/src/__tests__/unit/questionnaires.test.js +6 -6
  23. package/src/__tests__/unit/users.test.js +275 -12
  24. package/src/__tests__/utils/generators.js +87 -0
  25. package/src/__tests__/utils/mockClient.js +71 -5
  26. package/src/errors.js +156 -0
  27. package/src/http-client.js +276 -0
  28. package/src/index.js +856 -66
@@ -0,0 +1,585 @@
1
+ /**
2
+ * Property-Based Test: Leaderboard Filter Query Construction
3
+ * Feature: sdk-documentation-update, Property 10: Leaderboard Filter Query Construction
4
+ * Validates: Requirements 6.1, 6.2, 6.3
5
+ *
6
+ * For any leaderboard request with filter parameters (persona, minLevel, maxLevel,
7
+ * startDate, endDate), the SDK SHALL include the corresponding query parameters
8
+ * with correctly formatted values (dates as ISO 8601).
9
+ */
10
+
11
+ import fc from 'fast-check';
12
+ import { jest } from '@jest/globals';
13
+ import Rooguys from '../../index.js';
14
+ import { createMockFetch, createMockHeaders } from '../utils/mockClient.js';
15
+
16
+ describe('Property 10: Leaderboard Filter Query Construction', () => {
17
+ let mockFetch;
18
+
19
+ beforeEach(() => {
20
+ mockFetch = createMockFetch();
21
+ global.fetch = mockFetch;
22
+ });
23
+
24
+ afterEach(() => {
25
+ jest.clearAllMocks();
26
+ });
27
+
28
+ const createSuccessResponse = (data) => ({
29
+ ok: true,
30
+ headers: createMockHeaders({
31
+ 'X-RateLimit-Limit': '1000',
32
+ 'X-RateLimit-Remaining': '950',
33
+ 'X-RateLimit-Reset': '1704067200',
34
+ }),
35
+ json: async () => ({
36
+ success: true,
37
+ data,
38
+ }),
39
+ });
40
+
41
+ const leaderboardResponse = {
42
+ rankings: [],
43
+ pagination: { page: 1, limit: 50, total: 0, totalPages: 0 },
44
+ };
45
+
46
+ // Generator for valid persona names
47
+ const validPersona = fc.constantFrom(
48
+ 'Achiever',
49
+ 'Explorer',
50
+ 'Competitor',
51
+ 'Socializer'
52
+ );
53
+
54
+ // Generator for level numbers
55
+ const levelNumber = fc.integer({ min: 1, max: 100 });
56
+
57
+ // Generator for valid dates within a reasonable range
58
+ const validDate = fc.date({
59
+ min: new Date('2020-01-01'),
60
+ max: new Date('2030-12-31'),
61
+ });
62
+
63
+ // Generator for timeframe
64
+ const timeframe = fc.constantFrom('all-time', 'weekly', 'monthly');
65
+
66
+ // Generator for pagination
67
+ const pagination = fc.record({
68
+ page: fc.integer({ min: 1, max: 100 }),
69
+ limit: fc.integer({ min: 1, max: 100 }),
70
+ });
71
+
72
+ describe('Persona Filter (Requirement 6.1)', () => {
73
+ it('should include persona query parameter when provided', async () => {
74
+ await fc.assert(
75
+ fc.asyncProperty(
76
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
77
+ validPersona,
78
+ async (apiKey, persona) => {
79
+ // Arrange
80
+ mockFetch.mockClear();
81
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
82
+ const sdk = new Rooguys(apiKey);
83
+
84
+ // Act
85
+ await sdk.leaderboards.getGlobal({ persona });
86
+
87
+ // Assert
88
+ expect(mockFetch).toHaveBeenCalledTimes(1);
89
+ const callUrl = mockFetch.mock.calls[0][0];
90
+ const url = new URL(callUrl);
91
+
92
+ expect(url.searchParams.get('persona')).toBe(persona);
93
+ }
94
+ ),
95
+ { numRuns: 100 }
96
+ );
97
+ });
98
+
99
+ it('should not include persona parameter when not provided', async () => {
100
+ await fc.assert(
101
+ fc.asyncProperty(
102
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
103
+ async (apiKey) => {
104
+ // Arrange
105
+ mockFetch.mockClear();
106
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
107
+ const sdk = new Rooguys(apiKey);
108
+
109
+ // Act
110
+ await sdk.leaderboards.getGlobal({});
111
+
112
+ // Assert
113
+ const callUrl = mockFetch.mock.calls[0][0];
114
+ const url = new URL(callUrl);
115
+
116
+ expect(url.searchParams.has('persona')).toBe(false);
117
+ }
118
+ ),
119
+ { numRuns: 100 }
120
+ );
121
+ });
122
+
123
+ it('should include persona filter in custom leaderboard requests', async () => {
124
+ await fc.assert(
125
+ fc.asyncProperty(
126
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
127
+ fc.uuid(), // Leaderboard ID
128
+ validPersona,
129
+ async (apiKey, leaderboardId, persona) => {
130
+ // Arrange
131
+ mockFetch.mockClear();
132
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
133
+ const sdk = new Rooguys(apiKey);
134
+
135
+ // Act
136
+ await sdk.leaderboards.getCustom(leaderboardId, { persona });
137
+
138
+ // Assert
139
+ const callUrl = mockFetch.mock.calls[0][0];
140
+ const url = new URL(callUrl);
141
+
142
+ expect(url.searchParams.get('persona')).toBe(persona);
143
+ }
144
+ ),
145
+ { numRuns: 100 }
146
+ );
147
+ });
148
+ });
149
+
150
+ describe('Level Range Filter (Requirement 6.2)', () => {
151
+ it('should include minLevel query parameter when provided', async () => {
152
+ await fc.assert(
153
+ fc.asyncProperty(
154
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
155
+ levelNumber,
156
+ async (apiKey, minLevel) => {
157
+ // Arrange
158
+ mockFetch.mockClear();
159
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
160
+ const sdk = new Rooguys(apiKey);
161
+
162
+ // Act
163
+ await sdk.leaderboards.getGlobal({ minLevel });
164
+
165
+ // Assert
166
+ const callUrl = mockFetch.mock.calls[0][0];
167
+ const url = new URL(callUrl);
168
+
169
+ expect(url.searchParams.get('min_level')).toBe(String(minLevel));
170
+ }
171
+ ),
172
+ { numRuns: 100 }
173
+ );
174
+ });
175
+
176
+ it('should include maxLevel query parameter when provided', async () => {
177
+ await fc.assert(
178
+ fc.asyncProperty(
179
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
180
+ levelNumber,
181
+ async (apiKey, maxLevel) => {
182
+ // Arrange
183
+ mockFetch.mockClear();
184
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
185
+ const sdk = new Rooguys(apiKey);
186
+
187
+ // Act
188
+ await sdk.leaderboards.getGlobal({ maxLevel });
189
+
190
+ // Assert
191
+ const callUrl = mockFetch.mock.calls[0][0];
192
+ const url = new URL(callUrl);
193
+
194
+ expect(url.searchParams.get('max_level')).toBe(String(maxLevel));
195
+ }
196
+ ),
197
+ { numRuns: 100 }
198
+ );
199
+ });
200
+
201
+ it('should include both minLevel and maxLevel when provided', async () => {
202
+ await fc.assert(
203
+ fc.asyncProperty(
204
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
205
+ levelNumber,
206
+ levelNumber,
207
+ async (apiKey, minLevel, maxLevel) => {
208
+ // Arrange
209
+ mockFetch.mockClear();
210
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
211
+ const sdk = new Rooguys(apiKey);
212
+
213
+ // Act
214
+ await sdk.leaderboards.getGlobal({ minLevel, maxLevel });
215
+
216
+ // Assert
217
+ const callUrl = mockFetch.mock.calls[0][0];
218
+ const url = new URL(callUrl);
219
+
220
+ expect(url.searchParams.get('min_level')).toBe(String(minLevel));
221
+ expect(url.searchParams.get('max_level')).toBe(String(maxLevel));
222
+ }
223
+ ),
224
+ { numRuns: 100 }
225
+ );
226
+ });
227
+
228
+ it('should not include level parameters when not provided', async () => {
229
+ await fc.assert(
230
+ fc.asyncProperty(
231
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
232
+ async (apiKey) => {
233
+ // Arrange
234
+ mockFetch.mockClear();
235
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
236
+ const sdk = new Rooguys(apiKey);
237
+
238
+ // Act
239
+ await sdk.leaderboards.getGlobal({});
240
+
241
+ // Assert
242
+ const callUrl = mockFetch.mock.calls[0][0];
243
+ const url = new URL(callUrl);
244
+
245
+ expect(url.searchParams.has('min_level')).toBe(false);
246
+ expect(url.searchParams.has('max_level')).toBe(false);
247
+ }
248
+ ),
249
+ { numRuns: 100 }
250
+ );
251
+ });
252
+ });
253
+
254
+ describe('Date Range Filter (Requirement 6.3)', () => {
255
+ it('should format startDate as ISO 8601 string', async () => {
256
+ await fc.assert(
257
+ fc.asyncProperty(
258
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
259
+ validDate,
260
+ async (apiKey, startDate) => {
261
+ // Arrange
262
+ mockFetch.mockClear();
263
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
264
+ const sdk = new Rooguys(apiKey);
265
+
266
+ // Act
267
+ await sdk.leaderboards.getGlobal({ startDate });
268
+
269
+ // Assert
270
+ const callUrl = mockFetch.mock.calls[0][0];
271
+ const url = new URL(callUrl);
272
+ const startDateParam = url.searchParams.get('start_date');
273
+
274
+ expect(startDateParam).toBe(startDate.toISOString());
275
+
276
+ // Verify it's a valid ISO 8601 date
277
+ const parsedDate = new Date(startDateParam);
278
+ expect(parsedDate.getTime()).toBe(startDate.getTime());
279
+ }
280
+ ),
281
+ { numRuns: 100 }
282
+ );
283
+ });
284
+
285
+ it('should format endDate as ISO 8601 string', async () => {
286
+ await fc.assert(
287
+ fc.asyncProperty(
288
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
289
+ validDate,
290
+ async (apiKey, endDate) => {
291
+ // Arrange
292
+ mockFetch.mockClear();
293
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
294
+ const sdk = new Rooguys(apiKey);
295
+
296
+ // Act
297
+ await sdk.leaderboards.getGlobal({ endDate });
298
+
299
+ // Assert
300
+ const callUrl = mockFetch.mock.calls[0][0];
301
+ const url = new URL(callUrl);
302
+ const endDateParam = url.searchParams.get('end_date');
303
+
304
+ expect(endDateParam).toBe(endDate.toISOString());
305
+
306
+ // Verify it's a valid ISO 8601 date
307
+ const parsedDate = new Date(endDateParam);
308
+ expect(parsedDate.getTime()).toBe(endDate.getTime());
309
+ }
310
+ ),
311
+ { numRuns: 100 }
312
+ );
313
+ });
314
+
315
+ it('should include both startDate and endDate when provided', async () => {
316
+ await fc.assert(
317
+ fc.asyncProperty(
318
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
319
+ validDate,
320
+ validDate,
321
+ async (apiKey, startDate, endDate) => {
322
+ // Arrange
323
+ mockFetch.mockClear();
324
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
325
+ const sdk = new Rooguys(apiKey);
326
+
327
+ // Act
328
+ await sdk.leaderboards.getGlobal({ startDate, endDate });
329
+
330
+ // Assert
331
+ const callUrl = mockFetch.mock.calls[0][0];
332
+ const url = new URL(callUrl);
333
+
334
+ expect(url.searchParams.get('start_date')).toBe(startDate.toISOString());
335
+ expect(url.searchParams.get('end_date')).toBe(endDate.toISOString());
336
+ }
337
+ ),
338
+ { numRuns: 100 }
339
+ );
340
+ });
341
+
342
+ it('should accept ISO string dates and convert them correctly', async () => {
343
+ await fc.assert(
344
+ fc.asyncProperty(
345
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
346
+ validDate,
347
+ async (apiKey, date) => {
348
+ // Arrange
349
+ mockFetch.mockClear();
350
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
351
+ const sdk = new Rooguys(apiKey);
352
+ const isoString = date.toISOString();
353
+
354
+ // Act
355
+ await sdk.leaderboards.getGlobal({ startDate: isoString });
356
+
357
+ // Assert
358
+ const callUrl = mockFetch.mock.calls[0][0];
359
+ const url = new URL(callUrl);
360
+ const startDateParam = url.searchParams.get('start_date');
361
+
362
+ // Should be a valid ISO 8601 date
363
+ const parsedDate = new Date(startDateParam);
364
+ expect(parsedDate.getTime()).toBe(date.getTime());
365
+ }
366
+ ),
367
+ { numRuns: 100 }
368
+ );
369
+ });
370
+
371
+ it('should not include date parameters when not provided', async () => {
372
+ await fc.assert(
373
+ fc.asyncProperty(
374
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
375
+ async (apiKey) => {
376
+ // Arrange
377
+ mockFetch.mockClear();
378
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
379
+ const sdk = new Rooguys(apiKey);
380
+
381
+ // Act
382
+ await sdk.leaderboards.getGlobal({});
383
+
384
+ // Assert
385
+ const callUrl = mockFetch.mock.calls[0][0];
386
+ const url = new URL(callUrl);
387
+
388
+ expect(url.searchParams.has('start_date')).toBe(false);
389
+ expect(url.searchParams.has('end_date')).toBe(false);
390
+ }
391
+ ),
392
+ { numRuns: 100 }
393
+ );
394
+ });
395
+ });
396
+
397
+ describe('Combined Filters', () => {
398
+ it('should include all filter parameters when all are provided', async () => {
399
+ await fc.assert(
400
+ fc.asyncProperty(
401
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
402
+ validPersona,
403
+ levelNumber,
404
+ levelNumber,
405
+ validDate,
406
+ validDate,
407
+ timeframe,
408
+ pagination,
409
+ async (apiKey, persona, minLevel, maxLevel, startDate, endDate, tf, pag) => {
410
+ // Arrange
411
+ mockFetch.mockClear();
412
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
413
+ const sdk = new Rooguys(apiKey);
414
+
415
+ // Act
416
+ await sdk.leaderboards.getGlobal({
417
+ timeframe: tf,
418
+ page: pag.page,
419
+ limit: pag.limit,
420
+ persona,
421
+ minLevel,
422
+ maxLevel,
423
+ startDate,
424
+ endDate,
425
+ });
426
+
427
+ // Assert
428
+ const callUrl = mockFetch.mock.calls[0][0];
429
+ const url = new URL(callUrl);
430
+
431
+ // Verify all parameters are present
432
+ expect(url.searchParams.get('timeframe')).toBe(tf);
433
+ expect(url.searchParams.get('page')).toBe(String(pag.page));
434
+ expect(url.searchParams.get('limit')).toBe(String(pag.limit));
435
+ expect(url.searchParams.get('persona')).toBe(persona);
436
+ expect(url.searchParams.get('min_level')).toBe(String(minLevel));
437
+ expect(url.searchParams.get('max_level')).toBe(String(maxLevel));
438
+ expect(url.searchParams.get('start_date')).toBe(startDate.toISOString());
439
+ expect(url.searchParams.get('end_date')).toBe(endDate.toISOString());
440
+ }
441
+ ),
442
+ { numRuns: 100 }
443
+ );
444
+ });
445
+
446
+ it('should work with custom leaderboard and all filters', async () => {
447
+ await fc.assert(
448
+ fc.asyncProperty(
449
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
450
+ fc.uuid(), // Leaderboard ID
451
+ validPersona,
452
+ levelNumber,
453
+ levelNumber,
454
+ validDate,
455
+ validDate,
456
+ async (apiKey, leaderboardId, persona, minLevel, maxLevel, startDate, endDate) => {
457
+ // Arrange
458
+ mockFetch.mockClear();
459
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
460
+ const sdk = new Rooguys(apiKey);
461
+
462
+ // Act
463
+ await sdk.leaderboards.getCustom(leaderboardId, {
464
+ persona,
465
+ minLevel,
466
+ maxLevel,
467
+ startDate,
468
+ endDate,
469
+ });
470
+
471
+ // Assert
472
+ const callUrl = mockFetch.mock.calls[0][0];
473
+ const url = new URL(callUrl);
474
+
475
+ // Verify endpoint contains leaderboard ID
476
+ expect(url.pathname).toContain(leaderboardId);
477
+
478
+ // Verify all filter parameters are present
479
+ expect(url.searchParams.get('persona')).toBe(persona);
480
+ expect(url.searchParams.get('min_level')).toBe(String(minLevel));
481
+ expect(url.searchParams.get('max_level')).toBe(String(maxLevel));
482
+ expect(url.searchParams.get('start_date')).toBe(startDate.toISOString());
483
+ expect(url.searchParams.get('end_date')).toBe(endDate.toISOString());
484
+ }
485
+ ),
486
+ { numRuns: 100 }
487
+ );
488
+ });
489
+
490
+ it('should only include provided filters (partial filter set)', async () => {
491
+ await fc.assert(
492
+ fc.asyncProperty(
493
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
494
+ fc.option(validPersona, { nil: undefined }),
495
+ fc.option(levelNumber, { nil: undefined }),
496
+ fc.option(levelNumber, { nil: undefined }),
497
+ fc.option(validDate, { nil: undefined }),
498
+ fc.option(validDate, { nil: undefined }),
499
+ async (apiKey, persona, minLevel, maxLevel, startDate, endDate) => {
500
+ // Arrange
501
+ mockFetch.mockClear();
502
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
503
+ const sdk = new Rooguys(apiKey);
504
+
505
+ const options = {};
506
+ if (persona !== undefined) options.persona = persona;
507
+ if (minLevel !== undefined) options.minLevel = minLevel;
508
+ if (maxLevel !== undefined) options.maxLevel = maxLevel;
509
+ if (startDate !== undefined) options.startDate = startDate;
510
+ if (endDate !== undefined) options.endDate = endDate;
511
+
512
+ // Act
513
+ await sdk.leaderboards.getGlobal(options);
514
+
515
+ // Assert
516
+ const callUrl = mockFetch.mock.calls[0][0];
517
+ const url = new URL(callUrl);
518
+
519
+ // Verify only provided parameters are present
520
+ expect(url.searchParams.has('persona')).toBe(persona !== undefined);
521
+ expect(url.searchParams.has('min_level')).toBe(minLevel !== undefined);
522
+ expect(url.searchParams.has('max_level')).toBe(maxLevel !== undefined);
523
+ expect(url.searchParams.has('start_date')).toBe(startDate !== undefined);
524
+ expect(url.searchParams.has('end_date')).toBe(endDate !== undefined);
525
+
526
+ // Verify values are correct for provided parameters
527
+ if (persona !== undefined) {
528
+ expect(url.searchParams.get('persona')).toBe(persona);
529
+ }
530
+ if (minLevel !== undefined) {
531
+ expect(url.searchParams.get('min_level')).toBe(String(minLevel));
532
+ }
533
+ if (maxLevel !== undefined) {
534
+ expect(url.searchParams.get('max_level')).toBe(String(maxLevel));
535
+ }
536
+ if (startDate !== undefined) {
537
+ expect(url.searchParams.get('start_date')).toBe(startDate.toISOString());
538
+ }
539
+ if (endDate !== undefined) {
540
+ expect(url.searchParams.get('end_date')).toBe(endDate.toISOString());
541
+ }
542
+ }
543
+ ),
544
+ { numRuns: 100 }
545
+ );
546
+ });
547
+ });
548
+
549
+ describe('Null value handling', () => {
550
+ it('should not include parameters with null values', async () => {
551
+ await fc.assert(
552
+ fc.asyncProperty(
553
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
554
+ async (apiKey) => {
555
+ // Arrange
556
+ mockFetch.mockClear();
557
+ mockFetch.mockResolvedValue(createSuccessResponse(leaderboardResponse));
558
+ const sdk = new Rooguys(apiKey);
559
+
560
+ // Act
561
+ await sdk.leaderboards.getGlobal({
562
+ persona: null,
563
+ minLevel: null,
564
+ maxLevel: null,
565
+ startDate: null,
566
+ endDate: null,
567
+ });
568
+
569
+ // Assert
570
+ const callUrl = mockFetch.mock.calls[0][0];
571
+ const url = new URL(callUrl);
572
+
573
+ // Verify null parameters are NOT included
574
+ expect(url.searchParams.has('persona')).toBe(false);
575
+ expect(url.searchParams.has('min_level')).toBe(false);
576
+ expect(url.searchParams.has('max_level')).toBe(false);
577
+ expect(url.searchParams.has('start_date')).toBe(false);
578
+ expect(url.searchParams.has('end_date')).toBe(false);
579
+ }
580
+ ),
581
+ { numRuns: 100 }
582
+ );
583
+ });
584
+ });
585
+ });