diff --git a/README.md b/README.md index 4a80115..19aec56 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/TvkQWWs6) # Features of modern Java # Цели и задачи л/р: diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/org/lab/Main.java b/src/main/java/org/lab/Main.java index 22028ef..ebfdaad 100644 --- a/src/main/java/org/lab/Main.java +++ b/src/main/java/org/lab/Main.java @@ -1,4 +1,71 @@ -void main() { - IO.println("Hello and welcome!"); -} +package org.lab; + +import java.time.LocalDate; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.lab.service.MilestoneService; +import org.lab.service.ProjectRepository; +import org.lab.service.ProjectService; +import org.lab.service.TicketService; +import org.lab.service.UserService; + +public class Main { + public static void main(String[] args) { + try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + + Future dbInitTask = executor.submit(() -> { + System.out.println("Инициализация базы данных..."); + + try { + Thread.sleep(500); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + System.out.println("База данных инициализирована"); + return new ProjectRepository(); + }); + executor.submit(() -> { + try { + System.out.println("Ожидание базы данных..."); + var repo = dbInitTask.get(); + + System.out.println("База данных готова. Приложение запущено"); + + var projectService = new ProjectService(repo); + var userService = new UserService(); + var testUser = userService.register("Manager Viktor"); + var testProj = projectService.createProject("Viktor's project", testUser); + var testProjId = testProj.id(); + System.out.println("Создан проект '" + testProj.name() + "'"); + System.out.println("Роль администратора: " + projectService.getRoleDescription(testProj.id(), + testUser.id())); + + var milestoneService = new MilestoneService(repo); + milestoneService.createMilestone(testProjId, + "First milestone", + LocalDate.now(), + LocalDate.ofYearDay(2026, 2), + testUser); + var firstMilestone = projectService.getProject(testProjId).milestones().getFirst(); + var firstMilestoneId = firstMilestone.id(); + System.out.println("Создан milestone " + firstMilestone.name()); + + var ticketService = new TicketService(repo); + ticketService.createTicket(testProjId, + firstMilestoneId, + "First ticket", + "Ticket desc", + testUser); + var firstTicket = projectService.getProject(testProjId).milestones().getFirst().tickets().getFirst(); + System.out.println("Создан ticket " + firstTicket.title()); + } catch (ExecutionException | InterruptedException e) { + System.err.println("Ошибка в приложении: " + e.getMessage()); + } + }); + + } + } +} 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..cb1547f --- /dev/null +++ b/src/main/java/org/lab/model/BugReport.java @@ -0,0 +1,12 @@ +package org.lab.model; + +public record BugReport( + Long id, + String title, + String description, + Long projectId, + BugStatus status) { + public BugReport withStatus(BugStatus newStatus) { + return new BugReport(id, title, description, projectId, newStatus); + } +} diff --git a/src/main/java/org/lab/model/BugStatus.java b/src/main/java/org/lab/model/BugStatus.java new file mode 100644 index 0000000..9da332e --- /dev/null +++ b/src/main/java/org/lab/model/BugStatus.java @@ -0,0 +1,5 @@ +package org.lab.model; + +public enum BugStatus { + 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..e10e60d --- /dev/null +++ b/src/main/java/org/lab/model/Milestone.java @@ -0,0 +1,17 @@ +package org.lab.model; + +import java.time.LocalDate; +import java.util.List; + +public record Milestone( + Long id, + String name, + LocalDate startDate, + LocalDate endDate, + Long projectId, + MilestoneStatus status, + List tickets) { + public Milestone { + tickets = tickets == null ? List.of() : List.copyOf(tickets); + } +} 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..48a0334 --- /dev/null +++ b/src/main/java/org/lab/model/MilestoneStatus.java @@ -0,0 +1,5 @@ +package org.lab.model; + +public enum MilestoneStatus { + OPEN, 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..c18a38c --- /dev/null +++ b/src/main/java/org/lab/model/Project.java @@ -0,0 +1,25 @@ +package org.lab.model; + +import java.util.List; +import java.util.Optional; + +public record Project( + Long id, + String name, + User manager, + Optional teamLead, + List developers, + List testers, + List milestones, + List bugReports) { + public Project { + developers = developers == null ? List.of() : List.copyOf(developers); + testers = testers == null ? List.of() : List.copyOf(testers); + milestones = milestones == null ? List.of() : List.copyOf(milestones); + bugReports = bugReports == null ? List.of() : List.copyOf(bugReports); + } + + public Project withMilestone(Milestone milestone) { + return this; + } +} diff --git a/src/main/java/org/lab/model/Role.java b/src/main/java/org/lab/model/Role.java new file mode 100644 index 0000000..fcd084e --- /dev/null +++ b/src/main/java/org/lab/model/Role.java @@ -0,0 +1,15 @@ +package org.lab.model; + +public sealed interface Role permits Role.Manager, Role.TeamLead, Role.Developer, Role.Tester { + record Manager() implements Role { + } + + record TeamLead() implements Role { + } + + record Developer() implements Role { + } + + record Tester() implements 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..97457a5 --- /dev/null +++ b/src/main/java/org/lab/model/Ticket.java @@ -0,0 +1,22 @@ +package org.lab.model; + +import java.util.Optional; + +public record Ticket( + Long id, + String title, + String description, + Long projectId, + Long milestoneId, + Optional assignee, + Optional creator, + TicketStatus status) { + public Ticket withStatus(TicketStatus newStatus) { + return new Ticket(id, title, description, projectId, milestoneId, assignee, creator, newStatus); + } + + public Ticket withAssignee(User newAssignee) { + return new Ticket(id, title, description, projectId, milestoneId, Optional.ofNullable(newAssignee), creator, + 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..cbcbc8d --- /dev/null +++ b/src/main/java/org/lab/model/TicketStatus.java @@ -0,0 +1,5 @@ +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..e1fd2a8 --- /dev/null +++ b/src/main/java/org/lab/model/User.java @@ -0,0 +1,4 @@ +package org.lab.model; + +public record User(Long id, String name) { +} diff --git a/src/main/java/org/lab/service/AuthorizationUtils.java b/src/main/java/org/lab/service/AuthorizationUtils.java new file mode 100644 index 0000000..dbd869f --- /dev/null +++ b/src/main/java/org/lab/service/AuthorizationUtils.java @@ -0,0 +1,22 @@ +package org.lab.service; + +import org.lab.model.Project; +import org.lab.model.User; + +public class AuthorizationUtils { + public static boolean isManager(Project p, User u) { + return p.manager().id().equals(u.id()); + } + + public static boolean isTeamLead(Project p, User u) { + return p.teamLead().isPresent() && p.teamLead().get().id().equals(u.id()); + } + + public static boolean isDeveloper(Project p, User u) { + return p.developers().stream().anyMatch(d -> d.id().equals(u.id())); + } + + public static boolean isTester(Project p, User u) { + return p.testers().stream().anyMatch(t -> t.id().equals(u.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..89a5051 --- /dev/null +++ b/src/main/java/org/lab/service/BugReportService.java @@ -0,0 +1,63 @@ +package org.lab.service; + +import org.lab.model.BugReport; +import org.lab.model.BugStatus; +import org.lab.model.Project; +import org.lab.model.User; + +import java.util.ArrayList; +import java.util.List; + +import static org.lab.service.AuthorizationUtils.isDeveloper; +import static org.lab.service.AuthorizationUtils.isTester; + +public class BugReportService { + private final ProjectRepository repository; + + public BugReportService(ProjectRepository repository) { + this.repository = repository; + } + + public List getBugReportsForUser(User user) { + return repository.findAll().stream() + .filter(p -> p.developers().stream().anyMatch(d -> d.id().equals(user.id()))) + .flatMap(p -> p.bugReports().stream()) + .filter(b -> b.status() == BugStatus.NEW) + .toList(); + } + + public void createBugReport(Long projectId, String title, String description, User creator) { + repository.findById(projectId).ifPresent(project -> { + BugReport report = new BugReport(repository.generateId(), title, description, projectId, + BugStatus.NEW); + List newReports = new ArrayList<>(project.bugReports()); + newReports.add(report); + repository.save(new Project(project.id(), project.name(), project.manager(), project.teamLead(), + project.developers(), project.testers(), project.milestones(), newReports)); + }); + } + + public void updateBugStatus(Long projectId, Long bugId, BugStatus newStatus, User user) { + repository.findById(projectId).ifPresent(project -> { + if (isDeveloper(project, user)) { + if (newStatus != BugStatus.FIXED) + throw new IllegalArgumentException("Разработчик может пометить баг только как FIXED"); + } else if (isTester(project, user)) { + if (newStatus != BugStatus.TESTED && newStatus != BugStatus.CLOSED) + throw new IllegalArgumentException("Тестировщик может пометить баг только как TESTED или CLOSED"); + } else { + throw new IllegalArgumentException("Только разработчик или тестировщик может менять статус бага"); + } + + List newReports = project.bugReports().stream() + .map(b -> { + if (b.id().equals(bugId)) { + return b.withStatus(newStatus); + } + return b; + }).toList(); + repository.save(new Project(project.id(), project.name(), project.manager(), project.teamLead(), + project.developers(), project.testers(), project.milestones(), newReports)); + }); + } +} 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..605188b --- /dev/null +++ b/src/main/java/org/lab/service/MilestoneService.java @@ -0,0 +1,67 @@ +package org.lab.service; + +import org.lab.model.*; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import static org.lab.service.AuthorizationUtils.isManager; + +public class MilestoneService { + private final ProjectRepository repository; + + public MilestoneService(ProjectRepository repository) { + this.repository = repository; + } + + public void createMilestone(Long projectId, String name, LocalDate start, LocalDate end, User creator) { + repository.findById(projectId).ifPresent(project -> { + if (!isManager(project, creator)) { + throw new IllegalArgumentException("Только менеджер может создавать milestone"); + } + Milestone milestone = new Milestone(repository.generateId(), name, start, end, projectId, + MilestoneStatus.OPEN, List.of()); + List newMilestones = new ArrayList<>(project.milestones()); + newMilestones.add(milestone); + repository.save(new Project(project.id(), project.name(), project.manager(), project.teamLead(), + project.developers(), project.testers(), newMilestones, project.bugReports())); + }); + } + + public void updateMilestoneStatus(Long projectId, Long milestoneId, MilestoneStatus newStatus, User user) { + repository.findById(projectId).ifPresent(project -> { + if (!isManager(project, user)) { + throw new IllegalArgumentException("Только менеджер может менять статус milestone"); + } + + if (newStatus == MilestoneStatus.ACTIVE) { + boolean hasActive = project.milestones().stream() + .anyMatch(m -> m.status() == MilestoneStatus.ACTIVE && !m.id().equals(milestoneId)); + if (hasActive) { + throw new IllegalStateException( + "Проект уже имеет активный milestone. Закройте или деактивируйте его."); + } + } + + List newMilestones = project.milestones().stream() + .map(m -> { + if (m.id().equals(milestoneId)) { + if (newStatus == MilestoneStatus.CLOSED) { + boolean allTicketsCompleted = m.tickets().stream() + .allMatch(t -> t.status() == TicketStatus.COMPLETED); + if (!allTicketsCompleted) { + throw new IllegalStateException( + "Нельзя закрыть milestone с незавершенными тикетами"); + } + } + return new Milestone(m.id(), m.name(), m.startDate(), m.endDate(), m.projectId(), newStatus, + m.tickets()); + } + return m; + }).toList(); + repository.save(new Project(project.id(), project.name(), project.manager(), project.teamLead(), + project.developers(), project.testers(), newMilestones, project.bugReports())); + }); + } +} diff --git a/src/main/java/org/lab/service/ProjectRepository.java b/src/main/java/org/lab/service/ProjectRepository.java new file mode 100644 index 0000000..f169fc2 --- /dev/null +++ b/src/main/java/org/lab/service/ProjectRepository.java @@ -0,0 +1,30 @@ +package org.lab.service; + +import org.lab.model.Project; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public class ProjectRepository { + private final Map projects = new ConcurrentHashMap<>(); + private final AtomicLong idGenerator = new AtomicLong(1); + + public Project save(Project project) { + projects.put(project.id(), project); + return project; + } + + public Optional findById(Long id) { + return Optional.ofNullable(projects.get(id)); + } + + public long generateId() { + return idGenerator.getAndIncrement(); + } + + public Collection findAll() { + return projects.values(); + } +} 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..1f97ed6 --- /dev/null +++ b/src/main/java/org/lab/service/ProjectService.java @@ -0,0 +1,98 @@ +package org.lab.service; + +import org.lab.model.Project; +import org.lab.model.Role; +import org.lab.model.User; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.lab.service.AuthorizationUtils.*; + +public class ProjectService { + private final ProjectRepository repository; + + public ProjectService(ProjectRepository repository) { + this.repository = repository; + } + + public Project createProject(String name, User creator) { + long id = repository.generateId(); + Project project = new Project(id, name, creator, Optional.empty(), List.of(), List.of(), List.of(), List.of()); + return repository.save(project); + } + + public Project getProject(Long id) { + return repository.findById(id).orElse(null); + } + + public List getProjectsForUser(User user) { + return repository.findAll().stream() + .filter(p -> p.manager().id().equals(user.id()) || + (p.teamLead().isPresent() && p.teamLead().get().id().equals(user.id())) || + p.developers().stream().anyMatch(d -> d.id().equals(user.id())) || + p.testers().stream().anyMatch(t -> t.id().equals(user.id()))) + .toList(); + } + + public void addDeveloper(Long projectId, User developer, User assigner) { + repository.findById(projectId).ifPresent(project -> { + if (!isManager(project, assigner)) { + throw new IllegalArgumentException("Только менеджер может добавлять разработчиков"); + } + List newDevs = new ArrayList<>(project.developers()); + newDevs.add(developer); + repository.save(new Project(project.id(), project.name(), project.manager(), project.teamLead(), newDevs, + project.testers(), project.milestones(), project.bugReports())); + }); + } + + public void addTester(Long projectId, User tester, User assigner) { + repository.findById(projectId).ifPresent(project -> { + if (!isManager(project, assigner)) { + throw new IllegalArgumentException("Только менеджер может добавлять тестировщиков"); + } + List newTesters = new ArrayList<>(project.testers()); + newTesters.add(tester); + repository.save(new Project(project.id(), project.name(), project.manager(), project.teamLead(), + project.developers(), newTesters, project.milestones(), project.bugReports())); + }); + } + + public void setTeamLead(Long projectId, User teamLead, User assigner) { + repository.findById(projectId).ifPresent(project -> { + if (!isManager(project, assigner)) { + throw new IllegalArgumentException("Только менеджер может назначать team lead"); + } + repository.save(new Project(project.id(), project.name(), project.manager(), Optional.of(teamLead), + project.developers(), project.testers(), project.milestones(), project.bugReports())); + }); + } + + public Role getUserRole(Long projectId, Long userId) { + Project p = repository.findById(projectId) + .orElseThrow(() -> new IllegalArgumentException("Проект не найден")); + + if (p.manager().id().equals(userId)) + return new Role.Manager(); + if (p.teamLead().isPresent() && p.teamLead().get().id().equals(userId)) + return new Role.TeamLead(); + if (p.developers().stream().anyMatch(u -> u.id().equals(userId))) + return new Role.Developer(); + if (p.testers().stream().anyMatch(u -> u.id().equals(userId))) + return new Role.Tester(); + + throw new IllegalArgumentException("У пользователя нет роли в этом проекте"); + } + + public String getRoleDescription(Long projectId, Long userId) { + Role role = getUserRole(projectId, userId); + return switch (role) { + case Role.Manager _ -> "Менеджер: Может управлять всем проектом"; + case Role.TeamLead _ -> "Team Lead: Может управлять тикетами"; + case Role.Developer _ -> "Разработчик: Может выполнять тикеты и управлять багами"; + case Role.Tester _ -> "Тестировщик: Может управлять багами"; + }; + } +} 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..edb3fd6 --- /dev/null +++ b/src/main/java/org/lab/service/TicketService.java @@ -0,0 +1,110 @@ +package org.lab.service; + +import org.lab.model.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.lab.service.AuthorizationUtils.*; + +public class TicketService { + private final ProjectRepository repository; + + public TicketService(ProjectRepository repository) { + this.repository = repository; + } + + public List getTicketsForUser(User user) { + return repository.findAll().stream() + .flatMap(p -> p.milestones().stream()) + .flatMap(m -> m.tickets().stream()) + .filter(t -> t.assignee().isPresent() && t.assignee().get().id().equals(user.id())) + .toList(); + } + + public List getTicketsCreatedByUser(User user) { + return repository.findAll().stream() + .flatMap(p -> p.milestones().stream()) + .flatMap(m -> m.tickets().stream()) + .filter(t -> t.creator().isPresent() && t.creator().get().id().equals(user.id())) + .toList(); + } + + public void createTicket(Long projectId, Long milestoneId, String title, String description, User creator) { + repository.findById(projectId).ifPresent(project -> { + boolean isAuthorized = isManager(project, creator) || isTeamLead(project, creator); + if (!isAuthorized) { + throw new IllegalArgumentException("Только менеджер или team lead могут создавать тикеты"); + } + + List newMilestones = project.milestones().stream() + .map(m -> { + if (m.id().equals(milestoneId)) { + Ticket ticket = new Ticket(repository.generateId(), title, description, projectId, + milestoneId, Optional.empty(), Optional.of(creator), TicketStatus.NEW); + List newTickets = new ArrayList<>(m.tickets()); + newTickets.add(ticket); + return new Milestone(m.id(), m.name(), m.startDate(), m.endDate(), m.projectId(), + m.status(), newTickets); + } + return m; + }).toList(); + repository.save(new Project(project.id(), project.name(), project.manager(), project.teamLead(), + project.developers(), project.testers(), newMilestones, project.bugReports())); + }); + } + + public void assignTicket(Long projectId, Long milestoneId, Long ticketId, User developer, User assigner) { + repository.findById(projectId).ifPresent(project -> { + boolean isAuthorized = isManager(project, assigner) || isTeamLead(project, assigner); + if (!isAuthorized) { + throw new IllegalArgumentException("Только менеджер или team lead могут назначить разработчика"); + } + + List newMilestones = project.milestones().stream() + .map(m -> { + if (m.id().equals(milestoneId)) { + List newTickets = m.tickets().stream() + .map(t -> { + if (t.id().equals(ticketId)) { + return t.withAssignee(developer).withStatus(TicketStatus.ACCEPTED); + } + return t; + }).toList(); + return new Milestone(m.id(), m.name(), m.startDate(), m.endDate(), m.projectId(), + m.status(), newTickets); + } + return m; + }).toList(); + repository.save(new Project(project.id(), project.name(), project.manager(), project.teamLead(), + project.developers(), project.testers(), newMilestones, project.bugReports())); + }); + } + + public void updateTicketStatus(Long projectId, Long milestoneId, Long ticketId, TicketStatus newStatus, User user) { + repository.findById(projectId).ifPresent(project -> { + if (!isDeveloper(project, user) && !isManager(project, user) && !isTeamLead(project, user)) { + throw new IllegalArgumentException("Пользователь не имеет права менять статус тикета"); + } + + List newMilestones = project.milestones().stream() + .map(m -> { + if (m.id().equals(milestoneId)) { + List newTickets = m.tickets().stream() + .map(t -> { + if (t.id().equals(ticketId)) { + return t.withStatus(newStatus); + } + return t; + }).toList(); + return new Milestone(m.id(), m.name(), m.startDate(), m.endDate(), m.projectId(), + m.status(), newTickets); + } + return m; + }).toList(); + repository.save(new Project(project.id(), project.name(), project.manager(), project.teamLead(), + project.developers(), project.testers(), newMilestones, project.bugReports())); + }); + } +} 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..60515c8 --- /dev/null +++ b/src/main/java/org/lab/service/UserService.java @@ -0,0 +1,29 @@ +package org.lab.service; + +import org.lab.model.User; + +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public class UserService { + private final Map users = new ConcurrentHashMap<>(); + private final AtomicLong idGenerator = new AtomicLong(1); + + public User register(String name) { + long id = idGenerator.getAndIncrement(); + User user = new User(id, name); + users.put(id, user); + return user; + } + + public Optional getUser(Long id) { + return Optional.ofNullable(users.get(id)); + } + + public Collection getAllUsers() { + return users.values(); + } +} diff --git a/src/test/java/org/lab/ProjectSystemTest.java b/src/test/java/org/lab/ProjectSystemTest.java new file mode 100644 index 0000000..b64b1bc --- /dev/null +++ b/src/test/java/org/lab/ProjectSystemTest.java @@ -0,0 +1,146 @@ +package org.lab; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.lab.model.*; +import org.lab.service.*; + +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class ProjectSystemTest { + + private UserService userService; + private ProjectRepository projectRepository; + private ProjectService projectService; + private MilestoneService milestoneService; + private TicketService ticketService; + private BugReportService bugReportService; + + @BeforeEach + void setUp() { + projectRepository = new ProjectRepository(); + userService = new UserService(); + projectService = new ProjectService(projectRepository); + milestoneService = new MilestoneService(projectRepository); + ticketService = new TicketService(projectRepository); + bugReportService = new BugReportService(projectRepository); + } + + @Test + void testProjectCreationAndTeam() { + User manager = userService.register("Manager Viktor"); + User dev = userService.register("Dev Andrey"); + + Project project = projectService.createProject("Demo project", manager); + assertEquals(manager, project.manager()); + + projectService.addDeveloper(project.id(), dev, manager); + Project p = projectService.getProject(project.id()); + assertTrue(p.developers().stream().anyMatch(d -> d.id().equals(dev.id()))); + + User other = userService.register("Spy"); + assertThrows(IllegalArgumentException.class, () -> projectService.addDeveloper(project.id(), other, dev)); + } + + @Test + void testMilestoneLifecycle() { + User manager = userService.register("Manager Viktor"); + Project project = projectService.createProject("Demo project 1", manager); + LocalDate now = LocalDate.now(); + + milestoneService.createMilestone(project.id(), "Demo milestone 1", now, now, manager); + milestoneService.createMilestone(project.id(), "Demo milestone 2", now, now, manager); + + Project p = projectService.getProject(project.id()); + Long m1Id = p.milestones().get(0).id(); + Long m2Id = p.milestones().get(1).id(); + + milestoneService.updateMilestoneStatus(project.id(), m1Id, MilestoneStatus.ACTIVE, manager); + + assertThrows(IllegalStateException.class, + () -> milestoneService.updateMilestoneStatus(project.id(), m2Id, MilestoneStatus.ACTIVE, manager)); + + User tl = userService.register("Team Lead Kirill"); + projectService.setTeamLead(project.id(), tl, manager); + ticketService.createTicket(project.id(), m1Id, "Task", "Desc", tl); + + assertThrows(IllegalStateException.class, + () -> milestoneService.updateMilestoneStatus(project.id(), m1Id, MilestoneStatus.CLOSED, manager)); + } + + @Test + void testTicketWorkflow() { + User manager = userService.register("Manager Viktor"); + User tl = userService.register("Team Lead Kirill"); + User dev = userService.register("Dev Andrey"); + + Project project = projectService.createProject("Demo project 1", manager); + projectService.addDeveloper(project.id(), dev, manager); + projectService.setTeamLead(project.id(), tl, manager); + + LocalDate now = LocalDate.now(); + milestoneService.createMilestone(project.id(), "Demo milestone 1", now, now, manager); + Long mId = projectService.getProject(project.id()).milestones().get(0).id(); + + ticketService.createTicket(project.id(), mId, "Task 1", "Desc 1", tl); + Ticket ticket = projectService.getProject(project.id()).milestones().get(0).tickets().get(0); + + ticketService.assignTicket(project.id(), mId, ticket.id(), dev, tl); + + ticketService.updateTicketStatus(project.id(), mId, ticket.id(), TicketStatus.IN_PROGRESS, dev); + ticketService.updateTicketStatus(project.id(), mId, ticket.id(), TicketStatus.COMPLETED, dev); + + User tester = userService.register("Tester Vladimir"); + projectService.addTester(project.id(), tester, manager); + assertThrows(IllegalArgumentException.class, + () -> ticketService.updateTicketStatus(project.id(), mId, ticket.id(), TicketStatus.ACCEPTED, tester)); + ticketService.createTicket(project.id(), mId, "Team Lead Task", "Descr 2", manager); + Ticket t2 = projectService.getProject(project.id()).milestones().get(0).tickets().get(1); + ticketService.assignTicket(project.id(), mId, t2.id(), tl, manager); + + ticketService.updateTicketStatus(project.id(), mId, t2.id(), TicketStatus.COMPLETED, tl); + } + + @Test + void testBugReportWorkflow() { + User manager = userService.register("Manager Viktor"); + User dev = userService.register("Dev Andrey"); + User tester = userService.register("Tester Vladimir"); + + Project project = projectService.createProject("Demo project 1", manager); + projectService.addDeveloper(project.id(), dev, manager); + projectService.addTester(project.id(), tester, manager); + + bugReportService.createBugReport(project.id(), "Bug 1", "Desc 1", tester); + + bugReportService.createBugReport(project.id(), "Bug 2", "Desc 2", dev); + + Project p = projectService.getProject(project.id()); + assertEquals(2, p.bugReports().size()); + BugReport b1 = p.bugReports().get(0); + + bugReportService.updateBugStatus(project.id(), b1.id(), BugStatus.FIXED, dev); + + bugReportService.updateBugStatus(project.id(), b1.id(), BugStatus.TESTED, tester); + + List devBugs = bugReportService.getBugReportsForUser(dev); + assertEquals(1, devBugs.size()); + assertEquals("Bug 2", devBugs.get(0).title()); + } + + @Test + void testMultiRole() { + User u = userService.register("User Larisa"); + Project p1 = projectService.createProject("Demo project 1", u); + + User u2 = userService.register("Spy"); + Project p2 = projectService.createProject("Demo project 2", u2); + projectService.addDeveloper(p2.id(), u, u2); + + assertEquals(new Role.Manager(), projectService.getUserRole(p1.id(), u.id())); + assertEquals(new Role.Developer(), projectService.getUserRole(p2.id(), u.id())); + } +}