diff --git a/src/db/mutations.test.ts b/src/db/mutations.test.ts new file mode 100644 index 0000000..f63c745 --- /dev/null +++ b/src/db/mutations.test.ts @@ -0,0 +1,340 @@ +import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; +import { db } from '@/db'; +import { users, content, siteConfig } from '@/db/schema'; +import { eq } from 'drizzle-orm'; + +jest.mock('@/db', () => { + const mockDb = { + select: jest.fn().mockReturnThis(), + from: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + insert: jest.fn().mockReturnThis(), + values: jest.fn().mockReturnThis(), + returning: jest.fn().mockReturnThis(), + update: jest.fn().mockReturnThis(), + set: jest.fn().mockReturnThis(), + delete: jest.fn().mockReturnThis(), + }; + return { + db: mockDb, + }; +}); + +describe('database mutations', () => { + let mockDb: any; + + beforeEach(() => { + const { db: dbInstance } = require('@/db'); + mockDb = dbInstance; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('user mutations', () => { + it('should insert new user', async () => { + const newUser = { + id: 'user-123', + email: 'newuser@example.com', + name: 'New User', + role: 'editor', + createdAt: new Date(), + updatedAt: new Date(), + }; + mockDb.returning.mockResolvedValue([newUser]); + + const result = await db.insert(users).values(newUser).returning(); + const user = result[0]; + + expect(user).toBeDefined(); + expect(user.id).toBe('user-123'); + expect(user.email).toBe('newuser@example.com'); + expect(mockDb.insert).toHaveBeenCalledWith(users); + expect(mockDb.values).toHaveBeenCalledWith(newUser); + }); + + it('should update user', async () => { + const updatedUser = { + id: 'user-123', + email: 'updated@example.com', + name: 'Updated User', + role: 'admin', + createdAt: new Date(), + updatedAt: new Date(), + }; + mockDb.returning.mockResolvedValue([updatedUser]); + + const result = await db + .update(users) + .set({ name: 'Updated User', role: 'admin' }) + .where(eq(users.id, 'user-123')) + .returning(); + const user = result[0]; + + expect(user).toBeDefined(); + expect(user.name).toBe('Updated User'); + expect(user.role).toBe('admin'); + expect(mockDb.update).toHaveBeenCalledWith(users); + expect(mockDb.set).toHaveBeenCalledWith({ name: 'Updated User', role: 'admin' }); + }); + + it('should delete user', async () => { + const deletedUser = { + id: 'user-123', + email: 'deleted@example.com', + name: 'Deleted User', + role: 'editor', + createdAt: new Date(), + updatedAt: new Date(), + }; + mockDb.returning.mockResolvedValue([deletedUser]); + + const result = await db.delete(users).where(eq(users.id, 'user-123')).returning(); + const user = result[0]; + + expect(user).toBeDefined(); + expect(user.id).toBe('user-123'); + expect(mockDb.delete).toHaveBeenCalledWith(users); + expect(mockDb.where).toHaveBeenCalledWith(eq(users.id, 'user-123')); + }); + }); + + describe('content mutations', () => { + it('should insert new content', async () => { + const newContent = { + id: 'content-123', + type: 'news', + title: 'New Content', + slug: 'new-content', + content: 'Content body', + status: 'draft', + authorId: 'user-123', + createdAt: new Date(), + updatedAt: new Date(), + }; + mockDb.returning.mockResolvedValue([newContent]); + + const result = await db.insert(content).values(newContent).returning(); + const item = result[0]; + + expect(item).toBeDefined(); + expect(item.id).toBe('content-123'); + expect(item.title).toBe('New Content'); + expect(mockDb.insert).toHaveBeenCalledWith(content); + expect(mockDb.values).toHaveBeenCalledWith(newContent); + }); + + it('should update content', async () => { + const updatedContent = { + id: 'content-123', + type: 'news', + title: 'Updated Content', + slug: 'updated-content', + content: 'Updated content body', + status: 'published', + authorId: 'user-123', + createdAt: new Date(), + updatedAt: new Date(), + }; + mockDb.returning.mockResolvedValue([updatedContent]); + + const result = await db + .update(content) + .set({ title: 'Updated Content', status: 'published' }) + .where(eq(content.id, 'content-123')) + .returning(); + const item = result[0]; + + expect(item).toBeDefined(); + expect(item.title).toBe('Updated Content'); + expect(item.status).toBe('published'); + expect(mockDb.update).toHaveBeenCalledWith(content); + expect(mockDb.set).toHaveBeenCalledWith({ title: 'Updated Content', status: 'published' }); + }); + + it('should delete content', async () => { + const deletedContent = { + id: 'content-123', + type: 'news', + title: 'Deleted Content', + slug: 'deleted-content', + content: 'Deleted content body', + status: 'archived', + authorId: 'user-123', + createdAt: new Date(), + updatedAt: new Date(), + }; + mockDb.returning.mockResolvedValue([deletedContent]); + + const result = await db.delete(content).where(eq(content.id, 'content-123')).returning(); + const item = result[0]; + + expect(item).toBeDefined(); + expect(item.id).toBe('content-123'); + expect(mockDb.delete).toHaveBeenCalledWith(content); + expect(mockDb.where).toHaveBeenCalledWith(eq(content.id, 'content-123')); + }); + + it('should publish content', async () => { + const publishedContent = { + id: 'content-123', + type: 'news', + title: 'Published Content', + slug: 'published-content', + content: 'Published content body', + status: 'published', + publishedAt: new Date(), + authorId: 'user-123', + createdAt: new Date(), + updatedAt: new Date(), + }; + mockDb.returning.mockResolvedValue([publishedContent]); + + const result = await db + .update(content) + .set({ status: 'published', publishedAt: new Date() }) + .where(eq(content.id, 'content-123')) + .returning(); + const item = result[0]; + + expect(item).toBeDefined(); + expect(item.status).toBe('published'); + expect(item.publishedAt).toBeDefined(); + }); + }); + + describe('site config mutations', () => { + it('should insert new config', async () => { + const newConfig = { + id: 'config-123', + key: 'new.config', + value: { setting: 'value' }, + category: 'general', + updatedAt: new Date(), + }; + mockDb.returning.mockResolvedValue([newConfig]); + + const result = await db.insert(siteConfig).values(newConfig).returning(); + const config = result[0]; + + expect(config).toBeDefined(); + expect(config.key).toBe('new.config'); + expect(config.value).toEqual({ setting: 'value' }); + expect(mockDb.insert).toHaveBeenCalledWith(siteConfig); + }); + + it('should update config', async () => { + const updatedConfig = { + id: 'config-123', + key: 'updated.config', + value: { setting: 'updated value' }, + category: 'general', + updatedAt: new Date(), + }; + mockDb.returning.mockResolvedValue([updatedConfig]); + + const result = await db + .update(siteConfig) + .set({ value: { setting: 'updated value' } }) + .where(eq(siteConfig.key, 'updated.config')) + .returning(); + const config = result[0]; + + expect(config).toBeDefined(); + expect(config.value).toEqual({ setting: 'updated value' }); + expect(mockDb.update).toHaveBeenCalledWith(siteConfig); + }); + + it('should upsert config', async () => { + const upsertedConfig = { + id: 'config-123', + key: 'upsert.config', + value: { setting: 'upserted value' }, + category: 'general', + updatedAt: new Date(), + }; + mockDb.returning.mockResolvedValue([upsertedConfig]); + + const result = await db + .insert(siteConfig) + .values({ + key: 'upsert.config', + value: { setting: 'upserted value' }, + category: 'general', + updatedAt: new Date(), + }) + .returning(); + const config = result[0]; + + expect(config).toBeDefined(); + expect(config.key).toBe('upsert.config'); + }); + }); + + describe('batch operations', () => { + it('should insert multiple users', async () => { + const newUsers = [ + { id: 'user-1', email: 'user1@example.com', name: 'User 1', role: 'editor', createdAt: new Date(), updatedAt: new Date() }, + { id: 'user-2', email: 'user2@example.com', name: 'User 2', role: 'viewer', createdAt: new Date(), updatedAt: new Date() }, + { id: 'user-3', email: 'user3@example.com', name: 'User 3', role: 'editor', createdAt: new Date(), updatedAt: new Date() }, + ]; + mockDb.returning.mockResolvedValue(newUsers); + + const result = await db.insert(users).values(newUsers).returning(); + + expect(result.length).toBe(3); + expect(mockDb.values).toHaveBeenCalledWith(newUsers); + }); + + it('should insert multiple content items', async () => { + const newContent = [ + { id: 'content-1', type: 'news', title: 'News 1', slug: 'news-1', content: 'Content 1', status: 'published', authorId: 'user-1', createdAt: new Date(), updatedAt: new Date() }, + { id: 'content-2', type: 'news', title: 'News 2', slug: 'news-2', content: 'Content 2', status: 'published', authorId: 'user-1', createdAt: new Date(), updatedAt: new Date() }, + ]; + mockDb.returning.mockResolvedValue(newContent); + + const result = await db.insert(content).values(newContent).returning(); + + expect(result.length).toBe(2); + expect(mockDb.values).toHaveBeenCalledWith(newContent); + }); + }); + + describe('error handling', () => { + it('should handle duplicate key error', async () => { + mockDb.returning.mockRejectedValue(new Error('UNIQUE constraint failed: users.email')); + + await expect( + db.insert(users).values({ + id: 'user-123', + email: 'existing@example.com', + name: 'Test User', + role: 'editor', + createdAt: new Date(), + updatedAt: new Date(), + }).returning() + ).rejects.toThrow('UNIQUE constraint failed'); + }); + + it('should handle foreign key constraint error', async () => { + mockDb.returning.mockRejectedValue(new Error('FOREIGN KEY constraint failed')); + + await expect( + db.insert(content).values({ + id: 'content-123', + type: 'news', + title: 'Test Content', + slug: 'test-content', + content: 'Test content body', + status: 'draft', + authorId: 'non-existent-user', + createdAt: new Date(), + updatedAt: new Date(), + }).returning() + ).rejects.toThrow('FOREIGN KEY constraint failed'); + }); + }); +}); diff --git a/src/db/queries.test.ts b/src/db/queries.test.ts new file mode 100644 index 0000000..e3cd2e4 --- /dev/null +++ b/src/db/queries.test.ts @@ -0,0 +1,255 @@ +import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; +import { db } from '@/db'; +import { users, content, siteConfig } from '@/db/schema'; +import { eq, and, desc, like } from 'drizzle-orm'; + +jest.mock('@/db', () => { + const mockDb = { + select: jest.fn().mockReturnThis(), + from: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + insert: jest.fn().mockReturnThis(), + values: jest.fn().mockReturnThis(), + returning: jest.fn().mockReturnThis(), + update: jest.fn().mockReturnThis(), + set: jest.fn().mockReturnThis(), + delete: jest.fn().mockReturnThis(), + }; + return { + db: mockDb, + }; +}); + +describe('database queries', () => { + let mockDb: any; + + beforeEach(() => { + const { db: dbInstance } = require('@/db'); + mockDb = dbInstance; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('user queries', () => { + it('should query user by id', async () => { + const mockUser = { + id: '123', + email: 'test@example.com', + name: 'Test User', + role: 'editor', + createdAt: new Date(), + updatedAt: new Date(), + }; + mockDb.limit.mockResolvedValue([mockUser]); + + const result = await db.select().from(users).where(eq(users.id, '123')).limit(1); + const user = result[0]; + + expect(user).toBeDefined(); + expect(user.id).toBe('123'); + expect(user.email).toBe('test@example.com'); + }); + + it('should query user by email', async () => { + const mockUser = { + id: '123', + email: 'test@example.com', + name: 'Test User', + role: 'editor', + createdAt: new Date(), + updatedAt: new Date(), + }; + mockDb.limit.mockResolvedValue([mockUser]); + + const result = await db.select().from(users).where(eq(users.email, 'test@example.com')).limit(1); + const user = result[0]; + + expect(user).toBeDefined(); + expect(user.email).toBe('test@example.com'); + }); + + it('should return null for non-existent user', async () => { + mockDb.limit.mockResolvedValue([]); + + const result = await db.select().from(users).where(eq(users.id, 'non-existent')).limit(1); + const user = result[0]; + + expect(user).toBeUndefined(); + }); + + it('should query users by role', async () => { + const mockUsers = [ + { id: '1', email: 'admin@example.com', name: 'Admin', role: 'admin', createdAt: new Date(), updatedAt: new Date() }, + { id: '2', email: 'admin2@example.com', name: 'Admin2', role: 'admin', createdAt: new Date(), updatedAt: new Date() }, + ]; + mockDb.limit.mockResolvedValue(mockUsers); + + const result = await db.select().from(users).where(eq(users.role, 'admin')).limit(10); + + expect(result.length).toBe(2); + expect(result.every(u => u.role === 'admin')).toBe(true); + }); + }); + + describe('content queries', () => { + it('should query content by id', async () => { + const mockContent = { + id: 'content-1', + type: 'news', + title: 'Test News', + slug: 'test-news', + content: 'Test content', + status: 'published', + authorId: '123', + createdAt: new Date(), + updatedAt: new Date(), + }; + mockDb.limit.mockResolvedValue([mockContent]); + + const result = await db.select().from(content).where(eq(content.id, 'content-1')).limit(1); + const item = result[0]; + + expect(item).toBeDefined(); + expect(item.id).toBe('content-1'); + expect(item.title).toBe('Test News'); + }); + + it('should query content by slug', async () => { + const mockContent = { + id: 'content-1', + type: 'news', + title: 'Test News', + slug: 'test-news', + content: 'Test content', + status: 'published', + authorId: '123', + createdAt: new Date(), + updatedAt: new Date(), + }; + mockDb.limit.mockResolvedValue([mockContent]); + + const result = await db.select().from(content).where(eq(content.slug, 'test-news')).limit(1); + const item = result[0]; + + expect(item).toBeDefined(); + expect(item.slug).toBe('test-news'); + }); + + it('should query published content', async () => { + const mockContent = [ + { id: '1', type: 'news', title: 'News 1', slug: 'news-1', content: 'Content 1', status: 'published', authorId: '123', createdAt: new Date(), updatedAt: new Date() }, + { id: '2', type: 'news', title: 'News 2', slug: 'news-2', content: 'Content 2', status: 'published', authorId: '123', createdAt: new Date(), updatedAt: new Date() }, + ]; + mockDb.limit.mockResolvedValue(mockContent); + + const result = await db.select().from(content).where(eq(content.status, 'published')).limit(10); + + expect(result.length).toBe(2); + expect(result.every(c => c.status === 'published')).toBe(true); + }); + + it('should query content by type', async () => { + const mockContent = [ + { id: '1', type: 'news', title: 'News 1', slug: 'news-1', content: 'Content 1', status: 'published', authorId: '123', createdAt: new Date(), updatedAt: new Date() }, + { id: '2', type: 'news', title: 'News 2', slug: 'news-2', content: 'Content 2', status: 'published', authorId: '123', createdAt: new Date(), updatedAt: new Date() }, + ]; + mockDb.limit.mockResolvedValue(mockContent); + + const result = await db.select().from(content).where(eq(content.type, 'news')).limit(10); + + expect(result.length).toBe(2); + expect(result.every(c => c.type === 'news')).toBe(true); + }); + + it('should query content with multiple conditions', async () => { + const mockContent = { + id: 'content-1', + type: 'news', + title: 'Test News', + slug: 'test-news', + content: 'Test content', + status: 'published', + authorId: '123', + createdAt: new Date(), + updatedAt: new Date(), + }; + mockDb.limit.mockResolvedValue([mockContent]); + + const result = await db + .select() + .from(content) + .where(and(eq(content.status, 'published'), eq(content.type, 'news'))) + .limit(1); + const item = result[0]; + + expect(item).toBeDefined(); + expect(item.status).toBe('published'); + expect(item.type).toBe('news'); + }); + }); + + describe('site config queries', () => { + it('should query config by key', async () => { + const mockConfig = { + id: 'config-1', + key: 'site.title', + value: { title: 'My Site' }, + category: 'general', + updatedAt: new Date(), + }; + mockDb.limit.mockResolvedValue([mockConfig]); + + const result = await db.select().from(siteConfig).where(eq(siteConfig.key, 'site.title')).limit(1); + const config = result[0]; + + expect(config).toBeDefined(); + expect(config.key).toBe('site.title'); + expect(config.value).toEqual({ title: 'My Site' }); + }); + + it('should query config by category', async () => { + const mockConfigs = [ + { id: '1', key: 'site.title', value: { title: 'My Site' }, category: 'general', updatedAt: new Date() }, + { id: '2', key: 'site.description', value: { description: 'My Description' }, category: 'general', updatedAt: new Date() }, + ]; + mockDb.limit.mockResolvedValue(mockConfigs); + + const result = await db.select().from(siteConfig).where(eq(siteConfig.category, 'general')).limit(10); + + expect(result.length).toBe(2); + expect(result.every(c => c.category === 'general')).toBe(true); + }); + + it('should return null for non-existent config', async () => { + mockDb.limit.mockResolvedValue([]); + + const result = await db.select().from(siteConfig).where(eq(siteConfig.key, 'non.existent')).limit(1); + const config = result[0]; + + expect(config).toBeUndefined(); + }); + }); + + describe('query ordering', () => { + it('should order content by created date', async () => { + const mockContent = [ + { id: '1', type: 'news', title: 'News 1', slug: 'news-1', content: 'Content 1', status: 'published', authorId: '123', createdAt: new Date('2024-01-01'), updatedAt: new Date() }, + { id: '2', type: 'news', title: 'News 2', slug: 'news-2', content: 'Content 2', status: 'published', authorId: '123', createdAt: new Date('2024-01-02'), updatedAt: new Date() }, + ]; + mockDb.limit.mockResolvedValue(mockContent); + + const result = await db + .select() + .from(content) + .orderBy(desc(content.createdAt)) + .limit(10); + + expect(result.length).toBe(2); + expect(mockDb.orderBy).toHaveBeenCalledWith(desc(content.createdAt)); + }); + }); +});