@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.
@@ -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 { createMockAxiosInstance } from '../utils/mockClient';
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: any;
13
+ let mockAxios: MockAxiosInstance;
8
14
 
9
15
  beforeEach(() => {
10
- mockAxios = createMockAxiosInstance();
11
- client = new Rooguys('test-api-key');
12
- (client as any).client = mockAxios;
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.post.mockResolvedValue({ data: mockResponses.ahaDeclarationResponse });
23
+ setupMockRequest(mockAxios, mockResponses.ahaDeclarationResponse);
18
24
 
19
25
  const result = await client.aha.declare('user123', 4);
20
26
 
21
- expect(mockAxios.post).toHaveBeenCalledWith('/aha/declare', {
22
- user_id: 'user123',
23
- value: 4,
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.post.mockResolvedValue({ data: mockResponses.ahaDeclarationResponse });
37
+ setupMockRequest(mockAxios, mockResponses.ahaDeclarationResponse);
30
38
 
31
39
  const result = await client.aha.declare('user123', 1);
32
40
 
33
- expect(mockAxios.post).toHaveBeenCalledWith('/aha/declare', {
34
- user_id: 'user123',
35
- value: 1,
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.post.mockResolvedValue({ data: mockResponses.ahaDeclarationResponse });
50
+ setupMockRequest(mockAxios, mockResponses.ahaDeclarationResponse);
42
51
 
43
52
  const result = await client.aha.declare('user123', 5);
44
53
 
45
- expect(mockAxios.post).toHaveBeenCalledWith('/aha/declare', {
46
- user_id: 'user123',
47
- value: 5,
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.post).not.toHaveBeenCalled();
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.post).not.toHaveBeenCalled();
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.post).not.toHaveBeenCalled();
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.post).not.toHaveBeenCalled();
87
+ expect(mockAxios.request).not.toHaveBeenCalled();
78
88
  });
79
89
 
80
90
  it('should handle API error response', async () => {
81
- mockAxios.post.mockRejectedValue({
82
- isAxiosError: true,
83
- response: {
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
- mockAxios.get.mockResolvedValue({ data: mockResponses.ahaScoreResponse });
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
- expect(mockAxios.get).toHaveBeenCalledWith('/users/user123/aha');
99
- expect(result).toEqual(mockResponses.ahaScoreResponse);
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.get.mockResolvedValue({ data: mockResponses.ahaScoreResponse });
117
+ setupMockRequest(mockAxios, mockResponses.ahaScoreResponse);
104
118
 
105
119
  const result = await client.aha.getUserScore('user123');
106
120
 
107
- expect(result.success).toBe(true);
108
- expect(result.data.user_id).toBe('user123');
109
- expect(result.data.current_score).toBe(75);
110
- expect(result.data.declarative_score).toBe(80);
111
- expect(result.data.inferred_score).toBe(70);
112
- expect(result.data.status).toBe('activated');
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.get.mockResolvedValue({ data: mockResponses.ahaScoreResponse });
131
+ setupMockRequest(mockAxios, mockResponses.ahaScoreResponse);
117
132
 
118
133
  const result = await client.aha.getUserScore('user123');
119
134
 
120
- expect(result.data.history).toEqual({
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.get.mockRejectedValue({
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.get.mockResolvedValue({ data: responseWithNulls });
164
+ setupMockRequest(mockAxios, responseWithNulls);
156
165
 
157
166
  const result = await client.aha.getUserScore('user123');
158
167
 
159
- expect(result.data.declarative_score).toBeNull();
160
- expect(result.data.inferred_score).toBeNull();
161
- expect(result.data.history.initial).toBeNull();
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
  });