@youversion/platform-core 0.4.1
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/.env.example +7 -0
- package/.env.local +10 -0
- package/.turbo/turbo-build.log +18 -0
- package/CHANGELOG.md +7 -0
- package/LICENSE +201 -0
- package/README.md +369 -0
- package/dist/index.cjs +1330 -0
- package/dist/index.d.cts +737 -0
- package/dist/index.d.ts +737 -0
- package/dist/index.js +1286 -0
- package/package.json +46 -0
- package/src/AuthenticationStrategy.ts +78 -0
- package/src/SignInWithYouVersionResult.ts +53 -0
- package/src/StorageStrategy.ts +81 -0
- package/src/URLBuilder.ts +71 -0
- package/src/Users.ts +137 -0
- package/src/WebAuthenticationStrategy.ts +127 -0
- package/src/YouVersionAPI.ts +27 -0
- package/src/YouVersionPlatformConfiguration.ts +80 -0
- package/src/YouVersionUserInfo.ts +49 -0
- package/src/__tests__/StorageStrategy.test.ts +404 -0
- package/src/__tests__/URLBuilder.test.ts +289 -0
- package/src/__tests__/YouVersionPlatformConfiguration.test.ts +150 -0
- package/src/__tests__/authentication.test.ts +174 -0
- package/src/__tests__/bible.test.ts +356 -0
- package/src/__tests__/client.test.ts +109 -0
- package/src/__tests__/handlers.ts +41 -0
- package/src/__tests__/highlights.test.ts +485 -0
- package/src/__tests__/languages.test.ts +139 -0
- package/src/__tests__/setup.ts +17 -0
- package/src/authentication.ts +27 -0
- package/src/bible.ts +272 -0
- package/src/client.ts +162 -0
- package/src/highlight.ts +16 -0
- package/src/highlights.ts +173 -0
- package/src/index.ts +20 -0
- package/src/languages.ts +80 -0
- package/src/schemas/bible-index.ts +48 -0
- package/src/schemas/book.ts +34 -0
- package/src/schemas/chapter.ts +24 -0
- package/src/schemas/collection.ts +28 -0
- package/src/schemas/highlight.ts +23 -0
- package/src/schemas/index.ts +11 -0
- package/src/schemas/language.ts +38 -0
- package/src/schemas/passage.ts +14 -0
- package/src/schemas/user.ts +10 -0
- package/src/schemas/verse.ts +17 -0
- package/src/schemas/version.ts +31 -0
- package/src/schemas/votd.ts +10 -0
- package/src/types/api-config.ts +9 -0
- package/src/types/auth.ts +15 -0
- package/src/types/book.ts +116 -0
- package/src/types/chapter.ts +5 -0
- package/src/types/highlight.ts +9 -0
- package/src/types/index.ts +22 -0
- package/src/utils/constants.ts +219 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +12 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import { ApiClient } from '../client';
|
|
3
|
+
import { HighlightsClient } from '../highlights';
|
|
4
|
+
import { YouVersionPlatformConfiguration } from '../YouVersionPlatformConfiguration';
|
|
5
|
+
|
|
6
|
+
describe('HighlightsClient', () => {
|
|
7
|
+
let apiClient: ApiClient;
|
|
8
|
+
let highlightsClient: HighlightsClient;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
apiClient = new ApiClient({
|
|
12
|
+
baseUrl: 'https://api-dev.youversion.com',
|
|
13
|
+
appId: 'test-app',
|
|
14
|
+
version: 'v1',
|
|
15
|
+
installationId: 'test-installation',
|
|
16
|
+
});
|
|
17
|
+
highlightsClient = new HighlightsClient(apiClient);
|
|
18
|
+
// Set a default token for tests that don't explicitly pass one
|
|
19
|
+
YouVersionPlatformConfiguration.setAccessToken('test-token');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
// Clean up token after each test
|
|
24
|
+
YouVersionPlatformConfiguration.setAccessToken(null);
|
|
25
|
+
vi.clearAllMocks(); // Reset all mocked calls between tests
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('getHighlights', () => {
|
|
29
|
+
it('should fetch highlights with no options', async () => {
|
|
30
|
+
const highlights = await highlightsClient.getHighlights();
|
|
31
|
+
|
|
32
|
+
expect(highlights.data).toHaveLength(2);
|
|
33
|
+
expect(highlights.data[0]).toEqual({
|
|
34
|
+
version_id: 111,
|
|
35
|
+
passage_id: 'MAT.1.1',
|
|
36
|
+
color: 'fffe00',
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should fetch highlights with version_id option', async () => {
|
|
41
|
+
const highlights = await highlightsClient.getHighlights({ version_id: 1 });
|
|
42
|
+
|
|
43
|
+
expect(highlights.data).toHaveLength(2);
|
|
44
|
+
expect(highlights.data[0]?.version_id).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should fetch highlights with passage_id option', async () => {
|
|
48
|
+
const highlights = await highlightsClient.getHighlights({ passage_id: 'JHN.3.16' });
|
|
49
|
+
|
|
50
|
+
expect(highlights.data).toHaveLength(2);
|
|
51
|
+
expect(highlights.data[0]?.passage_id).toBe('JHN.3.16');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should fetch highlights with both options', async () => {
|
|
55
|
+
const highlights = await highlightsClient.getHighlights({
|
|
56
|
+
version_id: 1,
|
|
57
|
+
passage_id: 'JHN.3.16',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(highlights.data).toHaveLength(2);
|
|
61
|
+
expect(highlights.data[0]?.version_id).toBe(1);
|
|
62
|
+
expect(highlights.data[0]?.passage_id).toBe('JHN.3.16');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should throw an error for invalid version_id', async () => {
|
|
66
|
+
await expect(highlightsClient.getHighlights({ version_id: 0 })).rejects.toThrow(
|
|
67
|
+
'Version ID must be a positive integer',
|
|
68
|
+
);
|
|
69
|
+
await expect(highlightsClient.getHighlights({ version_id: -1 })).rejects.toThrow(
|
|
70
|
+
'Version ID must be a positive integer',
|
|
71
|
+
);
|
|
72
|
+
await expect(highlightsClient.getHighlights({ version_id: 1.5 })).rejects.toThrow();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should throw an error for invalid passage_id', async () => {
|
|
76
|
+
await expect(highlightsClient.getHighlights({ passage_id: '' })).rejects.toThrow(
|
|
77
|
+
'Passage ID must be a non-empty string',
|
|
78
|
+
);
|
|
79
|
+
await expect(highlightsClient.getHighlights({ passage_id: ' ' })).rejects.toThrow(
|
|
80
|
+
'Passage ID must be a non-empty string',
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should include lat parameter when token provided explicitly', async () => {
|
|
85
|
+
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
86
|
+
|
|
87
|
+
const highlights = await highlightsClient.getHighlights({ version_id: 1 }, 'explicit-token');
|
|
88
|
+
|
|
89
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
90
|
+
expect.stringContaining('lat=explicit-token'),
|
|
91
|
+
expect.any(Object),
|
|
92
|
+
);
|
|
93
|
+
expect(highlights.data).toHaveLength(2);
|
|
94
|
+
expect(highlights.data[0]?.version_id).toBe(1);
|
|
95
|
+
|
|
96
|
+
fetchSpy.mockRestore();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should include lat parameter when token auto-retrieved from config', async () => {
|
|
100
|
+
YouVersionPlatformConfiguration.setAccessToken('config-token');
|
|
101
|
+
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
102
|
+
|
|
103
|
+
const highlights = await highlightsClient.getHighlights({ version_id: 1 });
|
|
104
|
+
|
|
105
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
106
|
+
expect.stringContaining('lat=config-token'),
|
|
107
|
+
expect.any(Object),
|
|
108
|
+
);
|
|
109
|
+
expect(highlights.data).toHaveLength(2);
|
|
110
|
+
expect(highlights.data[0]?.version_id).toBe(1);
|
|
111
|
+
|
|
112
|
+
fetchSpy.mockRestore();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should throw an error when no token is available', async () => {
|
|
116
|
+
YouVersionPlatformConfiguration.setAccessToken(null);
|
|
117
|
+
|
|
118
|
+
await expect(highlightsClient.getHighlights({ version_id: 1 })).rejects.toThrow(
|
|
119
|
+
'Authentication required. Please provide a token or sign in before accessing highlights.',
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should use explicit token over config token', async () => {
|
|
124
|
+
YouVersionPlatformConfiguration.setAccessToken('config-token');
|
|
125
|
+
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
126
|
+
const highlights = await highlightsClient.getHighlights({ version_id: 1 }, 'explicit-token');
|
|
127
|
+
|
|
128
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
129
|
+
expect.stringContaining('lat=explicit-token'),
|
|
130
|
+
expect.any(Object),
|
|
131
|
+
);
|
|
132
|
+
expect(highlights.data).toHaveLength(2);
|
|
133
|
+
expect(highlights.data[0]?.version_id).toBe(1);
|
|
134
|
+
|
|
135
|
+
fetchSpy.mockRestore();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('createHighlight', () => {
|
|
140
|
+
it('should create a highlight', async () => {
|
|
141
|
+
const highlight = await highlightsClient.createHighlight({
|
|
142
|
+
version_id: 111,
|
|
143
|
+
passage_id: 'MAT.1.1',
|
|
144
|
+
color: 'fffe00',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(highlight).toEqual({
|
|
148
|
+
version_id: 111,
|
|
149
|
+
passage_id: 'MAT.1.1',
|
|
150
|
+
color: 'fffe00',
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should throw an error for invalid version_id', async () => {
|
|
155
|
+
await expect(
|
|
156
|
+
highlightsClient.createHighlight({
|
|
157
|
+
version_id: 0,
|
|
158
|
+
passage_id: 'MAT.1.1',
|
|
159
|
+
color: 'fffe00',
|
|
160
|
+
}),
|
|
161
|
+
).rejects.toThrow('Version ID must be a positive integer');
|
|
162
|
+
|
|
163
|
+
await expect(
|
|
164
|
+
highlightsClient.createHighlight({
|
|
165
|
+
version_id: -1,
|
|
166
|
+
passage_id: 'MAT.1.1',
|
|
167
|
+
color: 'fffe00',
|
|
168
|
+
}),
|
|
169
|
+
).rejects.toThrow('Version ID must be a positive integer');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should throw an error for invalid passage_id', async () => {
|
|
173
|
+
await expect(
|
|
174
|
+
highlightsClient.createHighlight({
|
|
175
|
+
version_id: 111,
|
|
176
|
+
passage_id: '',
|
|
177
|
+
color: 'fffe00',
|
|
178
|
+
}),
|
|
179
|
+
).rejects.toThrow('Passage ID must be a non-empty string');
|
|
180
|
+
|
|
181
|
+
await expect(
|
|
182
|
+
highlightsClient.createHighlight({
|
|
183
|
+
version_id: 111,
|
|
184
|
+
passage_id: ' ',
|
|
185
|
+
color: 'fffe00',
|
|
186
|
+
}),
|
|
187
|
+
).rejects.toThrow('Passage ID must be a non-empty string');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should throw an error for invalid color', async () => {
|
|
191
|
+
await expect(
|
|
192
|
+
highlightsClient.createHighlight({
|
|
193
|
+
version_id: 111,
|
|
194
|
+
passage_id: 'MAT.1.1',
|
|
195
|
+
color: 'invalid',
|
|
196
|
+
}),
|
|
197
|
+
).rejects.toThrow('Color must be a 6-character hex string without #');
|
|
198
|
+
|
|
199
|
+
await expect(
|
|
200
|
+
highlightsClient.createHighlight({
|
|
201
|
+
version_id: 111,
|
|
202
|
+
passage_id: 'MAT.1.1',
|
|
203
|
+
color: '#fffe00',
|
|
204
|
+
}),
|
|
205
|
+
).rejects.toThrow('Color must be a 6-character hex string without #');
|
|
206
|
+
|
|
207
|
+
await expect(
|
|
208
|
+
highlightsClient.createHighlight({
|
|
209
|
+
version_id: 111,
|
|
210
|
+
passage_id: 'MAT.1.1',
|
|
211
|
+
color: 'fff',
|
|
212
|
+
}),
|
|
213
|
+
).rejects.toThrow('Color must be a 6-character hex string without #');
|
|
214
|
+
|
|
215
|
+
await expect(
|
|
216
|
+
highlightsClient.createHighlight({
|
|
217
|
+
version_id: 111,
|
|
218
|
+
passage_id: 'MAT.1.1',
|
|
219
|
+
color: 'fffe00a',
|
|
220
|
+
}),
|
|
221
|
+
).rejects.toThrow('Color must be a 6-character hex string without #');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should accept valid hex colors', async () => {
|
|
225
|
+
const validColors = ['fffe00', '5dff79', '00d6ff', 'FFC66F', 'ff95ef'];
|
|
226
|
+
|
|
227
|
+
for (const color of validColors) {
|
|
228
|
+
const highlight = await highlightsClient.createHighlight({
|
|
229
|
+
version_id: 111,
|
|
230
|
+
passage_id: 'MAT.1.1',
|
|
231
|
+
color,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(highlight.color).toBe(color);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should accept passage ranges in passage_id', async () => {
|
|
239
|
+
const highlight = await highlightsClient.createHighlight({
|
|
240
|
+
version_id: 111,
|
|
241
|
+
passage_id: 'MAT.1.1-5',
|
|
242
|
+
color: 'fffe00',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(highlight.passage_id).toBe('MAT.1.1-5');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should include lat parameter when token provided explicitly', async () => {
|
|
249
|
+
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
250
|
+
const highlight = await highlightsClient.createHighlight(
|
|
251
|
+
{
|
|
252
|
+
version_id: 111,
|
|
253
|
+
passage_id: 'MAT.1.1',
|
|
254
|
+
color: 'fffe00',
|
|
255
|
+
},
|
|
256
|
+
'explicit-token',
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
260
|
+
expect.stringContaining('lat=explicit-token'),
|
|
261
|
+
expect.any(Object),
|
|
262
|
+
);
|
|
263
|
+
expect(highlight).toEqual({
|
|
264
|
+
version_id: 111,
|
|
265
|
+
passage_id: 'MAT.1.1',
|
|
266
|
+
color: 'fffe00',
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
fetchSpy.mockRestore();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should include lat parameter when token auto-retrieved from config', async () => {
|
|
273
|
+
YouVersionPlatformConfiguration.setAccessToken('config-token');
|
|
274
|
+
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
275
|
+
const highlight = await highlightsClient.createHighlight({
|
|
276
|
+
version_id: 111,
|
|
277
|
+
passage_id: 'MAT.1.1',
|
|
278
|
+
color: 'fffe00',
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
282
|
+
expect.stringContaining('lat=config-token'),
|
|
283
|
+
expect.any(Object),
|
|
284
|
+
);
|
|
285
|
+
expect(highlight).toEqual({
|
|
286
|
+
version_id: 111,
|
|
287
|
+
passage_id: 'MAT.1.1',
|
|
288
|
+
color: 'fffe00',
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
fetchSpy.mockRestore();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should throw an error when no token is available', async () => {
|
|
295
|
+
YouVersionPlatformConfiguration.setAccessToken(null);
|
|
296
|
+
|
|
297
|
+
await expect(
|
|
298
|
+
highlightsClient.createHighlight({
|
|
299
|
+
version_id: 111,
|
|
300
|
+
passage_id: 'MAT.1.1',
|
|
301
|
+
color: 'fffe00',
|
|
302
|
+
}),
|
|
303
|
+
).rejects.toThrow(
|
|
304
|
+
'Authentication required. Please provide a token or sign in before accessing highlights.',
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should use explicit token over config token', async () => {
|
|
309
|
+
YouVersionPlatformConfiguration.setAccessToken('config-token');
|
|
310
|
+
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
311
|
+
const highlight = await highlightsClient.createHighlight(
|
|
312
|
+
{
|
|
313
|
+
version_id: 111,
|
|
314
|
+
passage_id: 'MAT.1.1',
|
|
315
|
+
color: 'fffe00',
|
|
316
|
+
},
|
|
317
|
+
'explicit-token',
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
321
|
+
expect.stringContaining('lat=explicit-token'),
|
|
322
|
+
expect.any(Object),
|
|
323
|
+
);
|
|
324
|
+
expect(highlight).toEqual({
|
|
325
|
+
version_id: 111,
|
|
326
|
+
passage_id: 'MAT.1.1',
|
|
327
|
+
color: 'fffe00',
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
fetchSpy.mockRestore();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('deleteHighlight', () => {
|
|
335
|
+
it('should delete a highlight', async () => {
|
|
336
|
+
let capturedStatus: number | undefined;
|
|
337
|
+
const originalFetch = global.fetch;
|
|
338
|
+
const fetchSpy = vi.spyOn(global, 'fetch').mockImplementation(async (...args) => {
|
|
339
|
+
const response = await originalFetch(...args);
|
|
340
|
+
capturedStatus = response.status;
|
|
341
|
+
return response;
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
await highlightsClient.deleteHighlight('MAT.1.1');
|
|
345
|
+
|
|
346
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
347
|
+
expect.stringContaining('lat=test-token'),
|
|
348
|
+
expect.any(Object),
|
|
349
|
+
);
|
|
350
|
+
expect(capturedStatus).toBe(204);
|
|
351
|
+
|
|
352
|
+
fetchSpy.mockRestore();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should delete a highlight with version_id option', async () => {
|
|
356
|
+
let capturedStatus: number | undefined;
|
|
357
|
+
const originalFetch = global.fetch;
|
|
358
|
+
const fetchSpy = vi.spyOn(global, 'fetch').mockImplementation(async (...args) => {
|
|
359
|
+
const response = await originalFetch(...args);
|
|
360
|
+
capturedStatus = response.status;
|
|
361
|
+
return response;
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await highlightsClient.deleteHighlight('MAT.1.1', { version_id: 111 });
|
|
365
|
+
|
|
366
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
367
|
+
expect.stringContaining('lat=test-token'),
|
|
368
|
+
expect.any(Object),
|
|
369
|
+
);
|
|
370
|
+
expect(capturedStatus).toBe(204);
|
|
371
|
+
|
|
372
|
+
fetchSpy.mockRestore();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should throw an error for invalid passage_id', async () => {
|
|
376
|
+
await expect(highlightsClient.deleteHighlight('')).rejects.toThrow(
|
|
377
|
+
'Passage ID must be a non-empty string',
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
await expect(highlightsClient.deleteHighlight(' ')).rejects.toThrow(
|
|
381
|
+
'Passage ID must be a non-empty string',
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should throw an error for invalid version_id in options', async () => {
|
|
386
|
+
await expect(highlightsClient.deleteHighlight('MAT.1.1', { version_id: 0 })).rejects.toThrow(
|
|
387
|
+
'Version ID must be a positive integer',
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
await expect(highlightsClient.deleteHighlight('MAT.1.1', { version_id: -1 })).rejects.toThrow(
|
|
391
|
+
'Version ID must be a positive integer',
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should accept passage ranges in passage_id', async () => {
|
|
396
|
+
let capturedStatus: number | undefined;
|
|
397
|
+
const originalFetch = global.fetch;
|
|
398
|
+
const fetchSpy = vi.spyOn(global, 'fetch').mockImplementation(async (...args) => {
|
|
399
|
+
const response = await originalFetch(...args);
|
|
400
|
+
capturedStatus = response.status;
|
|
401
|
+
return response;
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
await highlightsClient.deleteHighlight('MAT.1.1-5');
|
|
405
|
+
|
|
406
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
407
|
+
expect.stringContaining('lat=test-token'),
|
|
408
|
+
expect.any(Object),
|
|
409
|
+
);
|
|
410
|
+
expect(capturedStatus).toBe(204);
|
|
411
|
+
|
|
412
|
+
fetchSpy.mockRestore();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should include lat parameter when token provided explicitly', async () => {
|
|
416
|
+
let capturedStatus: number | undefined;
|
|
417
|
+
const originalFetch = global.fetch;
|
|
418
|
+
const fetchSpy = vi.spyOn(global, 'fetch').mockImplementation(async (...args) => {
|
|
419
|
+
const response = await originalFetch(...args);
|
|
420
|
+
capturedStatus = response.status;
|
|
421
|
+
return response;
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
await highlightsClient.deleteHighlight('MAT.1.1', undefined, 'explicit-token');
|
|
425
|
+
|
|
426
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
427
|
+
expect.stringContaining('lat=explicit-token'),
|
|
428
|
+
expect.any(Object),
|
|
429
|
+
);
|
|
430
|
+
expect(capturedStatus).toBe(204);
|
|
431
|
+
|
|
432
|
+
fetchSpy.mockRestore();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should include lat parameter when token auto-retrieved from config', async () => {
|
|
436
|
+
YouVersionPlatformConfiguration.setAccessToken('config-token');
|
|
437
|
+
let capturedStatus: number | undefined;
|
|
438
|
+
const originalFetch = global.fetch;
|
|
439
|
+
const fetchSpy = vi.spyOn(global, 'fetch').mockImplementation(async (...args) => {
|
|
440
|
+
const response = await originalFetch(...args);
|
|
441
|
+
capturedStatus = response.status;
|
|
442
|
+
return response;
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
await highlightsClient.deleteHighlight('MAT.1.1');
|
|
446
|
+
|
|
447
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
448
|
+
expect.stringContaining('lat=config-token'),
|
|
449
|
+
expect.any(Object),
|
|
450
|
+
);
|
|
451
|
+
expect(capturedStatus).toBe(204);
|
|
452
|
+
|
|
453
|
+
fetchSpy.mockRestore();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should throw an error when no token is available', async () => {
|
|
457
|
+
YouVersionPlatformConfiguration.setAccessToken(null);
|
|
458
|
+
|
|
459
|
+
await expect(highlightsClient.deleteHighlight('MAT.1.1')).rejects.toThrow(
|
|
460
|
+
'Authentication required. Please provide a token or sign in before accessing highlights.',
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should use explicit token over config token', async () => {
|
|
465
|
+
YouVersionPlatformConfiguration.setAccessToken('config-token');
|
|
466
|
+
let capturedStatus: number | undefined;
|
|
467
|
+
const originalFetch = global.fetch;
|
|
468
|
+
const fetchSpy = vi.spyOn(global, 'fetch').mockImplementation(async (...args) => {
|
|
469
|
+
const response = await originalFetch(...args);
|
|
470
|
+
capturedStatus = response.status;
|
|
471
|
+
return response;
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
await highlightsClient.deleteHighlight('MAT.1.1', undefined, 'explicit-token');
|
|
475
|
+
|
|
476
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
477
|
+
expect.stringContaining('lat=explicit-token'),
|
|
478
|
+
expect.any(Object),
|
|
479
|
+
);
|
|
480
|
+
expect(capturedStatus).toBe(204);
|
|
481
|
+
|
|
482
|
+
fetchSpy.mockRestore();
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { ApiClient } from '../client';
|
|
3
|
+
import { LanguagesClient } from '../languages';
|
|
4
|
+
import { LanguageSchema } from '../schemas';
|
|
5
|
+
|
|
6
|
+
describe('LanguagesClient', () => {
|
|
7
|
+
let apiClient: ApiClient;
|
|
8
|
+
let languagesClient: LanguagesClient;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
apiClient = new ApiClient({
|
|
12
|
+
baseUrl: 'https://api-dev.youversion.com',
|
|
13
|
+
appId: process.env.YVP_APP_ID || '',
|
|
14
|
+
version: 'v1',
|
|
15
|
+
installationId: 'test-installation',
|
|
16
|
+
});
|
|
17
|
+
languagesClient = new LanguagesClient(apiClient);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('getLanguages', () => {
|
|
21
|
+
it('should fetch languages with required country param', async () => {
|
|
22
|
+
const languages = await languagesClient.getLanguages({ country: 'US' });
|
|
23
|
+
|
|
24
|
+
const { success } = LanguageSchema.safeParse(languages.data[0]);
|
|
25
|
+
expect(success).toBe(true);
|
|
26
|
+
expect(languages.data).toHaveLength(25);
|
|
27
|
+
expect(languages.data[0]).toHaveProperty('countries', expect.arrayContaining(['US']));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should fetch languages with page_size option', async () => {
|
|
31
|
+
const languages = await languagesClient.getLanguages({ country: 'US', page_size: 10 });
|
|
32
|
+
|
|
33
|
+
const { success } = LanguageSchema.safeParse(languages.data[0]);
|
|
34
|
+
expect(success).toBe(true);
|
|
35
|
+
expect(languages.data).toHaveLength(10);
|
|
36
|
+
expect(languages.next_page_token).toBe('eyJzdGFydCI6IDEwfQ==');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should fetch languages with page_token option', async () => {
|
|
40
|
+
const languages = await languagesClient.getLanguages({
|
|
41
|
+
country: 'US',
|
|
42
|
+
page_token: 'eyJzdGFydCI6IDEwMH0=',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const { success } = LanguageSchema.safeParse(languages.data[0]);
|
|
46
|
+
expect(success).toBe(true);
|
|
47
|
+
expect(languages.data).toHaveLength(25);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should uppercase country code', async () => {
|
|
51
|
+
const languages = await languagesClient.getLanguages({ country: 'us' });
|
|
52
|
+
|
|
53
|
+
expect(languages.data[0]?.countries).toContain('US');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should throw an error for invalid country code - empty string', async () => {
|
|
57
|
+
await expect(languagesClient.getLanguages({ country: '' })).rejects.toThrow(
|
|
58
|
+
'Country code must be a 2-character ISO 3166-1 alpha-2 code',
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should throw an error for invalid country code - wrong length', async () => {
|
|
63
|
+
await expect(languagesClient.getLanguages({ country: 'USA' })).rejects.toThrow(
|
|
64
|
+
'Country code must be a 2-character ISO 3166-1 alpha-2 code',
|
|
65
|
+
);
|
|
66
|
+
await expect(languagesClient.getLanguages({ country: 'U' })).rejects.toThrow(
|
|
67
|
+
'Country code must be a 2-character ISO 3166-1 alpha-2 code',
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should throw an error for invalid country code - whitespace', async () => {
|
|
72
|
+
await expect(languagesClient.getLanguages({ country: ' ' })).rejects.toThrow(
|
|
73
|
+
'Country code must be a 2-character ISO 3166-1 alpha-2 code',
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should throw an error for invalid page_size - zero', async () => {
|
|
78
|
+
await expect(languagesClient.getLanguages({ country: 'US', page_size: 0 })).rejects.toThrow();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should throw an error for invalid page_size - negative', async () => {
|
|
82
|
+
await expect(
|
|
83
|
+
languagesClient.getLanguages({ country: 'US', page_size: -1 }),
|
|
84
|
+
).rejects.toThrow();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should throw an error for invalid page_size - non-integer', async () => {
|
|
88
|
+
await expect(
|
|
89
|
+
languagesClient.getLanguages({ country: 'US', page_size: 1.5 }),
|
|
90
|
+
).rejects.toThrow();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('getLanguage', () => {
|
|
95
|
+
it('should fetch a language by ID', async () => {
|
|
96
|
+
const language = await languagesClient.getLanguage('en');
|
|
97
|
+
|
|
98
|
+
const { success } = LanguageSchema.safeParse(language);
|
|
99
|
+
expect(success).toBe(true);
|
|
100
|
+
expect(language.id).toBe('en');
|
|
101
|
+
expect(language.language).toBe('en');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should fetch a language with script', async () => {
|
|
105
|
+
const language = await languagesClient.getLanguage('sr-Latn');
|
|
106
|
+
|
|
107
|
+
const { success } = LanguageSchema.safeParse(language);
|
|
108
|
+
expect(success).toBe(true);
|
|
109
|
+
expect(language.id).toBe('sr-Latn');
|
|
110
|
+
expect(language.language).toBe('sr');
|
|
111
|
+
expect(language.script).toBe('Latn');
|
|
112
|
+
expect(language.script_name).toBe('latinica');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should throw an error for invalid language ID - empty string', async () => {
|
|
116
|
+
await expect(languagesClient.getLanguage('')).rejects.toThrow(
|
|
117
|
+
'Language ID must be a non-empty string',
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should throw an error for invalid language ID - whitespace', async () => {
|
|
122
|
+
await expect(languagesClient.getLanguage(' ')).rejects.toThrow(
|
|
123
|
+
'Language ID must be a non-empty string',
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should throw an error for invalid language ID - invalid format', async () => {
|
|
128
|
+
await expect(languagesClient.getLanguage('invalid-format-123')).rejects.toThrow(
|
|
129
|
+
'Language ID must match BCP 47 format (language or language+script)',
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should throw an error for invalid language ID - uppercase', async () => {
|
|
134
|
+
await expect(languagesClient.getLanguage('EN')).rejects.toThrow(
|
|
135
|
+
'Language ID must match BCP 47 format (language or language+script)',
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { beforeAll, afterEach, afterAll } from 'vitest';
|
|
2
|
+
import { setupServer } from 'msw/node';
|
|
3
|
+
import { handlers } from './handlers';
|
|
4
|
+
|
|
5
|
+
export const server = setupServer(...handlers);
|
|
6
|
+
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
server.listen();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
server.resetHandlers();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterAll(() => {
|
|
16
|
+
server.close();
|
|
17
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ApiClient } from './client';
|
|
2
|
+
import type { User } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Client for authentication-related API calls.
|
|
6
|
+
*/
|
|
7
|
+
export class AuthClient {
|
|
8
|
+
private client: ApiClient;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates an instance of AuthClient.
|
|
12
|
+
* @param client - The ApiClient instance to use for requests.
|
|
13
|
+
*/
|
|
14
|
+
constructor(client: ApiClient) {
|
|
15
|
+
this.client = client;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Retrieves the current authenticated user.
|
|
20
|
+
*
|
|
21
|
+
* @param lat - The long access token (LAT) used for authentication.
|
|
22
|
+
* @returns A promise that resolves to the authenticated User.
|
|
23
|
+
*/
|
|
24
|
+
async getUser(lat: string): Promise<User> {
|
|
25
|
+
return this.client.get<User>(`/auth/me`, { lat: lat });
|
|
26
|
+
}
|
|
27
|
+
}
|