@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
text/typescript
/*
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);
});
});
});