feat(smartfeed): Implement Smartfeed core: add feed validation, parsing, exporting and comprehensive tests

This commit is contained in:
2025-10-31 19:17:04 +00:00
parent 6d9538c5d2
commit c27a46ac62
15 changed files with 9258 additions and 57 deletions

View File

@@ -0,0 +1,101 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartfeed from '../ts/index.js';
tap.test('should create a Smartfeed instance', async () => {
const testSmartFeed = new smartfeed.Smartfeed();
expect(testSmartFeed).toBeInstanceOf(smartfeed.Smartfeed);
});
tap.test('should create a feed with valid options', async () => {
const testSmartFeed = new smartfeed.Smartfeed();
const feed = testSmartFeed.createFeed({
domain: 'example.com',
title: 'Example Feed',
category: 'Technology',
company: 'Example Inc',
companyDomain: 'https://example.com',
companyEmail: 'hello@example.com',
description: 'An example technology feed',
});
expect(feed).toBeInstanceOf(smartfeed.Feed);
expect(feed.options.domain).toEqual('example.com');
expect(feed.options.title).toEqual('Example Feed');
});
tap.test('should create a feed with HTTPS URLs', async () => {
const testSmartFeed = new smartfeed.Smartfeed();
const feed = testSmartFeed.createFeed({
domain: 'secure.example.com',
title: 'Secure Feed',
category: 'Security',
company: 'Secure Inc',
companyDomain: 'https://secure.example.com',
companyEmail: 'security@example.com',
description: 'A secure feed',
});
expect(feed.options.companyDomain).toEqual('https://secure.example.com');
});
tap.test('should add items to feed', async () => {
const testSmartFeed = new smartfeed.Smartfeed();
const feed = testSmartFeed.createFeed({
domain: 'blog.example.com',
title: 'Example Blog',
category: 'Blogging',
company: 'Example Inc',
companyDomain: 'https://example.com',
companyEmail: 'blog@example.com',
description: 'A blog about examples',
});
feed.addItem({
title: 'First Post',
authorName: 'John Doe',
imageUrl: 'https://example.com/image1.jpg',
timestamp: Date.now(),
url: 'https://example.com/posts/first',
content: 'This is the first post',
});
feed.addItem({
title: 'Second Post',
authorName: 'Jane Doe',
imageUrl: 'https://example.com/image2.jpg',
timestamp: Date.now(),
url: 'https://example.com/posts/second',
content: 'This is the second post',
});
expect(feed.items.length).toEqual(2);
expect(feed.items[0].title).toEqual('First Post');
expect(feed.items[1].title).toEqual('Second Post');
});
tap.test('should add items with custom IDs', async () => {
const testSmartFeed = new smartfeed.Smartfeed();
const feed = testSmartFeed.createFeed({
domain: 'example.com',
title: 'Example Feed',
category: 'Technology',
company: 'Example Inc',
companyDomain: 'https://example.com',
companyEmail: 'hello@example.com',
description: 'An example feed',
});
feed.addItem({
title: 'Post with custom ID',
authorName: 'John Doe',
imageUrl: 'https://example.com/image.jpg',
timestamp: Date.now(),
url: 'https://example.com/posts/custom',
content: 'This post has a custom ID',
id: 'custom-id-123',
});
expect(feed.items.length).toEqual(1);
});
export default tap.start();

View File

