382 lines
12 KiB
TypeScript
382 lines
12 KiB
TypeScript
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||
|
|
import * as smartfeed from '../ts/index.js';
|
||
|
|
|
||
|
|
let testSmartFeed: smartfeed.Smartfeed;
|
||
|
|
let advancedPodcast: smartfeed.PodcastFeed;
|
||
|
|
|
||
|
|
tap.test('setup', async () => {
|
||
|
|
testSmartFeed = new smartfeed.Smartfeed();
|
||
|
|
advancedPodcast = testSmartFeed.createPodcastFeed({
|
||
|
|
domain: 'advanced.example.com',
|
||
|
|
title: 'Advanced Podcast Features',
|
||
|
|
description: 'Testing advanced podcast features',
|
||
|
|
category: 'Technology',
|
||
|
|
company: 'Advanced Inc',
|
||
|
|
companyEmail: 'advanced@example.com',
|
||
|
|
companyDomain: 'https://example.com',
|
||
|
|
itunesCategory: 'Technology',
|
||
|
|
itunesAuthor: 'Tech Host',
|
||
|
|
itunesOwner: { name: 'Tech Host', email: 'host@example.com' },
|
||
|
|
itunesImage: 'https://example.com/podcast.jpg',
|
||
|
|
itunesExplicit: false,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('should add episode with persons (hosts and guests)', async () => {
|
||
|
|
advancedPodcast.addEpisode({
|
||
|
|
title: 'Episode with Guests',
|
||
|
|
authorName: 'Main Host',
|
||
|
|
imageUrl: 'https://example.com/episode.jpg',
|
||
|
|
timestamp: Date.now(),
|
||
|
|
url: 'https://example.com/episode/guests',
|
||
|
|
content: 'Episode featuring special guests',
|
||
|
|
audioUrl: 'https://example.com/audio/guests.mp3',
|
||
|
|
audioType: 'audio/mpeg',
|
||
|
|
audioLength: 50000000,
|
||
|
|
itunesDuration: 3600,
|
||
|
|
persons: [
|
||
|
|
{
|
||
|
|
name: 'Main Host',
|
||
|
|
role: 'host',
|
||
|
|
href: 'https://example.com/host',
|
||
|
|
img: 'https://example.com/host.jpg',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'Special Guest 1',
|
||
|
|
role: 'guest',
|
||
|
|
href: 'https://example.com/guest1',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'Special Guest 2',
|
||
|
|
role: 'guest',
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(advancedPodcast.episodes[0].persons).toBeArray();
|
||
|
|
expect(advancedPodcast.episodes[0].persons?.length).toEqual(3);
|
||
|
|
expect(advancedPodcast.episodes[0].persons?.[0].role).toEqual('host');
|
||
|
|
expect(advancedPodcast.episodes[0].persons?.[1].role).toEqual('guest');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('should include persons in RSS export', async () => {
|
||
|
|
const rss = advancedPodcast.exportPodcastRss();
|
||
|
|
|
||
|
|
expect(rss).toInclude('xmlns:podcast="https://podcastindex.org/namespace/1.0"');
|
||
|
|
expect(rss).toInclude('<podcast:person role="host"');
|
||
|
|
expect(rss).toInclude('Main Host</podcast:person>');
|
||
|
|
expect(rss).toInclude('<podcast:person role="guest"');
|
||
|
|
expect(rss).toInclude('Special Guest 1</podcast:person>');
|
||
|
|
expect(rss).toInclude('href="https://example.com/host"');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('should add episode with transcripts', async () => {
|
||
|
|
const podcast = testSmartFeed.createPodcastFeed({
|
||
|
|
domain: 'transcript.example.com',
|
||
|
|
title: 'Podcast with Transcripts',
|
||
|
|
description: 'Testing transcript features',
|
||
|
|
category: 'Education',
|
||
|
|
company: 'Edu Inc',
|
||
|
|
companyEmail: 'edu@example.com',
|
||
|
|
companyDomain: 'https://example.com',
|
||
|
|
itunesCategory: 'Education',
|
||
|
|
itunesAuthor: 'Teacher',
|
||
|
|
itunesOwner: { name: 'Teacher', email: 'teacher@example.com' },
|
||
|
|
itunesImage: 'https://example.com/edu.jpg',
|
||
|
|
itunesExplicit: false,
|
||
|
|
});
|
||
|
|
|
||
|
|
podcast.addEpisode({
|
||
|
|
title: 'Episode with Transcript',
|
||
|
|
authorName: 'Teacher',
|
||
|
|
imageUrl: 'https://example.com/episode.jpg',
|
||
|
|
timestamp: Date.now(),
|
||
|
|
url: 'https://example.com/episode/transcript',
|
||
|
|
content: 'Episode with multiple transcript formats',
|
||
|
|
audioUrl: 'https://example.com/audio/episode.mp3',
|
||
|
|
audioType: 'audio/mpeg',
|
||
|
|
audioLength: 40000000,
|
||
|
|
itunesDuration: 2400,
|
||
|
|
transcripts: [
|
||
|
|
{
|
||
|
|
url: 'https://example.com/transcripts/episode.txt',
|
||
|
|
type: 'text/plain',
|
||
|
|
language: 'en',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
url: 'https://example.com/transcripts/episode.srt',
|
||
|
|
type: 'application/srt',
|
||
|
|
language: 'en',
|
||
|
|
rel: 'captions',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
url: 'https://example.com/transcripts/episode.vtt',
|
||
|
|
type: 'text/vtt',
|
||
|
|
language: 'en',
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(podcast.episodes[0].transcripts).toBeArray();
|
||
|
|
expect(podcast.episodes[0].transcripts?.length).toEqual(3);
|
||
|
|
|
||
|
|
const rss = podcast.exportPodcastRss();
|
||
|
|
expect(rss).toInclude('<podcast:transcript url="https://example.com/transcripts/episode.txt"');
|
||
|
|
expect(rss).toInclude('type="text/plain"');
|
||
|
|
expect(rss).toInclude('language="en"');
|
||
|
|
expect(rss).toInclude('type="application/srt"');
|
||
|
|
expect(rss).toInclude('rel="captions"');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('should add episode with funding links', async () => {
|
||
|
|
const podcast = testSmartFeed.createPodcastFeed({
|
||
|
|
domain: 'funding.example.com',
|
||
|
|
title: 'Podcast with Funding',
|
||
|
|
description: 'Testing funding features',
|
||
|
|
category: 'Arts',
|
||
|
|
company: 'Arts Inc',
|
||
|
|
companyEmail: 'arts@example.com',
|
||
|
|
companyDomain: 'https://example.com',
|
||
|
|
itunesCategory: 'Arts',
|
||
|
|
itunesAuthor: 'Artist',
|
||
|
|
itunesOwner: { name: 'Artist', email: 'artist@example.com' },
|
||
|
|
itunesImage: 'https://example.com/arts.jpg',
|
||
|
|
itunesExplicit: false,
|
||
|
|
});
|
||
|
|
|
||
|
|
podcast.addEpisode({
|
||
|
|
title: 'Episode with Funding',
|
||
|
|
authorName: 'Artist',
|
||
|
|
imageUrl: 'https://example.com/episode.jpg',
|
||
|
|
timestamp: Date.now(),
|
||
|
|
url: 'https://example.com/episode/funding',
|
||
|
|
content: 'Support this podcast',
|
||
|
|
audioUrl: 'https://example.com/audio/episode.mp3',
|
||
|
|
audioType: 'audio/mpeg',
|
||
|
|
audioLength: 35000000,
|
||
|
|
itunesDuration: 2100,
|
||
|
|
funding: [
|
||
|
|
{
|
||
|
|
url: 'https://patreon.com/example',
|
||
|
|
message: 'Support us on Patreon',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
url: 'https://buymeacoffee.com/example',
|
||
|
|
message: 'Buy me a coffee',
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(podcast.episodes[0].funding).toBeArray();
|
||
|
|
expect(podcast.episodes[0].funding?.length).toEqual(2);
|
||
|
|
|
||
|
|
const rss = podcast.exportPodcastRss();
|
||
|
|
expect(rss).toInclude('<podcast:funding url="https://patreon.com/example">Support us on Patreon</podcast:funding>');
|
||
|
|
expect(rss).toInclude('<podcast:funding url="https://buymeacoffee.com/example">Buy me a coffee</podcast:funding>');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('should add episode with all advanced features', async () => {
|
||
|
|
const podcast = testSmartFeed.createPodcastFeed({
|
||
|
|
domain: 'complete.example.com',
|
||
|
|
title: 'Complete Podcast',
|
||
|
|
description: 'All features combined',
|
||
|
|
category: 'Society & Culture',
|
||
|
|
company: 'Complete Inc',
|
||
|
|
companyEmail: 'complete@example.com',
|
||
|
|
companyDomain: 'https://example.com',
|
||
|
|
itunesCategory: 'Society & Culture',
|
||
|
|
itunesAuthor: 'Host Name',
|
||
|
|
itunesOwner: { name: 'Host Name', email: 'host@example.com' },
|
||
|
|
itunesImage: 'https://example.com/complete.jpg',
|
||
|
|
itunesExplicit: false,
|
||
|
|
});
|
||
|
|
|
||
|
|
podcast.addEpisode({
|
||
|
|
title: 'Complete Feature Episode',
|
||
|
|
authorName: 'Host Name',
|
||
|
|
imageUrl: 'https://example.com/complete-episode.jpg',
|
||
|
|
timestamp: Date.now(),
|
||
|
|
url: 'https://example.com/episode/complete',
|
||
|
|
content: 'An episode with all advanced features enabled',
|
||
|
|
audioUrl: 'https://example.com/audio/complete.mp3',
|
||
|
|
audioType: 'audio/mpeg',
|
||
|
|
audioLength: 60000000,
|
||
|
|
itunesDuration: 4500,
|
||
|
|
itunesEpisode: 42,
|
||
|
|
itunesSeason: 2,
|
||
|
|
itunesEpisodeType: 'full',
|
||
|
|
itunesSubtitle: 'A subtitle for this episode',
|
||
|
|
itunesSummary: 'A longer summary describing this amazing episode in detail',
|
||
|
|
persons: [
|
||
|
|
{ name: 'Host Name', role: 'host', href: 'https://example.com/host' },
|
||
|
|
{ name: 'Co-Host', role: 'co-host' },
|
||
|
|
{ name: 'Guest Expert', role: 'guest' },
|
||
|
|
],
|
||
|
|
transcripts: [
|
||
|
|
{ url: 'https://example.com/transcript.txt', type: 'text/plain', language: 'en' },
|
||
|
|
],
|
||
|
|
funding: [
|
||
|
|
{ url: 'https://support.example.com', message: 'Support the show' },
|
||
|
|
],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(podcast.episodes.length).toEqual(1);
|
||
|
|
|
||
|
|
const rss = podcast.exportPodcastRss();
|
||
|
|
|
||
|
|
// Verify iTunes tags
|
||
|
|
expect(rss).toInclude('<itunes:episode>42</itunes:episode>');
|
||
|
|
expect(rss).toInclude('<itunes:season>2</itunes:season>');
|
||
|
|
expect(rss).toInclude('<itunes:episodeType>full</itunes:episodeType>');
|
||
|
|
expect(rss).toInclude('<itunes:subtitle>A subtitle for this episode</itunes:subtitle>');
|
||
|
|
expect(rss).toInclude('<itunes:summary>A longer summary describing this amazing episode in detail</itunes:summary>');
|
||
|
|
|
||
|
|
// Verify podcast namespace tags
|
||
|
|
expect(rss).toInclude('<podcast:person role="host"');
|
||
|
|
expect(rss).toInclude('<podcast:person role="co-host"');
|
||
|
|
expect(rss).toInclude('<podcast:person role="guest"');
|
||
|
|
expect(rss).toInclude('<podcast:transcript');
|
||
|
|
expect(rss).toInclude('<podcast:funding');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('should handle explicit content flag at episode level', async () => {
|
||
|
|
const podcast = testSmartFeed.createPodcastFeed({
|
||
|
|
domain: 'explicit.example.com',
|
||
|
|
title: 'Explicit Podcast',
|
||
|
|
description: 'Testing explicit flag',
|
||
|
|
category: 'Comedy',
|
||
|
|
company: 'Comedy Inc',
|
||
|
|
companyEmail: 'comedy@example.com',
|
||
|
|
companyDomain: 'https://example.com',
|
||
|
|
itunesCategory: 'Comedy',
|
||
|
|
itunesAuthor: 'Comedian',
|
||
|
|
itunesOwner: { name: 'Comedian', email: 'comedian@example.com' },
|
||
|
|
itunesImage: 'https://example.com/comedy.jpg',
|
||
|
|
itunesExplicit: false, // Podcast is not explicit by default
|
||
|
|
});
|
||
|
|
|
||
|
|
podcast.addEpisode({
|
||
|
|
title: 'Clean Episode',
|
||
|
|
authorName: 'Comedian',
|
||
|
|
imageUrl: 'https://example.com/clean.jpg',
|
||
|
|
timestamp: Date.now(),
|
||
|
|
url: 'https://example.com/episode/clean',
|
||
|
|
content: 'A clean episode',
|
||
|
|
audioUrl: 'https://example.com/audio/clean.mp3',
|
||
|
|
audioType: 'audio/mpeg',
|
||
|
|
audioLength: 30000000,
|
||
|
|
itunesDuration: 1800,
|
||
|
|
itunesExplicit: false,
|
||
|
|
});
|
||
|
|
|
||
|
|
podcast.addEpisode({
|
||
|
|
title: 'Explicit Episode',
|
||
|
|
authorName: 'Comedian',
|
||
|
|
imageUrl: 'https://example.com/explicit.jpg',
|
||
|
|
timestamp: Date.now() + 1,
|
||
|
|
url: 'https://example.com/episode/explicit',
|
||
|
|
content: 'An explicit episode',
|
||
|
|
audioUrl: 'https://example.com/audio/explicit.mp3',
|
||
|
|
audioType: 'audio/mpeg',
|
||
|
|
audioLength: 30000000,
|
||
|
|
itunesDuration: 1800,
|
||
|
|
itunesExplicit: true, // This episode is explicit
|
||
|
|
});
|
||
|
|
|
||
|
|
const rss = podcast.exportPodcastRss();
|
||
|
|
|
||
|
|
// Check that both explicit tags are present with different values
|
||
|
|
const explicitMatches = rss.match(/<itunes:explicit>(true|false)<\/itunes:explicit>/g);
|
||
|
|
expect(explicitMatches).toBeArray();
|
||
|
|
expect(rss).toInclude('<itunes:explicit>false</itunes:explicit>'); // Clean episode
|
||
|
|
expect(rss).toInclude('<itunes:explicit>true</itunes:explicit>'); // Explicit episode
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('should validate transcript URL', async () => {
|
||
|
|
const podcast = testSmartFeed.createPodcastFeed({
|
||
|
|
domain: 'test.com',
|
||
|
|
title: 'Test',
|
||
|
|
description: 'Test',
|
||
|
|
category: 'Test',
|
||
|
|
company: 'Test Inc',
|
||
|
|
companyEmail: 'test@example.com',
|
||
|
|
companyDomain: 'https://example.com',
|
||
|
|
itunesCategory: 'Technology',
|
||
|
|
itunesAuthor: 'Author',
|
||
|
|
itunesOwner: { name: 'Owner', email: 'owner@example.com' },
|
||
|
|
itunesImage: 'https://example.com/image.jpg',
|
||
|
|
itunesExplicit: false,
|
||
|
|
});
|
||
|
|
|
||
|
|
let errorThrown = false;
|
||
|
|
try {
|
||
|
|
podcast.addEpisode({
|
||
|
|
title: 'Episode 1',
|
||
|
|
authorName: 'Author',
|
||
|
|
imageUrl: 'https://example.com/episode.jpg',
|
||
|
|
timestamp: Date.now(),
|
||
|
|
url: 'https://example.com/episode/1',
|
||
|
|
content: 'Content',
|
||
|
|
audioUrl: 'https://example.com/audio.mp3',
|
||
|
|
audioType: 'audio/mpeg',
|
||
|
|
audioLength: 1000000,
|
||
|
|
itunesDuration: 600,
|
||
|
|
transcripts: [
|
||
|
|
{
|
||
|
|
url: 'not-a-url', // Invalid!
|
||
|
|
type: 'text/plain',
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
errorThrown = true;
|
||
|
|
expect(error.message).toInclude('Invalid or relative URL');
|
||
|
|
}
|
||
|
|
expect(errorThrown).toEqual(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('should validate funding URL', async () => {
|
||
|
|
const podcast = testSmartFeed.createPodcastFeed({
|
||
|
|
domain: 'test.com',
|
||
|
|
title: 'Test',
|
||
|
|
description: 'Test',
|
||
|
|
category: 'Test',
|
||
|
|
company: 'Test Inc',
|
||
|
|
companyEmail: 'test@example.com',
|
||
|
|
companyDomain: 'https://example.com',
|
||
|
|
itunesCategory: 'Technology',
|
||
|
|
itunesAuthor: 'Author',
|
||
|
|
itunesOwner: { name: 'Owner', email: 'owner@example.com' },
|
||
|
|
itunesImage: 'https://example.com/image.jpg',
|
||
|
|
itunesExplicit: false,
|
||
|
|
});
|
||
|
|
|
||
|
|
let errorThrown = false;
|
||
|
|
try {
|
||
|
|
podcast.addEpisode({
|
||
|
|
title: 'Episode 1',
|
||
|
|
authorName: 'Author',
|
||
|
|
imageUrl: 'https://example.com/episode.jpg',
|
||
|
|
timestamp: Date.now(),
|
||
|
|
url: 'https://example.com/episode/1',
|
||
|
|
content: 'Content',
|
||
|
|
audioUrl: 'https://example.com/audio.mp3',
|
||
|
|
audioType: 'audio/mpeg',
|
||
|
|
audioLength: 1000000,
|
||
|
|
itunesDuration: 600,
|
||
|
|
funding: [
|
||
|
|
{
|
||
|
|
url: 'relative/path', // Invalid!
|
||
|
|
message: 'Support us',
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
errorThrown = true;
|
||
|
|
expect(error.message).toInclude('Invalid or relative URL');
|
||
|
|
}
|
||
|
|
expect(errorThrown).toEqual(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
export default tap.start();
|