@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.
- package/README.md +277 -0
- package/jest.config.js +22 -0
- package/package.json +38 -0
- package/src/__tests__/fixtures/responses.js +330 -0
- package/src/__tests__/property/request-construction.property.test.js +211 -0
- package/src/__tests__/property/response-parsing.property.test.js +217 -0
- package/src/__tests__/setup.js +25 -0
- package/src/__tests__/unit/aha.test.js +191 -0
- package/src/__tests__/unit/badges.test.js +68 -0
- package/src/__tests__/unit/config.test.js +97 -0
- package/src/__tests__/unit/errors.test.js +141 -0
- package/src/__tests__/unit/events.test.js +223 -0
- package/src/__tests__/unit/leaderboards.test.js +113 -0
- package/src/__tests__/unit/levels.test.js +70 -0
- package/src/__tests__/unit/questionnaires.test.js +68 -0
- package/src/__tests__/unit/users.test.js +102 -0
- package/src/__tests__/utils/generators.js +80 -0
- package/src/__tests__/utils/mockClient.js +42 -0
- package/src/index.js +138 -0
|
@@ -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
|
+
});
|