@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
72
|
-
expect(
|
|
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
|
|
78
|
-
expect(
|
|
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
|
|
84
|
-
expect(
|
|
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
|
|
90
|
-
expect(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
|