Release 0.7.0
This commit is contained in:
1 parent
d586fbd8ce
commit
6d96900d81
27 files changed
+2060
-25
No files matched your search
@@ -0,0 +1,134 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { closeDatabase, getDatabase } from './database.js';
|
||||
import {
|
||||
discardFeedEntries,
|
||||
fetchDueFeeds,
|
||||
fetchFeed,
|
||||
importFeedEntries,
|
||||
listFeedEntries,
|
||||
saveFeedSource
|
||||
} from './feeds.js';
|
||||
import { getPiece } from './content.js';
|
||||
|
||||
vi.mock('node:dns/promises', () => ({
|
||||
lookup: vi.fn(async () => [{ address: '93.184.216.34', family: 4 }])
|
||||
}));
|
||||
|
||||
const validFeed = `<?xml version="1.0"?><rss version="2.0"><channel><title>Test</title><item>
|
||||
<guid>entry-1</guid><title>Ein Beitrag</title><link>https://example.org/post</link>
|
||||
<description><![CDATA[<p>Kurz <strong>und</strong> gut.</p><script>evil()</script>]]></description>
|
||||
<pubDate>Sat, 24 Oct 2026 12:00:00 GMT</pubDate></item></channel></rss>`;
|
||||
|
||||
let directory: string;
|
||||
|
||||
beforeEach(() => {
|
||||
directory = mkdtempSync(path.join(tmpdir(), 'sendewerk-feed-test-'));
|
||||
process.env.SENDEWERK_DB_PATH = path.join(directory, 'test.sqlite');
|
||||
process.env.SENDEWERK_UPLOAD_DIR = path.join(directory, 'uploads');
|
||||
execFileSync(process.execPath, ['scripts/migrate.mjs'], { cwd: process.cwd(), env: process.env });
|
||||
execFileSync(process.execPath, ['scripts/seed.mjs'], { cwd: process.cwd(), env: process.env });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
closeDatabase();
|
||||
delete process.env.SENDEWERK_DB_PATH;
|
||||
delete process.env.SENDEWERK_UPLOAD_DIR;
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('RSS import inbox', () => {
|
||||
it('keeps one entry across fetches and preserves a discard decision', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(
|
||||
async () => new Response(validFeed, { headers: { 'content-type': 'application/rss+xml' } })
|
||||
)
|
||||
);
|
||||
const feedId = saveFeedSource(null, {
|
||||
name: 'Feed',
|
||||
url: 'https://example.org/feed.xml',
|
||||
isEnabled: true,
|
||||
refreshIntervalMinutes: 60,
|
||||
defaultCategoryId: null
|
||||
});
|
||||
await fetchFeed(feedId);
|
||||
const [entry] = listFeedEntries();
|
||||
expect(entry.title).toBe('Ein Beitrag');
|
||||
expect(discardFeedEntries([entry.id])).toBe(1);
|
||||
await fetchFeed(feedId);
|
||||
expect(listFeedEntries()).toHaveLength(0);
|
||||
expect(listFeedEntries('discarded')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('sanitizes foreign HTML and retains the original source on explicit import', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(async () => new Response(validFeed))
|
||||
);
|
||||
const feedId = saveFeedSource(null, {
|
||||
name: 'Feed',
|
||||
url: 'https://example.org/feed.xml',
|
||||
isEnabled: true,
|
||||
refreshIntervalMinutes: 60,
|
||||
defaultCategoryId: 'cat-jc'
|
||||
});
|
||||
await fetchFeed(feedId);
|
||||
const [entry] = listFeedEntries();
|
||||
const [pieceId] = importFeedEntries([entry.id], []);
|
||||
const piece = getPiece(pieceId)!;
|
||||
expect(piece.summary).toContain('Kurz und gut.');
|
||||
expect(piece.summary).not.toContain('<script>');
|
||||
expect(piece.sources).toEqual([
|
||||
{ label: 'RSS: Originalbeitrag', url: 'https://example.org/post', timecodeSeconds: null }
|
||||
]);
|
||||
expect(piece.categoryIds).toEqual(['cat-jc']);
|
||||
});
|
||||
|
||||
it('isolates a broken feed while fetching other due feeds', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(
|
||||
async (url: URL) => new Response(url.href.includes('broken') ? '<not-feed />' : validFeed)
|
||||
)
|
||||
);
|
||||
saveFeedSource(null, {
|
||||
name: 'A broken',
|
||||
url: 'https://example.org/broken.xml',
|
||||
isEnabled: true,
|
||||
refreshIntervalMinutes: 60,
|
||||
defaultCategoryId: null
|
||||
});
|
||||
saveFeedSource(null, {
|
||||
name: 'B valid',
|
||||
url: 'https://example.org/valid.xml',
|
||||
isEnabled: true,
|
||||
refreshIntervalMinutes: 60,
|
||||
defaultCategoryId: null
|
||||
});
|
||||
await fetchDueFeeds();
|
||||
expect(listFeedEntries()).toHaveLength(1);
|
||||
const errors = getDatabase()
|
||||
.prepare("SELECT count(*) AS count FROM feed_sources WHERE last_error <> ''")
|
||||
.get() as { count: number };
|
||||
expect(errors.count).toBe(1);
|
||||
});
|
||||
|
||||
it('blocks loopback targets before sending a request', async () => {
|
||||
const request = vi.fn();
|
||||
vi.stubGlobal('fetch', request);
|
||||
const feedId = saveFeedSource(null, {
|
||||
name: 'Local',
|
||||
url: 'http://127.0.0.1/feed.xml',
|
||||
isEnabled: false,
|
||||
refreshIntervalMinutes: 60,
|
||||
defaultCategoryId: null
|
||||
});
|
||||
await expect(fetchFeed(feedId)).rejects.toThrow('FEED_BLOCKED');
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user