@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,345 @@
1
+ /**
2
+ * Property-Based Test: Timestamp Validation
3
+ * Feature: sdk-documentation-update, Property 5: Timestamp Validation
4
+ * Validates: Requirements 3.5, 3.6
5
+ *
6
+ * For any custom timestamp provided to event tracking methods, if the timestamp
7
+ * is more than 7 days in the past, the SDK SHALL throw a ValidationError with
8
+ * code TIMESTAMP_TOO_OLD. If the timestamp is within 7 days, it SHALL be
9
+ * included in the request body.
10
+ */
11
+
12
+ import fc from 'fast-check';
13
+ import { jest } from '@jest/globals';
14
+ import Rooguys, { ValidationError } from '../../index.js';
15
+ import { createMockFetch, createMockHeaders } from '../utils/mockClient.js';
16
+
17
+ describe('Property 5: Timestamp Validation', () => {
18
+ let mockFetch;
19
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
20
+
21
+ beforeEach(() => {
22
+ mockFetch = createMockFetch();
23
+ global.fetch = mockFetch;
24
+ });
25
+
26
+ afterEach(() => {
27
+ jest.clearAllMocks();
28
+ });
29
+
30
+ const createSuccessResponse = () => ({
31
+ ok: true,
32
+ headers: createMockHeaders({
33
+ 'X-RateLimit-Limit': '1000',
34
+ 'X-RateLimit-Remaining': '950',
35
+ 'X-RateLimit-Reset': '1704067200',
36
+ }),
37
+ json: async () => ({
38
+ success: true,
39
+ data: { status: 'queued', message: 'Event accepted for processing' },
40
+ }),
41
+ });
42
+
43
+ const createBatchSuccessResponse = (count) => ({
44
+ ok: true,
45
+ headers: createMockHeaders({
46
+ 'X-RateLimit-Limit': '1000',
47
+ 'X-RateLimit-Remaining': '950',
48
+ 'X-RateLimit-Reset': '1704067200',
49
+ }),
50
+ json: async () => ({
51
+ success: true,
52
+ data: {
53
+ results: Array.from({ length: count }, (_, i) => ({ index: i, status: 'queued' })),
54
+ },
55
+ }),
56
+ });
57
+
58
+ // Generator for timestamps older than 7 days
59
+ const oldTimestamp = () => fc.integer({ min: 8, max: 365 }).map(daysAgo =>
60
+ new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000)
61
+ );
62
+
63
+ // Generator for timestamps within 7 days (valid)
64
+ const validTimestamp = () => fc.integer({ min: 0, max: 6 }).chain(daysAgo =>
65
+ fc.integer({ min: 0, max: 23 }).chain(hours =>
66
+ fc.integer({ min: 0, max: 59 }).map(minutes => {
67
+ const ms = daysAgo * 24 * 60 * 60 * 1000 + hours * 60 * 60 * 1000 + minutes * 60 * 1000;
68
+ return new Date(Date.now() - ms);
69
+ })
70
+ )
71
+ );
72
+
73
+ describe('Single Event Tracking (track)', () => {
74
+ it('should throw ValidationError with TIMESTAMP_TOO_OLD for timestamps older than 7 days', async () => {
75
+ await fc.assert(
76
+ fc.asyncProperty(
77
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
78
+ fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), // Event name
79
+ fc.string({ minLength: 1, maxLength: 255 }).filter(s => s.trim().length > 0), // User ID
80
+ oldTimestamp(),
81
+ async (apiKey, eventName, userId, timestamp) => {
82
+ // Arrange
83
+ mockFetch.mockClear();
84
+ const sdk = new Rooguys(apiKey);
85
+
86
+ // Act & Assert
87
+ try {
88
+ await sdk.events.track(eventName, userId, {}, { timestamp });
89
+ // Should not reach here
90
+ expect(true).toBe(false);
91
+ } catch (error) {
92
+ expect(error).toBeInstanceOf(ValidationError);
93
+ expect(error.code).toBe('TIMESTAMP_TOO_OLD');
94
+ expect(error.message).toContain('7 days');
95
+ }
96
+
97
+ // Verify no API request was made
98
+ expect(mockFetch).not.toHaveBeenCalled();
99
+ }
100
+ ),
101
+ { numRuns: 100 }
102
+ );
103
+ });
104
+
105
+ it('should include timestamp in request body for valid timestamps within 7 days', async () => {
106
+ await fc.assert(
107
+ fc.asyncProperty(
108
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
109
+ fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), // Event name
110
+ fc.string({ minLength: 1, maxLength: 255 }).filter(s => s.trim().length > 0), // User ID
111
+ validTimestamp(),
112
+ async (apiKey, eventName, userId, timestamp) => {
113
+ // Arrange
114
+ mockFetch.mockClear();
115
+ mockFetch.mockResolvedValue(createSuccessResponse());
116
+ const sdk = new Rooguys(apiKey);
117
+
118
+ // Act
119
+ await sdk.events.track(eventName, userId, {}, { timestamp });
120
+
121
+ // Assert - request was made
122
+ expect(mockFetch).toHaveBeenCalledTimes(1);
123
+
124
+ // Assert - timestamp is in request body as ISO string
125
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
126
+ expect(callBody.timestamp).toBe(timestamp.toISOString());
127
+ }
128
+ ),
129
+ { numRuns: 100 }
130
+ );
131
+ });
132
+
133
+ it('should not include timestamp in request body when not provided', async () => {
134
+ await fc.assert(
135
+ fc.asyncProperty(
136
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
137
+ fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), // Event name
138
+ fc.string({ minLength: 1, maxLength: 255 }).filter(s => s.trim().length > 0), // User ID
139
+ async (apiKey, eventName, userId) => {
140
+ // Arrange
141
+ mockFetch.mockClear();
142
+ mockFetch.mockResolvedValue(createSuccessResponse());
143
+ const sdk = new Rooguys(apiKey);
144
+
145
+ // Act
146
+ await sdk.events.track(eventName, userId, {});
147
+
148
+ // Assert - request was made
149
+ expect(mockFetch).toHaveBeenCalledTimes(1);
150
+
151
+ // Assert - timestamp is NOT in request body
152
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
153
+ expect(callBody.timestamp).toBeUndefined();
154
+ }
155
+ ),
156
+ { numRuns: 100 }
157
+ );
158
+ });
159
+ });
160
+
161
+ describe('Batch Event Tracking (trackBatch)', () => {
162
+ it('should throw ValidationError for any event with timestamp older than 7 days', async () => {
163
+ await fc.assert(
164
+ fc.asyncProperty(
165
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
166
+ fc.integer({ min: 0, max: 9 }), // Index of the event with old timestamp
167
+ fc.integer({ min: 1, max: 10 }), // Total events (at least 1)
168
+ oldTimestamp(),
169
+ async (apiKey, oldTimestampIndex, totalEvents, timestamp) => {
170
+ // Ensure oldTimestampIndex is within bounds
171
+ const actualIndex = oldTimestampIndex % totalEvents;
172
+
173
+ // Arrange
174
+ mockFetch.mockClear();
175
+ const sdk = new Rooguys(apiKey);
176
+ const events = Array.from({ length: totalEvents }, (_, i) => ({
177
+ eventName: `event_${i}`,
178
+ userId: `user_${i}`,
179
+ ...(i === actualIndex ? { timestamp } : {}),
180
+ }));
181
+
182
+ // Act & Assert
183
+ try {
184
+ await sdk.events.trackBatch(events);
185
+ // Should not reach here
186
+ expect(true).toBe(false);
187
+ } catch (error) {
188
+ expect(error).toBeInstanceOf(ValidationError);
189
+ expect(error.code).toBe('TIMESTAMP_TOO_OLD');
190
+ expect(error.message).toContain(`index ${actualIndex}`);
191
+ }
192
+
193
+ // Verify no API request was made
194
+ expect(mockFetch).not.toHaveBeenCalled();
195
+ }
196
+ ),
197
+ { numRuns: 100 }
198
+ );
199
+ });
200
+
201
+ it('should include timestamps in batch request for valid timestamps', async () => {
202
+ await fc.assert(
203
+ fc.asyncProperty(
204
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
205
+ fc.array(
206
+ fc.record({
207
+ eventName: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
208
+ userId: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
209
+ hasTimestamp: fc.boolean(),
210
+ }),
211
+ { minLength: 1, maxLength: 20 }
212
+ ),
213
+ async (apiKey, eventConfigs) => {
214
+ // Arrange
215
+ mockFetch.mockClear();
216
+ mockFetch.mockResolvedValue(createBatchSuccessResponse(eventConfigs.length));
217
+ const sdk = new Rooguys(apiKey);
218
+
219
+ // Create events with valid timestamps for those that should have them
220
+ const events = eventConfigs.map((config, i) => ({
221
+ eventName: config.eventName,
222
+ userId: config.userId,
223
+ ...(config.hasTimestamp ? { timestamp: new Date(Date.now() - i * 60 * 60 * 1000) } : {}), // Each event i hours ago
224
+ }));
225
+
226
+ // Act
227
+ await sdk.events.trackBatch(events);
228
+
229
+ // Assert - request was made
230
+ expect(mockFetch).toHaveBeenCalledTimes(1);
231
+
232
+ // Assert - timestamps are correctly included/excluded
233
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
234
+ eventConfigs.forEach((config, i) => {
235
+ if (config.hasTimestamp) {
236
+ expect(callBody.events[i].timestamp).toBeDefined();
237
+ expect(typeof callBody.events[i].timestamp).toBe('string');
238
+ } else {
239
+ expect(callBody.events[i].timestamp).toBeUndefined();
240
+ }
241
+ });
242
+ }
243
+ ),
244
+ { numRuns: 100 }
245
+ );
246
+ });
247
+
248
+ it('should accept batch where all events have valid timestamps', async () => {
249
+ await fc.assert(
250
+ fc.asyncProperty(
251
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
252
+ fc.integer({ min: 1, max: 20 }), // Number of events
253
+ async (apiKey, eventCount) => {
254
+ // Arrange
255
+ mockFetch.mockClear();
256
+ mockFetch.mockResolvedValue(createBatchSuccessResponse(eventCount));
257
+ const sdk = new Rooguys(apiKey);
258
+
259
+ // Create events with valid timestamps (within 7 days)
260
+ const events = Array.from({ length: eventCount }, (_, i) => ({
261
+ eventName: `event_${i}`,
262
+ userId: `user_${i}`,
263
+ timestamp: new Date(Date.now() - i * 60 * 60 * 1000), // Each event i hours ago
264
+ }));
265
+
266
+ // Act
267
+ const result = await sdk.events.trackBatch(events);
268
+
269
+ // Assert - request was made and succeeded
270
+ expect(mockFetch).toHaveBeenCalledTimes(1);
271
+ expect(result.results).toHaveLength(eventCount);
272
+
273
+ // Assert - all timestamps are in ISO format
274
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
275
+ callBody.events.forEach((event, i) => {
276
+ expect(event.timestamp).toBe(events[i].timestamp.toISOString());
277
+ });
278
+ }
279
+ ),
280
+ { numRuns: 100 }
281
+ );
282
+ });
283
+ });
284
+
285
+ describe('Boundary Conditions', () => {
286
+ it('should accept timestamp exactly at 7 day boundary', async () => {
287
+ await fc.assert(
288
+ fc.asyncProperty(
289
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
290
+ fc.integer({ min: 0, max: 1000 }), // Milliseconds before the 7-day boundary
291
+ async (apiKey, msBeforeBoundary) => {
292
+ // Arrange
293
+ mockFetch.mockClear();
294
+ mockFetch.mockResolvedValue(createSuccessResponse());
295
+ const sdk = new Rooguys(apiKey);
296
+
297
+ // Timestamp just within 7 days (7 days minus some milliseconds)
298
+ const timestamp = new Date(Date.now() - SEVEN_DAYS_MS + msBeforeBoundary + 1000); // +1000ms buffer
299
+
300
+ // Act
301
+ await sdk.events.track('test_event', 'user_123', {}, { timestamp });
302
+
303
+ // Assert - request was made
304
+ expect(mockFetch).toHaveBeenCalledTimes(1);
305
+
306
+ // Assert - timestamp is in request body
307
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
308
+ expect(callBody.timestamp).toBe(timestamp.toISOString());
309
+ }
310
+ ),
311
+ { numRuns: 100 }
312
+ );
313
+ });
314
+
315
+ it('should reject timestamp just past 7 day boundary', async () => {
316
+ await fc.assert(
317
+ fc.asyncProperty(
318
+ fc.string({ minLength: 10, maxLength: 100 }), // API key
319
+ fc.integer({ min: 1000, max: 100000 }), // Milliseconds past the 7-day boundary
320
+ async (apiKey, msPastBoundary) => {
321
+ // Arrange
322
+ mockFetch.mockClear();
323
+ const sdk = new Rooguys(apiKey);
324
+
325
+ // Timestamp just past 7 days
326
+ const timestamp = new Date(Date.now() - SEVEN_DAYS_MS - msPastBoundary);
327
+
328
+ // Act & Assert
329
+ try {
330
+ await sdk.events.track('test_event', 'user_123', {}, { timestamp });
331
+ expect(true).toBe(false); // Should not reach here
332
+ } catch (error) {
333
+ expect(error).toBeInstanceOf(ValidationError);
334
+ expect(error.code).toBe('TIMESTAMP_TOO_OLD');
335
+ }
336
+
337
+ // Verify no API request was made
338
+ expect(mockFetch).not.toHaveBeenCalled();
339
+ }
340
+ ),
341
+ { numRuns: 100 }
342
+ );
343
+ });
344
+ });
345
+ });
@@ -1,6 +1,7 @@
1
1
  import { jest } from '@jest/globals';
