@syra.fm/sdk 0.9.0 → 0.10.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/lib/commonjs/client.js +236 -7
- package/lib/commonjs/client.js.map +1 -1
- package/lib/commonjs/index.js +36 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/live/components/CreateRoomSheet.js +5 -5
- package/lib/commonjs/live/components/CreateRoomSheet.js.map +1 -1
- package/lib/commonjs/live/components/RecordingsPanel.js +9 -9
- package/lib/commonjs/live/components/RecordingsPanel.js.map +1 -1
- package/lib/commonjs/live/validation.js +44 -15
- package/lib/commonjs/live/validation.js.map +1 -1
- package/lib/commonjs/schema.js +80 -9
- package/lib/commonjs/schema.js.map +1 -1
- package/lib/module/client.js +237 -8
- package/lib/module/client.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/live/components/CreateRoomSheet.js +5 -5
- package/lib/module/live/components/CreateRoomSheet.js.map +1 -1
- package/lib/module/live/components/RecordingsPanel.js +9 -9
- package/lib/module/live/components/RecordingsPanel.js.map +1 -1
- package/lib/module/live/validation.js +44 -15
- package/lib/module/live/validation.js.map +1 -1
- package/lib/module/schema.js +79 -8
- package/lib/module/schema.js.map +1 -1
- package/lib/typescript/commonjs/client.d.ts +159 -2
- package/lib/typescript/commonjs/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/live/components/RoomCard.d.ts +1 -1
- package/lib/typescript/commonjs/live/components/RoomCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/live/validation.d.ts +17 -20
- package/lib/typescript/commonjs/live/validation.d.ts.map +1 -1
- package/lib/typescript/commonjs/schema.d.ts +97 -7
- package/lib/typescript/commonjs/schema.d.ts.map +1 -1
- package/lib/typescript/module/client.d.ts +159 -2
- package/lib/typescript/module/client.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/live/components/RoomCard.d.ts +1 -1
- package/lib/typescript/module/live/components/RoomCard.d.ts.map +1 -1
- package/lib/typescript/module/live/validation.d.ts +17 -20
- package/lib/typescript/module/live/validation.d.ts.map +1 -1
- package/lib/typescript/module/schema.d.ts +97 -7
- package/lib/typescript/module/schema.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/client.ts +418 -10
- package/src/index.ts +17 -0
- package/src/live/components/CreateRoomSheet.tsx +5 -5
- package/src/live/components/RecordingsPanel.tsx +9 -9
- package/src/live/components/RoomCard.tsx +1 -1
- package/src/live/validation.ts +41 -15
- package/src/schema.ts +90 -7
- package/src/client.test.ts +0 -632
package/src/client.test.ts
DELETED
|
@@ -1,632 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'bun:test';
|
|
2
|
-
import {
|
|
3
|
-
createSyraClient,
|
|
4
|
-
SyraApiError,
|
|
5
|
-
DEFAULT_SYRA_BASE_URL,
|
|
6
|
-
DEFAULT_SYRA_WEB_BASE_URL,
|
|
7
|
-
} from './index';
|
|
8
|
-
|
|
9
|
-
// ── Fixtures ──────────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
function makeTrack(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
12
|
-
return {
|
|
13
|
-
id: '507f1f77bcf86cd799439011',
|
|
14
|
-
title: 'Test Track',
|
|
15
|
-
artistId: 'artist-1',
|
|
16
|
-
artistName: 'Test Artist',
|
|
17
|
-
duration: 180,
|
|
18
|
-
isExplicit: false,
|
|
19
|
-
isAvailable: true,
|
|
20
|
-
source: 'upload',
|
|
21
|
-
status: 'ready',
|
|
22
|
-
createdAt: '2026-01-01T00:00:00.000Z',
|
|
23
|
-
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
24
|
-
previewAvailable: true,
|
|
25
|
-
...overrides,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function makePodcast(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
30
|
-
return {
|
|
31
|
-
id: '507f1f77bcf86cd799439021',
|
|
32
|
-
title: 'Test Show',
|
|
33
|
-
author: 'Test Publisher',
|
|
34
|
-
description: 'A show about testing.',
|
|
35
|
-
image: '507f1f77bcf86cd799439022',
|
|
36
|
-
explicit: false,
|
|
37
|
-
type: 'episodic',
|
|
38
|
-
source: 'rss',
|
|
39
|
-
refreshIntervalMin: 60,
|
|
40
|
-
episodeCount: 12,
|
|
41
|
-
status: 'active',
|
|
42
|
-
createdAt: '2026-01-01T00:00:00.000Z',
|
|
43
|
-
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
44
|
-
...overrides,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function makeEpisode(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
49
|
-
return {
|
|
50
|
-
id: '507f1f77bcf86cd799439031',
|
|
51
|
-
podcastId: '507f1f77bcf86cd799439021',
|
|
52
|
-
podcastTitle: 'Test Show',
|
|
53
|
-
title: 'Test Episode',
|
|
54
|
-
description: 'An episode about testing.',
|
|
55
|
-
guid: 'guid-1',
|
|
56
|
-
enclosureUrl: 'https://api.fastcast.ai/audio/guid-1.mp3',
|
|
57
|
-
enclosureType: 'audio/mpeg',
|
|
58
|
-
enclosureLength: 12_345_678,
|
|
59
|
-
duration: 1800,
|
|
60
|
-
pubDate: '2026-01-01T00:00:00.000Z',
|
|
61
|
-
episodeType: 'full',
|
|
62
|
-
image: '507f1f77bcf86cd799439032',
|
|
63
|
-
imageSourceUrl: 'https://cdn.example.com/episode.jpg',
|
|
64
|
-
status: 'ready',
|
|
65
|
-
createdAt: '2026-01-01T00:00:00.000Z',
|
|
66
|
-
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
67
|
-
...overrides,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface FetchCall {
|
|
72
|
-
url: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function fakeFetch(
|
|
76
|
-
handler: (url: string) => { status?: number; body: unknown },
|
|
77
|
-
): { fetch: typeof fetch; calls: FetchCall[] } {
|
|
78
|
-
const calls: FetchCall[] = [];
|
|
79
|
-
const fetchImpl = (async (input: string | URL | Request) => {
|
|
80
|
-
const url = typeof input === 'string' ? input : input.toString();
|
|
81
|
-
calls.push({ url });
|
|
82
|
-
const { status = 200, body } = handler(url);
|
|
83
|
-
return {
|
|
84
|
-
ok: status >= 200 && status < 300,
|
|
85
|
-
status,
|
|
86
|
-
statusText: status === 200 ? 'OK' : 'Error',
|
|
87
|
-
json: async () => body,
|
|
88
|
-
} as Response;
|
|
89
|
-
}) as typeof fetch;
|
|
90
|
-
return { fetch: fetchImpl, calls };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ── searchTracks ──────────────────────────────────────────────────────────────
|
|
94
|
-
|
|
95
|
-
describe('createSyraClient.searchTracks', () => {
|
|
96
|
-
it('calls /api/search with category=tracks and the limit, returns a page of preview-available tracks', async () => {
|
|
97
|
-
const { fetch, calls } = fakeFetch(() => ({
|
|
98
|
-
body: {
|
|
99
|
-
results: {
|
|
100
|
-
tracks: [
|
|
101
|
-
makeTrack({ id: '507f1f77bcf86cd799439011', previewAvailable: true }),
|
|
102
|
-
makeTrack({ id: '507f1f77bcf86cd799439012', previewAvailable: false }),
|
|
103
|
-
],
|
|
104
|
-
},
|
|
105
|
-
hasMore: false,
|
|
106
|
-
limit: 10,
|
|
107
|
-
offset: 0,
|
|
108
|
-
},
|
|
109
|
-
}));
|
|
110
|
-
|
|
111
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
|
|
112
|
-
const page = await client.searchTracks('hello', { limit: 10 });
|
|
113
|
-
|
|
114
|
-
expect(page.items).toHaveLength(1);
|
|
115
|
-
expect(page.items[0].id).toBe('507f1f77bcf86cd799439011');
|
|
116
|
-
expect(page.hasMore).toBe(false);
|
|
117
|
-
expect(page.limit).toBe(10);
|
|
118
|
-
expect(page.offset).toBe(0);
|
|
119
|
-
|
|
120
|
-
expect(calls).toHaveLength(1);
|
|
121
|
-
const url = new URL(calls[0].url);
|
|
122
|
-
expect(url.pathname).toBe('/api/search');
|
|
123
|
-
expect(url.searchParams.get('q')).toBe('hello');
|
|
124
|
-
expect(url.searchParams.get('category')).toBe('tracks');
|
|
125
|
-
expect(url.searchParams.get('limit')).toBe('10');
|
|
126
|
-
expect(url.searchParams.has('offset')).toBe(false);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('sends the offset param and reports hasMore/offset/limit from the backend', async () => {
|
|
130
|
-
const { fetch, calls } = fakeFetch(() => ({
|
|
131
|
-
body: {
|
|
132
|
-
results: { tracks: [makeTrack()] },
|
|
133
|
-
hasMore: true,
|
|
134
|
-
limit: 20,
|
|
135
|
-
offset: 40,
|
|
136
|
-
},
|
|
137
|
-
}));
|
|
138
|
-
|
|
139
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
|
|
140
|
-
const page = await client.searchTracks('hello', { limit: 20, offset: 40 });
|
|
141
|
-
|
|
142
|
-
expect(page.items).toHaveLength(1);
|
|
143
|
-
expect(page.hasMore).toBe(true);
|
|
144
|
-
expect(page.limit).toBe(20);
|
|
145
|
-
expect(page.offset).toBe(40);
|
|
146
|
-
|
|
147
|
-
const url = new URL(calls[0].url);
|
|
148
|
-
expect(url.searchParams.get('offset')).toBe('40');
|
|
149
|
-
expect(url.searchParams.get('limit')).toBe('20');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('reports backend hasMore even when client-side preview filtering empties the page', async () => {
|
|
153
|
-
const { fetch } = fakeFetch(() => ({
|
|
154
|
-
body: {
|
|
155
|
-
results: {
|
|
156
|
-
tracks: [
|
|
157
|
-
makeTrack({ previewAvailable: false }),
|
|
158
|
-
makeTrack({ previewAvailable: false }),
|
|
159
|
-
],
|
|
160
|
-
},
|
|
161
|
-
hasMore: true,
|
|
162
|
-
limit: 2,
|
|
163
|
-
offset: 0,
|
|
164
|
-
},
|
|
165
|
-
}));
|
|
166
|
-
|
|
167
|
-
const client = createSyraClient({ fetch });
|
|
168
|
-
const page = await client.searchTracks('x', { limit: 2 });
|
|
169
|
-
|
|
170
|
-
// All rows were filtered out, but the page is NOT the last one.
|
|
171
|
-
expect(page.items).toHaveLength(0);
|
|
172
|
-
expect(page.hasMore).toBe(true);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('drops malformed rows without throwing', async () => {
|
|
176
|
-
const { fetch } = fakeFetch(() => ({
|
|
177
|
-
body: {
|
|
178
|
-
results: {
|
|
179
|
-
tracks: [
|
|
180
|
-
{ id: 'broken' }, // missing required fields → safeParse fails
|
|
181
|
-
makeTrack({ previewAvailable: true }),
|
|
182
|
-
],
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
}));
|
|
186
|
-
|
|
187
|
-
const client = createSyraClient({ fetch });
|
|
188
|
-
const page = await client.searchTracks('x');
|
|
189
|
-
expect(page.items).toHaveLength(1);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('returns an empty page with hasMore=false when results.tracks is absent', async () => {
|
|
193
|
-
const { fetch } = fakeFetch(() => ({ body: {} }));
|
|
194
|
-
const client = createSyraClient({ fetch });
|
|
195
|
-
const page = await client.searchTracks('x');
|
|
196
|
-
expect(page.items).toEqual([]);
|
|
197
|
-
expect(page.hasMore).toBe(false);
|
|
198
|
-
expect(page.offset).toBe(0);
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// ── getTrack ──────────────────────────────────────────────────────────────────
|
|
203
|
-
|
|
204
|
-
describe('createSyraClient.getTrack', () => {
|
|
205
|
-
it('fetches /api/tracks/:id and validates the response', async () => {
|
|
206
|
-
const { fetch, calls } = fakeFetch(() => ({ body: makeTrack() }));
|
|
207
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
|
|
208
|
-
|
|
209
|
-
const track = await client.getTrack('507f1f77bcf86cd799439011');
|
|
210
|
-
expect(track.title).toBe('Test Track');
|
|
211
|
-
expect(calls[0].url).toBe('https://api.example.test/api/tracks/507f1f77bcf86cd799439011');
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('throws SyraApiError on a non-2xx response', async () => {
|
|
215
|
-
const { fetch } = fakeFetch(() => ({ status: 404, body: { error: 'not found' } }));
|
|
216
|
-
const client = createSyraClient({ fetch });
|
|
217
|
-
|
|
218
|
-
await expect(client.getTrack('507f1f77bcf86cd799439011')).rejects.toBeInstanceOf(SyraApiError);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('throws when the response fails schema validation', async () => {
|
|
222
|
-
const { fetch } = fakeFetch(() => ({ body: { id: 'x' } }));
|
|
223
|
-
const client = createSyraClient({ fetch });
|
|
224
|
-
await expect(client.getTrack('x')).rejects.toThrow();
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// ── previewUrl ────────────────────────────────────────────────────────────────
|
|
229
|
-
|
|
230
|
-
describe('createSyraClient.previewUrl', () => {
|
|
231
|
-
it('builds the preview URL with a default start of 0', () => {
|
|
232
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test' });
|
|
233
|
-
expect(client.previewUrl('abc')).toBe('https://api.example.test/api/preview/abc.mp3?start=0');
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('uses the provided start offset and clamps to an integer >= 0', () => {
|
|
237
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test' });
|
|
238
|
-
expect(client.previewUrl('abc', 42.9)).toBe('https://api.example.test/api/preview/abc.mp3?start=42');
|
|
239
|
-
expect(client.previewUrl('abc', -5)).toBe('https://api.example.test/api/preview/abc.mp3?start=0');
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('defaults to the production base URL', () => {
|
|
243
|
-
const client = createSyraClient();
|
|
244
|
-
expect(client.previewUrl('abc')).toBe(`${DEFAULT_SYRA_BASE_URL}/api/preview/abc.mp3?start=0`);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// ── artworkUrl ────────────────────────────────────────────────────────────────
|
|
249
|
-
|
|
250
|
-
describe('createSyraClient.artworkUrl', () => {
|
|
251
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test' });
|
|
252
|
-
|
|
253
|
-
it('resolves a bare ObjectId string to an absolute images URL', () => {
|
|
254
|
-
expect(client.artworkUrl('507f1f77bcf86cd799439011')).toBe(
|
|
255
|
-
'https://api.example.test/api/images/507f1f77bcf86cd799439011',
|
|
256
|
-
);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it('prefixes a relative /api/images path', () => {
|
|
260
|
-
expect(client.artworkUrl('/api/images/507f1f77bcf86cd799439011')).toBe(
|
|
261
|
-
'https://api.example.test/api/images/507f1f77bcf86cd799439011',
|
|
262
|
-
);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it('passes through an absolute http(s) URL', () => {
|
|
266
|
-
expect(client.artworkUrl('https://cdn.example.com/x.jpg')).toBe('https://cdn.example.com/x.jpg');
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('prefers a named size from coverArtSizes', () => {
|
|
270
|
-
const url = client.artworkUrl(
|
|
271
|
-
{
|
|
272
|
-
coverArt: '/api/images/507f1f77bcf86cd799439011',
|
|
273
|
-
coverArtSizes: {
|
|
274
|
-
large: {
|
|
275
|
-
id: '507f1f77bcf86cd799439012',
|
|
276
|
-
url: '/api/images/507f1f77bcf86cd799439012',
|
|
277
|
-
width: 600,
|
|
278
|
-
height: 600,
|
|
279
|
-
},
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
'large',
|
|
283
|
-
);
|
|
284
|
-
expect(url).toBe('https://api.example.test/api/images/507f1f77bcf86cd799439012');
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('falls back to coverArt when the requested size is missing', () => {
|
|
288
|
-
const url = client.artworkUrl(
|
|
289
|
-
{ coverArt: '/api/images/507f1f77bcf86cd799439011', coverArtSizes: {} },
|
|
290
|
-
'large',
|
|
291
|
-
);
|
|
292
|
-
expect(url).toBe('https://api.example.test/api/images/507f1f77bcf86cd799439011');
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('returns undefined when nothing resolvable is present', () => {
|
|
296
|
-
expect(client.artworkUrl({})).toBeUndefined();
|
|
297
|
-
expect(client.artworkUrl('not-an-id')).toBeUndefined();
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
// ── searchPodcasts ──────────────────────────────────────────────────────────────
|
|
302
|
-
|
|
303
|
-
describe('createSyraClient.searchPodcasts', () => {
|
|
304
|
-
it('calls /api/podcasts/search with q and limit, returns a page of parsed shows', async () => {
|
|
305
|
-
const { fetch, calls } = fakeFetch(() => ({
|
|
306
|
-
body: {
|
|
307
|
-
data: [
|
|
308
|
-
makePodcast({ id: '507f1f77bcf86cd799439021' }),
|
|
309
|
-
makePodcast({ id: '507f1f77bcf86cd799439023', title: 'Second Show' }),
|
|
310
|
-
],
|
|
311
|
-
hasMore: false,
|
|
312
|
-
limit: 5,
|
|
313
|
-
offset: 0,
|
|
314
|
-
},
|
|
315
|
-
}));
|
|
316
|
-
|
|
317
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
|
|
318
|
-
const page = await client.searchPodcasts('news', { limit: 5 });
|
|
319
|
-
|
|
320
|
-
expect(page.items).toHaveLength(2);
|
|
321
|
-
expect(page.items[0].id).toBe('507f1f77bcf86cd799439021');
|
|
322
|
-
expect(page.items[0].author).toBe('Test Publisher');
|
|
323
|
-
expect(page.hasMore).toBe(false);
|
|
324
|
-
expect(page.limit).toBe(5);
|
|
325
|
-
expect(page.offset).toBe(0);
|
|
326
|
-
|
|
327
|
-
expect(calls).toHaveLength(1);
|
|
328
|
-
const url = new URL(calls[0].url);
|
|
329
|
-
expect(url.pathname).toBe('/api/podcasts/search');
|
|
330
|
-
expect(url.searchParams.get('q')).toBe('news');
|
|
331
|
-
expect(url.searchParams.get('limit')).toBe('5');
|
|
332
|
-
expect(url.searchParams.has('offset')).toBe(false);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
it('sends the offset param and reports hasMore/offset from the backend', async () => {
|
|
336
|
-
const { fetch, calls } = fakeFetch(() => ({
|
|
337
|
-
body: { data: [makePodcast()], hasMore: true, limit: 10, offset: 20 },
|
|
338
|
-
}));
|
|
339
|
-
|
|
340
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
|
|
341
|
-
const page = await client.searchPodcasts('news', { limit: 10, offset: 20 });
|
|
342
|
-
|
|
343
|
-
expect(page.hasMore).toBe(true);
|
|
344
|
-
expect(page.limit).toBe(10);
|
|
345
|
-
expect(page.offset).toBe(20);
|
|
346
|
-
|
|
347
|
-
const url = new URL(calls[0].url);
|
|
348
|
-
expect(url.searchParams.get('offset')).toBe('20');
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it('omits the limit and offset params when not provided', async () => {
|
|
352
|
-
const { fetch, calls } = fakeFetch(() => ({ body: { data: [makePodcast()] } }));
|
|
353
|
-
const client = createSyraClient({ fetch });
|
|
354
|
-
await client.searchPodcasts('news');
|
|
355
|
-
const url = new URL(calls[0].url);
|
|
356
|
-
expect(url.searchParams.has('limit')).toBe(false);
|
|
357
|
-
expect(url.searchParams.has('offset')).toBe(false);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it('drops malformed rows without throwing', async () => {
|
|
361
|
-
const { fetch } = fakeFetch(() => ({
|
|
362
|
-
body: { data: [{ id: 'broken' }, makePodcast()] },
|
|
363
|
-
}));
|
|
364
|
-
const client = createSyraClient({ fetch });
|
|
365
|
-
const page = await client.searchPodcasts('x');
|
|
366
|
-
expect(page.items).toHaveLength(1);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it('returns an empty page with hasMore=false when data is absent', async () => {
|
|
370
|
-
const { fetch } = fakeFetch(() => ({ body: {} }));
|
|
371
|
-
const client = createSyraClient({ fetch });
|
|
372
|
-
const page = await client.searchPodcasts('x');
|
|
373
|
-
expect(page.items).toEqual([]);
|
|
374
|
-
expect(page.hasMore).toBe(false);
|
|
375
|
-
expect(page.offset).toBe(0);
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
// ── getPodcast ──────────────────────────────────────────────────────────────────
|
|
380
|
-
|
|
381
|
-
describe('createSyraClient.getPodcast', () => {
|
|
382
|
-
it('fetches /api/podcasts/:id and validates data.podcast', async () => {
|
|
383
|
-
const { fetch, calls } = fakeFetch(() => ({
|
|
384
|
-
body: { data: { podcast: makePodcast(), episodes: [], persons: [] } },
|
|
385
|
-
}));
|
|
386
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
|
|
387
|
-
|
|
388
|
-
const podcast = await client.getPodcast('507f1f77bcf86cd799439021');
|
|
389
|
-
expect(podcast.title).toBe('Test Show');
|
|
390
|
-
expect(calls[0].url).toBe('https://api.example.test/api/podcasts/507f1f77bcf86cd799439021');
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it('throws SyraApiError on a non-2xx response', async () => {
|
|
394
|
-
const { fetch } = fakeFetch(() => ({ status: 404, body: { error: 'not found' } }));
|
|
395
|
-
const client = createSyraClient({ fetch });
|
|
396
|
-
await expect(client.getPodcast('507f1f77bcf86cd799439021')).rejects.toBeInstanceOf(SyraApiError);
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
it('throws when data.podcast fails schema validation', async () => {
|
|
400
|
-
const { fetch } = fakeFetch(() => ({ body: { data: { podcast: { id: 'x' } } } }));
|
|
401
|
-
const client = createSyraClient({ fetch });
|
|
402
|
-
await expect(client.getPodcast('x')).rejects.toThrow();
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
// ── podcastUrl ────────────────────────────────────────────────────────────────
|
|
407
|
-
|
|
408
|
-
describe('createSyraClient.podcastUrl', () => {
|
|
409
|
-
it('builds the web deep link from the web base URL, not the API host', () => {
|
|
410
|
-
const client = createSyraClient({
|
|
411
|
-
baseURL: 'https://api.example.test',
|
|
412
|
-
webBaseURL: 'https://web.example.test',
|
|
413
|
-
});
|
|
414
|
-
expect(client.podcastUrl('507f1f77bcf86cd799439021')).toBe(
|
|
415
|
-
'https://web.example.test/podcasts/507f1f77bcf86cd799439021',
|
|
416
|
-
);
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
it('defaults to the production web base URL', () => {
|
|
420
|
-
const client = createSyraClient();
|
|
421
|
-
expect(client.podcastUrl('abc')).toBe(`${DEFAULT_SYRA_WEB_BASE_URL}/podcasts/abc`);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it('does not use the API base URL for the deep link', () => {
|
|
425
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test' });
|
|
426
|
-
expect(client.podcastUrl('abc')).toBe(`${DEFAULT_SYRA_WEB_BASE_URL}/podcasts/abc`);
|
|
427
|
-
expect(DEFAULT_SYRA_WEB_BASE_URL).not.toBe(DEFAULT_SYRA_BASE_URL);
|
|
428
|
-
});
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
// ── podcastArtworkUrl ────────────────────────────────────────────────────────────
|
|
432
|
-
|
|
433
|
-
describe('createSyraClient.podcastArtworkUrl', () => {
|
|
434
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test' });
|
|
435
|
-
|
|
436
|
-
it('resolves the re-hosted image id to an absolute images URL', () => {
|
|
437
|
-
expect(client.podcastArtworkUrl({ image: '507f1f77bcf86cd799439022' })).toBe(
|
|
438
|
-
'https://api.example.test/api/images/507f1f77bcf86cd799439022',
|
|
439
|
-
);
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
it('prefers a named size from imageSizes', () => {
|
|
443
|
-
const url = client.podcastArtworkUrl(
|
|
444
|
-
{
|
|
445
|
-
image: '507f1f77bcf86cd799439022',
|
|
446
|
-
imageSizes: {
|
|
447
|
-
large: {
|
|
448
|
-
id: '507f1f77bcf86cd799439023',
|
|
449
|
-
url: '/api/images/507f1f77bcf86cd799439023',
|
|
450
|
-
width: 640,
|
|
451
|
-
height: 640,
|
|
452
|
-
},
|
|
453
|
-
},
|
|
454
|
-
},
|
|
455
|
-
'large',
|
|
456
|
-
);
|
|
457
|
-
expect(url).toBe('https://api.example.test/api/images/507f1f77bcf86cd799439023');
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
it('falls back to imageSourceUrl when no Syra image is present', () => {
|
|
461
|
-
expect(
|
|
462
|
-
client.podcastArtworkUrl({ imageSourceUrl: 'https://cdn.example.com/cover.jpg' }),
|
|
463
|
-
).toBe('https://cdn.example.com/cover.jpg');
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
it('returns undefined when nothing resolvable is present', () => {
|
|
467
|
-
expect(client.podcastArtworkUrl({})).toBeUndefined();
|
|
468
|
-
expect(client.podcastArtworkUrl({ image: 'not-an-id' })).toBeUndefined();
|
|
469
|
-
});
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
// ── getPodcastEpisodes ──────────────────────────────────────────────────────────
|
|
473
|
-
|
|
474
|
-
describe('createSyraClient.getPodcastEpisodes', () => {
|
|
475
|
-
it('calls /api/podcasts/:id/episodes with page and limit and returns parsed episodes', async () => {
|
|
476
|
-
const { fetch, calls } = fakeFetch(() => ({
|
|
477
|
-
body: {
|
|
478
|
-
data: [
|
|
479
|
-
makeEpisode({ id: '507f1f77bcf86cd799439031' }),
|
|
480
|
-
makeEpisode({ id: '507f1f77bcf86cd799439033', title: 'Second Episode' }),
|
|
481
|
-
],
|
|
482
|
-
total: 2,
|
|
483
|
-
page: 1,
|
|
484
|
-
limit: 20,
|
|
485
|
-
},
|
|
486
|
-
}));
|
|
487
|
-
|
|
488
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
|
|
489
|
-
const page = await client.getPodcastEpisodes('507f1f77bcf86cd799439021', { limit: 20 });
|
|
490
|
-
|
|
491
|
-
expect(page.items).toHaveLength(2);
|
|
492
|
-
expect(page.items[0].id).toBe('507f1f77bcf86cd799439031');
|
|
493
|
-
expect(page.items[0].enclosureUrl).toBe('https://api.fastcast.ai/audio/guid-1.mp3');
|
|
494
|
-
expect(page.hasMore).toBe(false);
|
|
495
|
-
expect(page.limit).toBe(20);
|
|
496
|
-
expect(page.offset).toBe(0);
|
|
497
|
-
|
|
498
|
-
expect(calls).toHaveLength(1);
|
|
499
|
-
const url = new URL(calls[0].url);
|
|
500
|
-
expect(url.pathname).toBe('/api/podcasts/507f1f77bcf86cd799439021/episodes');
|
|
501
|
-
expect(url.searchParams.get('page')).toBe('1');
|
|
502
|
-
expect(url.searchParams.get('limit')).toBe('20');
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
it('translates a zero-based offset into a 1-based page and derives hasMore from total', async () => {
|
|
506
|
-
const { fetch, calls } = fakeFetch(() => ({
|
|
507
|
-
body: { data: [makeEpisode()], total: 45, page: 3, limit: 10 },
|
|
508
|
-
}));
|
|
509
|
-
|
|
510
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
|
|
511
|
-
const page = await client.getPodcastEpisodes('507f1f77bcf86cd799439021', {
|
|
512
|
-
limit: 10,
|
|
513
|
-
offset: 20,
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
// offset 20 / limit 10 → page 3; 3 * 10 = 30 < 45 → more remain.
|
|
517
|
-
const url = new URL(calls[0].url);
|
|
518
|
-
expect(url.searchParams.get('page')).toBe('3');
|
|
519
|
-
expect(url.searchParams.get('limit')).toBe('10');
|
|
520
|
-
expect(page.offset).toBe(20);
|
|
521
|
-
expect(page.limit).toBe(10);
|
|
522
|
-
expect(page.hasMore).toBe(true);
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
it('reports hasMore=false when the page reaches the end of the total', async () => {
|
|
526
|
-
const { fetch } = fakeFetch(() => ({
|
|
527
|
-
body: { data: [makeEpisode()], total: 20, page: 2, limit: 10 },
|
|
528
|
-
}));
|
|
529
|
-
const client = createSyraClient({ fetch });
|
|
530
|
-
const page = await client.getPodcastEpisodes('507f1f77bcf86cd799439021', {
|
|
531
|
-
limit: 10,
|
|
532
|
-
offset: 10,
|
|
533
|
-
});
|
|
534
|
-
// page 2 * limit 10 = 20, not < total 20 → this is the last page.
|
|
535
|
-
expect(page.hasMore).toBe(false);
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
it('drops a row missing the required enclosureUrl without throwing', async () => {
|
|
539
|
-
const { fetch } = fakeFetch(() => ({
|
|
540
|
-
body: {
|
|
541
|
-
data: [makeEpisode({ enclosureUrl: undefined }), makeEpisode()],
|
|
542
|
-
total: 2,
|
|
543
|
-
},
|
|
544
|
-
}));
|
|
545
|
-
const client = createSyraClient({ fetch });
|
|
546
|
-
const page = await client.getPodcastEpisodes('507f1f77bcf86cd799439021', { limit: 10 });
|
|
547
|
-
expect(page.items).toHaveLength(1);
|
|
548
|
-
expect(page.items[0].enclosureUrl).toBe('https://api.fastcast.ai/audio/guid-1.mp3');
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
it('defaults the limit and returns an empty page when data is absent', async () => {
|
|
552
|
-
const { fetch, calls } = fakeFetch(() => ({ body: {} }));
|
|
553
|
-
const client = createSyraClient({ fetch });
|
|
554
|
-
const page = await client.getPodcastEpisodes('507f1f77bcf86cd799439021');
|
|
555
|
-
expect(page.items).toEqual([]);
|
|
556
|
-
expect(page.hasMore).toBe(false);
|
|
557
|
-
expect(page.offset).toBe(0);
|
|
558
|
-
expect(page.limit).toBe(20);
|
|
559
|
-
const url = new URL(calls[0].url);
|
|
560
|
-
expect(url.searchParams.get('page')).toBe('1');
|
|
561
|
-
expect(url.searchParams.get('limit')).toBe('20');
|
|
562
|
-
});
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
// ── getEpisode ──────────────────────────────────────────────────────────────────
|
|
566
|
-
|
|
567
|
-
describe('createSyraClient.getEpisode', () => {
|
|
568
|
-
it('fetches /api/episodes/:id and validates data.episode', async () => {
|
|
569
|
-
const { fetch, calls } = fakeFetch(() => ({
|
|
570
|
-
body: { data: { episode: makeEpisode(), persons: [] } },
|
|
571
|
-
}));
|
|
572
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
|
|
573
|
-
|
|
574
|
-
const episode = await client.getEpisode('507f1f77bcf86cd799439031');
|
|
575
|
-
expect(episode.title).toBe('Test Episode');
|
|
576
|
-
expect(episode.enclosureUrl).toBe('https://api.fastcast.ai/audio/guid-1.mp3');
|
|
577
|
-
expect(calls[0].url).toBe('https://api.example.test/api/episodes/507f1f77bcf86cd799439031');
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
it('throws SyraApiError on a non-2xx response', async () => {
|
|
581
|
-
const { fetch } = fakeFetch(() => ({ status: 404, body: { error: 'not found' } }));
|
|
582
|
-
const client = createSyraClient({ fetch });
|
|
583
|
-
await expect(client.getEpisode('507f1f77bcf86cd799439031')).rejects.toBeInstanceOf(SyraApiError);
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
it('throws when data.episode fails schema validation', async () => {
|
|
587
|
-
const { fetch } = fakeFetch(() => ({ body: { data: { episode: { id: 'x' } } } }));
|
|
588
|
-
const client = createSyraClient({ fetch });
|
|
589
|
-
await expect(client.getEpisode('x')).rejects.toThrow();
|
|
590
|
-
});
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
// ── episodeImageUrl ──────────────────────────────────────────────────────────────
|
|
594
|
-
|
|
595
|
-
describe('createSyraClient.episodeImageUrl', () => {
|
|
596
|
-
const client = createSyraClient({ baseURL: 'https://api.example.test' });
|
|
597
|
-
|
|
598
|
-
it('resolves the re-hosted image id to an absolute images URL', () => {
|
|
599
|
-
expect(client.episodeImageUrl({ image: '507f1f77bcf86cd799439032' })).toBe(
|
|
600
|
-
'https://api.example.test/api/images/507f1f77bcf86cd799439032',
|
|
601
|
-
);
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
it('prefers a named size from imageSizes', () => {
|
|
605
|
-
const url = client.episodeImageUrl(
|
|
606
|
-
{
|
|
607
|
-
image: '507f1f77bcf86cd799439032',
|
|
608
|
-
imageSizes: {
|
|
609
|
-
large: {
|
|
610
|
-
id: '507f1f77bcf86cd799439033',
|
|
611
|
-
url: '/api/images/507f1f77bcf86cd799439033',
|
|
612
|
-
width: 640,
|
|
613
|
-
height: 640,
|
|
614
|
-
},
|
|
615
|
-
},
|
|
616
|
-
},
|
|
617
|
-
'large',
|
|
618
|
-
);
|
|
619
|
-
expect(url).toBe('https://api.example.test/api/images/507f1f77bcf86cd799439033');
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
it('falls back to imageSourceUrl when no Syra image is present', () => {
|
|
623
|
-
expect(client.episodeImageUrl({ imageSourceUrl: 'https://cdn.example.com/episode.jpg' })).toBe(
|
|
624
|
-
'https://cdn.example.com/episode.jpg',
|
|
625
|
-
);
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
it('returns undefined when nothing resolvable is present', () => {
|
|
629
|
-
expect(client.episodeImageUrl({})).toBeUndefined();
|
|
630
|
-
expect(client.episodeImageUrl({ image: 'not-an-id' })).toBeUndefined();
|
|
631
|
-
});
|
|
632
|
-
});
|