@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
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
* Mock API response fixtures for testing
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
// Standardized response format helpers
|
|
6
|
+
export const wrapSuccess = (data, requestId = 'req_test123') => ({
|
|
7
|
+
success: true,
|
|
8
|
+
data,
|
|
9
|
+
request_id: requestId,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const wrapError = (code, message, details = null, requestId = 'req_test123') => ({
|
|
13
|
+
success: false,
|
|
14
|
+
error: {
|
|
15
|
+
code,
|
|
16
|
+
message,
|
|
17
|
+
...(details && { details }),
|
|
18
|
+
},
|
|
19
|
+
request_id: requestId,
|
|
20
|
+
});
|
|
21
|
+
|
|
5
22
|
export const mockResponses = {
|
|
6
23
|
userProfile: {
|
|
7
24
|
user_id: 'user123',
|
|
@@ -65,6 +82,21 @@ export const mockResponses = {
|
|
|
65
82
|
},
|
|
66
83
|
},
|
|
67
84
|
|
|
85
|
+
batchTrackEventResponse: {
|
|
86
|
+
results: [
|
|
87
|
+
{ index: 0, status: 'queued' },
|
|
88
|
+
{ index: 1, status: 'queued' },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
batchTrackEventWithErrorResponse: {
|
|
93
|
+
results: [
|
|
94
|
+
{ index: 0, status: 'queued' },
|
|
95
|
+
{ index: 1, status: 'error', error: 'Invalid event name' },
|
|
96
|
+
{ index: 2, status: 'queued' },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
|
|
68
100
|
leaderboardResponse: {
|
|
69
101
|
timeframe: 'all-time',
|
|
70
102
|
page: 1,
|
|
@@ -276,6 +308,95 @@ export const mockResponses = {
|
|
|
276
308
|
totalPages: 1,
|
|
277
309
|
},
|
|
278
310
|
},
|
|
311
|
+
|
|
312
|
+
// Enhanced leaderboard responses with filters and cache metadata
|
|
313
|
+
leaderboardWithFiltersResponse: {
|
|
314
|
+
timeframe: 'all-time',
|
|
315
|
+
page: 1,
|
|
316
|
+
limit: 50,
|
|
317
|
+
total: 50,
|
|
318
|
+
rankings: [
|
|
319
|
+
{
|
|
320
|
+
rank: 1,
|
|
321
|
+
user_id: 'user1',
|
|
322
|
+
points: 1000,
|
|
323
|
+
percentile: 99.5,
|
|
324
|
+
level: {
|
|
325
|
+
id: 'level3',
|
|
326
|
+
name: 'Gold',
|
|
327
|
+
level_number: 3,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
rank: 2,
|
|
332
|
+
user_id: 'user2',
|
|
333
|
+
points: 900,
|
|
334
|
+
percentile: 98.0,
|
|
335
|
+
level: {
|
|
336
|
+
id: 'level2',
|
|
337
|
+
name: 'Silver',
|
|
338
|
+
level_number: 2,
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
cache_metadata: {
|
|
343
|
+
cached_at: '2024-01-15T10:30:00Z',
|
|
344
|
+
ttl: 300,
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
aroundUserResponse: {
|
|
349
|
+
user: {
|
|
350
|
+
user_id: 'user123',
|
|
351
|
+
rank: 42,
|
|
352
|
+
points: 850,
|
|
353
|
+
percentile: 95.8,
|
|
354
|
+
},
|
|
355
|
+
rankings: [
|
|
356
|
+
{
|
|
357
|
+
rank: 40,
|
|
358
|
+
user_id: 'user40',
|
|
359
|
+
points: 870,
|
|
360
|
+
percentile: 96.0,
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
rank: 41,
|
|
364
|
+
user_id: 'user41',
|
|
365
|
+
points: 860,
|
|
366
|
+
percentile: 95.9,
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
rank: 42,
|
|
370
|
+
user_id: 'user123',
|
|
371
|
+
points: 850,
|
|
372
|
+
percentile: 95.8,
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
rank: 43,
|
|
376
|
+
user_id: 'user43',
|
|
377
|
+
points: 840,
|
|
378
|
+
percentile: 95.7,
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
rank: 44,
|
|
382
|
+
user_id: 'user44',
|
|
383
|
+
points: 830,
|
|
384
|
+
percentile: 95.6,
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
cache_metadata: {
|
|
388
|
+
cached_at: '2024-01-15T10:30:00Z',
|
|
389
|
+
ttl: 60,
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
userRankWithPercentileResponse: {
|
|
394
|
+
user_id: 'user123',
|
|
395
|
+
rank: 42,
|
|
396
|
+
points: 850,
|
|
397
|
+
total_users: 1000,
|
|
398
|
+
percentile: 95.8,
|
|
399
|
+
},
|
|
279
400
|
};
|
|
280
401
|
|
|
281
402
|
export const mockErrors = {
|
|
@@ -328,3 +449,131 @@ export const mockErrors = {
|
|
|
328
449
|
],
|
|
329
450
|
},
|
|
330
451
|
};
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
// Standardized format responses
|
|
455
|
+
export const standardizedResponses = {
|
|
456
|
+
userProfile: wrapSuccess({
|
|
457
|
+
user_id: 'user123',
|
|
458
|
+
points: 100,
|
|
459
|
+
persona: 'Achiever',
|
|
460
|
+
level: {
|
|
461
|
+
id: 'level1',
|
|
462
|
+
name: 'Bronze',
|
|
463
|
+
level_number: 1,
|
|
464
|
+
points_required: 0,
|
|
465
|
+
},
|
|
466
|
+
next_level: {
|
|
467
|
+
id: 'level2',
|
|
468
|
+
name: 'Silver',
|
|
469
|
+
level_number: 2,
|
|
470
|
+
points_required: 500,
|
|
471
|
+
points_needed: 400,
|
|
472
|
+
},
|
|
473
|
+
metrics: {
|
|
474
|
+
logins: 10,
|
|
475
|
+
purchases: 2,
|
|
476
|
+
},
|
|
477
|
+
badges: [],
|
|
478
|
+
activity_summary: {
|
|
479
|
+
last_event_at: '2024-01-15T10:30:00Z',
|
|
480
|
+
event_count: 150,
|
|
481
|
+
days_active: 30,
|
|
482
|
+
},
|
|
483
|
+
}),
|
|
484
|
+
|
|
485
|
+
trackEventResponse: wrapSuccess({
|
|
486
|
+
status: 'queued',
|
|
487
|
+
message: 'Event accepted for processing',
|
|
488
|
+
}),
|
|
489
|
+
|
|
490
|
+
batchTrackResponse: wrapSuccess({
|
|
491
|
+
results: [
|
|
492
|
+
{ index: 0, status: 'queued' },
|
|
493
|
+
{ index: 1, status: 'queued' },
|
|
494
|
+
{ index: 2, status: 'error', error: 'Invalid event name' },
|
|
495
|
+
],
|
|
496
|
+
}),
|
|
497
|
+
|
|
498
|
+
leaderboardResponse: wrapSuccess({
|
|
499
|
+
timeframe: 'all-time',
|
|
500
|
+
rankings: [
|
|
501
|
+
{
|
|
502
|
+
rank: 1,
|
|
503
|
+
user_id: 'user1',
|
|
504
|
+
score: 1000,
|
|
505
|
+
level_name: 'Gold',
|
|
506
|
+
level_number: 3,
|
|
507
|
+
percentile: 99.5,
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
rank: 2,
|
|
511
|
+
user_id: 'user2',
|
|
512
|
+
score: 900,
|
|
513
|
+
level_name: 'Silver',
|
|
514
|
+
level_number: 2,
|
|
515
|
+
percentile: 98.0,
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
stats: {
|
|
519
|
+
total_users: 1000,
|
|
520
|
+
average_score: 250,
|
|
521
|
+
},
|
|
522
|
+
pagination: {
|
|
523
|
+
page: 1,
|
|
524
|
+
limit: 50,
|
|
525
|
+
total: 1000,
|
|
526
|
+
totalPages: 20,
|
|
527
|
+
},
|
|
528
|
+
}),
|
|
529
|
+
|
|
530
|
+
healthResponse: wrapSuccess({
|
|
531
|
+
status: 'healthy',
|
|
532
|
+
version: '1.0.0',
|
|
533
|
+
timestamp: '2024-01-15T10:30:00Z',
|
|
534
|
+
services: {
|
|
535
|
+
database: 'healthy',
|
|
536
|
+
redis: 'healthy',
|
|
537
|
+
queue: 'healthy',
|
|
538
|
+
},
|
|
539
|
+
queue_depth: 150,
|
|
540
|
+
processing_lag_ms: 50,
|
|
541
|
+
}),
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
export const standardizedErrors = {
|
|
545
|
+
validationError: wrapError('VALIDATION_ERROR', 'Validation failed', [
|
|
546
|
+
{ field: 'user_id', message: 'User ID is required' },
|
|
547
|
+
{ field: 'event_name', message: 'Event name is required' },
|
|
548
|
+
]),
|
|
549
|
+
|
|
550
|
+
authenticationError: wrapError('INVALID_API_KEY', 'Invalid or missing API key'),
|
|
551
|
+
|
|
552
|
+
notFoundError: wrapError('USER_NOT_FOUND', "User 'user123' does not exist"),
|
|
553
|
+
|
|
554
|
+
conflictError: wrapError('USER_EXISTS', 'User with this ID already exists'),
|
|
555
|
+
|
|
556
|
+
rateLimitError: wrapError('RATE_LIMIT_EXCEEDED', 'Rate limit exceeded. Please retry later.'),
|
|
557
|
+
|
|
558
|
+
serverError: wrapError('INTERNAL_ERROR', 'An internal server error occurred'),
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// Rate limit headers for testing
|
|
562
|
+
export const rateLimitHeaders = {
|
|
563
|
+
normal: {
|
|
564
|
+
'X-RateLimit-Limit': '1000',
|
|
565
|
+
'X-RateLimit-Remaining': '950',
|
|
566
|
+
'X-RateLimit-Reset': '1704067200',
|
|
567
|
+
},
|
|
568
|
+
warning: {
|
|
569
|
+
'X-RateLimit-Limit': '1000',
|
|
570
|
+
'X-RateLimit-Remaining': '150',
|
|
571
|
+
'X-RateLimit-Reset': '1704067200',
|
|
572
|
+
},
|
|
573
|
+
exceeded: {
|
|
574
|
+
'X-RateLimit-Limit': '1000',
|
|
575
|
+
'X-RateLimit-Remaining': '0',
|
|
576
|
+
'X-RateLimit-Reset': '1704067200',
|
|
577
|
+
'Retry-After': '60',
|
|
578
|
+
},
|
|
579
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property-Based Test: Batch Event Validation
|
|
3
|
+
* Feature: sdk-documentation-update, Property 4: Batch Event Validation
|
|
4
|
+
* Validates: Requirements 3.1, 3.2
|
|
5
|
+
*
|
|
6
|
+
* For any array of events passed to trackBatch(), if the array length exceeds 100,
|
|
7
|
+
* the SDK SHALL throw a ValidationError before making an API request.
|
|
8
|
+
* If the array length is between 1 and 100, the SDK SHALL make exactly one API request.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fc from 'fast-check';
|
|
12
|
+
import { jest } from '@jest/globals';
|
|
13
|
+
import Rooguys, { ValidationError } from '../../index.js';
|
|
14
|
+
import { createMockFetch, createMockHeaders } from '../utils/mockClient.js';
|
|
15
|
+
|
|
16
|
+
describe('Property 4: Batch Event Validation', () => {
|
|
17
|
+
let mockFetch;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockFetch = createMockFetch();
|
|
21
|
+
global.fetch = mockFetch;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const createBatchSuccessResponse = (count) => ({
|
|
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
|
+
results: Array.from({ length: count }, (_, i) => ({ index: i, status: 'queued' })),
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Generator for valid batch events
|
|
44
|
+
const validBatchEvent = fc.record({
|
|
45
|
+
eventName: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
|
|
46
|
+
userId: fc.string({ minLength: 1, maxLength: 255 }).filter(s => s.trim().length > 0),
|
|
47
|
+
properties: fc.option(
|
|
48
|
+
fc.dictionary(
|
|
49
|
+
fc.string({ minLength: 1, maxLength: 50 }),
|
|
50
|
+
fc.oneof(fc.string(), fc.integer(), fc.boolean(), fc.constant(null))
|
|
51
|
+
),
|
|
52
|
+
{ nil: undefined }
|
|
53
|
+
),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should throw ValidationError for arrays exceeding 100 events without making API request', async () => {
|
|
57
|
+
await fc.assert(
|
|
58
|
+
fc.asyncProperty(
|
|
59
|
+
fc.string({ minLength: 10, maxLength: 100 }), // API key
|
|
60
|
+
fc.integer({ min: 101, max: 500 }), // Array size > 100
|
|
61
|
+
async (apiKey, arraySize) => {
|
|
62
|
+
// Arrange
|
|
63
|
+
mockFetch.mockClear();
|
|
64
|
+
const sdk = new Rooguys(apiKey);
|
|
65
|
+
const events = Array.from({ length: arraySize }, (_, i) => ({
|
|
66
|
+
eventName: `event_${i}`,
|
|
67
|
+
userId: `user_${i}`,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
// Act & Assert
|
|
71
|
+
await expect(sdk.events.trackBatch(events)).rejects.toThrow(ValidationError);
|
|
72
|
+
await expect(sdk.events.trackBatch(events)).rejects.toThrow('Batch size exceeds maximum of 100 events');
|
|
73
|
+
|
|
74
|
+
// Verify no API request was made
|
|
75
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
76
|
+
}
|
|
77
|
+
),
|
|
78
|
+
{ numRuns: 100 }
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should make exactly one API request for valid batch sizes (1-100)', async () => {
|
|
83
|
+
await fc.assert(
|
|
84
|
+
fc.asyncProperty(
|
|
85
|
+
fc.string({ minLength: 10, maxLength: 100 }), // API key
|
|
86
|
+
fc.integer({ min: 1, max: 100 }), // Valid array size
|
|
87
|
+
async (apiKey, arraySize) => {
|
|
88
|
+
// Arrange
|
|
89
|
+
mockFetch.mockClear();
|
|
90
|
+
mockFetch.mockResolvedValue(createBatchSuccessResponse(arraySize));
|
|
91
|
+
const sdk = new Rooguys(apiKey);
|
|
92
|
+
const events = Array.from({ length: arraySize }, (_, i) => ({
|
|
93
|
+
eventName: `event_${i}`,
|
|
94
|
+
userId: `user_${i}`,
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
// Act
|
|
98
|
+
const result = await sdk.events.trackBatch(events);
|
|
99
|
+
|
|
100
|
+
// Assert - exactly one API request
|
|
101
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
102
|
+
|
|
103
|
+
// Verify the request was to the batch endpoint
|
|
104
|
+
const callUrl = mockFetch.mock.calls[0][0];
|
|
105
|
+
expect(callUrl).toContain('/events/batch');
|
|
106
|
+
|
|
107
|
+
// Verify the request body contains all events
|
|
108
|
+
const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
109
|
+
expect(callBody.events).toHaveLength(arraySize);
|
|
110
|
+
|
|
111
|
+
// Verify response contains results for all events
|
|
112
|
+
expect(result.results).toHaveLength(arraySize);
|
|
113
|
+
}
|
|
114
|
+
),
|
|
115
|
+
{ numRuns: 100 }
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should throw ValidationError for empty arrays without making API request', async () => {
|
|
120
|
+
await fc.assert(
|
|
121
|
+
fc.asyncProperty(
|
|
122
|
+
fc.string({ minLength: 10, maxLength: 100 }), // API key
|
|
123
|
+
async (apiKey) => {
|
|
124
|
+
// Arrange
|
|
125
|
+
mockFetch.mockClear();
|
|
126
|
+
const sdk = new Rooguys(apiKey);
|
|
127
|
+
|
|
128
|
+
// Act & Assert
|
|
129
|
+
await expect(sdk.events.trackBatch([])).rejects.toThrow(ValidationError);
|
|
130
|
+
await expect(sdk.events.trackBatch([])).rejects.toThrow('Events array cannot be empty');
|
|
131
|
+
|
|
132
|
+
// Verify no API request was made
|
|
133
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
134
|
+
}
|
|
135
|
+
),
|
|
136
|
+
{ numRuns: 100 }
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should throw ValidationError for non-array inputs without making API request', async () => {
|
|
141
|
+
await fc.assert(
|
|
142
|
+
fc.asyncProperty(
|
|
143
|
+
fc.string({ minLength: 10, maxLength: 100 }), // API key
|
|
144
|
+
fc.oneof(
|
|
145
|
+
fc.string(),
|
|
146
|
+
fc.integer(),
|
|
147
|
+
fc.boolean(),
|
|
148
|
+
fc.constant(null),
|
|
149
|
+
fc.constant(undefined),
|
|
150
|
+
fc.record({ eventName: fc.string(), userId: fc.string() }) // Single object instead of array
|
|
151
|
+
),
|
|
152
|
+
async (apiKey, invalidInput) => {
|
|
153
|
+
// Arrange
|
|
154
|
+
mockFetch.mockClear();
|
|
155
|
+
const sdk = new Rooguys(apiKey);
|
|
156
|
+
|
|
157
|
+
// Act & Assert
|
|
158
|
+
await expect(sdk.events.trackBatch(invalidInput)).rejects.toThrow(ValidationError);
|
|
159
|
+
|
|
160
|
+
// Verify no API request was made
|
|
161
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
162
|
+
}
|
|
163
|
+
),
|
|
164
|
+
{ numRuns: 100 }
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should correctly transform event properties in batch request', async () => {
|
|
169
|
+
await fc.assert(
|
|
170
|
+
fc.asyncProperty(
|
|
171
|
+
fc.string({ minLength: 10, maxLength: 100 }), // API key
|
|
172
|
+
fc.array(validBatchEvent, { minLength: 1, maxLength: 50 }),
|
|
173
|
+
async (apiKey, events) => {
|
|
174
|
+
// Arrange
|
|
175
|
+
mockFetch.mockClear();
|
|
176
|
+
mockFetch.mockResolvedValue(createBatchSuccessResponse(events.length));
|
|
177
|
+
const sdk = new Rooguys(apiKey);
|
|
178
|
+
|
|
179
|
+
// Act
|
|
180
|
+
await sdk.events.trackBatch(events);
|
|
181
|
+
|
|
182
|
+
// Assert - verify transformation
|
|
183
|
+
const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
184
|
+
|
|
185
|
+
events.forEach((event, index) => {
|
|
186
|
+
expect(callBody.events[index].event_name).toBe(event.eventName);
|
|
187
|
+
expect(callBody.events[index].user_id).toBe(event.userId);
|
|
188
|
+
expect(callBody.events[index].properties).toEqual(event.properties || {});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
),
|
|
192
|
+
{ numRuns: 100 }
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should return individual status for each event in batch', async () => {
|
|
197
|
+
await fc.assert(
|
|
198
|
+
fc.asyncProperty(
|
|
199
|
+
fc.string({ minLength: 10, maxLength: 100 }), // API key
|
|
200
|
+
fc.integer({ min: 1, max: 50 }), // Batch size
|
|
201
|
+
async (apiKey, batchSize) => {
|
|
202
|
+
// Arrange
|
|
203
|
+
mockFetch.mockClear();
|
|
204
|
+
mockFetch.mockResolvedValue(createBatchSuccessResponse(batchSize));
|
|
205
|
+
const sdk = new Rooguys(apiKey);
|
|
206
|
+
const events = Array.from({ length: batchSize }, (_, i) => ({
|
|
207
|
+
eventName: `event_${i}`,
|
|
208
|
+
userId: `user_${i}`,
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
// Act
|
|
212
|
+
const result = await sdk.events.trackBatch(events);
|
|
213
|
+
|
|
214
|
+
// Assert - each event has a status
|
|
215
|
+
expect(result.results).toHaveLength(batchSize);
|
|
216
|
+
result.results.forEach((eventResult, index) => {
|
|
217
|
+
expect(eventResult.index).toBe(index);
|
|
218
|
+
expect(eventResult.status).toBeDefined();
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
),
|
|
222
|
+
{ numRuns: 100 }
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
});
|