From 4c740f66a6367df0268ea148fb49a28a21af889e Mon Sep 17 00:00:00 2001 From: lukasmatusiewicz Date: Wed, 8 Apr 2026 16:54:13 +0200 Subject: [PATCH 1/6] add taskservice tests --- README.md | 20 +++ pom.xml | 19 +++ .../taskmanager/service/TaskServiceTest.java | 123 ++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 src/test/java/com/lucc/taskmanager/service/TaskServiceTest.java diff --git a/README.md b/README.md index 7b670d8..cb76029 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # ๐Ÿš€ Modern Task Manager - Portfolio Project +![Coverage](.github/badges/jacoco.svg) +![Branches](.github/badges/branches.svg) + A comprehensive, full-stack Task Management application built with **Spring Boot 3** and **Java 21**. This project serves as a showcase of modern software engineering practices, featuring a layered architecture, robust security, and a responsive, interactive UI. --- @@ -27,6 +30,23 @@ This project is designed to be easily testable by anyone, whether they are a dev ### **4. Containerized Portability (Docker)** * No Java installed? No problem. Run `docker-compose up --build` to spin up both the application and a **PostgreSQL** database in isolated containers. +### **5. CI/CD with GitHub Actions** +* The project includes a GitHub Actions workflow (`maven.yml`) that automatically builds the project and runs all tests on every push or pull request to the `master`, `main`, or `dockerization` branches. + +--- + +## ๐Ÿงช Testing + +The project includes a suite of unit tests for the service layer, ensuring business logic correctness. + +* **TaskServiceTest**: Tests CRUD operations, user-based task access, and validation logic. +* **UserServiceTest**: Tests user registration, password encryption, and Spring Security's `UserDetailsService` implementation. + +To run the tests locally, use: +```bash +./mvnw test +``` + --- ## ๐Ÿ› ๏ธ Tech Stack & Skills Showcased diff --git a/pom.xml b/pom.xml index e0ce5e2..cc3450f 100644 --- a/pom.xml +++ b/pom.xml @@ -104,6 +104,25 @@ 21 + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + + prepare-agent + + + + report + test + + report + + + + diff --git a/src/test/java/com/lucc/taskmanager/service/TaskServiceTest.java b/src/test/java/com/lucc/taskmanager/service/TaskServiceTest.java new file mode 100644 index 0000000..9e70bc8 --- /dev/null +++ b/src/test/java/com/lucc/taskmanager/service/TaskServiceTest.java @@ -0,0 +1,123 @@ +package com.lucc.taskmanager.service; + +import com.lucc.taskmanager.model.Priority; +import com.lucc.taskmanager.model.Status; +import com.lucc.taskmanager.model.Task; +import com.lucc.taskmanager.model.User; +import com.lucc.taskmanager.repository.TaskRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class TaskServiceTest { + + @Mock + private TaskRepository taskRepository; + + @InjectMocks + private TaskService taskService; + + private User user; + private Task task; + + @BeforeEach + void setUp() { + user = new User(); + user.setUsername("testuser"); + + task = new Task(); + task.setId(1); + task.setTitle("Test Task"); + task.setDescription("Test Description"); + task.setUser(user); + task.setStatus(Status.TODO); + task.setPriority(Priority.MEDIUM); + } + + @Test + void getTasksByUser_ShouldReturnTasks() { + when(taskRepository.findByUser(user)).thenReturn(Arrays.asList(task)); + + List tasks = taskService.getTasksByUser(user); + + assertEquals(1, tasks.size()); + assertEquals(task.getTitle(), tasks.get(0).getTitle()); + verify(taskRepository, times(1)).findByUser(user); + } + + @Test + void addTask_ShouldSaveTask() { + when(taskRepository.save(any(Task.class))).thenReturn(task); + + Task savedTask = taskService.addTask(new Task(), user); + + assertNotNull(savedTask); + assertEquals(user, savedTask.getUser()); + verify(taskRepository, times(1)).save(any(Task.class)); + } + + @Test + void getTaskById_ShouldReturnTask_WhenTaskExistsAndUserMatches() { + when(taskRepository.findById(1)).thenReturn(Optional.of(task)); + + Task foundTask = taskService.getTaskById(1, user); + + assertNotNull(foundTask); + assertEquals(1, foundTask.getId()); + } + + @Test + void getTaskById_ShouldThrowException_WhenTaskDoesNotExist() { + when(taskRepository.findById(1)).thenReturn(Optional.empty()); + + assertThrows(IllegalArgumentException.class, () -> taskService.getTaskById(1, user)); + } + + @Test + void getTaskById_ShouldThrowException_WhenUserDoesNotMatch() { + User otherUser = new User(); + otherUser.setUsername("otheruser"); + when(taskRepository.findById(1)).thenReturn(Optional.of(task)); + + assertThrows(IllegalArgumentException.class, () -> taskService.getTaskById(1, otherUser)); + } + + @Test + void updateTask_ShouldUpdateAndSaveTask() { + Task updatedInfo = new Task(); + updatedInfo.setTitle("Updated Title"); + updatedInfo.setDescription("Updated Description"); + updatedInfo.setStatus(Status.DONE); + updatedInfo.setPriority(Priority.HIGH); + + when(taskRepository.findById(1)).thenReturn(Optional.of(task)); + when(taskRepository.save(any(Task.class))).thenReturn(task); + + Task result = taskService.updateTask(1, updatedInfo, user); + + assertEquals("Updated Title", result.getTitle()); + assertEquals(Status.DONE, result.getStatus()); + verify(taskRepository, times(1)).save(task); + } + + @Test + void deleteTask_ShouldDeleteTask_WhenTaskExistsAndUserMatches() { + when(taskRepository.findById(1)).thenReturn(Optional.of(task)); + + taskService.deleteTask(1, user); + + verify(taskRepository, times(1)).delete(task); + } +} From 4a1d138b70e4c3571f810a3ba6dd4b1bcc61a1fa Mon Sep 17 00:00:00 2001 From: lukasmatusiewicz Date: Wed, 8 Apr 2026 16:54:19 +0200 Subject: [PATCH 2/6] Create UserServiceTest.java --- .../taskmanager/service/UserServiceTest.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/test/java/com/lucc/taskmanager/service/UserServiceTest.java diff --git a/src/test/java/com/lucc/taskmanager/service/UserServiceTest.java b/src/test/java/com/lucc/taskmanager/service/UserServiceTest.java new file mode 100644 index 0000000..30e77c4 --- /dev/null +++ b/src/test/java/com/lucc/taskmanager/service/UserServiceTest.java @@ -0,0 +1,85 @@ +package com.lucc.taskmanager.service; + +import com.lucc.taskmanager.model.Role; +import com.lucc.taskmanager.model.User; +import com.lucc.taskmanager.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class UserServiceTest { + + @Mock + private UserRepository userRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @InjectMocks + private UserService userService; + + private User user; + + @BeforeEach + void setUp() { + user = new User(); + user.setUsername("testuser"); + user.setPassword("password"); + user.setEmail("test@example.com"); + } + + @Test + void getUsers_ShouldReturnAllUsers() { + when(userRepository.findAll()).thenReturn(Arrays.asList(user)); + + List users = userService.getUsers(); + + assertEquals(1, users.size()); + verify(userRepository, times(1)).findAll(); + } + + @Test + void addUser_ShouldEncryptPasswordAndSaveUser() { + when(passwordEncoder.encode("password")).thenReturn("encryptedPassword"); + when(userRepository.save(any(User.class))).thenReturn(user); + + User savedUser = userService.addUser(user); + + assertNotNull(savedUser); + assertEquals(Role.USER, user.getRole()); // Default role + assertEquals("encryptedPassword", user.getPassword()); + verify(userRepository, times(1)).save(user); + } + + @Test + void loadUserByUsername_ShouldReturnUser_WhenUserExists() { + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(user)); + + UserDetails userDetails = userService.loadUserByUsername("testuser"); + + assertNotNull(userDetails); + assertEquals("testuser", userDetails.getUsername()); + } + + @Test + void loadUserByUsername_ShouldThrowException_WhenUserDoesNotExist() { + when(userRepository.findByUsername("unknown")).thenReturn(Optional.empty()); + + assertThrows(UsernameNotFoundException.class, () -> userService.loadUserByUsername("unknown")); + } +} From 77a1a72bde05b7259f701eb1b6c2ec3e93628035 Mon Sep 17 00:00:00 2001 From: lukasmatusiewicz Date: Wed, 8 Apr 2026 16:54:47 +0200 Subject: [PATCH 3/6] create github workflow to run tests with coverage badge --- .github/workflows/maven.yml | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/maven.yml diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..3c99871 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,42 @@ +name: Java CI with Maven + +on: + push: + branches: [ "master", "main", "dockerization" ] + pull_request: + branches: [ "master", "main", "dockerization" ] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml + + - name: Generate JaCoCo Badge + uses: cicirello/jacoco-badge-generator@v2 + with: + generate-branches-badge: true + jacoco-csv-file: target/site/jacoco/jacoco.csv + + - name: Log coverage percentage + run: | + echo "coverage = ${{ steps.jacoco.outputs.coverage }}" + echo "branch coverage = ${{ steps.jacoco.outputs.branches }}" + + - name: Commit and push the badge (if it changed) + uses: EndBug/add-and-commit@v9 + with: + default_author: github_actions + message: 'Update coverage badge' + add: '*.svg' From b13dbba595b6fc98fb8ebc0901ee41c783b3b85e Mon Sep 17 00:00:00 2001 From: lukasmatusiewicz Date: Wed, 8 Apr 2026 16:58:02 +0200 Subject: [PATCH 4/6] fix tests --- .github/workflows/maven.yml | 2 +- .../lucc/taskmanager/TaskManagerApplicationTests.java | 2 ++ src/test/resources/application-test.properties | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/application-test.properties diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 3c99871..939353d 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -21,7 +21,7 @@ jobs: distribution: 'temurin' cache: maven - name: Build with Maven - run: mvn -B package --file pom.xml + run: mvn -B package -Dspring.profiles.active=test --file pom.xml - name: Generate JaCoCo Badge uses: cicirello/jacoco-badge-generator@v2 diff --git a/src/test/java/com/lucc/taskmanager/TaskManagerApplicationTests.java b/src/test/java/com/lucc/taskmanager/TaskManagerApplicationTests.java index 917d715..ab21d28 100644 --- a/src/test/java/com/lucc/taskmanager/TaskManagerApplicationTests.java +++ b/src/test/java/com/lucc/taskmanager/TaskManagerApplicationTests.java @@ -2,8 +2,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("test") class TaskManagerApplicationTests { @Test diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..9fe88ae --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,9 @@ +# Use H2 for tests +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password + +# Hibernate settings for H2 +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop From fa59b12f4b658af67c722d5bc764f4e45cbe6a4a Mon Sep 17 00:00:00 2001 From: lukasmatusiewicz Date: Wed, 8 Apr 2026 17:01:30 +0200 Subject: [PATCH 5/6] Update maven.yml --- .github/workflows/maven.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 939353d..20c420b 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -14,6 +14,9 @@ jobs: steps: - uses: actions/checkout@v4 + with: + # Use head_ref for PRs, and ref_name for pushes to stay on a branch + ref: ${{ github.head_ref || github.ref_name }} - name: Set up JDK 21 uses: actions/setup-java@v4 with: From e329f24dc3161e7047ba92ed143d8bebc211ea13 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:03:18 +0000 Subject: [PATCH 6/6] Update coverage badge --- .github/badges/branches.svg | 1 + .github/badges/jacoco.svg | 1 + 2 files changed, 2 insertions(+) create mode 100644 .github/badges/branches.svg create mode 100644 .github/badges/jacoco.svg diff --git a/.github/badges/branches.svg b/.github/badges/branches.svg new file mode 100644 index 0000000..fe9c87b --- /dev/null +++ b/.github/badges/branches.svg @@ -0,0 +1 @@ +branches31.2% \ No newline at end of file diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg new file mode 100644 index 0000000..bf86c61 --- /dev/null +++ b/.github/badges/jacoco.svg @@ -0,0 +1 @@ +coverage72.9% \ No newline at end of file