From 433874d11e916aaadb7bc533e4993432c000f74a Mon Sep 17 00:00:00 2001 From: Gabriel Sancho Date: Fri, 27 Mar 2026 03:38:41 -0300 Subject: [PATCH] fix(frontend): keep entity and message deletes in sync --- .../newsletter/service/EntityService.kt | 2 +- .../TaskGeneratedMessageControllerTest.kt | 28 +++++++++++++ .../controller/VirtualEntityControllerTest.kt | 11 ++++++ .../src/__tests__/pages/EditTaskPage.test.tsx | 39 +++++++++++++++++++ .../src/__tests__/pages/EntitiesPage.test.tsx | 17 ++++++++ frontend/src/pages/EditTaskPage.tsx | 11 +++++- frontend/src/pages/EntitiesPage.tsx | 8 +++- 7 files changed, 112 insertions(+), 4 deletions(-) diff --git a/backend/src/main/kotlin/com/condado/newsletter/service/EntityService.kt b/backend/src/main/kotlin/com/condado/newsletter/service/EntityService.kt index 225938e..023267a 100644 --- a/backend/src/main/kotlin/com/condado/newsletter/service/EntityService.kt +++ b/backend/src/main/kotlin/com/condado/newsletter/service/EntityService.kt @@ -21,7 +21,7 @@ class EntityService( /** Returns all virtual entities. */ fun findAll(): List = - virtualEntityRepository.findAll().map { VirtualEntityResponseDto.from(it) } + virtualEntityRepository.findAllByActiveTrue().map { VirtualEntityResponseDto.from(it) } /** Returns one entity by ID, or null if not found. */ fun findById(id: UUID): VirtualEntityResponseDto? = diff --git a/backend/src/test/kotlin/com/condado/newsletter/controller/TaskGeneratedMessageControllerTest.kt b/backend/src/test/kotlin/com/condado/newsletter/controller/TaskGeneratedMessageControllerTest.kt index 9511671..89bfaa3 100644 --- a/backend/src/test/kotlin/com/condado/newsletter/controller/TaskGeneratedMessageControllerTest.kt +++ b/backend/src/test/kotlin/com/condado/newsletter/controller/TaskGeneratedMessageControllerTest.kt @@ -138,4 +138,32 @@ class TaskGeneratedMessageControllerTest { .andExpect(jsonPath("$").isArray) .andExpect(jsonPath("$.length()").value(0)) } + + @Test + fun should_deleteOnlySelectedHistoryItem_when_multipleMessagesExist() { + val task = createTask() + val firstMessage = generatedMessageHistoryRepository.save( + GeneratedMessageHistory( + task = task, + label = "Message #1", + content = "SUBJECT: First\nBODY:\nHello" + ) + ) + val secondMessage = generatedMessageHistoryRepository.save( + GeneratedMessageHistory( + task = task, + label = "Message #2", + content = "SUBJECT: Second\nBODY:\nHi" + ) + ) + + mockMvc.perform(delete("/api/v1/tasks/${task.id}/generated-messages/${firstMessage.id}").cookie(authCookie())) + .andExpect(status().isNoContent) + + mockMvc.perform(get("/api/v1/tasks/${task.id}/generated-messages").cookie(authCookie())) + .andExpect(status().isOk) + .andExpect(jsonPath("$.length()").value(1)) + .andExpect(jsonPath("$[0].id").value(secondMessage.id.toString())) + .andExpect(jsonPath("$[0].label").value("Message #2")) + } } diff --git a/backend/src/test/kotlin/com/condado/newsletter/controller/VirtualEntityControllerTest.kt b/backend/src/test/kotlin/com/condado/newsletter/controller/VirtualEntityControllerTest.kt index ee5ed1b..c5f7c62 100644 --- a/backend/src/test/kotlin/com/condado/newsletter/controller/VirtualEntityControllerTest.kt +++ b/backend/src/test/kotlin/com/condado/newsletter/controller/VirtualEntityControllerTest.kt @@ -62,6 +62,17 @@ class VirtualEntityControllerTest { .andExpect(status().isOk).andExpect(jsonPath("$").isArray).andExpect(jsonPath("$[0].name").value("Test Entity")) } + @Test + fun should_returnOnlyActiveEntities_when_getAllEntities() { + virtualEntityRepository.save(VirtualEntity(name = "Active Entity", email = "active@condado.com", jobTitle = "Tester", active = true)) + virtualEntityRepository.save(VirtualEntity(name = "Inactive Entity", email = "inactive@condado.com", jobTitle = "Tester", active = false)) + + mockMvc.perform(get("/api/v1/virtual-entities").cookie(authCookie())) + .andExpect(status().isOk) + .andExpect(jsonPath("$.length()").value(1)) + .andExpect(jsonPath("$[0].name").value("Active Entity")) + } + @Test fun should_return200AndEntity_when_getById() { val entity = virtualEntityRepository.save(VirtualEntity(name = "Test Entity", email = "entity@condado.com", jobTitle = "Test Job")) diff --git a/frontend/src/__tests__/pages/EditTaskPage.test.tsx b/frontend/src/__tests__/pages/EditTaskPage.test.tsx index 8ca7c00..b95028f 100644 --- a/frontend/src/__tests__/pages/EditTaskPage.test.tsx +++ b/frontend/src/__tests__/pages/EditTaskPage.test.tsx @@ -294,6 +294,45 @@ describe('EditTaskPage', () => { }) }) + it('should_removeOnlyDeletedGeneratedMessage_when_deleteSucceedsWithoutRefetch', async () => { + persistedHistory = [ + { + id: 'message-2', + taskId: 'task-1', + label: 'Message #2', + content: 'SUBJECT: Second\nBODY:\nSecond output', + createdAt: '2026-03-27T12:10:00Z', + }, + { + id: 'message-1', + taskId: 'task-1', + label: 'Message #1', + content: 'SUBJECT: First\nBODY:\nFirst output', + createdAt: '2026-03-27T12:00:00Z', + }, + ] + vi.mocked(tasksApi.getTaskGeneratedMessages).mockResolvedValue(persistedHistory) + vi.mocked(tasksApi.deleteTaskGeneratedMessage).mockResolvedValue(undefined) + + renderPage() + + const secondMessageHistoryItem = await screen.findByRole('button', { name: /^message #2$/i }) + expect(await screen.findByRole('button', { name: /^message #1$/i })).toBeInTheDocument() + + fireEvent.click( + screen.getByRole('button', { + name: /delete message #1/i, + }) + ) + + await waitFor(() => { + expect(tasksApi.deleteTaskGeneratedMessage).toHaveBeenCalledWith('task-1', 'message-1') + expect(screen.queryByRole('button', { name: /^message #1$/i })).not.toBeInTheDocument() + expect(secondMessageHistoryItem).toBeInTheDocument() + expect(screen.getByText(/Second output/i)).toBeInTheDocument() + }) + }) + it('should_loadPersistedGeneratedMessageHistory_when_pageLoads', async () => { persistedHistory = [ { diff --git a/frontend/src/__tests__/pages/EntitiesPage.test.tsx b/frontend/src/__tests__/pages/EntitiesPage.test.tsx index 9167270..e155603 100644 --- a/frontend/src/__tests__/pages/EntitiesPage.test.tsx +++ b/frontend/src/__tests__/pages/EntitiesPage.test.tsx @@ -89,6 +89,23 @@ describe('EntitiesPage', () => { }) }) + it('should_removeDeletedEntityFromList_when_deleteSucceeds', async () => { + vi.mocked(entitiesApi.getEntities).mockResolvedValue([mockEntity]) + vi.mocked(entitiesApi.deleteEntity).mockResolvedValue(undefined) + + render(, { wrapper }) + + await waitFor(() => { + expect(screen.getByText('Test Entity')).toBeInTheDocument() + }) + + fireEvent.click(screen.getByRole('button', { name: /delete|deactivate/i })) + + await waitFor(() => { + expect(screen.queryByText('Test Entity')).not.toBeInTheDocument() + }) + }) + it('should_renderDetailLink_when_entitiesLoaded', async () => { vi.mocked(entitiesApi.getEntities).mockResolvedValue([mockEntity]) render(, { wrapper }) diff --git a/frontend/src/pages/EditTaskPage.tsx b/frontend/src/pages/EditTaskPage.tsx index dcb99ee..47c98ba 100644 --- a/frontend/src/pages/EditTaskPage.tsx +++ b/frontend/src/pages/EditTaskPage.tsx @@ -255,8 +255,15 @@ export default function EditTaskPage() { const deleteGeneratedMessageMutation = useMutation({ mutationFn: (messageId: string) => deleteTaskGeneratedMessage(taskId, messageId), - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ['task-generated-messages', taskId] }) + onSuccess: ( _data, messageId) => { + queryClient.setQueryData( + ['task-generated-messages', taskId], + ( + currentMessages: + | Awaited> + | undefined + ) => currentMessages?.filter((message) => message.id !== messageId) ?? [] + ) }, }) diff --git a/frontend/src/pages/EntitiesPage.tsx b/frontend/src/pages/EntitiesPage.tsx index 0f79c6e..8c58eff 100644 --- a/frontend/src/pages/EntitiesPage.tsx +++ b/frontend/src/pages/EntitiesPage.tsx @@ -37,7 +37,13 @@ export default function EntitiesPage() { const deleteMutation = useMutation({ mutationFn: (id: string) => deleteEntity(id), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['entities'] }), + onSuccess: (_data, id) => { + queryClient.setQueryData( + ['entities'], + (currentEntities: Awaited> | undefined) => + currentEntities?.filter((entity) => entity.id !== id) ?? [] + ) + }, }) return (