@@ -0,0 +1,141 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartfeed from '../ts/index.js';
let testSmartFeed: smartfeed.Smartfeed;
let testFeed: smartfeed.Feed;
tap.test('setup', async () => {
testSmartFeed = new smartfeed.Smartfeed();
testFeed = testSmartFeed.createFeed({
domain: 'central.eu',
title: 'central.eu - ideas for Europe',
category: 'Politics',
company: 'Lossless GmbH',
companyDomain: 'https://lossless.com',
companyEmail: 'hello@lossless.com',
description: 'ideas for Europe',
});
testFeed.addItem({
title: 'A better European Union',
authorName: 'Phil',
imageUrl: 'https://central.eu/someimage.png',
timestamp: Date.now(),
url: 'https://central.eu/article/somearticle',
content: 'somecontent',
});
});
tap.test('should export RSS 2.0 feed', async () => {
const rssFeed = testFeed.exportRssFeedString();
expect(rssFeed).toInclude('<?xml version="1.0" encoding="utf-8"?>');
expect(rssFeed).toInclude('<rss version="2.0"');
expect(rssFeed).toInclude('<title>central.eu - ideas for Europe</title>');
expect(rssFeed).toInclude('<link>https://central.eu</link>');
expect(rssFeed).toInclude('<description>ideas for Europe</description>');
expect(rssFeed).toInclude('<generator>@push.rocks/smartfeed</generator>');
expect(rssFeed).toInclude('<category>Politics</category>');
expect(rssFeed).toInclude('<item>');
expect(rssFeed).toInclude('A better European Union');
});
tap.test('should export Atom 1.0 feed', async () => {
const atomFeed = testFeed.exportAtomFeed();
expect(atomFeed).toInclude('<?xml version="1.0" encoding="utf-8"?>');
expect(atomFeed).toInclude('<feed xmlns="http://www.w3.org/2005/Atom">');
expect(atomFeed).toInclude('<title>central.eu - ideas for Europe</title>');
expect(atomFeed).toInclude('<subtitle>ideas for Europe</subtitle>');
expect(atomFeed).toInclude('<generator>@push.rocks/smartfeed</generator>');
expect(atomFeed).toInclude('<entry>');
expect(atomFeed).toInclude('A better European Union');
});
tap.test('should export JSON Feed 1.0', async () => {
const jsonFeedString = testFeed.exportJsonFeed();
const jsonFeed = JSON.parse(jsonFeedString);
expect(jsonFeed.version).toEqual('https://jsonfeed.org/version/1');
expect(jsonFeed.title).toEqual('central.eu - ideas for Europe');
expect(jsonFeed.home_page_url).toEqual('https://central.eu');
expect(jsonFeed.description).toEqual('ideas for Europe');
expect(jsonFeed.items).toBeArray();
expect(jsonFeed.items.length).toEqual(1);
expect(jsonFeed.items[0].title).toEqual('A better European Union');
});
tap.test('should include correct item data in RSS', async () => {
const rssFeed = testFeed.exportRssFeedString();
expect(rssFeed).toInclude('https://central.eu/article/somearticle');
expect(rssFeed).toInclude('https://central.eu/someimage.png');
expect(rssFeed).toInclude('somecontent');
expect(rssFeed).toInclude('<enclosure');
});
tap.test('should include correct author information', async () => {
const rssFeed = testFeed.exportRssFeedString();
const atomFeed = testFeed.exportAtomFeed();
// RSS doesn't always include dc:creator, but Atom should have author
expect(atomFeed).toInclude('<name>Phil</name>');
});
tap.test('should handle multiple items in export', async () => {
const feed = testSmartFeed.createFeed({
domain: 'multi.example.com',
title: 'Multi-item Feed',
category: 'Test',
company: 'Test Inc',
companyDomain: 'https://example.com',
companyEmail: 'test@example.com',
description: 'Testing multiple items',
});
for (let i = 1; i <= 5; i++) {
feed.addItem({
title: `Article ${i}`,
authorName: 'Author',
imageUrl: `https://example.com/image${i}.png`,
timestamp: Date.now() + i,
url: `https://example.com/article/${i}`,
content: `Content for article ${i}`,
});
}
const rssFeed = feed.exportRssFeedString();
const jsonFeedString = feed.exportJsonFeed();
const jsonFeed = JSON.parse(jsonFeedString);
expect(jsonFeed.items.length).toEqual(5);
expect(rssFeed).toInclude('Article 1');
expect(rssFeed).toInclude('Article 5');
});
tap.test('should export feed with custom item IDs', async () => {
const feed = testSmartFeed.createFeed({
domain: 'example.com',
title: 'Custom ID Feed',
category: 'Test',
company: 'Test Inc',
companyDomain: 'https://example.com',
companyEmail: 'test@example.com',
description: 'Testing custom IDs',
});
feed.addItem({
title: 'Article with custom ID',
authorName: 'Author',
imageUrl: 'https://example.com/image.png',
timestamp: Date.now(),
url: 'https://example.com/article/custom',
content: 'Content',
id: 'custom-uuid-123',
});
const rssFeed = feed.exportRssFeedString();
expect(rssFeed).toInclude('custom-uuid-123');
});
export default tap.start();

View File

