@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,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
+ });