@syra.fm/sdk 0.1.0 → 0.3.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.
@@ -484,3 +484,284 @@ export declare const trackSummarySchema: z.ZodObject<{
484
484
  previewAvailable?: boolean | undefined;
485
485
  }>;
486
486
  export type TrackSummary = z.infer<typeof trackSummarySchema>;
487
+ /**
488
+ * The summary view of a podcast SHOW returned by the public podcast endpoints
489
+ * (`GET /api/podcasts/search`, `GET /api/podcasts/:id`) — just enough to render
490
+ * a show card and deep-link into the Syra app.
491
+ *
492
+ * Artwork mirrors tracks: `image` is the re-hosted Syra image id (resolved via
493
+ * `/api/images/:id`); `imageSizes` is the multi-resolution variant set (each
494
+ * variant `url` is `/api/images/:id`); `imageSourceUrl` keeps the original
495
+ * external artwork URL as an absolute fallback when re-hosting has not run yet.
496
+ */
497
+ export declare const podcastSummarySchema: z.ZodObject<{
498
+ id: z.ZodString;
499
+ title: z.ZodString;
500
+ author: z.ZodOptional<z.ZodString>;
501
+ description: z.ZodOptional<z.ZodString>;
502
+ image: z.ZodOptional<z.ZodString>;
503
+ imageSizes: z.ZodOptional<z.ZodObject<{
504
+ small: z.ZodOptional<z.ZodObject<{
505
+ id: z.ZodOptional<z.ZodString>;
506
+ url: z.ZodString;
507
+ width: z.ZodOptional<z.ZodNumber>;
508
+ height: z.ZodOptional<z.ZodNumber>;
509
+ }, "strip", z.ZodTypeAny, {
510
+ url: string;
511
+ id?: string | undefined;
512
+ width?: number | undefined;
513
+ height?: number | undefined;
514
+ }, {
515
+ url: string;
516
+ id?: string | undefined;
517
+ width?: number | undefined;
518
+ height?: number | undefined;
519
+ }>>;
520
+ medium: z.ZodOptional<z.ZodObject<{
521
+ id: z.ZodOptional<z.ZodString>;
522
+ url: z.ZodString;
523
+ width: z.ZodOptional<z.ZodNumber>;
524
+ height: z.ZodOptional<z.ZodNumber>;
525
+ }, "strip", z.ZodTypeAny, {
526
+ url: string;
527
+ id?: string | undefined;
528
+ width?: number | undefined;
529
+ height?: number | undefined;
530
+ }, {
531
+ url: string;
532
+ id?: string | undefined;
533
+ width?: number | undefined;
534
+ height?: number | undefined;
535
+ }>>;
536
+ large: z.ZodOptional<z.ZodObject<{
537
+ id: z.ZodOptional<z.ZodString>;
538
+ url: z.ZodString;
539
+ width: z.ZodOptional<z.ZodNumber>;
540
+ height: z.ZodOptional<z.ZodNumber>;
541
+ }, "strip", z.ZodTypeAny, {
542
+ url: string;
543
+ id?: string | undefined;
544
+ width?: number | undefined;
545
+ height?: number | undefined;
546
+ }, {
547
+ url: string;
548
+ id?: string | undefined;
549
+ width?: number | undefined;
550
+ height?: number | undefined;
551
+ }>>;
552
+ xlarge: z.ZodOptional<z.ZodObject<{
553
+ id: z.ZodOptional<z.ZodString>;
554
+ url: z.ZodString;
555
+ width: z.ZodOptional<z.ZodNumber>;
556
+ height: z.ZodOptional<z.ZodNumber>;
557
+ }, "strip", z.ZodTypeAny, {
558
+ url: string;
559
+ id?: string | undefined;
560
+ width?: number | undefined;
561
+ height?: number | undefined;
562
+ }, {
563
+ url: string;
564
+ id?: string | undefined;
565
+ width?: number | undefined;
566
+ height?: number | undefined;
567
+ }>>;
568
+ xxlarge: z.ZodOptional<z.ZodObject<{
569
+ id: z.ZodOptional<z.ZodString>;
570
+ url: z.ZodString;
571
+ width: z.ZodOptional<z.ZodNumber>;
572
+ height: z.ZodOptional<z.ZodNumber>;
573
+ }, "strip", z.ZodTypeAny, {
574
+ url: string;
575
+ id?: string | undefined;
576
+ width?: number | undefined;
577
+ height?: number | undefined;
578
+ }, {
579
+ url: string;
580
+ id?: string | undefined;
581
+ width?: number | undefined;
582
+ height?: number | undefined;
583
+ }>>;
584
+ original: z.ZodOptional<z.ZodObject<{
585
+ id: z.ZodOptional<z.ZodString>;
586
+ url: z.ZodString;
587
+ width: z.ZodOptional<z.ZodNumber>;
588
+ height: z.ZodOptional<z.ZodNumber>;
589
+ }, "strip", z.ZodTypeAny, {
590
+ url: string;
591
+ id?: string | undefined;
592
+ width?: number | undefined;
593
+ height?: number | undefined;
594
+ }, {
595
+ url: string;
596
+ id?: string | undefined;
597
+ width?: number | undefined;
598
+ height?: number | undefined;
599
+ }>>;
600
+ }, "strip", z.ZodTypeAny, {
601
+ small?: {
602
+ url: string;
603
+ id?: string | undefined;
604
+ width?: number | undefined;
605
+ height?: number | undefined;
606
+ } | undefined;
607
+ medium?: {
608
+ url: string;
609
+ id?: string | undefined;
610
+ width?: number | undefined;
611
+ height?: number | undefined;
612
+ } | undefined;
613
+ large?: {
614
+ url: string;
615
+ id?: string | undefined;
616
+ width?: number | undefined;
617
+ height?: number | undefined;
618
+ } | undefined;
619
+ xlarge?: {
620
+ url: string;
621
+ id?: string | undefined;
622
+ width?: number | undefined;
623
+ height?: number | undefined;
624
+ } | undefined;
625
+ xxlarge?: {
626
+ url: string;
627
+ id?: string | undefined;
628
+ width?: number | undefined;
629
+ height?: number | undefined;
630
+ } | undefined;
631
+ original?: {
632
+ url: string;
633
+ id?: string | undefined;
634
+ width?: number | undefined;
635
+ height?: number | undefined;
636
+ } | undefined;
637
+ }, {
638
+ small?: {
639
+ url: string;
640
+ id?: string | undefined;
641
+ width?: number | undefined;
642
+ height?: number | undefined;
643
+ } | undefined;
644
+ medium?: {
645
+ url: string;
646
+ id?: string | undefined;
647
+ width?: number | undefined;
648
+ height?: number | undefined;
649
+ } | undefined;
650
+ large?: {
651
+ url: string;
652
+ id?: string | undefined;
653
+ width?: number | undefined;
654
+ height?: number | undefined;
655
+ } | undefined;
656
+ xlarge?: {
657
+ url: string;
658
+ id?: string | undefined;
659
+ width?: number | undefined;
660
+ height?: number | undefined;
661
+ } | undefined;
662
+ xxlarge?: {
663
+ url: string;
664
+ id?: string | undefined;
665
+ width?: number | undefined;
666
+ height?: number | undefined;
667
+ } | undefined;
668
+ original?: {
669
+ url: string;
670
+ id?: string | undefined;
671
+ width?: number | undefined;
672
+ height?: number | undefined;
673
+ } | undefined;
674
+ }>>;
675
+ imageSourceUrl: z.ZodOptional<z.ZodString>;
676
+ }, "strip", z.ZodTypeAny, {
677
+ id: string;
678
+ title: string;
679
+ author?: string | undefined;
680
+ description?: string | undefined;
681
+ image?: string | undefined;
682
+ imageSizes?: {
683
+ small?: {
684
+ url: string;
685
+ id?: string | undefined;
686
+ width?: number | undefined;
687
+ height?: number | undefined;
688
+ } | undefined;
689
+ medium?: {
690
+ url: string;
691
+ id?: string | undefined;
692
+ width?: number | undefined;
693
+ height?: number | undefined;
694
+ } | undefined;
695
+ large?: {
696
+ url: string;
697
+ id?: string | undefined;
698
+ width?: number | undefined;
699
+ height?: number | undefined;
700
+ } | undefined;
701
+ xlarge?: {
702
+ url: string;
703
+ id?: string | undefined;
704
+ width?: number | undefined;
705
+ height?: number | undefined;
706
+ } | undefined;
707
+ xxlarge?: {
708
+ url: string;
709
+ id?: string | undefined;
710
+ width?: number | undefined;
711
+ height?: number | undefined;
712
+ } | undefined;
713
+ original?: {
714
+ url: string;
715
+ id?: string | undefined;
716
+ width?: number | undefined;
717
+ height?: number | undefined;
718
+ } | undefined;
719
+ } | undefined;
720
+ imageSourceUrl?: string | undefined;
721
+ }, {
722
+ id: string;
723
+ title: string;
724
+ author?: string | undefined;
725
+ description?: string | undefined;
726
+ image?: string | undefined;
727
+ imageSizes?: {
728
+ small?: {
729
+ url: string;
730
+ id?: string | undefined;
731
+ width?: number | undefined;
732
+ height?: number | undefined;
733
+ } | undefined;
734
+ medium?: {
735
+ url: string;
736
+ id?: string | undefined;
737
+ width?: number | undefined;
738
+ height?: number | undefined;
739
+ } | undefined;
740
+ large?: {
741
+ url: string;
742
+ id?: string | undefined;
743
+ width?: number | undefined;
744
+ height?: number | undefined;
745
+ } | undefined;
746
+ xlarge?: {
747
+ url: string;
748
+ id?: string | undefined;
749
+ width?: number | undefined;
750
+ height?: number | undefined;
751
+ } | undefined;
752
+ xxlarge?: {
753
+ url: string;
754
+ id?: string | undefined;
755
+ width?: number | undefined;
756
+ height?: number | undefined;
757
+ } | undefined;
758
+ original?: {
759
+ url: string;
760
+ id?: string | undefined;
761
+ width?: number | undefined;
762
+ height?: number | undefined;
763
+ } | undefined;
764
+ } | undefined;
765
+ imageSourceUrl?: string | undefined;
766
+ }>;
767
+ export type PodcastSummary = z.infer<typeof podcastSummarySchema>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syra.fm/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Headless, isomorphic client for the Syra public API — catalog reads and 30s preview clips. No React/RN/DOM dependencies; public reads only.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -1,5 +1,10 @@
1
1
  import { describe, it, expect } from 'bun:test';
