mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:21:10 +02:00
test(todo): add 54 backend unit tests for kanban boards and reminders
- kanban.service.spec.ts: 35 tests (boards CRUD, columns CRUD, reorder, moveTask, global board) - reminder.service.spec.ts: 19 tests (CRUD, due date validation, time calculation, bulk delete) Total: 109 backend tests (was 55) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5345e19e24
commit
42c266b09a
2 changed files with 905 additions and 0 deletions
|
|
@ -0,0 +1,632 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { KanbanService } from '../kanban.service';
|
||||
import { DATABASE_CONNECTION } from '../../db/database.module';
|
||||
|
||||
const mockDb: any = {
|
||||
query: {
|
||||
kanbanBoards: {
|
||||
findMany: jest.fn(),
|
||||
findFirst: jest.fn(),
|
||||
},
|
||||
kanbanColumns: {
|
||||
findMany: jest.fn(),
|
||||
findFirst: jest.fn(),
|
||||
},
|
||||
tasks: {
|
||||
findMany: jest.fn(),
|
||||
findFirst: jest.fn(),
|
||||
},
|
||||
},
|
||||
insert: jest.fn().mockReturnThis(),
|
||||
update: jest.fn().mockReturnThis(),
|
||||
delete: jest.fn().mockReturnThis(),
|
||||
values: jest.fn().mockReturnThis(),
|
||||
set: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
returning: jest.fn(),
|
||||
transaction: jest.fn(),
|
||||
};
|
||||
|
||||
// Make transaction execute callback with mockDb as tx
|
||||
mockDb.transaction.mockImplementation((cb: any) => cb(mockDb));
|
||||
|
||||
describe('KanbanService', () => {
|
||||
let service: KanbanService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
KanbanService,
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useValue: mockDb,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<KanbanService>(KanbanService);
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Re-set transaction mock after clearAllMocks
|
||||
mockDb.transaction.mockImplementation((cb: any) => cb(mockDb));
|
||||
// Re-set chainable mocks
|
||||
mockDb.insert.mockReturnThis();
|
||||
mockDb.update.mockReturnThis();
|
||||
mockDb.delete.mockReturnThis();
|
||||
mockDb.values.mockReturnThis();
|
||||
mockDb.set.mockReturnThis();
|
||||
mockDb.where.mockReturnThis();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
// =====================
|
||||
// Board operations
|
||||
// =====================
|
||||
|
||||
describe('findAllBoards', () => {
|
||||
it('should return all boards for a user', async () => {
|
||||
const userId = 'user-123';
|
||||
const mockBoards = [
|
||||
{ id: 'board-1', name: 'Board 1', userId, order: 0 },
|
||||
{ id: 'board-2', name: 'Board 2', userId, order: 1 },
|
||||
];
|
||||
|
||||
mockDb.query.kanbanBoards.findMany.mockResolvedValue(mockBoards);
|
||||
|
||||
const result = await service.findAllBoards(userId);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(mockDb.query.kanbanBoards.findMany).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return empty array when no boards', async () => {
|
||||
mockDb.query.kanbanBoards.findMany.mockResolvedValue([]);
|
||||
|
||||
const result = await service.findAllBoards('user-123');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findBoardById', () => {
|
||||
it('should return a board when found', async () => {
|
||||
const userId = 'user-123';
|
||||
const boardId = 'board-1';
|
||||
const mockBoard = { id: boardId, name: 'Board 1', userId };
|
||||
|
||||
mockDb.query.kanbanBoards.findFirst.mockResolvedValue(mockBoard);
|
||||
|
||||
const result = await service.findBoardById(boardId, userId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.id).toBe(boardId);
|
||||
});
|
||||
|
||||
it('should return null when board not found', async () => {
|
||||
mockDb.query.kanbanBoards.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.findBoardById('non-existent', 'user-123');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findBoardByIdOrThrow', () => {
|
||||
it('should return a board when found', async () => {
|
||||
const userId = 'user-123';
|
||||
const boardId = 'board-1';
|
||||
const mockBoard = { id: boardId, name: 'Board 1', userId };
|
||||
|
||||
mockDb.query.kanbanBoards.findFirst.mockResolvedValue(mockBoard);
|
||||
|
||||
const result = await service.findBoardByIdOrThrow(boardId, userId);
|
||||
|
||||
expect(result.id).toBe(boardId);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when board not found', async () => {
|
||||
mockDb.query.kanbanBoards.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.findBoardByIdOrThrow('non-existent', 'user-123')).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBoard', () => {
|
||||
it('should create a board with correct order', async () => {
|
||||
const userId = 'user-123';
|
||||
const dto = { name: 'New Board', color: '#ff0000' };
|
||||
const existingBoards = [
|
||||
{ id: 'board-1', name: 'Board 1', userId, order: 0 },
|
||||
{ id: 'board-2', name: 'Board 2', userId, order: 1 },
|
||||
];
|
||||
const createdBoard = { id: 'board-new', name: 'New Board', userId, order: 2 };
|
||||
|
||||
mockDb.query.kanbanBoards.findMany.mockResolvedValue(existingBoards);
|
||||
mockDb.returning.mockResolvedValue([createdBoard]);
|
||||
|
||||
const result = await service.createBoard(userId, dto);
|
||||
|
||||
expect(result.order).toBe(2);
|
||||
expect(result.name).toBe('New Board');
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set order to 0 when no existing boards', async () => {
|
||||
const userId = 'user-123';
|
||||
const dto = { name: 'First Board' };
|
||||
const createdBoard = { id: 'board-first', name: 'First Board', userId, order: 0 };
|
||||
|
||||
mockDb.query.kanbanBoards.findMany.mockResolvedValue([]);
|
||||
mockDb.returning.mockResolvedValue([createdBoard]);
|
||||
|
||||
const result = await service.createBoard(userId, dto);
|
||||
|
||||
expect(result.order).toBe(0);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create default columns along with the board', async () => {
|
||||
const userId = 'user-123';
|
||||
const dto = { name: 'New Board' };
|
||||
const createdBoard = { id: 'board-new', name: 'New Board', userId, order: 0 };
|
||||
|
||||
mockDb.query.kanbanBoards.findMany.mockResolvedValue([]);
|
||||
mockDb.returning.mockResolvedValue([createdBoard]);
|
||||
|
||||
await service.createBoard(userId, dto);
|
||||
|
||||
// insert should be called twice: once for board, once for columns
|
||||
expect(mockDb.insert).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBoard', () => {
|
||||
it('should update a board', async () => {
|
||||
const userId = 'user-123';
|
||||
const boardId = 'board-1';
|
||||
const dto = { name: 'Updated Board' };
|
||||
const existingBoard = { id: boardId, name: 'Original', userId };
|
||||
const updatedBoard = { id: boardId, name: 'Updated Board', userId };
|
||||
|
||||
mockDb.query.kanbanBoards.findFirst.mockResolvedValue(existingBoard);
|
||||
mockDb.returning.mockResolvedValue([updatedBoard]);
|
||||
|
||||
const result = await service.updateBoard(boardId, userId, dto);
|
||||
|
||||
expect(result.name).toBe('Updated Board');
|
||||
});
|
||||
|
||||
it('should throw when board does not exist', async () => {
|
||||
mockDb.query.kanbanBoards.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(
|
||||
service.updateBoard('non-existent', 'user-123', { name: 'Test' })
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteBoard', () => {
|
||||
it('should delete a non-global board', async () => {
|
||||
const userId = 'user-123';
|
||||
const boardId = 'board-1';
|
||||
const existingBoard = { id: boardId, name: 'Board 1', userId, isGlobal: false };
|
||||
const globalBoard = { id: 'board-global', name: 'Alle Aufgaben', userId, isGlobal: true };
|
||||
const globalColumns = [
|
||||
{ id: 'col-g1', name: 'To Do', boardId: 'board-global', userId, order: 0 },
|
||||
];
|
||||
const boardColumns = [{ id: 'col-1', name: 'To Do', boardId, userId, order: 0 }];
|
||||
|
||||
// findBoardByIdOrThrow
|
||||
mockDb.query.kanbanBoards.findFirst
|
||||
.mockResolvedValueOnce(existingBoard) // deleteBoard -> findBoardByIdOrThrow
|
||||
.mockResolvedValueOnce(globalBoard); // getOrCreateGlobalBoard -> findFirst
|
||||
|
||||
// findAllColumns calls for global board, then board columns
|
||||
mockDb.query.kanbanColumns.findMany
|
||||
.mockResolvedValueOnce(globalColumns) // findAllColumns(globalBoard.id)
|
||||
.mockResolvedValueOnce(boardColumns); // findAllColumns(id) inside transaction
|
||||
|
||||
await service.deleteBoard(boardId, userId);
|
||||
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw when board does not exist', async () => {
|
||||
mockDb.query.kanbanBoards.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.deleteBoard('non-existent', 'user-123')).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when deleting global board', async () => {
|
||||
const userId = 'user-123';
|
||||
const boardId = 'board-global';
|
||||
const globalBoard = { id: boardId, name: 'Alle Aufgaben', userId, isGlobal: true };
|
||||
|
||||
mockDb.query.kanbanBoards.findFirst.mockResolvedValue(globalBoard);
|
||||
|
||||
await expect(service.deleteBoard(boardId, userId)).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reorderBoards', () => {
|
||||
it('should update order for each board and return all', async () => {
|
||||
const userId = 'user-123';
|
||||
const boardIds = ['board-2', 'board-1', 'board-3'];
|
||||
const reorderedBoards = [
|
||||
{ id: 'board-2', order: 0 },
|
||||
{ id: 'board-1', order: 1 },
|
||||
{ id: 'board-3', order: 2 },
|
||||
];
|
||||
|
||||
mockDb.query.kanbanBoards.findMany.mockResolvedValue(reorderedBoards);
|
||||
|
||||
const result = await service.reorderBoards(userId, boardIds);
|
||||
|
||||
expect(mockDb.update).toHaveBeenCalledTimes(3);
|
||||
expect(result).toEqual(reorderedBoards);
|
||||
});
|
||||
});
|
||||
|
||||
// =====================
|
||||
// Column operations
|
||||
// =====================
|
||||
|
||||
describe('findAllColumns', () => {
|
||||
it('should return all columns for a board', async () => {
|
||||
const userId = 'user-123';
|
||||
const boardId = 'board-1';
|
||||
const mockColumns = [
|
||||
{ id: 'col-1', name: 'To Do', boardId, userId, order: 0 },
|
||||
{ id: 'col-2', name: 'In Progress', boardId, userId, order: 1 },
|
||||
];
|
||||
|
||||
mockDb.query.kanbanColumns.findMany.mockResolvedValue(mockColumns);
|
||||
|
||||
const result = await service.findAllColumns(boardId, userId);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(mockDb.query.kanbanColumns.findMany).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return empty array when no columns', async () => {
|
||||
mockDb.query.kanbanColumns.findMany.mockResolvedValue([]);
|
||||
|
||||
const result = await service.findAllColumns('board-1', 'user-123');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findColumnByIdOrThrow', () => {
|
||||
it('should return a column when found', async () => {
|
||||
const userId = 'user-123';
|
||||
const columnId = 'col-1';
|
||||
const mockColumn = { id: columnId, name: 'To Do', userId };
|
||||
|
||||
mockDb.query.kanbanColumns.findFirst.mockResolvedValue(mockColumn);
|
||||
|
||||
const result = await service.findColumnByIdOrThrow(columnId, userId);
|
||||
|
||||
expect(result.id).toBe(columnId);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when column not found', async () => {
|
||||
mockDb.query.kanbanColumns.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.findColumnByIdOrThrow('non-existent', 'user-123')).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createColumn', () => {
|
||||
it('should create a column with correct order', async () => {
|
||||
const userId = 'user-123';
|
||||
const boardId = 'board-1';
|
||||
const dto = { boardId, name: 'New Column', defaultStatus: 'pending' as const };
|
||||
const existingBoard = { id: boardId, name: 'Board 1', userId };
|
||||
const existingColumns = [
|
||||
{ id: 'col-1', name: 'To Do', boardId, userId, order: 0 },
|
||||
{ id: 'col-2', name: 'Done', boardId, userId, order: 1 },
|
||||
];
|
||||
const createdColumn = { id: 'col-new', name: 'New Column', boardId, userId, order: 2 };
|
||||
|
||||
mockDb.query.kanbanBoards.findFirst.mockResolvedValue(existingBoard);
|
||||
mockDb.query.kanbanColumns.findMany.mockResolvedValue(existingColumns);
|
||||
mockDb.returning.mockResolvedValue([createdColumn]);
|
||||
|
||||
const result = await service.createColumn(userId, dto);
|
||||
|
||||
expect(result.order).toBe(2);
|
||||
expect(result.name).toBe('New Column');
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw when board does not exist', async () => {
|
||||
const dto = { boardId: 'non-existent', name: 'Column', defaultStatus: 'pending' as const };
|
||||
|
||||
mockDb.query.kanbanBoards.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.createColumn('user-123', dto)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateColumn', () => {
|
||||
it('should update a column', async () => {
|
||||
const userId = 'user-123';
|
||||
const columnId = 'col-1';
|
||||
const dto = { name: 'Updated Column' };
|
||||
const existingColumn = { id: columnId, name: 'Original', userId };
|
||||
const updatedColumn = { id: columnId, name: 'Updated Column', userId };
|
||||
|
||||
mockDb.query.kanbanColumns.findFirst.mockResolvedValue(existingColumn);
|
||||
mockDb.returning.mockResolvedValue([updatedColumn]);
|
||||
|
||||
const result = await service.updateColumn(columnId, userId, dto);
|
||||
|
||||
expect(result.name).toBe('Updated Column');
|
||||
});
|
||||
|
||||
it('should throw when column does not exist', async () => {
|
||||
mockDb.query.kanbanColumns.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(
|
||||
service.updateColumn('non-existent', 'user-123', { name: 'Test' })
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteColumn', () => {
|
||||
it('should delete a column and move tasks to another column', async () => {
|
||||
const userId = 'user-123';
|
||||
const columnId = 'col-2';
|
||||
const boardId = 'board-1';
|
||||
const existingColumn = { id: columnId, name: 'In Progress', boardId, userId };
|
||||
const allColumns = [
|
||||
{ id: 'col-1', name: 'To Do', boardId, userId, order: 0 },
|
||||
{ id: columnId, name: 'In Progress', boardId, userId, order: 1 },
|
||||
];
|
||||
|
||||
mockDb.query.kanbanColumns.findFirst.mockResolvedValue(existingColumn);
|
||||
mockDb.query.kanbanColumns.findMany.mockResolvedValue(allColumns);
|
||||
|
||||
await service.deleteColumn(columnId, userId);
|
||||
|
||||
// Should move tasks and then delete
|
||||
expect(mockDb.update).toHaveBeenCalled();
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw when column does not exist', async () => {
|
||||
mockDb.query.kanbanColumns.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.deleteColumn('non-existent', 'user-123')).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when deleting the last column', async () => {
|
||||
const userId = 'user-123';
|
||||
const columnId = 'col-1';
|
||||
const boardId = 'board-1';
|
||||
const existingColumn = { id: columnId, name: 'Only Column', boardId, userId };
|
||||
// Only this one column exists, no other column to move tasks to
|
||||
const allColumns = [{ id: columnId, name: 'Only Column', boardId, userId, order: 0 }];
|
||||
|
||||
mockDb.query.kanbanColumns.findFirst.mockResolvedValue(existingColumn);
|
||||
mockDb.query.kanbanColumns.findMany.mockResolvedValue(allColumns);
|
||||
|
||||
await expect(service.deleteColumn(columnId, userId)).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reorderColumns', () => {
|
||||
it('should update order for each column and return all', async () => {
|
||||
const userId = 'user-123';
|
||||
const boardId = 'board-1';
|
||||
const columnIds = ['col-3', 'col-1', 'col-2'];
|
||||
const firstColumn = { id: 'col-3', boardId, userId };
|
||||
const reorderedColumns = [
|
||||
{ id: 'col-3', order: 0, boardId },
|
||||
{ id: 'col-1', order: 1, boardId },
|
||||
{ id: 'col-2', order: 2, boardId },
|
||||
];
|
||||
|
||||
mockDb.query.kanbanColumns.findFirst.mockResolvedValue(firstColumn);
|
||||
mockDb.query.kanbanColumns.findMany.mockResolvedValue(reorderedColumns);
|
||||
|
||||
const result = await service.reorderColumns(userId, columnIds);
|
||||
|
||||
expect(mockDb.update).toHaveBeenCalledTimes(3);
|
||||
expect(result).toEqual(reorderedColumns);
|
||||
});
|
||||
|
||||
it('should return empty array when first column not found', async () => {
|
||||
const userId = 'user-123';
|
||||
const columnIds = ['col-non-existent'];
|
||||
|
||||
mockDb.query.kanbanColumns.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.reorderColumns(userId, columnIds);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// =====================
|
||||
// Task operations
|
||||
// =====================
|
||||
|
||||
describe('moveTaskToColumn', () => {
|
||||
it('should move a task to a column', async () => {
|
||||
const userId = 'user-123';
|
||||
const taskId = 'task-1';
|
||||
const columnId = 'col-2';
|
||||
const existingTask = { id: taskId, userId, columnId: 'col-1' };
|
||||
const column = {
|
||||
id: columnId,
|
||||
userId,
|
||||
autoComplete: false,
|
||||
defaultStatus: 'in_progress',
|
||||
};
|
||||
const movedTask = {
|
||||
id: taskId,
|
||||
userId,
|
||||
columnId,
|
||||
status: 'in_progress',
|
||||
isCompleted: false,
|
||||
};
|
||||
|
||||
mockDb.query.tasks.findFirst.mockResolvedValue(existingTask);
|
||||
mockDb.query.kanbanColumns.findFirst.mockResolvedValue(column);
|
||||
mockDb.returning.mockResolvedValue([movedTask]);
|
||||
|
||||
const result = await service.moveTaskToColumn(taskId, userId, columnId);
|
||||
|
||||
expect(result.columnId).toBe(columnId);
|
||||
expect(result.status).toBe('in_progress');
|
||||
});
|
||||
|
||||
it('should auto-complete task when moving to autoComplete column', async () => {
|
||||
const userId = 'user-123';
|
||||
const taskId = 'task-1';
|
||||
const columnId = 'col-done';
|
||||
const existingTask = { id: taskId, userId, columnId: 'col-1', isCompleted: false };
|
||||
const column = {
|
||||
id: columnId,
|
||||
userId,
|
||||
autoComplete: true,
|
||||
defaultStatus: 'completed',
|
||||
};
|
||||
const completedTask = {
|
||||
id: taskId,
|
||||
userId,
|
||||
columnId,
|
||||
status: 'completed',
|
||||
isCompleted: true,
|
||||
};
|
||||
|
||||
mockDb.query.tasks.findFirst.mockResolvedValue(existingTask);
|
||||
mockDb.query.kanbanColumns.findFirst.mockResolvedValue(column);
|
||||
mockDb.returning.mockResolvedValue([completedTask]);
|
||||
|
||||
const result = await service.moveTaskToColumn(taskId, userId, columnId);
|
||||
|
||||
expect(result.isCompleted).toBe(true);
|
||||
expect(result.status).toBe('completed');
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when task not found', async () => {
|
||||
mockDb.query.tasks.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.moveTaskToColumn('non-existent', 'user-123', 'col-1')).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when column not found', async () => {
|
||||
const existingTask = { id: 'task-1', userId: 'user-123' };
|
||||
|
||||
mockDb.query.tasks.findFirst.mockResolvedValue(existingTask);
|
||||
mockDb.query.kanbanColumns.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.moveTaskToColumn('task-1', 'user-123', 'non-existent')).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept optional order parameter', async () => {
|
||||
const userId = 'user-123';
|
||||
const taskId = 'task-1';
|
||||
const columnId = 'col-2';
|
||||
const existingTask = { id: taskId, userId };
|
||||
const column = { id: columnId, userId, autoComplete: false, defaultStatus: null };
|
||||
const movedTask = { id: taskId, userId, columnId, columnOrder: 5 };
|
||||
|
||||
mockDb.query.tasks.findFirst.mockResolvedValue(existingTask);
|
||||
mockDb.query.kanbanColumns.findFirst.mockResolvedValue(column);
|
||||
mockDb.returning.mockResolvedValue([movedTask]);
|
||||
|
||||
const result = await service.moveTaskToColumn(taskId, userId, columnId, 5);
|
||||
|
||||
expect(result.columnOrder).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOrCreateGlobalBoard', () => {
|
||||
it('should return existing global board', async () => {
|
||||
const userId = 'user-123';
|
||||
const globalBoard = { id: 'board-global', name: 'Alle Aufgaben', userId, isGlobal: true };
|
||||
|
||||
mockDb.query.kanbanBoards.findFirst.mockResolvedValue(globalBoard);
|
||||
|
||||
const result = await service.getOrCreateGlobalBoard(userId);
|
||||
|
||||
expect(result.isGlobal).toBe(true);
|
||||
expect(mockDb.insert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create global board when none exists', async () => {
|
||||
const userId = 'user-123';
|
||||
const createdBoard = {
|
||||
id: 'board-new',
|
||||
name: 'Alle Aufgaben',
|
||||
userId,
|
||||
isGlobal: true,
|
||||
order: 0,
|
||||
};
|
||||
|
||||
mockDb.query.kanbanBoards.findFirst.mockResolvedValue(undefined);
|
||||
mockDb.returning.mockResolvedValue([createdBoard]);
|
||||
|
||||
const result = await service.getOrCreateGlobalBoard(userId);
|
||||
|
||||
expect(result.isGlobal).toBe(true);
|
||||
expect(result.name).toBe('Alle Aufgaben');
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeDefaultColumns', () => {
|
||||
it('should return existing columns if they exist', async () => {
|
||||
const userId = 'user-123';
|
||||
const boardId = 'board-1';
|
||||
const existingColumns = [{ id: 'col-1', name: 'To Do', boardId, userId, order: 0 }];
|
||||
|
||||
mockDb.query.kanbanColumns.findMany.mockResolvedValue(existingColumns);
|
||||
|
||||
const result = await service.initializeDefaultColumns(boardId, userId);
|
||||
|
||||
expect(result).toEqual(existingColumns);
|
||||
expect(mockDb.insert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create default columns when none exist', async () => {
|
||||
const userId = 'user-123';
|
||||
const boardId = 'board-1';
|
||||
const createdColumns = [
|
||||
{ id: 'col-1', name: 'To Do', boardId, userId, order: 0 },
|
||||
{ id: 'col-2', name: 'In Arbeit', boardId, userId, order: 1 },
|
||||
{ id: 'col-3', name: 'Erledigt', boardId, userId, order: 2 },
|
||||
];
|
||||
|
||||
mockDb.query.kanbanColumns.findMany
|
||||
.mockResolvedValueOnce([]) // first call: check existing
|
||||
.mockResolvedValueOnce(createdColumns); // second call: return created
|
||||
|
||||
const result = await service.initializeDefaultColumns(boardId, userId);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { ReminderService } from '../reminder.service';
|
||||
import { TaskService } from '../../task/task.service';
|
||||
import { DATABASE_CONNECTION } from '../../db/database.module';
|
||||
|
||||
const mockDb = {
|
||||
query: {
|
||||
reminders: {
|
||||
findMany: jest.fn(),
|
||||
findFirst: jest.fn(),
|
||||
},
|
||||
},
|
||||
insert: jest.fn().mockReturnThis(),
|
||||
update: jest.fn().mockReturnThis(),
|
||||
delete: jest.fn().mockReturnThis(),
|
||||
values: jest.fn().mockReturnThis(),
|
||||
set: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
returning: jest.fn(),
|
||||
};
|
||||
|
||||
const mockTaskService = {
|
||||
findByIdOrThrow: jest.fn(),
|
||||
};
|
||||
|
||||
describe('ReminderService', () => {
|
||||
let service: ReminderService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ReminderService,
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useValue: mockDb,
|
||||
},
|
||||
{
|
||||
provide: TaskService,
|
||||
useValue: mockTaskService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ReminderService>(ReminderService);
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Re-set chainable mocks after clearAllMocks
|
||||
mockDb.insert.mockReturnThis();
|
||||
mockDb.update.mockReturnThis();
|
||||
mockDb.delete.mockReturnThis();
|
||||
mockDb.values.mockReturnThis();
|
||||
mockDb.set.mockReturnThis();
|
||||
mockDb.where.mockReturnThis();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('findByTask', () => {
|
||||
it('should return all reminders for a task', async () => {
|
||||
const userId = 'user-123';
|
||||
const taskId = 'task-1';
|
||||
const mockTask = { id: taskId, title: 'Test Task', userId };
|
||||
const mockReminders = [
|
||||
{ id: 'rem-1', taskId, userId, minutesBefore: 15, type: 'push' },
|
||||
{ id: 'rem-2', taskId, userId, minutesBefore: 60, type: 'email' },
|
||||
];
|
||||
|
||||
mockTaskService.findByIdOrThrow.mockResolvedValue(mockTask);
|
||||
mockDb.query.reminders.findMany.mockResolvedValue(mockReminders);
|
||||
|
||||
const result = await service.findByTask(taskId, userId);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(mockTaskService.findByIdOrThrow).toHaveBeenCalledWith(taskId, userId);
|
||||
expect(mockDb.query.reminders.findMany).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return empty array when no reminders', async () => {
|
||||
const mockTask = { id: 'task-1', title: 'Test Task', userId: 'user-123' };
|
||||
|
||||
mockTaskService.findByIdOrThrow.mockResolvedValue(mockTask);
|
||||
mockDb.query.reminders.findMany.mockResolvedValue([]);
|
||||
|
||||
const result = await service.findByTask('task-1', 'user-123');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should throw when task does not belong to user', async () => {
|
||||
mockTaskService.findByIdOrThrow.mockRejectedValue(new NotFoundException());
|
||||
|
||||
await expect(service.findByTask('non-existent', 'user-123')).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return a reminder when found', async () => {
|
||||
const userId = 'user-123';
|
||||
const reminderId = 'rem-1';
|
||||
const mockReminder = { id: reminderId, taskId: 'task-1', userId, minutesBefore: 15 };
|
||||
|
||||
mockDb.query.reminders.findFirst.mockResolvedValue(mockReminder);
|
||||
|
||||
const result = await service.findById(reminderId, userId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.id).toBe(reminderId);
|
||||
});
|
||||
|
||||
it('should return null when reminder not found', async () => {
|
||||
mockDb.query.reminders.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.findById('non-existent', 'user-123');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByIdOrThrow', () => {
|
||||
it('should return a reminder when found', async () => {
|
||||
const userId = 'user-123';
|
||||
const reminderId = 'rem-1';
|
||||
const mockReminder = { id: reminderId, taskId: 'task-1', userId, minutesBefore: 15 };
|
||||
|
||||
mockDb.query.reminders.findFirst.mockResolvedValue(mockReminder);
|
||||
|
||||
const result = await service.findByIdOrThrow(reminderId, userId);
|
||||
|
||||
expect(result.id).toBe(reminderId);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when reminder not found', async () => {
|
||||
mockDb.query.reminders.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.findByIdOrThrow('non-existent', 'user-123')).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a reminder for a task with a due date', async () => {
|
||||
const userId = 'user-123';
|
||||
const taskId = 'task-1';
|
||||
const dueDate = new Date('2026-04-01T10:00:00Z');
|
||||
const mockTask = { id: taskId, title: 'Test Task', userId, dueDate };
|
||||
const dto = { minutesBefore: 30, type: 'push' as const };
|
||||
const createdReminder = {
|
||||
id: 'rem-new',
|
||||
taskId,
|
||||
userId,
|
||||
minutesBefore: 30,
|
||||
type: 'push',
|
||||
reminderTime: new Date('2026-04-01T09:30:00Z'),
|
||||
};
|
||||
|
||||
mockTaskService.findByIdOrThrow.mockResolvedValue(mockTask);
|
||||
mockDb.returning.mockResolvedValue([createdReminder]);
|
||||
|
||||
const result = await service.create(taskId, userId, dto);
|
||||
|
||||
expect(result.minutesBefore).toBe(30);
|
||||
expect(result.type).toBe('push');
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(mockTaskService.findByIdOrThrow).toHaveBeenCalledWith(taskId, userId);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when task has no due date', async () => {
|
||||
const userId = 'user-123';
|
||||
const taskId = 'task-1';
|
||||
const mockTask = { id: taskId, title: 'Test Task', userId, dueDate: null };
|
||||
const dto = { minutesBefore: 30 };
|
||||
|
||||
mockTaskService.findByIdOrThrow.mockResolvedValue(mockTask);
|
||||
|
||||
await expect(service.create(taskId, userId, dto)).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw when task does not exist', async () => {
|
||||
mockTaskService.findByIdOrThrow.mockRejectedValue(new NotFoundException());
|
||||
|
||||
await expect(
|
||||
service.create('non-existent', 'user-123', { minutesBefore: 30 })
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should default to push type when not provided', async () => {
|
||||
const userId = 'user-123';
|
||||
const taskId = 'task-1';
|
||||
const dueDate = new Date('2026-04-01T10:00:00Z');
|
||||
const mockTask = { id: taskId, title: 'Test Task', userId, dueDate };
|
||||
const dto = { minutesBefore: 15 };
|
||||
const createdReminder = {
|
||||
id: 'rem-new',
|
||||
taskId,
|
||||
userId,
|
||||
minutesBefore: 15,
|
||||
type: 'push',
|
||||
reminderTime: new Date('2026-04-01T09:45:00Z'),
|
||||
};
|
||||
|
||||
mockTaskService.findByIdOrThrow.mockResolvedValue(mockTask);
|
||||
mockDb.returning.mockResolvedValue([createdReminder]);
|
||||
|
||||
const result = await service.create(taskId, userId, dto);
|
||||
|
||||
expect(result.type).toBe('push');
|
||||
});
|
||||
|
||||
it('should calculate correct reminder time', async () => {
|
||||
const userId = 'user-123';
|
||||
const taskId = 'task-1';
|
||||
const dueDate = new Date('2026-04-01T10:00:00Z');
|
||||
const mockTask = { id: taskId, title: 'Test Task', userId, dueDate };
|
||||
const dto = { minutesBefore: 60 };
|
||||
|
||||
// Capture the values passed to insert
|
||||
const expectedReminderTime = new Date('2026-04-01T09:00:00Z');
|
||||
const createdReminder = {
|
||||
id: 'rem-new',
|
||||
taskId,
|
||||
userId,
|
||||
minutesBefore: 60,
|
||||
type: 'push',
|
||||
reminderTime: expectedReminderTime,
|
||||
};
|
||||
|
||||
mockTaskService.findByIdOrThrow.mockResolvedValue(mockTask);
|
||||
mockDb.returning.mockResolvedValue([createdReminder]);
|
||||
|
||||
const result = await service.create(taskId, userId, dto);
|
||||
|
||||
expect(result.reminderTime).toEqual(expectedReminderTime);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a reminder', async () => {
|
||||
const userId = 'user-123';
|
||||
const reminderId = 'rem-1';
|
||||
const existingReminder = { id: reminderId, taskId: 'task-1', userId };
|
||||
|
||||
mockDb.query.reminders.findFirst.mockResolvedValue(existingReminder);
|
||||
|
||||
await service.delete(reminderId, userId);
|
||||
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw when reminder does not exist', async () => {
|
||||
mockDb.query.reminders.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.delete('non-existent', 'user-123')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteByTask', () => {
|
||||
it('should delete all reminders for a task', async () => {
|
||||
const userId = 'user-123';
|
||||
const taskId = 'task-1';
|
||||
|
||||
await service.deleteByTask(taskId, userId);
|
||||
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue