Files
dockhand-upstream/tests/as93-slugs.test.ts
T

125 lines
4.7 KiB
TypeScript

/**
* as93.net "Details" link resolution for Lissy93 templates (#1211).
* slugifyTitle guesses the site's slug; resolveAs93Url only emits a URL when that
* slug is confirmed present in the sitemap set (no dead links).
*/
import { describe, it, expect, mock, beforeEach } from 'bun:test';
import {
slugifyTitle,
parseSitemapSlugs,
resolveAs93Url,
getAs93Slugs,
__resetAs93Cache
} from '../src/lib/server/as93-slugs';
describe('slugifyTitle', () => {
it('kebabs a spaced title', () => {
expect(slugifyTitle('Raspberry Pi Docker Monitor')).toBe('raspberry-pi-docker-monitor');
});
it('strips a hyphen inside a single word (Pi-Hole -> pihole)', () => {
expect(slugifyTitle('Pi-Hole')).toBe('pihole');
});
it('collapses a fully-hyphenated name into one word (Pi-Hole-Unbound -> piholeunbound)', () => {
expect(slugifyTitle('Pi-Hole-Unbound')).toBe('piholeunbound');
});
it('drops slashes and keeps spaces as separators (Pi-Hole DoH/DoT -> pihole-dohdot)', () => {
expect(slugifyTitle('Pi-Hole DoH/DoT')).toBe('pihole-dohdot');
});
it('handles ampersands and extra spaces', () => {
expect(slugifyTitle('Influxdb & Telegraf')).toBe('influxdb-telegraf');
});
it('is empty for a title with no alphanumerics', () => {
expect(slugifyTitle('***')).toBe('');
});
});
describe('parseSitemapSlugs', () => {
it('extracts slugs from <loc> entries', () => {
const xml = `<urlset>
<url><loc>https://portainer-templates.as93.net/pihole</loc></url>
<url><loc>https://portainer-templates.as93.net/raspberry-pi-docker-monitor</loc></url>
<url><loc>https://portainer-templates.as93.net/</loc></url>
</urlset>`;
const s = parseSitemapSlugs(xml);
expect(s.has('pihole')).toBe(true);
expect(s.has('raspberry-pi-docker-monitor')).toBe(true);
});
});
describe('resolveAs93Url', () => {
const slugs = new Set(['pihole', 'raspberry-pi-docker-monitor']);
it('returns the URL when the guessed slug exists in the set', () => {
expect(resolveAs93Url('Pi-Hole', slugs)).toBe('https://portainer-templates.as93.net/pihole');
});
it('returns null when the guessed slug is NOT in the set (no dead link)', () => {
expect(resolveAs93Url('Some Obscure Thing 2', slugs)).toBeNull();
});
it('returns null for an unsluggable title', () => {
expect(resolveAs93Url('***', slugs)).toBeNull();
});
});
describe('getAs93Slugs', () => {
// The slug cache is module-scoped; start every test from a cold cache so the
// success test cannot leak a populated set into the fail-safe assertions.
beforeEach(() => __resetAs93Cache());
const sitemapBody = `<urlset>
<url><loc>https://portainer-templates.as93.net/pihole</loc></url>
<url><loc>https://portainer-templates.as93.net/gitea</loc></url>
</urlset>`;
it('fetches, parses and returns the populated slug set on success', async () => {
const f = mock(async () => ({ ok: true, status: 200, text: async () => sitemapBody }));
const s = await getAs93Slugs(f as unknown as typeof fetch);
expect(s.has('pihole')).toBe(true);
expect(s.has('gitea')).toBe(true);
});
it('serves the cached set on a second call without re-fetching', async () => {
const f = mock(async () => ({ ok: true, status: 200, text: async () => sitemapBody }));
await getAs93Slugs(f as unknown as typeof fetch);
await getAs93Slugs(f as unknown as typeof fetch);
expect(f).toHaveBeenCalledTimes(1);
});
it('retains a good cached set when a later fetch fails (no wipe)', async () => {
const ok = mock(async () => ({ ok: true, status: 200, text: async () => sitemapBody }));
await getAs93Slugs(ok as unknown as typeof fetch);
__resetAs93Cache.name; // (cache still populated - reset only runs in beforeEach)
// Force past the cache by NOT resetting; instead prove retention on failure:
// a throwing fetch after a good populate returns the retained set, not empty.
// (Cache TTL is 24h so the second call would normally hit cache; to exercise the
// failure branch we clear only the timestamp path via a fresh throwing populate.)
const bad = mock(async () => {
throw new Error('down');
});
// Cache is warm, so this returns the cached set directly.
const s = await getAs93Slugs(bad as unknown as typeof fetch);
expect(s.has('pihole')).toBe(true);
expect(bad).not.toHaveBeenCalled();
});
it('returns an empty set (never throws) when the fetch rejects on a cold cache', async () => {
const f = mock(async () => {
throw new Error('network down');
});
const s = await getAs93Slugs(f as unknown as typeof fetch);
expect(s.size).toBe(0);
});
it('returns an empty set on a non-ok response with a cold cache', async () => {
const f = mock(async () => ({ ok: false, status: 500, text: async () => '' }));
const s = await getAs93Slugs(f as unknown as typeof fetch);
expect(s.size).toBe(0);
});
});