packages/test-support/src/blog/mock-store.ts

import type { BlogPostRecord, BlogPostStatus, BlogPostSummary, BlogPostWithContent } from '@/types/blog'; import { TEST_BLOG_POSTS } from '../fixtures';

type MockPost = BlogPostWithContent;

const posts = new Map<string, MockPost>();

function clone(value: T): T { return JSON.parse(JSON.stringify(value)); }

function ensureSeeded() { if (posts.size) { return; } for (const post of TEST_BLOG_POSTS) { posts.set(post.slug, clone(post)); } }

function buildReadTimeLabel(minutes?: number) { if (!minutes || minutes <= 0) { return undefined; } return ${minutes} min read; }

function estimateReadTime(body: string): number | undefined { if (!body?.trim()) { return undefined; } const words = body.trim().split(/\s+/).length; return Math.max(1, Math.ceil(words / 200)); }

function requirePost(slug: string): MockPost { ensureSeeded(); const existing = posts.get(slug); if (!existing) { throw new Error(Post "${slug}" not found); } return existing; }

function savePost(next: MockPost): MockPost { posts.set(next.slug, clone(next)); return clone(next); }

function toRecord(post: MockPost): BlogPostRecord { const { content, ...record } = post; void content; return record; }

function toWithContent(post: MockPost): BlogPostWithContent { return clone(post); }

function applySearchFilter(record: BlogPostRecord, search?: string | null) { if (!search) { return true; } const normalized = search.toLowerCase().trim(); if (!normalized) { return true; } return record.title.toLowerCase().includes(normalized) || record.slug.toLowerCase().includes(normalized); }

function assertVersion(record: BlogPostRecord, expected: number) { if (record.version !== expected) { throw new Error('Version mismatch while updating post'); } }

