From 86b66b803d93bcdd978fe87945fb747b2aa081f3 Mon Sep 17 00:00:00 2001 From: Ricardo Campos Date: Sun, 2 Mar 2025 18:45:36 -0300 Subject: [PATCH] fix: loggin issues (#319) * fix: loggin issues closes #302 feat: improve build number in docker image * test: fix integration test for the taskController * test: add unit test to taskService * test: add more unit tests to taskService * test: add delete unit tests for taskService * test: add patch unit tests for taskService --- .github/workflows/pr.yml | 14 +- README.md | 8 + client/src/api-service/api.ts | 2 +- server/Dockerfile | 3 + .../server/controller/TaskController.java | 6 +- .../server/service/TaskService.java | 15 +- .../server/controller/TaskControllerTest.java | 13 +- .../server/service/TaskServiceTest.java | 201 ++++++++++++++++++ tools/tools.md | 4 +- 9 files changed, 250 insertions(+), 16 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index e52a5da..82b5c7d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -86,6 +86,10 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Get current date + id: date + run: echo "date=$(date +'%Y-%m-%d-%H%M%S')" >> $GITHUB_OUTPUT - name: Build and push uses: docker/build-push-action@v6 @@ -93,7 +97,8 @@ jobs: push: true context: ./client tags: ghcr.io/ricardo-campos-org/react-typescript-todolist/client:${{ github.event.number }} - build-args: VITE_BUILD=${{ github.event.number }} + build-args: | + BUILD=v${{ github.event.number }}-${{ github.run_id }}-${{ steps.date.outputs.date }} cache-from: type=gha cache-to: type=gha,mode=max @@ -164,6 +169,10 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Get current date + id: date + run: echo "date=$(date +'%Y-%m-%d-%H%M%S')" >> $GITHUB_OUTPUT - name: Build and push uses: docker/build-push-action@v6 @@ -171,6 +180,7 @@ jobs: push: true context: ./server tags: ghcr.io/ricardo-campos-org/react-typescript-todolist/server:${{ github.event.number }} - build-args: BUILD=${{ github.event.number }} + build-args: | + BUILD=v${{ github.event.number }}-${{ github.run_id }}-${{ steps.date.outputs.date }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/README.md b/README.md index 5c5d96f..8758664 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,16 @@ ### TaskNote API [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=br.com.tasknoteapp%3Aserver&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=br.com.tasknoteapp%3Aserver) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ricardo-campos-org_react-typescript-todolist_server&metric=coverage)](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_server) +[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=br.com.tasknoteapp%3Aserver&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=br.com.tasknoteapp%3Aserver) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=br.com.tasknoteapp%3Aserver&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=br.com.tasknoteapp%3Aserver) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=br.com.tasknoteapp%3Aserver&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=br.com.tasknoteapp%3Aserver) ### TaskNote WebApp +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ricardo-campos-org_react-typescript-todolist_client&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ricardo-campos-org_react-typescript-todolist_client&metric=coverage)](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client) +[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=ricardo-campos-org_react-typescript-todolist_client&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=ricardo-campos-org_react-typescript-todolist_client&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=ricardo-campos-org_react-typescript-todolist_client&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client) ## 📋 Table of Contents diff --git a/client/src/api-service/api.ts b/client/src/api-service/api.ts index e7fa05f..189012a 100644 --- a/client/src/api-service/api.ts +++ b/client/src/api-service/api.ts @@ -53,7 +53,7 @@ function handleError(httpStatusCode: number) { async function handleResponse(response: Response) { // Successful responses if (response.ok) { - const codesToIgnore: number[] = [204, 201]; + const codesToIgnore: number[] = [204]; if (codesToIgnore.includes(response.status)) { return; } diff --git a/server/Dockerfile b/server/Dockerfile index 015c12e..a18011c 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -22,6 +22,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /app COPY --from=build /app/target/server ./tasknote-api +ARG BUILD +ENV BUILD=${BUILD} + # User, port and health check USER 1001 EXPOSE ${PORT} diff --git a/server/src/main/java/br/com/tasknoteapp/server/controller/TaskController.java b/server/src/main/java/br/com/tasknoteapp/server/controller/TaskController.java index 06b847d..fb7b34a 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/controller/TaskController.java +++ b/server/src/main/java/br/com/tasknoteapp/server/controller/TaskController.java @@ -162,7 +162,7 @@ public class TaskController { content = @Content( mediaType = "application/json", - schema = @Schema(implementation = Void.class))), + schema = @Schema(implementation = TaskResponse.class))), @ApiResponse( responseCode = "400", description = "Wrong or missing information", @@ -179,8 +179,8 @@ public class TaskController { @RequestBody @Valid TaskRequest taskRequest) { - taskService.createTask(taskRequest); - return ResponseEntity.status(HttpStatus.CREATED).build(); + TaskResponse response = taskService.createTask(taskRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(response); } /** diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/TaskService.java b/server/src/main/java/br/com/tasknoteapp/server/service/TaskService.java index 3cd698f..313ef37 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/TaskService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/TaskService.java @@ -49,7 +49,7 @@ public class TaskService { log.info("{} tasks found!", tasks.size()); return tasks.stream() - .map((TaskEntity tr) -> TaskResponse.fromEntity(tr, getAllNotesUrls(tr.getId()))) + .map((TaskEntity tr) -> TaskResponse.fromEntity(tr, getAllTasksUrls(tr.getId()))) .toList(); } @@ -69,7 +69,7 @@ public class TaskService { } log.info("Task found! Id {}", taskId); - return TaskResponse.fromEntity(task.get(), getAllNotesUrls(taskId)); + return TaskResponse.fromEntity(task.get(), getAllTasksUrls(taskId)); } /** @@ -77,7 +77,7 @@ public class TaskService { * * @param taskRequest The {@link TaskRequest} containing all task data. */ - public void createTask(TaskRequest taskRequest) { + public TaskResponse createTask(TaskRequest taskRequest) { UserEntity user = getCurrentUser(); log.info("Creating task to user {}", user.getId()); @@ -99,6 +99,7 @@ public class TaskService { } log.info("Task created! Id {}", created.getId()); + return TaskResponse.fromEntity(created, getAllTasksUrls(created.getId())); } /** @@ -161,7 +162,7 @@ public class TaskService { log.info("Task patched! Id {}", patchedTask.getId()); - return TaskResponse.fromEntity(patchedTask, getAllNotesUrls(taskId)); + return TaskResponse.fromEntity(patchedTask, getAllTasksUrls(taskId)); } /** @@ -209,7 +210,7 @@ public class TaskService { log.info("{} tasks found!", tasks.size()); return tasks.stream() - .map((TaskEntity tr) -> TaskResponse.fromEntity(tr, getAllNotesUrls(tr.getId()))) + .map((TaskEntity tr) -> TaskResponse.fromEntity(tr, getAllTasksUrls(tr.getId()))) .toList(); } @@ -219,8 +220,8 @@ public class TaskService { return authService.findByEmail(email).orElseThrow(); } - private List getAllNotesUrls(Long noteId) { - List urls = taskUrlRepository.findAllById_taskId(noteId); + private List getAllTasksUrls(Long taskId) { + List urls = taskUrlRepository.findAllById_taskId(taskId); return urls.stream().map(TaskUrlEntity::getId).map(TaskUrlEntityPk::getUrl).toList(); } diff --git a/server/src/test/java/br/com/tasknoteapp/server/controller/TaskControllerTest.java b/server/src/test/java/br/com/tasknoteapp/server/controller/TaskControllerTest.java index 3fd8264..86b4705 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/controller/TaskControllerTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/controller/TaskControllerTest.java @@ -267,7 +267,18 @@ class TaskControllerTest { void postTasks_happyPath_shouldSucceed() throws Exception { TaskRequest request = new TaskRequest("Test task", List.of("www.url.com"), null, true, "tag"); - doNothing().when(taskService).createTask(request); + TaskResponse taskResponse = + new TaskResponse( + 858L, + "Description patched", + false, + true, + null, + null, + "Moments ago", + "tag", + List.of()); + when(taskService.createTask(request)).thenReturn(taskResponse); final String payloadJson = """ diff --git a/server/src/test/java/br/com/tasknoteapp/server/service/TaskServiceTest.java b/server/src/test/java/br/com/tasknoteapp/server/service/TaskServiceTest.java index 0be43cb..6246a01 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/service/TaskServiceTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/service/TaskServiceTest.java @@ -1,16 +1,23 @@ package br.com.tasknoteapp.server.service; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import br.com.tasknoteapp.server.entity.TaskEntity; +import br.com.tasknoteapp.server.entity.TaskUrlEntity; +import br.com.tasknoteapp.server.entity.TaskUrlEntityPk; import br.com.tasknoteapp.server.entity.UserEntity; import br.com.tasknoteapp.server.exception.TaskNotFoundException; import br.com.tasknoteapp.server.repository.TaskRepository; import br.com.tasknoteapp.server.repository.TaskUrlRepository; +import br.com.tasknoteapp.server.request.TaskPatchRequest; import br.com.tasknoteapp.server.request.TaskRequest; import br.com.tasknoteapp.server.response.TaskResponse; import br.com.tasknoteapp.server.util.AuthUtil; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -164,4 +171,198 @@ class TaskServiceTest { Assertions.assertNotNull(entity); Assertions.assertEquals("development", entity.getTag()); } + + @Test + @DisplayName("Create task with null url it should succeed") + void createTask_nullUrl_shouldSucceed() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(USER_EMAIL)); + + UserEntity userEntity = new UserEntity(); + userEntity.setId(USER_ID); + userEntity.setEmail(USER_EMAIL); + when(authService.findByEmail(USER_EMAIL)).thenReturn(Optional.of(userEntity)); + + TaskRequest request = + new TaskRequest("Write unit tests", null, "2025-12-12", false, "development"); + + TaskEntity entity = new TaskEntity(); + entity.setId(123L); + entity.setDescription(request.description()); + entity.setHighPriority(request.highPriority()); + entity.setTag(request.tag()); + when(taskRepository.save(any())).thenReturn(new TaskEntity()); + + TaskResponse response = taskService.createTask(request); + + Assertions.assertNotNull(response); + Assertions.assertTrue(response.urls().isEmpty()); + } + + @Test + @DisplayName("Create task with empty url it should succeed") + void createTask_emptyUrl_shouldSucceed() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(USER_EMAIL)); + + UserEntity userEntity = new UserEntity(); + userEntity.setId(USER_ID); + userEntity.setEmail(USER_EMAIL); + when(authService.findByEmail(USER_EMAIL)).thenReturn(Optional.of(userEntity)); + + TaskRequest request = + new TaskRequest("Write unit tests", List.of(), "2025-12-12", false, "development"); + + TaskEntity entity = new TaskEntity(); + entity.setId(123L); + entity.setDescription(request.description()); + entity.setHighPriority(request.highPriority()); + entity.setTag(request.tag()); + when(taskRepository.save(any())).thenReturn(new TaskEntity()); + + TaskResponse response = taskService.createTask(request); + + Assertions.assertNotNull(response); + Assertions.assertTrue(response.urls().isEmpty()); + } + + @Test + @DisplayName("Create task with valid url it should succeed") + void createTask_fullUrl_shouldSucceed() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(USER_EMAIL)); + + UserEntity userEntity = new UserEntity(); + userEntity.setId(USER_ID); + userEntity.setEmail(USER_EMAIL); + when(authService.findByEmail(USER_EMAIL)).thenReturn(Optional.of(userEntity)); + + TaskRequest request = + new TaskRequest( + "Write unit tests", List.of("debian.org"), "2025-12-12", false, "development"); + + TaskEntity entity = new TaskEntity(); + entity.setId(123L); + entity.setDescription(request.description()); + entity.setHighPriority(request.highPriority()); + entity.setTag(request.tag()); + when(taskRepository.save(any())).thenReturn(entity); + + TaskUrlEntity urlEntity = new TaskUrlEntity(); + urlEntity.setId(new TaskUrlEntityPk(entity.getId(), "debian.org")); + when(taskUrlRepository.findAllById_taskId(entity.getId())).thenReturn(List.of(urlEntity)); + + TaskResponse response = taskService.createTask(request); + + Assertions.assertNotNull(response); + Assertions.assertFalse(response.urls().isEmpty()); + Assertions.assertEquals("debian.org", response.urls().get(0)); + } + + @Test + @DisplayName("Get all tasks happy path should succeed") + void getAllTasks_happyPath_shouldSucceed() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(USER_EMAIL)); + + UserEntity userEntity = new UserEntity(); + userEntity.setId(USER_ID); + userEntity.setEmail(USER_EMAIL); + when(authService.findByEmail(USER_EMAIL)).thenReturn(Optional.of(userEntity)); + + TaskEntity entity = new TaskEntity(); + entity.setDescription("Writ unit tests"); + entity.setHighPriority(true); + entity.setTag("dev"); + when(taskRepository.findAllByUser_id(USER_ID)).thenReturn(List.of(entity)); + + List responses = taskService.getAllTasks(); + + Assertions.assertFalse(responses.isEmpty()); + Assertions.assertEquals(1, responses.size()); + Assertions.assertEquals(entity.getTag(), responses.get(0).tag()); + } + + @Test + @DisplayName("Delete a task following the happy path should succeed") + void deleteTask_happyPath_shouldSucceed() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(USER_EMAIL)); + + UserEntity userEntity = new UserEntity(); + userEntity.setId(USER_ID); + userEntity.setEmail(USER_EMAIL); + when(authService.findByEmail(USER_EMAIL)).thenReturn(Optional.of(userEntity)); + + Long taskId = 2525L; + + TaskEntity taskEntity = new TaskEntity(); + taskEntity.setId(taskId); + taskEntity.setDescription("Test task"); + taskEntity.setHighPriority(true); + taskEntity.setTag("test"); + taskEntity.setUser(userEntity); + when(taskRepository.findById(taskId)).thenReturn(Optional.of(taskEntity)); + + when(taskUrlRepository.findAllById_taskId(taskId)).thenReturn(List.of()); + + doNothing().when(taskRepository).delete(taskEntity); + + taskService.deleteTask(taskId); + + verify(taskRepository, times(1)).delete(any()); + } + + @Test + @DisplayName("Delete a not existing tasks it should fail") + void deleteTask_notFound_shouldFail() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(USER_EMAIL)); + + UserEntity userEntity = new UserEntity(); + userEntity.setId(USER_ID); + userEntity.setEmail(USER_EMAIL); + when(authService.findByEmail(USER_EMAIL)).thenReturn(Optional.of(userEntity)); + + Long taskId = 2526L; + + when(taskRepository.findById(taskId)).thenReturn(Optional.empty()); + + Assertions.assertThrows( + TaskNotFoundException.class, + () -> { + taskService.deleteTask(taskId); + }); + } + + @Test + @DisplayName("Patch a task following the happy path it should succeed") + void patchTask_happyPath_shouldSucceed() { + when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(USER_EMAIL)); + + UserEntity userEntity = new UserEntity(); + userEntity.setId(USER_ID); + userEntity.setEmail(USER_EMAIL); + when(authService.findByEmail(USER_EMAIL)).thenReturn(Optional.of(userEntity)); + + Long taskId = 2525L; + + TaskEntity taskEntity = new TaskEntity(); + taskEntity.setId(taskId); + taskEntity.setDescription("Test task"); + taskEntity.setHighPriority(true); + taskEntity.setTag("test"); + taskEntity.setUser(userEntity); + when(taskRepository.findById(taskId)).thenReturn(Optional.of(taskEntity)); + + when(taskUrlRepository.findAllById_taskId(taskId)).thenReturn(List.of()); + + TaskEntity entity = new TaskEntity(); + entity.setDescription("Updated description"); + entity.setHighPriority(false); + entity.setTag(taskEntity.getTag()); + when(taskRepository.save(any())).thenReturn(entity); + + TaskPatchRequest patch = + new TaskPatchRequest("Updated description", null, null, null, false, null); + TaskResponse patched = taskService.patchTask(taskId, patch); + + Assertions.assertNotNull(patched); + Assertions.assertEquals("Updated description", patched.description()); + Assertions.assertFalse(patched.highPriority()); + } } diff --git a/tools/tools.md b/tools/tools.md index 6b21ef5..ed64f32 100644 --- a/tools/tools.md +++ b/tools/tools.md @@ -27,7 +27,7 @@ npm ci --ignore-scripts --no-update-notifier --omit=dev \ If you want to build with Docker: ```sh -docker build -t client ./client +docker build --build-arg VITE_BUILD="Development-$(date "+%Y-%m-%d %H:%M:%S")" -t client ./client ``` That's it! @@ -37,7 +37,7 @@ That's it! For the backend there's a Dockerfile ready, just run (from the project root): ```bash -docker build -t server ./server +docker build --build-arg BUILD="Development-$(date "+%Y-%m-%d %H:%M:%S")" -t server ./server ``` That's it!