Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/badges/branches.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions .github/badges/jacoco.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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
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:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B package -Dspring.profiles.active=test --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'
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

---
Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,25 @@
<target>21</target>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
123 changes: 123 additions & 0 deletions src/test/java/com/lucc/taskmanager/service/TaskServiceTest.java
Original file line number Diff line number Diff line change
@@ -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<Task> 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);
}
}
85 changes: 85 additions & 0 deletions src/test/java/com/lucc/taskmanager/service/UserServiceTest.java
Original file line number Diff line number Diff line change
@@ -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<User> 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"));
}
}
9 changes: 9 additions & 0 deletions src/test/resources/application-test.properties
Original file line number Diff line number Diff line change
@@ -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