mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-06-18 10:19:03 -04:00
240 lines
8.1 KiB
TypeScript
240 lines
8.1 KiB
TypeScript
/**
|
|
* Tests for the database announcements parser. Verifies frontmatter +
|
|
* markdown body extraction, validation, and the "skip + log invalid files"
|
|
* behaviour. Pure filesystem operations, no DB.
|
|
*/
|
|
|
|
import { BaseTest } from '../../base/BaseTest.ts';
|
|
import { assertEquals, assertExists } from '@std/assert';
|
|
import { parseAnnouncementsDir } from '$announcements/database/parser.ts';
|
|
|
|
const VALID = `---
|
|
title: Migration to v2
|
|
severity: warning
|
|
published_at: 2026-04-20T10:00:00Z
|
|
expires_at: 2026-05-20T10:00:00Z
|
|
link: https://example.com/migration
|
|
---
|
|
|
|
# Migration guide
|
|
|
|
Run \`tool x\` then \`tool y\`.
|
|
`;
|
|
|
|
class DatabaseAnnouncementsParserTest extends BaseTest {
|
|
private async writeAnnouncement(
|
|
tempDir: string,
|
|
filename: string,
|
|
content: string
|
|
): Promise<void> {
|
|
const dir = `${tempDir}/announcements`;
|
|
await Deno.mkdir(dir, { recursive: true });
|
|
await Deno.writeTextFile(`${dir}/${filename}`, content);
|
|
}
|
|
|
|
runTests(): void {
|
|
// ─── Empty / missing directory ───────────────────────────────────────
|
|
|
|
this.test('missing announcements directory returns empty result', async (ctx) => {
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.parsed, []);
|
|
assertEquals(result.errors, []);
|
|
});
|
|
|
|
this.test('empty announcements directory returns empty result', async (ctx) => {
|
|
await Deno.mkdir(`${ctx.tempDir}/announcements`, { recursive: true });
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.parsed, []);
|
|
assertEquals(result.errors, []);
|
|
});
|
|
|
|
// ─── Valid files ────────────────────────────────────────────────────
|
|
|
|
this.test('single valid file parses', async (ctx) => {
|
|
await this.writeAnnouncement(ctx.tempDir, '01HXYZ0000000000000000000A.md', VALID);
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
|
|
assertEquals(result.errors, []);
|
|
assertEquals(result.parsed.length, 1);
|
|
|
|
const a = result.parsed[0];
|
|
assertEquals(a.id, '01HXYZ0000000000000000000A');
|
|
assertEquals(a.title, 'Migration to v2');
|
|
assertEquals(a.severity, 'warning');
|
|
assertEquals(a.published_at, '2026-04-20T10:00:00Z');
|
|
assertEquals(a.expires_at, '2026-05-20T10:00:00Z');
|
|
assertEquals(a.link, 'https://example.com/migration');
|
|
assertEquals(a.body, '# Migration guide\n\nRun `tool x` then `tool y`.\n');
|
|
});
|
|
|
|
this.test('id is derived from filename without .md extension', async (ctx) => {
|
|
await this.writeAnnouncement(ctx.tempDir, 'custom-id-name.md', VALID);
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.parsed[0].id, 'custom-id-name');
|
|
});
|
|
|
|
this.test('optional fields default to null when absent', async (ctx) => {
|
|
const minimal = `---
|
|
title: Hello
|
|
severity: info
|
|
published_at: 2026-04-20T10:00:00Z
|
|
---
|
|
|
|
Body here.
|
|
`;
|
|
await this.writeAnnouncement(ctx.tempDir, 'minimal.md', minimal);
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.errors, []);
|
|
assertEquals(result.parsed[0].expires_at, null);
|
|
assertEquals(result.parsed[0].link, null);
|
|
});
|
|
|
|
this.test('multiple valid files all parse', async (ctx) => {
|
|
await this.writeAnnouncement(ctx.tempDir, 'a.md', VALID);
|
|
await this.writeAnnouncement(ctx.tempDir, 'b.md', VALID);
|
|
await this.writeAnnouncement(ctx.tempDir, 'c.md', VALID);
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.errors, []);
|
|
assertEquals(result.parsed.length, 3);
|
|
const ids = result.parsed.map((p) => p.id).sort();
|
|
assertEquals(ids, ['a', 'b', 'c']);
|
|
});
|
|
|
|
this.test('body preserves multi-line markdown verbatim', async (ctx) => {
|
|
const multiline = `---
|
|
title: Multi
|
|
severity: info
|
|
published_at: 2026-04-20T10:00:00Z
|
|
---
|
|
|
|
Line 1.
|
|
|
|
\`\`\`bash
|
|
deno task dev
|
|
\`\`\`
|
|
|
|
- bullet
|
|
- list
|
|
`;
|
|
await this.writeAnnouncement(ctx.tempDir, 'm.md', multiline);
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.errors, []);
|
|
assertEquals(
|
|
result.parsed[0].body,
|
|
'Line 1.\n\n```bash\ndeno task dev\n```\n\n- bullet\n- list\n'
|
|
);
|
|
});
|
|
|
|
this.test('empty body is allowed', async (ctx) => {
|
|
const noBody = `---
|
|
title: No body
|
|
severity: info
|
|
published_at: 2026-04-20T10:00:00Z
|
|
---
|
|
`;
|
|
await this.writeAnnouncement(ctx.tempDir, 'nb.md', noBody);
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.errors, []);
|
|
assertEquals(result.parsed[0].body, '');
|
|
});
|
|
|
|
// ─── Validation errors (skip + log) ──────────────────────────────────
|
|
|
|
this.test('missing required field title is reported and skipped', async (ctx) => {
|
|
const noTitle = `---
|
|
severity: info
|
|
published_at: 2026-04-20T10:00:00Z
|
|
---
|
|
Body.
|
|
`;
|
|
await this.writeAnnouncement(ctx.tempDir, 'broken.md', noTitle);
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.parsed, []);
|
|
assertEquals(result.errors.length, 1);
|
|
assertEquals(result.errors[0].filename, 'broken.md');
|
|
});
|
|
|
|
this.test('invalid severity is reported and skipped', async (ctx) => {
|
|
const badSev = `---
|
|
title: Bad
|
|
severity: catastrophic
|
|
published_at: 2026-04-20T10:00:00Z
|
|
---
|
|
Body.
|
|
`;
|
|
await this.writeAnnouncement(ctx.tempDir, 'bad-sev.md', badSev);
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.parsed, []);
|
|
assertEquals(result.errors.length, 1);
|
|
});
|
|
|
|
this.test('unparseable published_at is reported and skipped', async (ctx) => {
|
|
const bad = `---
|
|
title: Bad date
|
|
severity: info
|
|
published_at: not-a-date
|
|
---
|
|
Body.
|
|
`;
|
|
await this.writeAnnouncement(ctx.tempDir, 'bad-date.md', bad);
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.parsed, []);
|
|
assertEquals(result.errors.length, 1);
|
|
});
|
|
|
|
this.test('malformed YAML frontmatter is reported and skipped', async (ctx) => {
|
|
const broken = `---
|
|
title: "unclosed quote
|
|
severity: info
|
|
published_at: 2026-04-20T10:00:00Z
|
|
---
|
|
Body.
|
|
`;
|
|
await this.writeAnnouncement(ctx.tempDir, 'mal.md', broken);
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.parsed, []);
|
|
assertEquals(result.errors.length, 1);
|
|
});
|
|
|
|
this.test('missing frontmatter delimiter is reported and skipped', async (ctx) => {
|
|
await this.writeAnnouncement(ctx.tempDir, 'no-fm.md', '# Just a heading\n\nNo frontmatter.');
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.parsed, []);
|
|
assertEquals(result.errors.length, 1);
|
|
});
|
|
|
|
this.test('one bad file does not stop other files from parsing', async (ctx) => {
|
|
await this.writeAnnouncement(ctx.tempDir, 'good.md', VALID);
|
|
await this.writeAnnouncement(ctx.tempDir, 'bad.md', '# no frontmatter');
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.parsed.length, 1);
|
|
assertEquals(result.parsed[0].id, 'good');
|
|
assertEquals(result.errors.length, 1);
|
|
assertEquals(result.errors[0].filename, 'bad.md');
|
|
});
|
|
|
|
// ─── Non-target files ───────────────────────────────────────────────
|
|
|
|
this.test('non-.md files are ignored', async (ctx) => {
|
|
await this.writeAnnouncement(ctx.tempDir, 'real.md', VALID);
|
|
await this.writeAnnouncement(ctx.tempDir, 'README.txt', 'plain text');
|
|
await this.writeAnnouncement(ctx.tempDir, 'image.png', 'binary');
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.parsed.length, 1);
|
|
assertEquals(result.errors, []);
|
|
});
|
|
|
|
this.test('subdirectories inside announcements/ are ignored', async (ctx) => {
|
|
await Deno.mkdir(`${ctx.tempDir}/announcements/nested`, { recursive: true });
|
|
await Deno.writeTextFile(`${ctx.tempDir}/announcements/nested/x.md`, VALID);
|
|
await this.writeAnnouncement(ctx.tempDir, 'top.md', VALID);
|
|
const result = await parseAnnouncementsDir(ctx.tempDir);
|
|
assertEquals(result.parsed.length, 1);
|
|
assertExists(result.parsed.find((p) => p.id === 'top'));
|
|
});
|
|
}
|
|
}
|
|
|
|
const suite = new DatabaseAnnouncementsParserTest();
|
|
suite.runTests();
|