@@ -0,0 +1,74 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartfeed from '../ts/index.js';
/**
* Integration test - Quick smoke test for the entire module
* For detailed tests, see:
* - test.creation.node+bun+deno.ts
* - test.validation.node+bun+deno.ts
* - test.export.node+bun+deno.ts
* - test.parsing.node+bun+deno.ts
*/
tap.test('integration: should create, populate, export, and parse a feed', async () => {
// Create instance
const smartfeedInstance = new smartfeed.Smartfeed();
expect(smartfeedInstance).toBeInstanceOf(smartfeed.Smartfeed);
// Create feed
const feed = smartfeedInstance.createFeed({
domain: 'example.com',
title: 'Integration Test Feed',
category: 'Technology',
company: 'Test Inc',
companyDomain: 'https://example.com',
companyEmail: 'test@example.com',
description: 'Full integration test',
});
expect(feed).toBeInstanceOf(smartfeed.Feed);
// Add items
feed.addItem({
title: 'Test Article 1',
authorName: 'Author',
imageUrl: 'https://example.com/image1.jpg',
timestamp: Date.now(),
url: 'https://example.com/article1',
content: 'Content 1',
});
feed.addItem({
title: 'Test Article 2',
authorName: 'Author',
imageUrl: 'https://example.com/image2.jpg',
timestamp: Date.now(),
url: 'https://example.com/article2',
content: 'Content 2',
});
expect(feed.items.length).toEqual(2);
// Export RSS
const rssString = feed.exportRssFeedString();
expect(rssString).toInclude('Integration Test Feed');
expect(rssString).toInclude('Test Article 1');
expect(rssString).toInclude('Test Article 2');
// Export Atom
const atomString = feed.exportAtomFeed();
expect(atomString).toInclude('Integration Test Feed');
// Export JSON
const jsonString = feed.exportJsonFeed();
const jsonFeed = JSON.parse(jsonString);
expect(jsonFeed.title).toEqual('Integration Test Feed');
expect(jsonFeed.items.length).toEqual(2);
// Parse back
const parsed = await smartfeedInstance.parseFeedFromString(rssString);
expect(parsed.title).toEqual('Integration Test Feed');
expect(parsed.items.length).toEqual(2);
});
export default tap.start();

View File

@@ -0,0 +1,179 @@
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 parse RSS feed from string', async () => {
const feed = testSmartFeed.createFeed({
domain: 'example.com',
title: 'Example Feed',
category: 'Test',
company: 'Test Inc',
companyDomain: 'https://example.com',
companyEmail: 'test@example.com',
description: 'Test description',
});
feed.addItem({
title: 'Test Article',
authorName: 'John Doe',
imageUrl: 'https://example.com/image.png',
timestamp: Date.now(),
url: 'https://example.com/article/test',
content: 'Test content',
});
const rssFeed = feed.exportRssFeedString();
const parsedFeed = await testSmartFeed.parseFeedFromString(rssFeed);
expect(parsedFeed.title).toEqual('Example Feed');
expect(parsedFeed.description).toEqual('Test description');
expect(parsedFeed.items).toBeArray();
expect(parsedFeed.items.length).toEqual(1);
expect(parsedFeed.items[0].title).toEqual('Test Article');
});
tap.test('should parse Atom feed from string', async () => {
const feed = testSmartFeed.createFeed({
domain: 'example.com',
title: 'Atom Test Feed',
category: 'Test',
company: 'Test Inc',
companyDomain: 'https://example.com',
companyEmail: 'test@example.com',
description: 'Atom test description',
});
feed.addItem({
title: 'Atom Article',
authorName: 'Jane Doe',
imageUrl: 'https://example.com/atom-image.png',
timestamp: Date.now(),
url: 'https://example.com/article/atom-test',
content: 'Atom content',
});
const atomFeed = feed.exportAtomFeed();
const parsedFeed = await testSmartFeed.parseFeedFromString(atomFeed);
expect(parsedFeed.title).toEqual('Atom Test Feed');
expect(parsedFeed.items).toBeArray();
expect(parsedFeed.items.length).toEqual(1);
expect(parsedFeed.items[0].title).toEqual('Atom Article');
});
tap.test('should parse feed with multiple items', async () => {
const feed = testSmartFeed.createFeed({
domain: 'example.com',
title: 'Multi-item Feed',
category: 'Test',
company: 'Test Inc',
companyDomain: 'https://example.com',
companyEmail: 'test@example.com',
description: 'Feed with multiple items',
});
const itemCount = 10;
for (let i = 1; i <= itemCount; i++) {
feed.addItem({
title: `Article ${i}`,
authorName: `Author ${i}`,
imageUrl: `https://example.com/image${i}.png`,
timestamp: Date.now() + i,
url: `https://example.com/article/${i}`,
content: `Content ${i}`,
});
}
const rssFeed = feed.exportRssFeedString();
const parsedFeed = await testSmartFeed.parseFeedFromString(rssFeed);
expect(parsedFeed.items.length).toEqual(itemCount);
expect(parsedFeed.items[0].title).toEqual('Article 1');
expect(parsedFeed.items[9].title).toEqual('Article 10');
});
tap.test('should parse live RSS feed from URL', async () => {
try {
const parsedFeed = await testSmartFeed.parseFeedFromUrl(
'https://www.theverge.com/rss/index.xml'
);
expect(parsedFeed).toBeObject();
expect(parsedFeed.title).toBeString();
expect(parsedFeed.items).toBeArray();
expect(parsedFeed.items.length).toBeGreaterThan(0);
} catch (error) {
// Network errors are acceptable in tests
console.log('Network test skipped (expected in CI/offline):', error.message);
}
});
tap.test('should preserve feed metadata when parsing', async () => {
const feed = testSmartFeed.createFeed({
domain: 'meta.example.com',
title: 'Metadata Test Feed',
category: 'Metadata',
company: 'Metadata Inc',
companyDomain: 'https://meta.example.com',
companyEmail: 'meta@example.com',
description: 'Testing metadata preservation',
});
feed.addItem({
title: 'Meta Article',
authorName: 'Meta Author',
imageUrl: 'https://example.com/meta.png',
timestamp: Date.now(),
url: 'https://example.com/meta',
content: 'Meta content',
});
const rssFeed = feed.exportRssFeedString();
const parsedFeed = await testSmartFeed.parseFeedFromString(rssFeed);
expect(parsedFeed.title).toEqual('Metadata Test Feed');
expect(parsedFeed.description).toEqual('Testing metadata preservation');
expect(parsedFeed.language).toEqual('en');
expect(parsedFeed.generator).toEqual('@push.rocks/smartfeed');
expect(parsedFeed.copyright).toInclude('Metadata Inc');
});
tap.test('should handle feed with enclosures', async () => {
const feed = testSmartFeed.createFeed({
domain: 'example.com',
title: 'Enclosure Test',
category: 'Test',
company: 'Test Inc',
companyDomain: 'https://example.com',
companyEmail: 'test@example.com',
description: 'Testing enclosures',
});
feed.addItem({
title: 'Article with image',
authorName: 'Author',
imageUrl: 'https://example.com/large-image.jpg',
timestamp: Date.now(),
url: 'https://example.com/article',
content: 'Content with enclosure',
});
const rssFeed = feed.exportRssFeedString();
const parsedFeed = await testSmartFeed.parseFeedFromString(rssFeed);
// Enclosure support depends on the parser and feed format
expect(parsedFeed.items.length).toBeGreaterThan(0);
expect(parsedFeed.items[0].title).toEqual('Article with image');
// If enclosure exists, verify it
if (parsedFeed.items[0].enclosure) {
expect(parsedFeed.items[0].enclosure.url).toInclude('https://example.com/large-image.jpg');
}
});
export default tap.start();

View File

@@ -0,0 +1,260 @@
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,
});
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,42 +0,0 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartfeed from '../ts/index.js';
let testSmartFeed: smartfeed.Smartfeed;
tap.test('should create a feedVersion', async () => {
testSmartFeed = new smartfeed.Smartfeed();
expect(testSmartFeed).toBeInstanceOf(smartfeed.Smartfeed);
});
tap.test('should create a feed', async () => {
const feed = testSmartFeed.createFeed({
domain: 'central.eu',
title: 'central.eu - ideas for Europe',
category: 'Politics',
company: 'Lossless GmbH',
companyDomain: 'https://lossless.com',
companyEmail: 'hello@lossless.com',
description: 'ideas for Europe',
});
feed.addItem({
title: 'A better European Union',
authorName: 'Phil',
imageUrl: 'https://central.eu/someimage.png',
timestamp: Date.now(),
url: 'https://central.eu/article/somearticle',
content: 'somecontent',
});
const rssFeed = feed.exportRssFeedString();
console.log(rssFeed);
const parsedFeed = await testSmartFeed.parseFeedFromString(rssFeed);
console.log(parsedFeed);
});
tap.test('should parse a Url', async () => {
const result = await testSmartFeed.parseFeedFromUrl(
'https://www.theverge.com/rss/index.xml',
);
// console.log(result);
});
tap.start();

View File

@@ -0,0 +1,271 @@
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 feed options', async () => {
let errorThrown = false;
try {
testSmartFeed.createFeed({
domain: '',
title: '',
description: '',
category: '',
company: '',
companyEmail: '',
companyDomain: '',
} as any);
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('validation failed');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate domain format', async () => {
let errorThrown = false;
try {
testSmartFeed.createFeed({
domain: 'not a domain!',
title: 'Test',
description: 'Test',
category: 'Test',
company: 'Test Inc',
companyEmail: 'test@example.com',
companyDomain: 'https://example.com',
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('Invalid domain format');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate email format', async () => {
let errorThrown = false;
try {
testSmartFeed.createFeed({
domain: 'example.com',
title: 'Test',
description: 'Test',
category: 'Test',
company: 'Test Inc',
companyEmail: 'not-an-email',
companyDomain: 'https://example.com',
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('Invalid email');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate company domain URL', async () => {
let errorThrown = false;
try {
testSmartFeed.createFeed({
domain: 'example.com',
title: 'Test',
description: 'Test',
category: 'Test',
company: 'Test Inc',
companyEmail: 'test@example.com',
companyDomain: 'not-a-url',
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('Invalid or relative URL');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate item URLs', async () => {
const feed = testSmartFeed.createFeed({
domain: 'example.com',
title: 'Test Feed',
category: 'Test',
company: 'Test Inc',
companyDomain: 'https://example.com',
companyEmail: 'test@example.com',
description: 'Test description',
});
let errorThrown = false;
try {
feed.addItem({
title: 'Test',
authorName: 'John',
imageUrl: 'not-a-url',
timestamp: Date.now(),
url: 'also-not-a-url',
content: 'content',
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('Invalid or relative URL');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate relative URLs', async () => {
const feed = testSmartFeed.createFeed({
domain: 'example.com',
title: 'Test Feed',
category: 'Test',
company: 'Test Inc',
companyDomain: 'https://example.com',
companyEmail: 'test@example.com',
description: 'Test description',
});
let errorThrown = false;
try {
feed.addItem({
title: 'Test',
authorName: 'John',
imageUrl: '/images/test.jpg',
timestamp: Date.now(),
url: '/posts/test',
content: 'content',
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('Invalid or relative URL');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate duplicate IDs', async () => {
const feed = testSmartFeed.createFeed({
domain: 'example.com',
title: 'Test Feed',
category: 'Test',
company: 'Test Inc',
companyDomain: 'https://example.com',
companyEmail: 'test@example.com',
description: 'Test description',
});
const itemData = {
title: 'Test Article',
authorName: 'John',
imageUrl: 'https://example.com/image.png',
timestamp: Date.now(),
url: 'https://example.com/article/test',
content: 'content',
};
feed.addItem(itemData);
// Try to add same item again (same URL = same ID)
let errorThrown = false;
try {
feed.addItem(itemData);
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('Duplicate item ID');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate duplicate custom IDs', async () => {
const feed = testSmartFeed.createFeed({
domain: 'example.com',
title: 'Test Feed',
category: 'Test',
company: 'Test Inc',
companyDomain: 'https://example.com',
companyEmail: 'test@example.com',
description: 'Test description',
});
feed.addItem({
title: 'First Article',
authorName: 'John',
imageUrl: 'https://example.com/image1.png',
timestamp: Date.now(),
url: 'https://example.com/article/first',
content: 'first content',
id: 'custom-id-1',
});
// Try to add another item with same custom ID
let errorThrown = false;
try {
feed.addItem({
title: 'Second Article',
authorName: 'Jane',
imageUrl: 'https://example.com/image2.png',
timestamp: Date.now(),
url: 'https://example.com/article/second',
content: 'second content',
id: 'custom-id-1',
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('Duplicate item ID');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate timestamp', async () => {
const feed = testSmartFeed.createFeed({
domain: 'example.com',
title: 'Test Feed',
category: 'Test',
company: 'Test Inc',
companyDomain: 'https://example.com',
companyEmail: 'test@example.com',
description: 'Test description',
});
let errorThrown = false;
try {
feed.addItem({
title: 'Test',
authorName: 'John',
imageUrl: 'https://example.com/image.png',
timestamp: -1,
url: 'https://example.com/article/test',
content: 'content',
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('cannot be negative');
}
expect(errorThrown).toEqual(true);
});
tap.test('should validate required item fields', async () => {
const feed = testSmartFeed.createFeed({
domain: 'example.com',
title: 'Test Feed',
category: 'Test',
company: 'Test Inc',
companyDomain: 'https://example.com',
companyEmail: 'test@example.com',
description: 'Test description',
});
let errorThrown = false;
try {
feed.addItem({
title: '',
authorName: '',
imageUrl: '',
timestamp: Date.now(),
url: '',
content: '',
} as any);
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('validation failed');
}
expect(errorThrown).toEqual(true);
});
export default tap.start();