feat(parsing): Replace rss-parser with fast-xml-parser and add native feed parser; update Smartfeed to use parseFeedXML and adjust plugins/tests

This commit is contained in:
2025-10-31 21:26:07 +00:00
parent 64c7414682
commit 645f9c0e64
20 changed files with 353 additions and 1064 deletions

View File

@@ -1,381 +0,0 @@
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();

View File

@@ -1,261 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartfeed from '../ts/index.js';
let testSmartFeed: smartfeed.Smartfeed;
let testPodcast: smartfeed.PodcastFeed;
tap.test('setup', async () => {
testSmartFeed = new smartfeed.Smartfeed();
});
tap.test('should create a podcast feed', async () => {
testPodcast = testSmartFeed.createPodcastFeed({
domain: 'podcast.example.com',
title: 'Test Podcast',
description: 'A test podcast about testing',
category: 'Technology',
company: 'Test Podcast Inc',
companyEmail: 'podcast@example.com',
companyDomain: 'https://example.com',
itunesCategory: 'Technology',
itunesAuthor: 'John Tester',
itunesOwner: {
name: 'John Tester',
email: 'john@example.com',
},
itunesImage: 'https://example.com/podcast-artwork.jpg',
itunesExplicit: false,
podcastGuid: 'test-podcast-guid-001',
});
expect(testPodcast).toBeInstanceOf(smartfeed.PodcastFeed);
expect(testPodcast.podcastOptions.itunesCategory).toEqual('Technology');
expect(testPodcast.podcastOptions.itunesAuthor).toEqual('John Tester');
});
tap.test('should create podcast feed with episodic type', async () => {
const podcast = testSmartFeed.createPodcastFeed({
domain: 'episodic.example.com',
title: 'Episodic Podcast',
description: 'An episodic podcast',
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: true,
itunesType: 'episodic',
});
expect(podcast.podcastOptions.itunesType).toEqual('episodic');
expect(podcast.podcastOptions.itunesExplicit).toEqual(true);
});
tap.test('should create podcast feed with serial type', async () => {
const podcast = testSmartFeed.createPodcastFeed({
domain: 'serial.example.com',
title: 'Serial Podcast',
description: 'A serial podcast',
category: 'True Crime',
company: 'Crime Inc',
companyEmail: 'crime@example.com',
companyDomain: 'https://example.com',
itunesCategory: 'True Crime',
itunesAuthor: 'Detective',
itunesOwner: { name: 'Detective', email: 'detective@example.com' },
itunesImage: 'https://example.com/crime.jpg',
itunesExplicit: false,
itunesType: 'serial',
});
expect(podcast.podcastOptions.itunesType).toEqual('serial');
});
tap.test('should add episode to podcast', async () => {
testPodcast.addEpisode({
title: 'Episode 1: Introduction',
authorName: 'John Tester',
imageUrl: 'https://example.com/episode1.jpg',
timestamp: Date.now(),
url: 'https://podcast.example.com/episode/1',
content: 'In this episode, we introduce the podcast',
audioUrl: 'https://example.com/audio/episode1.mp3',
audioType: 'audio/mpeg',
audioLength: 45678900,
itunesDuration: 3600,
itunesEpisode: 1,
itunesSeason: 1,
itunesEpisodeType: 'full',
});
expect(testPodcast.episodes.length).toEqual(1);
expect(testPodcast.episodes[0].title).toEqual('Episode 1: Introduction');
expect(testPodcast.episodes[0].itunesEpisode).toEqual(1);
});
tap.test('should add multiple episodes', async () => {
const podcast = testSmartFeed.createPodcastFeed({
domain: 'multi.example.com',
title: 'Multi-Episode Podcast',
description: 'Podcast with multiple episodes',
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,
});
for (let i = 1; i <= 5; i++) {
podcast.addEpisode({
title: `Episode ${i}`,
authorName: 'Teacher',
imageUrl: `https://example.com/episode${i}.jpg`,
timestamp: Date.now() + i,
url: `https://example.com/episode/${i}`,
content: `Content for episode ${i}`,
audioUrl: `https://example.com/audio/episode${i}.mp3`,
audioType: 'audio/mpeg',
audioLength: 40000000 + i * 1000000,
itunesDuration: 3000 + i * 100,
itunesEpisode: i,
itunesSeason: 1,
});
}
expect(podcast.episodes.length).toEqual(5);
});
tap.test('should add episode with trailer type', async () => {
const podcast = testSmartFeed.createPodcastFeed({
domain: 'trailer.example.com',
title: 'Podcast with Trailer',
description: 'Podcast with trailer episode',
category: 'News',
company: 'News Inc',
companyEmail: 'news@example.com',
companyDomain: 'https://example.com',
itunesCategory: 'News',
itunesAuthor: 'Reporter',
itunesOwner: { name: 'Reporter', email: 'reporter@example.com' },
itunesImage: 'https://example.com/news.jpg',
itunesExplicit: false,
});
podcast.addEpisode({
title: 'Season 1 Trailer',
authorName: 'Reporter',
imageUrl: 'https://example.com/trailer.jpg',
timestamp: Date.now(),
url: 'https://example.com/episode/trailer',
content: 'Trailer for season 1',
audioUrl: 'https://example.com/audio/trailer.mp3',
audioType: 'audio/mpeg',
audioLength: 5000000,
itunesDuration: 300,
itunesEpisodeType: 'trailer',
itunesSeason: 1,
});
expect(podcast.episodes[0].itunesEpisodeType).toEqual('trailer');
});
tap.test('should add episode with bonus type', async () => {
const podcast = testSmartFeed.createPodcastFeed({
domain: 'bonus.example.com',
title: 'Podcast with Bonus',
description: 'Podcast with bonus episode',
category: 'Business',
company: 'Business Inc',
companyEmail: 'business@example.com',
companyDomain: 'https://example.com',
itunesCategory: 'Business',
itunesAuthor: 'Entrepreneur',
itunesOwner: { name: 'Entrepreneur', email: 'entrepreneur@example.com' },
itunesImage: 'https://example.com/business.jpg',
itunesExplicit: false,
});
podcast.addEpisode({
title: 'Bonus: Behind the Scenes',
authorName: 'Entrepreneur',
imageUrl: 'https://example.com/bonus.jpg',
timestamp: Date.now(),
url: 'https://example.com/episode/bonus',
content: 'Bonus behind the scenes content',
audioUrl: 'https://example.com/audio/bonus.mp3',
audioType: 'audio/mpeg',
audioLength: 8000000,
itunesDuration: 600,
itunesEpisodeType: 'bonus',
});
expect(podcast.episodes[0].itunesEpisodeType).toEqual('bonus');
});
tap.test('should support M4A audio format', async () => {
const podcast = testSmartFeed.createPodcastFeed({
domain: 'm4a.example.com',
title: 'M4A Podcast',
description: 'Podcast with M4A audio',
category: 'Music',
company: 'Music Inc',
companyEmail: 'music@example.com',
companyDomain: 'https://example.com',
itunesCategory: 'Music',
itunesAuthor: 'Musician',
itunesOwner: { name: 'Musician', email: 'musician@example.com' },
itunesImage: 'https://example.com/music.jpg',
itunesExplicit: false,
});
podcast.addEpisode({
title: 'Musical Episode',
authorName: 'Musician',
imageUrl: 'https://example.com/musical.jpg',
timestamp: Date.now(),
url: 'https://example.com/episode/musical',
content: 'A musical episode',
audioUrl: 'https://example.com/audio/episode.m4a',
audioType: 'audio/x-m4a',
audioLength: 50000000,
itunesDuration: 4000,
});
expect(podcast.episodes[0].audioType).toEqual('audio/x-m4a');
});
tap.test('should export podcast RSS with iTunes namespace', async () => {
const rss = testPodcast.exportPodcastRss();
expect(rss).toInclude('<?xml version="1.0" encoding="UTF-8"?>');
expect(rss).toInclude('xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"');
expect(rss).toInclude('<itunes:author>John Tester</itunes:author>');
expect(rss).toInclude('<itunes:owner>');
expect(rss).toInclude('<itunes:name>John Tester</itunes:name>');
expect(rss).toInclude('<itunes:email>john@example.com</itunes:email>');
expect(rss).toInclude('<itunes:category text="Technology"');
expect(rss).toInclude('<itunes:image href="https://example.com/podcast-artwork.jpg"');
expect(rss).toInclude('<itunes:explicit>false</itunes:explicit>');
});
tap.test('should include episode in RSS export', async () => {
const rss = testPodcast.exportPodcastRss();
expect(rss).toInclude('Episode 1: Introduction');
expect(rss).toInclude('https://example.com/audio/episode1.mp3');
expect(rss).toInclude('<enclosure url="https://example.com/audio/episode1.mp3"');
expect(rss).toInclude('length="45678900"');
expect(rss).toInclude('type="audio/mpeg"');
expect(rss).toInclude('<itunes:duration>01:00:00</itunes:duration>');
expect(rss).toInclude('<itunes:episode>1</itunes:episode>');
expect(rss).toInclude('<itunes:season>1</itunes:season>');
});
export default tap.start();

