@youversion/platform-core 0.6.0 → 0.7.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,418 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { SignInWithYouVersionPKCEAuthorizationRequestBuilder } from '../SignInWithYouVersionPKCE';
3
+ import { YouVersionPlatformConfiguration } from '../YouVersionPlatformConfiguration';
4
+ import { SignInWithYouVersionPermission } from '../SignInWithYouVersionResult';
5
+ import type { SignInWithYouVersionPermissionValues } from '../types/auth';
6
+ import { setupBrowserMocks, cleanupBrowserMocks } from './mocks/browser';
7
+
8
+ describe('SignInWithYouVersionPKCEAuthorizationRequestBuilder', () => {
9
+ let mocks: ReturnType<typeof setupBrowserMocks>;
10
+
11
+ beforeEach(() => {
12
+ mocks = setupBrowserMocks();
13
+
14
+ // Reset YouVersionPlatformConfiguration
15
+ YouVersionPlatformConfiguration.appKey = 'test-app-key';
16
+ YouVersionPlatformConfiguration.apiHost = 'api-test.youversion.com';
17
+ });
18
+
19
+ afterEach(() => {
20
+ vi.clearAllMocks();
21
+ cleanupBrowserMocks();
22
+ });
23
+
24
+ describe('make', () => {
25
+ it('should generate authorization request with all required parameters', async () => {
26
+ // Mock crypto.getRandomValues to return predictable values
27
+ mocks.crypto.getRandomValues
28
+ .mockImplementationOnce((array: Uint8Array) => {
29
+ // Mock code verifier generation (32 bytes)
30
+ for (let i = 0; i < 32; i++) {
31
+ array[i] = i + 1;
32
+ }
33
+ return array;
34
+ })
35
+ .mockImplementationOnce((array: Uint8Array) => {
36
+ // Mock state generation (24 bytes)
37
+ for (let i = 0; i < 24; i++) {
38
+ array[i] = i + 100;
39
+ }
40
+ return array;
41
+ })
42
+ .mockImplementationOnce((array: Uint8Array) => {
43
+ // Mock nonce generation (24 bytes)
44
+ for (let i = 0; i < 24; i++) {
45
+ array[i] = i + 200;
46
+ }
47
+ return array;
48
+ });
49
+
50
+ // Mock crypto.subtle.digest for code challenge
51
+ const mockDigest = new Uint8Array(32);
52
+ for (let i = 0; i < 32; i++) {
53
+ mockDigest[i] = i + 50;
54
+ }
55
+ mocks.crypto.subtle.digest.mockResolvedValue(mockDigest.buffer);
56
+
57
+ // Mock btoa for base64 encoding
58
+ mocks.btoa
59
+ .mockReturnValueOnce('codeVerifierBase64==') // Code verifier
60
+ .mockReturnValueOnce('codeChallengeBase64==') // Code challenge
61
+ .mockReturnValueOnce('stateBase64==') // State
62
+ .mockReturnValueOnce('nonceBase64=='); // Nonce
63
+
64
+ const permissions = new Set<SignInWithYouVersionPermissionValues>([
65
+ SignInWithYouVersionPermission.bibles,
66
+ SignInWithYouVersionPermission.highlights,
67
+ ]);
68
+ const redirectURL = new URL('https://example.com/callback');
69
+
70
+ const result = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
71
+ 'test-app-key',
72
+ permissions,
73
+ redirectURL,
74
+ );
75
+
76
+ // Verify parameters structure
77
+ expect(result).toHaveProperty('url');
78
+ expect(result).toHaveProperty('parameters');
79
+ expect(result.parameters).toHaveProperty('codeVerifier');
80
+ expect(result.parameters).toHaveProperty('codeChallenge');
81
+ expect(result.parameters).toHaveProperty('state');
82
+ expect(result.parameters).toHaveProperty('nonce');
83
+
84
+ // Verify URL structure
85
+ expect(result.url).toBeInstanceOf(URL);
86
+ expect(result.url.hostname).toBe('api-test.youversion.com');
87
+ expect(result.url.pathname).toBe('/auth/authorize');
88
+ });
89
+
90
+ it('should generate unique parameters on each call', async () => {
91
+ // Mock crypto to return different values for each call
92
+ let callCount = 0;
93
+ mocks.crypto.getRandomValues.mockImplementation((array: Uint8Array) => {
94
+ for (let i = 0; i < array.length; i++) {
95
+ array[i] = callCount + i;
96
+ }
97
+ callCount += 10;
98
+ return array;
99
+ });
100
+
101
+ mocks.crypto.subtle.digest.mockResolvedValue(new Uint8Array(32).buffer);
102
+ mocks.btoa.mockImplementation((str: string) => `base64_${callCount}_${str.length}`);
103
+
104
+ const permissions = new Set<SignInWithYouVersionPermissionValues>();
105
+ const redirectURL = new URL('https://example.com/callback');
106
+
107
+ const result1 = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
108
+ 'app-key',
109
+ permissions,
110
+ redirectURL,
111
+ );
112
+ const result2 = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
113
+ 'app-key',
114
+ permissions,
115
+ redirectURL,
116
+ );
117
+
118
+ // Parameters should be different between calls
119
+ expect(result1.parameters.codeVerifier).not.toBe(result2.parameters.codeVerifier);
120
+ expect(result1.parameters.state).not.toBe(result2.parameters.state);
121
+ expect(result1.parameters.nonce).not.toBe(result2.parameters.nonce);
122
+ });
123
+ });
124
+
125
+ describe('authorizeURL', () => {
126
+ beforeEach(() => {
127
+ // Setup mocks for btoa which are needed for these tests
128
+ mocks.crypto.getRandomValues.mockImplementation((array: Uint8Array) => {
129
+ for (let i = 0; i < array.length; i++) {
130
+ array[i] = i;
131
+ }
132
+ return array;
133
+ });
134
+ mocks.crypto.subtle.digest.mockResolvedValue(new Uint8Array(32).buffer);
135
+ mocks.btoa.mockImplementation((str: string) => Buffer.from(str).toString('base64'));
136
+ });
137
+
138
+ it('should build authorization URL with all required OAuth2 parameters', async () => {
139
+ const permissions = new Set<SignInWithYouVersionPermissionValues>([
140
+ SignInWithYouVersionPermission.bibles,
141
+ ]);
142
+ const redirectURL = new URL('https://example.com/callback');
143
+
144
+ const result = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
145
+ 'test-app-key',
146
+ permissions,
147
+ redirectURL,
148
+ );
149
+
150
+ const url = result.url;
151
+ const params = new URLSearchParams(url.search);
152
+
153
+ // Required OAuth2 parameters
154
+ expect(params.get('response_type')).toBe('code');
155
+ expect(params.get('client_id')).toBe('test-app-key');
156
+ expect(params.get('redirect_uri')).toBe('https://example.com/callback');
157
+ expect(params.get('code_challenge_method')).toBe('S256');
158
+
159
+ // PKCE parameters
160
+ expect(params.get('code_challenge')).toBeTruthy();
161
+ expect(params.get('state')).toBeTruthy();
162
+ expect(params.get('nonce')).toBeTruthy();
163
+ });
164
+
165
+ it('should handle redirect URL with trailing slash', async () => {
166
+ const permissions = new Set<SignInWithYouVersionPermissionValues>();
167
+ const redirectURL = new URL('https://example.com/callback/');
168
+
169
+ const result = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
170
+ 'test-app-key',
171
+ permissions,
172
+ redirectURL,
173
+ );
174
+
175
+ const params = new URLSearchParams(result.url.search);
176
+ expect(params.get('redirect_uri')).toBe('https://example.com/callback');
177
+ });
178
+
179
+ it('should include x-yvp-installation-id param', async () => {
180
+ const permissions = new Set<SignInWithYouVersionPermissionValues>();
181
+ const redirectURL = new URL('https://example.com/callback');
182
+
183
+ const result = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
184
+ 'test-app-key',
185
+ permissions,
186
+ redirectURL,
187
+ );
188
+
189
+ const params = new URLSearchParams(result.url.search);
190
+ expect(params.get('x-yvp-installation-id')).not.toBeFalsy();
191
+ });
192
+
193
+ it('should create scope string with permissions and openid', async () => {
194
+ const permissions = new Set<SignInWithYouVersionPermissionValues>([
195
+ SignInWithYouVersionPermission.bibles,
196
+ SignInWithYouVersionPermission.highlights,
197
+ ]);
198
+ const redirectURL = new URL('https://example.com/callback');
199
+
200
+ const result = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
201
+ 'test-app-key',
202
+ permissions,
203
+ redirectURL,
204
+ );
205
+
206
+ const params = new URLSearchParams(result.url.search);
207
+ const scope = params.get('scope');
208
+
209
+ expect(scope).toContain('bibles');
210
+ expect(scope).toContain('highlights');
211
+ expect(scope).toContain('openid');
212
+ });
213
+
214
+ it('should sort permissions alphabetically', async () => {
215
+ const permissions = new Set<SignInWithYouVersionPermissionValues>([
216
+ SignInWithYouVersionPermission.votd,
217
+ SignInWithYouVersionPermission.bibles,
218
+ SignInWithYouVersionPermission.demographics,
219
+ ]);
220
+ const redirectURL = new URL('https://example.com/callback');
221
+
222
+ const result = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
223
+ 'test-app-key',
224
+ permissions,
225
+ redirectURL,
226
+ );
227
+
228
+ const params = new URLSearchParams(result.url.search);
229
+ const scope = params.get('scope');
230
+
231
+ // Should be sorted: bibles demographics votd openid
232
+ expect(scope).toBe('bibles demographics votd openid');
233
+ });
234
+
235
+ it('should add openid when not present', async () => {
236
+ const permissions = new Set<SignInWithYouVersionPermissionValues>([
237
+ SignInWithYouVersionPermission.bibles,
238
+ ]);
239
+ const redirectURL = new URL('https://example.com/callback');
240
+
241
+ const result = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
242
+ 'test-app-key',
243
+ permissions,
244
+ redirectURL,
245
+ );
246
+
247
+ const params = new URLSearchParams(result.url.search);
248
+ const scope = params.get('scope');
249
+
250
+ expect(scope).toBe('bibles openid');
251
+ });
252
+
253
+ it('should handle empty permissions set', async () => {
254
+ const permissions = new Set<SignInWithYouVersionPermissionValues>();
255
+ const redirectURL = new URL('https://example.com/callback');
256
+
257
+ const result = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
258
+ 'test-app-key',
259
+ permissions,
260
+ redirectURL,
261
+ );
262
+
263
+ const params = new URLSearchParams(result.url.search);
264
+ const scope = params.get('scope');
265
+
266
+ expect(scope).toBe('openid');
267
+ });
268
+
269
+ it('should not duplicate openid if already present', async () => {
270
+ // This test simulates if openid was somehow in the permissions set
271
+ const permissions = new Set<SignInWithYouVersionPermissionValues>([
272
+ 'openid' as SignInWithYouVersionPermissionValues,
273
+ SignInWithYouVersionPermission.bibles,
274
+ ]);
275
+ const redirectURL = new URL('https://example.com/callback');
276
+
277
+ const result = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
278
+ 'test-app-key',
279
+ permissions,
280
+ redirectURL,
281
+ );
282
+
283
+ const params = new URLSearchParams(result.url.search);
284
+ const scope = params.get('scope');
285
+
286
+ // Should not have duplicate openid
287
+ const openidCount = (scope?.match(/openid/g) || []).length;
288
+ expect(openidCount).toBe(1);
289
+ });
290
+ });
291
+
292
+ describe('tokenURLRequest', () => {
293
+ it('should create POST request with correct parameters', async () => {
294
+ const code = 'auth-code-123';
295
+ const codeVerifier = 'code-verifier-456';
296
+ const redirectUri = 'https://example.com/callback';
297
+
298
+ const request = SignInWithYouVersionPKCEAuthorizationRequestBuilder.tokenURLRequest(
299
+ code,
300
+ codeVerifier,
301
+ redirectUri,
302
+ );
303
+
304
+ expect(request.method).toBe('POST');
305
+ expect(request.url).toBe('https://api-test.youversion.com/auth/token');
306
+ expect(request.headers.get('Content-Type')).toBe('application/x-www-form-urlencoded');
307
+
308
+ const body = await request.text();
309
+ const params = new URLSearchParams(body);
310
+
311
+ expect(params.get('grant_type')).toBe('authorization_code');
312
+ expect(params.get('code')).toBe(code);
313
+ expect(params.get('redirect_uri')).toBe(redirectUri);
314
+ expect(params.get('client_id')).toBe('test-app-key');
315
+ expect(params.get('code_verifier')).toBe(codeVerifier);
316
+ });
317
+
318
+ it('should handle empty app key gracefully', async () => {
319
+ YouVersionPlatformConfiguration.appKey = null;
320
+
321
+ const request = SignInWithYouVersionPKCEAuthorizationRequestBuilder.tokenURLRequest(
322
+ 'code',
323
+ 'verifier',
324
+ 'https://example.com/callback',
325
+ );
326
+
327
+ expect(request).toBeInstanceOf(Request);
328
+
329
+ const body = await request.text();
330
+ const params = new URLSearchParams(body);
331
+
332
+ expect(params.get('client_id')).toBe('');
333
+ });
334
+ });
335
+
336
+ describe('randomness and security', () => {
337
+ beforeEach(() => {
338
+ // Setup mocks for btoa which are needed for these tests
339
+ mocks.crypto.getRandomValues.mockImplementation((array: Uint8Array) => {
340
+ for (let i = 0; i < array.length; i++) {
341
+ array[i] = i;
342
+ }
343
+ return array;
344
+ });
345
+ mocks.crypto.subtle.digest.mockResolvedValue(new Uint8Array(32).buffer);
346
+ mocks.btoa.mockImplementation((str: string) => Buffer.from(str).toString('base64'));
347
+ });
348
+
349
+ it('should use crypto.getRandomValues for secure random generation', async () => {
350
+ const permissions = new Set<SignInWithYouVersionPermissionValues>();
351
+ const redirectURL = new URL('https://example.com/callback');
352
+
353
+ await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
354
+ 'test-app-key',
355
+ permissions,
356
+ redirectURL,
357
+ );
358
+
359
+ // Should call crypto.getRandomValues for code verifier, state, and nonce
360
+ expect(mocks.crypto.getRandomValues).toHaveBeenCalledTimes(3);
361
+
362
+ // Verify correct byte lengths
363
+ const calls = mocks.crypto.getRandomValues.mock.calls;
364
+ expect(calls[0]?.[0]).toHaveLength(32); // Code verifier
365
+ expect(calls[1]?.[0]).toHaveLength(24); // State
366
+ expect(calls[2]?.[0]).toHaveLength(24); // Nonce
367
+ });
368
+
369
+ it('should use SHA-256 for code challenge', async () => {
370
+ const permissions = new Set<SignInWithYouVersionPermissionValues>();
371
+ const redirectURL = new URL('https://example.com/callback');
372
+
373
+ await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
374
+ 'test-app-key',
375
+ permissions,
376
+ redirectURL,
377
+ );
378
+
379
+ expect(mocks.crypto.subtle.digest).toHaveBeenCalledWith('SHA-256', expect.any(Uint8Array));
380
+ });
381
+
382
+ it('should generate parameters with sufficient entropy', async () => {
383
+ // Use real crypto for this test to verify actual randomness
384
+ cleanupBrowserMocks();
385
+
386
+ const permissions = new Set<SignInWithYouVersionPermissionValues>();
387
+ const redirectURL = new URL('https://example.com/callback');
388
+
389
+ const result1 = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
390
+ 'test-app-key',
391
+ permissions,
392
+ redirectURL,
393
+ );
394
+ const result2 = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
395
+ 'test-app-key',
396
+ permissions,
397
+ redirectURL,
398
+ );
399
+
400
+ // All parameters should be different between calls
401
+ expect(result1.parameters.codeVerifier).not.toBe(result2.parameters.codeVerifier);
402
+ expect(result1.parameters.codeChallenge).not.toBe(result2.parameters.codeChallenge);
403
+ expect(result1.parameters.state).not.toBe(result2.parameters.state);
404
+ expect(result1.parameters.nonce).not.toBe(result2.parameters.nonce);
405
+
406
+ // Parameters should have reasonable length (base64url encoded)
407
+ expect(result1.parameters.codeVerifier.length).toBeGreaterThan(40);
408
+ expect(result1.parameters.state.length).toBeGreaterThan(30);
409
+ expect(result1.parameters.nonce.length).toBeGreaterThan(30);
410
+
411
+ // Parameters should be URL-safe (no +, /, or =)
412
+ expect(result1.parameters.codeVerifier).not.toMatch(/[+/=]/);
413
+ expect(result1.parameters.codeChallenge).not.toMatch(/[+/=]/);
414
+ expect(result1.parameters.state).not.toMatch(/[+/=]/);
415
+ expect(result1.parameters.nonce).not.toMatch(/[+/=]/);
416
+ });
417
+ });
418
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { SignInWithYouVersionResult } from '../SignInWithYouVersionResult';
3
+
4
+ describe('SignInWithYouVersionResult', () => {
5
+ it('sets all properties properly', () => {
6
+ const fixedDate = new Date(2025, 2, 11, 12, 0, 0);
7
+ vi.setSystemTime(fixedDate);
8
+ const result = new SignInWithYouVersionResult({
9
+ accessToken: 'test-access-token',
10
+ expiresIn: 3600,
11
+ refreshToken: 'test-refresh-token',
12
+ permissions: ['votd', 'bibles'],
13
+ yvpUserId: 'test-user-id',
14
+ name: 'test user',
15
+ profilePicture: 'https://this-is-a-test-picture.com',
16
+ email: 'test@example.com',
17
+ });
18
+
19
+ expect(result.accessToken).toBe('test-access-token');
20
+ expect(result.expiryDate).toStrictEqual(new Date(fixedDate.getTime() + 60 * 60 * 1000));
21
+ expect(result.refreshToken).toBe('test-refresh-token');
22
+ expect(result.permissions).toStrictEqual(['votd', 'bibles']);
23
+ expect(result.yvpUserId).toBe('test-user-id');
24
+ expect(result.name).toBe('test user');
25
+ expect(result.profilePicture).toBe('https://this-is-a-test-picture.com');
26
+ expect(result.email).toBe('test@example.com');
27
+ });
28
+ });
@@ -3,7 +3,6 @@
3
3
  */