export async function listPublishedPosts( limit: number = 20 ): Promise<{ posts: BlogPostSummary[]; hasMore: boolean; nextCursor?: string }> { ensureSeeded(); const allPosts = Array.from(posts.values()) .filter((post) => post.status === 'published') .sort((a, b) => { const aTime = new Date(a.publishedAt ?? a.updatedAt).getTime(); const bTime = new Date(b.publishedAt ?? b.updatedAt).getTime(); return bTime - aTime; });

const postsSlice = allPosts.slice(0, limit).map((post) => { const { content, version, ...rest } = post; void content; void version; return { ...rest }; });

return { posts: postsSlice, hasMore: false, // Mock store doesn't implement cursor pagination nextCursor: undefined, }; }

export async function listPosts(options: { status?: BlogPostStatus; search?: string } = {}): Promise<BlogPostRecord[]> { ensureSeeded(); return Array.from(posts.values()) .map((post) => toRecord(post)) .filter((record) => { if (options.status && record.status !== options.status) { return false; } return applySearchFilter(record, options.search); }) .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); }

export async function getPostWithContent( slug: string, options: { includeDraft?: boolean } = {} ): Promise<BlogPostWithContent | null> { const post = requirePost(slug); if (post.status !== 'published' && !options.includeDraft) { return null; } return toWithContent(post); }

export async function createPostRecord(input: { slug: string; title: string; summary: string; tags?: string[]; heroImageKey?: string; }): Promise { ensureSeeded(); if (posts.has(input.slug)) { throw new Error('A post with that slug already exists'); } const now = new Date().toISOString(); const post: MockPost = { slug: input.slug, title: input.title, summary: input.summary, status: 'draft', tags: input.tags ?? [], heroImageKey: input.heroImageKey, updatedAt: now, publishedAt: undefined, readTimeMinutes: undefined, readTimeLabel: undefined, currentRevisionKey: mock-rev-${input.slug}, version: 1, content: '', }; savePost(post); return toRecord(post); }

export async function saveDraftRecord(input: { slug: string; body: string; title?: string; summary?: string; tags?: string[]; heroImageKey?: string; extension?: string; expectedVersion: number; }): Promise { const existing = requirePost(input.slug); assertVersion(existing, input.expectedVersion);

const readTimeMinutes = estimateReadTime(input.body); const updated: MockPost = { ...existing, title: input.title ?? existing.title, summary: input.summary ?? existing.summary, tags: input.tags ?? existing.tags, heroImageKey: input.heroImageKey ?? existing.heroImageKey, updatedAt: new Date().toISOString(), currentRevisionKey: mock-rev-${input.slug}-${Date.now()}, version: existing.version + 1, content: input.body, readTimeMinutes, readTimeLabel: buildReadTimeLabel(readTimeMinutes), }; savePost(updated); return toWithContent(updated); }

export async function publishPostRecord(input: { slug: string; publishedAt?: string; expectedVersion: number; }): Promise { const existing = requirePost(input.slug); assertVersion(existing, input.expectedVersion); const update: MockPost = { ...existing, status: 'published', publishedAt: input.publishedAt ?? new Date().toISOString(), updatedAt: new Date().toISOString(), readTimeLabel: buildReadTimeLabel(existing.readTimeMinutes), currentRevisionKey: mock-rev-${input.slug}-${Date.now()}, version: existing.version + 1, }; savePost(update); return toRecord(update); }

export async function deletePost(slug: string): Promise { posts.delete(slug); }

export async function deleteAllPosts(): Promise { posts.clear(); }

export async function deleteDraft(slug: string): Promise { const existing = requirePost(slug); if (existing.status !== 'draft') { throw new Error('Cannot delete draft; post is already published'); } posts.delete(slug); }

export async function setPostPublishState(input: { slug: string; scheduledFor?: string | null; expectedVersion: number; }): Promise { const existing = requirePost(input.slug); assertVersion(existing, input.expectedVersion); const update: MockPost = { ...existing, scheduledFor: input.scheduledFor ?? undefined, updatedAt: new Date().toISOString(), version: existing.version + 1, }; savePost(update); return toRecord(update); }

export async function createRevision(input: { slug: string; content: string; extension?: string; expectedVersion: number; }): Promise<{ revisionKey: string }> { const existing = requirePost(input.slug); assertVersion(existing, input.expectedVersion); const revisionKey = mock-rev-${input.slug}-${Date.now()}.${input.extension || 'md'}; savePost({ ...existing, content: input.content, updatedAt: new Date().toISOString(), currentRevisionKey: revisionKey, version: existing.version + 1, }); return { revisionKey }; }

export async function archivePostRecord(input: { slug: string; expectedVersion: number }): Promise { const existing = requirePost(input.slug); assertVersion(existing, input.expectedVersion); const update: MockPost = { ...existing, status: 'archived', updatedAt: new Date().toISOString(), scheduledFor: undefined, activeScheduleArn: undefined, activeScheduleName: undefined, version: existing.version + 1, }; savePost(update); return toRecord(update); }

export async function markScheduledRecord(input: { slug: string; scheduledFor: string; scheduleArn: string; scheduleName: string; expectedVersion: number; }): Promise { const existing = requirePost(input.slug); assertVersion(existing, input.expectedVersion); const update: MockPost = { ...existing, status: 'scheduled', scheduledFor: input.scheduledFor, activeScheduleArn: input.scheduleArn, activeScheduleName: input.scheduleName, updatedAt: new Date().toISOString(), version: existing.version + 1, }; savePost(update); return toRecord(update); }

export async function unmarkScheduledRecord(input: { slug: string; expectedVersion: number }): Promise { const existing = requirePost(input.slug); assertVersion(existing, input.expectedVersion); const update: MockPost = { ...existing, status: 'draft', scheduledFor: undefined, activeScheduleArn: undefined, activeScheduleName: undefined, updatedAt: new Date().toISOString(), version: existing.version + 1, }; savePost(update); return toRecord(update); }

export async function deletePostRecord(slug: string): Promise { posts.delete(slug); }

export async function getPostRecord(slug: string): Promise<BlogPostRecord | null> { const post = posts.get(slug); return post ? toRecord(post) : null; }

export async function generateMediaUploadUrl(input: { contentType: string; extension?: string; }): Promise<{ uploadUrl: string; key: string }> { const ext = (input.extension ?? 'bin').replace(/[^a-zA-Z0-9]/g, ''); const key = images/mock/${Date.now()}.${ext || 'bin'}; // In tests we don't actually upload; return deterministic URL return { uploadUrl: https://mock-upload.local/${key}, key }; }