diff --git a/.Jules/vantage.md b/.Jules/vantage.md index 6e9850b5..b475310f 100644 --- a/.Jules/vantage.md +++ b/.Jules/vantage.md @@ -15,3 +15,5 @@ ## 2026-05-20 - Hardening Telemetry Verification and Refining Stream Types | **Learning:** Index-based assertions in telemetry tests are brittle; using `.find()` ensures robust verification. Standardizing on `String(error)` across all API catch blocks prevents context loss during failure events. | **Action:** Refactored `chat-api.test.ts` and `github-releases.test.ts` to use search-based spy assertions, and added explicit return types to `chat-stream.ts` to resolve Astro compiler hints. ## 2026-05-24 - Comprehensive Unit Testing for Visitor Changelog Formatting | **Learning:** Single-pass Edge transformations require dedicated unit tests covering edge cases like empty inputs, internal agent filtering, and fallback formatting to prevent silent regressions in SSR release streams. | **Action:** Added `src/utils/visitor-changelog.test.ts` to test `toVisitorChangelogTitle`, `stripChangelogChrome`, `toVisitorReleaseBody`, and `toVisitorRelease`. + +## 2026-05-28 - Testing RSS and Sitemap Route Endpoints | **Learning:** Standalone route handlers that export `GET` responses (like RSS feeds and Sitemap XML generators) require unit test coverage for content type headers, XML character escaping, element structure, and sorting logic to prevent regressions in search indexing and RSS feed syndication. | **Action:** Added `src/__tests__/rss-sitemap.test.ts` and updated `src/__tests__/mocks/astro-content.ts` to export `getCollection`, achieving 100% test coverage for `rss.xml.ts` and `sitemap.xml.ts`. diff --git a/src/__tests__/mocks/astro-content.ts b/src/__tests__/mocks/astro-content.ts index 81d06b27..277ae43d 100644 --- a/src/__tests__/mocks/astro-content.ts +++ b/src/__tests__/mocks/astro-content.ts @@ -1,2 +1,4 @@ import { vi } from 'vitest'; export const defineCollection = vi.fn((config) => config); +export const getCollection = vi.fn(); +export const getEntry = vi.fn(); diff --git a/src/__tests__/rss-sitemap.test.ts b/src/__tests__/rss-sitemap.test.ts new file mode 100644 index 00000000..489ffd81 --- /dev/null +++ b/src/__tests__/rss-sitemap.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import * as astroContent from 'astro:content'; +import { GET as getRss } from '../pages/rss.xml'; +import { GET as getSitemap } from '../pages/sitemap.xml'; + +type GetContext = Parameters[0]; + +function createContext(url = 'https://me.alehar.workers.dev/rss.xml') { + return { + request: new Request(url), + locals: {}, + } as unknown as GetContext; +} + +const mockWorkEntries = [ + { + id: 'older-project', + data: { + title: 'Older Project & Test ', + description: 'An older "project" description', + publishDate: new Date('2023-01-01T12:00:00Z'), + }, + }, + { + id: 'newer-project', + data: { + title: "Newer Project 'Rock'", + description: 'A > newer project', + publishDate: new Date('2024-06-15T12:00:00Z'), + }, + }, + { + id: 'lidkoping-stenhuggeri', + data: { + title: 'Lidköping Stenhuggeri', + description: 'Filtered project', + publishDate: new Date('2022-01-01T12:00:00Z'), + }, + }, +]; + +describe('RSS XML endpoint (src/pages/rss.xml.ts)', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('generates valid RSS XML feed with properly escaped and sorted items', async () => { + vi.spyOn(astroContent, 'getCollection').mockResolvedValue( + mockWorkEntries as unknown as Awaited>, + ); + + const response = await getRss(createContext()); + + expect(response.status).toBe(200); + expect(response.headers.get('Content-Type')).toBe('application/rss+xml; charset=utf-8'); + + const xml = await response.text(); + + expect(xml).toContain(''); + expect(xml).toContain(''); + expect(xml).toContain( + 'Alexander Härenstam | Product Manager, Developer Experience at IFS', + ); + + // Check character escaping + expect(xml).toContain('Older Project & Test <Tag>'); + expect(xml).toContain('An older "project" description'); + expect(xml).toContain('Newer Project 'Rock''); + expect(xml).toContain('A > newer project'); + + // Check item sorting (newer-project should appear before older-project in the XML output) + const indexNewer = xml.indexOf('newer-project'); + const indexOlder = xml.indexOf('older-project'); + expect(indexNewer).toBeGreaterThan(-1); + expect(indexOlder).toBeGreaterThan(-1); + expect(indexNewer).toBeLessThan(indexOlder); + + // Filtered project shouldn't be included + expect(xml).not.toContain('lidkoping-stenhuggeri'); + }); +}); + +describe('Sitemap XML endpoint (src/pages/sitemap.xml.ts)', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('generates valid Sitemap XML with static paths and work collection items', async () => { + vi.spyOn(astroContent, 'getCollection').mockResolvedValue( + mockWorkEntries as unknown as Awaited>, + ); + + const response = await getSitemap(createContext('https://me.alehar.workers.dev/sitemap.xml')); + + expect(response.status).toBe(200); + expect(response.headers.get('Content-Type')).toBe('application/xml; charset=utf-8'); + + const xml = await response.text(); + + expect(xml).toContain(''); + expect(xml).toContain(''); + + // Static paths check + expect(xml).toContain('https://me.alehar.workers.dev/'); + expect(xml).toContain('https://me.alehar.workers.dev/work/'); + expect(xml).toContain('https://me.alehar.workers.dev/biography/'); + + // Dynamic work entry paths check + expect(xml).toContain('https://me.alehar.workers.dev/work/newer-project/'); + expect(xml).toContain('2024-06-15'); + + expect(xml).toContain('https://me.alehar.workers.dev/work/older-project/'); + expect(xml).toContain('2023-01-01'); + + // Filtered project check + expect(xml).not.toContain('lidkoping-stenhuggeri'); + }); +});