@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,68 @@
|
|
|
1
|
+
import Rooguys from '../../index';
|
|
2
|
+
import { mockSuccessResponse, mockErrorResponse } from '../utils/mockClient';
|
|
3
|
+
import { mockResponses, mockErrors } from '../fixtures/responses';
|
|
4
|
+
|
|
5
|
+
describe('Badges Resource', () => {
|
|
6
|
+
let client;
|
|
7
|
+
const apiKey = 'test-api-key';
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
client = new Rooguys(apiKey);
|
|
11
|
+
global.fetch.mockClear();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('list', () => {
|
|
15
|
+
it('should list badges with default parameters', async () => {
|
|
16
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.badgesListResponse));
|
|
17
|
+
|
|
18
|
+
const result = await client.badges.list();
|
|
19
|
+
|
|
20
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
21
|
+
expect(callUrl).toContain('page=1');
|
|
22
|
+
expect(callUrl).toContain('limit=50');
|
|
23
|
+
expect(callUrl).toContain('active_only=false');
|
|
24
|
+
expect(result.badges).toHaveLength(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should list badges with custom pagination', async () => {
|
|
28
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.badgesListResponse));
|
|
29
|
+
|
|
30
|
+
await client.badges.list(2, 25);
|
|
31
|
+
|
|
32
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
33
|
+
expect(callUrl).toContain('page=2');
|
|
34
|
+
expect(callUrl).toContain('limit=25');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should list only active badges', async () => {
|
|
38
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.badgesListResponse));
|
|
39
|
+
|
|
40
|
+
await client.badges.list(1, 50, true);
|
|
41
|
+
|
|
42
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
43
|
+
expect(callUrl).toContain('active_only=true');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should handle empty badge list', async () => {
|
|
47
|
+
const emptyResponse = {
|
|
48
|
+
badges: [],
|
|
49
|
+
pagination: { page: 1, limit: 50, total: 0, totalPages: 0 },
|
|
50
|
+
};
|
|
51
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(emptyResponse));
|
|
52
|
+
|
|
53
|
+
const result = await client.badges.list();
|
|
54
|
+
|
|
55
|
+
expect(result.badges).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should throw error for invalid pagination', async () => {
|
|
59
|
+
global.fetch.mockResolvedValue(
|
|
60
|
+
mockErrorResponse(400, mockErrors.invalidPaginationError.message)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
await expect(client.badges.list(1, 150)).rejects.toThrow(
|
|
64
|
+
'Limit must be between 1 and 100'
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import Rooguys from '../../index';
|
|
2
|
+
|
|
3
|
+
describe('SDK Configuration', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
global.fetch.mockClear();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
describe('initialization', () => {
|
|
9
|
+
it('should initialize with API key', () => {
|
|
10
|
+
const client = new Rooguys('test-api-key');
|
|
11
|
+
|
|
12
|
+
expect(client.apiKey).toBe('test-api-key');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should use default base URL when not provided', () => {
|
|
16
|
+
const client = new Rooguys('test-api-key');
|
|
17
|
+
|
|
18
|
+
expect(client.baseUrl).toBe('https://api.rooguys.com/v1');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should use custom base URL when provided', () => {
|
|
22
|
+
const client = new Rooguys('test-api-key', {
|
|
23
|
+
baseUrl: 'https://custom.api.com/v1',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(client.baseUrl).toBe('https://custom.api.com/v1');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should use default timeout when not provided', () => {
|
|
30
|
+
const client = new Rooguys('test-api-key');
|
|
31
|
+
|
|
32
|
+
expect(client.timeout).toBe(10000);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should use custom timeout when provided', () => {
|
|
36
|
+
const client = new Rooguys('test-api-key', {
|
|
37
|
+
timeout: 30000,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(client.timeout).toBe(30000);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should accept both baseUrl and timeout options', () => {
|
|
44
|
+
const client = new Rooguys('test-api-key', {
|
|
45
|
+
baseUrl: 'https://staging.api.com/v1',
|
|
46
|
+
timeout: 20000,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(client.baseUrl).toBe('https://staging.api.com/v1');
|
|
50
|
+
expect(client.timeout).toBe(20000);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should handle empty options object', () => {
|
|
54
|
+
const client = new Rooguys('test-api-key', {});
|
|
55
|
+
|
|
56
|
+
expect(client.baseUrl).toBe('https://api.rooguys.com/v1');
|
|
57
|
+
expect(client.timeout).toBe(10000);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should handle localhost base URL', () => {
|
|
61
|
+
const client = new Rooguys('test-api-key', {
|
|
62
|
+
baseUrl: 'http://localhost:3001/v1',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(client.baseUrl).toBe('http://localhost:3001/v1');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('API key handling', () => {
|
|
70
|
+
it('should include API key in all requests', async () => {
|
|
71
|
+
const client = new Rooguys('my-secret-key');
|
|
72
|
+
global.fetch.mockResolvedValue({
|
|
73
|
+
ok: true,
|
|
74
|
+
json: () => Promise.resolve({}),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await client.events.track('test', 'user1');
|
|
78
|
+
|
|
79
|
+
const callHeaders = global.fetch.mock.calls[0][1].headers;
|
|
80
|
+
expect(callHeaders['x-api-key']).toBe('my-secret-key');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should handle long API keys', () => {
|
|
84
|
+
const longKey = 'sk_live_' + 'a'.repeat(100);
|
|
85
|
+
const client = new Rooguys(longKey);
|
|
86
|
+
|
|
87
|
+
expect(client.apiKey).toBe(longKey);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle API keys with special characters', () => {
|
|
91
|
+
const keyWithSpecialChars = 'sk_test_abc-123_XYZ.456';
|
|
92
|
+
const client = new Rooguys(keyWithSpecialChars);
|
|
93
|
+
|
|
94
|
+
expect(client.apiKey).toBe(keyWithSpecialChars);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import Rooguys from '../../index';
|
|
2
|
+
import { mockErrorResponse, mockTimeoutError, mockNetworkError } from '../utils/mockClient';
|
|
3
|
+
import { mockErrors } from '../fixtures/responses';
|
|
4
|
+
|
|
5
|
+
describe('Error Handling', () => {
|
|
6
|
+
let client;
|
|
7
|
+
const apiKey = 'test-api-key';
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
client = new Rooguys(apiKey);
|
|
11
|
+
global.fetch.mockClear();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('4xx client errors', () => {
|
|
15
|
+
it('should throw error with message for 400 Bad Request', async () => {
|
|
16
|
+
global.fetch.mockResolvedValue(mockErrorResponse(400, 'Bad Request'));
|
|
17
|
+
|
|
18
|
+
await expect(client.events.track('test', 'user1')).rejects.toThrow('Bad Request');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should throw error with message for 401 Unauthorized', async () => {
|
|
22
|
+
global.fetch.mockResolvedValue(
|
|
23
|
+
mockErrorResponse(401, mockErrors.unauthorizedError.message)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
await expect(client.users.get('user1')).rejects.toThrow('Invalid or missing API key');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should throw error with message for 404 Not Found', async () => {
|
|
30
|
+
global.fetch.mockResolvedValue(
|
|
31
|
+
mockErrorResponse(404, mockErrors.notFoundError.message)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
await expect(client.users.get('nonexistent')).rejects.toThrow("User 'user123' does not exist in this project");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should throw error for 429 Too Many Requests', async () => {
|
|
38
|
+
global.fetch.mockResolvedValue(mockErrorResponse(429, 'Rate limit exceeded'));
|
|
39
|
+
|
|
40
|
+
await expect(client.users.get('user1')).rejects.toThrow('Rate limit exceeded');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('5xx server errors', () => {
|
|
45
|
+
it('should throw error with message for 500 Internal Server Error', async () => {
|
|
46
|
+
global.fetch.mockResolvedValue(mockErrorResponse(500, 'Internal server error'));
|
|
47
|
+
|
|
48
|
+
await expect(client.events.track('test', 'user1')).rejects.toThrow(
|
|
49
|
+
'Internal server error'
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should throw error with message for 503 Service Unavailable', async () => {
|
|
54
|
+
global.fetch.mockResolvedValue(
|
|
55
|
+
mockErrorResponse(503, mockErrors.queueFullError.message)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
await expect(client.events.track('test', 'user1')).rejects.toThrow(
|
|
59
|
+
'Event queue is full'
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should throw error for 502 Bad Gateway', async () => {
|
|
64
|
+
global.fetch.mockResolvedValue(mockErrorResponse(502, 'Bad Gateway'));
|
|
65
|
+
|
|
66
|
+
await expect(client.users.get('user1')).rejects.toThrow('Bad Gateway');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('network errors', () => {
|
|
71
|
+
it('should throw error for network timeout', async () => {
|
|
72
|
+
global.fetch.mockRejectedValue(mockTimeoutError());
|
|
73
|
+
|
|
74
|
+
await expect(client.events.track('test', 'user1')).rejects.toThrow('aborted');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should throw error for network failure', async () => {
|
|
78
|
+
global.fetch.mockRejectedValue(mockNetworkError('Network request failed'));
|
|
79
|
+
|
|
80
|
+
await expect(client.users.get('user1')).rejects.toThrow('Network');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should throw error for connection refused', async () => {
|
|
84
|
+
global.fetch.mockRejectedValue(mockNetworkError('Failed to fetch'));
|
|
85
|
+
|
|
86
|
+
await expect(client.users.get('user1')).rejects.toThrow('Failed to fetch');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('malformed responses', () => {
|
|
91
|
+
it('should handle response without error message', async () => {
|
|
92
|
+
global.fetch.mockResolvedValue({
|
|
93
|
+
ok: false,
|
|
94
|
+
status: 500,
|
|
95
|
+
statusText: 'Internal Server Error',
|
|
96
|
+
json: () => Promise.resolve({}),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await expect(client.events.track('test', 'user1')).rejects.toThrow(
|
|
100
|
+
'Internal Server Error'
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should handle response with invalid JSON', async () => {
|
|
105
|
+
global.fetch.mockResolvedValue({
|
|
106
|
+
ok: false,
|
|
107
|
+
status: 500,
|
|
108
|
+
statusText: 'Internal Server Error',
|
|
109
|
+
json: () => Promise.reject(new Error('Invalid JSON')),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await expect(client.events.track('test', 'user1')).rejects.toThrow(
|
|
113
|
+
'Internal Server Error'
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('error detail preservation', () => {
|
|
119
|
+
it('should preserve error details from API response', async () => {
|
|
120
|
+
global.fetch.mockResolvedValue(
|
|
121
|
+
mockErrorResponse(400, 'Validation failed', [
|
|
122
|
+
{ field: 'user_id', message: 'User ID is required' },
|
|
123
|
+
])
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
await expect(client.events.track('', '')).rejects.toThrow('Validation failed');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should handle errors with nested details', async () => {
|
|
130
|
+
global.fetch.mockResolvedValue(
|
|
131
|
+
mockErrorResponse(400, 'Complex validation error', {
|
|
132
|
+
errors: { properties: { amount: 'Must be a positive number' } },
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await expect(client.events.track('test', 'user1')).rejects.toThrow(
|
|
137
|
+
'Complex validation error'
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import Rooguys from '../../index';
|
|
2
|
+
import { mockSuccessResponse, mockErrorResponse, mockTimeoutError } from '../utils/mockClient';
|
|
3
|
+
import { mockResponses, mockErrors } from '../fixtures/responses';
|
|
4
|
+
|
|
5
|
+
describe('Events Resource', () => {
|
|
6
|
+
let client;
|
|
7
|
+
const apiKey = 'test-api-key';
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
client = new Rooguys(apiKey);
|
|
11
|
+
global.fetch.mockClear();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('track', () => {
|
|
15
|
+
it('should track an event with valid inputs', async () => {
|
|
16
|
+
global.fetch.mockResolvedValue(
|
|
17
|
+
mockSuccessResponse(mockResponses.trackEventResponse)
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const result = await client.events.track('purchase_completed', 'user_123', {
|
|
21
|
+
amount: 50.0,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
25
|
+
expect.stringContaining('/event'),
|
|
26
|
+
expect.objectContaining({
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: expect.objectContaining({
|
|
29
|
+
'x-api-key': apiKey,
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
}),
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
event_name: 'purchase_completed',
|
|
34
|
+
user_id: 'user_123',
|
|
35
|
+
properties: { amount: 50.0 },
|
|
36
|
+
}),
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
expect(result).toEqual(mockResponses.trackEventResponse);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should track an event with empty properties', async () => {
|
|
43
|
+
global.fetch.mockResolvedValue(
|
|
44
|
+
mockSuccessResponse(mockResponses.trackEventResponse)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const result = await client.events.track('user_login', 'user_456');
|
|
48
|
+
|
|
49
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
50
|
+
expect(result).toEqual(mockResponses.trackEventResponse);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should include profile when includeProfile is true', async () => {
|
|
54
|
+
global.fetch.mockResolvedValue(
|
|
55
|
+
mockSuccessResponse(mockResponses.trackEventWithProfileResponse)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const result = await client.events.track(
|
|
59
|
+
'purchase_completed',
|
|
60
|
+
'user_123',
|
|
61
|
+
{ amount: 50.0 },
|
|
62
|
+
{ includeProfile: true }
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
66
|
+
expect(callUrl).toContain('include_profile=true');
|
|
67
|
+
expect(result).toEqual(mockResponses.trackEventWithProfileResponse);
|
|
68
|
+
expect(result.profile).toBeDefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should handle special characters in event name', async () => {
|
|
72
|
+
global.fetch.mockResolvedValue(
|
|
73
|
+
mockSuccessResponse(mockResponses.trackEventResponse)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
await client.events.track('user-signup_v2', 'user_123');
|
|
77
|
+
|
|
78
|
+
const callBody = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
79
|
+
expect(callBody.event_name).toBe('user-signup_v2');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should handle special characters in user ID', async () => {
|
|
83
|
+
global.fetch.mockResolvedValue(
|
|
84
|
+
mockSuccessResponse(mockResponses.trackEventResponse)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
await client.events.track('user_login', 'user@example.com');
|
|
88
|
+
|
|
89
|
+
const callBody = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
90
|
+
expect(callBody.user_id).toBe('user@example.com');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle complex nested properties', async () => {
|
|
94
|
+
global.fetch.mockResolvedValue(
|
|
95
|
+
mockSuccessResponse(mockResponses.trackEventResponse)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const complexProperties = {
|
|
99
|
+
order: {
|
|
100
|
+
id: 'order_123',
|
|
101
|
+
items: [
|
|
102
|
+
{ sku: 'ITEM1', quantity: 2 },
|
|
103
|
+
{ sku: 'ITEM2', quantity: 1 },
|
|
104
|
+
],
|
|
105
|
+
total: 150.0,
|
|
106
|
+
},
|
|
107
|
+
metadata: {
|
|
108
|
+
source: 'mobile_app',
|
|
109
|
+
version: '2.1.0',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
await client.events.track('order_placed', 'user_123', complexProperties);
|
|
114
|
+
|
|
115
|
+
const callBody = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
116
|
+
expect(callBody.properties).toEqual(complexProperties);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should throw error when API returns 400', async () => {
|
|
120
|
+
global.fetch.mockResolvedValue(
|
|
121
|
+
mockErrorResponse(400, 'Validation failed', mockErrors.validationError.details)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
await expect(
|
|
125
|
+
client.events.track('', 'user_123')
|
|
126
|
+
).rejects.toThrow('Validation failed');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should throw error when API returns 500', async () => {
|
|
130
|
+
global.fetch.mockResolvedValue(
|
|
131
|
+
mockErrorResponse(500, 'Internal server error')
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
await expect(
|
|
135
|
+
client.events.track('user_login', 'user_123')
|
|
136
|
+
).rejects.toThrow('Internal server error');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should throw error when API returns 503 (queue full)', async () => {
|
|
140
|
+
global.fetch.mockResolvedValue(
|
|
141
|
+
mockErrorResponse(503, mockErrors.queueFullError.message)
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
await expect(
|
|
145
|
+
client.events.track('user_login', 'user_123')
|
|
146
|
+
).rejects.toThrow('Event queue is full');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should handle network timeout', async () => {
|
|
150
|
+
global.fetch.mockRejectedValue(mockTimeoutError());
|
|
151
|
+
|
|
152
|
+
await expect(
|
|
153
|
+
client.events.track('user_login', 'user_123')
|
|
154
|
+
).rejects.toThrow('aborted');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should handle properties with null values', async () => {
|
|
158
|
+
global.fetch.mockResolvedValue(
|
|
159
|
+
mockSuccessResponse(mockResponses.trackEventResponse)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
await client.events.track('user_updated', 'user_123', {
|
|
163
|
+
email: 'user@example.com',
|
|
164
|
+
phone: null,
|
|
165
|
+
address: null,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const callBody = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
169
|
+
expect(callBody.properties).toEqual({
|
|
170
|
+
email: 'user@example.com',
|
|
171
|
+
phone: null,
|
|
172
|
+
address: null,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should handle properties with boolean values', async () => {
|
|
177
|
+
global.fetch.mockResolvedValue(
|
|
178
|
+
mockSuccessResponse(mockResponses.trackEventResponse)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
await client.events.track('feature_toggled', 'user_123', {
|
|
182
|
+
feature_name: 'dark_mode',
|
|
183
|
+
enabled: true,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const callBody = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
187
|
+
expect(callBody.properties.enabled).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should handle properties with numeric values', async () => {
|
|
191
|
+
global.fetch.mockResolvedValue(
|
|
192
|
+
mockSuccessResponse(mockResponses.trackEventResponse)
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
await client.events.track('score_updated', 'user_123', {
|
|
196
|
+
score: 1500,
|
|
197
|
+
multiplier: 1.5,
|
|
198
|
+
rank: 42,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const callBody = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
202
|
+
expect(callBody.properties).toEqual({
|
|
203
|
+
score: 1500,
|
|
204
|
+
multiplier: 1.5,
|
|
205
|
+
rank: 42,
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should handle empty string properties', async () => {
|
|
210
|
+
global.fetch.mockResolvedValue(
|
|
211
|
+
mockSuccessResponse(mockResponses.trackEventResponse)
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
await client.events.track('form_submitted', 'user_123', {
|
|
215
|
+
name: 'John Doe',
|
|
216
|
+
comment: '',
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const callBody = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
220
|
+
expect(callBody.properties.comment).toBe('');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import Rooguys from '../../index';
|
|
2
|
+
import { mockSuccessResponse, mockErrorResponse } from '../utils/mockClient';
|
|
3
|
+
import { mockResponses, mockErrors } from '../fixtures/responses';
|
|
4
|
+
|
|
5
|
+
describe('Leaderboards Resource', () => {
|
|
6
|
+
let client;
|
|
7
|
+
const apiKey = 'test-api-key';
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
client = new Rooguys(apiKey);
|
|
11
|
+
global.fetch.mockClear();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('getGlobal', () => {
|
|
15
|
+
it('should get global leaderboard with default parameters', async () => {
|
|
16
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardResponse));
|
|
17
|
+
|
|
18
|
+
const result = await client.leaderboards.getGlobal();
|
|
19
|
+
|
|
20
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
21
|
+
expect(callUrl).toContain('timeframe=all-time');
|
|
22
|
+
expect(callUrl).toContain('page=1');
|
|
23
|
+
expect(callUrl).toContain('limit=50');
|
|
24
|
+
expect(result.rankings).toHaveLength(2);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should get global leaderboard with weekly timeframe', async () => {
|
|
28
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardResponse));
|
|
29
|
+
|
|
30
|
+
await client.leaderboards.getGlobal('weekly');
|
|
31
|
+
|
|
32
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
33
|
+
expect(callUrl).toContain('timeframe=weekly');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should get global leaderboard with custom pagination', async () => {
|
|
37
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardResponse));
|
|
38
|
+
|
|
39
|
+
await client.leaderboards.getGlobal('all-time', 2, 25);
|
|
40
|
+
|
|
41
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
42
|
+
expect(callUrl).toContain('page=2');
|
|
43
|
+
expect(callUrl).toContain('limit=25');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should throw error for invalid timeframe', async () => {
|
|
47
|
+
global.fetch.mockResolvedValue(
|
|
48
|
+
mockErrorResponse(400, mockErrors.invalidTimeframeError.message)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
await expect(client.leaderboards.getGlobal('invalid')).rejects.toThrow(
|
|
52
|
+
'Timeframe must be one of'
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('list', () => {
|
|
58
|
+
it('should list all leaderboards', async () => {
|
|
59
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardsListResponse));
|
|
60
|
+
|
|
61
|
+
const result = await client.leaderboards.list();
|
|
62
|
+
|
|
63
|
+
expect(result.leaderboards).toHaveLength(1);
|
|
64
|
+
expect(result.pagination).toBeDefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should list leaderboards with search', async () => {
|
|
68
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.leaderboardsListResponse));
|
|
69
|
+
|
|
70
|
+
await client.leaderboards.list(1, 50, 'top');
|
|
71
|
+
|
|
72
|
+
const callUrl = global.fetch.mock.calls[0][0];
|
|
73
|
+
expect(callUrl).toContain('search=top');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('getCustom', () => {
|
|
78
|
+
it('should get custom leaderboard by ID', async () => {
|
|
79
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.customLeaderboardResponse));
|
|
80
|
+
|
|
81
|
+
const result = await client.leaderboards.getCustom('lb1');
|
|
82
|
+
|
|
83
|
+
expect(result.leaderboard).toBeDefined();
|
|
84
|
+
expect(result.rankings).toBeDefined();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should throw 404 for non-existent leaderboard', async () => {
|
|
88
|
+
global.fetch.mockResolvedValue(mockErrorResponse(404, 'Leaderboard not found'));
|
|
89
|
+
|
|
90
|
+
await expect(client.leaderboards.getCustom('invalid_id')).rejects.toThrow(
|
|
91
|
+
'Leaderboard not found'
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('getUserRank', () => {
|
|
97
|
+
it('should get user rank in custom leaderboard', async () => {
|
|
98
|
+
global.fetch.mockResolvedValue(mockSuccessResponse(mockResponses.userRankResponse));
|
|
99
|
+
|
|
100
|
+
const result = await client.leaderboards.getUserRank('lb1', 'user_123');
|
|
101
|
+
|
|
102
|
+
expect(result.rank).toBe(42);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should throw 404 for non-existent user or leaderboard', async () => {
|
|
106
|
+
global.fetch.mockResolvedValue(mockErrorResponse(404, 'Not found'));
|
|
107
|
+
|
|
108
|
+
await expect(
|
|
109
|
+
client.leaderboards.getUserRank('lb1', 'invalid_user')
|
|
110
|
+
).rejects.toThrow('Not found');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|