From e49ae5b049b18c0ed1e2fac4ac177f67d30971c7 Mon Sep 17 00:00:00 2001 From: egorsv21 Date: Thu, 25 Dec 2025 10:48:33 +0300 Subject: [PATCH 1/3] lab4 --- .gitignore | 3 +- build.gradle.kts | 4 +- .../exception/BugReportNotFoundException.java | 10 +++ .../exception/MilestoneNotFoundException.java | 10 +++ .../exception/ProjectNotFoundException.java | 10 +++ .../exception/TicketNotFoundException.java | 10 +++ src/main/java/org/lab/model/BugReport.java | 13 +++ .../java/org/lab/model/BugReportStatus.java | 9 +++ src/main/java/org/lab/model/Milestone.java | 17 ++++ .../java/org/lab/model/MilestoneStatus.java | 8 ++ src/main/java/org/lab/model/Project.java | 27 +++++++ src/main/java/org/lab/model/Ticket.java | 16 ++++ src/main/java/org/lab/model/TicketStatus.java | 9 +++ src/main/java/org/lab/model/User.java | 12 +++ .../lab/repository/BugReportRepository.java | 18 +++++ .../lab/repository/MilestoneRepository.java | 18 +++++ .../org/lab/repository/ProjectRepository.java | 18 +++++ .../org/lab/repository/TicketRepository.java | 18 +++++ .../org/lab/repository/UserRepository.java | 18 +++++ .../org/lab/service/BugReportService.java | 46 +++++++++++ .../org/lab/service/MilestoneService.java | 38 +++++++++ .../java/org/lab/service/ProjectService.java | 79 +++++++++++++++++++ .../java/org/lab/service/TicketService.java | 62 +++++++++++++++ .../java/org/lab/service/UserService.java | 25 ++++++ 24 files changed, 496 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/lab/exception/BugReportNotFoundException.java create mode 100644 src/main/java/org/lab/exception/MilestoneNotFoundException.java create mode 100644 src/main/java/org/lab/exception/ProjectNotFoundException.java create mode 100644 src/main/java/org/lab/exception/TicketNotFoundException.java create mode 100644 src/main/java/org/lab/model/BugReport.java create mode 100644 src/main/java/org/lab/model/BugReportStatus.java create mode 100644 src/main/java/org/lab/model/Milestone.java create mode 100644 src/main/java/org/lab/model/MilestoneStatus.java create mode 100644 src/main/java/org/lab/model/Project.java create mode 100644 src/main/java/org/lab/model/Ticket.java create mode 100644 src/main/java/org/lab/model/TicketStatus.java create mode 100644 src/main/java/org/lab/model/User.java create mode 100644 src/main/java/org/lab/repository/BugReportRepository.java create mode 100644 src/main/java/org/lab/repository/MilestoneRepository.java create mode 100644 src/main/java/org/lab/repository/ProjectRepository.java create mode 100644 src/main/java/org/lab/repository/TicketRepository.java create mode 100644 src/main/java/org/lab/repository/UserRepository.java create mode 100644 src/main/java/org/lab/service/BugReportService.java create mode 100644 src/main/java/org/lab/service/MilestoneService.java create mode 100644 src/main/java/org/lab/service/ProjectService.java create mode 100644 src/main/java/org/lab/service/TicketService.java create mode 100644 src/main/java/org/lab/service/UserService.java diff --git a/.gitignore b/.gitignore index b63da45..da20993 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/test/**/build/ ### IntelliJ IDEA ### +.idea .idea/modules.xml .idea/jarRepositories.xml .idea/compiler.xml @@ -39,4 +40,4 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store diff --git a/build.gradle.kts b/build.gradle.kts index 79bf52a..95a1b0e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("java") + id("io.freefair.lombok") version "9.1.0" } group = "org.lab" @@ -10,6 +11,7 @@ repositories { } dependencies { + implementation("org.jetbrains:annotations:26.0.2") testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") @@ -17,4 +19,4 @@ dependencies { tasks.test { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/src/main/java/org/lab/exception/BugReportNotFoundException.java b/src/main/java/org/lab/exception/BugReportNotFoundException.java new file mode 100644 index 0000000..0204a59 --- /dev/null +++ b/src/main/java/org/lab/exception/BugReportNotFoundException.java @@ -0,0 +1,10 @@ +package org.lab.exception; + +import java.util.UUID; + +public class BugReportNotFoundException extends RuntimeException { + public BugReportNotFoundException(UUID bugReportId) { + super("Bug report not found: " + bugReportId); + } +} + diff --git a/src/main/java/org/lab/exception/MilestoneNotFoundException.java b/src/main/java/org/lab/exception/MilestoneNotFoundException.java new file mode 100644 index 0000000..76919f0 --- /dev/null +++ b/src/main/java/org/lab/exception/MilestoneNotFoundException.java @@ -0,0 +1,10 @@ +package org.lab.exception; + +import java.util.UUID; + +public class MilestoneNotFoundException extends RuntimeException { + public MilestoneNotFoundException(UUID milestoneId) { + super("Milestone not found: " + milestoneId); + } +} + diff --git a/src/main/java/org/lab/exception/ProjectNotFoundException.java b/src/main/java/org/lab/exception/ProjectNotFoundException.java new file mode 100644 index 0000000..b7800a3 --- /dev/null +++ b/src/main/java/org/lab/exception/ProjectNotFoundException.java @@ -0,0 +1,10 @@ +package org.lab.exception; + +import java.util.UUID; + +public class ProjectNotFoundException extends RuntimeException { + public ProjectNotFoundException(UUID projectId) { + super("Project not found: " + projectId); + } +} + diff --git a/src/main/java/org/lab/exception/TicketNotFoundException.java b/src/main/java/org/lab/exception/TicketNotFoundException.java new file mode 100644 index 0000000..edebf3f --- /dev/null +++ b/src/main/java/org/lab/exception/TicketNotFoundException.java @@ -0,0 +1,10 @@ +package org.lab.exception; + +import java.util.UUID; + +public class TicketNotFoundException extends RuntimeException { + public TicketNotFoundException(UUID ticketId) { + super("Ticket not found: " + ticketId); + } +} + diff --git a/src/main/java/org/lab/model/BugReport.java b/src/main/java/org/lab/model/BugReport.java new file mode 100644 index 0000000..6eac7d9 --- /dev/null +++ b/src/main/java/org/lab/model/BugReport.java @@ -0,0 +1,13 @@ +package org.lab.model; + +import lombok.With; +import java.util.UUID; + +public record BugReport( + UUID id, + UUID projectId, + String description, + @With BugReportStatus status +) { +} + diff --git a/src/main/java/org/lab/model/BugReportStatus.java b/src/main/java/org/lab/model/BugReportStatus.java new file mode 100644 index 0000000..e8f03a3 --- /dev/null +++ b/src/main/java/org/lab/model/BugReportStatus.java @@ -0,0 +1,9 @@ +package org.lab.model; + +public enum BugReportStatus { + NEW, + FIXED, + TESTED, + CLOSED +} + diff --git a/src/main/java/org/lab/model/Milestone.java b/src/main/java/org/lab/model/Milestone.java new file mode 100644 index 0000000..00cdd09 --- /dev/null +++ b/src/main/java/org/lab/model/Milestone.java @@ -0,0 +1,17 @@ +package org.lab.model; + +import lombok.With; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +public record Milestone( + UUID id, + UUID projectId, + LocalDate startDate, + LocalDate endDate, + List ticketIds, + @With MilestoneStatus status +) { +} + diff --git a/src/main/java/org/lab/model/MilestoneStatus.java b/src/main/java/org/lab/model/MilestoneStatus.java new file mode 100644 index 0000000..8948a9a --- /dev/null +++ b/src/main/java/org/lab/model/MilestoneStatus.java @@ -0,0 +1,8 @@ +package org.lab.model; + +public enum MilestoneStatus { + OPENED, + ACTIVE, + CLOSED +} + diff --git a/src/main/java/org/lab/model/Project.java b/src/main/java/org/lab/model/Project.java new file mode 100644 index 0000000..1599aa0 --- /dev/null +++ b/src/main/java/org/lab/model/Project.java @@ -0,0 +1,27 @@ +package org.lab.model; + +import lombok.With; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.UUID; + +public record Project( + UUID id, + String title, + String description, + @With List developers, + @With List testers, + UUID manager, + @With @Nullable UUID teamLead, + List milestoneIds, + List bugReportIds +) { +} + +// AuthService (checkPermission(user, permission, projectId)) +// Enum Role ROLE1(name, list permissions), ... +// Enum Permission PERM1(name, description), ... +// AccessBinding (userId, projectId, role) +// AccessBindingService (create(userId, projectId, role), delete(userId, projectId, role)) + diff --git a/src/main/java/org/lab/model/Ticket.java b/src/main/java/org/lab/model/Ticket.java new file mode 100644 index 0000000..60627a2 --- /dev/null +++ b/src/main/java/org/lab/model/Ticket.java @@ -0,0 +1,16 @@ +package org.lab.model; + +import lombok.With; +import java.util.List; +import java.util.UUID; + +public record Ticket( + UUID id, + UUID projectId, + UUID milestoneId, + String description, + @With List assignedDevelopers, + @With TicketStatus status +) { +} + diff --git a/src/main/java/org/lab/model/TicketStatus.java b/src/main/java/org/lab/model/TicketStatus.java new file mode 100644 index 0000000..c69665c --- /dev/null +++ b/src/main/java/org/lab/model/TicketStatus.java @@ -0,0 +1,9 @@ +package org.lab.model; + +public enum TicketStatus { + NEW, + ACCEPTED, + IN_PROGRESS, + COMPLETED +} + diff --git a/src/main/java/org/lab/model/User.java b/src/main/java/org/lab/model/User.java new file mode 100644 index 0000000..54f4a15 --- /dev/null +++ b/src/main/java/org/lab/model/User.java @@ -0,0 +1,12 @@ +package org.lab.model; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record User( + UUID id, + String name, + LocalDateTime createdAt +) { +} + diff --git a/src/main/java/org/lab/repository/BugReportRepository.java b/src/main/java/org/lab/repository/BugReportRepository.java new file mode 100644 index 0000000..59edb3f --- /dev/null +++ b/src/main/java/org/lab/repository/BugReportRepository.java @@ -0,0 +1,18 @@ +package org.lab.repository; + +import org.lab.model.BugReport; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface BugReportRepository { + BugReport save(BugReport bugReport); + + Optional findById(UUID id); + + List findAll(); + + void deleteById(UUID id); +} + diff --git a/src/main/java/org/lab/repository/MilestoneRepository.java b/src/main/java/org/lab/repository/MilestoneRepository.java new file mode 100644 index 0000000..a9445e2 --- /dev/null +++ b/src/main/java/org/lab/repository/MilestoneRepository.java @@ -0,0 +1,18 @@ +package org.lab.repository; + +import org.lab.model.Milestone; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface MilestoneRepository { + Milestone save(Milestone milestone); + + Optional findById(UUID id); + + List findAll(); + + void deleteById(UUID id); +} + diff --git a/src/main/java/org/lab/repository/ProjectRepository.java b/src/main/java/org/lab/repository/ProjectRepository.java new file mode 100644 index 0000000..06c3a77 --- /dev/null +++ b/src/main/java/org/lab/repository/ProjectRepository.java @@ -0,0 +1,18 @@ +package org.lab.repository; + +import org.lab.model.Project; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface ProjectRepository { + Project save(Project project); + + Optional findById(UUID id); + + List findAll(); + + void deleteById(UUID id); +} + diff --git a/src/main/java/org/lab/repository/TicketRepository.java b/src/main/java/org/lab/repository/TicketRepository.java new file mode 100644 index 0000000..8f9e999 --- /dev/null +++ b/src/main/java/org/lab/repository/TicketRepository.java @@ -0,0 +1,18 @@ +package org.lab.repository; + +import org.lab.model.Ticket; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface TicketRepository { + Ticket save(Ticket ticket); + + Optional findById(UUID id); + + List findAll(); + + void deleteById(UUID id); +} + diff --git a/src/main/java/org/lab/repository/UserRepository.java b/src/main/java/org/lab/repository/UserRepository.java new file mode 100644 index 0000000..8b3cb73 --- /dev/null +++ b/src/main/java/org/lab/repository/UserRepository.java @@ -0,0 +1,18 @@ +package org.lab.repository; + +import org.lab.model.User; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface UserRepository { + User save(User user); + + Optional findById(UUID id); + + List findAll(); + + void deleteById(UUID id); +} + diff --git a/src/main/java/org/lab/service/BugReportService.java b/src/main/java/org/lab/service/BugReportService.java new file mode 100644 index 0000000..a33d143 --- /dev/null +++ b/src/main/java/org/lab/service/BugReportService.java @@ -0,0 +1,46 @@ +package org.lab.service; + +import org.lab.exception.BugReportNotFoundException; +import org.lab.model.BugReport; +import org.lab.model.BugReportStatus; +import org.lab.repository.BugReportRepository; + +import java.util.UUID; + +public class BugReportService { + private final BugReportRepository bugReportRepository; + + public BugReportService(BugReportRepository bugReportRepository) { + this.bugReportRepository = bugReportRepository; + } + + public BugReport create(UUID projectId, String description) { + BugReport bugReport = new BugReport( + UUID.randomUUID(), + projectId, + description, + BugReportStatus.NEW + ); + return bugReportRepository.save(bugReport); + } + + public void fix(UUID bugReportId) { + updateStatus(bugReportId, BugReportStatus.FIXED); + } + + public void test(UUID bugReportId) { + updateStatus(bugReportId, BugReportStatus.TESTED); + } + + public void close(UUID bugReportId) { + updateStatus(bugReportId, BugReportStatus.CLOSED); + } + + private void updateStatus(UUID bugReportId, BugReportStatus status) { + BugReport bugReport = bugReportRepository.findById(bugReportId) + .orElseThrow(() -> new BugReportNotFoundException(bugReportId)); + + bugReportRepository.save(bugReport.withStatus(status)); + } +} + diff --git a/src/main/java/org/lab/service/MilestoneService.java b/src/main/java/org/lab/service/MilestoneService.java new file mode 100644 index 0000000..f38749f --- /dev/null +++ b/src/main/java/org/lab/service/MilestoneService.java @@ -0,0 +1,38 @@ +package org.lab.service; + +import org.lab.exception.MilestoneNotFoundException; +import org.lab.model.Milestone; +import org.lab.model.MilestoneStatus; +import org.lab.repository.MilestoneRepository; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.UUID; + +public class MilestoneService { + private final MilestoneRepository milestoneRepository; + + public MilestoneService(MilestoneRepository milestoneRepository) { + this.milestoneRepository = milestoneRepository; + } + + public Milestone create(UUID projectId, LocalDate startDate, LocalDate endDate) { + Milestone milestone = new Milestone( + UUID.randomUUID(), + projectId, + startDate, + endDate, + new ArrayList<>(), + MilestoneStatus.OPENED + ); + return milestoneRepository.save(milestone); + } + + public void setStatus(UUID milestoneId, MilestoneStatus status) { + Milestone milestone = milestoneRepository.findById(milestoneId) + .orElseThrow(() -> new MilestoneNotFoundException(milestoneId)); + + milestoneRepository.save(milestone.withStatus(status)); + } +} + diff --git a/src/main/java/org/lab/service/ProjectService.java b/src/main/java/org/lab/service/ProjectService.java new file mode 100644 index 0000000..445b81e --- /dev/null +++ b/src/main/java/org/lab/service/ProjectService.java @@ -0,0 +1,79 @@ +package org.lab.service; + +import org.lab.exception.ProjectNotFoundException; +import org.lab.model.Project; +import org.lab.repository.ProjectRepository; + +import java.io.IO; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class ProjectService { + private final ProjectRepository projectRepository; + + public ProjectService(ProjectRepository projectRepository) { + this.projectRepository = projectRepository; + } + + public List list(UUID userId) { + return projectRepository.findAll().stream() + .filter(project -> project.manager().equals(userId) || + project.teamLead() != null && project.teamLead().equals(userId) || + project.developers().contains(userId) || + project.testers().contains(userId)) + .toList(); + } + + public Project create(UUID userId, String title, String description) { + Project project = new Project( + UUID.randomUUID(), + title, + description, + new ArrayList<>(), + new ArrayList<>(), + userId, + null, + new ArrayList<>(), + new ArrayList<>() + ); + return projectRepository.save(project); + } + + public void setTeamLead(UUID projectId, UUID teamLeadId) { + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> new ProjectNotFoundException(projectId)); + + projectRepository.save(project.withTeamLead(teamLeadId)); + } + + public void addDeveloper(UUID projectId, UUID developerId) { + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> new ProjectNotFoundException(projectId)); + + List developers = new ArrayList<>(project.developers()); + if (!developers.contains(developerId)) { + developers.add(developerId); + } + + projectRepository.save(project.withDevelopers(developers)); + } + + public void addTester(UUID projectId, UUID testerId) { + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> new ProjectNotFoundException(projectId)); + + List testers = new ArrayList<>(project.testers()); + if (!testers.contains(testerId)) { + testers.add(testerId); + } + projectRepository.save(project.withTesters(testers)); + } + + public void test(UUID projectId) { + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> new ProjectNotFoundException(projectId)); + IO.println("Testing project " + project.title() + "#" + project.id() + "..."); + } +} + diff --git a/src/main/java/org/lab/service/TicketService.java b/src/main/java/org/lab/service/TicketService.java new file mode 100644 index 0000000..752b794 --- /dev/null +++ b/src/main/java/org/lab/service/TicketService.java @@ -0,0 +1,62 @@ +package org.lab.service; + +import org.lab.exception.TicketNotFoundException; +import org.lab.model.Ticket; +import org.lab.model.TicketStatus; +import org.lab.repository.TicketRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class TicketService { + private final TicketRepository ticketRepository; + + public TicketService(TicketRepository ticketRepository) { + this.ticketRepository = ticketRepository; + } + + public Ticket create(UUID projectId, UUID milestoneId, String description) { + Ticket ticket = new Ticket( + UUID.randomUUID(), + projectId, + milestoneId, + description, + new ArrayList<>(), + TicketStatus.NEW + ); + return ticketRepository.save(ticket); + } + + public List listByUser(UUID userId) { + return ticketRepository.findAll().stream() + .filter(ticket -> ticket.assignedDevelopers().contains(userId)) + .toList(); + } + + public void assignDeveloper(UUID ticketId, UUID developerId) { + Ticket ticket = ticketRepository.findById(ticketId) + .orElseThrow(() -> new TicketNotFoundException(ticketId)); + + List developers = new ArrayList<>(ticket.assignedDevelopers()); + if (!developers.contains(developerId)) { + developers.add(developerId); + } + + ticketRepository.save(ticket.withAssignedDevelopers(developers)); + } + + public TicketStatus getStatus(UUID ticketId) { + Ticket ticket = ticketRepository.findById(ticketId) + .orElseThrow(() -> new TicketNotFoundException(ticketId)); + return ticket.status(); + } + + public void complete(UUID ticketId) { + Ticket ticket = ticketRepository.findById(ticketId) + .orElseThrow(() -> new TicketNotFoundException(ticketId)); + + ticketRepository.save(ticket.withStatus(TicketStatus.COMPLETED)); + } +} + diff --git a/src/main/java/org/lab/service/UserService.java b/src/main/java/org/lab/service/UserService.java new file mode 100644 index 0000000..3518a4e --- /dev/null +++ b/src/main/java/org/lab/service/UserService.java @@ -0,0 +1,25 @@ +package org.lab.service; + +import org.lab.model.User; +import org.lab.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.UUID; + +public class UserService { + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public User register(String name) { + User user = new User( + UUID.randomUUID(), + name, + LocalDateTime.now() + ); + return userRepository.save(user); + } +} + From d14687dc989a0ef6b9ee3ed2dc49f30ea5fe9751 Mon Sep 17 00:00:00 2001 From: egorsv21 Date: Thu, 25 Dec 2025 15:07:30 +0300 Subject: [PATCH 2/3] auth system + tests --- build.gradle.kts | 6 + gradlew | 0 .../java/org/lab/auth/AuthRepository.java | 18 ++ src/main/java/org/lab/auth/AuthService.java | 21 +++ .../java/org/lab/auth/AuthServiceImpl.java | 62 +++++++ .../org/lab/auth/AuthenticationContext.java | 21 +++ .../org/lab/auth/InMemoryAuthRepository.java | 40 +++++ .../lab/auth/PermissionDeniedException.java | 12 ++ .../org/lab/auth/model/AccessBinding.java | 11 ++ .../java/org/lab/auth/model/Permission.java | 38 ++++ src/main/java/org/lab/auth/model/Role.java | 54 ++++++ .../ActiveMilestoneExistsException.java | 10 ++ .../NotAllTicketsCompletedException.java | 10 ++ src/main/java/org/lab/model/Milestone.java | 2 +- src/main/java/org/lab/model/Project.java | 14 -- .../inmemory/InMemoryBugReportRepository.java | 37 ++++ .../inmemory/InMemoryMilestoneRepository.java | 37 ++++ .../inmemory/InMemoryProjectRepository.java | 37 ++++ .../inmemory/InMemoryTicketRepository.java | 37 ++++ .../inmemory/InMemoryUserRepository.java | 37 ++++ .../org/lab/service/BugReportService.java | 46 ++++- .../org/lab/service/MilestoneService.java | 41 ++++- .../java/org/lab/service/ProjectService.java | 72 ++++---- .../java/org/lab/service/TicketService.java | 15 +- src/test/java/org/lab/TestBase.java | 65 +++++++ .../java/org/lab/auth/AuthServiceTest.java | 82 +++++++++ .../org/lab/service/BugReportServiceTest.java | 112 ++++++++++++ .../org/lab/service/MilestoneServiceTest.java | 141 +++++++++++++++ .../org/lab/service/ProjectServiceTest.java | 143 +++++++++++++++ .../org/lab/service/RoleBasedAccessTest.java | 165 ++++++++++++++++++ .../org/lab/service/TicketServiceTest.java | 124 +++++++++++++ .../java/org/lab/service/UserServiceTest.java | 41 +++++ 32 files changed, 1495 insertions(+), 56 deletions(-) mode change 100644 => 100755 gradlew create mode 100644 src/main/java/org/lab/auth/AuthRepository.java create mode 100644 src/main/java/org/lab/auth/AuthService.java create mode 100644 src/main/java/org/lab/auth/AuthServiceImpl.java create mode 100644 src/main/java/org/lab/auth/AuthenticationContext.java create mode 100644 src/main/java/org/lab/auth/InMemoryAuthRepository.java create mode 100644 src/main/java/org/lab/auth/PermissionDeniedException.java create mode 100644 src/main/java/org/lab/auth/model/AccessBinding.java create mode 100644 src/main/java/org/lab/auth/model/Permission.java create mode 100644 src/main/java/org/lab/auth/model/Role.java create mode 100644 src/main/java/org/lab/exception/ActiveMilestoneExistsException.java create mode 100644 src/main/java/org/lab/exception/NotAllTicketsCompletedException.java create mode 100644 src/main/java/org/lab/repository/inmemory/InMemoryBugReportRepository.java create mode 100644 src/main/java/org/lab/repository/inmemory/InMemoryMilestoneRepository.java create mode 100644 src/main/java/org/lab/repository/inmemory/InMemoryProjectRepository.java create mode 100644 src/main/java/org/lab/repository/inmemory/InMemoryTicketRepository.java create mode 100644 src/main/java/org/lab/repository/inmemory/InMemoryUserRepository.java create mode 100644 src/test/java/org/lab/TestBase.java create mode 100644 src/test/java/org/lab/auth/AuthServiceTest.java create mode 100644 src/test/java/org/lab/service/BugReportServiceTest.java create mode 100644 src/test/java/org/lab/service/MilestoneServiceTest.java create mode 100644 src/test/java/org/lab/service/ProjectServiceTest.java create mode 100644 src/test/java/org/lab/service/RoleBasedAccessTest.java create mode 100644 src/test/java/org/lab/service/TicketServiceTest.java create mode 100644 src/test/java/org/lab/service/UserServiceTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 95a1b0e..9e43dd7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,12 @@ repositories { mavenCentral() } +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(25)) + } +} + dependencies { implementation("org.jetbrains:annotations:26.0.2") testImplementation(platform("org.junit:junit-bom:5.10.0")) diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/org/lab/auth/AuthRepository.java b/src/main/java/org/lab/auth/AuthRepository.java new file mode 100644 index 0000000..d3ef051 --- /dev/null +++ b/src/main/java/org/lab/auth/AuthRepository.java @@ -0,0 +1,18 @@ +package org.lab.auth; + +import org.lab.auth.model.AccessBinding; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface AuthRepository { + AccessBinding save(AccessBinding accessBinding); + + Optional findByUserIdAndProjectId(UUID userId, UUID projectId); + + List findAll(); + + void deleteByUserIdAndProjectId(UUID userId, UUID projectId); +} + diff --git a/src/main/java/org/lab/auth/AuthService.java b/src/main/java/org/lab/auth/AuthService.java new file mode 100644 index 0000000..da5f077 --- /dev/null +++ b/src/main/java/org/lab/auth/AuthService.java @@ -0,0 +1,21 @@ +package org.lab.auth; + +import org.lab.auth.model.AccessBinding; +import org.lab.auth.model.Permission; +import org.lab.auth.model.Role; + +import java.util.List; +import java.util.UUID; +import java.util.function.Supplier; + +public interface AuthService { + void checkPermission(UUID projectId, Permission permission); + + void addBinding(UUID userId, UUID projectId, Role role); + + void removeBinding(UUID userId, UUID projectId, Role role); + + List findAllByUserId(UUID userId); + + void removeAllByProjectIdAndRole(UUID projectId, Role role); +} diff --git a/src/main/java/org/lab/auth/AuthServiceImpl.java b/src/main/java/org/lab/auth/AuthServiceImpl.java new file mode 100644 index 0000000..266544b --- /dev/null +++ b/src/main/java/org/lab/auth/AuthServiceImpl.java @@ -0,0 +1,62 @@ +package org.lab.auth; + +import org.lab.auth.model.AccessBinding; +import org.lab.auth.model.Permission; +import org.lab.auth.model.Role; + +import java.util.List; +import java.util.UUID; +import java.util.function.Supplier; + +public class AuthServiceImpl implements AuthService { + private final AuthRepository authRepository; + + public AuthServiceImpl(AuthRepository authRepository) { + this.authRepository = authRepository; + } + + @Override + public void checkPermission(UUID projectId, Permission permission) { + UUID userId = AuthenticationContext.get(); + if (!hasPermission(userId, projectId, permission)) { + throw new PermissionDeniedException(userId, projectId, permission); + } + } + + private boolean hasPermission(UUID userId, UUID projectId, Permission permission) { + AccessBinding binding = authRepository.findByUserIdAndProjectId(userId, projectId) + .orElse(null); + + return binding != null && binding.role().getPermissions().contains(permission.getName()); + } + + @Override + public void addBinding(UUID userId, UUID projectId, Role role) { + authRepository.save(new AccessBinding(userId, projectId, role)); + } + + @Override + public void removeBinding(UUID userId, UUID projectId, Role role) { + AccessBinding binding = authRepository.findByUserIdAndProjectId(userId, projectId) + .orElse(null); + + if (binding != null && binding.role() == role) { + authRepository.deleteByUserIdAndProjectId(userId, projectId); + } + } + + @Override + public List findAllByUserId(UUID userId) { + return authRepository.findAll().stream() + .filter(binding -> binding.userId().equals(userId)) + .toList(); + } + + @Override + public void removeAllByProjectIdAndRole(UUID projectId, Role role) { + authRepository.findAll().stream() + .filter(binding -> binding.projectId().equals(projectId) && binding.role() == role) + .forEach(binding -> authRepository.deleteByUserIdAndProjectId(binding.userId(), binding.projectId())); + } +} + diff --git a/src/main/java/org/lab/auth/AuthenticationContext.java b/src/main/java/org/lab/auth/AuthenticationContext.java new file mode 100644 index 0000000..b8bc4ea --- /dev/null +++ b/src/main/java/org/lab/auth/AuthenticationContext.java @@ -0,0 +1,21 @@ +package org.lab.auth; + +import java.util.UUID; + +public class AuthenticationContext { + + private static final ThreadLocal USER_ID = new ThreadLocal<>(); + + public static UUID get() { + return USER_ID.get(); + } + + public static void set(UUID userId) { + USER_ID.set(userId); + } + + public static void clear() { + USER_ID.remove(); + } + +} diff --git a/src/main/java/org/lab/auth/InMemoryAuthRepository.java b/src/main/java/org/lab/auth/InMemoryAuthRepository.java new file mode 100644 index 0000000..77c5b74 --- /dev/null +++ b/src/main/java/org/lab/auth/InMemoryAuthRepository.java @@ -0,0 +1,40 @@ +package org.lab.auth; + +import org.lab.auth.model.AccessBinding; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class InMemoryAuthRepository implements AuthRepository { + private final Map storage = new ConcurrentHashMap<>(); + + private String key(UUID userId, UUID projectId) { + return userId + ":" + projectId; + } + + @Override + public AccessBinding save(AccessBinding accessBinding) { + storage.put(key(accessBinding.userId(), accessBinding.projectId()), accessBinding); + return accessBinding; + } + + @Override + public Optional findByUserIdAndProjectId(UUID userId, UUID projectId) { + return Optional.ofNullable(storage.get(key(userId, projectId))); + } + + @Override + public List findAll() { + return new ArrayList<>(storage.values()); + } + + @Override + public void deleteByUserIdAndProjectId(UUID userId, UUID projectId) { + storage.remove(key(userId, projectId)); + } +} + diff --git a/src/main/java/org/lab/auth/PermissionDeniedException.java b/src/main/java/org/lab/auth/PermissionDeniedException.java new file mode 100644 index 0000000..fdf2f8e --- /dev/null +++ b/src/main/java/org/lab/auth/PermissionDeniedException.java @@ -0,0 +1,12 @@ +package org.lab.auth; + +import org.lab.auth.model.Permission; + +import java.util.UUID; + +public class PermissionDeniedException extends RuntimeException { + public PermissionDeniedException(UUID userId, UUID projectId, Permission permission) { + super("Permission denied: userId=" + userId + ", projectId=" + projectId + ", permission=" + permission.getName()); + } +} + diff --git a/src/main/java/org/lab/auth/model/AccessBinding.java b/src/main/java/org/lab/auth/model/AccessBinding.java new file mode 100644 index 0000000..24f5f33 --- /dev/null +++ b/src/main/java/org/lab/auth/model/AccessBinding.java @@ -0,0 +1,11 @@ +package org.lab.auth.model; + +import java.util.UUID; + +public record AccessBinding( + UUID userId, + UUID projectId, + Role role +) { +} + diff --git a/src/main/java/org/lab/auth/model/Permission.java b/src/main/java/org/lab/auth/model/Permission.java new file mode 100644 index 0000000..8a49194 --- /dev/null +++ b/src/main/java/org/lab/auth/model/Permission.java @@ -0,0 +1,38 @@ +package org.lab.auth.model; + +public enum Permission { + PROJECT_SET_TEAM_LEAD("project.setTeamLead", "Set team lead for a project"), + PROJECT_ADD_DEVELOPER("project.addDeveloper", "Add developer to a project"), + PROJECT_ADD_TESTER("project.addTester", "Add tester to a project"), + PROJECT_TEST("project.test", "Test a project"), + + TICKET_CREATE("ticket.create", "Create a new ticket"), + TICKET_ASSIGN_DEVELOPER("ticket.assignDeveloper", "Assign developer to a ticket"), + TICKET_GET_STATUS("ticket.getStatus", "Get ticket status"), + TICKET_COMPLETE("ticket.complete", "Complete a ticket"), + + BUG_REPORT_CREATE("bugReport.create", "Create a new bug report"), + BUG_REPORT_FIX("bugReport.fix", "Mark bug report as fixed"), + BUG_REPORT_TEST("bugReport.test", "Mark bug report as tested"), + BUG_REPORT_CLOSE("bugReport.close", "Close a bug report"), + + MILESTONE_CREATE("milestone.create", "Create a new milestone"), + MILESTONE_SET_STATUS("milestone.setStatus", "Set milestone status"); + + private final String name; + private final String description; + + Permission(String name, String description) { + this.name = name; + this.description = description; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } +} + diff --git a/src/main/java/org/lab/auth/model/Role.java b/src/main/java/org/lab/auth/model/Role.java new file mode 100644 index 0000000..bb1ef4d --- /dev/null +++ b/src/main/java/org/lab/auth/model/Role.java @@ -0,0 +1,54 @@ +package org.lab.auth.model; + +import java.util.List; + +public enum Role { + MANAGER("manager", List.of( + Permission.PROJECT_SET_TEAM_LEAD.getName(), + Permission.PROJECT_ADD_DEVELOPER.getName(), + Permission.PROJECT_ADD_TESTER.getName(), + Permission.TICKET_CREATE.getName(), + Permission.TICKET_ASSIGN_DEVELOPER.getName(), + Permission.TICKET_GET_STATUS.getName(), + Permission.MILESTONE_CREATE.getName(), + Permission.MILESTONE_SET_STATUS.getName() + )), + + DEVELOPER("developer", List.of( + Permission.TICKET_COMPLETE.getName(), + Permission.BUG_REPORT_CREATE.getName(), + Permission.BUG_REPORT_FIX.getName(), + Permission.BUG_REPORT_CLOSE.getName() + )), + + TESTER("tester", List.of( + Permission.PROJECT_TEST.getName(), + Permission.BUG_REPORT_CREATE.getName(), + Permission.BUG_REPORT_TEST.getName(), + Permission.BUG_REPORT_CLOSE.getName() + )), + + TEAM_LEAD("teamLead", List.of( + Permission.TICKET_CREATE.getName(), + Permission.TICKET_ASSIGN_DEVELOPER.getName(), + Permission.TICKET_GET_STATUS.getName(), + Permission.TICKET_COMPLETE.getName() + )); + + private final String name; + private final List permissions; + + Role(String name, List permissions) { + this.name = name; + this.permissions = permissions; + } + + public String getName() { + return name; + } + + public List getPermissions() { + return permissions; + } +} + diff --git a/src/main/java/org/lab/exception/ActiveMilestoneExistsException.java b/src/main/java/org/lab/exception/ActiveMilestoneExistsException.java new file mode 100644 index 0000000..9b7bb2d --- /dev/null +++ b/src/main/java/org/lab/exception/ActiveMilestoneExistsException.java @@ -0,0 +1,10 @@ +package org.lab.exception; + +import java.util.UUID; + +public class ActiveMilestoneExistsException extends RuntimeException { + public ActiveMilestoneExistsException(UUID projectId) { + super("Project already has an active milestone: " + projectId); + } +} + diff --git a/src/main/java/org/lab/exception/NotAllTicketsCompletedException.java b/src/main/java/org/lab/exception/NotAllTicketsCompletedException.java new file mode 100644 index 0000000..d8b16ce --- /dev/null +++ b/src/main/java/org/lab/exception/NotAllTicketsCompletedException.java @@ -0,0 +1,10 @@ +package org.lab.exception; + +import java.util.UUID; + +public class NotAllTicketsCompletedException extends RuntimeException { + public NotAllTicketsCompletedException(UUID milestoneId) { + super("Cannot close milestone: not all tickets are completed: " + milestoneId); + } +} + diff --git a/src/main/java/org/lab/model/Milestone.java b/src/main/java/org/lab/model/Milestone.java index 00cdd09..de2001d 100644 --- a/src/main/java/org/lab/model/Milestone.java +++ b/src/main/java/org/lab/model/Milestone.java @@ -10,7 +10,7 @@ public record Milestone( UUID projectId, LocalDate startDate, LocalDate endDate, - List ticketIds, + @With List ticketIds, @With MilestoneStatus status ) { } diff --git a/src/main/java/org/lab/model/Project.java b/src/main/java/org/lab/model/Project.java index 1599aa0..3d0d95f 100644 --- a/src/main/java/org/lab/model/Project.java +++ b/src/main/java/org/lab/model/Project.java @@ -1,8 +1,5 @@ package org.lab.model; -import lombok.With; -import org.jetbrains.annotations.Nullable; - import java.util.List; import java.util.UUID; @@ -10,18 +7,7 @@ public record Project( UUID id, String title, String description, - @With List developers, - @With List testers, - UUID manager, - @With @Nullable UUID teamLead, List milestoneIds, List bugReportIds ) { } - -// AuthService (checkPermission(user, permission, projectId)) -// Enum Role ROLE1(name, list permissions), ... -// Enum Permission PERM1(name, description), ... -// AccessBinding (userId, projectId, role) -// AccessBindingService (create(userId, projectId, role), delete(userId, projectId, role)) - diff --git a/src/main/java/org/lab/repository/inmemory/InMemoryBugReportRepository.java b/src/main/java/org/lab/repository/inmemory/InMemoryBugReportRepository.java new file mode 100644 index 0000000..68b058a --- /dev/null +++ b/src/main/java/org/lab/repository/inmemory/InMemoryBugReportRepository.java @@ -0,0 +1,37 @@ +package org.lab.repository.inmemory; + +import org.lab.model.BugReport; +import org.lab.repository.BugReportRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class InMemoryBugReportRepository implements BugReportRepository { + private final Map storage = new ConcurrentHashMap<>(); + + @Override + public BugReport save(BugReport bugReport) { + storage.put(bugReport.id(), bugReport); + return bugReport; + } + + @Override + public Optional findById(UUID id) { + return Optional.ofNullable(storage.get(id)); + } + + @Override + public List findAll() { + return new ArrayList<>(storage.values()); + } + + @Override + public void deleteById(UUID id) { + storage.remove(id); + } +} + diff --git a/src/main/java/org/lab/repository/inmemory/InMemoryMilestoneRepository.java b/src/main/java/org/lab/repository/inmemory/InMemoryMilestoneRepository.java new file mode 100644 index 0000000..c0149e5 --- /dev/null +++ b/src/main/java/org/lab/repository/inmemory/InMemoryMilestoneRepository.java @@ -0,0 +1,37 @@ +package org.lab.repository.inmemory; + +import org.lab.model.Milestone; +import org.lab.repository.MilestoneRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class InMemoryMilestoneRepository implements MilestoneRepository { + private final Map storage = new ConcurrentHashMap<>(); + + @Override + public Milestone save(Milestone milestone) { + storage.put(milestone.id(), milestone); + return milestone; + } + + @Override + public Optional findById(UUID id) { + return Optional.ofNullable(storage.get(id)); + } + + @Override + public List findAll() { + return new ArrayList<>(storage.values()); + } + + @Override + public void deleteById(UUID id) { + storage.remove(id); + } +} + diff --git a/src/main/java/org/lab/repository/inmemory/InMemoryProjectRepository.java b/src/main/java/org/lab/repository/inmemory/InMemoryProjectRepository.java new file mode 100644 index 0000000..c3129fc --- /dev/null +++ b/src/main/java/org/lab/repository/inmemory/InMemoryProjectRepository.java @@ -0,0 +1,37 @@ +package org.lab.repository.inmemory; + +import org.lab.model.Project; +import org.lab.repository.ProjectRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class InMemoryProjectRepository implements ProjectRepository { + private final Map storage = new ConcurrentHashMap<>(); + + @Override + public Project save(Project project) { + storage.put(project.id(), project); + return project; + } + + @Override + public Optional findById(UUID id) { + return Optional.ofNullable(storage.get(id)); + } + + @Override + public List findAll() { + return new ArrayList<>(storage.values()); + } + + @Override + public void deleteById(UUID id) { + storage.remove(id); + } +} + diff --git a/src/main/java/org/lab/repository/inmemory/InMemoryTicketRepository.java b/src/main/java/org/lab/repository/inmemory/InMemoryTicketRepository.java new file mode 100644 index 0000000..1b7948c --- /dev/null +++ b/src/main/java/org/lab/repository/inmemory/InMemoryTicketRepository.java @@ -0,0 +1,37 @@ +package org.lab.repository.inmemory; + +import org.lab.model.Ticket; +import org.lab.repository.TicketRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class InMemoryTicketRepository implements TicketRepository { + private final Map storage = new ConcurrentHashMap<>(); + + @Override + public Ticket save(Ticket ticket) { + storage.put(ticket.id(), ticket); + return ticket; + } + + @Override + public Optional findById(UUID id) { + return Optional.ofNullable(storage.get(id)); + } + + @Override + public List findAll() { + return new ArrayList<>(storage.values()); + } + + @Override + public void deleteById(UUID id) { + storage.remove(id); + } +} + diff --git a/src/main/java/org/lab/repository/inmemory/InMemoryUserRepository.java b/src/main/java/org/lab/repository/inmemory/InMemoryUserRepository.java new file mode 100644 index 0000000..de34bf6 --- /dev/null +++ b/src/main/java/org/lab/repository/inmemory/InMemoryUserRepository.java @@ -0,0 +1,37 @@ +package org.lab.repository.inmemory; + +import org.lab.model.User; +import org.lab.repository.UserRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class InMemoryUserRepository implements UserRepository { + private final Map storage = new ConcurrentHashMap<>(); + + @Override + public User save(User user) { + storage.put(user.id(), user); + return user; + } + + @Override + public Optional findById(UUID id) { + return Optional.ofNullable(storage.get(id)); + } + + @Override + public List findAll() { + return new ArrayList<>(storage.values()); + } + + @Override + public void deleteById(UUID id) { + storage.remove(id); + } +} + diff --git a/src/main/java/org/lab/service/BugReportService.java b/src/main/java/org/lab/service/BugReportService.java index a33d143..f580f9a 100644 --- a/src/main/java/org/lab/service/BugReportService.java +++ b/src/main/java/org/lab/service/BugReportService.java @@ -1,20 +1,30 @@ package org.lab.service; +import org.lab.auth.AuthService; +import org.lab.auth.model.AccessBinding; +import org.lab.auth.model.Permission; import org.lab.exception.BugReportNotFoundException; import org.lab.model.BugReport; import org.lab.model.BugReportStatus; import org.lab.repository.BugReportRepository; +import java.util.List; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; public class BugReportService { private final BugReportRepository bugReportRepository; + private final AuthService authService; - public BugReportService(BugReportRepository bugReportRepository) { + public BugReportService(BugReportRepository bugReportRepository, AuthService authService) { this.bugReportRepository = bugReportRepository; + this.authService = authService; } public BugReport create(UUID projectId, String description) { + authService.checkPermission(projectId, Permission.BUG_REPORT_CREATE); + BugReport bugReport = new BugReport( UUID.randomUUID(), projectId, @@ -24,23 +34,41 @@ public BugReport create(UUID projectId, String description) { return bugReportRepository.save(bugReport); } + public List listByUser(UUID userId) { + Set userProjectIds = authService.findAllByUserId(userId).stream() + .map(AccessBinding::projectId) + .collect(Collectors.toSet()); + + return bugReportRepository.findAll().stream() + .filter(bugReport -> userProjectIds.contains(bugReport.projectId())) + .toList(); + } + public void fix(UUID bugReportId) { - updateStatus(bugReportId, BugReportStatus.FIXED); + BugReport bugReport = bugReportRepository.findById(bugReportId) + .orElseThrow(() -> new BugReportNotFoundException(bugReportId)); + + authService.checkPermission(bugReport.projectId(), Permission.BUG_REPORT_FIX); + + bugReportRepository.save(bugReport.withStatus(BugReportStatus.FIXED)); } public void test(UUID bugReportId) { - updateStatus(bugReportId, BugReportStatus.TESTED); + BugReport bugReport = bugReportRepository.findById(bugReportId) + .orElseThrow(() -> new BugReportNotFoundException(bugReportId)); + + authService.checkPermission(bugReport.projectId(), Permission.BUG_REPORT_TEST); + + bugReportRepository.save(bugReport.withStatus(BugReportStatus.TESTED)); } public void close(UUID bugReportId) { - updateStatus(bugReportId, BugReportStatus.CLOSED); - } - - private void updateStatus(UUID bugReportId, BugReportStatus status) { BugReport bugReport = bugReportRepository.findById(bugReportId) .orElseThrow(() -> new BugReportNotFoundException(bugReportId)); - - bugReportRepository.save(bugReport.withStatus(status)); + + authService.checkPermission(bugReport.projectId(), Permission.BUG_REPORT_CLOSE); + + bugReportRepository.save(bugReport.withStatus(BugReportStatus.CLOSED)); } } diff --git a/src/main/java/org/lab/service/MilestoneService.java b/src/main/java/org/lab/service/MilestoneService.java index f38749f..fa64cf4 100644 --- a/src/main/java/org/lab/service/MilestoneService.java +++ b/src/main/java/org/lab/service/MilestoneService.java @@ -1,22 +1,36 @@ package org.lab.service; +import org.lab.auth.AuthService; +import org.lab.auth.model.Permission; +import org.lab.exception.ActiveMilestoneExistsException; import org.lab.exception.MilestoneNotFoundException; +import org.lab.exception.NotAllTicketsCompletedException; import org.lab.model.Milestone; import org.lab.model.MilestoneStatus; +import org.lab.model.Ticket; +import org.lab.model.TicketStatus; import org.lab.repository.MilestoneRepository; +import org.lab.repository.TicketRepository; import java.time.LocalDate; import java.util.ArrayList; +import java.util.Optional; import java.util.UUID; public class MilestoneService { private final MilestoneRepository milestoneRepository; + private final TicketRepository ticketRepository; + private final AuthService authService; - public MilestoneService(MilestoneRepository milestoneRepository) { + public MilestoneService(MilestoneRepository milestoneRepository, TicketRepository ticketRepository, AuthService authService) { this.milestoneRepository = milestoneRepository; + this.ticketRepository = ticketRepository; + this.authService = authService; } public Milestone create(UUID projectId, LocalDate startDate, LocalDate endDate) { + authService.checkPermission(projectId, Permission.MILESTONE_CREATE); + Milestone milestone = new Milestone( UUID.randomUUID(), projectId, @@ -31,6 +45,31 @@ public Milestone create(UUID projectId, LocalDate startDate, LocalDate endDate) public void setStatus(UUID milestoneId, MilestoneStatus status) { Milestone milestone = milestoneRepository.findById(milestoneId) .orElseThrow(() -> new MilestoneNotFoundException(milestoneId)); + + authService.checkPermission(milestone.projectId(), Permission.MILESTONE_SET_STATUS); + + if (status == MilestoneStatus.CLOSED) { + boolean allTicketsCompleted = milestone.ticketIds().stream() + .map(ticketRepository::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .allMatch(ticket -> ticket.status() == TicketStatus.COMPLETED); + + if (!allTicketsCompleted) { + throw new NotAllTicketsCompletedException(milestoneId); + } + } + + if (status == MilestoneStatus.ACTIVE) { + boolean hasActiveMilestone = milestoneRepository.findAll().stream() + .anyMatch(m -> m.projectId().equals(milestone.projectId()) + && m.status() == MilestoneStatus.ACTIVE + && !m.id().equals(milestoneId)); + + if (hasActiveMilestone) { + throw new ActiveMilestoneExistsException(milestone.projectId()); + } + } milestoneRepository.save(milestone.withStatus(status)); } diff --git a/src/main/java/org/lab/service/ProjectService.java b/src/main/java/org/lab/service/ProjectService.java index 445b81e..b8c73b3 100644 --- a/src/main/java/org/lab/service/ProjectService.java +++ b/src/main/java/org/lab/service/ProjectService.java @@ -1,28 +1,41 @@ package org.lab.service; +import org.lab.auth.AuthService; +import org.lab.auth.model.AccessBinding; +import org.lab.auth.model.Permission; +import org.lab.auth.model.Role; import org.lab.exception.ProjectNotFoundException; import org.lab.model.Project; import org.lab.repository.ProjectRepository; -import java.io.IO; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; +/** + * У человека может быть только одна роль в проекте. + * У проекта может быть только один teamLead. + * У проекта может быть только один manager, определяется создателем. + */ public class ProjectService { private final ProjectRepository projectRepository; + private final AuthService authService; - public ProjectService(ProjectRepository projectRepository) { + public ProjectService(ProjectRepository projectRepository, AuthService authService) { this.projectRepository = projectRepository; + this.authService = authService; } public List list(UUID userId) { + Set userProjectIds = authService.findAllByUserId(userId).stream() + .map(AccessBinding::projectId) + .collect(Collectors.toSet()); + return projectRepository.findAll().stream() - .filter(project -> project.manager().equals(userId) || - project.teamLead() != null && project.teamLead().equals(userId) || - project.developers().contains(userId) || - project.testers().contains(userId)) - .toList(); + .filter(project -> userProjectIds.contains(project.id())) + .toList(); } public Project create(UUID userId, String title, String description) { @@ -31,48 +44,47 @@ public Project create(UUID userId, String title, String description) { title, description, new ArrayList<>(), - new ArrayList<>(), - userId, - null, - new ArrayList<>(), new ArrayList<>() ); - return projectRepository.save(project); + Project savedProject = projectRepository.save(project); + authService.addBinding(userId, savedProject.id(), Role.MANAGER); + return savedProject; } public void setTeamLead(UUID projectId, UUID teamLeadId) { - Project project = projectRepository.findById(projectId) + projectRepository.findById(projectId) .orElseThrow(() -> new ProjectNotFoundException(projectId)); - - projectRepository.save(project.withTeamLead(teamLeadId)); + + authService.checkPermission(projectId, Permission.PROJECT_SET_TEAM_LEAD); + + authService.removeAllByProjectIdAndRole(projectId, Role.TEAM_LEAD); + authService.addBinding(teamLeadId, projectId, Role.TEAM_LEAD); } public void addDeveloper(UUID projectId, UUID developerId) { - Project project = projectRepository.findById(projectId) + projectRepository.findById(projectId) .orElseThrow(() -> new ProjectNotFoundException(projectId)); - - List developers = new ArrayList<>(project.developers()); - if (!developers.contains(developerId)) { - developers.add(developerId); - } - - projectRepository.save(project.withDevelopers(developers)); + + authService.checkPermission(projectId, Permission.PROJECT_ADD_DEVELOPER); + + authService.addBinding(developerId, projectId, Role.DEVELOPER); } public void addTester(UUID projectId, UUID testerId) { - Project project = projectRepository.findById(projectId) + projectRepository.findById(projectId) .orElseThrow(() -> new ProjectNotFoundException(projectId)); - - List testers = new ArrayList<>(project.testers()); - if (!testers.contains(testerId)) { - testers.add(testerId); - } - projectRepository.save(project.withTesters(testers)); + + authService.checkPermission(projectId, Permission.PROJECT_ADD_TESTER); + + authService.addBinding(testerId, projectId, Role.TESTER); } public void test(UUID projectId) { Project project = projectRepository.findById(projectId) .orElseThrow(() -> new ProjectNotFoundException(projectId)); + + authService.checkPermission(projectId, Permission.PROJECT_TEST); + IO.println("Testing project " + project.title() + "#" + project.id() + "..."); } } diff --git a/src/main/java/org/lab/service/TicketService.java b/src/main/java/org/lab/service/TicketService.java index 752b794..b000780 100644 --- a/src/main/java/org/lab/service/TicketService.java +++ b/src/main/java/org/lab/service/TicketService.java @@ -1,5 +1,7 @@ package org.lab.service; +import org.lab.auth.AuthService; +import org.lab.auth.model.Permission; import org.lab.exception.TicketNotFoundException; import org.lab.model.Ticket; import org.lab.model.TicketStatus; @@ -11,12 +13,16 @@ public class TicketService { private final TicketRepository ticketRepository; + private final AuthService authService; - public TicketService(TicketRepository ticketRepository) { + public TicketService(TicketRepository ticketRepository, AuthService authService) { this.ticketRepository = ticketRepository; + this.authService = authService; } public Ticket create(UUID projectId, UUID milestoneId, String description) { + authService.checkPermission(projectId, Permission.TICKET_CREATE); + Ticket ticket = new Ticket( UUID.randomUUID(), projectId, @@ -37,6 +43,8 @@ public List listByUser(UUID userId) { public void assignDeveloper(UUID ticketId, UUID developerId) { Ticket ticket = ticketRepository.findById(ticketId) .orElseThrow(() -> new TicketNotFoundException(ticketId)); + + authService.checkPermission(ticket.projectId(), Permission.TICKET_ASSIGN_DEVELOPER); List developers = new ArrayList<>(ticket.assignedDevelopers()); if (!developers.contains(developerId)) { @@ -49,12 +57,17 @@ public void assignDeveloper(UUID ticketId, UUID developerId) { public TicketStatus getStatus(UUID ticketId) { Ticket ticket = ticketRepository.findById(ticketId) .orElseThrow(() -> new TicketNotFoundException(ticketId)); + + authService.checkPermission(ticket.projectId(), Permission.TICKET_GET_STATUS); + return ticket.status(); } public void complete(UUID ticketId) { Ticket ticket = ticketRepository.findById(ticketId) .orElseThrow(() -> new TicketNotFoundException(ticketId)); + + authService.checkPermission(ticket.projectId(), Permission.TICKET_COMPLETE); ticketRepository.save(ticket.withStatus(TicketStatus.COMPLETED)); } diff --git a/src/test/java/org/lab/TestBase.java b/src/test/java/org/lab/TestBase.java new file mode 100644 index 0000000..0f78aee --- /dev/null +++ b/src/test/java/org/lab/TestBase.java @@ -0,0 +1,65 @@ +package org.lab; + +import org.junit.jupiter.api.BeforeEach; +import org.lab.auth.AuthRepository; +import org.lab.auth.AuthService; +import org.lab.auth.AuthServiceImpl; +import org.lab.auth.AuthenticationContext; +import org.lab.auth.InMemoryAuthRepository; +import org.lab.repository.*; +import org.lab.repository.inmemory.*; +import org.lab.service.*; + +import java.util.UUID; + +public abstract class TestBase { + protected UserRepository userRepository; + protected ProjectRepository projectRepository; + protected TicketRepository ticketRepository; + protected MilestoneRepository milestoneRepository; + protected BugReportRepository bugReportRepository; + protected AuthRepository authRepository; + + protected UserService userService; + protected ProjectService projectService; + protected TicketService ticketService; + protected MilestoneService milestoneService; + protected BugReportService bugReportService; + protected AuthService authService; + + protected UUID managerId; + protected UUID teamLeadId; + protected UUID developerId; + protected UUID testerId; + + @BeforeEach + protected void baseSetUp() { + userRepository = new InMemoryUserRepository(); + projectRepository = new InMemoryProjectRepository(); + ticketRepository = new InMemoryTicketRepository(); + milestoneRepository = new InMemoryMilestoneRepository(); + bugReportRepository = new InMemoryBugReportRepository(); + authRepository = new InMemoryAuthRepository(); + + authService = new AuthServiceImpl(authRepository); + userService = new UserService(userRepository); + projectService = new ProjectService(projectRepository, authService); + ticketService = new TicketService(ticketRepository, authService); + milestoneService = new MilestoneService(milestoneRepository, ticketRepository, authService); + bugReportService = new BugReportService(bugReportRepository, authService); + + managerId = userService.register("Manager").id(); + teamLeadId = userService.register("TeamLead").id(); + developerId = userService.register("Developer").id(); + testerId = userService.register("Tester").id(); + } + + protected void setCurrentUser(UUID userId) { + AuthenticationContext.set(userId); + } + + protected void clearCurrentUser() { + AuthenticationContext.clear(); + } +} + diff --git a/src/test/java/org/lab/auth/AuthServiceTest.java b/src/test/java/org/lab/auth/AuthServiceTest.java new file mode 100644 index 0000000..3c75c3c --- /dev/null +++ b/src/test/java/org/lab/auth/AuthServiceTest.java @@ -0,0 +1,82 @@ +package org.lab.auth; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.lab.TestBase; +import org.lab.auth.PermissionDeniedException; +import org.lab.auth.model.Permission; +import org.lab.auth.model.Role; +import org.lab.model.Project; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class AuthServiceTest extends TestBase { + + private UUID projectId; + + @BeforeEach + void setUp() { + setCurrentUser(managerId); + Project project = projectService.create(managerId, "Project", "Desc"); + projectId = project.id(); + } + + @Test + void testAddBinding() { + authService.addBinding(developerId, projectId, Role.DEVELOPER); + + var binding = authRepository.findByUserIdAndProjectId(developerId, projectId); + assertTrue(binding.isPresent()); + assertEquals(Role.DEVELOPER, binding.get().role()); + } + + @Test + void testRemoveBinding() { + authService.addBinding(developerId, projectId, Role.DEVELOPER); + authService.removeBinding(developerId, projectId, Role.DEVELOPER); + + var binding = authRepository.findByUserIdAndProjectId(developerId, projectId); + assertFalse(binding.isPresent()); + } + + @Test + void testRemoveBindingOnlyRemovesCorrectRole() { + authService.addBinding(developerId, projectId, Role.DEVELOPER); + authService.removeBinding(developerId, projectId, Role.TESTER); + + var binding = authRepository.findByUserIdAndProjectId(developerId, projectId); + assertTrue(binding.isPresent()); + } + + @Test + void testCheckPermissionSuccess() { + setCurrentUser(managerId); + authService.addBinding(managerId, projectId, Role.MANAGER); + + assertDoesNotThrow(() -> + authService.checkPermission(projectId, Permission.PROJECT_SET_TEAM_LEAD)); + } + + @Test + void testCheckPermissionDenied() { + setCurrentUser(developerId); + authService.addBinding(developerId, projectId, Role.DEVELOPER); + + assertThrows(PermissionDeniedException.class, + () -> authService.checkPermission(projectId, Permission.PROJECT_SET_TEAM_LEAD)); + } + + @Test + void testFindAllByUserId() { + UUID projectId2 = projectService.create(managerId, "Project 2", "Desc").id(); + + authService.addBinding(developerId, projectId, Role.DEVELOPER); + authService.addBinding(developerId, projectId2, Role.DEVELOPER); + + var bindings = authService.findAllByUserId(developerId); + assertEquals(2, bindings.size()); + } +} + diff --git a/src/test/java/org/lab/service/BugReportServiceTest.java b/src/test/java/org/lab/service/BugReportServiceTest.java new file mode 100644 index 0000000..fe1a9ef --- /dev/null +++ b/src/test/java/org/lab/service/BugReportServiceTest.java @@ -0,0 +1,112 @@ +package org.lab.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.lab.TestBase; +import org.lab.auth.model.Role; +import org.lab.exception.BugReportNotFoundException; +import org.lab.model.BugReport; +import org.lab.model.BugReportStatus; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class BugReportServiceTest extends TestBase { + + private UUID projectId; + + @BeforeEach + void setUp() { + setCurrentUser(managerId); + var project = projectService.create(managerId, "Project", "Desc"); + projectId = project.id(); + } + + @Test + void testCreateBugReport() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + setCurrentUser(developerId); + + BugReport bugReport = bugReportService.create(projectId, "Bug description"); + + assertNotNull(bugReport); + assertNotNull(bugReport.id()); + assertEquals(projectId, bugReport.projectId()); + assertEquals("Bug description", bugReport.description()); + assertEquals(BugReportStatus.NEW, bugReport.status()); + } + + @Test + void testFixBugReport() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + setCurrentUser(developerId); + + BugReport bugReport = bugReportService.create(projectId, "Bug"); + bugReportService.fix(bugReport.id()); + + BugReport fixed = bugReportRepository.findById(bugReport.id()).orElseThrow(); + assertEquals(BugReportStatus.FIXED, fixed.status()); + } + + @Test + void testTestBugReport() { + setCurrentUser(managerId); + projectService.addTester(projectId, testerId); + projectService.addDeveloper(projectId, developerId); + setCurrentUser(developerId); + BugReport bugReport = bugReportService.create(projectId, "Bug"); + bugReportService.fix(bugReport.id()); + setCurrentUser(testerId); + bugReportService.test(bugReport.id()); + + BugReport tested = bugReportRepository.findById(bugReport.id()).orElseThrow(); + assertEquals(BugReportStatus.TESTED, tested.status()); + } + + @Test + void testCloseBugReport() { + setCurrentUser(managerId); + projectService.addTester(projectId, testerId); + projectService.addDeveloper(projectId, developerId); + setCurrentUser(developerId); + BugReport bugReport = bugReportService.create(projectId, "Bug"); + bugReportService.fix(bugReport.id()); + setCurrentUser(testerId); + bugReportService.test(bugReport.id()); + bugReportService.close(bugReport.id()); + + BugReport closed = bugReportRepository.findById(bugReport.id()).orElseThrow(); + assertEquals(BugReportStatus.CLOSED, closed.status()); + } + + @Test + void testListByUser() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + setCurrentUser(developerId); + + BugReport bugReport1 = bugReportService.create(projectId, "Bug 1"); + BugReport bugReport2 = bugReportService.create(projectId, "Bug 2"); + + List userBugs = bugReportService.listByUser(developerId); + assertTrue(userBugs.size() >= 2); + assertTrue(userBugs.stream().anyMatch(b -> b.id().equals(bugReport1.id()))); + assertTrue(userBugs.stream().anyMatch(b -> b.id().equals(bugReport2.id()))); + } + + @Test + void testBugReportNotFound() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + setCurrentUser(developerId); + UUID nonExistentId = UUID.randomUUID(); + + assertThrows(BugReportNotFoundException.class, + () -> bugReportService.fix(nonExistentId)); + } +} + diff --git a/src/test/java/org/lab/service/MilestoneServiceTest.java b/src/test/java/org/lab/service/MilestoneServiceTest.java new file mode 100644 index 0000000..8754dbe --- /dev/null +++ b/src/test/java/org/lab/service/MilestoneServiceTest.java @@ -0,0 +1,141 @@ +package org.lab.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.lab.TestBase; +import org.lab.auth.model.Role; +import org.lab.exception.ActiveMilestoneExistsException; +import org.lab.exception.MilestoneNotFoundException; +import org.lab.exception.NotAllTicketsCompletedException; +import org.lab.model.Milestone; +import org.lab.model.MilestoneStatus; +import org.lab.model.Ticket; +import org.lab.model.TicketStatus; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class MilestoneServiceTest extends TestBase { + + private UUID projectId; + private UUID milestoneId; + + @BeforeEach + void setUp() { + setCurrentUser(managerId); + var project = projectService.create(managerId, "Project", "Desc"); + projectId = project.id(); + } + + @Test + void testCreateMilestone() { + setCurrentUser(managerId); + Milestone milestone = milestoneService.create( + projectId, + LocalDate.now(), + LocalDate.now().plusDays(30) + ); + + assertNotNull(milestone); + assertNotNull(milestone.id()); + assertEquals(projectId, milestone.projectId()); + assertEquals(MilestoneStatus.OPENED, milestone.status()); + } + + @Test + void testSetStatusToActive() { + setCurrentUser(managerId); + Milestone milestone = milestoneService.create( + projectId, + LocalDate.now(), + LocalDate.now().plusDays(30) + ); + + milestoneService.setStatus(milestone.id(), MilestoneStatus.ACTIVE); + + Milestone updated = milestoneRepository.findById(milestone.id()).orElseThrow(); + assertEquals(MilestoneStatus.ACTIVE, updated.status()); + } + + @Test + void testOnlyOneActiveMilestonePerProject() { + setCurrentUser(managerId); + Milestone milestone1 = milestoneService.create( + projectId, + LocalDate.now(), + LocalDate.now().plusDays(30) + ); + milestoneService.setStatus(milestone1.id(), MilestoneStatus.ACTIVE); + + Milestone milestone2 = milestoneService.create( + projectId, + LocalDate.now().plusDays(31), + LocalDate.now().plusDays(60) + ); + + assertThrows(ActiveMilestoneExistsException.class, + () -> milestoneService.setStatus(milestone2.id(), MilestoneStatus.ACTIVE)); + } + + @Test + void testCannotCloseMilestoneWithIncompleteTickets() { + setCurrentUser(managerId); + Milestone milestone = milestoneService.create( + projectId, + LocalDate.now(), + LocalDate.now().plusDays(30) + ); + + Ticket ticket = ticketService.create(projectId, milestone.id(), "Test ticket"); + var updatedMilestone = milestoneRepository.findById(milestone.id()).orElseThrow(); + var milestoneWithTicket = updatedMilestone.withTicketIds( + List.of(ticket.id()) + ); + milestoneRepository.save(milestoneWithTicket); + + assertThrows(NotAllTicketsCompletedException.class, + () -> milestoneService.setStatus(milestone.id(), MilestoneStatus.CLOSED)); + } + + @Test + void testCanCloseMilestoneWhenAllTicketsCompleted() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + Milestone milestone = milestoneService.create( + projectId, + LocalDate.now(), + LocalDate.now().plusDays(30) + ); + + Ticket ticket = ticketService.create(projectId, milestone.id(), "Test ticket"); + ticketService.assignDeveloper(ticket.id(), developerId); + setCurrentUser(developerId); + ticketService.complete(ticket.id()); + + var updatedMilestone = milestoneRepository.findById(milestone.id()).orElseThrow(); + var milestoneWithTicket = updatedMilestone.withTicketIds( + List.of(ticket.id()) + ); + milestoneRepository.save(milestoneWithTicket); + + setCurrentUser(managerId); + assertDoesNotThrow(() -> + milestoneService.setStatus(milestone.id(), MilestoneStatus.CLOSED)); + + Milestone closed = milestoneRepository.findById(milestone.id()).orElseThrow(); + assertEquals(MilestoneStatus.CLOSED, closed.status()); + } + + @Test + void testMilestoneNotFound() { + setCurrentUser(managerId); + UUID nonExistentId = UUID.randomUUID(); + + assertThrows(MilestoneNotFoundException.class, + () -> milestoneService.setStatus(nonExistentId, MilestoneStatus.ACTIVE)); + } +} + diff --git a/src/test/java/org/lab/service/ProjectServiceTest.java b/src/test/java/org/lab/service/ProjectServiceTest.java new file mode 100644 index 0000000..1562bee --- /dev/null +++ b/src/test/java/org/lab/service/ProjectServiceTest.java @@ -0,0 +1,143 @@ +package org.lab.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.lab.TestBase; +import org.lab.auth.model.Role; +import org.lab.exception.ProjectNotFoundException; +import org.lab.model.Project; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class ProjectServiceTest extends TestBase { + + private UUID projectId; + + @BeforeEach + void setUp() { + setCurrentUser(managerId); + Project project = projectService.create(managerId, "Test Project", "Description"); + projectId = project.id(); + } + + @Test + void testCreateProject() { + setCurrentUser(managerId); + Project project = projectService.create(managerId, "New Project", "New Description"); + + assertNotNull(project); + assertNotNull(project.id()); + assertEquals("New Project", project.title()); + assertEquals("New Description", project.description()); + } + + @Test + void testCreateProjectSetsManagerRole() { + setCurrentUser(managerId); + Project project = projectService.create(managerId, "Project", "Desc"); + + var binding = authRepository.findByUserIdAndProjectId(managerId, project.id()); + assertTrue(binding.isPresent()); + assertEquals(Role.MANAGER, binding.get().role()); + } + + @Test + void testListProjectsForUser() { + UUID testUserId = userService.register("Test User").id(); + + setCurrentUser(testUserId); + Project managerProject = projectService.create(testUserId, "Manager Project", "Desc"); + + setCurrentUser(managerId); + Project teamLeadProject = projectService.create(managerId, "TeamLead Project", "Desc"); + projectService.setTeamLead(teamLeadProject.id(), testUserId); + + Project developerProject = projectService.create(managerId, "Developer Project", "Desc"); + projectService.addDeveloper(developerProject.id(), testUserId); + + Project testerProject = projectService.create(managerId, "Tester Project", "Desc"); + projectService.addTester(testerProject.id(), testUserId); + + UUID otherUserId = userService.register("Other User").id(); + setCurrentUser(otherUserId); + Project otherProject = projectService.create(otherUserId, "Other Project 1", "Desc"); + + List userProjects = projectService.list(testUserId); + + assertEquals(4, userProjects.size()); + assertTrue(userProjects.stream().anyMatch(p -> p.id().equals(managerProject.id()))); + assertTrue(userProjects.stream().anyMatch(p -> p.id().equals(teamLeadProject.id()))); + assertTrue(userProjects.stream().anyMatch(p -> p.id().equals(developerProject.id()))); + assertTrue(userProjects.stream().anyMatch(p -> p.id().equals(testerProject.id()))); + + assertFalse(userProjects.stream().anyMatch(p -> p.id().equals(otherProject.id()))); + } + + @Test + void testSetTeamLead() { + setCurrentUser(managerId); + projectService.setTeamLead(projectId, teamLeadId); + + var binding = authRepository.findByUserIdAndProjectId(teamLeadId, projectId); + assertTrue(binding.isPresent()); + assertEquals(Role.TEAM_LEAD, binding.get().role()); + } + + @Test + void testSetTeamLeadRemovesPreviousTeamLead() { + setCurrentUser(managerId); + UUID previousTeamLead = userService.register("Previous TeamLead").id(); + projectService.setTeamLead(projectId, previousTeamLead); + + projectService.setTeamLead(projectId, teamLeadId); + + var previousBinding = authRepository.findByUserIdAndProjectId(previousTeamLead, projectId); + var newBinding = authRepository.findByUserIdAndProjectId(teamLeadId, projectId); + + assertFalse(previousBinding.isPresent()); + assertTrue(newBinding.isPresent()); + assertEquals(Role.TEAM_LEAD, newBinding.get().role()); + } + + @Test + void testAddDeveloper() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + + var binding = authRepository.findByUserIdAndProjectId(developerId, projectId); + assertTrue(binding.isPresent()); + assertEquals(Role.DEVELOPER, binding.get().role()); + } + + @Test + void testAddTester() { + setCurrentUser(managerId); + projectService.addTester(projectId, testerId); + + var binding = authRepository.findByUserIdAndProjectId(testerId, projectId); + assertTrue(binding.isPresent()); + assertEquals(Role.TESTER, binding.get().role()); + } + + @Test + void testTestProject() { + setCurrentUser(managerId); + projectService.addTester(projectId, testerId); + setCurrentUser(testerId); + + assertDoesNotThrow(() -> projectService.test(projectId)); + } + + @Test + void testProjectNotFound() { + setCurrentUser(managerId); + UUID nonExistentId = UUID.randomUUID(); + + assertThrows(ProjectNotFoundException.class, + () -> projectService.setTeamLead(nonExistentId, teamLeadId)); + } +} + diff --git a/src/test/java/org/lab/service/RoleBasedAccessTest.java b/src/test/java/org/lab/service/RoleBasedAccessTest.java new file mode 100644 index 0000000..f292336 --- /dev/null +++ b/src/test/java/org/lab/service/RoleBasedAccessTest.java @@ -0,0 +1,165 @@ +package org.lab.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.lab.TestBase; +import org.lab.auth.PermissionDeniedException; +import org.lab.auth.model.Permission; +import org.lab.auth.model.Role; +import org.lab.model.Project; + +import java.time.LocalDate; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class RoleBasedAccessTest extends TestBase { + + private UUID projectId; + private UUID milestoneId; + + @BeforeEach + void setUp() { + setCurrentUser(managerId); + Project project = projectService.create(managerId, "Project", "Desc"); + projectId = project.id(); + + var milestone = milestoneService.create( + projectId, + LocalDate.now(), + LocalDate.now().plusDays(30) + ); + milestoneId = milestone.id(); + } + + @Test + void testManagerCanSetTeamLead() { + setCurrentUser(managerId); + assertDoesNotThrow(() -> + projectService.setTeamLead(projectId, teamLeadId)); + } + + @Test + void testManagerCanAddDeveloper() { + setCurrentUser(managerId); + assertDoesNotThrow(() -> + projectService.addDeveloper(projectId, developerId)); + } + + @Test + void testManagerCanAddTester() { + setCurrentUser(managerId); + assertDoesNotThrow(() -> + projectService.addTester(projectId, testerId)); + } + + @Test + void testManagerCanCreateMilestone() { + setCurrentUser(managerId); + assertDoesNotThrow(() -> + milestoneService.create(projectId, LocalDate.now(), LocalDate.now().plusDays(30))); + } + + @Test + void testManagerCanCreateTicket() { + setCurrentUser(managerId); + assertDoesNotThrow(() -> + ticketService.create(projectId, milestoneId, "Ticket")); + } + + @Test + void testTeamLeadCanCreateTicket() { + setCurrentUser(managerId); + projectService.setTeamLead(projectId, teamLeadId); + setCurrentUser(teamLeadId); + + assertDoesNotThrow(() -> + ticketService.create(projectId, milestoneId, "Ticket")); + } + + @Test + void testTeamLeadCanAssignDeveloper() { + setCurrentUser(managerId); + projectService.setTeamLead(projectId, teamLeadId); + projectService.addDeveloper(projectId, developerId); + + var ticket = ticketService.create(projectId, milestoneId, "Ticket"); + setCurrentUser(teamLeadId); + + assertDoesNotThrow(() -> + ticketService.assignDeveloper(ticket.id(), developerId)); + } + + @Test + void testDeveloperCannotSetTeamLead() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + setCurrentUser(developerId); + + assertThrows(PermissionDeniedException.class, + () -> projectService.setTeamLead(projectId, teamLeadId)); + } + + @Test + void testDeveloperCanCompleteTicket() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + var ticket = ticketService.create(projectId, milestoneId, "Ticket"); + ticketService.assignDeveloper(ticket.id(), developerId); + + setCurrentUser(developerId); + assertDoesNotThrow(() -> ticketService.complete(ticket.id())); + } + + @Test + void testDeveloperCanCreateBugReport() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + setCurrentUser(developerId); + + assertDoesNotThrow(() -> + bugReportService.create(projectId, "Bug description")); + } + + @Test + void testDeveloperCanFixBugReport() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + setCurrentUser(developerId); + + var bugReport = bugReportService.create(projectId, "Bug"); + assertDoesNotThrow(() -> bugReportService.fix(bugReport.id())); + } + + @Test + void testTesterCanTestProject() { + setCurrentUser(managerId); + projectService.addTester(projectId, testerId); + setCurrentUser(testerId); + + assertDoesNotThrow(() -> projectService.test(projectId)); + } + + @Test + void testTesterCanCreateBugReport() { + setCurrentUser(managerId); + projectService.addTester(projectId, testerId); + setCurrentUser(testerId); + + assertDoesNotThrow(() -> + bugReportService.create(projectId, "Bug description")); + } + + @Test + void testTesterCanTestBugReport() { + setCurrentUser(managerId); + projectService.addTester(projectId, testerId); + projectService.addDeveloper(projectId, developerId); + setCurrentUser(developerId); + var bugReport = bugReportService.create(projectId, "Bug"); + bugReportService.fix(bugReport.id()); + setCurrentUser(testerId); + assertDoesNotThrow(() -> bugReportService.test(bugReport.id())); + } +} + diff --git a/src/test/java/org/lab/service/TicketServiceTest.java b/src/test/java/org/lab/service/TicketServiceTest.java new file mode 100644 index 0000000..5f2db23 --- /dev/null +++ b/src/test/java/org/lab/service/TicketServiceTest.java @@ -0,0 +1,124 @@ +package org.lab.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.lab.TestBase; +import org.lab.auth.model.Role; +import org.lab.exception.TicketNotFoundException; +import org.lab.model.Milestone; +import org.lab.model.MilestoneStatus; +import org.lab.model.Ticket; +import org.lab.model.TicketStatus; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class TicketServiceTest extends TestBase { + + private UUID projectId; + private UUID milestoneId; + + @BeforeEach + void setUp() { + setCurrentUser(managerId); + var project = projectService.create(managerId, "Project", "Desc"); + projectId = project.id(); + + var milestone = milestoneService.create( + projectId, + LocalDate.now(), + LocalDate.now().plusDays(30) + ); + milestoneId = milestone.id(); + } + + @Test + void testCreateTicket() { + setCurrentUser(managerId); + Ticket ticket = ticketService.create(projectId, milestoneId, "Test ticket"); + + assertNotNull(ticket); + assertNotNull(ticket.id()); + assertEquals(projectId, ticket.projectId()); + assertEquals(milestoneId, ticket.milestoneId()); + assertEquals("Test ticket", ticket.description()); + assertEquals(TicketStatus.NEW, ticket.status()); + } + + @Test + void testAssignDeveloper() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + + Ticket ticket = ticketService.create(projectId, milestoneId, "Ticket"); + ticketService.assignDeveloper(ticket.id(), developerId); + + Ticket updated = ticketRepository.findById(ticket.id()).orElseThrow(); + assertTrue(updated.assignedDevelopers().contains(developerId)); + } + + @Test + void testAssignDeveloperDoesNotDuplicate() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + + Ticket ticket = ticketService.create(projectId, milestoneId, "Ticket"); + ticketService.assignDeveloper(ticket.id(), developerId); + ticketService.assignDeveloper(ticket.id(), developerId); + + Ticket updated = ticketRepository.findById(ticket.id()).orElseThrow(); + assertEquals(1, updated.assignedDevelopers().size()); + } + + @Test + void testGetStatus() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + + Ticket ticket = ticketService.create(projectId, milestoneId, "Ticket"); + TicketStatus status = ticketService.getStatus(ticket.id()); + + assertEquals(TicketStatus.NEW, status); + } + + @Test + void testCompleteTicket() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + Ticket ticket = ticketService.create(projectId, milestoneId, "Ticket"); + ticketService.assignDeveloper(ticket.id(), developerId); + setCurrentUser(developerId); + ticketService.complete(ticket.id()); + + Ticket completed = ticketRepository.findById(ticket.id()).orElseThrow(); + assertEquals(TicketStatus.COMPLETED, completed.status()); + } + + @Test + void testListByUser() { + setCurrentUser(managerId); + projectService.addDeveloper(projectId, developerId); + + Ticket ticket1 = ticketService.create(projectId, milestoneId, "Ticket 1"); + Ticket ticket2 = ticketService.create(projectId, milestoneId, "Ticket 2"); + + ticketService.assignDeveloper(ticket1.id(), developerId); + ticketService.assignDeveloper(ticket2.id(), developerId); + + List userTickets = ticketService.listByUser(developerId); + assertEquals(2, userTickets.size()); + } + + @Test + void testTicketNotFound() { + setCurrentUser(managerId); + UUID nonExistentId = UUID.randomUUID(); + + assertThrows(TicketNotFoundException.class, + () -> ticketService.getStatus(nonExistentId)); + } +} + diff --git a/src/test/java/org/lab/service/UserServiceTest.java b/src/test/java/org/lab/service/UserServiceTest.java new file mode 100644 index 0000000..7d2823d --- /dev/null +++ b/src/test/java/org/lab/service/UserServiceTest.java @@ -0,0 +1,41 @@ +package org.lab.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.lab.TestBase; +import org.lab.model.User; +import org.lab.repository.inmemory.InMemoryUserRepository; + +import java.time.LocalDateTime; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class UserServiceTest extends TestBase { + + @BeforeEach + void setUp() { + } + + @Test + void testRegister() { + User user = userService.register("John Doe"); + + assertNotNull(user); + assertNotNull(user.id()); + assertEquals("John Doe", user.name()); + assertNotNull(user.createdAt()); + assertTrue(user.createdAt().isBefore(LocalDateTime.now().plusSeconds(1))); + } + + @Test + void testRegisterMultipleUsers() { + User user1 = userService.register("User1"); + User user2 = userService.register("User2"); + + assertNotEquals(user1.id(), user2.id()); + assertEquals("User1", user1.name()); + assertEquals("User2", user2.name()); + } +} + From 672d4600b497cb2d2840866a079b7ad4cb4b624f Mon Sep 17 00:00:00 2001 From: egorsv21 Date: Thu, 25 Dec 2025 15:30:12 +0300 Subject: [PATCH 3/3] refactored everything --- .../java/org/lab/auth/AuthRepository.java | 4 + .../java/org/lab/auth/AuthServiceImpl.java | 8 +- .../exception/BugReportNotFoundException.java | 5 + .../exception/MilestoneNotFoundException.java | 5 + .../exception/ProjectNotFoundException.java | 5 + .../exception/TicketNotFoundException.java | 5 + src/main/java/org/lab/model/BugReport.java | 2 +- src/main/java/org/lab/model/Entity.java | 8 ++ src/main/java/org/lab/model/Milestone.java | 2 +- src/main/java/org/lab/model/Project.java | 3 +- src/main/java/org/lab/model/Ticket.java | 5 +- src/main/java/org/lab/model/User.java | 2 +- .../org/lab/service/BugReportService.java | 17 ++-- .../org/lab/service/MilestoneService.java | 12 +-- .../java/org/lab/service/ProjectService.java | 16 ++-- .../java/org/lab/service/TicketService.java | 16 ++-- .../java/org/lab/service/UserService.java | 2 +- src/test/java/org/lab/TestBase.java | 12 +-- .../java/org/lab/auth/AuthServiceTest.java | 38 ++++---- .../org/lab/service/BugReportServiceTest.java | 58 ++++++------ .../org/lab/service/MilestoneServiceTest.java | 94 +++++++++---------- .../org/lab/service/ProjectServiceTest.java | 84 ++++++++--------- .../org/lab/service/RoleBasedAccessTest.java | 78 +++++++-------- .../org/lab/service/TicketServiceTest.java | 76 +++++++-------- .../java/org/lab/service/UserServiceTest.java | 16 ++-- 25 files changed, 304 insertions(+), 269 deletions(-) create mode 100644 src/main/java/org/lab/model/Entity.java diff --git a/src/main/java/org/lab/auth/AuthRepository.java b/src/main/java/org/lab/auth/AuthRepository.java index d3ef051..b9c7b59 100644 --- a/src/main/java/org/lab/auth/AuthRepository.java +++ b/src/main/java/org/lab/auth/AuthRepository.java @@ -14,5 +14,9 @@ public interface AuthRepository { List findAll(); void deleteByUserIdAndProjectId(UUID userId, UUID projectId); + + default void delete(AccessBinding binding) { + deleteByUserIdAndProjectId(binding.userId(), binding.projectId()); + } } diff --git a/src/main/java/org/lab/auth/AuthServiceImpl.java b/src/main/java/org/lab/auth/AuthServiceImpl.java index 266544b..406b52c 100644 --- a/src/main/java/org/lab/auth/AuthServiceImpl.java +++ b/src/main/java/org/lab/auth/AuthServiceImpl.java @@ -17,14 +17,14 @@ public AuthServiceImpl(AuthRepository authRepository) { @Override public void checkPermission(UUID projectId, Permission permission) { - UUID userId = AuthenticationContext.get(); + var userId = AuthenticationContext.get(); if (!hasPermission(userId, projectId, permission)) { throw new PermissionDeniedException(userId, projectId, permission); } } private boolean hasPermission(UUID userId, UUID projectId, Permission permission) { - AccessBinding binding = authRepository.findByUserIdAndProjectId(userId, projectId) + var binding = authRepository.findByUserIdAndProjectId(userId, projectId) .orElse(null); return binding != null && binding.role().getPermissions().contains(permission.getName()); @@ -37,7 +37,7 @@ public void addBinding(UUID userId, UUID projectId, Role role) { @Override public void removeBinding(UUID userId, UUID projectId, Role role) { - AccessBinding binding = authRepository.findByUserIdAndProjectId(userId, projectId) + var binding = authRepository.findByUserIdAndProjectId(userId, projectId) .orElse(null); if (binding != null && binding.role() == role) { @@ -56,7 +56,7 @@ public List findAllByUserId(UUID userId) { public void removeAllByProjectIdAndRole(UUID projectId, Role role) { authRepository.findAll().stream() .filter(binding -> binding.projectId().equals(projectId) && binding.role() == role) - .forEach(binding -> authRepository.deleteByUserIdAndProjectId(binding.userId(), binding.projectId())); + .forEach(authRepository::delete); } } diff --git a/src/main/java/org/lab/exception/BugReportNotFoundException.java b/src/main/java/org/lab/exception/BugReportNotFoundException.java index 0204a59..c653897 100644 --- a/src/main/java/org/lab/exception/BugReportNotFoundException.java +++ b/src/main/java/org/lab/exception/BugReportNotFoundException.java @@ -1,10 +1,15 @@ package org.lab.exception; import java.util.UUID; +import java.util.function.Supplier; public class BugReportNotFoundException extends RuntimeException { public BugReportNotFoundException(UUID bugReportId) { super("Bug report not found: " + bugReportId); } + + public static Supplier supplier(UUID bugReportId) { + return () -> new BugReportNotFoundException(bugReportId); + } } diff --git a/src/main/java/org/lab/exception/MilestoneNotFoundException.java b/src/main/java/org/lab/exception/MilestoneNotFoundException.java index 76919f0..4b1bf75 100644 --- a/src/main/java/org/lab/exception/MilestoneNotFoundException.java +++ b/src/main/java/org/lab/exception/MilestoneNotFoundException.java @@ -1,10 +1,15 @@ package org.lab.exception; import java.util.UUID; +import java.util.function.Supplier; public class MilestoneNotFoundException extends RuntimeException { public MilestoneNotFoundException(UUID milestoneId) { super("Milestone not found: " + milestoneId); } + + public static Supplier supplier(UUID milestoneId) { + return () -> new MilestoneNotFoundException(milestoneId); + } } diff --git a/src/main/java/org/lab/exception/ProjectNotFoundException.java b/src/main/java/org/lab/exception/ProjectNotFoundException.java index b7800a3..6a1d76e 100644 --- a/src/main/java/org/lab/exception/ProjectNotFoundException.java +++ b/src/main/java/org/lab/exception/ProjectNotFoundException.java @@ -1,10 +1,15 @@ package org.lab.exception; import java.util.UUID; +import java.util.function.Supplier; public class ProjectNotFoundException extends RuntimeException { public ProjectNotFoundException(UUID projectId) { super("Project not found: " + projectId); } + + public static Supplier supplier(UUID projectId) { + return () -> new ProjectNotFoundException(projectId); + } } diff --git a/src/main/java/org/lab/exception/TicketNotFoundException.java b/src/main/java/org/lab/exception/TicketNotFoundException.java index edebf3f..13d2cd4 100644 --- a/src/main/java/org/lab/exception/TicketNotFoundException.java +++ b/src/main/java/org/lab/exception/TicketNotFoundException.java @@ -1,10 +1,15 @@ package org.lab.exception; import java.util.UUID; +import java.util.function.Supplier; public class TicketNotFoundException extends RuntimeException { public TicketNotFoundException(UUID ticketId) { super("Ticket not found: " + ticketId); } + + public static Supplier supplier(UUID ticketId) { + return () -> new TicketNotFoundException(ticketId); + } } diff --git a/src/main/java/org/lab/model/BugReport.java b/src/main/java/org/lab/model/BugReport.java index 6eac7d9..237f737 100644 --- a/src/main/java/org/lab/model/BugReport.java +++ b/src/main/java/org/lab/model/BugReport.java @@ -8,6 +8,6 @@ public record BugReport( UUID projectId, String description, @With BugReportStatus status -) { +) implements Entity { } diff --git a/src/main/java/org/lab/model/Entity.java b/src/main/java/org/lab/model/Entity.java new file mode 100644 index 0000000..4e97979 --- /dev/null +++ b/src/main/java/org/lab/model/Entity.java @@ -0,0 +1,8 @@ +package org.lab.model; + +import java.util.UUID; + +public sealed interface Entity permits BugReport, Milestone, Project, Ticket, User { + UUID id(); +} + diff --git a/src/main/java/org/lab/model/Milestone.java b/src/main/java/org/lab/model/Milestone.java index de2001d..457a48c 100644 --- a/src/main/java/org/lab/model/Milestone.java +++ b/src/main/java/org/lab/model/Milestone.java @@ -12,6 +12,6 @@ public record Milestone( LocalDate endDate, @With List ticketIds, @With MilestoneStatus status -) { +) implements Entity { } diff --git a/src/main/java/org/lab/model/Project.java b/src/main/java/org/lab/model/Project.java index 3d0d95f..a3640ff 100644 --- a/src/main/java/org/lab/model/Project.java +++ b/src/main/java/org/lab/model/Project.java @@ -3,11 +3,12 @@ import java.util.List; import java.util.UUID; +// Can be value object public record Project( UUID id, String title, String description, List milestoneIds, List bugReportIds -) { +) implements Entity { } diff --git a/src/main/java/org/lab/model/Ticket.java b/src/main/java/org/lab/model/Ticket.java index 60627a2..1969c1a 100644 --- a/src/main/java/org/lab/model/Ticket.java +++ b/src/main/java/org/lab/model/Ticket.java @@ -11,6 +11,9 @@ public record Ticket( String description, @With List assignedDevelopers, @With TicketStatus status -) { +) implements Entity { + public boolean isCompleted() { + return status == TicketStatus.COMPLETED; + } } diff --git a/src/main/java/org/lab/model/User.java b/src/main/java/org/lab/model/User.java index 54f4a15..79a5e9c 100644 --- a/src/main/java/org/lab/model/User.java +++ b/src/main/java/org/lab/model/User.java @@ -7,6 +7,6 @@ public record User( UUID id, String name, LocalDateTime createdAt -) { +) implements Entity { } diff --git a/src/main/java/org/lab/service/BugReportService.java b/src/main/java/org/lab/service/BugReportService.java index f580f9a..1b8de48 100644 --- a/src/main/java/org/lab/service/BugReportService.java +++ b/src/main/java/org/lab/service/BugReportService.java @@ -9,7 +9,6 @@ import org.lab.repository.BugReportRepository; import java.util.List; -import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -25,7 +24,7 @@ public BugReportService(BugReportRepository bugReportRepository, AuthService aut public BugReport create(UUID projectId, String description) { authService.checkPermission(projectId, Permission.BUG_REPORT_CREATE); - BugReport bugReport = new BugReport( + var bugReport = new BugReport( UUID.randomUUID(), projectId, description, @@ -35,7 +34,7 @@ public BugReport create(UUID projectId, String description) { } public List listByUser(UUID userId) { - Set userProjectIds = authService.findAllByUserId(userId).stream() + var userProjectIds = authService.findAllByUserId(userId).stream() .map(AccessBinding::projectId) .collect(Collectors.toSet()); @@ -45,8 +44,8 @@ public List listByUser(UUID userId) { } public void fix(UUID bugReportId) { - BugReport bugReport = bugReportRepository.findById(bugReportId) - .orElseThrow(() -> new BugReportNotFoundException(bugReportId)); + var bugReport = bugReportRepository.findById(bugReportId) + .orElseThrow(BugReportNotFoundException.supplier(bugReportId)); authService.checkPermission(bugReport.projectId(), Permission.BUG_REPORT_FIX); @@ -54,8 +53,8 @@ public void fix(UUID bugReportId) { } public void test(UUID bugReportId) { - BugReport bugReport = bugReportRepository.findById(bugReportId) - .orElseThrow(() -> new BugReportNotFoundException(bugReportId)); + var bugReport = bugReportRepository.findById(bugReportId) + .orElseThrow(BugReportNotFoundException.supplier(bugReportId)); authService.checkPermission(bugReport.projectId(), Permission.BUG_REPORT_TEST); @@ -63,8 +62,8 @@ public void test(UUID bugReportId) { } public void close(UUID bugReportId) { - BugReport bugReport = bugReportRepository.findById(bugReportId) - .orElseThrow(() -> new BugReportNotFoundException(bugReportId)); + var bugReport = bugReportRepository.findById(bugReportId) + .orElseThrow(BugReportNotFoundException.supplier(bugReportId)); authService.checkPermission(bugReport.projectId(), Permission.BUG_REPORT_CLOSE); diff --git a/src/main/java/org/lab/service/MilestoneService.java b/src/main/java/org/lab/service/MilestoneService.java index fa64cf4..f0a4512 100644 --- a/src/main/java/org/lab/service/MilestoneService.java +++ b/src/main/java/org/lab/service/MilestoneService.java @@ -31,7 +31,7 @@ public MilestoneService(MilestoneRepository milestoneRepository, TicketRepositor public Milestone create(UUID projectId, LocalDate startDate, LocalDate endDate) { authService.checkPermission(projectId, Permission.MILESTONE_CREATE); - Milestone milestone = new Milestone( + var milestone = new Milestone( UUID.randomUUID(), projectId, startDate, @@ -43,17 +43,17 @@ public Milestone create(UUID projectId, LocalDate startDate, LocalDate endDate) } public void setStatus(UUID milestoneId, MilestoneStatus status) { - Milestone milestone = milestoneRepository.findById(milestoneId) - .orElseThrow(() -> new MilestoneNotFoundException(milestoneId)); + var milestone = milestoneRepository.findById(milestoneId) + .orElseThrow(MilestoneNotFoundException.supplier(milestoneId)); authService.checkPermission(milestone.projectId(), Permission.MILESTONE_SET_STATUS); if (status == MilestoneStatus.CLOSED) { - boolean allTicketsCompleted = milestone.ticketIds().stream() + var allTicketsCompleted = milestone.ticketIds().stream() .map(ticketRepository::findById) .filter(Optional::isPresent) .map(Optional::get) - .allMatch(ticket -> ticket.status() == TicketStatus.COMPLETED); + .allMatch(Ticket::isCompleted); if (!allTicketsCompleted) { throw new NotAllTicketsCompletedException(milestoneId); @@ -61,7 +61,7 @@ public void setStatus(UUID milestoneId, MilestoneStatus status) { } if (status == MilestoneStatus.ACTIVE) { - boolean hasActiveMilestone = milestoneRepository.findAll().stream() + var hasActiveMilestone = milestoneRepository.findAll().stream() .anyMatch(m -> m.projectId().equals(milestone.projectId()) && m.status() == MilestoneStatus.ACTIVE && !m.id().equals(milestoneId)); diff --git a/src/main/java/org/lab/service/ProjectService.java b/src/main/java/org/lab/service/ProjectService.java index b8c73b3..e8d5b8a 100644 --- a/src/main/java/org/lab/service/ProjectService.java +++ b/src/main/java/org/lab/service/ProjectService.java @@ -29,7 +29,7 @@ public ProjectService(ProjectRepository projectRepository, AuthService authServi } public List list(UUID userId) { - Set userProjectIds = authService.findAllByUserId(userId).stream() + var userProjectIds = authService.findAllByUserId(userId).stream() .map(AccessBinding::projectId) .collect(Collectors.toSet()); @@ -39,21 +39,21 @@ public List list(UUID userId) { } public Project create(UUID userId, String title, String description) { - Project project = new Project( + var project = new Project( UUID.randomUUID(), title, description, new ArrayList<>(), new ArrayList<>() ); - Project savedProject = projectRepository.save(project); + var savedProject = projectRepository.save(project); authService.addBinding(userId, savedProject.id(), Role.MANAGER); return savedProject; } public void setTeamLead(UUID projectId, UUID teamLeadId) { projectRepository.findById(projectId) - .orElseThrow(() -> new ProjectNotFoundException(projectId)); + .orElseThrow(ProjectNotFoundException.supplier(projectId)); authService.checkPermission(projectId, Permission.PROJECT_SET_TEAM_LEAD); @@ -63,7 +63,7 @@ public void setTeamLead(UUID projectId, UUID teamLeadId) { public void addDeveloper(UUID projectId, UUID developerId) { projectRepository.findById(projectId) - .orElseThrow(() -> new ProjectNotFoundException(projectId)); + .orElseThrow(ProjectNotFoundException.supplier(projectId)); authService.checkPermission(projectId, Permission.PROJECT_ADD_DEVELOPER); @@ -72,7 +72,7 @@ public void addDeveloper(UUID projectId, UUID developerId) { public void addTester(UUID projectId, UUID testerId) { projectRepository.findById(projectId) - .orElseThrow(() -> new ProjectNotFoundException(projectId)); + .orElseThrow(ProjectNotFoundException.supplier(projectId)); authService.checkPermission(projectId, Permission.PROJECT_ADD_TESTER); @@ -80,8 +80,8 @@ public void addTester(UUID projectId, UUID testerId) { } public void test(UUID projectId) { - Project project = projectRepository.findById(projectId) - .orElseThrow(() -> new ProjectNotFoundException(projectId)); + var project = projectRepository.findById(projectId) + .orElseThrow(ProjectNotFoundException.supplier(projectId)); authService.checkPermission(projectId, Permission.PROJECT_TEST); diff --git a/src/main/java/org/lab/service/TicketService.java b/src/main/java/org/lab/service/TicketService.java index b000780..fb2f582 100644 --- a/src/main/java/org/lab/service/TicketService.java +++ b/src/main/java/org/lab/service/TicketService.java @@ -23,7 +23,7 @@ public TicketService(TicketRepository ticketRepository, AuthService authService) public Ticket create(UUID projectId, UUID milestoneId, String description) { authService.checkPermission(projectId, Permission.TICKET_CREATE); - Ticket ticket = new Ticket( + var ticket = new Ticket( UUID.randomUUID(), projectId, milestoneId, @@ -41,12 +41,12 @@ public List listByUser(UUID userId) { } public void assignDeveloper(UUID ticketId, UUID developerId) { - Ticket ticket = ticketRepository.findById(ticketId) - .orElseThrow(() -> new TicketNotFoundException(ticketId)); + var ticket = ticketRepository.findById(ticketId) + .orElseThrow(TicketNotFoundException.supplier(ticketId)); authService.checkPermission(ticket.projectId(), Permission.TICKET_ASSIGN_DEVELOPER); - List developers = new ArrayList<>(ticket.assignedDevelopers()); + var developers = new ArrayList<>(ticket.assignedDevelopers()); if (!developers.contains(developerId)) { developers.add(developerId); } @@ -55,8 +55,8 @@ public void assignDeveloper(UUID ticketId, UUID developerId) { } public TicketStatus getStatus(UUID ticketId) { - Ticket ticket = ticketRepository.findById(ticketId) - .orElseThrow(() -> new TicketNotFoundException(ticketId)); + var ticket = ticketRepository.findById(ticketId) + .orElseThrow(TicketNotFoundException.supplier(ticketId)); authService.checkPermission(ticket.projectId(), Permission.TICKET_GET_STATUS); @@ -64,8 +64,8 @@ public TicketStatus getStatus(UUID ticketId) { } public void complete(UUID ticketId) { - Ticket ticket = ticketRepository.findById(ticketId) - .orElseThrow(() -> new TicketNotFoundException(ticketId)); + var ticket = ticketRepository.findById(ticketId) + .orElseThrow(TicketNotFoundException.supplier(ticketId)); authService.checkPermission(ticket.projectId(), Permission.TICKET_COMPLETE); diff --git a/src/main/java/org/lab/service/UserService.java b/src/main/java/org/lab/service/UserService.java index 3518a4e..a265f34 100644 --- a/src/main/java/org/lab/service/UserService.java +++ b/src/main/java/org/lab/service/UserService.java @@ -14,7 +14,7 @@ public UserService(UserRepository userRepository) { } public User register(String name) { - User user = new User( + var user = new User( UUID.randomUUID(), name, LocalDateTime.now() diff --git a/src/test/java/org/lab/TestBase.java b/src/test/java/org/lab/TestBase.java index 0f78aee..eb30be0 100644 --- a/src/test/java/org/lab/TestBase.java +++ b/src/test/java/org/lab/TestBase.java @@ -19,14 +19,14 @@ public abstract class TestBase { protected MilestoneRepository milestoneRepository; protected BugReportRepository bugReportRepository; protected AuthRepository authRepository; - + protected UserService userService; protected ProjectService projectService; protected TicketService ticketService; protected MilestoneService milestoneService; protected BugReportService bugReportService; protected AuthService authService; - + protected UUID managerId; protected UUID teamLeadId; protected UUID developerId; @@ -40,24 +40,24 @@ protected void baseSetUp() { milestoneRepository = new InMemoryMilestoneRepository(); bugReportRepository = new InMemoryBugReportRepository(); authRepository = new InMemoryAuthRepository(); - + authService = new AuthServiceImpl(authRepository); userService = new UserService(userRepository); projectService = new ProjectService(projectRepository, authService); ticketService = new TicketService(ticketRepository, authService); milestoneService = new MilestoneService(milestoneRepository, ticketRepository, authService); bugReportService = new BugReportService(bugReportRepository, authService); - + managerId = userService.register("Manager").id(); teamLeadId = userService.register("TeamLead").id(); developerId = userService.register("Developer").id(); testerId = userService.register("Tester").id(); } - + protected void setCurrentUser(UUID userId) { AuthenticationContext.set(userId); } - + protected void clearCurrentUser() { AuthenticationContext.clear(); } diff --git a/src/test/java/org/lab/auth/AuthServiceTest.java b/src/test/java/org/lab/auth/AuthServiceTest.java index 3c75c3c..6bad0e9 100644 --- a/src/test/java/org/lab/auth/AuthServiceTest.java +++ b/src/test/java/org/lab/auth/AuthServiceTest.java @@ -13,68 +13,68 @@ import static org.junit.jupiter.api.Assertions.*; class AuthServiceTest extends TestBase { - + private UUID projectId; - + @BeforeEach void setUp() { setCurrentUser(managerId); - Project project = projectService.create(managerId, "Project", "Desc"); + var project = projectService.create(managerId, "Project", "Desc"); projectId = project.id(); } - + @Test void testAddBinding() { authService.addBinding(developerId, projectId, Role.DEVELOPER); - + var binding = authRepository.findByUserIdAndProjectId(developerId, projectId); assertTrue(binding.isPresent()); assertEquals(Role.DEVELOPER, binding.get().role()); } - + @Test void testRemoveBinding() { authService.addBinding(developerId, projectId, Role.DEVELOPER); authService.removeBinding(developerId, projectId, Role.DEVELOPER); - + var binding = authRepository.findByUserIdAndProjectId(developerId, projectId); assertFalse(binding.isPresent()); } - + @Test void testRemoveBindingOnlyRemovesCorrectRole() { authService.addBinding(developerId, projectId, Role.DEVELOPER); authService.removeBinding(developerId, projectId, Role.TESTER); - + var binding = authRepository.findByUserIdAndProjectId(developerId, projectId); assertTrue(binding.isPresent()); } - + @Test void testCheckPermissionSuccess() { setCurrentUser(managerId); authService.addBinding(managerId, projectId, Role.MANAGER); - - assertDoesNotThrow(() -> + + assertDoesNotThrow(() -> authService.checkPermission(projectId, Permission.PROJECT_SET_TEAM_LEAD)); } - + @Test void testCheckPermissionDenied() { setCurrentUser(developerId); authService.addBinding(developerId, projectId, Role.DEVELOPER); - - assertThrows(PermissionDeniedException.class, + + assertThrows(PermissionDeniedException.class, () -> authService.checkPermission(projectId, Permission.PROJECT_SET_TEAM_LEAD)); } - + @Test void testFindAllByUserId() { - UUID projectId2 = projectService.create(managerId, "Project 2", "Desc").id(); - + var projectId2 = projectService.create(managerId, "Project 2", "Desc").id(); + authService.addBinding(developerId, projectId, Role.DEVELOPER); authService.addBinding(developerId, projectId2, Role.DEVELOPER); - + var bindings = authService.findAllByUserId(developerId); assertEquals(2, bindings.size()); } diff --git a/src/test/java/org/lab/service/BugReportServiceTest.java b/src/test/java/org/lab/service/BugReportServiceTest.java index fe1a9ef..afe167e 100644 --- a/src/test/java/org/lab/service/BugReportServiceTest.java +++ b/src/test/java/org/lab/service/BugReportServiceTest.java @@ -14,98 +14,98 @@ import static org.junit.jupiter.api.Assertions.*; class BugReportServiceTest extends TestBase { - + private UUID projectId; - + @BeforeEach void setUp() { setCurrentUser(managerId); var project = projectService.create(managerId, "Project", "Desc"); projectId = project.id(); } - + @Test void testCreateBugReport() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); setCurrentUser(developerId); - - BugReport bugReport = bugReportService.create(projectId, "Bug description"); - + + var bugReport = bugReportService.create(projectId, "Bug description"); + assertNotNull(bugReport); assertNotNull(bugReport.id()); assertEquals(projectId, bugReport.projectId()); assertEquals("Bug description", bugReport.description()); assertEquals(BugReportStatus.NEW, bugReport.status()); } - + @Test void testFixBugReport() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); setCurrentUser(developerId); - - BugReport bugReport = bugReportService.create(projectId, "Bug"); + + var bugReport = bugReportService.create(projectId, "Bug"); bugReportService.fix(bugReport.id()); - - BugReport fixed = bugReportRepository.findById(bugReport.id()).orElseThrow(); + + var fixed = bugReportRepository.findById(bugReport.id()).orElseThrow(); assertEquals(BugReportStatus.FIXED, fixed.status()); } - + @Test void testTestBugReport() { setCurrentUser(managerId); projectService.addTester(projectId, testerId); projectService.addDeveloper(projectId, developerId); setCurrentUser(developerId); - BugReport bugReport = bugReportService.create(projectId, "Bug"); + var bugReport = bugReportService.create(projectId, "Bug"); bugReportService.fix(bugReport.id()); setCurrentUser(testerId); bugReportService.test(bugReport.id()); - - BugReport tested = bugReportRepository.findById(bugReport.id()).orElseThrow(); + + var tested = bugReportRepository.findById(bugReport.id()).orElseThrow(); assertEquals(BugReportStatus.TESTED, tested.status()); } - + @Test void testCloseBugReport() { setCurrentUser(managerId); projectService.addTester(projectId, testerId); projectService.addDeveloper(projectId, developerId); setCurrentUser(developerId); - BugReport bugReport = bugReportService.create(projectId, "Bug"); + var bugReport = bugReportService.create(projectId, "Bug"); bugReportService.fix(bugReport.id()); setCurrentUser(testerId); bugReportService.test(bugReport.id()); bugReportService.close(bugReport.id()); - - BugReport closed = bugReportRepository.findById(bugReport.id()).orElseThrow(); + + var closed = bugReportRepository.findById(bugReport.id()).orElseThrow(); assertEquals(BugReportStatus.CLOSED, closed.status()); } - + @Test void testListByUser() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); setCurrentUser(developerId); - - BugReport bugReport1 = bugReportService.create(projectId, "Bug 1"); - BugReport bugReport2 = bugReportService.create(projectId, "Bug 2"); - - List userBugs = bugReportService.listByUser(developerId); + + var bugReport1 = bugReportService.create(projectId, "Bug 1"); + var bugReport2 = bugReportService.create(projectId, "Bug 2"); + + var userBugs = bugReportService.listByUser(developerId); assertTrue(userBugs.size() >= 2); assertTrue(userBugs.stream().anyMatch(b -> b.id().equals(bugReport1.id()))); assertTrue(userBugs.stream().anyMatch(b -> b.id().equals(bugReport2.id()))); } - + @Test void testBugReportNotFound() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); setCurrentUser(developerId); - UUID nonExistentId = UUID.randomUUID(); - - assertThrows(BugReportNotFoundException.class, + var nonExistentId = UUID.randomUUID(); + + assertThrows(BugReportNotFoundException.class, () -> bugReportService.fix(nonExistentId)); } } diff --git a/src/test/java/org/lab/service/MilestoneServiceTest.java b/src/test/java/org/lab/service/MilestoneServiceTest.java index 8754dbe..f921bd5 100644 --- a/src/test/java/org/lab/service/MilestoneServiceTest.java +++ b/src/test/java/org/lab/service/MilestoneServiceTest.java @@ -19,122 +19,122 @@ import static org.junit.jupiter.api.Assertions.*; class MilestoneServiceTest extends TestBase { - + private UUID projectId; private UUID milestoneId; - + @BeforeEach void setUp() { setCurrentUser(managerId); var project = projectService.create(managerId, "Project", "Desc"); projectId = project.id(); } - + @Test void testCreateMilestone() { setCurrentUser(managerId); - Milestone milestone = milestoneService.create( - projectId, - LocalDate.now(), + var milestone = milestoneService.create( + projectId, + LocalDate.now(), LocalDate.now().plusDays(30) ); - + assertNotNull(milestone); assertNotNull(milestone.id()); assertEquals(projectId, milestone.projectId()); assertEquals(MilestoneStatus.OPENED, milestone.status()); } - + @Test void testSetStatusToActive() { setCurrentUser(managerId); - Milestone milestone = milestoneService.create( - projectId, - LocalDate.now(), + var milestone = milestoneService.create( + projectId, + LocalDate.now(), LocalDate.now().plusDays(30) ); - + milestoneService.setStatus(milestone.id(), MilestoneStatus.ACTIVE); - - Milestone updated = milestoneRepository.findById(milestone.id()).orElseThrow(); + + var updated = milestoneRepository.findById(milestone.id()).orElseThrow(); assertEquals(MilestoneStatus.ACTIVE, updated.status()); } - + @Test void testOnlyOneActiveMilestonePerProject() { setCurrentUser(managerId); - Milestone milestone1 = milestoneService.create( - projectId, - LocalDate.now(), + var milestone1 = milestoneService.create( + projectId, + LocalDate.now(), LocalDate.now().plusDays(30) ); milestoneService.setStatus(milestone1.id(), MilestoneStatus.ACTIVE); - - Milestone milestone2 = milestoneService.create( - projectId, - LocalDate.now().plusDays(31), + + var milestone2 = milestoneService.create( + projectId, + LocalDate.now().plusDays(31), LocalDate.now().plusDays(60) ); - - assertThrows(ActiveMilestoneExistsException.class, + + assertThrows(ActiveMilestoneExistsException.class, () -> milestoneService.setStatus(milestone2.id(), MilestoneStatus.ACTIVE)); } - + @Test void testCannotCloseMilestoneWithIncompleteTickets() { setCurrentUser(managerId); - Milestone milestone = milestoneService.create( - projectId, - LocalDate.now(), + var milestone = milestoneService.create( + projectId, + LocalDate.now(), LocalDate.now().plusDays(30) ); - - Ticket ticket = ticketService.create(projectId, milestone.id(), "Test ticket"); + + var ticket = ticketService.create(projectId, milestone.id(), "Test ticket"); var updatedMilestone = milestoneRepository.findById(milestone.id()).orElseThrow(); var milestoneWithTicket = updatedMilestone.withTicketIds( List.of(ticket.id()) ); milestoneRepository.save(milestoneWithTicket); - - assertThrows(NotAllTicketsCompletedException.class, + + assertThrows(NotAllTicketsCompletedException.class, () -> milestoneService.setStatus(milestone.id(), MilestoneStatus.CLOSED)); } - + @Test void testCanCloseMilestoneWhenAllTicketsCompleted() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); - Milestone milestone = milestoneService.create( - projectId, - LocalDate.now(), + var milestone = milestoneService.create( + projectId, + LocalDate.now(), LocalDate.now().plusDays(30) ); - - Ticket ticket = ticketService.create(projectId, milestone.id(), "Test ticket"); + + var ticket = ticketService.create(projectId, milestone.id(), "Test ticket"); ticketService.assignDeveloper(ticket.id(), developerId); setCurrentUser(developerId); ticketService.complete(ticket.id()); - + var updatedMilestone = milestoneRepository.findById(milestone.id()).orElseThrow(); var milestoneWithTicket = updatedMilestone.withTicketIds( List.of(ticket.id()) ); milestoneRepository.save(milestoneWithTicket); - + setCurrentUser(managerId); - assertDoesNotThrow(() -> + assertDoesNotThrow(() -> milestoneService.setStatus(milestone.id(), MilestoneStatus.CLOSED)); - - Milestone closed = milestoneRepository.findById(milestone.id()).orElseThrow(); + + var closed = milestoneRepository.findById(milestone.id()).orElseThrow(); assertEquals(MilestoneStatus.CLOSED, closed.status()); } - + @Test void testMilestoneNotFound() { setCurrentUser(managerId); - UUID nonExistentId = UUID.randomUUID(); - - assertThrows(MilestoneNotFoundException.class, + var nonExistentId = UUID.randomUUID(); + + assertThrows(MilestoneNotFoundException.class, () -> milestoneService.setStatus(nonExistentId, MilestoneStatus.ACTIVE)); } } diff --git a/src/test/java/org/lab/service/ProjectServiceTest.java b/src/test/java/org/lab/service/ProjectServiceTest.java index 1562bee..40b9b32 100644 --- a/src/test/java/org/lab/service/ProjectServiceTest.java +++ b/src/test/java/org/lab/service/ProjectServiceTest.java @@ -13,130 +13,130 @@ import static org.junit.jupiter.api.Assertions.*; class ProjectServiceTest extends TestBase { - + private UUID projectId; - + @BeforeEach void setUp() { setCurrentUser(managerId); - Project project = projectService.create(managerId, "Test Project", "Description"); + var project = projectService.create(managerId, "Test Project", "Description"); projectId = project.id(); } - + @Test void testCreateProject() { setCurrentUser(managerId); - Project project = projectService.create(managerId, "New Project", "New Description"); - + var project = projectService.create(managerId, "New Project", "New Description"); + assertNotNull(project); assertNotNull(project.id()); assertEquals("New Project", project.title()); assertEquals("New Description", project.description()); } - + @Test void testCreateProjectSetsManagerRole() { setCurrentUser(managerId); - Project project = projectService.create(managerId, "Project", "Desc"); - + var project = projectService.create(managerId, "Project", "Desc"); + var binding = authRepository.findByUserIdAndProjectId(managerId, project.id()); assertTrue(binding.isPresent()); assertEquals(Role.MANAGER, binding.get().role()); } - + @Test void testListProjectsForUser() { - UUID testUserId = userService.register("Test User").id(); - + var testUserId = userService.register("Test User").id(); + setCurrentUser(testUserId); - Project managerProject = projectService.create(testUserId, "Manager Project", "Desc"); - + var managerProject = projectService.create(testUserId, "Manager Project", "Desc"); + setCurrentUser(managerId); - Project teamLeadProject = projectService.create(managerId, "TeamLead Project", "Desc"); + var teamLeadProject = projectService.create(managerId, "TeamLead Project", "Desc"); projectService.setTeamLead(teamLeadProject.id(), testUserId); - - Project developerProject = projectService.create(managerId, "Developer Project", "Desc"); + + var developerProject = projectService.create(managerId, "Developer Project", "Desc"); projectService.addDeveloper(developerProject.id(), testUserId); - - Project testerProject = projectService.create(managerId, "Tester Project", "Desc"); + + var testerProject = projectService.create(managerId, "Tester Project", "Desc"); projectService.addTester(testerProject.id(), testUserId); - - UUID otherUserId = userService.register("Other User").id(); + + var otherUserId = userService.register("Other User").id(); setCurrentUser(otherUserId); - Project otherProject = projectService.create(otherUserId, "Other Project 1", "Desc"); + var otherProject = projectService.create(otherUserId, "Other Project 1", "Desc"); + + var userProjects = projectService.list(testUserId); - List userProjects = projectService.list(testUserId); - assertEquals(4, userProjects.size()); assertTrue(userProjects.stream().anyMatch(p -> p.id().equals(managerProject.id()))); assertTrue(userProjects.stream().anyMatch(p -> p.id().equals(teamLeadProject.id()))); assertTrue(userProjects.stream().anyMatch(p -> p.id().equals(developerProject.id()))); assertTrue(userProjects.stream().anyMatch(p -> p.id().equals(testerProject.id()))); - + assertFalse(userProjects.stream().anyMatch(p -> p.id().equals(otherProject.id()))); } - + @Test void testSetTeamLead() { setCurrentUser(managerId); projectService.setTeamLead(projectId, teamLeadId); - + var binding = authRepository.findByUserIdAndProjectId(teamLeadId, projectId); assertTrue(binding.isPresent()); assertEquals(Role.TEAM_LEAD, binding.get().role()); } - + @Test void testSetTeamLeadRemovesPreviousTeamLead() { setCurrentUser(managerId); - UUID previousTeamLead = userService.register("Previous TeamLead").id(); + var previousTeamLead = userService.register("Previous TeamLead").id(); projectService.setTeamLead(projectId, previousTeamLead); - + projectService.setTeamLead(projectId, teamLeadId); - + var previousBinding = authRepository.findByUserIdAndProjectId(previousTeamLead, projectId); var newBinding = authRepository.findByUserIdAndProjectId(teamLeadId, projectId); - + assertFalse(previousBinding.isPresent()); assertTrue(newBinding.isPresent()); assertEquals(Role.TEAM_LEAD, newBinding.get().role()); } - + @Test void testAddDeveloper() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); - + var binding = authRepository.findByUserIdAndProjectId(developerId, projectId); assertTrue(binding.isPresent()); assertEquals(Role.DEVELOPER, binding.get().role()); } - + @Test void testAddTester() { setCurrentUser(managerId); projectService.addTester(projectId, testerId); - + var binding = authRepository.findByUserIdAndProjectId(testerId, projectId); assertTrue(binding.isPresent()); assertEquals(Role.TESTER, binding.get().role()); } - + @Test void testTestProject() { setCurrentUser(managerId); projectService.addTester(projectId, testerId); setCurrentUser(testerId); - + assertDoesNotThrow(() -> projectService.test(projectId)); } - + @Test void testProjectNotFound() { setCurrentUser(managerId); - UUID nonExistentId = UUID.randomUUID(); - - assertThrows(ProjectNotFoundException.class, + var nonExistentId = UUID.randomUUID(); + + assertThrows(ProjectNotFoundException.class, () -> projectService.setTeamLead(nonExistentId, teamLeadId)); } } diff --git a/src/test/java/org/lab/service/RoleBasedAccessTest.java b/src/test/java/org/lab/service/RoleBasedAccessTest.java index f292336..3d4ed46 100644 --- a/src/test/java/org/lab/service/RoleBasedAccessTest.java +++ b/src/test/java/org/lab/service/RoleBasedAccessTest.java @@ -14,142 +14,142 @@ import static org.junit.jupiter.api.Assertions.*; class RoleBasedAccessTest extends TestBase { - + private UUID projectId; private UUID milestoneId; - + @BeforeEach void setUp() { setCurrentUser(managerId); - Project project = projectService.create(managerId, "Project", "Desc"); + var project = projectService.create(managerId, "Project", "Desc"); projectId = project.id(); - + var milestone = milestoneService.create( - projectId, - LocalDate.now(), + projectId, + LocalDate.now(), LocalDate.now().plusDays(30) ); milestoneId = milestone.id(); } - + @Test void testManagerCanSetTeamLead() { setCurrentUser(managerId); - assertDoesNotThrow(() -> + assertDoesNotThrow(() -> projectService.setTeamLead(projectId, teamLeadId)); } - + @Test void testManagerCanAddDeveloper() { setCurrentUser(managerId); - assertDoesNotThrow(() -> + assertDoesNotThrow(() -> projectService.addDeveloper(projectId, developerId)); } - + @Test void testManagerCanAddTester() { setCurrentUser(managerId); - assertDoesNotThrow(() -> + assertDoesNotThrow(() -> projectService.addTester(projectId, testerId)); } - + @Test void testManagerCanCreateMilestone() { setCurrentUser(managerId); - assertDoesNotThrow(() -> + assertDoesNotThrow(() -> milestoneService.create(projectId, LocalDate.now(), LocalDate.now().plusDays(30))); } - + @Test void testManagerCanCreateTicket() { setCurrentUser(managerId); - assertDoesNotThrow(() -> + assertDoesNotThrow(() -> ticketService.create(projectId, milestoneId, "Ticket")); } - + @Test void testTeamLeadCanCreateTicket() { setCurrentUser(managerId); projectService.setTeamLead(projectId, teamLeadId); setCurrentUser(teamLeadId); - - assertDoesNotThrow(() -> + + assertDoesNotThrow(() -> ticketService.create(projectId, milestoneId, "Ticket")); } - + @Test void testTeamLeadCanAssignDeveloper() { setCurrentUser(managerId); projectService.setTeamLead(projectId, teamLeadId); projectService.addDeveloper(projectId, developerId); - + var ticket = ticketService.create(projectId, milestoneId, "Ticket"); setCurrentUser(teamLeadId); - - assertDoesNotThrow(() -> + + assertDoesNotThrow(() -> ticketService.assignDeveloper(ticket.id(), developerId)); } - + @Test void testDeveloperCannotSetTeamLead() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); setCurrentUser(developerId); - - assertThrows(PermissionDeniedException.class, + + assertThrows(PermissionDeniedException.class, () -> projectService.setTeamLead(projectId, teamLeadId)); } - + @Test void testDeveloperCanCompleteTicket() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); var ticket = ticketService.create(projectId, milestoneId, "Ticket"); ticketService.assignDeveloper(ticket.id(), developerId); - + setCurrentUser(developerId); assertDoesNotThrow(() -> ticketService.complete(ticket.id())); } - + @Test void testDeveloperCanCreateBugReport() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); setCurrentUser(developerId); - - assertDoesNotThrow(() -> + + assertDoesNotThrow(() -> bugReportService.create(projectId, "Bug description")); } - + @Test void testDeveloperCanFixBugReport() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); setCurrentUser(developerId); - + var bugReport = bugReportService.create(projectId, "Bug"); assertDoesNotThrow(() -> bugReportService.fix(bugReport.id())); } - + @Test void testTesterCanTestProject() { setCurrentUser(managerId); projectService.addTester(projectId, testerId); setCurrentUser(testerId); - + assertDoesNotThrow(() -> projectService.test(projectId)); } - + @Test void testTesterCanCreateBugReport() { setCurrentUser(managerId); projectService.addTester(projectId, testerId); setCurrentUser(testerId); - - assertDoesNotThrow(() -> + + assertDoesNotThrow(() -> bugReportService.create(projectId, "Bug description")); } - + @Test void testTesterCanTestBugReport() { setCurrentUser(managerId); diff --git a/src/test/java/org/lab/service/TicketServiceTest.java b/src/test/java/org/lab/service/TicketServiceTest.java index 5f2db23..dce654c 100644 --- a/src/test/java/org/lab/service/TicketServiceTest.java +++ b/src/test/java/org/lab/service/TicketServiceTest.java @@ -17,29 +17,29 @@ import static org.junit.jupiter.api.Assertions.*; class TicketServiceTest extends TestBase { - + private UUID projectId; private UUID milestoneId; - + @BeforeEach void setUp() { setCurrentUser(managerId); var project = projectService.create(managerId, "Project", "Desc"); projectId = project.id(); - + var milestone = milestoneService.create( - projectId, - LocalDate.now(), + projectId, + LocalDate.now(), LocalDate.now().plusDays(30) ); milestoneId = milestone.id(); } - + @Test void testCreateTicket() { setCurrentUser(managerId); - Ticket ticket = ticketService.create(projectId, milestoneId, "Test ticket"); - + var ticket = ticketService.create(projectId, milestoneId, "Test ticket"); + assertNotNull(ticket); assertNotNull(ticket.id()); assertEquals(projectId, ticket.projectId()); @@ -47,77 +47,77 @@ void testCreateTicket() { assertEquals("Test ticket", ticket.description()); assertEquals(TicketStatus.NEW, ticket.status()); } - + @Test void testAssignDeveloper() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); - - Ticket ticket = ticketService.create(projectId, milestoneId, "Ticket"); + + var ticket = ticketService.create(projectId, milestoneId, "Ticket"); ticketService.assignDeveloper(ticket.id(), developerId); - - Ticket updated = ticketRepository.findById(ticket.id()).orElseThrow(); + + var updated = ticketRepository.findById(ticket.id()).orElseThrow(); assertTrue(updated.assignedDevelopers().contains(developerId)); } - + @Test void testAssignDeveloperDoesNotDuplicate() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); - - Ticket ticket = ticketService.create(projectId, milestoneId, "Ticket"); + + var ticket = ticketService.create(projectId, milestoneId, "Ticket"); ticketService.assignDeveloper(ticket.id(), developerId); ticketService.assignDeveloper(ticket.id(), developerId); - - Ticket updated = ticketRepository.findById(ticket.id()).orElseThrow(); + + var updated = ticketRepository.findById(ticket.id()).orElseThrow(); assertEquals(1, updated.assignedDevelopers().size()); } - + @Test void testGetStatus() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); - - Ticket ticket = ticketService.create(projectId, milestoneId, "Ticket"); - TicketStatus status = ticketService.getStatus(ticket.id()); - + + var ticket = ticketService.create(projectId, milestoneId, "Ticket"); + var status = ticketService.getStatus(ticket.id()); + assertEquals(TicketStatus.NEW, status); } - + @Test void testCompleteTicket() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); - Ticket ticket = ticketService.create(projectId, milestoneId, "Ticket"); + var ticket = ticketService.create(projectId, milestoneId, "Ticket"); ticketService.assignDeveloper(ticket.id(), developerId); setCurrentUser(developerId); ticketService.complete(ticket.id()); - - Ticket completed = ticketRepository.findById(ticket.id()).orElseThrow(); + + var completed = ticketRepository.findById(ticket.id()).orElseThrow(); assertEquals(TicketStatus.COMPLETED, completed.status()); } - + @Test void testListByUser() { setCurrentUser(managerId); projectService.addDeveloper(projectId, developerId); - - Ticket ticket1 = ticketService.create(projectId, milestoneId, "Ticket 1"); - Ticket ticket2 = ticketService.create(projectId, milestoneId, "Ticket 2"); - + + var ticket1 = ticketService.create(projectId, milestoneId, "Ticket 1"); + var ticket2 = ticketService.create(projectId, milestoneId, "Ticket 2"); + ticketService.assignDeveloper(ticket1.id(), developerId); ticketService.assignDeveloper(ticket2.id(), developerId); - - List userTickets = ticketService.listByUser(developerId); + + var userTickets = ticketService.listByUser(developerId); assertEquals(2, userTickets.size()); } - + @Test void testTicketNotFound() { setCurrentUser(managerId); - UUID nonExistentId = UUID.randomUUID(); - - assertThrows(TicketNotFoundException.class, + var nonExistentId = UUID.randomUUID(); + + assertThrows(TicketNotFoundException.class, () -> ticketService.getStatus(nonExistentId)); } } diff --git a/src/test/java/org/lab/service/UserServiceTest.java b/src/test/java/org/lab/service/UserServiceTest.java index 7d2823d..428d14a 100644 --- a/src/test/java/org/lab/service/UserServiceTest.java +++ b/src/test/java/org/lab/service/UserServiceTest.java @@ -12,27 +12,27 @@ import static org.junit.jupiter.api.Assertions.*; class UserServiceTest extends TestBase { - + @BeforeEach void setUp() { } - + @Test void testRegister() { - User user = userService.register("John Doe"); - + var user = userService.register("John Doe"); + assertNotNull(user); assertNotNull(user.id()); assertEquals("John Doe", user.name()); assertNotNull(user.createdAt()); assertTrue(user.createdAt().isBefore(LocalDateTime.now().plusSeconds(1))); } - + @Test void testRegisterMultipleUsers() { - User user1 = userService.register("User1"); - User user2 = userService.register("User2"); - + var user1 = userService.register("User1"); + var user2 = userService.register("User2"); + assertNotEquals(user1.id(), user2.id()); assertEquals("User1", user1.name()); assertEquals("User2", user2.name());