@rooguys/sdk 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 +478 -113
- package/dist/__tests__/utils/mockClient.d.ts +65 -3
- package/dist/__tests__/utils/mockClient.js +144 -5
- package/dist/errors.d.ts +123 -0
- package/dist/errors.js +163 -0
- package/dist/http-client.d.ts +167 -0
- package/dist/http-client.js +250 -0
- package/dist/index.d.ts +160 -10
- package/dist/index.js +585 -146
- package/dist/types.d.ts +372 -50
- package/dist/types.js +21 -0
- package/package.json +1 -1
- package/src/__tests__/property/request-construction.property.test.ts +142 -91
- package/src/__tests__/property/response-parsing.property.test.ts +118 -67
- package/src/__tests__/property/sdk-modules.property.test.ts +450 -0
- package/src/__tests__/unit/aha.test.ts +61 -50
- package/src/__tests__/unit/badges.test.ts +27 -33
- package/src/__tests__/unit/config.test.ts +94 -126
- package/src/__tests__/unit/errors.test.ts +106 -150
- package/src/__tests__/unit/events.test.ts +119 -144
- package/src/__tests__/unit/leaderboards.test.ts +173 -40
- package/src/__tests__/unit/levels.test.ts +25 -33
- package/src/__tests__/unit/questionnaires.test.ts +33 -42
- package/src/__tests__/unit/users.test.ts +214 -99
- package/src/__tests__/utils/mockClient.ts +193 -6
- package/src/errors.ts +255 -0
- package/src/http-client.ts +433 -0
- package/src/index.ts +742 -150
- package/src/types.ts +429 -51
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property-Based Tests: Node.js SDK Modules
|
|
3
|
+
* Task 8.5: Property tests for batch validation, email validation, filter construction
|
|
4
|
+
*
|
|
5
|
+
* Properties tested:
|
|
6
|
+
* - Property 4: Batch Event Validation (Requirements 3.1, 3.2)
|
|
7
|
+
* - Property 7: Email Validation (Requirements 4.5)
|
|
8
|
+
* - Property 10: Leaderboard Filter Query Construction (Requirements 6.1, 6.2, 6.3)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fc from 'fast-check';
|
|
12
|
+
import { Rooguys } from '../../index';
|
|
13
|
+
import { ValidationError } from '../../errors';
|
|
14
|
+
import {
|
|
15
|
+
createMockRooguysClient,
|
|
16
|
+
mockAxiosResponse,
|
|
17
|
+
getLastRequestConfig,
|
|
18
|
+
MockAxiosInstance,
|
|
19
|
+
} from '../utils/mockClient';
|
|
20
|
+
|
|
21
|
+
describe('Property 4: Batch Event Validation', () => {
|
|
22
|
+
let client: Rooguys;
|
|
23
|
+
let mockAxios: MockAxiosInstance;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
const mock = createMockRooguysClient();
|
|
27
|
+
client = mock.client;
|
|
28
|
+
mockAxios = mock.mockAxios;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
jest.clearAllMocks();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should reject batch with more than 100 events before making API request', async () => {
|
|
36
|
+
await fc.assert(
|
|
37
|
+
fc.asyncProperty(
|
|
38
|
+
// Generate array with 101-500 events
|
|
39
|
+
fc.array(
|
|
40
|
+
fc.record({
|
|
41
|
+
eventName: fc.string({ minLength: 1, maxLength: 50 }),
|
|
42
|
+
userId: fc.string({ minLength: 1, maxLength: 50 }),
|
|
43
|
+
properties: fc.dictionary(fc.string(), fc.string()),
|
|
44
|
+
}),
|
|
45
|
+
{ minLength: 101, maxLength: 500 }
|
|
46
|
+
),
|
|
47
|
+
async (events) => {
|
|
48
|
+
// Act & Assert
|
|
49
|
+
await expect(client.events.trackBatch(events)).rejects.toThrow(ValidationError);
|
|
50
|
+
await expect(client.events.trackBatch(events)).rejects.toMatchObject({
|
|
51
|
+
code: 'BATCH_TOO_LARGE',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Verify no API request was made
|
|
55
|
+
expect(mockAxios.request).not.toHaveBeenCalled();
|
|
56
|
+
}
|
|
57
|
+
),
|
|
58
|
+
{ numRuns: 100 }
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should accept batch with 1-100 events and make exactly one API request', async () => {
|
|
63
|
+
await fc.assert(
|
|
64
|
+
fc.asyncProperty(
|
|
65
|
+
// Generate array with 1-100 events
|
|
66
|
+
fc.array(
|
|
67
|
+
fc.record({
|
|
68
|
+
eventName: fc.string({ minLength: 1, maxLength: 50 }),
|
|
69
|
+
userId: fc.string({ minLength: 1, maxLength: 50 }),
|
|
70
|
+
properties: fc.dictionary(fc.string(), fc.string()),
|
|
71
|
+
}),
|
|
72
|
+
{ minLength: 1, maxLength: 100 }
|
|
73
|
+
),
|
|
74
|
+
async (events) => {
|
|
75
|
+
// Reset mock before each iteration
|
|
76
|
+
mockAxios.request.mockReset();
|
|
77
|
+
mockAxios.request.mockResolvedValue(mockAxiosResponse({
|
|
78
|
+
results: events.map((_, index) => ({ index, status: 'queued' })),
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
await client.events.trackBatch(events);
|
|
83
|
+
|
|
84
|
+
// Assert - exactly one API request was made
|
|
85
|
+
expect(mockAxios.request).toHaveBeenCalledTimes(1);
|
|
86
|
+
|
|
87
|
+
// Verify request was to batch endpoint
|
|
88
|
+
const config = getLastRequestConfig(mockAxios);
|
|
89
|
+
expect(config.url).toBe('/events/batch');
|
|
90
|
+
expect(config.method).toBe('POST');
|
|
91
|
+
expect(config.data.events).toHaveLength(events.length);
|
|
92
|
+
}
|
|
93
|
+
),
|
|
94
|
+
{ numRuns: 100 }
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should reject empty batch before making API request', async () => {
|
|
99
|
+
// Act & Assert
|
|
100
|
+
await expect(client.events.trackBatch([])).rejects.toThrow(ValidationError);
|
|
101
|
+
await expect(client.events.trackBatch([])).rejects.toMatchObject({
|
|
102
|
+
code: 'EMPTY_EVENTS',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Verify no API request was made
|
|
106
|
+
expect(mockAxios.request).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('Property 7: Email Validation', () => {
|
|
111
|
+
let client: Rooguys;
|
|
112
|
+
let mockAxios: MockAxiosInstance;
|
|
113
|
+
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
const mock = createMockRooguysClient();
|
|
116
|
+
client = mock.client;
|
|
117
|
+
mockAxios = mock.mockAxios;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
jest.clearAllMocks();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should reject invalid email formats before making API request (user create)', async () => {
|
|
125
|
+
await fc.assert(
|
|
126
|
+
fc.asyncProperty(
|
|
127
|
+
fc.string({ minLength: 1, maxLength: 50 }), // userId
|
|
128
|
+
// Generate invalid emails (no @ or no domain)
|
|
129
|
+
fc.oneof(
|
|
130
|
+
fc.string({ minLength: 1, maxLength: 50 }).filter(s => !s.includes('@')),
|
|
131
|
+
fc.string({ minLength: 1, maxLength: 50 }).map(s => `${s}@`),
|
|
132
|
+
fc.string({ minLength: 1, maxLength: 50 }).map(s => `@${s}`),
|
|
133
|
+
fc.constant('invalid'),
|
|
134
|
+
fc.constant('no-at-sign'),
|
|
135
|
+
fc.constant('@nodomain'),
|
|
136
|
+
fc.constant('missing@'),
|
|
137
|
+
),
|
|
138
|
+
async (userId, invalidEmail) => {
|
|
139
|
+
// Act & Assert
|
|
140
|
+
await expect(client.users.create({ userId, email: invalidEmail })).rejects.toThrow(ValidationError);
|
|
141
|
+
await expect(client.users.create({ userId, email: invalidEmail })).rejects.toMatchObject({
|
|
142
|
+
code: 'INVALID_EMAIL',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Verify no API request was made
|
|
146
|
+
expect(mockAxios.request).not.toHaveBeenCalled();
|
|
147
|
+
}
|
|
148
|
+
),
|
|
149
|
+
{ numRuns: 100 }
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should reject invalid email formats before making API request (user update)', async () => {
|
|
154
|
+
await fc.assert(
|
|
155
|
+
fc.asyncProperty(
|
|
156
|
+
fc.string({ minLength: 1, maxLength: 50 }), // userId
|
|
157
|
+
// Generate invalid emails
|
|
158
|
+
fc.oneof(
|
|
159
|
+
fc.string({ minLength: 1, maxLength: 50 }).filter(s => !s.includes('@')),
|
|
160
|
+
fc.string({ minLength: 1, maxLength: 50 }).map(s => `${s}@`),
|
|
161
|
+
fc.constant('invalid'),
|
|
162
|
+
fc.constant('no-at-sign'),
|
|
163
|
+
),
|
|
164
|
+
async (userId, invalidEmail) => {
|
|
165
|
+
// Act & Assert
|
|
166
|
+
await expect(client.users.update(userId, { email: invalidEmail })).rejects.toThrow(ValidationError);
|
|
167
|
+
await expect(client.users.update(userId, { email: invalidEmail })).rejects.toMatchObject({
|
|
168
|
+
code: 'INVALID_EMAIL',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Verify no API request was made
|
|
172
|
+
expect(mockAxios.request).not.toHaveBeenCalled();
|
|
173
|
+
}
|
|
174
|
+
),
|
|
175
|
+
{ numRuns: 100 }
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should accept valid email formats and make API request', async () => {
|
|
180
|
+
await fc.assert(
|
|
181
|
+
fc.asyncProperty(
|
|
182
|
+
fc.string({ minLength: 1, maxLength: 50 }), // userId
|
|
183
|
+
// Generate valid emails
|
|
184
|
+
fc.tuple(
|
|
185
|
+
fc.string({ minLength: 1, maxLength: 20 }).filter(s => /^[a-zA-Z0-9._-]+$/.test(s)),
|
|
186
|
+
fc.string({ minLength: 1, maxLength: 20 }).filter(s => /^[a-zA-Z0-9.-]+$/.test(s)),
|
|
187
|
+
fc.constantFrom('com', 'org', 'net', 'io', 'co.uk')
|
|
188
|
+
).map(([local, domain, tld]) => `${local}@${domain}.${tld}`),
|
|
189
|
+
async (userId, validEmail) => {
|
|
190
|
+
// Reset mock before each iteration
|
|
191
|
+
mockAxios.request.mockReset();
|
|
192
|
+
mockAxios.request.mockResolvedValue(mockAxiosResponse({
|
|
193
|
+
user_id: userId,
|
|
194
|
+
email: validEmail,
|
|
195
|
+
points: 0,
|
|
196
|
+
}));
|
|
197
|
+
|
|
198
|
+
// Act
|
|
199
|
+
await client.users.create({ userId, email: validEmail });
|
|
200
|
+
|
|
201
|
+
// Assert - API request was made
|
|
202
|
+
expect(mockAxios.request).toHaveBeenCalledTimes(1);
|
|
203
|
+
|
|
204
|
+
const config = getLastRequestConfig(mockAxios);
|
|
205
|
+
expect(config.data.email).toBe(validEmail);
|
|
206
|
+
}
|
|
207
|
+
),
|
|
208
|
+
{ numRuns: 100 }
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should allow user creation without email', async () => {
|
|
213
|
+
await fc.assert(
|
|
214
|
+
fc.asyncProperty(
|
|
215
|
+
fc.string({ minLength: 1, maxLength: 50 }), // userId
|
|
216
|
+
async (userId) => {
|
|
217
|
+
// Reset mock before each iteration
|
|
218
|
+
mockAxios.request.mockReset();
|
|
219
|
+
mockAxios.request.mockResolvedValue(mockAxiosResponse({
|
|
220
|
+
user_id: userId,
|
|
221
|
+
points: 0,
|
|
222
|
+
}));
|
|
223
|
+
|
|
224
|
+
// Act
|
|
225
|
+
await client.users.create({ userId });
|
|
226
|
+
|
|
227
|
+
// Assert - API request was made
|
|
228
|
+
expect(mockAxios.request).toHaveBeenCalledTimes(1);
|
|
229
|
+
|
|
230
|
+
const config = getLastRequestConfig(mockAxios);
|
|
231
|
+
expect(config.data.user_id).toBe(userId);
|
|
232
|
+
expect(config.data.email).toBeUndefined();
|
|
233
|
+
}
|
|
234
|
+
),
|
|
235
|
+
{ numRuns: 100 }
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('Property 10: Leaderboard Filter Query Construction', () => {
|
|
241
|
+
let client: Rooguys;
|
|
242
|
+
let mockAxios: MockAxiosInstance;
|
|
243
|
+
|
|
244
|
+
beforeEach(() => {
|
|
245
|
+
const mock = createMockRooguysClient();
|
|
246
|
+
client = mock.client;
|
|
247
|
+
mockAxios = mock.mockAxios;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
afterEach(() => {
|
|
251
|
+
jest.clearAllMocks();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should include persona filter in query parameters', async () => {
|
|
255
|
+
await fc.assert(
|
|
256
|
+
fc.asyncProperty(
|
|
257
|
+
fc.constantFrom('Competitor', 'Explorer', 'Achiever', 'Socializer'),
|
|
258
|
+
async (persona) => {
|
|
259
|
+
// Arrange
|
|
260
|
+
mockAxios.request.mockResolvedValue(mockAxiosResponse({
|
|
261
|
+
rankings: [],
|
|
262
|
+
page: 1,
|
|
263
|
+
limit: 50,
|
|
264
|
+
total: 0,
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
// Act
|
|
268
|
+
await client.leaderboards.getGlobal({ persona });
|
|
269
|
+
|
|
270
|
+
// Assert
|
|
271
|
+
const config = getLastRequestConfig(mockAxios);
|
|
272
|
+
expect(config.params.persona).toBe(persona);
|
|
273
|
+
}
|
|
274
|
+
),
|
|
275
|
+
{ numRuns: 100 }
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should include level range filters in query parameters', async () => {
|
|
280
|
+
await fc.assert(
|
|
281
|
+
fc.asyncProperty(
|
|
282
|
+
fc.integer({ min: 1, max: 50 }),
|
|
283
|
+
fc.integer({ min: 51, max: 100 }),
|
|
284
|
+
async (minLevel, maxLevel) => {
|
|
285
|
+
// Arrange
|
|
286
|
+
mockAxios.request.mockResolvedValue(mockAxiosResponse({
|
|
287
|
+
rankings: [],
|
|
288
|
+
page: 1,
|
|
289
|
+
limit: 50,
|
|
290
|
+
total: 0,
|
|
291
|
+
}));
|
|
292
|
+
|
|
293
|
+
// Act
|
|
294
|
+
await client.leaderboards.getGlobal({ minLevel, maxLevel });
|
|
295
|
+
|
|
296
|
+
// Assert
|
|
297
|
+
const config = getLastRequestConfig(mockAxios);
|
|
298
|
+
expect(config.params.min_level).toBe(minLevel);
|
|
299
|
+
expect(config.params.max_level).toBe(maxLevel);
|
|
300
|
+
}
|
|
301
|
+
),
|
|
302
|
+
{ numRuns: 100 }
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should format date filters as ISO 8601 strings', async () => {
|
|
307
|
+
await fc.assert(
|
|
308
|
+
fc.asyncProperty(
|
|
309
|
+
fc.date({ min: new Date('2020-01-01'), max: new Date('2025-12-31') }),
|
|
310
|
+
fc.date({ min: new Date('2020-01-01'), max: new Date('2025-12-31') }),
|
|
311
|
+
async (startDate, endDate) => {
|
|
312
|
+
// Arrange
|
|
313
|
+
mockAxios.request.mockResolvedValue(mockAxiosResponse({
|
|
314
|
+
rankings: [],
|
|
315
|
+
page: 1,
|
|
316
|
+
limit: 50,
|
|
317
|
+
total: 0,
|
|
318
|
+
}));
|
|
319
|
+
|
|
320
|
+
// Act
|
|
321
|
+
await client.leaderboards.getGlobal({ startDate, endDate });
|
|
322
|
+
|
|
323
|
+
// Assert
|
|
324
|
+
const config = getLastRequestConfig(mockAxios);
|
|
325
|
+
|
|
326
|
+
// Verify dates are ISO 8601 formatted
|
|
327
|
+
expect(config.params.start_date).toBe(startDate.toISOString());
|
|
328
|
+
expect(config.params.end_date).toBe(endDate.toISOString());
|
|
329
|
+
|
|
330
|
+
// Verify ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ)
|
|
331
|
+
expect(config.params.start_date).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/);
|
|
332
|
+
expect(config.params.end_date).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/);
|
|
333
|
+
}
|
|
334
|
+
),
|
|
335
|
+
{ numRuns: 100 }
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should include all filter parameters when provided together', async () => {
|
|
340
|
+
await fc.assert(
|
|
341
|
+
fc.asyncProperty(
|
|
342
|
+
fc.record({
|
|
343
|
+
persona: fc.constantFrom('Competitor', 'Explorer', 'Achiever', 'Socializer'),
|
|
344
|
+
minLevel: fc.integer({ min: 1, max: 50 }),
|
|
345
|
+
maxLevel: fc.integer({ min: 51, max: 100 }),
|
|
346
|
+
startDate: fc.date({ min: new Date('2020-01-01'), max: new Date('2023-12-31') }),
|
|
347
|
+
endDate: fc.date({ min: new Date('2024-01-01'), max: new Date('2025-12-31') }),
|
|
348
|
+
page: fc.integer({ min: 1, max: 100 }),
|
|
349
|
+
limit: fc.integer({ min: 1, max: 100 }),
|
|
350
|
+
}),
|
|
351
|
+
async (filters) => {
|
|
352
|
+
// Arrange
|
|
353
|
+
mockAxios.request.mockResolvedValue(mockAxiosResponse({
|
|
354
|
+
rankings: [],
|
|
355
|
+
page: filters.page,
|
|
356
|
+
limit: filters.limit,
|
|
357
|
+
total: 0,
|
|
358
|
+
}));
|
|
359
|
+
|
|
360
|
+
// Act
|
|
361
|
+
await client.leaderboards.getGlobal(filters);
|
|
362
|
+
|
|
363
|
+
// Assert
|
|
364
|
+
const config = getLastRequestConfig(mockAxios);
|
|
365
|
+
expect(config.params.persona).toBe(filters.persona);
|
|
366
|
+
expect(config.params.min_level).toBe(filters.minLevel);
|
|
367
|
+
expect(config.params.max_level).toBe(filters.maxLevel);
|
|
368
|
+
expect(config.params.start_date).toBe(filters.startDate.toISOString());
|
|
369
|
+
expect(config.params.end_date).toBe(filters.endDate.toISOString());
|
|
370
|
+
expect(config.params.page).toBe(filters.page);
|
|
371
|
+
expect(config.params.limit).toBe(filters.limit);
|
|
372
|
+
}
|
|
373
|
+
),
|
|
374
|
+
{ numRuns: 100 }
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should work with custom leaderboard endpoint with filters', async () => {
|
|
379
|
+
await fc.assert(
|
|
380
|
+
fc.asyncProperty(
|
|
381
|
+
fc.uuid(),
|
|
382
|
+
fc.record({
|
|
383
|
+
persona: fc.option(fc.constantFrom('Competitor', 'Explorer', 'Achiever', 'Socializer'), { nil: undefined }),
|
|
384
|
+
minLevel: fc.option(fc.integer({ min: 1, max: 50 }), { nil: undefined }),
|
|
385
|
+
maxLevel: fc.option(fc.integer({ min: 51, max: 100 }), { nil: undefined }),
|
|
386
|
+
}),
|
|
387
|
+
async (leaderboardId, filters) => {
|
|
388
|
+
// Arrange
|
|
389
|
+
mockAxios.request.mockResolvedValue(mockAxiosResponse({
|
|
390
|
+
rankings: [],
|
|
391
|
+
page: 1,
|
|
392
|
+
limit: 50,
|
|
393
|
+
total: 0,
|
|
394
|
+
}));
|
|
395
|
+
|
|
396
|
+
// Act
|
|
397
|
+
await client.leaderboards.getCustom(leaderboardId, filters);
|
|
398
|
+
|
|
399
|
+
// Assert
|
|
400
|
+
const config = getLastRequestConfig(mockAxios);
|
|
401
|
+
expect(config.url).toBe(`/leaderboards/${encodeURIComponent(leaderboardId)}`);
|
|
402
|
+
|
|
403
|
+
// Verify only provided filters are included
|
|
404
|
+
if (filters.persona !== undefined) {
|
|
405
|
+
expect(config.params.persona).toBe(filters.persona);
|
|
406
|
+
}
|
|
407
|
+
if (filters.minLevel !== undefined) {
|
|
408
|
+
expect(config.params.min_level).toBe(filters.minLevel);
|
|
409
|
+
}
|
|
410
|
+
if (filters.maxLevel !== undefined) {
|
|
411
|
+
expect(config.params.max_level).toBe(filters.maxLevel);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
),
|
|
415
|
+
{ numRuns: 100 }
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should not include undefined filter parameters', async () => {
|
|
420
|
+
await fc.assert(
|
|
421
|
+
fc.asyncProperty(
|
|
422
|
+
fc.constantFrom('all-time', 'weekly', 'monthly'),
|
|
423
|
+
async (timeframe) => {
|
|
424
|
+
// Arrange
|
|
425
|
+
mockAxios.request.mockResolvedValue(mockAxiosResponse({
|
|
426
|
+
rankings: [],
|
|
427
|
+
page: 1,
|
|
428
|
+
limit: 50,
|
|
429
|
+
total: 0,
|
|
430
|
+
}));
|
|
431
|
+
|
|
432
|
+
// Act - call with no filters
|
|
433
|
+
await client.leaderboards.getGlobal({ timeframe: timeframe as any });
|
|
434
|
+
|
|
435
|
+
// Assert
|
|
436
|
+
const config = getLastRequestConfig(mockAxios);
|
|
437
|
+
expect(config.params.timeframe).toBe(timeframe);
|
|
438
|
+
|
|
439
|
+
// Verify undefined filters are not included
|
|
440
|
+
expect(config.params.persona).toBeUndefined();
|
|
441
|
+
expect(config.params.min_level).toBeUndefined();
|
|
442
|
+
expect(config.params.max_level).toBeUndefined();
|
|
443
|
+
expect(config.params.start_date).toBeUndefined();
|
|
444
|
+
expect(config.params.end_date).toBeUndefined();
|
|
445
|
+
}
|
|
446
|
+
),
|
|
447
|
+
{ numRuns: 100 }
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
@@ -1,50 +1,60 @@
|
|
|
1
1
|
import { Rooguys } from '../../index';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createMockRooguysClient,
|
|
4
|
+
setupMockRequest,
|
|
5
|
+
setupMockRequestError,
|
|
6
|
+
expectRequestWith,
|
|
7
|
+
MockAxiosInstance,
|
|
8
|
+
} from '../utils/mockClient';
|
|
3
9
|
import { mockResponses, mockErrors } from '../fixtures/responses';
|
|
4
10
|
|
|
5
11
|
describe('Aha Resource', () => {
|
|
6
12
|
let client: Rooguys;
|
|
7
|
-
let mockAxios:
|
|
13
|
+
let mockAxios: MockAxiosInstance;
|
|
8
14
|
|
|
9
15
|
beforeEach(() => {
|
|
10
|
-
|
|
11
|
-
client =
|
|
12
|
-
|
|
16
|
+
const mock = createMockRooguysClient();
|
|
17
|
+
client = mock.client;
|
|
18
|
+
mockAxios = mock.mockAxios;
|
|
13
19
|
});
|
|
14
20
|
|
|
15
21
|
describe('declare', () => {
|
|
16
22
|
it('should declare aha score with valid value', async () => {
|
|
17
|
-
mockAxios
|
|
23
|
+
setupMockRequest(mockAxios, mockResponses.ahaDeclarationResponse);
|
|
18
24
|
|
|
19
25
|
const result = await client.aha.declare('user123', 4);
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
expectRequestWith(mockAxios, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
url: '/aha/declare',
|
|
30
|
+
data: { user_id: 'user123', value: 4 },
|
|
24
31
|
});
|
|
32
|
+
// SDK returns the full response since there's no data field to unwrap
|
|
25
33
|
expect(result).toEqual(mockResponses.ahaDeclarationResponse);
|
|
26
34
|
});
|
|
27
35
|
|
|
28
36
|
it('should declare aha score with value 1', async () => {
|
|
29
|
-
mockAxios
|
|
37
|
+
setupMockRequest(mockAxios, mockResponses.ahaDeclarationResponse);
|
|
30
38
|
|
|
31
39
|
const result = await client.aha.declare('user123', 1);
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
expectRequestWith(mockAxios, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
url: '/aha/declare',
|
|
44
|
+
data: { user_id: 'user123', value: 1 },
|
|
36
45
|
});
|
|
37
46
|
expect(result).toEqual(mockResponses.ahaDeclarationResponse);
|
|
38
47
|
});
|
|
39
48
|
|
|
40
49
|
it('should declare aha score with value 5', async () => {
|
|
41
|
-
mockAxios
|
|
50
|
+
setupMockRequest(mockAxios, mockResponses.ahaDeclarationResponse);
|
|
42
51
|
|
|
43
52
|
const result = await client.aha.declare('user123', 5);
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
expectRequestWith(mockAxios, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
url: '/aha/declare',
|
|
57
|
+
data: { user_id: 'user123', value: 5 },
|
|
48
58
|
});
|
|
49
59
|
expect(result).toEqual(mockResponses.ahaDeclarationResponse);
|
|
50
60
|
});
|
|
@@ -53,37 +63,34 @@ describe('Aha Resource', () => {
|
|
|
53
63
|
await expect(client.aha.declare('user123', 0)).rejects.toThrow(
|
|
54
64
|
'Aha score value must be an integer between 1 and 5'
|
|
55
65
|
);
|
|
56
|
-
expect(mockAxios.
|
|
66
|
+
expect(mockAxios.request).not.toHaveBeenCalled();
|
|
57
67
|
});
|
|
58
68
|
|
|
59
69
|
it('should throw error for value 6', async () => {
|
|
60
70
|
await expect(client.aha.declare('user123', 6)).rejects.toThrow(
|
|
61
71
|
'Aha score value must be an integer between 1 and 5'
|
|
62
72
|
);
|
|
63
|
-
expect(mockAxios.
|
|
73
|
+
expect(mockAxios.request).not.toHaveBeenCalled();
|
|
64
74
|
});
|
|
65
75
|
|
|
66
76
|
it('should throw error for negative value', async () => {
|
|
67
77
|
await expect(client.aha.declare('user123', -1)).rejects.toThrow(
|
|
68
78
|
'Aha score value must be an integer between 1 and 5'
|
|
69
79
|
);
|
|
70
|
-
expect(mockAxios.
|
|
80
|
+
expect(mockAxios.request).not.toHaveBeenCalled();
|
|
71
81
|
});
|
|
72
82
|
|
|
73
83
|
it('should throw error for non-integer value', async () => {
|
|
74
84
|
await expect(client.aha.declare('user123', 3.5)).rejects.toThrow(
|
|
75
85
|
'Aha score value must be an integer between 1 and 5'
|
|
76
86
|
);
|
|
77
|
-
expect(mockAxios.
|
|
87
|
+
expect(mockAxios.request).not.toHaveBeenCalled();
|
|
78
88
|
});
|
|
79
89
|
|
|
80
90
|
it('should handle API error response', async () => {
|
|
81
|
-
mockAxios
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
data: mockErrors.ahaValueError,
|
|
85
|
-
},
|
|
86
|
-
});
|
|
91
|
+
setupMockRequestError(mockAxios, 400, 'Validation failed', 'VALIDATION_ERROR', [
|
|
92
|
+
{ field: 'value', message: 'value must be an integer between 1 and 5' },
|
|
93
|
+
]);
|
|
87
94
|
|
|
88
95
|
await expect(client.aha.declare('user123', 3)).rejects.toThrow();
|
|
89
96
|
});
|
|
@@ -91,33 +98,41 @@ describe('Aha Resource', () => {
|
|
|
91
98
|
|
|
92
99
|
describe('getUserScore', () => {
|
|
93
100
|
it('should get user aha score successfully', async () => {
|
|
94
|
-
|
|
101
|
+
// The API returns { success: true, data: {...} } and SDK unwraps to just data
|
|
102
|
+
// So we mock the wrapped response but expect the unwrapped data
|
|
103
|
+
setupMockRequest(mockAxios, mockResponses.ahaScoreResponse);
|
|
95
104
|
|
|
96
105
|
const result = await client.aha.getUserScore('user123');
|
|
97
106
|
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
expectRequestWith(mockAxios, {
|
|
108
|
+
method: 'GET',
|
|
109
|
+
url: '/users/user123/aha',
|
|
110
|
+
});
|
|
111
|
+
// SDK unwraps { success: true, data: {...} } to just the data part
|
|
112
|
+
// Cast to any since the type expects the wrapper but we get unwrapped data
|
|
113
|
+
expect((result as any).user_id).toBe('user123');
|
|
100
114
|
});
|
|
101
115
|
|
|
102
116
|
it('should parse all aha score fields correctly', async () => {
|
|
103
|
-
mockAxios
|
|
117
|
+
setupMockRequest(mockAxios, mockResponses.ahaScoreResponse);
|
|
104
118
|
|
|
105
119
|
const result = await client.aha.getUserScore('user123');
|
|
106
120
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
expect(
|
|
110
|
-
expect(
|
|
111
|
-
expect(
|
|
112
|
-
expect(
|
|
121
|
+
// Result is the unwrapped data (cast to any to access fields)
|
|
122
|
+
const data = result as any;
|
|
123
|
+
expect(data.user_id).toBe('user123');
|
|
124
|
+
expect(data.current_score).toBe(75);
|
|
125
|
+
expect(data.declarative_score).toBe(80);
|
|
126
|
+
expect(data.inferred_score).toBe(70);
|
|
127
|
+
expect(data.status).toBe('activated');
|
|
113
128
|
});
|
|
114
129
|
|
|
115
130
|
it('should preserve history structure', async () => {
|
|
116
|
-
mockAxios
|
|
131
|
+
setupMockRequest(mockAxios, mockResponses.ahaScoreResponse);
|
|
117
132
|
|
|
118
133
|
const result = await client.aha.getUserScore('user123');
|
|
119
134
|
|
|
120
|
-
expect(result.
|
|
135
|
+
expect((result as any).history).toEqual({
|
|
121
136
|
initial: 50,
|
|
122
137
|
initial_date: '2024-01-01T00:00:00Z',
|
|
123
138
|
previous: 70,
|
|
@@ -125,13 +140,7 @@ describe('Aha Resource', () => {
|
|
|
125
140
|
});
|
|
126
141
|
|
|
127
142
|
it('should handle 404 error when user not found', async () => {
|
|
128
|
-
mockAxios
|
|
129
|
-
isAxiosError: true,
|
|
130
|
-
response: {
|
|
131
|
-
status: 404,
|
|
132
|
-
data: mockErrors.notFoundError,
|
|
133
|
-
},
|
|
134
|
-
});
|
|
143
|
+
setupMockRequestError(mockAxios, 404, 'User not found', 'NOT_FOUND');
|
|
135
144
|
|
|
136
145
|
await expect(client.aha.getUserScore('nonexistent')).rejects.toThrow();
|
|
137
146
|
});
|
|
@@ -152,13 +161,15 @@ describe('Aha Resource', () => {
|
|
|
152
161
|
},
|
|
153
162
|
},
|
|
154
163
|
};
|
|
155
|
-
mockAxios
|
|
164
|
+
setupMockRequest(mockAxios, responseWithNulls);
|
|
156
165
|
|
|
157
166
|
const result = await client.aha.getUserScore('user123');
|
|
158
167
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
expect(
|
|
168
|
+
// Result is the unwrapped data
|
|
169
|
+
const data = result as any;
|
|
170
|
+
expect(data.declarative_score).toBeNull();
|
|
171
|
+
expect(data.inferred_score).toBeNull();
|
|
172
|
+
expect(data.history.initial).toBeNull();
|
|
162
173
|
});
|
|
163
174
|
});
|
|
164
175
|
});
|