View File

@@ -1,407 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartfeed from '../ts/index.js';
let testSmartFeed: smartfeed.Smartfeed;
tap.test('setup', async () => {
testSmartFeed = new smartfeed.Smartfeed();
});
tap.test('should validate required podcast fields', async () => {
let errorThrown = false;
try {
testSmartFeed.createPodcastFeed({
domain: 'test.com',
title: 'Test',
description: 'Test',
category: 'Test',
company: 'Test Inc',
companyEmail: 'test@example.com',
companyDomain: 'https://example.com',
// Missing iTunes required fields
} as any);
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('validation failed');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate iTunes owner email', async () => {
let errorThrown = false;
try {
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: 'not-an-email' },
itunesImage: 'https://example.com/image.jpg',
itunesExplicit: false,
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('Invalid email');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate iTunes image URL', async () => {
let errorThrown = false;
try {
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: 'not-a-url',
itunesExplicit: false,
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('Invalid or relative URL');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate iTunes type', async () => {
let errorThrown = false;
try {
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,
itunesType: 'invalid' as any,
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('must be either "episodic" or "serial"');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate episode audio URL', async () => {
const podcast = testSmartFeed.createPodcastFeed({
domain: 'test.com',
title: 'Test Podcast',
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: 'not-a-url',
audioType: 'audio/mpeg',
audioLength: 1000000,
itunesDuration: 600,
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('Invalid or relative URL');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate audio type', async () => {
const podcast = testSmartFeed.createPodcastFeed({
domain: 'test.com',
title: 'Test Podcast',
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: 'video/mp4', // Wrong type!
audioLength: 1000000,
itunesDuration: 600,
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('Invalid audio type');
expect(error.message).toInclude('Must start with \'audio/\'');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate audio length', async () => {
const podcast = testSmartFeed.createPodcastFeed({
domain: 'test.com',
title: 'Test Podcast',
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: -100, // Invalid!
itunesDuration: 600,
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('must be a positive number');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate duration', async () => {
const podcast = testSmartFeed.createPodcastFeed({
domain: 'test.com',
title: 'Test Podcast',
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: 0, // Invalid!
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('duration must be a positive number');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate episode type', async () => {
const podcast = testSmartFeed.createPodcastFeed({
domain: 'test.com',
title: 'Test Podcast',
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,
itunesEpisodeType: 'invalid' as any,
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('must be "full", "trailer", or "bonus"');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate episode number', async () => {
const podcast = testSmartFeed.createPodcastFeed({
domain: 'test.com',
title: 'Test Podcast',
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,
itunesEpisode: 0, // Invalid!
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('episode number must be a positive integer');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate season number', async () => {
const podcast = testSmartFeed.createPodcastFeed({
domain: 'test.com',
title: 'Test Podcast',
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,
itunesSeason: -1, // Invalid!
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('season number must be a positive integer');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate duplicate episode IDs', async () => {
const podcast = testSmartFeed.createPodcastFeed({
domain: 'test.com',
title: 'Test Podcast',
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,
});
const episodeData = {
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,
};
podcast.addEpisode(episodeData);
let errorThrown = false;
try {
podcast.addEpisode(episodeData);
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('Duplicate episode ID');
}
expect(errorThrown).toEqual(true);
});
export default tap.start();