@rooguys/sdk 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,200 @@
1
+ /**
2
+ * Property-Based Test: Response Parsing Preservation
3
+ * Feature: sdk-testing-enhancement, Property 2: Response Parsing Preservation
4
+ *
5
+ * Tests that any successful response is parsed correctly and data structure
6
+ * is preserved including nested objects, arrays, and null values.
7
+ */
8
+
9
+ import fc from 'fast-check';
10
+ import { Rooguys } from '../../index';
11
+ import { createMockAxiosInstance } from '../utils/mockClient';
12
+ import axios from 'axios';
13
+
14
+ // Mock axios
15
+ jest.mock('axios');
16
+ const mockedAxios = axios as jest.Mocked<typeof axios>;
17
+
18
+ describe('Property: Response Parsing Preservation', () => {
19
+ let mockClient: any;
20
+
21
+ beforeEach(() => {
22
+ mockClient = createMockAxiosInstance();
23
+ mockedAxios.create.mockReturnValue(mockClient);
24
+ });
25
+
26
+ afterEach(() => {
27
+ jest.clearAllMocks();
28
+ });
29
+
30
+ it('should preserve nested object structures in responses', async () => {
31
+ await fc.assert(
32
+ fc.asyncProperty(
33
+ fc.string({ minLength: 10, maxLength: 100 }),
34
+ fc.string({ minLength: 1, maxLength: 255 }),
35
+ fc.record({
36
+ user_id: fc.string(),
37
+ points: fc.integer(),
38
+ level: fc.record({
39
+ id: fc.string(),
40
+ name: fc.string(),
41
+ level_number: fc.integer(),
42
+ }),
43
+ next_level: fc.option(fc.record({
44
+ id: fc.string(),
45
+ name: fc.string(),
46
+ points_required: fc.integer(),
47
+ }), { nil: null }),
48
+ metrics: fc.dictionary(fc.string(), fc.integer()),
49
+ }),
50
+ async (apiKey, userId, responseData) => {
51
+ // Arrange
52
+ mockClient.get.mockResolvedValue({ data: responseData });
53
+ const sdk = new Rooguys(apiKey);
54
+
55
+ // Act
56
+ const result = await sdk.users.get(userId);
57
+
58
+ // Assert
59
+ expect(result).toEqual(responseData);
60
+ expect(result.level).toEqual(responseData.level);
61
+ expect(result.next_level).toEqual(responseData.next_level);
62
+ expect(result.metrics).toEqual(responseData.metrics);
63
+ }
64
+ ),
65
+ { numRuns: 100 }
66
+ );
67
+ });
68
+
69
+ it('should preserve arrays in responses', async () => {
70
+ await fc.assert(
71
+ fc.asyncProperty(
72
+ fc.string({ minLength: 10, maxLength: 100 }),
73
+ fc.array(fc.string({ minLength: 1, maxLength: 255 }), { minLength: 1, maxLength: 10 }),
74
+ fc.array(fc.record({
75
+ user_id: fc.string(),
76
+ points: fc.integer(),
77
+ }), { minLength: 0, maxLength: 20 }),
78
+ async (apiKey, userIds, usersData) => {
79
+ // Arrange
80
+ const responseData = { users: usersData };
81
+ mockClient.post.mockResolvedValue({ data: responseData });
82
+ const sdk = new Rooguys(apiKey);
83
+
84
+ // Act
85
+ const result = await sdk.users.getBulk(userIds);
86
+
87
+ // Assert
88
+ expect(result).toEqual(responseData);
89
+ expect(Array.isArray(result.users)).toBe(true);
90
+ expect(result.users).toHaveLength(usersData.length);
91
+ expect(result.users).toEqual(usersData);
92
+ }
93
+ ),
94
+ { numRuns: 100 }
95
+ );
96
+ });
97
+
98
+ it('should preserve null values in responses', async () => {
99
+ await fc.assert(
100
+ fc.asyncProperty(
101
+ fc.string({ minLength: 10, maxLength: 100 }),
102
+ fc.string({ minLength: 1, maxLength: 255 }),
103
+ fc.record({
104
+ user_id: fc.string(),
105
+ declarative_score: fc.option(fc.integer({ min: 1, max: 5 }), { nil: null }),
106
+ inferred_score: fc.option(fc.integer({ min: 0, max: 100 }), { nil: null }),
107
+ history: fc.record({
108
+ initial: fc.option(fc.integer(), { nil: null }),
109
+ initial_date: fc.option(fc.string(), { nil: null }),
110
+ previous: fc.option(fc.integer(), { nil: null }),
111
+ }),
112
+ }),
113
+ async (apiKey, userId, responseData) => {
114
+ // Arrange
115
+ mockClient.get.mockResolvedValue({ data: responseData });
116
+ const sdk = new Rooguys(apiKey);
117
+
118
+ // Act
119
+ const result = await sdk.aha.getUserScore(userId);
120
+
121
+ // Assert
122
+ expect(result).toEqual(responseData);
123
+ expect((result as any).declarative_score).toBe(responseData.declarative_score);
124
+ expect((result as any).inferred_score).toBe(responseData.inferred_score);
125
+ expect((result as any).history.initial).toBe(responseData.history.initial);
126
+ expect((result as any).history.initial_date).toBe(responseData.history.initial_date);
127
+ expect((result as any).history.previous).toBe(responseData.history.previous);
128
+ }
129
+ ),
130
+ { numRuns: 100 }
131
+ );
132
+ });
133
+
134
+ it('should handle empty objects and arrays', async () => {
135
+ await fc.assert(
136
+ fc.asyncProperty(
137
+ fc.string({ minLength: 10, maxLength: 100 }),
138
+ fc.constantFrom('all-time', 'weekly', 'monthly'),
139
+ async (apiKey, timeframe) => {
140
+ // Arrange
141
+ const responseData = {
142
+ timeframe,
143
+ page: 1,
144
+ limit: 50,
145
+ total: 0,
146
+ rankings: [],
147
+ };
148
+ mockClient.get.mockResolvedValue({ data: responseData });
149
+ const sdk = new Rooguys(apiKey);
150
+
151
+ // Act
152
+ const result = await sdk.leaderboards.getGlobal(timeframe as any);
153
+
154
+ // Assert
155
+ expect(result).toEqual(responseData);
156
+ expect(Array.isArray(result.rankings)).toBe(true);
157
+ expect(result.rankings).toHaveLength(0);
158
+ }
159
+ ),
160
+ { numRuns: 100 }
161
+ );
162
+ });
163
+
164
+ it('should preserve complex nested structures', async () => {
165
+ await fc.assert(
166
+ fc.asyncProperty(
167
+ fc.string({ minLength: 10, maxLength: 100 }),
168
+ fc.record({
169
+ success: fc.boolean(),
170
+ data: fc.record({
171
+ user_id: fc.string(),
172
+ current_score: fc.integer({ min: 0, max: 100 }),
173
+ declarative_score: fc.option(fc.integer({ min: 1, max: 5 }), { nil: null }),
174
+ inferred_score: fc.option(fc.integer({ min: 0, max: 100 }), { nil: null }),
175
+ status: fc.constantFrom('not_started', 'progressing', 'activated'),
176
+ history: fc.record({
177
+ initial: fc.option(fc.integer(), { nil: null }),
178
+ initial_date: fc.option(fc.string(), { nil: null }),
179
+ previous: fc.option(fc.integer(), { nil: null }),
180
+ }),
181
+ }),
182
+ }),
183
+ async (apiKey, responseData) => {
184
+ // Arrange
185
+ mockClient.get.mockResolvedValue({ data: responseData });
186
+ const sdk = new Rooguys(apiKey);
187
+
188
+ // Act
189
+ const result = await sdk.aha.getUserScore('test-user');
190
+
191
+ // Assert
192
+ expect(result).toEqual(responseData);
193
+ expect(result.data).toEqual(responseData.data);
194
+ expect(result.data.history).toEqual(responseData.data.history);
195
+ }
196
+ ),
197
+ { numRuns: 100 }
198
+ );
199
+ });
200
+ });
@@ -0,0 +1,164 @@
1
+ import { Rooguys } from '../../index';
2
+ import { createMockAxiosInstance } from '../utils/mockClient';
3
+ import { mockResponses, mockErrors } from '../fixtures/responses';
4
+
5
+ describe('Aha Resource', () => {
6
+ let client: Rooguys;
7
+ let mockAxios: any;
8
+
9
+ beforeEach(() => {
10
+ mockAxios = createMockAxiosInstance();
11
+ client = new Rooguys('test-api-key');
12
+ (client as any).client = mockAxios;
13
+ });
14
+
15
+ describe('declare', () => {
16
+ it('should declare aha score with valid value', async () => {
17
+ mockAxios.post.mockResolvedValue({ data: mockResponses.ahaDeclarationResponse });
18
+
19
+ const result = await client.aha.declare('user123', 4);
20
+
21
+ expect(mockAxios.post).toHaveBeenCalledWith('/aha/declare', {
22
+ user_id: 'user123',
23
+ value: 4,
24
+ });
25
+ expect(result).toEqual(mockResponses.ahaDeclarationResponse);
26
+ });
27
+
28
+ it('should declare aha score with value 1', async () => {
29
+ mockAxios.post.mockResolvedValue({ data: mockResponses.ahaDeclarationResponse });
30
+
31
+ const result = await client.aha.declare('user123', 1);
32
+
33
+ expect(mockAxios.post).toHaveBeenCalledWith('/aha/declare', {
34
+ user_id: 'user123',
35
+ value: 1,
36
+ });
37
+ expect(result).toEqual(mockResponses.ahaDeclarationResponse);
38
+ });
39
+
40
+ it('should declare aha score with value 5', async () => {
41
+ mockAxios.post.mockResolvedValue({ data: mockResponses.ahaDeclarationResponse });
42
+
43
+ const result = await client.aha.declare('user123', 5);
44
+
45
+ expect(mockAxios.post).toHaveBeenCalledWith('/aha/declare', {
46
+ user_id: 'user123',
47
+ value: 5,
48
+ });
49
+ expect(result).toEqual(mockResponses.ahaDeclarationResponse);
50
+ });
51
+
52
+ it('should throw error for value 0', async () => {
53
+ await expect(client.aha.declare('user123', 0)).rejects.toThrow(
54
+ 'Aha score value must be an integer between 1 and 5'
55
+ );
56
+ expect(mockAxios.post).not.toHaveBeenCalled();
57
+ });
58
+
59
+ it('should throw error for value 6', async () => {
60
+ await expect(client.aha.declare('user123', 6)).rejects.toThrow(
61
+ 'Aha score value must be an integer between 1 and 5'
62
+ );
63
+ expect(mockAxios.post).not.toHaveBeenCalled();
64
+ });
65
+
66
+ it('should throw error for negative value', async () => {
67
+ await expect(client.aha.declare('user123', -1)).rejects.toThrow(
68
+ 'Aha score value must be an integer between 1 and 5'
69
+ );
70
+ expect(mockAxios.post).not.toHaveBeenCalled();
71
+ });
72
+
73
+ it('should throw error for non-integer value', async () => {
74
+ await expect(client.aha.declare('user123', 3.5)).rejects.toThrow(
75
+ 'Aha score value must be an integer between 1 and 5'
76
+ );
77
+ expect(mockAxios.post).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it('should handle API error response', async () => {
81
+ mockAxios.post.mockRejectedValue({
82
+ isAxiosError: true,
83
+ response: {
84
+ data: mockErrors.ahaValueError,
85
+ },
86
+ });
87
+
88
+ await expect(client.aha.declare('user123', 3)).rejects.toThrow();
89
+ });
90
+ });
91
+
92
+ describe('getUserScore', () => {
93
+ it('should get user aha score successfully', async () => {
94
+ mockAxios.get.mockResolvedValue({ data: mockResponses.ahaScoreResponse });
95
+
96
+ const result = await client.aha.getUserScore('user123');
97
+
98
+ expect(mockAxios.get).toHaveBeenCalledWith('/users/user123/aha');
99
+ expect(result).toEqual(mockResponses.ahaScoreResponse);
100
+ });
101
+
102
+ it('should parse all aha score fields correctly', async () => {
103
+ mockAxios.get.mockResolvedValue({ data: mockResponses.ahaScoreResponse });
104
+
105
+ const result = await client.aha.getUserScore('user123');
106
+
107
+ expect(result.success).toBe(true);
108
+ expect(result.data.user_id).toBe('user123');
109
+ expect(result.data.current_score).toBe(75);
110
+ expect(result.data.declarative_score).toBe(80);
111
+ expect(result.data.inferred_score).toBe(70);
112
+ expect(result.data.status).toBe('activated');
113
+ });
114
+
115
+ it('should preserve history structure', async () => {
116
+ mockAxios.get.mockResolvedValue({ data: mockResponses.ahaScoreResponse });
117
+
118
+ const result = await client.aha.getUserScore('user123');
119
+
120
+ expect(result.data.history).toEqual({
121
+ initial: 50,
122
+ initial_date: '2024-01-01T00:00:00Z',
123
+ previous: 70,
124
+ });
125
+ });
126
+
127
+ it('should handle 404 error when user not found', async () => {
128
+ mockAxios.get.mockRejectedValue({
129
+ isAxiosError: true,
130
+ response: {
131
+ status: 404,
132
+ data: mockErrors.notFoundError,
133
+ },
134
+ });
135
+
136
+ await expect(client.aha.getUserScore('nonexistent')).rejects.toThrow();
137
+ });
138
+
139
+ it('should handle null declarative and inferred scores', async () => {
140
+ const responseWithNulls = {
141
+ success: true,
142
+ data: {
143
+ user_id: 'user123',
144
+ current_score: 0,
145
+ declarative_score: null,
146
+ inferred_score: null,
147
+ status: 'not_started',
148
+ history: {
149
+ initial: null,
150
+ initial_date: null,
151
+ previous: null,
152
+ },
153
+ },
154
+ };
155
+ mockAxios.get.mockResolvedValue({ data: responseWithNulls });
156
+
157
+ const result = await client.aha.getUserScore('user123');
158
+
159
+ expect(result.data.declarative_score).toBeNull();
160
+ expect(result.data.inferred_score).toBeNull();
161
+ expect(result.data.history.initial).toBeNull();
162
+ });
163
+ });
164
+ });
@@ -0,0 +1,112 @@
1
+ import axios from 'axios';
2
+ import { Rooguys } from '../../index';
3
+ import { createMockAxiosInstance, mockSuccessResponse, mockErrorResponse } from '../utils/mockClient';
4
+ import { mockResponses, mockErrors } from '../fixtures/responses';
5
+
6
+ jest.mock('axios');
7
+ const mockedAxios = axios as jest.Mocked<typeof axios>;
8
+
9
+ describe('Badges Resource', () => {
10
+ let client: Rooguys;
11
+ let mockAxiosInstance: ReturnType<typeof createMockAxiosInstance>;
12
+ const apiKey = 'test-api-key';
13
+
14
+ beforeEach(() => {
15
+ mockAxiosInstance = createMockAxiosInstance();
16
+ mockedAxios.create.mockReturnValue(mockAxiosInstance as any);
17
+ client = new Rooguys(apiKey);
18
+ jest.clearAllMocks();
19
+ });
20
+
21
+ describe('list', () => {
22
+ it('should list badges with default parameters', async () => {
23
+ mockAxiosInstance.get.mockResolvedValue(
24
+ mockSuccessResponse(mockResponses.badgesListResponse)
25
+ );
26
+
27
+ const result = await client.badges.list();
28
+
29
+ expect(mockAxiosInstance.get).toHaveBeenCalledWith('/badges', {
30
+ params: { page: 1, limit: 50, active_only: false },
31
+ });
32
+ expect(result).toEqual(mockResponses.badgesListResponse);
33
+ expect(result.badges).toHaveLength(1);
34
+ });
35
+
36
+ it('should list badges with custom pagination', async () => {
37
+ mockAxiosInstance.get.mockResolvedValue(
38
+ mockSuccessResponse(mockResponses.badgesListResponse)
39
+ );
40
+
41
+ await client.badges.list(2, 25);
42
+
43
+ expect(mockAxiosInstance.get).toHaveBeenCalledWith('/badges', {
44
+ params: { page: 2, limit: 25, active_only: false },
45
+ });
46
+ });
47
+
48
+ it('should list only active badges', async () => {
49
+ mockAxiosInstance.get.mockResolvedValue(
50
+ mockSuccessResponse(mockResponses.badgesListResponse)
51
+ );
52
+
53
+ await client.badges.list(1, 50, true);
54
+
55
+ expect(mockAxiosInstance.get).toHaveBeenCalledWith('/badges', {
56
+ params: { page: 1, limit: 50, active_only: true },
57
+ });
58
+ });
59
+
60
+ it('should handle empty badge list', async () => {
61
+ const emptyResponse = {
62
+ badges: [],
63
+ pagination: {
64
+ page: 1,
65
+ limit: 50,
66
+ total: 0,
67
+ totalPages: 0,
68
+ },
69
+ };
70
+ mockAxiosInstance.get.mockResolvedValue(mockSuccessResponse(emptyResponse));
71
+
72
+ const result = await client.badges.list();
73
+
74
+ expect(result.badges).toEqual([]);
75
+ expect(result.pagination.total).toBe(0);
76
+ });
77
+
78
+ it('should throw error for invalid pagination', async () => {
79
+ mockAxiosInstance.get.mockRejectedValue(
80
+ mockErrorResponse(400, mockErrors.invalidPaginationError.message)
81
+ );
82
+
83
+ await expect(client.badges.list(1, 150)).rejects.toThrow(
84
+ 'Limit must be between 1 and 100'
85
+ );
86
+ });
87
+
88
+ it('should handle badges with all fields', async () => {
89
+ mockAxiosInstance.get.mockResolvedValue(
90
+ mockSuccessResponse(mockResponses.badgesListResponse)
91
+ );
92
+
93
+ const result = await client.badges.list();
94
+
95
+ const badge = result.badges[0];
96
+ expect(badge.id).toBeDefined();
97
+ expect(badge.name).toBeDefined();
98
+ expect(badge.description).toBeDefined();
99
+ expect(badge.icon_url).toBeDefined();
100
+ expect(badge.is_active).toBeDefined();
101
+ expect(badge.unlock_criteria).toBeDefined();
102
+ });
103
+
104
+ it('should handle server error', async () => {
105
+ mockAxiosInstance.get.mockRejectedValue(
106
+ mockErrorResponse(500, 'Internal server error')
107
+ );
108
+
109
+ await expect(client.badges.list()).rejects.toThrow('Internal server error');
110
+ });
111
+ });
112
+ });
@@ -0,0 +1,187 @@
1
+ import axios from 'axios';
2
+ import { Rooguys } from '../../index';
3
+ import { createMockAxiosInstance } from '../utils/mockClient';
4
+
5
+ jest.mock('axios');
6
+ const mockedAxios = axios as jest.Mocked<typeof axios>;
7
+
8
+ describe('SDK Configuration', () => {
9
+ let mockAxiosInstance: ReturnType<typeof createMockAxiosInstance>;
10
+
11
+ beforeEach(() => {
12
+ mockAxiosInstance = createMockAxiosInstance();
13
+ mockedAxios.create.mockReturnValue(mockAxiosInstance as any);
14
+ jest.clearAllMocks();
15
+ });
16
+
17
+ describe('initialization', () => {
18
+ it('should initialize with API key', () => {
19
+ const client = new Rooguys('test-api-key');
20
+
21
+ expect(mockedAxios.create).toHaveBeenCalledWith(
22
+ expect.objectContaining({
23
+ headers: expect.objectContaining({
24
+ 'x-api-key': 'test-api-key',
25
+ }),
26
+ })
27
+ );
28
+ });
29
+
30
+ it('should use default base URL when not provided', () => {
31
+ new Rooguys('test-api-key');
32
+
33
+ expect(mockedAxios.create).toHaveBeenCalledWith(
34
+ expect.objectContaining({
35
+ baseURL: 'https://api.rooguys.com/v1',
36
+ })
37
+ );
38
+ });
39
+
40
+ it('should use custom base URL when provided', () => {
41
+ new Rooguys('test-api-key', {
42
+ baseUrl: 'https://custom.api.com/v1',
43
+ });
44
+
45
+ expect(mockedAxios.create).toHaveBeenCalledWith(
46
+ expect.objectContaining({
47
+ baseURL: 'https://custom.api.com/v1',
48
+ })
49
+ );
50
+ });
51
+
52
+ it('should use default timeout when not provided', () => {
53
+ new Rooguys('test-api-key');
54
+
55
+ expect(mockedAxios.create).toHaveBeenCalledWith(
56
+ expect.objectContaining({
57
+ timeout: 10000,
58
+ })
59
+ );
60
+ });
61
+
62
+ it('should use custom timeout when provided', () => {
63
+ new Rooguys('test-api-key', {
64
+ timeout: 30000,
65
+ });
66
+
67
+ expect(mockedAxios.create).toHaveBeenCalledWith(
68
+ expect.objectContaining({
69
+ timeout: 30000,
70
+ })
71
+ );
72
+ });
73
+
74
+ it('should set Content-Type header', () => {
75
+ new Rooguys('test-api-key');
76
+
77
+ expect(mockedAxios.create).toHaveBeenCalledWith(
78
+ expect.objectContaining({
79
+ headers: expect.objectContaining({
80
+ 'Content-Type': 'application/json',
81
+ }),
82
+ })
83
+ );
84
+ });
85
+
86
+ it('should accept both baseUrl and timeout options', () => {
87
+ new Rooguys('test-api-key', {
88
+ baseUrl: 'https://staging.api.com/v1',
89
+ timeout: 20000,
90
+ });
91
+
92
+ expect(mockedAxios.create).toHaveBeenCalledWith(
93
+ expect.objectContaining({
94
+ baseURL: 'https://staging.api.com/v1',
95
+ timeout: 20000,
96
+ })
97
+ );
98
+ });
99
+
100
+ it('should handle empty options object', () => {
101
+ new Rooguys('test-api-key', {});
102
+
103
+ expect(mockedAxios.create).toHaveBeenCalledWith(
104
+ expect.objectContaining({
105
+ baseURL: 'https://api.rooguys.com/v1',
106
+ timeout: 10000,
107
+ })
108
+ );
109
+ });
110
+
111
+ it('should handle localhost base URL', () => {
112
+ new Rooguys('test-api-key', {
113
+ baseUrl: 'http://localhost:3001/v1',
114
+ });
115
+
116
+ expect(mockedAxios.create).toHaveBeenCalledWith(
117
+ expect.objectContaining({
118
+ baseURL: 'http://localhost:3001/v1',
119
+ })
120
+ );
121
+ });
122
+
123
+ it('should handle very short timeout', () => {
124
+ new Rooguys('test-api-key', {
125
+ timeout: 1000,
126
+ });
127
+
128
+ expect(mockedAxios.create).toHaveBeenCalledWith(
129
+ expect.objectContaining({
130
+ timeout: 1000,
131
+ })
132
+ );
133
+ });
134
+
135
+ it('should handle very long timeout', () => {
136
+ new Rooguys('test-api-key', {
137
+ timeout: 60000,
138
+ });
139
+
140
+ expect(mockedAxios.create).toHaveBeenCalledWith(
141
+ expect.objectContaining({
142
+ timeout: 60000,
143
+ })
144
+ );
145
+ });
146
+ });
147
+
148
+ describe('API key handling', () => {
149
+ it('should include API key in all requests', () => {
150
+ new Rooguys('my-secret-key');
151
+
152
+ expect(mockedAxios.create).toHaveBeenCalledWith(
153
+ expect.objectContaining({
154
+ headers: expect.objectContaining({
155
+ 'x-api-key': 'my-secret-key',
156
+ }),
157
+ })
158
+ );
159
+ });
160
+
161
+ it('should handle long API keys', () => {
162
+ const longKey = 'sk_live_' + 'a'.repeat(100);
163
+ new Rooguys(longKey);
164
+
165
+ expect(mockedAxios.create).toHaveBeenCalledWith(
166
+ expect.objectContaining({
167
+ headers: expect.objectContaining({
168
+ 'x-api-key': longKey,
169
+ }),
170
+ })
171
+ );
172
+ });
173
+
174
+ it('should handle API keys with special characters', () => {
175
+ const keyWithSpecialChars = 'sk_test_abc-123_XYZ.456';
176
+ new Rooguys(keyWithSpecialChars);
177
+
178
+ expect(mockedAxios.create).toHaveBeenCalledWith(
179
+ expect.objectContaining({
180
+ headers: expect.objectContaining({
181
+ 'x-api-key': keyWithSpecialChars,
182
+ }),
183
+ })
184
+ );
185
+ });
186
+ });
187
+ });