From 4c0f227e273a233498dda37145e23909ccac813b Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 21 Nov 2025 16:47:04 +0100 Subject: [PATCH] test(JiraSync): further refine test coverage for synchronization and change detection scenarios --- .../task-management/__tests__/daily.test.ts | 309 +++++++++++++ .../task-management/__tests__/tags.test.ts | 286 ++++++++++++ .../task-management/__tests__/tasks.test.ts | 436 ++++++++++++++++++ 3 files changed, 1031 insertions(+) create mode 100644 src/services/task-management/__tests__/daily.test.ts create mode 100644 src/services/task-management/__tests__/tags.test.ts create mode 100644 src/services/task-management/__tests__/tasks.test.ts diff --git a/src/services/task-management/__tests__/daily.test.ts b/src/services/task-management/__tests__/daily.test.ts new file mode 100644 index 0000000..9736cdf --- /dev/null +++ b/src/services/task-management/__tests__/daily.test.ts @@ -0,0 +1,309 @@ +/** + * Tests unitaires pour DailyService + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { DailyService } from '../daily'; +import { prisma } from '@/services/core/database'; +import { + getPreviousWorkday, + normalizeDate, + getToday, + getYesterday, +} from '@/lib/date-utils'; + +// Mock de prisma +vi.mock('@/services/core/database', () => ({ + prisma: { + dailyCheckbox: { + findMany: vi.fn(), + findUnique: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + aggregate: vi.fn(), + }, + }, +})); + +// Mock de date-utils +vi.mock('@/lib/date-utils', () => ({ + getPreviousWorkday: vi.fn(), + normalizeDate: vi.fn(), + formatDateForAPI: vi.fn(), + getToday: vi.fn(), + getYesterday: vi.fn(), +})); + +describe('DailyService', () => { + let service: DailyService; + const mockUserId = 'user-123'; + const mockDate = new Date('2024-01-15'); + + beforeEach(() => { + vi.clearAllMocks(); + service = new DailyService(); + vi.mocked(normalizeDate).mockImplementation((date) => date); + vi.mocked(getPreviousWorkday).mockReturnValue(new Date('2024-01-14')); + vi.mocked(getToday).mockReturnValue(mockDate); + vi.mocked(getYesterday).mockReturnValue(new Date('2024-01-14')); + }); + + describe('getDailyView', () => { + it('devrait récupérer la vue daily pour une date', async () => { + const yesterdayCheckboxes: any[] = []; + const todayCheckboxes: any[] = []; + + vi.mocked(prisma.dailyCheckbox.findMany) + .mockResolvedValueOnce(yesterdayCheckboxes) + .mockResolvedValueOnce(todayCheckboxes); + + const view = await service.getDailyView(mockDate, mockUserId); + + expect(view.date).toEqual(mockDate); + expect(view.yesterday).toEqual(yesterdayCheckboxes); + expect(view.today).toEqual(todayCheckboxes); + }); + }); + + describe('getCheckboxesByDate', () => { + it('devrait récupérer les checkboxes pour une date', async () => { + const mockCheckboxes = [ + { + id: 'checkbox-1', + date: mockDate, + text: 'Checkbox 1', + isChecked: false, + type: 'task', + order: 0, + taskId: null, + userId: mockUserId, + task: null, + user: { id: mockUserId }, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]; + + vi.mocked(prisma.dailyCheckbox.findMany).mockResolvedValue( + mockCheckboxes as any + ); + + const checkboxes = await service.getCheckboxesByDate( + mockDate, + mockUserId + ); + + expect(checkboxes).toBeDefined(); + expect(prisma.dailyCheckbox.findMany).toHaveBeenCalledWith({ + where: { + date: mockDate, + userId: mockUserId, + }, + include: { + task: { + include: { + taskTags: { + include: { + tag: true, + }, + }, + primaryTag: true, + }, + }, + user: true, + }, + orderBy: { order: 'asc' }, + }); + }); + }); + + describe('addCheckbox', () => { + it('devrait ajouter une checkbox', async () => { + const mockCheckbox = { + id: 'checkbox-1', + date: mockDate, + text: 'New checkbox', + isChecked: false, + type: 'task', + order: 0, + taskId: null, + userId: mockUserId, + task: null, + user: { id: mockUserId }, + createdAt: new Date(), + updatedAt: new Date(), + }; + + vi.mocked(prisma.dailyCheckbox.aggregate).mockResolvedValue({ + _max: { order: null }, + } as any); + vi.mocked(prisma.dailyCheckbox.create).mockResolvedValue( + mockCheckbox as any + ); + + const checkbox = await service.addCheckbox({ + date: mockDate, + text: 'New checkbox', + userId: mockUserId, + }); + + expect(checkbox).toBeDefined(); + expect(prisma.dailyCheckbox.create).toHaveBeenCalled(); + }); + + it("devrait utiliser l'ordre spécifié", async () => { + const mockCheckbox = { + id: 'checkbox-1', + date: mockDate, + text: 'New checkbox', + isChecked: false, + type: 'task', + order: 5, + taskId: null, + userId: mockUserId, + task: null, + user: { id: mockUserId }, + createdAt: new Date(), + updatedAt: new Date(), + }; + + vi.mocked(prisma.dailyCheckbox.create).mockResolvedValue( + mockCheckbox as any + ); + + await service.addCheckbox({ + date: mockDate, + text: 'New checkbox', + userId: mockUserId, + order: 5, + }); + + expect(prisma.dailyCheckbox.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + order: 5, + }), + }) + ); + }); + }); + + describe('updateCheckbox', () => { + it('devrait mettre à jour une checkbox', async () => { + const mockCheckbox = { + id: 'checkbox-1', + date: mockDate, + text: 'Updated checkbox', + isChecked: true, + type: 'task', + order: 0, + taskId: null, + userId: mockUserId, + task: null, + user: { id: mockUserId }, + createdAt: new Date(), + updatedAt: new Date(), + }; + + vi.mocked(prisma.dailyCheckbox.update).mockResolvedValue( + mockCheckbox as any + ); + + const checkbox = await service.updateCheckbox('checkbox-1', { + text: 'Updated checkbox', + isChecked: true, + }); + + expect(checkbox).toBeDefined(); + expect(prisma.dailyCheckbox.update).toHaveBeenCalled(); + }); + }); + + describe('toggleCheckbox', () => { + it("devrait toggle l'état d'une checkbox", async () => { + const existingCheckbox = { + id: 'checkbox-1', + isChecked: false, + }; + + const updatedCheckbox = { + id: 'checkbox-1', + date: mockDate, + text: 'Checkbox', + isChecked: true, + type: 'task', + order: 0, + taskId: null, + userId: mockUserId, + task: null, + user: { id: mockUserId }, + createdAt: new Date(), + updatedAt: new Date(), + }; + + vi.mocked(prisma.dailyCheckbox.findUnique).mockResolvedValue( + existingCheckbox as any + ); + vi.mocked(prisma.dailyCheckbox.update).mockResolvedValue( + updatedCheckbox as any + ); + + const checkbox = await service.toggleCheckbox('checkbox-1'); + + expect(checkbox.isChecked).toBe(true); + expect(prisma.dailyCheckbox.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + isChecked: true, + }), + }) + ); + }); + + it("devrait lancer une erreur si la checkbox n'existe pas", async () => { + vi.mocked(prisma.dailyCheckbox.findUnique).mockResolvedValue(null); + + await expect(service.toggleCheckbox('non-existent')).rejects.toThrow( + 'non trouvée' + ); + }); + }); + + describe('deleteCheckbox', () => { + it('devrait supprimer une checkbox', async () => { + const existingCheckbox = { + id: 'checkbox-1', + date: mockDate, + text: 'Checkbox', + isChecked: false, + type: 'task', + order: 0, + taskId: null, + userId: mockUserId, + createdAt: new Date(), + updatedAt: new Date(), + }; + + vi.mocked(prisma.dailyCheckbox.findUnique).mockResolvedValue( + existingCheckbox as any + ); + vi.mocked(prisma.dailyCheckbox.delete).mockResolvedValue({} as any); + + await service.deleteCheckbox('checkbox-1'); + + expect(prisma.dailyCheckbox.delete).toHaveBeenCalledWith({ + where: { id: 'checkbox-1' }, + }); + }); + + it("devrait lancer une erreur si la checkbox n'existe pas", async () => { + vi.mocked(prisma.dailyCheckbox.findUnique).mockResolvedValue(null); + + await expect(service.deleteCheckbox('non-existent')).rejects.toThrow( + 'non trouvée' + ); + }); + }); +}); diff --git a/src/services/task-management/__tests__/tags.test.ts b/src/services/task-management/__tests__/tags.test.ts new file mode 100644 index 0000000..7d0bc97 --- /dev/null +++ b/src/services/task-management/__tests__/tags.test.ts @@ -0,0 +1,286 @@ +/** + * Tests unitaires pour tagsService + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { tagsService } from '../tags'; +import { prisma } from '@/services/core/database'; + +// Mock de prisma +vi.mock('@/services/core/database', () => ({ + prisma: { + tag: { + findMany: vi.fn(), + findFirst: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + }, + taskTag: { + deleteMany: vi.fn(), + }, + }, +})); + +describe('tagsService', () => { + const mockUserId = 'user-123'; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('getTags', () => { + it('devrait récupérer tous les tags avec leur usage', async () => { + const mockTags = [ + { + id: 'tag-1', + name: 'Tag 1', + color: '#ff0000', + isPinned: false, + _count: { taskTags: 5 }, + }, + { + id: 'tag-2', + name: 'Tag 2', + color: '#00ff00', + isPinned: true, + _count: { taskTags: 2 }, + }, + ]; + + vi.mocked(prisma.tag.findMany).mockResolvedValue(mockTags as any); + + const tags = await tagsService.getTags(mockUserId); + + expect(tags).toHaveLength(2); + expect(tags[0]).toEqual({ + id: 'tag-1', + name: 'Tag 1', + color: '#ff0000', + isPinned: false, + usage: 5, + }); + expect(prisma.tag.findMany).toHaveBeenCalledWith({ + where: { ownerId: mockUserId }, + include: { + _count: { + select: { + taskTags: true, + }, + }, + }, + orderBy: { name: 'asc' }, + }); + }); + }); + + describe('getTagById', () => { + it('devrait récupérer un tag par son ID', async () => { + const mockTag = { + id: 'tag-1', + name: 'Tag 1', + color: '#ff0000', + isPinned: false, + }; + + vi.mocked(prisma.tag.findFirst).mockResolvedValue(mockTag as any); + + const tag = await tagsService.getTagById('tag-1', mockUserId); + + expect(tag).toEqual(mockTag); + expect(prisma.tag.findFirst).toHaveBeenCalledWith({ + where: { + id: 'tag-1', + ownerId: mockUserId, + }, + }); + }); + + it("devrait retourner null si le tag n'existe pas", async () => { + vi.mocked(prisma.tag.findFirst).mockResolvedValue(null); + + const tag = await tagsService.getTagById('non-existent', mockUserId); + + expect(tag).toBeNull(); + }); + }); + + describe('getTagByName', () => { + it('devrait récupérer un tag par son nom', async () => { + const mockTag = { + id: 'tag-1', + name: 'Tag 1', + color: '#ff0000', + isPinned: false, + }; + + vi.mocked(prisma.tag.findFirst).mockResolvedValue(mockTag as any); + + const tag = await tagsService.getTagByName('Tag 1', mockUserId); + + expect(tag).toEqual(mockTag); + expect(prisma.tag.findFirst).toHaveBeenCalledWith({ + where: { + name: { + equals: 'Tag 1', + }, + ownerId: mockUserId, + }, + }); + }); + }); + + describe('createTag', () => { + it('devrait créer un nouveau tag', async () => { + const mockTag = { + id: 'tag-1', + name: 'New Tag', + color: '#ff0000', + isPinned: false, + }; + + vi.mocked(prisma.tag.findFirst).mockResolvedValue(null); // Pas de tag existant + vi.mocked(prisma.tag.create).mockResolvedValue(mockTag as any); + + const tag = await tagsService.createTag({ + name: 'New Tag', + color: '#ff0000', + userId: mockUserId, + }); + + expect(tag).toEqual(mockTag); + expect(prisma.tag.create).toHaveBeenCalledWith({ + data: { + name: 'New Tag', + color: '#ff0000', + isPinned: false, + ownerId: mockUserId, + }, + }); + }); + + it('devrait lancer une erreur si le tag existe déjà', async () => { + const existingTag = { + id: 'tag-1', + name: 'Existing Tag', + color: '#ff0000', + isPinned: false, + }; + + vi.mocked(prisma.tag.findFirst).mockResolvedValue(existingTag as any); + + await expect( + tagsService.createTag({ + name: 'Existing Tag', + color: '#ff0000', + userId: mockUserId, + }) + ).rejects.toThrow('existe déjà'); + }); + + it('devrait trimmer le nom du tag', async () => { + const mockTag = { + id: 'tag-1', + name: 'Trimmed Tag', + color: '#ff0000', + isPinned: false, + }; + + vi.mocked(prisma.tag.findFirst).mockResolvedValue(null); + vi.mocked(prisma.tag.create).mockResolvedValue(mockTag as any); + + await tagsService.createTag({ + name: ' Trimmed Tag ', + color: '#ff0000', + userId: mockUserId, + }); + + expect(prisma.tag.create).toHaveBeenCalledWith({ + data: { + name: 'Trimmed Tag', + color: '#ff0000', + isPinned: false, + ownerId: mockUserId, + }, + }); + }); + }); + + describe('updateTag', () => { + it('devrait mettre à jour un tag', async () => { + const mockTag = { + id: 'tag-1', + name: 'Updated Tag', + color: '#00ff00', + isPinned: true, + }; + + vi.mocked(prisma.tag.findFirst).mockResolvedValue({ + id: 'tag-1', + name: 'Old Tag', + color: '#ff0000', + isPinned: false, + } as any); + vi.mocked(prisma.tag.update).mockResolvedValue(mockTag as any); + + const tag = await tagsService.updateTag('tag-1', mockUserId, { + name: 'Updated Tag', + color: '#00ff00', + isPinned: true, + }); + + expect(tag).toEqual(mockTag); + expect(prisma.tag.update).toHaveBeenCalledWith({ + where: { id: 'tag-1' }, + data: { + name: 'Updated Tag', + color: '#00ff00', + isPinned: true, + }, + }); + }); + + it("devrait lancer une erreur si le tag n'existe pas", async () => { + vi.mocked(prisma.tag.findFirst).mockResolvedValue(null); + + await expect( + tagsService.updateTag('non-existent', mockUserId, { + name: 'Updated Tag', + }) + ).rejects.toThrow('non trouvé'); + }); + }); + + describe('deleteTag', () => { + it('devrait supprimer un tag', async () => { + vi.mocked(prisma.tag.findFirst).mockResolvedValue({ + id: 'tag-1', + name: 'Tag to delete', + color: '#ff0000', + isPinned: false, + } as any); + vi.mocked(prisma.taskTag.deleteMany).mockResolvedValue({ + count: 0, + } as any); + vi.mocked(prisma.tag.delete).mockResolvedValue({} as any); + + await tagsService.deleteTag('tag-1', mockUserId); + + expect(prisma.taskTag.deleteMany).toHaveBeenCalledWith({ + where: { tagId: 'tag-1' }, + }); + expect(prisma.tag.delete).toHaveBeenCalledWith({ + where: { id: 'tag-1' }, + }); + }); + + it("devrait lancer une erreur si le tag n'existe pas", async () => { + vi.mocked(prisma.tag.findFirst).mockResolvedValue(null); + + await expect( + tagsService.deleteTag('non-existent', mockUserId) + ).rejects.toThrow('non trouvé'); + }); + }); +}); diff --git a/src/services/task-management/__tests__/tasks.test.ts b/src/services/task-management/__tests__/tasks.test.ts new file mode 100644 index 0000000..6d73adc --- /dev/null +++ b/src/services/task-management/__tests__/tasks.test.ts @@ -0,0 +1,436 @@ +/** + * Tests unitaires pour TasksService + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { TasksService } from '../tasks'; +import { prisma } from '@/services/core/database'; +import { tagsService } from '../tags'; +import { getReadonlyFieldsForTask } from '../readonly-fields'; +import { getToday } from '@/lib/date-utils'; + +// Mock de prisma +vi.mock('@/services/core/database', () => ({ + prisma: { + task: { + findMany: vi.fn(), + findFirst: vi.fn(), + findUnique: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + aggregate: vi.fn(), + }, + taskTag: { + createMany: vi.fn(), + deleteMany: vi.fn(), + findMany: vi.fn(), + }, + dailyCheckbox: { + findMany: vi.fn(), + }, + }, +})); + +// Mock de tagsService +vi.mock('../tags', () => ({ + tagsService: { + ensureTagsExist: vi.fn(), + }, +})); + +// Mock de readonly-fields +vi.mock('../readonly-fields', () => ({ + getReadonlyFieldsForTask: vi.fn(), +})); + +// Mock de date-utils +vi.mock('@/lib/date-utils', () => ({ + getToday: vi.fn(), +})); + +describe('TasksService', () => { + let service: TasksService; + const mockUserId = 'user-123'; + + beforeEach(() => { + vi.clearAllMocks(); + service = new TasksService(); + vi.mocked(getToday).mockReturnValue(new Date('2024-01-15')); + }); + + describe('getTasks', () => { + it('devrait récupérer toutes les tâches pour un utilisateur', async () => { + const mockTasks = [ + { + id: 'task-1', + title: 'Task 1', + description: null, + status: 'todo', + priority: 'medium', + source: 'manual', + sourceId: 'manual-123', + ownerId: mockUserId, + createdAt: new Date(), + updatedAt: new Date(), + dueDate: null, + completedAt: null, + primaryTagId: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + tfsProject: null, + tfsPullRequestId: null, + tfsRepository: null, + tfsSourceBranch: null, + tfsTargetBranch: null, + assignee: null, + taskTags: [], + primaryTag: null, + _count: { dailyCheckboxes: 0 }, + }, + ]; + + vi.mocked(prisma.task.findMany).mockResolvedValue(mockTasks as any); + vi.mocked(getReadonlyFieldsForTask).mockReturnValue([]); + + const tasks = await service.getTasks(mockUserId); + + expect(tasks).toHaveLength(1); + expect(prisma.task.findMany).toHaveBeenCalledWith({ + where: { ownerId: mockUserId }, + include: { + taskTags: { + include: { + tag: true, + }, + }, + primaryTag: true, + _count: { + select: { + dailyCheckboxes: true, + }, + }, + }, + take: undefined, + skip: 0, + orderBy: [ + { completedAt: 'desc' }, + { dueDate: 'asc' }, + { createdAt: 'desc' }, + ], + }); + }); + + it('devrait filtrer par statut', async () => { + vi.mocked(prisma.task.findMany).mockResolvedValue([]); + + await service.getTasks(mockUserId, { status: ['todo', 'in_progress'] }); + + expect(prisma.task.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + status: { in: ['todo', 'in_progress'] }, + }), + }) + ); + }); + + it('devrait rechercher dans le titre et la description', async () => { + vi.mocked(prisma.task.findMany).mockResolvedValue([]); + + await service.getTasks(mockUserId, { search: 'test' }); + + expect(prisma.task.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + OR: [ + { title: { contains: 'test' } }, + { description: { contains: 'test' } }, + ], + }), + }) + ); + }); + }); + + describe('createTask', () => { + it('devrait créer une nouvelle tâche', async () => { + const mockTask = { + id: 'task-1', + title: 'New Task', + description: 'Description', + status: 'todo', + priority: 'medium', + ownerId: mockUserId, + source: 'manual', + sourceId: 'manual-123', + createdAt: new Date(), + updatedAt: new Date(), + dueDate: null, + completedAt: null, + primaryTagId: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + tfsProject: null, + tfsPullRequestId: null, + tfsRepository: null, + tfsSourceBranch: null, + tfsTargetBranch: null, + assignee: null, + taskTags: [], + primaryTag: null, + }; + + vi.mocked(prisma.task.create).mockResolvedValue(mockTask as any); + vi.mocked(prisma.task.findUnique).mockResolvedValue(mockTask as any); + vi.mocked(tagsService.ensureTagsExist).mockResolvedValue([]); + vi.mocked(getReadonlyFieldsForTask).mockReturnValue([]); + + const task = await service.createTask({ + title: 'New Task', + description: 'Description', + ownerId: mockUserId, + }); + + expect(task).toBeDefined(); + expect(prisma.task.create).toHaveBeenCalled(); + }); + + it('devrait définir completedAt si le statut est done', async () => { + const mockTask = { + id: 'task-1', + title: 'Done Task', + description: null, + status: 'done', + priority: 'medium', + ownerId: mockUserId, + source: 'manual', + sourceId: 'manual-123', + createdAt: new Date(), + updatedAt: new Date(), + dueDate: null, + completedAt: new Date('2024-01-15'), + primaryTagId: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + tfsProject: null, + tfsPullRequestId: null, + tfsRepository: null, + tfsSourceBranch: null, + tfsTargetBranch: null, + assignee: null, + taskTags: [], + primaryTag: null, + }; + + vi.mocked(prisma.task.create).mockResolvedValue(mockTask as any); + vi.mocked(prisma.task.findUnique).mockResolvedValue(mockTask as any); + vi.mocked(tagsService.ensureTagsExist).mockResolvedValue([]); + vi.mocked(getReadonlyFieldsForTask).mockReturnValue([]); + + await service.createTask({ + title: 'Done Task', + status: 'done', + ownerId: mockUserId, + }); + + expect(prisma.task.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + completedAt: expect.any(Date), + }), + }) + ); + }); + }); + + describe('updateTask', () => { + it('devrait mettre à jour une tâche', async () => { + const mockTask = { + id: 'task-1', + title: 'Updated Task', + description: null, + status: 'in_progress', + priority: 'medium', + ownerId: mockUserId, + source: 'manual', + sourceId: 'manual-123', + createdAt: new Date(), + updatedAt: new Date(), + dueDate: null, + completedAt: null, + primaryTagId: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + tfsProject: null, + tfsPullRequestId: null, + tfsRepository: null, + tfsSourceBranch: null, + tfsTargetBranch: null, + assignee: null, + taskTags: [], + primaryTag: null, + }; + + vi.mocked(prisma.task.findFirst).mockResolvedValue(mockTask as any); + vi.mocked(prisma.task.update).mockResolvedValue(mockTask as any); + vi.mocked(prisma.task.findUnique).mockResolvedValue(mockTask as any); + vi.mocked(getReadonlyFieldsForTask).mockReturnValue([]); + + const task = await service.updateTask(mockUserId, 'task-1', { + title: 'Updated Task', + status: 'in_progress', + }); + + expect(task).toBeDefined(); + expect(prisma.task.update).toHaveBeenCalled(); + }); + + it('devrait définir completedAt quand le statut passe à done', async () => { + const mockTask = { + id: 'task-1', + title: 'Task', + description: null, + status: 'todo', + priority: 'medium', + ownerId: mockUserId, + source: 'manual', + sourceId: 'manual-123', + createdAt: new Date(), + updatedAt: new Date(), + dueDate: null, + completedAt: null, + primaryTagId: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + tfsProject: null, + tfsPullRequestId: null, + tfsRepository: null, + tfsSourceBranch: null, + tfsTargetBranch: null, + assignee: null, + taskTags: [], + primaryTag: null, + }; + + vi.mocked(prisma.task.findFirst).mockResolvedValue(mockTask as any); + vi.mocked(prisma.task.update).mockResolvedValue({ + ...mockTask, + status: 'done', + completedAt: new Date('2024-01-15'), + } as any); + vi.mocked(prisma.task.findUnique).mockResolvedValue({ + ...mockTask, + status: 'done', + completedAt: new Date('2024-01-15'), + } as any); + vi.mocked(getReadonlyFieldsForTask).mockReturnValue([]); + + await service.updateTask(mockUserId, 'task-1', { + status: 'done', + }); + + expect(prisma.task.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + completedAt: expect.any(Date), + }), + }) + ); + }); + + it("devrait lancer une erreur si la tâche n'existe pas", async () => { + vi.mocked(prisma.task.findFirst).mockResolvedValue(null); + + await expect( + service.updateTask(mockUserId, 'non-existent', { + title: 'Updated', + }) + ).rejects.toThrow('introuvable'); + }); + }); + + describe('deleteTask', () => { + it('devrait supprimer une tâche', async () => { + const mockTask = { + id: 'task-1', + title: 'Task to delete', + ownerId: mockUserId, + }; + + vi.mocked(prisma.task.findFirst).mockResolvedValue(mockTask as any); + vi.mocked(prisma.task.delete).mockResolvedValue({} as any); + + await service.deleteTask(mockUserId, 'task-1'); + + expect(prisma.task.delete).toHaveBeenCalledWith({ + where: { id: 'task-1' }, + }); + }); + + it("devrait lancer une erreur si la tâche n'existe pas", async () => { + vi.mocked(prisma.task.findFirst).mockResolvedValue(null); + + await expect( + service.deleteTask(mockUserId, 'non-existent') + ).rejects.toThrow('introuvable'); + }); + }); + + describe('updateTaskStatus', () => { + it("devrait mettre à jour le statut d'une tâche", async () => { + const mockTask = { + id: 'task-1', + title: 'Task', + description: null, + status: 'todo', + priority: 'medium', + ownerId: mockUserId, + source: 'manual', + sourceId: 'manual-123', + createdAt: new Date(), + updatedAt: new Date(), + dueDate: null, + completedAt: null, + primaryTagId: null, + jiraProject: null, + jiraKey: null, + jiraType: null, + tfsProject: null, + tfsPullRequestId: null, + tfsRepository: null, + tfsSourceBranch: null, + tfsTargetBranch: null, + assignee: null, + taskTags: [], + primaryTag: null, + }; + + vi.mocked(prisma.task.findFirst).mockResolvedValue(mockTask as any); + vi.mocked(prisma.task.update).mockResolvedValue({ + ...mockTask, + status: 'in_progress', + } as any); + vi.mocked(prisma.task.findUnique).mockResolvedValue({ + ...mockTask, + status: 'in_progress', + } as any); + vi.mocked(getReadonlyFieldsForTask).mockReturnValue([]); + + const task = await service.updateTaskStatus( + mockUserId, + 'task-1', + 'in_progress' + ); + + expect(task).toBeDefined(); + expect(prisma.task.update).toHaveBeenCalled(); + }); + }); +});