feat(smartfeed): Implement Smartfeed core: add feed validation, parsing, exporting and comprehensive tests
This commit is contained in:
101
test/test.creation.node+bun+deno.ts
Normal file
101
test/test.creation.node+bun+deno.ts
Normal 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();
|
||||
141
test/test.export.node+bun+deno.ts
Normal file
141
test/test.export.node+bun+deno.ts
Normal 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();
|
||||
74
test/test.integration.node+bun+deno.ts
Normal file
74
test/test.integration.node+bun+deno.ts
Normal 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();
|
||||
179
test/test.parsing.node+bun+deno.ts
Normal file
179
test/test.parsing.node+bun+deno.ts
Normal 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();
|
||||
260
test/test.podcast.node+bun+deno.ts
Normal file
260
test/test.podcast.node+bun+deno.ts
Normal 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();
|
||||
42
test/test.ts
42
test/test.ts
@@ -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();
|
||||
271
test/test.validation.node+bun+deno.ts
Normal file
271
test/test.validation.node+bun+deno.ts
Normal 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();
|
||||
Reference in New Issue
Block a user