4
4
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
5
5
  import { SessionStorageStrategy, MemoryStorageStrategy } from '../StorageStrategy';
6
- import { WebAuthenticationStrategy } from '../WebAuthenticationStrategy';
7
6
 
8
7
  describe('SessionStorageStrategy', () => {
9
8
  let strategy: SessionStorageStrategy;
@@ -291,77 +290,6 @@ describe('MemoryStorageStrategy', () => {
291
290
  });
292
291
  });
293
292
 
294
- describe('WebAuthenticationStrategy - Custom Storage', () => {
295
- beforeEach(() => {
296
- sessionStorage.clear();
297
- });
298
-
299
- afterEach(() => {
300
- sessionStorage.clear();
301
- });
302
-
303
- it('should use SessionStorageStrategy by default', () => {
304
- const strategy = new WebAuthenticationStrategy();
305
-
306
- // The strategy should use sessionStorage internally (we can't access private field directly)
307
- // But we can verify it works by checking that data persists in sessionStorage
308
- // This is an indirect test
309
- expect(strategy).toBeInstanceOf(WebAuthenticationStrategy);
310
- });
311
-
312
- it('should accept custom storage strategy in constructor', () => {
313
- const customStorage = new MemoryStorageStrategy();
314
-
315
- const strategy = new WebAuthenticationStrategy({
316
- storage: customStorage,
317
- });
318
-
319
- expect(strategy).toBeInstanceOf(WebAuthenticationStrategy);
320
- // The strategy should use the custom storage
321
- });
322
-
323
- it('should use custom MemoryStorageStrategy when provided', () => {
324
- const memoryStorage = new MemoryStorageStrategy();
325
-
326
- const strategy = new WebAuthenticationStrategy({
327
- storage: memoryStorage,
328
- });
329
-
330
- expect(strategy).toBeInstanceOf(WebAuthenticationStrategy);
331
- });
332
-
333
- it('should support custom storage implementations', () => {
334
- // Create a custom storage implementation
335
- class CustomStorageStrategy {
336
- private data = new Map<string, string>();
337
-
338
- setItem(key: string, value: string): void {
339
- this.data.set(key, value);
340
- }
341
-
342
- getItem(key: string): string | null {
343
- return this.data.get(key) ?? null;
344
- }
345
-
346
- removeItem(key: string): void {
347
- this.data.delete(key);
348
- }
349
-
350
- clear(): void {
351
- this.data.clear();
352
- }
353
- }
354
-
355
- const customStorage = new CustomStorageStrategy();
356
-
357
- const strategy = new WebAuthenticationStrategy({
358
- storage: customStorage,
359
- });
360
-
361
- expect(strategy).toBeInstanceOf(WebAuthenticationStrategy);
362
- });
363
- });
364
-
365
293
  describe('StorageStrategy Interface Compliance', () => {
366
294
  const testCases = [
367
295
  { name: 'SessionStorageStrategy', factory: () => new SessionStorageStrategy() },
@@ -154,79 +154,6 @@ describe('URLBuilder - Input Validation', () => {
154
154
  });
155
155
  });
156
156
 
157
- describe('userURL - accessToken validation', () => {
158
- it('should throw error for empty string accessToken', () => {
159
- expect(() => {
160
- URLBuilder.userURL('');
161
- }).toThrow('accessToken must be a non-empty string');
162
- });
163
-
164
- it('should throw error for whitespace-only accessToken', () => {
165
- expect(() => {
166
- URLBuilder.userURL(' ');
167
- }).toThrow('accessToken must be a non-empty string');
168
- });
169
-
170
- it('should throw error for tab/newline-only accessToken', () => {
171
- expect(() => {
172
- URLBuilder.userURL('\t\n ');
173
- }).toThrow('accessToken must be a non-empty string');
174
- });
175
-
176
- it('should throw descriptive error message', () => {
177
- try {
178
- URLBuilder.userURL('');
179
- expect.fail('Should have thrown an error');
180
- } catch (error) {
181
- expect(error).toBeInstanceOf(Error);
182
- expect((error as Error).message).toContain('accessToken');
183
- expect((error as Error).message).toContain('non-empty string');
184
- }
185
- });
186
-
187
- it('should accept valid non-empty accessToken', () => {
188
- const url = URLBuilder.userURL('valid-access-token-123');
189
-
190
- expect(url).toBeInstanceOf(URL);
191
- expect(url.hostname.endsWith('.youversion.com')).toBe(true);
192
- expect(url.pathname).toBe('/auth/me');
193
- expect(url.searchParams.get('lat')).toBe('valid-access-token-123');
194
- });
195
-
196
- it('should accept accessToken with special characters', () => {
197
- const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test.signature';
198
- const url = URLBuilder.userURL(token);
199
-
200
- expect(url.searchParams.get('lat')).toBe(token);
201
- });
202
- });
203
-
204
- describe('userURL - URL construction', () => {
205
- it('should construct correct base URL and pathname', () => {
206
- const url = URLBuilder.userURL('test-token');
207
-
208
- expect(url.protocol).toBe('https:');
209
- expect(url.hostname.endsWith('.youversion.com')).toBe(true);
210
- expect(url.pathname).toBe('/auth/me');
211
- });
212
-
213
- it('should include access token in lat query parameter', () => {
214
- const token = 'my-access-token-abc123';
215
- const url = URLBuilder.userURL(token);
216
-
217
- expect(url.searchParams.get('lat')).toBe(token);
218
- });
219
-
220
- it('should properly encode special characters in token', () => {
221
- const tokenWithSpecialChars = 'token+with/special=chars';
222
- const url = URLBuilder.userURL(tokenWithSpecialChars);
223
-
224
- // URLSearchParams automatically encodes special characters
225
- expect(url.searchParams.get('lat')).toBe(tokenWithSpecialChars);
226
- expect(url.toString()).toContain('token%2Bwith%2Fspecial%3Dchars');
227
- });
228
- });
229
-
230
157
  describe('Error handling', () => {
231
158
  it('should throw errors instead of returning null for invalid appKey', () => {
232
159
  // Verify that the method throws, not returns null
@@ -243,20 +170,6 @@ describe('URLBuilder - Input Validation', () => {
243
170
  expect(returnValue).toBeUndefined();
244
171
  });
245
172
 
246
- it('should throw errors instead of returning null for invalid accessToken', () => {
247
- let threwError = false;
248
- let returnValue: any;
249
-
250
- try {
251
- returnValue = URLBuilder.userURL('');
252
- } catch {
253
- threwError = true;
254
- }
255
-
256
- expect(threwError).toBe(true);
257
- expect(returnValue).toBeUndefined();
258
- });
259
-
260
173
  it('should wrap URL construction errors with descriptive message for authURL', () => {
261
174
  // This is hard to trigger, but we can at least verify the pattern exists
262
175
  // by checking that valid inputs don't trigger the catch block
@@ -264,12 +177,6 @@ describe('URLBuilder - Input Validation', () => {
264
177
  URLBuilder.authURL('valid-app-key');
265
178
  }).not.toThrow(/Failed to construct auth URL/);
266
179
  });
267
-
268
- it('should wrap URL construction errors with descriptive message for userURL', () => {
269
- expect(() => {
270
- URLBuilder.userURL('valid-token');
271
- }).not.toThrow(/Failed to construct user URL/);
272
- });
273
180
  });
274
181
 
275
182
  describe('Return type validation', () => {
@@ -279,12 +186,5 @@ describe('URLBuilder - Input Validation', () => {
279
186
  expect(result).toBeInstanceOf(URL);
280
187
  expect(result).not.toBeNull();
281
188
  });
282
-
283
- it('userURL should return URL object (not null)', () => {
284
- const result = URLBuilder.userURL('test-token');
285
-
286
- expect(result).toBeInstanceOf(URL);
287
- expect(result).not.toBeNull();
288
- });
289
189
  });
290
190
  });