2
- import Rooguys from '../../index.js';
2
+ import Rooguys, { ValidationError } from '../../index.js';
3
3
  import { mockResponses, mockErrors } from '../fixtures/responses.js';
4
+ import { createMockHeaders } from '../utils/mockClient.js';
4
5
 
5
6
  describe('Aha Resource', () => {
6
7
  let client;
@@ -19,7 +20,12 @@ describe('Aha Resource', () => {
19
20
  it('should declare aha score with valid value', async () => {
20
21
  global.fetch = jest.fn().mockResolvedValue({
21
22
  ok: true,
22
- json: async () => mockResponses.ahaDeclarationResponse,
23
+ headers: createMockHeaders({
24
+ 'X-RateLimit-Limit': '1000',
25
+ 'X-RateLimit-Remaining': '950',
26
+ 'X-RateLimit-Reset': '1704067200',
27
+ }),
28
+ json: async () => ({ success: true, data: mockResponses.ahaDeclarationResponse }),
23
29
  });
24
30
 
25
31
  const result = await client.aha.declare('user123', 4);
@@ -37,7 +43,12 @@ describe('Aha Resource', () => {
37
43
  it('should declare aha score with value 1', async () => {
38
44
  global.fetch = jest.fn().mockResolvedValue({
39
45
  ok: true,
40
- json: async () => mockResponses.ahaDeclarationResponse,
46
+ headers: createMockHeaders({
47
+ 'X-RateLimit-Limit': '1000',
48
+ 'X-RateLimit-Remaining': '950',
49
+ 'X-RateLimit-Reset': '1704067200',
50
+ }),
51
+ json: async () => ({ success: true, data: mockResponses.ahaDeclarationResponse }),
41
52
  });
42
53
 
43
54
  const result = await client.aha.declare('user123', 1);
@@ -54,7 +65,12 @@ describe('Aha Resource', () => {
54
65
  it('should declare aha score with value 5', async () => {
55
66
  global.fetch = jest.fn().mockResolvedValue({
56
67
  ok: true,
57
- json: async () => mockResponses.ahaDeclarationResponse,
68
+ headers: createMockHeaders({
69
+ 'X-RateLimit-Limit': '1000',
70
+ 'X-RateLimit-Remaining': '950',
71
+ 'X-RateLimit-Reset': '1704067200',
72
+ }),
73
+ json: async () => ({ success: true, data: mockResponses.ahaDeclarationResponse }),
58
74
  });
59
75
 
60
76
  const result = await client.aha.declare('user123', 5);
@@ -68,35 +84,29 @@ describe('Aha Resource', () => {
68
84
  expect(result).toEqual(mockResponses.ahaDeclarationResponse);
69
85
  });
70
86
 
71
- it('should throw error for value 0', () => {
72
- expect(() => client.aha.declare('user123', 0)).toThrow(
73
- 'Aha score value must be an integer between 1 and 5'
74
- );
87
+ it('should throw ValidationError for value 0', async () => {
88
+ await expect(client.aha.declare('user123', 0)).rejects.toThrow(ValidationError);
75
89
  });
76
90
 
77
- it('should throw error for value 6', () => {
78
- expect(() => client.aha.declare('user123', 6)).toThrow(
79
- 'Aha score value must be an integer between 1 and 5'
80
- );
91
+ it('should throw ValidationError for value 6', async () => {
92
+ await expect(client.aha.declare('user123', 6)).rejects.toThrow(ValidationError);
81
93
  });
82
94
 
83
- it('should throw error for negative value', () => {
84
- expect(() => client.aha.declare('user123', -1)).toThrow(
85
- 'Aha score value must be an integer between 1 and 5'
86
- );
95
+ it('should throw ValidationError for negative value', async () => {
96
+ await expect(client.aha.declare('user123', -1)).rejects.toThrow(ValidationError);
87
97
  });
88
98
 
89
- it('should throw error for non-integer value', () => {
90
- expect(() => client.aha.declare('user123', 3.5)).toThrow(
91
- 'Aha score value must be an integer between 1 and 5'
92
- );
99
+ it('should throw ValidationError for non-integer value', async () => {
100
+ await expect(client.aha.declare('user123', 3.5)).rejects.toThrow(ValidationError);
93
101
  });
94
102
 
95
103
  it('should handle API error response', async () => {
96
104
  global.fetch = jest.fn().mockResolvedValue({
97
105
  ok: false,
106
+ status: 400,
98
107
  statusText: 'Bad Request',
99
- json: async () => mockErrors.ahaValueError,
108
+ headers: createMockHeaders({}),
109
+ json: async () => ({ success: false, error: mockErrors.ahaValueError }),
100
110
  });
101
111
 
102
112
  await expect(client.aha.declare('user123', 3)).rejects.toThrow();
@@ -107,7 +117,12 @@ describe('Aha Resource', () => {
107
117
  it('should get user aha score successfully', async () => {
108
118
  global.fetch = jest.fn().mockResolvedValue({
109
119
  ok: true,
110
- json: async () => mockResponses.ahaScoreResponse,
120
+ headers: createMockHeaders({
121
+ 'X-RateLimit-Limit': '1000',
122
+ 'X-RateLimit-Remaining': '950',
123
+ 'X-RateLimit-Reset': '1704067200',
124
+ }),
125
+ json: async () => ({ success: true, data: mockResponses.ahaScoreResponse }),
111
126
  });
112
127
 
113
128
  const result = await client.aha.getUserScore('user123');
@@ -121,7 +136,12 @@ describe('Aha Resource', () => {
121
136
  it('should parse all aha score fields correctly', async () => {
122
137
  global.fetch = jest.fn().mockResolvedValue({
123
138
  ok: true,
124
- json: async () => mockResponses.ahaScoreResponse,
139
+ headers: createMockHeaders({
140
+ 'X-RateLimit-Limit': '1000',
141
+ 'X-RateLimit-Remaining': '950',
142
+ 'X-RateLimit-Reset': '1704067200',
143
+ }),
144
+ json: async () => ({ success: true, data: mockResponses.ahaScoreResponse }),
125
145
  });
126
146
 
127
147
  const result = await client.aha.getUserScore('user123');
@@ -137,7 +157,12 @@ describe('Aha Resource', () => {
137
157
  it('should preserve history structure', async () => {
138
158
  global.fetch = jest.fn().mockResolvedValue({
139
159
  ok: true,
140
- json: async () => mockResponses.ahaScoreResponse,
160
+ headers: createMockHeaders({
161
+ 'X-RateLimit-Limit': '1000',
162
+ 'X-RateLimit-Remaining': '950',
163
+ 'X-RateLimit-Reset': '1704067200',
164
+ }),
165
+ json: async () => ({ success: true, data: mockResponses.ahaScoreResponse }),
141
166
  });
142
167
 
143
168
  const result = await client.aha.getUserScore('user123');
@@ -154,7 +179,8 @@ describe('Aha Resource', () => {
154
179
  ok: false,
155
180
  status: 404,
156
181
  statusText: 'Not Found',
157
- json: async () => mockErrors.notFoundError,
182
+ headers: createMockHeaders({}),
183
+ json: async () => ({ success: false, error: mockErrors.notFoundError }),
158
184
  });
159
185
 
160
186
  await expect(client.aha.getUserScore('nonexistent')).rejects.toThrow();
@@ -178,7 +204,12 @@ describe('Aha Resource', () => {
178
204
  };
179
205
  global.fetch = jest.fn().mockResolvedValue({
180
206
  ok: true,
181
- json: async () => responseWithNulls,
207
+ headers: createMockHeaders({
208
+ 'X-RateLimit-Limit': '1000',
209
+ 'X-RateLimit-Remaining': '950',
210
+ 'X-RateLimit-Reset': '1704067200',
211
+ }),
212
+ json: async () => ({ success: true, data: responseWithNulls }),
182
213
  });
183
214
 
184
215
  const result = await client.aha.getUserScore('user123');
@@ -1,4 +1,5 @@
1
1
  import Rooguys from '../../index';
2
+ import { createMockHeaders } from '../utils/mockClient';
2
3
 
3
4
  describe('SDK Configuration', () => {
4
5
  beforeEach(() => {
@@ -71,7 +72,12 @@ describe('SDK Configuration', () => {
71
72
  const client = new Rooguys('my-secret-key');
72
73
  global.fetch.mockResolvedValue({
73
74
  ok: true,
74
- json: () => Promise.resolve({}),
75
+ headers: createMockHeaders({
76
+ 'X-RateLimit-Limit': '1000',
77
+ 'X-RateLimit-Remaining': '950',
78
+ 'X-RateLimit-Reset': '1704067200',
79
+ }),
80
+ json: () => Promise.resolve({ success: true, data: {} }),
75
81
  });
76
82
 
77
83
  await client.events.track('test', 'user1');
@@ -1,5 +1,5 @@
1
1
  import Rooguys from '../../index';
2
- import { mockErrorResponse, mockTimeoutError, mockNetworkError } from '../utils/mockClient';
2
+ import { mockErrorResponse, mockTimeoutError, mockNetworkError, createMockHeaders } from '../utils/mockClient';
3
3
  import { mockErrors } from '../fixtures/responses';
4
4
 
5
5
  describe('Error Handling', () => {
@@ -71,7 +71,7 @@ describe('Error Handling', () => {
71
71
  it('should throw error for network timeout', async () => {
72
72
  global.fetch.mockRejectedValue(mockTimeoutError());
73
73
 
74
- await expect(client.events.track('test', 'user1')).rejects.toThrow('aborted');
74
+ await expect(client.events.track('test', 'user1')).rejects.toThrow('Request timeout');
75
75
  });
76
76
 
77
77
  it('should throw error for network failure', async () => {
@@ -93,12 +93,11 @@ describe('Error Handling', () => {
93
93
  ok: false,
94
94
  status: 500,
95
95
  statusText: 'Internal Server Error',
96
+ headers: createMockHeaders({}),
96
97
  json: () => Promise.resolve({}),
97
98
  });
98
99
 
99
- await expect(client.events.track('test', 'user1')).rejects.toThrow(
100
- 'Internal Server Error'
101
- );
100
+ await expect(client.events.track('test', 'user1')).rejects.toThrow();
102
101
  });
103
102
 
104
103
  it('should handle response with invalid JSON', async () => {
@@ -106,12 +105,11 @@ describe('Error Handling', () => {
106
105
  ok: false,
107
106
  status: 500,
108
107
  statusText: 'Internal Server Error',
108
+ headers: createMockHeaders({}),
109
109
  json: () => Promise.reject(new Error('Invalid JSON')),
110
110
  });
111
111
 
112
- await expect(client.events.track('test', 'user1')).rejects.toThrow(
113
- 'Internal Server Error'
114
- );
112
+ await expect(client.events.track('test', 'user1')).rejects.toThrow();
115
113
  });
116
114
  });
117
115