@youversion/platform-core 0.6.0 → 0.8.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.
@@ -6,11 +6,23 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
6
  // Mock globals before importing the module
7
7
  const mockUUID = '550e8400-e29b-41d4-a716-446655440000';
8
8
  const mockRandomUUID = vi.fn(() => mockUUID);
9
- const mockGetItem = vi.fn();
10
- const mockSetItem = vi.fn();
9
+
10
+ // Create a working localStorage mock
11
+ const mockStorage: Record<string, string> = {};
12
+ const mockGetItem = vi.fn((key: string) => mockStorage[key] || null);
13
+ const mockSetItem = vi.fn((key: string, value: string) => {
14
+ mockStorage[key] = value;
15
+ });
16
+ const mockRemoveItem = vi.fn((key: string) => {
17
+ delete mockStorage[key];
18
+ });
11
19
 
12
20
  vi.stubGlobal('crypto', { randomUUID: mockRandomUUID });
13
- vi.stubGlobal('localStorage', { getItem: mockGetItem, setItem: mockSetItem });
21
+ vi.stubGlobal('localStorage', {
22
+ getItem: mockGetItem,
23
+ setItem: mockSetItem,
24
+ removeItem: mockRemoveItem,
25
+ });
14
26
 
15
27
  import { YouVersionPlatformConfiguration } from '../YouVersionPlatformConfiguration';
16
28
 
