@rooguys/js 0.1.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,211 @@
1
+ /**
2
+ * Property-Based Test: HTTP Request Construction
3
+ * Feature: sdk-testing-enhancement, Property 1: HTTP Request Construction
4
+ * Validates: Requirements 1.1, 3.1
5
+ *
6
+ * Tests that any valid SDK method call constructs correct HTTP request
7
+ * with proper method, URL, headers, and body structure.
8
+ */
9
+
10
+ import fc from 'fast-check';
11
+ import { jest } from '@jest/globals';
12
+ import Rooguys from '../../index.js';
13
+ import { createMockFetch } from '../utils/mockClient.js';
14
+ import { arbitraries } from '../utils/generators.js';
15
+
16
+ describe('Property: HTTP Request Construction', () => {
17
+ let mockFetch;
18
+
19
+ beforeEach(() => {
20
+ mockFetch = createMockFetch();
21
+ global.fetch = mockFetch;
22
+ });
23
+
24
+ afterEach(() => {
25
+ jest.clearAllMocks();
26
+ });
27
+
28
+ it('should construct valid POST request for event tracking', async () => {
29
+ await fc.assert(
30
+ fc.asyncProperty(
31
+ arbitraries.apiKey(),
32
+ arbitraries.eventName(),
33
+ arbitraries.userId(),
34
+ arbitraries.properties(),
35
+ async (apiKey, eventName, userId, properties) => {
36
+ // Arrange
37
+ mockFetch.mockClear();
38
+ mockFetch.mockResolvedValue({
39
+ ok: true,
40
+ json: async () => ({ status: 'queued', message: 'Event accepted' })
41
+ });
42
+ const sdk = new Rooguys(apiKey);
43
+
44
+ // Act
45
+ await sdk.events.track(eventName, userId, properties);
46
+
47
+ // Assert
48
+ expect(mockFetch).toHaveBeenCalledTimes(1);
49
+ const callArgs = mockFetch.mock.calls[0];
50
+ expect(callArgs[0]).toContain('/event');
51
+ expect(callArgs[1].method).toBe('POST');
52
+ const body = JSON.parse(callArgs[1].body);
53
+ expect(body.event_name).toBe(eventName);
54
+ expect(body.user_id).toBe(userId);
55
+ expect(body.properties).toEqual(properties);
56
+ }
57
+ ),
58
+ { numRuns: 100 }
59
+ );
60
+ });
61
+
62
+ it('should construct valid GET request for user profile', async () => {
63
+ await fc.assert(
64
+ fc.asyncProperty(
65
+ arbitraries.apiKey(),
66
+ arbitraries.userId(),
67
+ async (apiKey, userId) => {
68
+ // Arrange
69
+ mockFetch.mockClear();
70
+ mockFetch.mockResolvedValue({
71
+ ok: true,
72
+ json: async () => ({ user_id: userId, points: 100 })
73
+ });
74
+ const sdk = new Rooguys(apiKey);
75
+
76
+ // Act
77
+ await sdk.users.get(userId);
78
+
79
+ // Assert
80
+ expect(mockFetch).toHaveBeenCalledTimes(1);
81
+ const callArgs = mockFetch.mock.calls[0];
82
+ expect(callArgs[0]).toContain(`/user/${encodeURIComponent(userId)}`);
83
+ expect(callArgs[1].method).toBe('GET');
84
+ }
85
+ ),
86
+ { numRuns: 100 }
87
+ );
88
+ });
89
+
90
+ it('should construct valid POST request for bulk user fetch', async () => {
91
+ await fc.assert(
92
+ fc.asyncProperty(
93
+ arbitraries.apiKey(),
94
+ arbitraries.userIds(),
95
+ async (apiKey, userIds) => {
96
+ // Arrange
97
+ mockFetch.mockClear();
98
+ mockFetch.mockResolvedValue({
99
+ ok: true,
100
+ json: async () => ({ users: [] })
101
+ });
102
+ const sdk = new Rooguys(apiKey);
103
+
104
+ // Act
105
+ await sdk.users.getBulk(userIds);
106
+
107
+ // Assert
108
+ expect(mockFetch).toHaveBeenCalledTimes(1);
109
+ const callArgs = mockFetch.mock.calls[0];
110
+ expect(callArgs[0]).toContain('/users/bulk');
111
+ expect(callArgs[1].method).toBe('POST');
112
+ const body = JSON.parse(callArgs[1].body);
113
+ expect(body.user_ids).toEqual(userIds);
114
+ }
115
+ ),
116
+ { numRuns: 100 }
117
+ );
118
+ });
119
+
120
+ it('should construct valid GET request with query parameters for leaderboard', async () => {
121
+ await fc.assert(
122
+ fc.asyncProperty(
123
+ arbitraries.apiKey(),
124
+ arbitraries.timeframe(),
125
+ arbitraries.pagination(),
126
+ async (apiKey, timeframe, { page, limit }) => {
127
+ // Arrange
128
+ mockFetch.mockClear();
129
+ mockFetch.mockResolvedValue({
130
+ ok: true,
131
+ json: async () => ({ rankings: [], page, limit, total: 0 })
132
+ });
133
+ const sdk = new Rooguys(apiKey);
134
+
135
+ // Act
136
+ await sdk.leaderboards.getGlobal(timeframe, page, limit);
137
+
138
+ // Assert
139
+ expect(mockFetch).toHaveBeenCalledTimes(1);
140
+ const callArgs = mockFetch.mock.calls[0];
141
+ const url = new URL(callArgs[0]);
142
+ expect(url.pathname).toContain('/leaderboard');
143
+ expect(url.searchParams.get('timeframe')).toBe(timeframe);
144
+ expect(url.searchParams.get('page')).toBe(String(page));
145
+ expect(url.searchParams.get('limit')).toBe(String(limit));
146
+ }
147
+ ),
148
+ { numRuns: 100 }
149
+ );
150
+ });
151
+
152
+ it('should construct valid POST request for Aha score declaration', async () => {
153
+ await fc.assert(
154
+ fc.asyncProperty(
155
+ arbitraries.apiKey(),
156
+ arbitraries.userId(),
157
+ arbitraries.ahaValue(),
158
+ async (apiKey, userId, value) => {
159
+ // Arrange
160
+ mockFetch.mockClear();
161
+ mockFetch.mockResolvedValue({
162
+ ok: true,
163
+ json: async () => ({ success: true, message: 'Score declared' })
164
+ });
165
+ const sdk = new Rooguys(apiKey);
166
+
167
+ // Act
168
+ await sdk.aha.declare(userId, value);
169
+
170
+ // Assert
171
+ expect(mockFetch).toHaveBeenCalledTimes(1);
172
+ const callArgs = mockFetch.mock.calls[0];
173
+ expect(callArgs[0]).toContain('/aha/declare');
174
+ expect(callArgs[1].method).toBe('POST');
175
+ const body = JSON.parse(callArgs[1].body);
176
+ expect(body.user_id).toBe(userId);
177
+ expect(body.value).toBe(value);
178
+ }
179
+ ),
180
+ { numRuns: 100 }
181
+ );
182
+ });
183
+
184
+ it('should include API key in request headers', async () => {
185
+ await fc.assert(
186
+ fc.asyncProperty(
187
+ arbitraries.apiKey(),
188
+ arbitraries.userId(),
189
+ async (apiKey, userId) => {
190
+ // Arrange
191
+ mockFetch.mockClear();
192
+ mockFetch.mockResolvedValue({
193
+ ok: true,
194
+ json: async () => ({ user_id: userId, points: 100 })
195
+ });
196
+ const sdk = new Rooguys(apiKey);
197
+
198
+ // Act
199
+ await sdk.users.get(userId);
200
+
201
+ // Assert
202
+ expect(mockFetch).toHaveBeenCalledTimes(1);
203
+ const callArgs = mockFetch.mock.calls[0];
204
+ expect(callArgs[1].headers['x-api-key']).toBe(apiKey);
205
+ expect(callArgs[1].headers['Content-Type']).toBe('application/json');
206
+ }
207
+ ),
208
+ { numRuns: 100 }
209
+ );
210
+ });
211
+ });
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Property-Based Test: Response Parsing Preservation
3
+ * Feature: sdk-testing-enhancement, Property 2: Response Parsing Preservation
4
+ * Validates: Requirements 1.2, 7.1, 7.2, 7.3, 7.4
5
+ *
6
+ * Tests that any successful response is parsed correctly and data structure
7
+ * is preserved including nested objects, arrays, and null values.
8
+ */
9
+
10
+ import fc from 'fast-check';
11
+ import { jest } from '@jest/globals';
12
+ import Rooguys from '../../index.js';
13
+ import { createMockFetch } from '../utils/mockClient.js';
14
+
15
+ describe('Property: Response Parsing Preservation', () => {
16
+ let mockFetch;
17
+
18
+ beforeEach(() => {
19
+ mockFetch = createMockFetch();
20
+ global.fetch = mockFetch;
21
+ });
22
+
23
+ afterEach(() => {
24
+ jest.clearAllMocks();
25
+ });
26
+
27
+ it('should preserve nested object structures in responses', async () => {
28
+ await fc.assert(
29
+ fc.asyncProperty(
30
+ fc.string({ minLength: 10, maxLength: 100 }),
31
+ fc.string({ minLength: 1, maxLength: 255 }),
32
+ fc.record({
33
+ user_id: fc.string(),
34
+ points: fc.integer(),
35
+ level: fc.record({
36
+ id: fc.string(),
37
+ name: fc.string(),
38
+ level_number: fc.integer(),
39
+ }),
40
+ next_level: fc.option(fc.record({
41
+ id: fc.string(),
42
+ name: fc.string(),
43
+ points_required: fc.integer(),
44
+ }), { nil: null }),
45
+ metrics: fc.dictionary(fc.string(), fc.integer()),
46
+ }),
47
+ async (apiKey, userId, responseData) => {
48
+ // Arrange
49
+ mockFetch.mockClear();
50
+ mockFetch.mockResolvedValue({
51
+ ok: true,
52
+ json: async () => responseData
53
+ });
54
+ const sdk = new Rooguys(apiKey);
55
+
56
+ // Act
57
+ const result = await sdk.users.get(userId);
58
+
59
+ // Assert
60
+ expect(result).toEqual(responseData);
61
+ expect(result.level).toEqual(responseData.level);
62
+ expect(result.next_level).toEqual(responseData.next_level);
63
+ expect(result.metrics).toEqual(responseData.metrics);
64
+ }
65
+ ),
66
+ { numRuns: 100 }
67
+ );
68
+ });
69
+
70
+ it('should preserve arrays in responses', async () => {
71
+ await fc.assert(
72
+ fc.asyncProperty(
73
+ fc.string({ minLength: 10, maxLength: 100 }),
74
+ fc.array(fc.string({ minLength: 1, maxLength: 255 }), { minLength: 1, maxLength: 10 }),
75
+ fc.array(fc.record({
76
+ user_id: fc.string(),
77
+ points: fc.integer(),
78
+ }), { minLength: 0, maxLength: 20 }),
79
+ async (apiKey, userIds, usersData) => {
80
+ // Arrange
81
+ const responseData = { users: usersData };
82
+ mockFetch.mockClear();
83
+ mockFetch.mockResolvedValue({
84
+ ok: true,
85
+ json: async () => responseData
86
+ });
87
+ const sdk = new Rooguys(apiKey);
88
+
89
+ // Act
90
+ const result = await sdk.users.getBulk(userIds);
91
+
92
+ // Assert
93
+ expect(result).toEqual(responseData);
94
+ expect(Array.isArray(result.users)).toBe(true);
95
+ expect(result.users).toHaveLength(usersData.length);
96
+ expect(result.users).toEqual(usersData);
97
+ }
98
+ ),
99
+ { numRuns: 100 }
100
+ );
101
+ });
102
+
103
+ it('should preserve null values in responses', async () => {
104
+ await fc.assert(
105
+ fc.asyncProperty(
106
+ fc.string({ minLength: 10, maxLength: 100 }),
107
+ fc.string({ minLength: 1, maxLength: 255 }),
108
+ fc.record({
109
+ user_id: fc.string(),
110
+ declarative_score: fc.option(fc.integer({ min: 1, max: 5 }), { nil: null }),
111
+ inferred_score: fc.option(fc.integer({ min: 0, max: 100 }), { nil: null }),
112
+ history: fc.record({
113
+ initial: fc.option(fc.integer(), { nil: null }),
114
+ initial_date: fc.option(fc.string(), { nil: null }),
115
+ previous: fc.option(fc.integer(), { nil: null }),
116
+ }),
117
+ }),
118
+ async (apiKey, userId, responseData) => {
119
+ // Arrange
120
+ mockFetch.mockClear();
121
+ mockFetch.mockResolvedValue({
122
+ ok: true,
123
+ json: async () => responseData
124
+ });
125
+ const sdk = new Rooguys(apiKey);
126
+
127
+ // Act
128
+ const result = await sdk.aha.getUserScore(userId);
129
+
130
+ // Assert
131
+ expect(result).toEqual(responseData);
132
+ expect(result.declarative_score).toBe(responseData.declarative_score);
133
+ expect(result.inferred_score).toBe(responseData.inferred_score);
134
+ expect(result.history.initial).toBe(responseData.history.initial);
135
+ expect(result.history.initial_date).toBe(responseData.history.initial_date);
136
+ expect(result.history.previous).toBe(responseData.history.previous);
137
+ }
138
+ ),
139
+ { numRuns: 100 }
140
+ );
141
+ });
142
+
143
+ it('should handle empty objects and arrays', async () => {
144
+ await fc.assert(
145
+ fc.asyncProperty(
146
+ fc.string({ minLength: 10, maxLength: 100 }),
147
+ fc.constantFrom('all-time', 'weekly', 'monthly'),
148
+ async (apiKey, timeframe) => {
149
+ // Arrange
150
+ const responseData = {
151
+ timeframe,
152
+ page: 1,
153
+ limit: 50,
154
+ total: 0,
155
+ rankings: [],
156
+ };
157
+ mockFetch.mockClear();
158
+ mockFetch.mockResolvedValue({
159
+ ok: true,
160
+ json: async () => responseData
161
+ });
162
+ const sdk = new Rooguys(apiKey);
163
+
164
+ // Act
165
+ const result = await sdk.leaderboards.getGlobal(timeframe);
166
+
167
+ // Assert
168
+ expect(result).toEqual(responseData);
169
+ expect(Array.isArray(result.rankings)).toBe(true);
170
+ expect(result.rankings).toHaveLength(0);
171
+ }
172
+ ),
173
+ { numRuns: 100 }
174
+ );
175
+ });
176
+
177
+ it('should preserve complex nested structures', async () => {
178
+ await fc.assert(
179
+ fc.asyncProperty(
180
+ fc.string({ minLength: 10, maxLength: 100 }),
181
+ fc.record({
182
+ success: fc.boolean(),
183
+ data: fc.record({
184
+ user_id: fc.string(),
185
+ current_score: fc.integer({ min: 0, max: 100 }),
186
+ declarative_score: fc.option(fc.integer({ min: 1, max: 5 }), { nil: null }),
187
+ inferred_score: fc.option(fc.integer({ min: 0, max: 100 }), { nil: null }),
188
+ status: fc.constantFrom('not_started', 'progressing', 'activated'),
189
+ history: fc.record({
190
+ initial: fc.option(fc.integer(), { nil: null }),
191
+ initial_date: fc.option(fc.string(), { nil: null }),
192
+ previous: fc.option(fc.integer(), { nil: null }),
193
+ }),
194
+ }),
195
+ }),
196
+ async (apiKey, responseData) => {
197
+ // Arrange
198
+ mockFetch.mockClear();
199
+ mockFetch.mockResolvedValue({
200
+ ok: true,
201
+ json: async () => responseData
202
+ });
203
+ const sdk = new Rooguys(apiKey);
204
+
205
+ // Act
206
+ const result = await sdk.aha.getUserScore('test-user');
207
+
208
+ // Assert
209
+ expect(result).toEqual(responseData);
210
+ expect(result.data).toEqual(responseData.data);
211
+ expect(result.data.history).toEqual(responseData.data.history);
212
+ }
213
+ ),
214
+ { numRuns: 100 }
215
+ );
216
+ });
217
+ });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Jest setup file for browser SDK tests
3
+ */
4
+ import { jest } from '@jest/globals';
5
+
6
+ // Setup fetch mock
7
+ global.fetch = jest.fn();
8
+
9
+ // Setup AbortController for timeout tests
10
+ global.AbortController = class AbortController {
11
+ constructor() {
12
+ this.signal = {
13
+ aborted: false,
14
+ };
15
+ }
16
+
17
+ abort() {
18
+ this.signal.aborted = true;
19
+ }
20
+ };
21
+
22
+ // Reset mocks after each test
23
+ afterEach(() => {
24
+ jest.clearAllMocks();
25
+ });
@@ -0,0 +1,191 @@
1
+ import { jest } from '@jest/globals';
2
+ import Rooguys from '../../index.js';
3
+ import { mockResponses, mockErrors } from '../fixtures/responses.js';
4
+
5
+ describe('Aha Resource', () => {
6
+ let client;
7
+ let originalFetch;
8
+
9
+ beforeEach(() => {
10
+ client = new Rooguys('test-api-key');
11
+ originalFetch = global.fetch;
12
+ });
13
+
14
+ afterEach(() => {
15
+ global.fetch = originalFetch;
16
+ });
17
+
18
+ describe('declare', () => {
19
+ it('should declare aha score with valid value', async () => {
20
+ global.fetch = jest.fn().mockResolvedValue({
21
+ ok: true,
22
+ json: async () => mockResponses.ahaDeclarationResponse,
23
+ });
24
+
25
+ const result = await client.aha.declare('user123', 4);
26
+
27
+ expect(global.fetch).toHaveBeenCalled();
28
+ const callArgs = global.fetch.mock.calls[0];
29
+ expect(callArgs[0]).toContain('/aha/declare');
30
+ expect(JSON.parse(callArgs[1].body)).toEqual({
31
+ user_id: 'user123',
32
+ value: 4,
33
+ });
34
+ expect(result).toEqual(mockResponses.ahaDeclarationResponse);
35
+ });
36
+
37
+ it('should declare aha score with value 1', async () => {
38
+ global.fetch = jest.fn().mockResolvedValue({
39
+ ok: true,
40
+ json: async () => mockResponses.ahaDeclarationResponse,
41
+ });
42
+
43
+ const result = await client.aha.declare('user123', 1);
44
+
45
+ expect(global.fetch).toHaveBeenCalled();
46
+ const callArgs = global.fetch.mock.calls[0];
47
+ expect(JSON.parse(callArgs[1].body)).toEqual({
48
+ user_id: 'user123',
49
+ value: 1,
50
+ });
51
+ expect(result).toEqual(mockResponses.ahaDeclarationResponse);
52
+ });
53
+
54
+ it('should declare aha score with value 5', async () => {
55
+ global.fetch = jest.fn().mockResolvedValue({
56
+ ok: true,
57
+ json: async () => mockResponses.ahaDeclarationResponse,
58
+ });
59
+
60
+ const result = await client.aha.declare('user123', 5);
61
+
62
+ expect(global.fetch).toHaveBeenCalled();
63
+ const callArgs = global.fetch.mock.calls[0];
64
+ expect(JSON.parse(callArgs[1].body)).toEqual({
65
+ user_id: 'user123',
66
+ value: 5,
67
+ });
68
+ expect(result).toEqual(mockResponses.ahaDeclarationResponse);
69
+ });
70
+
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
+ );
75
+ });
76
+
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
+ );
81
+ });
82
+
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
+ );
87
+ });
88
+
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
+ );
93
+ });
94
+
95
+ it('should handle API error response', async () => {
96
+ global.fetch = jest.fn().mockResolvedValue({
97
+ ok: false,
98
+ statusText: 'Bad Request',
99
+ json: async () => mockErrors.ahaValueError,
100
+ });
101
+
102
+ await expect(client.aha.declare('user123', 3)).rejects.toThrow();
103
+ });
104
+ });
105
+
106
+ describe('getUserScore', () => {
107
+ it('should get user aha score successfully', async () => {
108
+ global.fetch = jest.fn().mockResolvedValue({
109
+ ok: true,
110
+ json: async () => mockResponses.ahaScoreResponse,
111
+ });
112
+
113
+ const result = await client.aha.getUserScore('user123');
114
+
115
+ expect(global.fetch).toHaveBeenCalled();
116
+ const callArgs = global.fetch.mock.calls[0];
117
+ expect(callArgs[0]).toContain('/users/user123/aha');
118
+ expect(result).toEqual(mockResponses.ahaScoreResponse);
119
+ });
120
+
121
+ it('should parse all aha score fields correctly', async () => {
122
+ global.fetch = jest.fn().mockResolvedValue({
123
+ ok: true,
124
+ json: async () => mockResponses.ahaScoreResponse,
125
+ });
126
+
127
+ const result = await client.aha.getUserScore('user123');
128
+
129
+ expect(result.success).toBe(true);
130
+ expect(result.data.user_id).toBe('user123');
131
+ expect(result.data.current_score).toBe(75);
132
+ expect(result.data.declarative_score).toBe(80);
133
+ expect(result.data.inferred_score).toBe(70);
134
+ expect(result.data.status).toBe('activated');
135
+ });
136
+
137
+ it('should preserve history structure', async () => {
138
+ global.fetch = jest.fn().mockResolvedValue({
139
+ ok: true,
140
+ json: async () => mockResponses.ahaScoreResponse,
141
+ });
142
+
143
+ const result = await client.aha.getUserScore('user123');
144
+
145
+ expect(result.data.history).toEqual({
146
+ initial: 50,
147
+ initial_date: '2024-01-01T00:00:00Z',
148
+ previous: 70,
149
+ });
150
+ });
151
+
152
+ it('should handle 404 error when user not found', async () => {
153
+ global.fetch = jest.fn().mockResolvedValue({
154
+ ok: false,
155
+ status: 404,
156
+ statusText: 'Not Found',
157
+ json: async () => mockErrors.notFoundError,
158
+ });
159
+
160
+ await expect(client.aha.getUserScore('nonexistent')).rejects.toThrow();
161
+ });
162
+
163
+ it('should handle null declarative and inferred scores', async () => {
164
+ const responseWithNulls = {
165
+ success: true,
166
+ data: {
167
+ user_id: 'user123',
168
+ current_score: 0,
169
+ declarative_score: null,
170
+ inferred_score: null,
171
+ status: 'not_started',
172
+ history: {
173
+ initial: null,
174
+ initial_date: null,
175
+ previous: null,
176
+ },
177
+ },
178
+ };
179
+ global.fetch = jest.fn().mockResolvedValue({
180
+ ok: true,
181
+ json: async () => responseWithNulls,
182
+ });
183
+
184
+ const result = await client.aha.getUserScore('user123');
185
+
186
+ expect(result.data.declarative_score).toBeNull();
187
+ expect(result.data.inferred_score).toBeNull();
188
+ expect(result.data.history.initial).toBeNull();
189
+ });
190
+ });
191
+ });