@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,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property-Based Test: Email Validation
|
|
3
|
+
* Feature: sdk-documentation-update, Property 7: Email Validation
|
|
4
|
+
* Validates: Requirements 4.5
|
|
5
|
+
*
|
|
6
|
+
* For any user creation or update with an email field, the SDK SHALL validate
|
|
7
|
+
* the email format client-side. Invalid emails SHALL cause a ValidationError
|
|
8
|
+
* to be thrown before making an API request.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fc from 'fast-check';
|
|
12
|
+
import { jest } from '@jest/globals';
|
|
13
|
+
import Rooguys, { ValidationError } from '../../index.js';
|
|
14
|
+
import { createMockFetch, createMockHeaders } from '../utils/mockClient.js';
|
|
15
|
+
|
|
16
|
+
describe('Property 7: Email Validation', () => {
|
|
17
|
+
let mockFetch;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockFetch = createMockFetch();
|
|
21
|
+
global.fetch = mockFetch;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const createSuccessResponse = (data) => ({
|
|
29
|
+
ok: true,
|
|
30
|
+
headers: createMockHeaders({
|
|
31
|
+
'X-RateLimit-Limit': '1000',
|
|
32
|
+
'X-RateLimit-Remaining': '950',
|
|
33
|
+
'X-RateLimit-Reset': '1704067200',
|
|
34
|
+
}),
|
|
35
|
+
json: async () => ({
|
|
36
|
+
success: true,
|
|
37
|
+
data,
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Generator for valid emails
|
|
42
|
+
const validEmail = fc.tuple(
|
|
43
|
+
fc.string({ minLength: 1, maxLength: 50 }).filter(s => /^[a-zA-Z0-9._+-]+$/.test(s) && s.length > 0),
|
|
44
|
+
fc.string({ minLength: 1, maxLength: 30 }).filter(s => /^[a-zA-Z0-9.-]+$/.test(s) && s.length > 0),
|
|
45
|
+
fc.constantFrom('com', 'org', 'net', 'io', 'co.uk', 'edu', 'gov')
|
|
46
|
+
).map(([local, domain, tld]) => `${local}@${domain}.${tld}`);
|
|
47
|
+
|
|
48
|
+
// Generator for invalid emails (missing @, missing domain, etc.)
|
|
49
|
+
const invalidEmail = fc.oneof(
|
|
50
|
+
// Missing @ symbol
|
|
51
|
+
fc.string({ minLength: 1, maxLength: 50 }).filter(s => !s.includes('@') && s.trim().length > 0),
|
|
52
|
+
// Missing local part
|
|
53
|
+
fc.string({ minLength: 1, maxLength: 30 }).map(s => `@${s}.com`),
|
|
54
|
+
// Missing domain
|
|
55
|
+
fc.string({ minLength: 1, maxLength: 30 }).map(s => `${s}@`),
|
|
56
|
+
// Missing TLD
|
|
57
|
+
fc.string({ minLength: 1, maxLength: 30 }).map(s => `${s}@domain`),
|
|
58
|
+
// Multiple @ symbols
|
|
59
|
+
fc.tuple(
|
|
60
|
+
fc.string({ minLength: 1, maxLength: 20 }),
|
|
61
|
+
fc.string({ minLength: 1, maxLength: 20 }),
|
|
62
|
+
fc.string({ minLength: 1, maxLength: 20 })
|
|
63
|
+
).map(([a, b, c]) => `${a}@${b}@${c}.com`),
|
|
64
|
+
// Whitespace only
|
|
65
|
+
fc.constantFrom(' ', '\t', '\n'),
|
|
66
|
+
// Just dots
|
|
67
|
+
fc.constantFrom('.', '..', '...'),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
describe('users.create()', () => {
|
|
71
|
+
it('should accept valid email formats without throwing ValidationError', async () => {
|
|
72
|
+
await fc.assert(
|
|
73
|
+
fc.asyncProperty(
|
|
74
|
+
fc.string({ minLength: 10, maxLength: 100 }), // API key
|
|
75
|
+
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), // User ID
|
|
76
|
+
validEmail,
|
|
77
|
+
async (apiKey, userId, email) => {
|
|
78
|
+
// Arrange
|
|
79
|
+
mockFetch.mockClear();
|
|
80
|
+
mockFetch.mockResolvedValue(createSuccessResponse({
|
|
81
|
+
user_id: userId,
|
|
82
|
+
email,
|
|
83
|
+
points: 0,
|
|
84
|
+
}));
|
|
85
|
+
const sdk = new Rooguys(apiKey);
|
|
86
|
+
|
|
87
|
+
// Act - should not throw
|
|
88
|
+
const result = await sdk.users.create({ userId, email });
|
|
89
|
+
|
|
90
|
+
// Assert - API request was made
|
|
91
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
92
|
+
expect(result.user_id).toBe(userId);
|
|
93
|
+
}
|
|
94
|
+
),
|
|
95
|
+
{ numRuns: 100 }
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should throw ValidationError for invalid email formats before making API request', async () => {
|
|
100
|
+
await fc.assert(
|
|
101
|
+
fc.asyncProperty(
|
|
102
|
+
fc.string({ minLength: 10, maxLength: 100 }), // API key
|
|
103
|
+
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), // User ID
|
|
104
|
+
invalidEmail,
|
|
105
|
+
async (apiKey, userId, email) => {
|
|
106
|
+
// Arrange
|
|
107
|
+
mockFetch.mockClear();
|
|
108
|
+
const sdk = new Rooguys(apiKey);
|
|
109
|
+
|
|
110
|
+
// Act & Assert
|
|
111
|
+
await expect(sdk.users.create({ userId, email })).rejects.toThrow(ValidationError);
|
|
112
|
+
await expect(sdk.users.create({ userId, email })).rejects.toThrow('Invalid email format');
|
|
113
|
+
|
|
114
|
+
// Verify no API request was made
|
|
115
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
116
|
+
}
|
|
117
|
+
),
|
|
118
|
+
{ numRuns: 100 }
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should allow user creation without email (optional field)', async () => {
|
|
123
|
+
await fc.assert(
|
|
124
|
+
fc.asyncProperty(
|
|
125
|
+
fc.string({ minLength: 10, maxLength: 100 }), // API key
|
|
126
|
+
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), // User ID
|
|
127
|
+
async (apiKey, userId) => {
|
|
128
|
+
// Arrange
|
|
129
|
+
mockFetch.mockClear();
|
|
130
|
+
mockFetch.mockResolvedValue(createSuccessResponse({
|
|
131
|
+
user_id: userId,
|
|
132
|
+
points: 0,
|
|
133
|
+
}));
|
|
134
|
+
const sdk = new Rooguys(apiKey);
|
|
135
|
+
|
|
136
|
+
// Act - should not throw
|
|
137
|
+
const result = await sdk.users.create({ userId });
|
|
138
|
+
|
|
139
|
+
// Assert - API request was made
|
|
140
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
141
|
+
expect(result.user_id).toBe(userId);
|
|
142
|
+
}
|
|
143
|
+
),
|
|
144
|
+
{ numRuns: 100 }
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('users.update()', () => {
|
|
150
|
+
it('should accept valid email formats in updates without throwing ValidationError', async () => {
|
|
151
|
+
await fc.assert(
|
|
152
|
+
fc.asyncProperty(
|
|
153
|
+
fc.string({ minLength: 10, maxLength: 100 }), // API key
|
|
154
|
+
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), // User ID
|
|
155
|
+
validEmail,
|
|
156
|
+
async (apiKey, userId, email) => {
|
|
157
|
+
// Arrange
|
|
158
|
+
mockFetch.mockClear();
|
|
159
|
+
mockFetch.mockResolvedValue(createSuccessResponse({
|
|
160
|
+
user_id: userId,
|
|
161
|
+
email,
|
|
162
|
+
points: 100,
|
|
163
|
+
}));
|
|
164
|
+
const sdk = new Rooguys(apiKey);
|
|
165
|
+
|
|
166
|
+
// Act - should not throw
|
|
167
|
+
const result = await sdk.users.update(userId, { email });
|
|
168
|
+
|
|
169
|
+
// Assert - API request was made
|
|
170
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
171
|
+
expect(result.email).toBe(email);
|
|
172
|
+
}
|
|
173
|
+
),
|
|
174
|
+
{ numRuns: 100 }
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should throw ValidationError for invalid email formats in updates before making API request', async () => {
|
|
179
|
+
await fc.assert(
|
|
180
|
+
fc.asyncProperty(
|
|
181
|
+
fc.string({ minLength: 10, maxLength: 100 }), // API key
|
|
182
|
+
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), // User ID
|
|
183
|
+
invalidEmail,
|
|
184
|
+
async (apiKey, userId, email) => {
|
|
185
|
+
// Arrange
|
|
186
|
+
mockFetch.mockClear();
|
|
187
|
+
const sdk = new Rooguys(apiKey);
|
|
188
|
+
|
|
189
|
+
// Act & Assert
|
|
190
|
+
await expect(sdk.users.update(userId, { email })).rejects.toThrow(ValidationError);
|
|
191
|
+
await expect(sdk.users.update(userId, { email })).rejects.toThrow('Invalid email format');
|
|
192
|
+
|
|
193
|
+
// Verify no API request was made
|
|
194
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
195
|
+
}
|
|
196
|
+
),
|
|
197
|
+
{ numRuns: 100 }
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('users.createBatch()', () => {
|
|
203
|
+
it('should accept valid email formats in batch without throwing ValidationError', async () => {
|
|
204
|
+
await fc.assert(
|
|
205
|
+
fc.asyncProperty(
|
|
206
|
+
fc.string({ minLength: 10, maxLength: 100 }), // API key
|
|
207
|
+
fc.array(
|
|
208
|
+
fc.record({
|
|
209
|
+
userId: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
|
210
|
+
email: validEmail,
|
|
211
|
+
}),
|
|
212
|
+
{ minLength: 1, maxLength: 10 }
|
|
213
|
+
),
|
|
214
|
+
async (apiKey, users) => {
|
|
215
|
+
// Arrange
|
|
216
|
+
mockFetch.mockClear();
|
|
217
|
+
mockFetch.mockResolvedValue(createSuccessResponse({
|
|
218
|
+
results: users.map((_, i) => ({ index: i, status: 'created' })),
|
|
219
|
+
}));
|
|
220
|
+
const sdk = new Rooguys(apiKey);
|
|
221
|
+
|
|
222
|
+
// Act - should not throw
|
|
223
|
+
const result = await sdk.users.createBatch(users);
|
|
224
|
+
|
|
225
|
+
// Assert - API request was made
|
|
226
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
227
|
+
expect(result.results).toHaveLength(users.length);
|
|
228
|
+
}
|
|
229
|
+
),
|
|
230
|
+
{ numRuns: 100 }
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should throw ValidationError for any invalid email in batch before making API request', async () => {
|
|
235
|
+
await fc.assert(
|
|
236
|
+
fc.asyncProperty(
|
|
237
|
+
fc.string({ minLength: 10, maxLength: 100 }), // API key
|
|
238
|
+
fc.integer({ min: 0, max: 5 }), // Index of invalid email
|
|
239
|
+
fc.array(
|
|
240
|
+
fc.record({
|
|
241
|
+
userId: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
|
242
|
+
email: validEmail,
|
|
243
|
+
}),
|
|
244
|
+
{ minLength: 1, maxLength: 5 }
|
|
245
|
+
),
|
|
246
|
+
invalidEmail,
|
|
247
|
+
async (apiKey, invalidIndex, validUsers, invalidEmailValue) => {
|
|
248
|
+
// Arrange
|
|
249
|
+
mockFetch.mockClear();
|
|
250
|
+
const sdk = new Rooguys(apiKey);
|
|
251
|
+
|
|
252
|
+
// Insert invalid email at specified index
|
|
253
|
+
const users = [...validUsers];
|
|
254
|
+
const actualIndex = Math.min(invalidIndex, users.length);
|
|
255
|
+
users.splice(actualIndex, 0, {
|
|
256
|
+
userId: `invalid_user_${actualIndex}`,
|
|
257
|
+
email: invalidEmailValue,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Act & Assert
|
|
261
|
+
await expect(sdk.users.createBatch(users)).rejects.toThrow(ValidationError);
|
|
262
|
+
await expect(sdk.users.createBatch(users)).rejects.toThrow('Invalid email format');
|
|
263
|
+
|
|
264
|
+
// Verify no API request was made
|
|
265
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
266
|
+
}
|
|
267
|
+
),
|
|
268
|
+
{ numRuns: 100 }
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|