@@ -23,21 +35,20 @@ if (!envApiHost) {
23
35
 
24
36
  describe('YouVersionPlatformConfiguration', () => {
25
37
  beforeEach(() => {
26
- // Reset all static properties
27
- YouVersionPlatformConfiguration.appKey = null;
28
- YouVersionPlatformConfiguration.installationId = null;
29
- YouVersionPlatformConfiguration.setAccessToken(null);
30
- YouVersionPlatformConfiguration.apiHost = envApiHost;
31
- YouVersionPlatformConfiguration.isPreviewMode = false;
32
- YouVersionPlatformConfiguration.previewUserInfo = null;
33
-
34
38
  // Clear call history but keep implementation
35
39
  mockRandomUUID.mockClear();
36
40
  mockGetItem.mockClear();
37
41
  mockSetItem.mockClear();
42
+ mockRemoveItem.mockClear();
43
+
44
+ // Clear mock storage
45
+ Object.keys(mockStorage).forEach((key) => delete mockStorage[key]);
38
46
 
39
- // Setup localStorage to return null by default (empty)
40
- mockGetItem.mockReturnValue(null);
47
+ // Reset all static properties
48
+ YouVersionPlatformConfiguration.appKey = null;
49
+ YouVersionPlatformConfiguration.installationId = null;
50
+ YouVersionPlatformConfiguration.saveAuthData(null, null, null, null);
51
+ YouVersionPlatformConfiguration.apiHost = envApiHost;
41
52
  });
42
53
 
43
54
  afterEach(() => {
@@ -80,7 +91,10 @@ describe('YouVersionPlatformConfiguration', () => {
80
91
 
81
92
  it('should use existing UUID from localStorage when installationId is null', () => {
82
93
  const existingUUID = 'existing-uuid-from-storage';
83
- mockGetItem.mockReturnValue(existingUUID);
94
+ mockStorage['x-yvp-installation-id'] = existingUUID;
95
+
96
+ // Clear the call history after setting up localStorage but before the test
97
+ mockRandomUUID.mockClear();
84
98
 
85
99
  YouVersionPlatformConfiguration.installationId = null;
86
100
 
@@ -96,16 +110,176 @@ describe('YouVersionPlatformConfiguration', () => {
96
110
  });
97
111
  });
98
112
 
99
- describe('accessToken', () => {
100
- it('should set and get access token', () => {
113
+ describe('saveAuthData', () => {
114
+ it('should save and retrieve access token', () => {
101
115
  expect(YouVersionPlatformConfiguration.accessToken).toBeNull();
102
116
 
103
117
  const token = 'test-access-token';
104
- YouVersionPlatformConfiguration.setAccessToken(token);
118
+ YouVersionPlatformConfiguration.saveAuthData(token, null, null, null);
105
119
  expect(YouVersionPlatformConfiguration.accessToken).toBe(token);
106
120
 
107
- YouVersionPlatformConfiguration.setAccessToken(null);
121
+ YouVersionPlatformConfiguration.saveAuthData(null, null, null, null);
122
+ expect(YouVersionPlatformConfiguration.accessToken).toBeNull();
123
+ });
124
+
125
+ it('should save and retrieve refresh token', () => {
126
+ expect(YouVersionPlatformConfiguration.refreshToken).toBeNull();
127
+
128
+ const refreshToken = 'test-refresh-token';
129
+ YouVersionPlatformConfiguration.saveAuthData(null, refreshToken, null, null);
130
+ expect(YouVersionPlatformConfiguration.refreshToken).toBe(refreshToken);
131
+
132
+ YouVersionPlatformConfiguration.saveAuthData(null, null, null, null);
133
+ expect(YouVersionPlatformConfiguration.refreshToken).toBeNull();
134
+ });
135
+
136
+ it('should save and retrieve expiry date', () => {
137
+ expect(YouVersionPlatformConfiguration.tokenExpiryDate).toBeNull();
138
+
139
+ const expiryDate = new Date('2024-12-31T23:59:59Z');
140
+ YouVersionPlatformConfiguration.saveAuthData(null, null, null, expiryDate);
141
+ expect(YouVersionPlatformConfiguration.tokenExpiryDate).toEqual(expiryDate);
142
+
143
+ YouVersionPlatformConfiguration.saveAuthData(null, null, null, null);
144
+ expect(YouVersionPlatformConfiguration.tokenExpiryDate).toBeNull();
145
+ });
146
+
147
+ it('should save all auth data together', () => {
148
+ const accessToken = 'test-access-token';
149
+ const refreshToken = 'test-refresh-token';
150
+ const idToken = 'test-id-token';
151
+ const expiryDate = new Date('2024-12-31T23:59:59Z');
152
+
153
+ YouVersionPlatformConfiguration.saveAuthData(accessToken, refreshToken, idToken, expiryDate);
154
+
155
+ expect(YouVersionPlatformConfiguration.accessToken).toBe(accessToken);
156
+ expect(YouVersionPlatformConfiguration.refreshToken).toBe(refreshToken);
157
+ expect(YouVersionPlatformConfiguration.idToken).toBe(idToken);
158
+ expect(YouVersionPlatformConfiguration.tokenExpiryDate).toEqual(expiryDate);
159
+ });
160
+
161
+ it('should handle partial updates without affecting other tokens', () => {
162
+ // Set up initial state
163
+ const initialAccess = 'initial-access';
164
+ const initialRefresh = 'initial-refresh';
165
+ const initialIdToken = 'initial-id-token';
166
+ const initialExpiry = new Date('2024-01-01T00:00:00Z');
167
+ YouVersionPlatformConfiguration.saveAuthData(
168
+ initialAccess,
169
+ initialRefresh,
170
+ initialIdToken,
171
+ initialExpiry,
172
+ );
173
+
174
+ // Update only access token
175
+ const newAccess = 'new-access-token';
176
+ YouVersionPlatformConfiguration.saveAuthData(newAccess, null, null, null);
177
+
178
+ expect(YouVersionPlatformConfiguration.accessToken).toBe(newAccess);
179
+ expect(YouVersionPlatformConfiguration.refreshToken).toBeNull();
180
+ expect(YouVersionPlatformConfiguration.idToken).toBeNull();
181
+ expect(YouVersionPlatformConfiguration.tokenExpiryDate).toBeNull();
182
+ });
183
+
184
+ it('should properly serialize and deserialize dates', () => {
185
+ const originalDate = new Date('2024-06-15T14:30:45.123Z');
186
+ YouVersionPlatformConfiguration.saveAuthData(null, null, null, originalDate);
187
+
188
+ const retrievedDate = YouVersionPlatformConfiguration.tokenExpiryDate;
189
+ expect(retrievedDate).toEqual(originalDate);
190
+ expect(retrievedDate?.getTime()).toBe(originalDate.getTime());
191
+ });
192
+
193
+ it('should call localStorage methods correctly', () => {
194
+ const accessToken = 'test-access';
195
+ const refreshToken = 'test-refresh';
196
+ const idToken = 'test-id';
197
+ const expiryDate = new Date('2024-12-31T23:59:59Z');
198
+
199
+ YouVersionPlatformConfiguration.saveAuthData(accessToken, refreshToken, idToken, expiryDate);
200
+
201
+ expect(mockSetItem).toHaveBeenCalledWith('accessToken', accessToken);
202
+ expect(mockSetItem).toHaveBeenCalledWith('refreshToken', refreshToken);
203
+ expect(mockSetItem).toHaveBeenCalledWith('idToken', idToken);
204
+ expect(mockSetItem).toHaveBeenCalledWith('expiryDate', expiryDate.toISOString());
205
+ });
206
+
207
+ it('should call removeItem when tokens are null', () => {
208
+ // First set some values
209
+ YouVersionPlatformConfiguration.saveAuthData('access', 'refresh', 'id-token', new Date());
210
+
211
+ // Clear the mock calls
212
+ mockRemoveItem.mockClear();
213
+
214
+ // Now set to null
215
+ YouVersionPlatformConfiguration.saveAuthData(null, null, null, null);
216
+
217
+ expect(mockRemoveItem).toHaveBeenCalledWith('accessToken');
218
+ expect(mockRemoveItem).toHaveBeenCalledWith('refreshToken');
219
+ expect(mockRemoveItem).toHaveBeenCalledWith('idToken');
220
+ expect(mockRemoveItem).toHaveBeenCalledWith('expiryDate');
221
+ });
222
+ });
223
+
224
+ describe('clearAuthTokens', () => {
225
+ it('should clear all auth tokens', () => {
226
+ // Set up initial auth data
227
+ const accessToken = 'test-access-token';
228
+ const refreshToken = 'test-refresh-token';
229
+ const idToken = 'test-id-token';
230
+ const expiryDate = new Date('2024-12-31T23:59:59Z');
231
+ YouVersionPlatformConfiguration.saveAuthData(accessToken, refreshToken, idToken, expiryDate);
232
+
233
+ // Verify data is set
234
+ expect(YouVersionPlatformConfiguration.accessToken).toBe(accessToken);
235
+ expect(YouVersionPlatformConfiguration.refreshToken).toBe(refreshToken);
236
+ expect(YouVersionPlatformConfiguration.idToken).toBe(idToken);
237
+ expect(YouVersionPlatformConfiguration.tokenExpiryDate).toEqual(expiryDate);
238
+
239
+ // Clear all tokens
240
+ YouVersionPlatformConfiguration.clearAuthTokens();
241
+
242
+ // Verify all tokens are null
108
243
  expect(YouVersionPlatformConfiguration.accessToken).toBeNull();
244
+ expect(YouVersionPlatformConfiguration.refreshToken).toBeNull();
245
+ expect(YouVersionPlatformConfiguration.idToken).toBeNull();
246
+ expect(YouVersionPlatformConfiguration.tokenExpiryDate).toBeNull();
247
+ });
248
+
249
+ it('should call removeItem for all token types', () => {
250
+ // Set up some auth data first
251
+ YouVersionPlatformConfiguration.saveAuthData('access', 'refresh', 'id-token', new Date());
252
+
253
+ // Clear mock calls
254
+ mockRemoveItem.mockClear();
255
+
256
+ // Clear auth tokens
257
+ YouVersionPlatformConfiguration.clearAuthTokens();
258
+
259
+ // Verify removeItem was called for each token type
260
+ expect(mockRemoveItem).toHaveBeenCalledWith('accessToken');
261
+ expect(mockRemoveItem).toHaveBeenCalledWith('refreshToken');
262
+ expect(mockRemoveItem).toHaveBeenCalledWith('idToken');
263
+ expect(mockRemoveItem).toHaveBeenCalledWith('expiryDate');
264
+ });
265
+
266
+ it('should work when no tokens are previously set', () => {
267
+ // Ensure clean state
268
+ expect(YouVersionPlatformConfiguration.accessToken).toBeNull();
269
+ expect(YouVersionPlatformConfiguration.refreshToken).toBeNull();
270
+ expect(YouVersionPlatformConfiguration.idToken).toBeNull();
271
+ expect(YouVersionPlatformConfiguration.tokenExpiryDate).toBeNull();
272
+
273
+ // Should not throw when clearing empty state
274
+ expect(() => {
275
+ YouVersionPlatformConfiguration.clearAuthTokens();
276
+ }).not.toThrow();
277
+
278
+ // State should remain null
279
+ expect(YouVersionPlatformConfiguration.accessToken).toBeNull();
280
+ expect(YouVersionPlatformConfiguration.refreshToken).toBeNull();
281
+ expect(YouVersionPlatformConfiguration.idToken).toBeNull();
282
+ expect(YouVersionPlatformConfiguration.tokenExpiryDate).toBeNull();
109
283
  });
110
284
  });
111
285
 
@@ -120,18 +294,6 @@ describe('YouVersionPlatformConfiguration', () => {
120
294
  });
121
295
  });
122
296
 
123
- describe('isPreviewMode', () => {
124
- it('should get and set preview mode', () => {
125
- expect(YouVersionPlatformConfiguration.isPreviewMode).toBe(false);
126
-
127
- YouVersionPlatformConfiguration.isPreviewMode = true;
128
- expect(YouVersionPlatformConfiguration.isPreviewMode).toBe(true);
129
-
130
- YouVersionPlatformConfiguration.isPreviewMode = false;
131
- expect(YouVersionPlatformConfiguration.isPreviewMode).toBe(false);
132
- });
133
- });
134
-
135
297
  describe('UUID generation edge cases', () => {
136
298
  it('should generate valid UUID format', () => {
137
299
  YouVersionPlatformConfiguration.installationId = null;
@@ -0,0 +1,347 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { YouVersionUserInfoJSON } from '../YouVersionUserInfo';
3
+ import { YouVersionUserInfo } from '../YouVersionUserInfo';
4
+
5
+ describe('YouVersionUserInfo', () => {
6
+ describe('constructor', () => {
7
+ it('should create instance with valid data', () => {
8
+ const userData: YouVersionUserInfoJSON = {
9
+ name: 'John Doe',
10
+ id: 'user123',
11
+ avatar_url: 'https://example.com/avatar/{width}x{height}.jpg',
12
+ email: 'john.doe@example.com',
13
+ };
14
+
15
+ const userInfo = new YouVersionUserInfo(userData);
16
+
17
+ expect(userInfo.name).toBe('John Doe');
18
+ expect(userInfo.userId).toBe('user123');
19
+ expect(userInfo.email).toBe('john.doe@example.com');
20
+ expect(userInfo.avatarUrlFormat).toBe('https://example.com/avatar/{width}x{height}.jpg');
21
+ });
22
+
23
+ it('should create instance with partial data', () => {
24
+ const userData: YouVersionUserInfoJSON = {
25
+ name: 'Jane',
26
+ id: 'user456',
27
+ };
28
+
29
+ const userInfo = new YouVersionUserInfo(userData);
30
+
31
+ expect(userInfo.name).toBe('Jane');
32
+ expect(userInfo.email).toBeUndefined();
33
+ expect(userInfo.userId).toBe('user456');
34
+ expect(userInfo.avatarUrlFormat).toBeUndefined();
35
+ });
36
+
37
+ it('should create instance with empty object', () => {
38
+ const userData: YouVersionUserInfoJSON = {};
39
+
40
+ const userInfo = new YouVersionUserInfo(userData);
41
+
42
+ expect(userInfo.name).toBeUndefined();
43
+ expect(userInfo.email).toBeUndefined();
44
+ expect(userInfo.userId).toBeUndefined();
45
+ expect(userInfo.avatarUrlFormat).toBeUndefined();
46
+ });
47
+
48
+ it('should throw error for null data', () => {
49
+ expect(() => {
50
+ // @ts-expect-error - Testing invalid input type
51
+ new YouVersionUserInfo(null);
52
+ }).toThrow('Invalid user data provided');
53
+ });
54
+
55
+ it('should throw error for undefined data', () => {
56
+ expect(() => {
57
+ // @ts-expect-error - Testing invalid input type
58
+ new YouVersionUserInfo(undefined);
59
+ }).toThrow('Invalid user data provided');
60
+ });
61
+
62
+ it('should throw error for non-object data', () => {
63
+ expect(() => {
64
+ // @ts-expect-error - Testing invalid input type
65
+ new YouVersionUserInfo('invalid data');
66
+ }).toThrow('Invalid user data provided');
67
+
68
+ expect(() => {
69
+ // @ts-expect-error - Testing invalid input type
70
+ new YouVersionUserInfo(123);
71
+ }).toThrow('Invalid user data provided');
72
+
73
+ expect(() => {
74
+ // @ts-expect-error - Testing invalid input type
75
+ new YouVersionUserInfo(true);
76
+ }).toThrow('Invalid user data provided');
77
+ });
78
+ });
79
+
80
+ describe('getAvatarUrl', () => {
81
+ it('should return null when avatarUrlFormat is undefined', () => {
82
+ const userData: YouVersionUserInfoJSON = {
83
+ name: 'John',
84
+ id: 'user123',
85
+ };
86
+
87
+ const userInfo = new YouVersionUserInfo(userData);
88
+ const avatarUrl = userInfo.getAvatarUrl();
89
+
90
+ expect(avatarUrl).toBeNull();
91
+ });
92
+
93
+ it('should return null when avatarUrlFormat is empty string', () => {
94
+ const userData: YouVersionUserInfoJSON = {
95
+ name: 'John',
96
+ id: 'user123',
97
+ avatar_url: '',
98
+ };
99
+
100
+ const userInfo = new YouVersionUserInfo(userData);
101
+ const avatarUrl = userInfo.getAvatarUrl();
102
+
103
+ expect(avatarUrl).toBeNull();
104
+ });
105
+
106
+ it('should replace width and height placeholders with default values', () => {
107
+ const userData: YouVersionUserInfoJSON = {
108
+ name: 'John',
109
+ avatar_url: 'https://example.com/avatar/{width}x{height}.jpg',
110
+ };
111
+
112
+ const userInfo = new YouVersionUserInfo(userData);
113
+ const avatarUrl = userInfo.getAvatarUrl();
114
+
115
+ expect(avatarUrl).toBeInstanceOf(URL);
116
+ expect(avatarUrl?.toString()).toBe('https://example.com/avatar/200x200.jpg');
117
+ });
118
+
119
+ it('should replace width and height placeholders with custom values', () => {
120
+ const userData: YouVersionUserInfoJSON = {
121
+ name: 'John',
122
+ avatar_url: 'https://example.com/avatar/{width}x{height}.jpg',
123
+ };
124
+
125
+ const userInfo = new YouVersionUserInfo(userData);
126
+ const avatarUrl = userInfo.getAvatarUrl(100, 150);
127
+
128
+ expect(avatarUrl).toBeInstanceOf(URL);
129
+ expect(avatarUrl?.toString()).toBe('https://example.com/avatar/100x150.jpg');
130
+ });
131
+
132
+ it('should handle URL starting with // by adding https:', () => {
133
+ const userData: YouVersionUserInfoJSON = {
134
+ name: 'John',
135
+ avatar_url: '//example.com/avatar/{width}x{height}.jpg',
136
+ };
137
+
138
+ const userInfo = new YouVersionUserInfo(userData);
139
+ const avatarUrl = userInfo.getAvatarUrl(50, 75);
140
+
141
+ expect(avatarUrl).toBeInstanceOf(URL);
142
+ expect(avatarUrl?.toString()).toBe('https://example.com/avatar/50x75.jpg');
143
+ });
144
+
145
+ it('should handle URL without placeholders', () => {
146
+ const userData: YouVersionUserInfoJSON = {
147
+ name: 'John',
148
+ avatar_url: 'https://example.com/static-avatar.jpg',
149
+ };
150
+
151
+ const userInfo = new YouVersionUserInfo(userData);
152
+ const avatarUrl = userInfo.getAvatarUrl(100, 100);
153
+
154
+ expect(avatarUrl).toBeInstanceOf(URL);
155
+ expect(avatarUrl?.toString()).toBe('https://example.com/static-avatar.jpg');
156
+ });
157
+
158
+ it('should handle URL with only width placeholder', () => {
159
+ const userData: YouVersionUserInfoJSON = {
160
+ name: 'John',
161
+ avatar_url: 'https://example.com/avatar/w{width}.jpg',
162
+ };
163
+
164
+ const userInfo = new YouVersionUserInfo(userData);
165
+ const avatarUrl = userInfo.getAvatarUrl(300, 400);
166
+
167
+ expect(avatarUrl).toBeInstanceOf(URL);
168
+ expect(avatarUrl?.toString()).toBe('https://example.com/avatar/w300.jpg');
169
+ });
170
+
171
+ it('should handle URL with only height placeholder', () => {
172
+ const userData: YouVersionUserInfoJSON = {
173
+ name: 'John',
174
+ avatar_url: 'https://example.com/avatar/h{height}.jpg',
175
+ };
176
+
177
+ const userInfo = new YouVersionUserInfo(userData);
178
+ const avatarUrl = userInfo.getAvatarUrl(300, 400);
179
+
180
+ expect(avatarUrl).toBeInstanceOf(URL);
181
+ expect(avatarUrl?.toString()).toBe('https://example.com/avatar/h400.jpg');
182
+ });
183
+
184
+ it('should not handle multiple occurrences of placeholders', () => {
185
+ const userData: YouVersionUserInfoJSON = {
186
+ name: 'John',
187
+ avatar_url: 'https://example.com/avatar/{width}/thumb_{height}_{width}.jpg',
188
+ };
189
+
190
+ const userInfo = new YouVersionUserInfo(userData);
191
+ const avatarUrl = userInfo.getAvatarUrl(120, 80);
192
+
193
+ expect(avatarUrl).toBeInstanceOf(URL);
194
+ // URL constructor encodes curly braces, so {width} becomes %7Bwidth%7D
195
+ expect(avatarUrl?.toString()).toBe('https://example.com/avatar/120/thumb_80_%7Bwidth%7D.jpg');
196
+ });
197
+
198
+ it('should return null for malformed URLs', () => {
199
+ const userData: YouVersionUserInfoJSON = {
200
+ name: 'John',
201
+ avatar_url: 'not-a-valid-url-{width}x{height}',
202
+ };
203
+
204
+ const userInfo = new YouVersionUserInfo(userData);
205
+ const avatarUrl = userInfo.getAvatarUrl();
206
+
207
+ expect(avatarUrl).toBeNull();
208
+ });
209
+
210
+ it('should handle zero width and height', () => {
211
+ const userData: YouVersionUserInfoJSON = {
212
+ name: 'John',
213
+ avatar_url: 'https://example.com/avatar/{width}x{height}.jpg',
214
+ };
215
+
216
+ const userInfo = new YouVersionUserInfo(userData);
217
+ const avatarUrl = userInfo.getAvatarUrl(0, 0);
218
+
219
+ expect(avatarUrl).toBeInstanceOf(URL);
220
+ expect(avatarUrl?.toString()).toBe('https://example.com/avatar/0x0.jpg');
221
+ });
222
+
223
+ it('should handle negative width and height', () => {
224
+ const userData: YouVersionUserInfoJSON = {
225
+ name: 'John',
226
+ avatar_url: 'https://example.com/avatar/{width}x{height}.jpg',
227
+ };
228
+
229
+ const userInfo = new YouVersionUserInfo(userData);
230
+ const avatarUrl = userInfo.getAvatarUrl(-100, -50);
231
+
232
+ expect(avatarUrl).toBeInstanceOf(URL);
233
+ expect(avatarUrl?.toString()).toBe('https://example.com/avatar/-100x-50.jpg');
234
+ });
235
+
236
+ it('should handle large width and height values', () => {
237
+ const userData: YouVersionUserInfoJSON = {
238
+ name: 'John',
239
+ avatar_url: 'https://example.com/avatar/{width}x{height}.jpg',
240
+ };
241
+
242
+ const userInfo = new YouVersionUserInfo(userData);
243
+ const avatarUrl = userInfo.getAvatarUrl(9999, 8888);
244
+
245
+ expect(avatarUrl).toBeInstanceOf(URL);
246
+ expect(avatarUrl?.toString()).toBe('https://example.com/avatar/9999x8888.jpg');
247
+ });
248
+ });
249
+
250
+ describe('avatarUrl getter', () => {
251
+ it('should return same result as getAvatarUrl() with default parameters', () => {
252
+ const userData: YouVersionUserInfoJSON = {
253
+ name: 'John',
254
+ avatar_url: 'https://example.com/avatar/{width}x{height}.jpg',
255
+ };
256
+
257
+ const userInfo = new YouVersionUserInfo(userData);
258
+ const getterResult = userInfo.avatarUrl;
259
+ const methodResult = userInfo.getAvatarUrl();
260
+
261
+ expect(getterResult).toEqual(methodResult);
262
+ expect(getterResult?.toString()).toBe('https://example.com/avatar/200x200.jpg');
263
+ });
264
+
265
+ it('should return null when avatarUrlFormat is undefined', () => {
266
+ const userData: YouVersionUserInfoJSON = {
267
+ name: 'John',
268
+ id: 'user123',
269
+ };
270
+
271
+ const userInfo = new YouVersionUserInfo(userData);
272
+
273
+ expect(userInfo.avatarUrl).toBeNull();
274
+ });
275
+
276
+ it('should return null for malformed URLs', () => {
277
+ const userData: YouVersionUserInfoJSON = {
278
+ name: 'John',
279
+ avatar_url: 'invalid-url-format',
280
+ };
281
+
282
+ const userInfo = new YouVersionUserInfo(userData);
283
+
284
+ expect(userInfo.avatarUrl).toBeNull();
285
+ });
286
+ });
287
+
288
+ describe('edge cases and real-world scenarios', () => {
289
+ it('should handle typical YouVersion API response format', () => {
290
+ const userData: YouVersionUserInfoJSON = {
291
+ id: '12345',
292
+ name: 'Sarah Johnson',
293
+ avatar_url: '//images.youversion.com/users/12345/{width}x{height}/avatar.jpg',
294
+ };
295
+
296
+ const userInfo = new YouVersionUserInfo(userData);
297
+
298
+ expect(userInfo.userId).toBe('12345');
299
+ expect(userInfo.name).toBe('Sarah Johnson');
300
+
301
+ const avatarUrl = userInfo.getAvatarUrl(150, 150);
302
+ expect(avatarUrl?.toString()).toBe(
303
+ 'https://images.youversion.com/users/12345/150x150/avatar.jpg',
304
+ );
305
+ });
306
+
307
+ it('should handle user with single name', () => {
308
+ const userData: YouVersionUserInfoJSON = {
309
+ id: 'user789',
310
+ name: 'Madonna',
311
+ avatar_url: 'https://cdn.example.com/{width}/{height}/user.png',
312
+ };
313
+
314
+ const userInfo = new YouVersionUserInfo(userData);
315
+
316
+ expect(userInfo.name).toBe('Madonna');
317
+ expect(userInfo.userId).toBe('user789');
318
+ expect(userInfo.avatarUrl?.toString()).toBe('https://cdn.example.com/200/200/user.png');
319
+ });
320
+
321
+ it('should handle complex URL with query parameters', () => {
322
+ const userData: YouVersionUserInfoJSON = {
323
+ name: 'Alex',
324
+ avatar_url: 'https://api.service.com/v1/avatar?user=123&w={width}&h={height}&format=jpg',
325
+ };
326
+
327
+ const userInfo = new YouVersionUserInfo(userData);
328
+ const avatarUrl = userInfo.getAvatarUrl(64, 64);
329
+
330
+ expect(avatarUrl?.toString()).toBe(
331
+ 'https://api.service.com/v1/avatar?user=123&w=64&h=64&format=jpg',
332
+ );
333
+ });
334
+
335
+ it('should handle URL with fragment identifier', () => {
336
+ const userData: YouVersionUserInfoJSON = {
337
+ name: 'Taylor',
338
+ avatar_url: 'https://example.com/avatar/{width}x{height}.jpg#section1',
339
+ };
340
+
341
+ const userInfo = new YouVersionUserInfo(userData);
342
+ const avatarUrl = userInfo.getAvatarUrl(128, 128);
343
+
344
+ expect(avatarUrl?.toString()).toBe('https://example.com/avatar/128x128.jpg#section1');
345
+ });
346
+ });
347
+ });