feat(mvp): phase 8 - integration, testing & deployment

Fix all build/type/lint errors (zod 3.25 compat wrapper, Svelte 5 fixes),
write 115 unit tests across 10 test files, expand seed script with demo
data, update Docker config with migration on startup.
This commit is contained in:
2026-03-24 22:09:17 +03:00
parent 0bd30c5e17
commit e6b50fb4f1
36 changed files with 1634 additions and 99 deletions
@@ -0,0 +1,171 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('../../prisma.js', () => ({
prisma: {
board: {
findMany: vi.fn(),
findUnique: vi.fn(),
findFirst: vi.fn(),
create: vi.fn(),
update: vi.fn(),
updateMany: vi.fn(),
delete: vi.fn()
},
section: {
findUnique: vi.fn(),
findFirst: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn()
},
widget: {
findUnique: vi.fn(),
findFirst: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn()
}
}
}));
import { prisma } from '../../prisma.js';
import * as boardService from '../boardService.js';
const mockBoard = prisma.board as unknown as Record<string, ReturnType<typeof vi.fn>>;
const mockSection = prisma.section as unknown as Record<string, ReturnType<typeof vi.fn>>;
const mockWidget = prisma.widget as unknown as Record<string, ReturnType<typeof vi.fn>>;
describe('boardService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('findAllBoards', () => {
it('returns all boards', async () => {
const boards = [{ id: '1', name: 'Main', _count: { sections: 2 } }];
mockBoard.findMany.mockResolvedValue(boards);
const result = await boardService.findAllBoards();
expect(result).toEqual(boards);
});
});
describe('findBoardById', () => {
it('returns board with sections and widgets', async () => {
const board = { id: '1', name: 'Main', sections: [] };
mockBoard.findUnique.mockResolvedValue(board);
const result = await boardService.findBoardById('1');
expect(result.name).toBe('Main');
});
it('throws when not found', async () => {
mockBoard.findUnique.mockResolvedValue(null);
await expect(boardService.findBoardById('missing')).rejects.toThrow('Board not found');
});
});
describe('createBoard', () => {
it('creates a board', async () => {
mockBoard.create.mockResolvedValue({ id: '1', name: 'New Board' });
const result = await boardService.createBoard({ name: 'New Board' });
expect(result.name).toBe('New Board');
});
it('unsets other defaults when creating a default board', async () => {
mockBoard.updateMany.mockResolvedValue({ count: 1 });
mockBoard.create.mockResolvedValue({ id: '1', name: 'Default', isDefault: true });
await boardService.createBoard({ name: 'Default', isDefault: true });
expect(mockBoard.updateMany).toHaveBeenCalledWith({
where: { isDefault: true },
data: { isDefault: false }
});
});
});
describe('updateBoard', () => {
it('updates board fields', async () => {
mockBoard.findUnique.mockResolvedValue({ id: '1' });
mockBoard.update.mockResolvedValue({ id: '1', name: 'Updated' });
const result = await boardService.updateBoard('1', { name: 'Updated' });
expect(result.name).toBe('Updated');
});
});
describe('removeBoard', () => {
it('deletes a board', async () => {
mockBoard.findUnique.mockResolvedValue({ id: '1' });
mockBoard.delete.mockResolvedValue({});
await boardService.removeBoard('1');
expect(mockBoard.delete).toHaveBeenCalledWith({ where: { id: '1' } });
});
});
describe('createSection', () => {
it('creates a section with auto-calculated order', async () => {
mockSection.findFirst.mockResolvedValue({ order: 2 });
mockSection.create.mockResolvedValue({
id: 's1',
title: 'Media',
order: 3
});
const result = await boardService.createSection({
boardId: 'b1',
title: 'Media'
});
expect(result.order).toBe(3);
});
it('starts order at 0 for first section', async () => {
mockSection.findFirst.mockResolvedValue(null);
mockSection.create.mockResolvedValue({
id: 's1',
title: 'First',
order: 0
});
await boardService.createSection({ boardId: 'b1', title: 'First' });
expect(mockSection.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ order: 0 })
})
);
});
});
describe('createWidget', () => {
it('creates a widget', async () => {
mockWidget.findFirst.mockResolvedValue(null);
mockWidget.create.mockResolvedValue({
id: 'w1',
type: 'app',
order: 0
});
const result = await boardService.createWidget({
sectionId: 's1',
type: 'app'
});
expect(result.type).toBe('app');
});
});
describe('removeWidget', () => {
it('deletes a widget', async () => {
mockWidget.findUnique.mockResolvedValue({ id: 'w1' });
mockWidget.delete.mockResolvedValue({});
await boardService.removeWidget('w1');
expect(mockWidget.delete).toHaveBeenCalledWith({ where: { id: 'w1' } });
});
});
});