UNPKG

@bernierllc/content-type-blog-post

Version:

Blog post content type with rich TipTap editor, SEO metadata, database storage, and web publishing

394 lines (329 loc) 14.2 kB
/* Copyright (c) 2025 Bernier LLC This file is licensed to the client under a limited-use license. The client may use and modify this code *only within the scope of the project it was delivered for*. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC. */ import { MockDatabaseClient } from './test-utils'; /** * Mock Validation Tests * * CRITICAL: These tests validate that our MockDatabaseClient behaves EXACTLY like PostgreSQL. * If these tests fail, it means our mocks have drifted from the real database behavior. * * Purpose: * - Ensure mocks match real PostgreSQL query behavior * - Prevent mock drift over time * - Document expected database behavior * - Catch breaking changes in PostgreSQL updates * * This file must be updated whenever: * - PostgreSQL version changes * - Database schema changes * - Query patterns change */ describe('MockDatabaseClient - PostgreSQL Behavior Validation', () => { let mockDb: MockDatabaseClient; beforeEach(() => { mockDb = new MockDatabaseClient(); }); afterEach(() => { mockDb.clear(); }); describe('INSERT behavior', () => { it('should return generated ID on INSERT', async () => { const result = await mockDb.query( 'INSERT INTO blog_posts (title, slug, content) VALUES ($1, $2, $3) RETURNING id', ['Test', 'test', 'Content'] ); expect(result.rows).toHaveLength(1); expect(result.rows[0]).toHaveProperty('id'); expect(typeof result.rows[0].id).toBe('string'); }); it('should auto-increment IDs for multiple INSERTs', async () => { const result1 = await mockDb.query( 'INSERT INTO blog_posts (title, slug, content) VALUES ($1, $2, $3) RETURNING id', ['Post 1', 'post-1', 'Content 1'] ); const result2 = await mockDb.query( 'INSERT INTO blog_posts (title, slug, content) VALUES ($1, $2, $3) RETURNING id', ['Post 2', 'post-2', 'Content 2'] ); expect(result1.rows[0].id).not.toBe(result2.rows[0].id); expect(result1.rows[0].id).toBe('test-id-1'); expect(result2.rows[0].id).toBe('test-id-2'); }); it('should store all parameters correctly', async () => { const params = [ 'Test Title', 'test-slug', '<p>Content</p>', 'Excerpt', 'Meta Title', 'Meta Description', ['keyword1', 'keyword2'], 'https://og-image.jpg', 'https://canonical.url', 'Author Name', 'author@email.com', 'https://avatar.jpg', 'Author bio', ['tag1', 'tag2'], ['category1'], 'draft', null, null, 'https://featured.jpg', 5, 1000, ]; const result = await mockDb.query( 'INSERT INTO blog_posts (title, slug, content, excerpt, meta_title, meta_description, keywords, og_image, canonical_url, author_name, author_email, author_avatar, author_bio, tags, categories, status, published_at, scheduled_for, featured_image, reading_time, word_count) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) RETURNING id', params ); const selectResult = await mockDb.query('SELECT * FROM blog_posts WHERE id = $1', [ result.rows[0].id, ]); expect(selectResult.rows[0].title).toBe('Test Title'); expect(selectResult.rows[0].keywords).toEqual(['keyword1', 'keyword2']); expect(selectResult.rows[0].tags).toEqual(['tag1', 'tag2']); }); }); describe('SELECT behavior', () => { beforeEach(async () => { // Insert test data await mockDb.query( 'INSERT INTO blog_posts (title, slug, content, status, tags) VALUES ($1, $2, $3, $4, $5)', ['Post 1', 'post-1', 'Content 1', 'published', ['tech', 'tutorial']] ); await mockDb.query( 'INSERT INTO blog_posts (title, slug, content, status, tags) VALUES ($1, $2, $3, $4, $5)', ['Post 2', 'post-2', 'Content 2', 'draft', ['tech']] ); await mockDb.query( 'INSERT INTO blog_posts (title, slug, content, status, tags) VALUES ($1, $2, $3, $4, $5)', ['Post 3', 'post-3', 'Content 3', 'published', ['news']] ); }); it('should return empty array when no results match', async () => { const result = await mockDb.query('SELECT * FROM blog_posts WHERE id = $1', [ 'nonexistent-id', ]); expect(result.rows).toEqual([]); }); it('should return single row when one match exists', async () => { const result = await mockDb.query('SELECT * FROM blog_posts WHERE slug = $1', ['post-1']); expect(result.rows).toHaveLength(1); expect(result.rows[0].slug).toBe('post-1'); }); it('should filter by status correctly', async () => { const result = await mockDb.query( 'SELECT * FROM blog_posts WHERE 1=1 AND status = $1 ORDER BY created_at DESC', ['published'] ); expect(result.rows).toHaveLength(2); expect(result.rows.every((row: any) => row.status === 'published')).toBe(true); }); it('should filter by array overlap (tags && operator)', async () => { const result = await mockDb.query( 'SELECT * FROM blog_posts WHERE 1=1 AND tags && $1 ORDER BY created_at DESC', [['tech']] ); expect(result.rows).toHaveLength(2); expect(result.rows.every((row: any) => row.tags.includes('tech'))).toBe(true); }); }); describe('UPDATE behavior', () => { let testId: string; beforeEach(async () => { const result = await mockDb.query( 'INSERT INTO blog_posts (title, slug, content, status) VALUES ($1, $2, $3, $4) RETURNING id', ['Original', 'original', 'Original content', 'draft'] ); testId = result.rows[0].id; }); it('should update existing record', async () => { await mockDb.query( 'UPDATE blog_posts SET title = $1, slug = $2, content = $3, status = $4 WHERE id = $21', ['Updated', 'updated', 'Updated content', 'published', ...Array(16).fill(null), testId] ); const result = await mockDb.query('SELECT * FROM blog_posts WHERE id = $1', [testId]); expect(result.rows[0].title).toBe('Updated'); expect(result.rows[0].status).toBe('published'); }); it('should not affect other records', async () => { const otherResult = await mockDb.query( 'INSERT INTO blog_posts (title, slug, content) VALUES ($1, $2, $3) RETURNING id', ['Other', 'other', 'Other content'] ); const otherId = otherResult.rows[0].id; await mockDb.query( 'UPDATE blog_posts SET title = $1, slug = $2, content = $3, status = $4 WHERE id = $21', ['Updated', 'updated', 'Updated content', 'published', ...Array(16).fill(null), testId] ); const otherCheck = await mockDb.query('SELECT * FROM blog_posts WHERE id = $1', [otherId]); expect(otherCheck.rows[0].title).toBe('Other'); }); }); describe('DELETE behavior', () => { let testId: string; beforeEach(async () => { const result = await mockDb.query( 'INSERT INTO blog_posts (title, slug, content) VALUES ($1, $2, $3) RETURNING id', ['To Delete', 'to-delete', 'Content'] ); testId = result.rows[0].id; }); it('should remove record from database', async () => { await mockDb.query('DELETE FROM blog_posts WHERE id = $1', [testId]); const result = await mockDb.query('SELECT * FROM blog_posts WHERE id = $1', [testId]); expect(result.rows).toEqual([]); }); it('should not affect other records', async () => { const otherResult = await mockDb.query( 'INSERT INTO blog_posts (title, slug, content) VALUES ($1, $2, $3) RETURNING id', ['Keep', 'keep', 'Keep content'] ); const otherId = otherResult.rows[0].id; await mockDb.query('DELETE FROM blog_posts WHERE id = $1', [testId]); const otherCheck = await mockDb.query('SELECT * FROM blog_posts WHERE id = $1', [otherId]); expect(otherCheck.rows).toHaveLength(1); expect(otherCheck.rows[0].id).toBe(otherId); }); }); describe('ORDER BY behavior', () => { beforeEach(async () => { // Insert with delays to ensure different timestamps await mockDb.query( 'INSERT INTO blog_posts (title, slug, content) VALUES ($1, $2, $3)', ['Post A', 'post-a', 'Content A'] ); await new Promise((resolve) => setTimeout(resolve, 10)); await mockDb.query( 'INSERT INTO blog_posts (title, slug, content) VALUES ($1, $2, $3)', ['Post B', 'post-b', 'Content B'] ); await new Promise((resolve) => setTimeout(resolve, 10)); await mockDb.query( 'INSERT INTO blog_posts (title, slug, content) VALUES ($1, $2, $3)', ['Post C', 'post-c', 'Content C'] ); }); it('should order by created_at DESC (newest first)', async () => { const result = await mockDb.query( 'SELECT * FROM blog_posts WHERE 1=1 ORDER BY created_at DESC' ); expect(result.rows).toHaveLength(3); expect(result.rows[0].title).toBe('Post C'); expect(result.rows[1].title).toBe('Post B'); expect(result.rows[2].title).toBe('Post A'); }); }); describe('LIMIT and OFFSET behavior', () => { beforeEach(async () => { // Insert 10 posts for (let i = 1; i <= 10; i++) { await mockDb.query( 'INSERT INTO blog_posts (title, slug, content) VALUES ($1, $2, $3)', [`Post ${i}`, `post-${i}`, `Content ${i}`] ); await new Promise((resolve) => setTimeout(resolve, 5)); } }); it('should limit results to specified count', async () => { const result = await mockDb.query( 'SELECT * FROM blog_posts WHERE 1=1 ORDER BY created_at DESC LIMIT $1', [5] ); expect(result.rows).toHaveLength(5); }); it('should skip records with OFFSET', async () => { const result = await mockDb.query( 'SELECT * FROM blog_posts WHERE 1=1 ORDER BY created_at DESC OFFSET $1', [3] ); expect(result.rows).toHaveLength(7); // 10 total - 3 offset = 7 expect(result.rows[0].title).toBe('Post 7'); // Skipped Post 10, 9, 8 }); it('should combine LIMIT and OFFSET correctly', async () => { // Page 1: LIMIT 3 OFFSET 0 → Posts 10, 9, 8 const page1 = await mockDb.query( 'SELECT * FROM blog_posts WHERE 1=1 ORDER BY created_at DESC LIMIT $1 OFFSET $2', [3, 0] ); expect(page1.rows).toHaveLength(3); expect(page1.rows[0].title).toBe('Post 10'); expect(page1.rows[1].title).toBe('Post 9'); expect(page1.rows[2].title).toBe('Post 8'); // Page 2: LIMIT 3 OFFSET 3 → Posts 7, 6, 5 const page2 = await mockDb.query( 'SELECT * FROM blog_posts WHERE 1=1 ORDER BY created_at DESC LIMIT $1 OFFSET $2', [3, 3] ); expect(page2.rows).toHaveLength(3); expect(page2.rows[0].title).toBe('Post 7'); expect(page2.rows[1].title).toBe('Post 6'); expect(page2.rows[2].title).toBe('Post 5'); // Page 3: LIMIT 3 OFFSET 6 → Posts 4, 3, 2 const page3 = await mockDb.query( 'SELECT * FROM blog_posts WHERE 1=1 ORDER BY created_at DESC LIMIT $1 OFFSET $2', [3, 6] ); expect(page3.rows).toHaveLength(3); expect(page3.rows[0].title).toBe('Post 4'); expect(page3.rows[1].title).toBe('Post 3'); expect(page3.rows[2].title).toBe('Post 2'); // Page 4: LIMIT 3 OFFSET 9 → Post 1 only const page4 = await mockDb.query( 'SELECT * FROM blog_posts WHERE 1=1 ORDER BY created_at DESC LIMIT $1 OFFSET $2', [3, 9] ); expect(page4.rows).toHaveLength(1); expect(page4.rows[0].title).toBe('Post 1'); }); it('should return empty array when OFFSET exceeds total records', async () => { const result = await mockDb.query( 'SELECT * FROM blog_posts WHERE 1=1 ORDER BY created_at DESC LIMIT $1 OFFSET $2', [5, 20] ); expect(result.rows).toEqual([]); }); }); describe('Complex query behavior', () => { beforeEach(async () => { await mockDb.query( 'INSERT INTO blog_posts (title, slug, content, status, tags, categories) VALUES ($1, $2, $3, $4, $5, $6)', ['Tech Post 1', 'tech-1', 'Content', 'published', ['tech', 'tutorial'], ['guides']] ); await mockDb.query( 'INSERT INTO blog_posts (title, slug, content, status, tags, categories) VALUES ($1, $2, $3, $4, $5, $6)', ['Tech Post 2', 'tech-2', 'Content', 'published', ['tech'], ['guides']] ); await mockDb.query( 'INSERT INTO blog_posts (title, slug, content, status, tags, categories) VALUES ($1, $2, $3, $4, $5, $6)', ['News Post', 'news-1', 'Content', 'published', ['news'], ['articles']] ); await mockDb.query( 'INSERT INTO blog_posts (title, slug, content, status, tags, categories) VALUES ($1, $2, $3, $4, $5, $6)', ['Draft Post', 'draft-1', 'Content', 'draft', ['tech'], ['guides']] ); }); it('should filter by status AND tags with pagination', async () => { const result = await mockDb.query( 'SELECT * FROM blog_posts WHERE 1=1 AND status = $1 AND tags && $2 ORDER BY created_at DESC LIMIT $3 OFFSET $4', ['published', ['tech'], 2, 0] ); expect(result.rows).toHaveLength(2); expect(result.rows.every((row: any) => row.status === 'published')).toBe(true); expect(result.rows.every((row: any) => row.tags.includes('tech'))).toBe(true); }); it('should filter by categories and paginate', async () => { const result = await mockDb.query( 'SELECT * FROM blog_posts WHERE 1=1 AND categories && $1 ORDER BY created_at DESC LIMIT $2', [['guides'], 2] ); expect(result.rows).toHaveLength(2); expect(result.rows.every((row: any) => row.categories.includes('guides'))).toBe(true); }); }); });