2
- import { createSyraClient, SyraApiError, DEFAULT_SYRA_BASE_URL } from './index';
2
+ import {
3
+ createSyraClient,
4
+ SyraApiError,
5
+ DEFAULT_SYRA_BASE_URL,
6
+ DEFAULT_SYRA_WEB_BASE_URL,
7
+ } from './index';
3
8
 
4
9
  // ── Fixtures ──────────────────────────────────────────────────────────────────
5
10
 
@@ -21,6 +26,25 @@ function makeTrack(overrides: Record<string, unknown> = {}): Record<string, unkn
21
26
  };
22
27
  }
23
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
+
24
48
  interface FetchCall {
25
49
  url: string;
26
50
  }
@@ -46,7 +70,7 @@ function fakeFetch(
46
70
  // ── searchTracks ──────────────────────────────────────────────────────────────
47
71
 
48
72
  describe('createSyraClient.searchTracks', () => {
49
- it('calls /api/search with category=tracks and the limit, returns preview-available tracks', async () => {
73
+ it('calls /api/search with category=tracks and the limit, returns a page of preview-available tracks', async () => {
50
74
  const { fetch, calls } = fakeFetch(() => ({
51
75
  body: {
52
76
  results: {
@@ -55,14 +79,20 @@ describe('createSyraClient.searchTracks', () => {
55
79
  makeTrack({ id: '507f1f77bcf86cd799439012', previewAvailable: false }),
56
80
  ],
57
81
  },
82
+ hasMore: false,
83
+ limit: 10,
84
+ offset: 0,
58
85
  },
59
86
  }));
60
87
 
61
88
  const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
62
- const tracks = await client.searchTracks('hello', { limit: 10 });
89
+ const page = await client.searchTracks('hello', { limit: 10 });
63
90
 
64
- expect(tracks).toHaveLength(1);
65
- expect(tracks[0].id).toBe('507f1f77bcf86cd799439011');
91
+ expect(page.items).toHaveLength(1);
92
+ expect(page.items[0].id).toBe('507f1f77bcf86cd799439011');
93
+ expect(page.hasMore).toBe(false);
94
+ expect(page.limit).toBe(10);
95
+ expect(page.offset).toBe(0);
66
96
 
67
97
  expect(calls).toHaveLength(1);
68
98
  const url = new URL(calls[0].url);
@@ -70,6 +100,53 @@ describe('createSyraClient.searchTracks', () => {
70
100
  expect(url.searchParams.get('q')).toBe('hello');
71
101
  expect(url.searchParams.get('category')).toBe('tracks');
72
102
  expect(url.searchParams.get('limit')).toBe('10');
103
+ expect(url.searchParams.has('offset')).toBe(false);
104
+ });
105
+
106
+ it('sends the offset param and reports hasMore/offset/limit from the backend', async () => {
107
+ const { fetch, calls } = fakeFetch(() => ({
108
+ body: {
109
+ results: { tracks: [makeTrack()] },
110
+ hasMore: true,
111
+ limit: 20,
112
+ offset: 40,
113
+ },
114
+ }));
115
+
116
+ const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
117
+ const page = await client.searchTracks('hello', { limit: 20, offset: 40 });
118
+
119
+ expect(page.items).toHaveLength(1);
120
+ expect(page.hasMore).toBe(true);
121
+ expect(page.limit).toBe(20);
122
+ expect(page.offset).toBe(40);
123
+
124
+ const url = new URL(calls[0].url);
125
+ expect(url.searchParams.get('offset')).toBe('40');
126
+ expect(url.searchParams.get('limit')).toBe('20');
127
+ });
128
+
129
+ it('reports backend hasMore even when client-side preview filtering empties the page', async () => {
130
+ const { fetch } = fakeFetch(() => ({
131
+ body: {
132
+ results: {
133
+ tracks: [
134
+ makeTrack({ previewAvailable: false }),
135
+ makeTrack({ previewAvailable: false }),
136
+ ],
137
+ },
138
+ hasMore: true,
139
+ limit: 2,
140
+ offset: 0,
141
+ },
142
+ }));
143
+
144
+ const client = createSyraClient({ fetch });
145
+ const page = await client.searchTracks('x', { limit: 2 });
146
+
147
+ // All rows were filtered out, but the page is NOT the last one.
148
+ expect(page.items).toHaveLength(0);
149
+ expect(page.hasMore).toBe(true);
73
150
  });
74
151
 
75
152
  it('drops malformed rows without throwing', async () => {
@@ -85,14 +162,17 @@ describe('createSyraClient.searchTracks', () => {
85
162
  }));
86
163
 
87
164
  const client = createSyraClient({ fetch });
88
- const tracks = await client.searchTracks('x');
89
- expect(tracks).toHaveLength(1);
165
+ const page = await client.searchTracks('x');
166
+ expect(page.items).toHaveLength(1);
90
167
  });
91
168
 
92
- it('returns an empty array when results.tracks is absent', async () => {
169
+ it('returns an empty page with hasMore=false when results.tracks is absent', async () => {
93
170
  const { fetch } = fakeFetch(() => ({ body: {} }));
94
171
  const client = createSyraClient({ fetch });
95
- expect(await client.searchTracks('x')).toEqual([]);
172
+ const page = await client.searchTracks('x');
173
+ expect(page.items).toEqual([]);
174
+ expect(page.hasMore).toBe(false);
175
+ expect(page.offset).toBe(0);
96
176
  });
97
177
  });
98
178
 
@@ -194,3 +274,174 @@ describe('createSyraClient.artworkUrl', () => {
194
274
  expect(client.artworkUrl('not-an-id')).toBeUndefined();
195
275
  });
196
276
  });
277
+
278
+ // ── searchPodcasts ──────────────────────────────────────────────────────────────
279
+
280
+ describe('createSyraClient.searchPodcasts', () => {
281
+ it('calls /api/podcasts/search with q and limit, returns a page of parsed shows', async () => {
282
+ const { fetch, calls } = fakeFetch(() => ({
283
+ body: {
284
+ data: [
285
+ makePodcast({ id: '507f1f77bcf86cd799439021' }),
286
+ makePodcast({ id: '507f1f77bcf86cd799439023', title: 'Second Show' }),
287
+ ],
288
+ hasMore: false,
289
+ limit: 5,
290
+ offset: 0,
291
+ },
292
+ }));
293
+
294
+ const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
295
+ const page = await client.searchPodcasts('news', { limit: 5 });
296
+
297
+ expect(page.items).toHaveLength(2);
298
+ expect(page.items[0].id).toBe('507f1f77bcf86cd799439021');
299
+ expect(page.items[0].author).toBe('Test Publisher');
300
+ expect(page.hasMore).toBe(false);
301
+ expect(page.limit).toBe(5);
302
+ expect(page.offset).toBe(0);
303
+
304
+ expect(calls).toHaveLength(1);
305
+ const url = new URL(calls[0].url);
306
+ expect(url.pathname).toBe('/api/podcasts/search');
307
+ expect(url.searchParams.get('q')).toBe('news');
308
+ expect(url.searchParams.get('limit')).toBe('5');
309
+ expect(url.searchParams.has('offset')).toBe(false);
310
+ });
311
+
312
+ it('sends the offset param and reports hasMore/offset from the backend', async () => {
313
+ const { fetch, calls } = fakeFetch(() => ({
314
+ body: { data: [makePodcast()], hasMore: true, limit: 10, offset: 20 },
315
+ }));
316
+
317
+ const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
318
+ const page = await client.searchPodcasts('news', { limit: 10, offset: 20 });
319
+
320
+ expect(page.hasMore).toBe(true);
321
+ expect(page.limit).toBe(10);
322
+ expect(page.offset).toBe(20);
323
+
324
+ const url = new URL(calls[0].url);
325
+ expect(url.searchParams.get('offset')).toBe('20');
326
+ });
327
+
328
+ it('omits the limit and offset params when not provided', async () => {
329
+ const { fetch, calls } = fakeFetch(() => ({ body: { data: [makePodcast()] } }));
330
+ const client = createSyraClient({ fetch });
331
+ await client.searchPodcasts('news');
332
+ const url = new URL(calls[0].url);
333
+ expect(url.searchParams.has('limit')).toBe(false);
334
+ expect(url.searchParams.has('offset')).toBe(false);
335
+ });
336
+
337
+ it('drops malformed rows without throwing', async () => {
338
+ const { fetch } = fakeFetch(() => ({
339
+ body: { data: [{ id: 'broken' }, makePodcast()] },
340
+ }));
341
+ const client = createSyraClient({ fetch });
342
+ const page = await client.searchPodcasts('x');
343
+ expect(page.items).toHaveLength(1);
344
+ });
345
+
346
+ it('returns an empty page with hasMore=false when data is absent', async () => {
347
+ const { fetch } = fakeFetch(() => ({ body: {} }));
348
+ const client = createSyraClient({ fetch });
349
+ const page = await client.searchPodcasts('x');
350
+ expect(page.items).toEqual([]);
351
+ expect(page.hasMore).toBe(false);
352
+ expect(page.offset).toBe(0);
353
+ });
354
+ });
355
+
356
+ // ── getPodcast ──────────────────────────────────────────────────────────────────
357
+
358
+ describe('createSyraClient.getPodcast', () => {
359
+ it('fetches /api/podcasts/:id and validates data.podcast', async () => {
360
+ const { fetch, calls } = fakeFetch(() => ({
361
+ body: { data: { podcast: makePodcast(), episodes: [], persons: [] } },
362
+ }));
363
+ const client = createSyraClient({ baseURL: 'https://api.example.test', fetch });
364
+
365
+ const podcast = await client.getPodcast('507f1f77bcf86cd799439021');
366
+ expect(podcast.title).toBe('Test Show');
367
+ expect(calls[0].url).toBe('https://api.example.test/api/podcasts/507f1f77bcf86cd799439021');
368
+ });
369
+
370
+ it('throws SyraApiError on a non-2xx response', async () => {
371
+ const { fetch } = fakeFetch(() => ({ status: 404, body: { error: 'not found' } }));
372
+ const client = createSyraClient({ fetch });
373
+ await expect(client.getPodcast('507f1f77bcf86cd799439021')).rejects.toBeInstanceOf(SyraApiError);
374
+ });
375
+
376
+ it('throws when data.podcast fails schema validation', async () => {
377
+ const { fetch } = fakeFetch(() => ({ body: { data: { podcast: { id: 'x' } } } }));
378
+ const client = createSyraClient({ fetch });
379
+ await expect(client.getPodcast('x')).rejects.toThrow();
380
+ });
381
+ });
382
+
383
+ // ── podcastUrl ────────────────────────────────────────────────────────────────
384
+
385
+ describe('createSyraClient.podcastUrl', () => {
386
+ it('builds the web deep link from the web base URL, not the API host', () => {
387
+ const client = createSyraClient({
388
+ baseURL: 'https://api.example.test',
389
+ webBaseURL: 'https://web.example.test',
390
+ });
391
+ expect(client.podcastUrl('507f1f77bcf86cd799439021')).toBe(
392
+ 'https://web.example.test/podcasts/507f1f77bcf86cd799439021',
393
+ );
394
+ });
395
+
396
+ it('defaults to the production web base URL', () => {
397
+ const client = createSyraClient();
398
+ expect(client.podcastUrl('abc')).toBe(`${DEFAULT_SYRA_WEB_BASE_URL}/podcasts/abc`);
399
+ });
400
+
401
+ it('does not use the API base URL for the deep link', () => {
402
+ const client = createSyraClient({ baseURL: 'https://api.example.test' });
403
+ expect(client.podcastUrl('abc')).toBe(`${DEFAULT_SYRA_WEB_BASE_URL}/podcasts/abc`);
404
+ expect(DEFAULT_SYRA_WEB_BASE_URL).not.toBe(DEFAULT_SYRA_BASE_URL);
405
+ });
406
+ });
407
+
408
+ // ── podcastArtworkUrl ────────────────────────────────────────────────────────────
409
+
410
+ describe('createSyraClient.podcastArtworkUrl', () => {
411
+ const client = createSyraClient({ baseURL: 'https://api.example.test' });
412
+
413
+ it('resolves the re-hosted image id to an absolute images URL', () => {
414
+ expect(client.podcastArtworkUrl({ image: '507f1f77bcf86cd799439022' })).toBe(
415
+ 'https://api.example.test/api/images/507f1f77bcf86cd799439022',
416
+ );
417
+ });
418
+
419
+ it('prefers a named size from imageSizes', () => {
420
+ const url = client.podcastArtworkUrl(
421
+ {
422
+ image: '507f1f77bcf86cd799439022',
423
+ imageSizes: {
424
+ large: {
425
+ id: '507f1f77bcf86cd799439023',
426
+ url: '/api/images/507f1f77bcf86cd799439023',
427
+ width: 640,
428
+ height: 640,
429
+ },
430
+ },
431
+ },
432
+ 'large',
433
+ );
434
+ expect(url).toBe('https://api.example.test/api/images/507f1f77bcf86cd799439023');
435
+ });
436
+
437
+ it('falls back to imageSourceUrl when no Syra image is present', () => {
438
+ expect(
439
+ client.podcastArtworkUrl({ imageSourceUrl: 'https://cdn.example.com/cover.jpg' }),
440
+ ).toBe('https://cdn.example.com/cover.jpg');
441
+ });
442
+
443
+ it('returns undefined when nothing resolvable is present', () => {
444
+ expect(client.podcastArtworkUrl({})).toBeUndefined();
445
+ expect(client.podcastArtworkUrl({ image: 'not-an-id' })).toBeUndefined();
446
+ });
447
+ });