@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.
@@ -1,33 +1,33 @@
1
- import axios from 'axios';
2
- import { Rooguys } from '../../index';
3
- import { createMockAxiosInstance, mockSuccessResponse, mockErrorResponse } from '../utils/mockClient';
1
+ import { Rooguys, ValidationError } from '../../index';
2
+ import {
3
+ createMockRooguysClient,
4
+ setupMockRequest,
5
+ setupMockRequestError,
6
+ expectRequestWith,
7
+ MockAxiosInstance,
8
+ } from '../utils/mockClient';
4
9
  import { mockResponses, mockErrors } from '../fixtures/responses';
5
10
 
6
- jest.mock('axios');
7
- const mockedAxios = axios as jest.Mocked<typeof axios>;
8
-
9
11
  describe('Users Resource', () => {
10
12
  let client: Rooguys;
11
- let mockAxiosInstance: ReturnType<typeof createMockAxiosInstance>;
12
- const apiKey = 'test-api-key';
13
+ let mockAxios: MockAxiosInstance;
13
14
 
14
15
  beforeEach(() => {
15
- mockAxiosInstance = createMockAxiosInstance();
16
- mockedAxios.create.mockReturnValue(mockAxiosInstance as any);
17
- client = new Rooguys(apiKey);
18
- jest.clearAllMocks();
16
+ const mock = createMockRooguysClient();
17
+ client = mock.client;
18
+ mockAxios = mock.mockAxios;
19
19
  });
20
20
 
21
21
  describe('get', () => {
22
22
  it('should get a user profile', async () => {
23
- mockAxiosInstance.get.mockResolvedValue(
24
- mockSuccessResponse(mockResponses.userProfile)
25
- );
23
+ setupMockRequest(mockAxios, mockResponses.userProfile);
26
24
 
27
- const result = await client.users.get('user_123');
25
+ const result = await client.users.get('user123');
28
26
 
29
- expect(mockAxiosInstance.get).toHaveBeenCalledWith('/user/user_123');
30
- expect(result).toEqual(mockResponses.userProfile);
27
+ expectRequestWith(mockAxios, {
28
+ method: 'GET',
29
+ url: '/users/user123',
30
+ });
31
31
  expect(result.user_id).toBe('user123');
32
32
  expect(result.points).toBe(100);
33
33
  });
@@ -38,7 +38,7 @@ describe('Users Resource', () => {
38
38
  level: null,
39
39
  next_level: null,
40
40
  };
41
- mockAxiosInstance.get.mockResolvedValue(mockSuccessResponse(userWithoutLevel));
41
+ setupMockRequest(mockAxios, userWithoutLevel);
42
42
 
43
43
  const result = await client.users.get('user_new');
44
44
 
@@ -51,7 +51,7 @@ describe('Users Resource', () => {
51
51
  ...mockResponses.userProfile,
52
52
  badges: [],
53
53
  };
54
- mockAxiosInstance.get.mockResolvedValue(mockSuccessResponse(userWithoutBadges));
54
+ setupMockRequest(mockAxios, userWithoutBadges);
55
55
 
56
56
  const result = await client.users.get('user_123');
57
57
 
@@ -59,9 +59,7 @@ describe('Users Resource', () => {
59
59
  });
60
60
 
61
61
  it('should throw 404 error when user not found', async () => {
62
- mockAxiosInstance.get.mockRejectedValue(
63
- mockErrorResponse(404, mockErrors.notFoundError.message)
64
- );
62
+ setupMockRequestError(mockAxios, 404, "User 'user123' does not exist in this project");
65
63
 
66
64
  await expect(client.users.get('nonexistent_user')).rejects.toThrow(
67
65
  "User 'user123' does not exist in this project"
@@ -69,35 +67,154 @@ describe('Users Resource', () => {
69
67
  });
70
68
 
71
69
  it('should handle special characters in user ID', async () => {
72
- mockAxiosInstance.get.mockResolvedValue(
73
- mockSuccessResponse(mockResponses.userProfile)
74
- );
70
+ setupMockRequest(mockAxios, mockResponses.userProfile);
75
71
 
76
72
  await client.users.get('user@example.com');
77
73
 
78
- expect(mockAxiosInstance.get).toHaveBeenCalledWith('/user/user%40example.com');
74
+ expectRequestWith(mockAxios, {
75
+ method: 'GET',
76
+ url: '/users/user%40example.com',
77
+ });
78
+ });
79
+
80
+ it('should support field selection', async () => {
81
+ setupMockRequest(mockAxios, mockResponses.userProfile);
82
+
83
+ await client.users.get('user123', { fields: ['points', 'level'] });
84
+
85
+ expectRequestWith(mockAxios, {
86
+ method: 'GET',
87
+ url: '/users/user123',
88
+ });
89
+ });
90
+ });
91
+
92
+ describe('create', () => {
93
+ it('should create a new user', async () => {
94
+ setupMockRequest(mockAxios, mockResponses.userProfile);
95
+
96
+ const result = await client.users.create({
97
+ userId: 'new_user',
98
+ displayName: 'New User',
99
+ email: 'new@example.com',
100
+ });
101
+
102
+ expectRequestWith(mockAxios, {
103
+ method: 'POST',
104
+ url: '/users',
105
+ data: {
106
+ user_id: 'new_user',
107
+ display_name: 'New User',
108
+ email: 'new@example.com',
109
+ },
110
+ });
111
+ expect(result).toBeDefined();
112
+ });
113
+
114
+ it('should throw error for missing user ID', async () => {
115
+ await expect(client.users.create({} as any)).rejects.toThrow(ValidationError);
116
+ await expect(client.users.create({} as any)).rejects.toThrow('User ID is required');
117
+ });
118
+
119
+ it('should throw error for invalid email format', async () => {
120
+ await expect(
121
+ client.users.create({ userId: 'user1', email: 'invalid-email' })
122
+ ).rejects.toThrow(ValidationError);
123
+ await expect(
124
+ client.users.create({ userId: 'user1', email: 'invalid-email' })
125
+ ).rejects.toThrow('Invalid email format');
126
+ });
127
+ });
128
+
129
+ describe('update', () => {
130
+ it('should update an existing user', async () => {
131
+ setupMockRequest(mockAxios, mockResponses.userProfile);
132
+
133
+ const result = await client.users.update('user123', {
134
+ displayName: 'Updated Name',
135
+ });
136
+
137
+ expectRequestWith(mockAxios, {
138
+ method: 'PATCH',
139
+ url: '/users/user123',
140
+ data: {
141
+ display_name: 'Updated Name',
142
+ },
143
+ });
144
+ expect(result).toBeDefined();
145
+ });
146
+
147
+ it('should throw error for missing user ID', async () => {
148
+ await expect(client.users.update('', { displayName: 'Test' })).rejects.toThrow(ValidationError);
149
+ });
150
+
151
+ it('should throw error for invalid email format', async () => {
152
+ await expect(
153
+ client.users.update('user123', { email: 'invalid-email' })
154
+ ).rejects.toThrow(ValidationError);
155
+ });
156
+ });
157
+
158
+ describe('createBatch', () => {
159
+ it('should create multiple users', async () => {
160
+ const batchResponse = { created: 2, failed: 0 };
161
+ setupMockRequest(mockAxios, batchResponse);
162
+
163
+ const users = [
164
+ { userId: 'user1', displayName: 'User 1' },
165
+ { userId: 'user2', displayName: 'User 2' },
166
+ ];
167
+
168
+ const result = await client.users.createBatch(users);
169
+
170
+ expectRequestWith(mockAxios, {
171
+ method: 'POST',
172
+ url: '/users/batch',
173
+ });
174
+ expect(result).toEqual(batchResponse);
175
+ });
176
+
177
+ it('should throw error for empty users array', async () => {
178
+ await expect(client.users.createBatch([])).rejects.toThrow(ValidationError);
179
+ await expect(client.users.createBatch([])).rejects.toThrow('cannot be empty');
180
+ });
181
+
182
+ it('should throw error for more than 100 users', async () => {
183
+ const manyUsers = Array.from({ length: 101 }, (_, i) => ({
184
+ userId: `user_${i}`,
185
+ }));
186
+
187
+ await expect(client.users.createBatch(manyUsers)).rejects.toThrow(ValidationError);
188
+ await expect(client.users.createBatch(manyUsers)).rejects.toThrow('maximum of 100');
189
+ });
190
+
191
+ it('should throw error for user without userId', async () => {
192
+ const users = [{ displayName: 'User 1' }] as any;
193
+
194
+ await expect(client.users.createBatch(users)).rejects.toThrow(ValidationError);
195
+ await expect(client.users.createBatch(users)).rejects.toThrow('User ID is required');
79
196
  });
80
197
  });
81
198
 
82
199
  describe('getBulk', () => {
83
200
  it('should get multiple user profiles', async () => {
84
- mockAxiosInstance.post.mockResolvedValue(
85
- mockSuccessResponse(mockResponses.bulkUsersResponse)
86
- );
201
+ setupMockRequest(mockAxios, mockResponses.bulkUsersResponse);
87
202
 
88
203
  const result = await client.users.getBulk(['user1', 'user2']);
89
204
 
90
- expect(mockAxiosInstance.post).toHaveBeenCalledWith('/users/bulk', {
91
- user_ids: ['user1', 'user2'],
205
+ expectRequestWith(mockAxios, {
206
+ method: 'POST',
207
+ url: '/users/bulk',
208
+ data: {
209
+ user_ids: ['user1', 'user2'],
210
+ },
92
211
  });
93
212
  expect(result).toEqual(mockResponses.bulkUsersResponse);
94
213
  expect(result.users).toHaveLength(2);
95
214
  });
96
215
 
97
216
  it('should handle single user in bulk request', async () => {
98
- mockAxiosInstance.post.mockResolvedValue(
99
- mockSuccessResponse({ users: [mockResponses.userProfile] })
100
- );
217
+ setupMockRequest(mockAxios, { users: [mockResponses.userProfile] });
101
218
 
102
219
  const result = await client.users.getBulk(['user_123']);
103
220
 
@@ -105,45 +222,30 @@ describe('Users Resource', () => {
105
222
  });
106
223
 
107
224
  it('should handle empty results', async () => {
108
- mockAxiosInstance.post.mockResolvedValue(
109
- mockSuccessResponse({ users: [] })
110
- );
225
+ setupMockRequest(mockAxios, { users: [] });
111
226
 
112
227
  const result = await client.users.getBulk(['nonexistent1', 'nonexistent2']);
113
228
 
114
229
  expect(result.users).toEqual([]);
115
230
  });
116
-
117
- it('should throw error for more than 100 users', async () => {
118
- mockAxiosInstance.post.mockRejectedValue(
119
- mockErrorResponse(400, 'Maximum 100 user IDs allowed')
120
- );
121
-
122
- const manyUsers = Array.from({ length: 101 }, (_, i) => `user_${i}`);
123
-
124
- await expect(client.users.getBulk(manyUsers)).rejects.toThrow(
125
- 'Maximum 100 user IDs allowed'
126
- );
127
- });
128
231
  });
129
232
 
130
233
  describe('getBadges', () => {
131
234
  it('should get user badges', async () => {
132
- mockAxiosInstance.get.mockResolvedValue(
133
- mockSuccessResponse({ badges: mockResponses.userProfile.badges })
134
- );
235
+ setupMockRequest(mockAxios, { badges: mockResponses.userProfile.badges });
135
236
 
136
237
  const result = await client.users.getBadges('user_123');
137
238
 
138
- expect(mockAxiosInstance.get).toHaveBeenCalledWith('/user/user_123/badges');
239
+ expectRequestWith(mockAxios, {
240
+ method: 'GET',
241
+ url: '/users/user_123/badges',
242
+ });
139
243
  expect(result.badges).toHaveLength(1);
140
244
  expect(result.badges[0].name).toBe('First Steps');
141
245
  });
142
246
 
143
247
  it('should handle user with no badges', async () => {
144
- mockAxiosInstance.get.mockResolvedValue(
145
- mockSuccessResponse({ badges: [] })
146
- );
248
+ setupMockRequest(mockAxios, { badges: [] });
147
249
 
148
250
  const result = await client.users.getBadges('user_new');
149
251
 
@@ -151,9 +253,7 @@ describe('Users Resource', () => {
151
253
  });
152
254
 
153
255
  it('should throw 404 error when user not found', async () => {
154
- mockAxiosInstance.get.mockRejectedValue(
155
- mockErrorResponse(404, mockErrors.notFoundError.message)
156
- );
256
+ setupMockRequestError(mockAxios, 404, "User 'user123' does not exist in this project");
157
257
 
158
258
  await expect(client.users.getBadges('nonexistent_user')).rejects.toThrow(
159
259
  "User 'user123' does not exist in this project"
@@ -163,47 +263,41 @@ describe('Users Resource', () => {
163
263
 
164
264
  describe('getRank', () => {
165
265
  it('should get user rank with default timeframe', async () => {
166
- mockAxiosInstance.get.mockResolvedValue(
167
- mockSuccessResponse(mockResponses.userRankResponse)
168
- );
266
+ setupMockRequest(mockAxios, mockResponses.userRankResponse);
169
267
 
170
268
  const result = await client.users.getRank('user_123');
171
269
 
172
- expect(mockAxiosInstance.get).toHaveBeenCalledWith('/user/user_123/rank', {
173
- params: { timeframe: 'all-time' },
270
+ expectRequestWith(mockAxios, {
271
+ method: 'GET',
272
+ url: '/users/user_123/rank',
174
273
  });
175
- expect(result).toEqual(mockResponses.userRankResponse);
176
274
  expect(result.rank).toBe(42);
177
275
  });
178
276
 
179
277
  it('should get user rank with weekly timeframe', async () => {
180
- mockAxiosInstance.get.mockResolvedValue(
181
- mockSuccessResponse(mockResponses.userRankResponse)
182
- );
278
+ setupMockRequest(mockAxios, mockResponses.userRankResponse);
183
279
 
184
280
  await client.users.getRank('user_123', 'weekly');
185
281
 
186
- expect(mockAxiosInstance.get).toHaveBeenCalledWith('/user/user_123/rank', {
187
- params: { timeframe: 'weekly' },
282
+ expectRequestWith(mockAxios, {
283
+ method: 'GET',
284
+ url: '/users/user_123/rank',
188
285
  });
189
286
  });
190
287
 
191
288
  it('should get user rank with monthly timeframe', async () => {
192
- mockAxiosInstance.get.mockResolvedValue(
193
- mockSuccessResponse(mockResponses.userRankResponse)
194
- );
289
+ setupMockRequest(mockAxios, mockResponses.userRankResponse);
195
290
 
196
291
  await client.users.getRank('user_123', 'monthly');
197
292
 
198
- expect(mockAxiosInstance.get).toHaveBeenCalledWith('/user/user_123/rank', {
199
- params: { timeframe: 'monthly' },
293
+ expectRequestWith(mockAxios, {
294
+ method: 'GET',
295
+ url: '/users/user_123/rank',
200
296
  });
201
297
  });
202
298
 
203
299
  it('should throw 404 error when user not found', async () => {
204
- mockAxiosInstance.get.mockRejectedValue(
205
- mockErrorResponse(404, mockErrors.notFoundError.message)
206
- );
300
+ setupMockRequestError(mockAxios, 404, "User 'user123' does not exist in this project");
207
301
 
208
302
  await expect(client.users.getRank('nonexistent_user')).rejects.toThrow(
209
303
  "User 'user123' does not exist in this project"
@@ -213,9 +307,7 @@ describe('Users Resource', () => {
213
307
 
214
308
  describe('submitAnswers', () => {
215
309
  it('should submit questionnaire answers', async () => {
216
- mockAxiosInstance.post.mockResolvedValue(
217
- mockSuccessResponse(mockResponses.answerSubmissionResponse)
218
- );
310
+ setupMockRequest(mockAxios, mockResponses.answerSubmissionResponse);
219
311
 
220
312
  const answers = [
221
313
  { question_id: 'q1', answer_option_id: 'a1' },
@@ -228,30 +320,30 @@ describe('Users Resource', () => {
228
320
  answers
229
321
  );
230
322
 
231
- expect(mockAxiosInstance.post).toHaveBeenCalledWith('/user/user_123/answers', {
232
- questionnaire_id: 'questionnaire_id',
233
- answers,
323
+ expectRequestWith(mockAxios, {
324
+ method: 'POST',
325
+ url: '/users/user_123/answers',
326
+ data: {
327
+ questionnaire_id: 'questionnaire_id',
328
+ answers,
329
+ },
234
330
  });
235
331
  expect(result).toEqual(mockResponses.answerSubmissionResponse);
236
332
  expect(result.status).toBe('accepted');
237
333
  });
238
334
 
239
335
  it('should handle single answer submission', async () => {
240
- mockAxiosInstance.post.mockResolvedValue(
241
- mockSuccessResponse(mockResponses.answerSubmissionResponse)
242
- );
336
+ setupMockRequest(mockAxios, mockResponses.answerSubmissionResponse);
243
337
 
244
338
  const answers = [{ question_id: 'q1', answer_option_id: 'a1' }];
245
339
 
246
340
  await client.users.submitAnswers('user_123', 'questionnaire_id', answers);
247
341
 
248
- expect(mockAxiosInstance.post).toHaveBeenCalled();
342
+ expect(mockAxios.request).toHaveBeenCalled();
249
343
  });
250
344
 
251
345
  it('should throw error for invalid questionnaire ID', async () => {
252
- mockAxiosInstance.post.mockRejectedValue(
253
- mockErrorResponse(400, 'Invalid questionnaire ID')
254
- );
346
+ setupMockRequestError(mockAxios, 400, 'Invalid questionnaire ID');
255
347
 
256
348
  const answers = [{ question_id: 'q1', answer_option_id: 'a1' }];
257
349
 
@@ -259,15 +351,38 @@ describe('Users Resource', () => {
259
351
  client.users.submitAnswers('user_123', 'invalid_id', answers)
260
352
  ).rejects.toThrow('Invalid questionnaire ID');
261
353
  });
354
+ });
262
355
 
263
- it('should throw error for empty answers array', async () => {
264
- mockAxiosInstance.post.mockRejectedValue(
265
- mockErrorResponse(400, 'Answers must be a non-empty array')
266
- );
356
+ describe('search', () => {
357
+ it('should search users', async () => {
358
+ const searchResponse = {
359
+ users: [mockResponses.userProfile],
360
+ pagination: { page: 1, limit: 50, total: 1, totalPages: 1 },
361
+ };
362
+ setupMockRequest(mockAxios, searchResponse);
267
363
 
268
- await expect(
269
- client.users.submitAnswers('user_123', 'questionnaire_id', [])
270
- ).rejects.toThrow('non-empty array');
364
+ const result = await client.users.search('john');
365
+
366
+ expectRequestWith(mockAxios, {
367
+ method: 'GET',
368
+ url: '/users/search',
369
+ });
370
+ expect(result.users).toHaveLength(1);
371
+ });
372
+
373
+ it('should search users with pagination', async () => {
374
+ const searchResponse = {
375
+ users: [],
376
+ pagination: { page: 2, limit: 25, total: 0, totalPages: 0 },
377
+ };
378
+ setupMockRequest(mockAxios, searchResponse);
379
+
380
+ await client.users.search('john', { page: 2, limit: 25 });
381
+
382
+ expectRequestWith(mockAxios, {
383
+ method: 'GET',
384
+ url: '/users/search',
385
+ });
271
386
  });
272
387
  });
273
388
  });
@@ -1,8 +1,14 @@
1
- import { AxiosInstance } from 'axios';
1
+ import { AxiosResponse } from 'axios';
2
+ import { Rooguys } from '../../index';
3
+ import { HttpClient, ApiResponse, RateLimitInfo } from '../../http-client';
2
4
 
3
5
  export interface MockAxiosInstance {
4
6
  get: jest.Mock;
5
7
  post: jest.Mock;
8
+ put: jest.Mock;
9
+ patch: jest.Mock;
10
+ delete: jest.Mock;
11
+ request: jest.Mock;
6
12
  defaults: {
7
13
  headers: {
8
14
  common: Record<string, string>;
@@ -18,6 +24,10 @@ export function createMockAxiosInstance(): MockAxiosInstance {
18
24
  return {
19
25
  get: jest.fn(),
20
26
  post: jest.fn(),
27
+ put: jest.fn(),
28
+ patch: jest.fn(),
29
+ delete: jest.fn(),
30
+ request: jest.fn(),
21
31
  defaults: {
22
32
  headers: {
23
33
  common: {},
@@ -30,19 +40,196 @@ export function createMockAxiosInstance(): MockAxiosInstance {
30
40
  };
31
41
  }
32
42
 
33
- export function mockSuccessResponse<T>(data: T) {
34
- return { data };
43
+ /**
44
+ * Create a mock Rooguys client with mocked HTTP client
45
+ * Returns both the client and the mock axios instance for setting up expectations
46
+ */
47
+ export function createMockRooguysClient(apiKey = 'test-api-key'): {
48
+ client: Rooguys;
49
+ mockAxios: MockAxiosInstance;
50
+ } {
51
+ const client = new Rooguys(apiKey);
52
+ const mockAxios = createMockAxiosInstance();
53
+
54
+ // Access the internal HttpClient and replace its axios instance
55
+ const httpClient = (client as any)._httpClient as HttpClient;
56
+ (httpClient as any).client = mockAxios;
57
+
58
+ return { client, mockAxios };
35
59
  }
36
60
 
37
- export function mockErrorResponse(status: number, message: string, details?: any) {
61
+ /**
62
+ * Create a mock AxiosResponse with rate limit headers
63
+ */
64
+ export function mockAxiosResponse<T>(
65
+ data: T,
66
+ status = 200,
67
+ headers: Record<string, string> = {}
68
+ ): AxiosResponse<T> {
69
+ return {
70
+ data,
71
+ status,
72
+ statusText: status === 200 ? 'OK' : 'Error',
73
+ headers: {
74
+ 'x-ratelimit-limit': '1000',
75
+ 'x-ratelimit-remaining': '999',
76
+ 'x-ratelimit-reset': String(Math.floor(Date.now() / 1000) + 3600),
77
+ ...headers,
78
+ },
79
+ config: {} as any,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Create a mock AxiosResponse for standardized API format
85
+ */
86
+ export function mockStandardizedResponse<T>(
87
+ data: T,
88
+ requestId = 'req-123',
89
+ headers: Record<string, string> = {}
90
+ ): AxiosResponse<{ success: true; data: T; request_id: string }> {
91
+ return mockAxiosResponse(
92
+ {
93
+ success: true as const,
94
+ data,
95
+ request_id: requestId,
96
+ },
97
+ 200,
98
+ {
99
+ 'x-request-id': requestId,
100
+ ...headers,
101
+ }
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Create a mock success response (legacy format - data directly in response)
107
+ */
108
+ export function mockSuccessResponse<T>(
109
+ data: T,
110
+ headers: Record<string, string> = {}
111
+ ): AxiosResponse<T> {
112
+ return mockAxiosResponse(data, 200, headers);
113
+ }
114
+
115
+ /**
116
+ * Create a mock error that mimics an Axios error
117
+ */
118
+ export function mockErrorResponse(
119
+ status: number,
120
+ message: string,
121
+ code?: string,
122
+ details?: Array<{ field: string; message: string }>
123
+ ): any {
38
124
  const error: any = new Error(message);
39
125
  error.response = {
40
126
  status,
41
127
  data: {
42
- error: message,
43
- details,
128
+ success: false,
129
+ error: {
130
+ message,
131
+ code: code || 'ERROR',
132
+ details,
133
+ },
134
+ request_id: 'req-error-123',
135
+ },
136
+ headers: {
137
+ 'x-ratelimit-limit': '1000',
138
+ 'x-ratelimit-remaining': '999',
139
+ 'x-ratelimit-reset': String(Math.floor(Date.now() / 1000) + 3600),
140
+ 'x-request-id': 'req-error-123',
141
+ },
142
+ };
143
+ error.isAxiosError = true;
144
+ return error;
145
+ }
146
+
147
+ /**
148
+ * Create a mock rate limit error
149
+ */
150
+ export function mockRateLimitError(retryAfter = 60): any {
151
+ const error: any = new Error('Rate limit exceeded');
152
+ error.response = {
153
+ status: 429,
154
+ data: {
155
+ success: false,
156
+ error: {
157
+ message: 'Rate limit exceeded',
158
+ code: 'RATE_LIMIT_EXCEEDED',
159
+ },
160
+ request_id: 'req-ratelimit-123',
161
+ },
162
+ headers: {
163
+ 'x-ratelimit-limit': '1000',
164
+ 'x-ratelimit-remaining': '0',
165
+ 'x-ratelimit-reset': String(Math.floor(Date.now() / 1000) + retryAfter),
166
+ 'retry-after': String(retryAfter),
167
+ 'x-request-id': 'req-ratelimit-123',
44
168
  },
45
169
  };
46
170
  error.isAxiosError = true;
47
171
  return error;
48
172
  }
173
+
174
+ /**
175
+ * Helper to set up mock for a successful request
176
+ * The mock axios instance uses request() method internally
177
+ */
178
+ export function setupMockRequest<T>(
179
+ mockAxios: MockAxiosInstance,
180
+ responseData: T,
181
+ headers: Record<string, string> = {}
182
+ ): void {
183
+ mockAxios.request.mockResolvedValue(mockAxiosResponse(responseData, 200, headers));
184
+ }
185
+
186
+ /**
187
+ * Helper to set up mock for a failed request
188
+ */
189
+ export function setupMockRequestError(
190
+ mockAxios: MockAxiosInstance,
191
+ status: number,
192
+ message: string,
193
+ code?: string,
194
+ details?: Array<{ field: string; message: string }>
195
+ ): void {
196
+ mockAxios.request.mockRejectedValue(mockErrorResponse(status, message, code, details));
197
+ }
198
+
199
+ /**
200
+ * Get the last request config from mock axios
201
+ */
202
+ export function getLastRequestConfig(mockAxios: MockAxiosInstance): any {
203
+ const calls = mockAxios.request.mock.calls;
204
+ if (calls.length === 0) return null;
205
+ return calls[calls.length - 1][0];
206
+ }
207
+
208
+ /**
209
+ * Assert that a request was made with specific config
210
+ */
211
+ export function expectRequestWith(
212
+ mockAxios: MockAxiosInstance,
213
+ expected: {
214
+ method?: string;
215
+ url?: string;
216
+ data?: any;
217
+ params?: any;
218
+ }
219
+ ): void {
220
+ const lastConfig = getLastRequestConfig(mockAxios);
221
+ expect(lastConfig).toBeTruthy();
222
+
223
+ if (expected.method) {
224
+ expect(lastConfig.method).toBe(expected.method);
225
+ }
226
+ if (expected.url) {
227
+ expect(lastConfig.url).toBe(expected.url);
228
+ }
229
+ if (expected.data) {
230
+ expect(lastConfig.data).toEqual(expected.data);
231
+ }
232
+ if (expected.params) {
233
+ expect(lastConfig.params).toEqual(expected.params);
234
+ }
235
+ }