@rooguys/sdk 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 +478 -113
- package/dist/__tests__/utils/mockClient.d.ts +65 -3
- package/dist/__tests__/utils/mockClient.js +144 -5
- package/dist/errors.d.ts +123 -0
- package/dist/errors.js +163 -0
- package/dist/http-client.d.ts +167 -0
- package/dist/http-client.js +250 -0
- package/dist/index.d.ts +160 -10
- package/dist/index.js +585 -146
- package/dist/types.d.ts +372 -50
- package/dist/types.js +21 -0
- package/package.json +1 -1
- package/src/__tests__/property/request-construction.property.test.ts +142 -91
- package/src/__tests__/property/response-parsing.property.test.ts +118 -67
- package/src/__tests__/property/sdk-modules.property.test.ts +450 -0
- package/src/__tests__/unit/aha.test.ts +61 -50
- package/src/__tests__/unit/badges.test.ts +27 -33
- package/src/__tests__/unit/config.test.ts +94 -126
- package/src/__tests__/unit/errors.test.ts +106 -150
- package/src/__tests__/unit/events.test.ts +119 -144
- package/src/__tests__/unit/leaderboards.test.ts +173 -40
- package/src/__tests__/unit/levels.test.ts +25 -33
- package/src/__tests__/unit/questionnaires.test.ts +33 -42
- package/src/__tests__/unit/users.test.ts +214 -99
- package/src/__tests__/utils/mockClient.ts +193 -6
- package/src/errors.ts +255 -0
- package/src/http-client.ts +433 -0
- package/src/index.ts +742 -150
- package/src/types.ts +429 -51
|
@@ -1,220 +1,176 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { Rooguys, RooguysError, ValidationError, AuthenticationError, NotFoundError, RateLimitError, ServerError } from '../../index';
|
|
2
|
+
import {
|
|
3
|
+
createMockRooguysClient,
|
|
4
|
+
setupMockRequest,
|
|
5
|
+
setupMockRequestError,
|
|
6
|
+
mockErrorResponse,
|
|
7
|
+
MockAxiosInstance,
|
|
8
|
+
} from '../utils/mockClient';
|
|
4
9
|
import { mockErrors } from '../fixtures/responses';
|
|
5
10
|
|
|
6
|
-
jest.mock('axios');
|
|
7
|
-
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
|
8
|
-
|
|
9
11
|
describe('Error Handling', () => {
|
|
10
12
|
let client: Rooguys;
|
|
11
|
-
let
|
|
12
|
-
const apiKey = 'test-api-key';
|
|
13
|
+
let mockAxios: MockAxiosInstance;
|
|
13
14
|
|
|
14
15
|
beforeEach(() => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
jest.clearAllMocks();
|
|
16
|
+
const mock = createMockRooguysClient();
|
|
17
|
+
client = mock.client;
|
|
18
|
+
mockAxios = mock.mockAxios;
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
describe('4xx client errors', () => {
|
|
22
|
-
it('should throw
|
|
23
|
-
|
|
24
|
-
mockErrorResponse(400, 'Bad Request')
|
|
25
|
-
);
|
|
22
|
+
it('should throw ValidationError for 400 Bad Request', async () => {
|
|
23
|
+
setupMockRequestError(mockAxios, 400, 'Bad Request', 'VALIDATION_ERROR');
|
|
26
24
|
|
|
27
|
-
await expect(client.events.track('test', 'user1')).rejects.toThrow(
|
|
28
|
-
'Bad Request'
|
|
29
|
-
);
|
|
25
|
+
await expect(client.events.track('test', 'user1')).rejects.toThrow(ValidationError);
|
|
30
26
|
});
|
|
31
27
|
|
|
32
|
-
it('should throw
|
|
33
|
-
|
|
34
|
-
mockErrorResponse(401, mockErrors.unauthorizedError.message)
|
|
35
|
-
);
|
|
28
|
+
it('should throw AuthenticationError for 401 Unauthorized', async () => {
|
|
29
|
+
setupMockRequestError(mockAxios, 401, 'Invalid or missing API key', 'UNAUTHORIZED');
|
|
36
30
|
|
|
37
|
-
await expect(client.users.get('user1')).rejects.toThrow(
|
|
38
|
-
'Invalid or missing API key'
|
|
39
|
-
);
|
|
31
|
+
await expect(client.users.get('user1')).rejects.toThrow(AuthenticationError);
|
|
40
32
|
});
|
|
41
33
|
|
|
42
|
-
it('should throw
|
|
43
|
-
|
|
44
|
-
mockErrorResponse(404, mockErrors.notFoundError.message)
|
|
45
|
-
);
|
|
34
|
+
it('should throw NotFoundError for 404 Not Found', async () => {
|
|
35
|
+
setupMockRequestError(mockAxios, 404, "User 'user123' does not exist in this project", 'NOT_FOUND');
|
|
46
36
|
|
|
47
|
-
await expect(client.users.get('nonexistent')).rejects.toThrow(
|
|
48
|
-
"User 'user123' does not exist in this project"
|
|
49
|
-
);
|
|
37
|
+
await expect(client.users.get('nonexistent')).rejects.toThrow(NotFoundError);
|
|
50
38
|
});
|
|
51
39
|
|
|
52
40
|
it('should include validation details in error', async () => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
);
|
|
41
|
+
setupMockRequestError(mockAxios, 400, 'Validation failed', 'VALIDATION_ERROR', [
|
|
42
|
+
{ field: 'user_id', message: 'User ID is required' },
|
|
43
|
+
]);
|
|
56
44
|
|
|
57
|
-
|
|
58
|
-
'
|
|
59
|
-
|
|
45
|
+
try {
|
|
46
|
+
await client.events.track('', 'user1');
|
|
47
|
+
fail('Should have thrown an error');
|
|
48
|
+
} catch (error) {
|
|
49
|
+
expect(error).toBeInstanceOf(ValidationError);
|
|
50
|
+
expect((error as ValidationError).fieldErrors).toBeDefined();
|
|
51
|
+
}
|
|
60
52
|
});
|
|
61
53
|
|
|
62
|
-
it('should throw
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
);
|
|
54
|
+
it('should throw RateLimitError for 429 Too Many Requests', async () => {
|
|
55
|
+
const error = mockErrorResponse(429, 'Rate limit exceeded', 'RATE_LIMIT_EXCEEDED');
|
|
56
|
+
error.response.headers['retry-after'] = '60';
|
|
57
|
+
mockAxios.request.mockRejectedValue(error);
|
|
66
58
|
|
|
67
|
-
await expect(client.users.get('user1')).rejects.toThrow(
|
|
68
|
-
'Rate limit exceeded'
|
|
69
|
-
);
|
|
59
|
+
await expect(client.users.get('user1')).rejects.toThrow(RateLimitError);
|
|
70
60
|
});
|
|
71
61
|
});
|
|
72
62
|
|
|
73
63
|
describe('5xx server errors', () => {
|
|
74
|
-
it('should throw
|
|
75
|
-
|
|
76
|
-
mockErrorResponse(500, 'Internal server error')
|
|
77
|
-
);
|
|
64
|
+
it('should throw ServerError for 500 Internal Server Error', async () => {
|
|
65
|
+
setupMockRequestError(mockAxios, 500, 'Internal server error', 'SERVER_ERROR');
|
|
78
66
|
|
|
79
|
-
await expect(client.events.track('test', 'user1')).rejects.toThrow(
|
|
80
|
-
'Internal server error'
|
|
81
|
-
);
|
|
67
|
+
await expect(client.events.track('test', 'user1')).rejects.toThrow(ServerError);
|
|
82
68
|
});
|
|
83
69
|
|
|
84
|
-
it('should throw
|
|
85
|
-
|
|
86
|
-
mockErrorResponse(503, mockErrors.queueFullError.message)
|
|
87
|
-
);
|
|
70
|
+
it('should throw ServerError for 503 Service Unavailable', async () => {
|
|
71
|
+
setupMockRequestError(mockAxios, 503, 'Event queue is full. Please retry later.', 'SERVICE_UNAVAILABLE');
|
|
88
72
|
|
|
89
|
-
await expect(client.events.track('test', 'user1')).rejects.toThrow(
|
|
90
|
-
'Event queue is full'
|
|
91
|
-
);
|
|
73
|
+
await expect(client.events.track('test', 'user1')).rejects.toThrow(ServerError);
|
|
92
74
|
});
|
|
93
75
|
|
|
94
|
-
it('should throw
|
|
95
|
-
|
|
96
|
-
mockErrorResponse(502, 'Bad Gateway')
|
|
97
|
-
);
|
|
76
|
+
it('should throw ServerError for 502 Bad Gateway', async () => {
|
|
77
|
+
setupMockRequestError(mockAxios, 502, 'Bad Gateway', 'BAD_GATEWAY');
|
|
98
78
|
|
|
99
|
-
await expect(client.users.get('user1')).rejects.toThrow(
|
|
100
|
-
'Bad Gateway'
|
|
101
|
-
);
|
|
79
|
+
await expect(client.users.get('user1')).rejects.toThrow(ServerError);
|
|
102
80
|
});
|
|
103
81
|
});
|
|
104
82
|
|
|
105
83
|
describe('network errors', () => {
|
|
106
|
-
it('should throw
|
|
84
|
+
it('should throw RooguysError for network timeout', async () => {
|
|
107
85
|
const timeoutError = new Error('timeout of 10000ms exceeded');
|
|
108
|
-
(timeoutError
|
|
109
|
-
mockAxiosInstance.post.mockRejectedValue(timeoutError);
|
|
86
|
+
mockAxios.request.mockRejectedValue(timeoutError);
|
|
110
87
|
|
|
111
|
-
await expect(client.events.track('test', 'user1')).rejects.toThrow(
|
|
112
|
-
'timeout'
|
|
113
|
-
);
|
|
88
|
+
await expect(client.events.track('test', 'user1')).rejects.toThrow(RooguysError);
|
|
114
89
|
});
|
|
115
90
|
|
|
116
|
-
it('should throw
|
|
91
|
+
it('should throw RooguysError for connection refused', async () => {
|
|
117
92
|
const connectionError = new Error('connect ECONNREFUSED');
|
|
118
|
-
(connectionError
|
|
119
|
-
mockAxiosInstance.get.mockRejectedValue(connectionError);
|
|
93
|
+
mockAxios.request.mockRejectedValue(connectionError);
|
|
120
94
|
|
|
121
|
-
await expect(client.users.get('user1')).rejects.toThrow(
|
|
122
|
-
'ECONNREFUSED'
|
|
123
|
-
);
|
|
95
|
+
await expect(client.users.get('user1')).rejects.toThrow(RooguysError);
|
|
124
96
|
});
|
|
125
97
|
|
|
126
|
-
it('should throw
|
|
98
|
+
it('should throw RooguysError for DNS lookup failure', async () => {
|
|
127
99
|
const dnsError = new Error('getaddrinfo ENOTFOUND');
|
|
128
|
-
(dnsError
|
|
129
|
-
mockAxiosInstance.get.mockRejectedValue(dnsError);
|
|
100
|
+
mockAxios.request.mockRejectedValue(dnsError);
|
|
130
101
|
|
|
131
|
-
await expect(client.users.get('user1')).rejects.toThrow(
|
|
132
|
-
'ENOTFOUND'
|
|
133
|
-
);
|
|
102
|
+
await expect(client.users.get('user1')).rejects.toThrow(RooguysError);
|
|
134
103
|
});
|
|
135
104
|
});
|
|
136
105
|
|
|
137
|
-
describe('
|
|
138
|
-
it('should
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
error
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
'Request failed'
|
|
149
|
-
);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('should handle response with null data', async () => {
|
|
153
|
-
const error: any = new Error('Request failed');
|
|
154
|
-
error.response = {
|
|
155
|
-
status: 500,
|
|
156
|
-
data: null,
|
|
157
|
-
};
|
|
158
|
-
error.isAxiosError = true;
|
|
159
|
-
mockAxiosInstance.get.mockRejectedValue(error);
|
|
160
|
-
|
|
161
|
-
await expect(client.users.get('user1')).rejects.toThrow(
|
|
162
|
-
'Request failed'
|
|
163
|
-
);
|
|
106
|
+
describe('error properties', () => {
|
|
107
|
+
it('should include status code in error', async () => {
|
|
108
|
+
setupMockRequestError(mockAxios, 404, 'Not found', 'NOT_FOUND');
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await client.users.get('user1');
|
|
112
|
+
fail('Should have thrown an error');
|
|
113
|
+
} catch (error) {
|
|
114
|
+
expect(error).toBeInstanceOf(NotFoundError);
|
|
115
|
+
expect((error as NotFoundError).statusCode).toBe(404);
|
|
116
|
+
}
|
|
164
117
|
});
|
|
165
|
-
});
|
|
166
118
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
mockAxiosInstance.post.mockRejectedValue(
|
|
170
|
-
mockErrorResponse(400, 'Validation failed', [
|
|
171
|
-
{ field: 'user_id', message: 'User ID is required' },
|
|
172
|
-
{ field: 'event_name', message: 'Event name is required' },
|
|
173
|
-
])
|
|
174
|
-
);
|
|
119
|
+
it('should include error code in error', async () => {
|
|
120
|
+
setupMockRequestError(mockAxios, 400, 'Validation failed', 'VALIDATION_ERROR');
|
|
175
121
|
|
|
176
122
|
try {
|
|
177
|
-
await client.events.track('', '');
|
|
123
|
+
await client.events.track('test', 'user1');
|
|
178
124
|
fail('Should have thrown an error');
|
|
179
|
-
} catch (error
|
|
180
|
-
expect(error
|
|
125
|
+
} catch (error) {
|
|
126
|
+
expect(error).toBeInstanceOf(ValidationError);
|
|
127
|
+
expect((error as ValidationError).code).toBe('VALIDATION_ERROR');
|
|
181
128
|
}
|
|
182
129
|
});
|
|
183
130
|
|
|
184
|
-
it('should
|
|
185
|
-
|
|
186
|
-
mockErrorResponse(400, 'Complex validation error', {
|
|
187
|
-
errors: {
|
|
188
|
-
properties: {
|
|
189
|
-
amount: 'Must be a positive number',
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
})
|
|
193
|
-
);
|
|
131
|
+
it('should include request ID in error when available', async () => {
|
|
132
|
+
setupMockRequestError(mockAxios, 500, 'Server error', 'SERVER_ERROR');
|
|
194
133
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
134
|
+
try {
|
|
135
|
+
await client.users.get('user1');
|
|
136
|
+
fail('Should have thrown an error');
|
|
137
|
+
} catch (error) {
|
|
138
|
+
expect(error).toBeInstanceOf(ServerError);
|
|
139
|
+
expect((error as ServerError).requestId).toBeDefined();
|
|
140
|
+
}
|
|
198
141
|
});
|
|
199
142
|
});
|
|
200
143
|
|
|
201
|
-
describe('
|
|
202
|
-
it('should
|
|
203
|
-
const
|
|
204
|
-
|
|
144
|
+
describe('error class hierarchy', () => {
|
|
145
|
+
it('ValidationError should be instance of RooguysError', () => {
|
|
146
|
+
const error = new ValidationError('Test error');
|
|
147
|
+
expect(error).toBeInstanceOf(RooguysError);
|
|
148
|
+
expect(error).toBeInstanceOf(Error);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('AuthenticationError should be instance of RooguysError', () => {
|
|
152
|
+
const error = new AuthenticationError('Test error');
|
|
153
|
+
expect(error).toBeInstanceOf(RooguysError);
|
|
154
|
+
expect(error).toBeInstanceOf(Error);
|
|
155
|
+
});
|
|
205
156
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
);
|
|
157
|
+
it('NotFoundError should be instance of RooguysError', () => {
|
|
158
|
+
const error = new NotFoundError('Test error');
|
|
159
|
+
expect(error).toBeInstanceOf(RooguysError);
|
|
160
|
+
expect(error).toBeInstanceOf(Error);
|
|
209
161
|
});
|
|
210
162
|
|
|
211
|
-
it('should
|
|
212
|
-
const
|
|
213
|
-
|
|
163
|
+
it('RateLimitError should be instance of RooguysError', () => {
|
|
164
|
+
const error = new RateLimitError('Test error', { retryAfter: 60 });
|
|
165
|
+
expect(error).toBeInstanceOf(RooguysError);
|
|
166
|
+
expect(error).toBeInstanceOf(Error);
|
|
167
|
+
expect(error.retryAfter).toBe(60);
|
|
168
|
+
});
|
|
214
169
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
);
|
|
170
|
+
it('ServerError should be instance of RooguysError', () => {
|
|
171
|
+
const error = new ServerError('Test error');
|
|
172
|
+
expect(error).toBeInstanceOf(RooguysError);
|
|
173
|
+
expect(error).toBeInstanceOf(Error);
|
|
218
174
|
});
|
|
219
175
|
});
|
|
220
176
|
});
|