auto-claude: subtask-4-1 - Create tests for blog.ts (parseMarkdownPost, getAllBlogPosts, getBlogPostBySlug)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 11:58:02 +01:00
co-authored by Claude Sonnet 4.5
parent ef62a67534
commit 3b6035c349
+463
View File
@@ -0,0 +1,463 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { getAllBlogPosts, getBlogPostBySlug, getBlogPostSlugs, type BlogPost } from './blog';
import fs from 'fs';
import path from 'path';
// Mock fs and path modules
vi.mock('fs');
vi.mock('path');
describe('blog', () => {
const mockBlogPostsDir = '/mock/blog-posts';
beforeEach(() => {
vi.clearAllMocks();
// Mock path.join to return predictable paths
vi.mocked(path.join).mockImplementation((...args) => args.join('/'));
// Mock process.cwd
vi.stubGlobal('process', {
...process,
cwd: () => '/mock'
});
});
describe('getAllBlogPosts', () => {
it('should return empty array when directory does not exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const posts = getAllBlogPosts();
expect(posts).toEqual([]);
expect(fs.existsSync).toHaveBeenCalledWith(mockBlogPostsDir);
});
it('should return empty array when directory is empty', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([]);
const posts = getAllBlogPosts();
expect(posts).toEqual([]);
});
it('should parse and return blog posts from markdown files', () => {
const mockContent = `# Test Blog Post
**Meta-Description:** This is a test blog post about testing.
**Keywords:** testing, vitest, typescript
## Einführung
This is the introduction to the blog post. It provides an overview of what will be covered.
## Main Content
Here is the main content of the blog post.`;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test-blog-post.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts).toHaveLength(1);
expect(posts[0]).toMatchObject({
slug: 'test-blog-post',
title: 'Test Blog Post',
excerpt: 'This is a test blog post about testing.',
tags: ['testing', 'vitest', 'typescript'],
category: 'Technologie'
});
expect(posts[0].date).toBe('2026-01-19');
expect(posts[0].coverImage).toBe('/images/posts/test-blog-post/cover.jpg');
expect(posts[0].content).toBe(mockContent);
});
it('should sort posts by date descending (newest first)', () => {
const mockContent1 = '# Post 1\n\n**Meta-Description:** First post';
const mockContent2 = '# Post 2\n\n**Meta-Description:** Second post';
const mockContent3 = '# Post 3\n\n**Meta-Description:** Third post';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
'001-first-post.md',
'002-second-post.md',
'003-third-post.md'
] as any);
vi.mocked(fs.readFileSync)
.mockReturnValueOnce(mockContent1)
.mockReturnValueOnce(mockContent2)
.mockReturnValueOnce(mockContent3);
const posts = getAllBlogPosts();
expect(posts).toHaveLength(3);
// Posts should be sorted newest first (higher numbers = older posts)
expect(posts[0].slug).toBe('first-post');
expect(posts[1].slug).toBe('second-post');
expect(posts[2].slug).toBe('third-post');
expect(new Date(posts[0].date).getTime()).toBeGreaterThanOrEqual(new Date(posts[1].date).getTime());
expect(new Date(posts[1].date).getTime()).toBeGreaterThanOrEqual(new Date(posts[2].date).getTime());
});
it('should filter out non-markdown files', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
'001-test-post.md',
'README.txt',
'image.png',
'002-another-post.md',
'config.json'
] as any);
const mockContent = '# Test\n\n**Meta-Description:** Test';
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts).toHaveLength(2);
expect(fs.readFileSync).toHaveBeenCalledTimes(2);
});
it('should detect category from filename - AI Agents', () => {
const mockContent = '# AI Agent Post\n\n**Meta-Description:** About AI agents';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-agentic-workflow.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].category).toBe('KI-Agenten');
});
it('should detect category from filename - Voice AI', () => {
const mockContent = '# Voice AI Post\n\n**Meta-Description:** About voice technology';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-vapi-integration.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].category).toBe('Voice AI');
});
it('should detect category from content when not in filename', () => {
const mockContent = `# My Post
**Meta-Description:** A post about automation
I love using n8n for automation workflows.`;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-my-post.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].category).toBe('Automatisierung');
});
it('should use default category when no keywords match', () => {
const mockContent = '# Generic Post\n\n**Meta-Description:** Generic content';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-generic-post.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].category).toBe('Technologie');
});
it('should extract tags from keywords field', () => {
const mockContent = `# Test Post
**Meta-Description:** Test description
**Keywords:** javascript, typescript, testing, vitest, unit-tests, integration-tests, extra-tag`;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
// Should limit to 6 tags
expect(posts[0].tags).toHaveLength(6);
expect(posts[0].tags).toEqual([
'javascript',
'typescript',
'testing',
'vitest',
'unit-tests',
'integration-tests'
]);
});
it('should handle posts without keywords', () => {
const mockContent = '# Test Post\n\n**Meta-Description:** Test';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].tags).toEqual([]);
});
it('should use introduction as excerpt when meta description is missing', () => {
const mockContent = `# Test Post
## Einführung
This is a very long introduction that should be truncated to 200 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
## Main Content`;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].excerpt).toHaveLength(203); // 200 chars + '...'
expect(posts[0].excerpt).toContain('This is a very long introduction');
expect(posts[0].excerpt).toEndWith('...');
});
it('should use title as excerpt when both meta description and introduction are missing', () => {
const mockContent = '# Test Post Title\n\nSome content';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].excerpt).toBe('Test Post Title');
});
it('should handle posts without H1 title', () => {
const mockContent = 'No title here\n\n**Meta-Description:** Test';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-fallback-title.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].title).toBe('fallback-title');
});
it('should skip posts that fail to parse', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
'001-good-post.md',
'002-bad-post.md'
] as any);
vi.mocked(fs.readFileSync)
.mockReturnValueOnce('# Good Post\n\n**Meta-Description:** Good')
.mockImplementationOnce(() => {
throw new Error('File read error');
});
const posts = getAllBlogPosts();
expect(posts).toHaveLength(1);
expect(posts[0].title).toBe('Good Post');
});
it('should generate correct dates based on post numbers', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
'001-first.md',
'005-fifth.md',
'010-tenth.md'
] as any);
const mockContent = '# Post\n\n**Meta-Description:** Test';
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
// Post 001 should be 2026-01-19
expect(posts.find(p => p.slug === 'first')?.date).toBe('2026-01-19');
// Post 005 should be 4 days earlier (2026-01-15)
expect(posts.find(p => p.slug === 'fifth')?.date).toBe('2026-01-15');
// Post 010 should be 9 days earlier (2026-01-10)
expect(posts.find(p => p.slug === 'tenth')?.date).toBe('2026-01-10');
});
it('should trim whitespace from extracted fields', () => {
const mockContent = `# Test Post With Spaces
**Meta-Description:** Description with spaces
**Keywords:** tag1 , tag2 , tag3 `;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].title).toBe('Test Post With Spaces');
expect(posts[0].excerpt).toBe('Description with spaces');
expect(posts[0].tags).toEqual(['tag1', 'tag2', 'tag3']);
});
});
describe('getBlogPostBySlug', () => {
it('should return post when slug matches', () => {
const mockContent = '# Test Post\n\n**Meta-Description:** Test';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test-post.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const post = getBlogPostBySlug('test-post');
expect(post).not.toBeNull();
expect(post?.slug).toBe('test-post');
expect(post?.title).toBe('Test Post');
});
it('should return null when slug does not match', () => {
const mockContent = '# Test Post\n\n**Meta-Description:** Test';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test-post.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const post = getBlogPostBySlug('non-existent-slug');
expect(post).toBeNull();
});
it('should return null when no posts exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([]);
const post = getBlogPostBySlug('any-slug');
expect(post).toBeNull();
});
it('should return null when directory does not exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const post = getBlogPostBySlug('any-slug');
expect(post).toBeNull();
});
it('should return correct post when multiple posts exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
'001-first-post.md',
'002-second-post.md',
'003-third-post.md'
] as any);
vi.mocked(fs.readFileSync)
.mockReturnValueOnce('# First Post\n\n**Meta-Description:** First')
.mockReturnValueOnce('# Second Post\n\n**Meta-Description:** Second')
.mockReturnValueOnce('# Third Post\n\n**Meta-Description:** Third');
const post = getBlogPostBySlug('second-post');
expect(post).not.toBeNull();
expect(post?.slug).toBe('second-post');
expect(post?.title).toBe('Second Post');
expect(post?.excerpt).toBe('Second');
});
it('should return post with all content included', () => {
const mockContent = `# Full Post
**Meta-Description:** Description
**Keywords:** tag1, tag2
## Einführung
Introduction text
## Main Content
This is the main content of the post with lots of details.`;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-full-post.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const post = getBlogPostBySlug('full-post');
expect(post?.content).toBe(mockContent);
});
});
describe('getBlogPostSlugs', () => {
it('should return all post slugs', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
'001-first-post.md',
'002-second-post.md',
'003-third-post.md'
] as any);
const mockContent = '# Post\n\n**Meta-Description:** Test';
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const slugs = getBlogPostSlugs();
expect(slugs).toHaveLength(3);
expect(slugs).toContain('first-post');
expect(slugs).toContain('second-post');
expect(slugs).toContain('third-post');
});
it('should return empty array when no posts exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([]);
const slugs = getBlogPostSlugs();
expect(slugs).toEqual([]);
});
it('should return empty array when directory does not exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const slugs = getBlogPostSlugs();
expect(slugs).toEqual([]);
});
it('should return slugs in date order (newest first)', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
'003-third.md',
'001-first.md',
'002-second.md'
] as any);
const mockContent = '# Post\n\n**Meta-Description:** Test';
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const slugs = getBlogPostSlugs();
expect(slugs).toEqual(['first', 'second', 'third']);
});
});
});