From 22d828c542466fc5f110d9dde68b3c416db4c064 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 11:11:33 +0000 Subject: [PATCH 1/4] add deadline --- README.md | 1 + 1 file changed, 1 insertion(+) 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 # Цели и задачи л/р: From 90e69a49c01a24f71c30b99c1ac221b2f17c1bbf Mon Sep 17 00:00:00 2001 From: Viktor Shamin Date: Wed, 24 Dec 2025 20:29:17 +0300 Subject: [PATCH 2/4] First version --- src/main/java/org/lab/Main.java | 52 ++++++- src/main/java/org/lab/model/BugReport.java | 12 ++ src/main/java/org/lab/model/BugStatus.java | 5 + src/main/java/org/lab/model/Milestone.java | 17 ++ .../java/org/lab/model/MilestoneStatus.java | 5 + src/main/java/org/lab/model/Project.java | 25 +++ src/main/java/org/lab/model/Role.java | 15 ++ src/main/java/org/lab/model/Ticket.java | 22 +++ src/main/java/org/lab/model/TicketStatus.java | 5 + src/main/java/org/lab/model/User.java | 4 + .../org/lab/service/AuthorizationUtils.java | 22 +++ .../org/lab/service/BugReportService.java | 63 ++++++++ .../org/lab/service/MilestoneService.java | 67 ++++++++ .../org/lab/service/ProjectRepository.java | 30 ++++ .../java/org/lab/service/ProjectService.java | 98 ++++++++++++ .../java/org/lab/service/TicketService.java | 110 +++++++++++++ .../java/org/lab/service/UserService.java | 29 ++++ src/test/java/org/lab/ProjectSystemTest.java | 146 ++++++++++++++++++ 18 files changed, 724 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/lab/model/BugReport.java create mode 100644 src/main/java/org/lab/model/BugStatus.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/Role.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/service/AuthorizationUtils.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/ProjectRepository.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 create mode 100644 src/test/java/org/lab/ProjectSystemTest.java diff --git a/src/main/java/org/lab/Main.java b/src/main/java/org/lab/Main.java index 22028ef..d6ca133 100644 --- a/src/main/java/org/lab/Main.java +++ b/src/main/java/org/lab/Main.java @@ -1,4 +1,50 @@ -void main() { - IO.println("Hello and welcome!"); -} +package org.lab; + +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import org.lab.service.ProjectRepository; +import org.lab.service.ProjectService; +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("Ожидание базы данных..."); + ProjectRepository repo = dbInitTask.get(); + System.out.println("База данных готова. Приложение запущено"); + + ProjectService service = new ProjectService(repo); + UserService userService = new UserService(); + + var userAdmin = userService.register("Admin"); + var proj = service.createProject("Demo project", userAdmin); + + System.out.println("Создан проект '" + proj.name() + "'"); + System.out.println("Роль администратора: " + service.getRoleDescription(proj.id(), userAdmin.id())); + + } 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..0b63286 --- /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 Bob"); + User dev = userService.register("Dev John"); + + Project project = projectService.createProject("Enterprise System", 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("Other"); + assertThrows(IllegalArgumentException.class, () -> projectService.addDeveloper(project.id(), other, dev)); + } + + @Test + void testMilestoneLifecycle() { + User manager = userService.register("Manager"); + Project project = projectService.createProject("P1", manager); + LocalDate now = LocalDate.now(); + + milestoneService.createMilestone(project.id(), "M1", now, now, manager); + milestoneService.createMilestone(project.id(), "M2", 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("TL"); + 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("Mgr"); + User tl = userService.register("TL"); + User dev = userService.register("Dev"); + + Project project = projectService.createProject("P1", manager); + projectService.addDeveloper(project.id(), dev, manager); + projectService.setTeamLead(project.id(), tl, manager); + + LocalDate now = LocalDate.now(); + milestoneService.createMilestone(project.id(), "M1", now, now, manager); + Long mId = projectService.getProject(project.id()).milestones().get(0).id(); + + ticketService.createTicket(project.id(), mId, "Task 1", "D", 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"); + projectService.addTester(project.id(), tester, manager); + assertThrows(IllegalArgumentException.class, + () -> ticketService.updateTicketStatus(project.id(), mId, ticket.id(), TicketStatus.ACCEPTED, tester)); + ticketService.createTicket(project.id(), mId, "TL Task", "D", 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("Mgr"); + User dev = userService.register("Dev"); + User tester = userService.register("Tester"); + + Project project = projectService.createProject("P1", manager); + projectService.addDeveloper(project.id(), dev, manager); + projectService.addTester(project.id(), tester, manager); + + bugReportService.createBugReport(project.id(), "Bug 1", "Desc", tester); + + bugReportService.createBugReport(project.id(), "Bug 2", "Desc", 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("Multi"); + Project p1 = projectService.createProject("P1", u); + + User u2 = userService.register("Other"); + Project p2 = projectService.createProject("P2", 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())); + } +} From 0a5b6f04360dbe63cc4b5d8c24628741e3ad6847 Mon Sep 17 00:00:00 2001 From: Viktor Shamin Date: Thu, 25 Dec 2025 14:55:46 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=D1=8B=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/org/lab/ProjectSystemTest.java | 54 ++++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/test/java/org/lab/ProjectSystemTest.java b/src/test/java/org/lab/ProjectSystemTest.java index 0b63286..b64b1bc 100644 --- a/src/test/java/org/lab/ProjectSystemTest.java +++ b/src/test/java/org/lab/ProjectSystemTest.java @@ -31,28 +31,28 @@ void setUp() { @Test void testProjectCreationAndTeam() { - User manager = userService.register("Manager Bob"); - User dev = userService.register("Dev John"); + User manager = userService.register("Manager Viktor"); + User dev = userService.register("Dev Andrey"); - Project project = projectService.createProject("Enterprise System", manager); + 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("Other"); + User other = userService.register("Spy"); assertThrows(IllegalArgumentException.class, () -> projectService.addDeveloper(project.id(), other, dev)); } @Test void testMilestoneLifecycle() { - User manager = userService.register("Manager"); - Project project = projectService.createProject("P1", manager); + User manager = userService.register("Manager Viktor"); + Project project = projectService.createProject("Demo project 1", manager); LocalDate now = LocalDate.now(); - milestoneService.createMilestone(project.id(), "M1", now, now, manager); - milestoneService.createMilestone(project.id(), "M2", now, now, manager); + 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(); @@ -63,7 +63,7 @@ void testMilestoneLifecycle() { assertThrows(IllegalStateException.class, () -> milestoneService.updateMilestoneStatus(project.id(), m2Id, MilestoneStatus.ACTIVE, manager)); - User tl = userService.register("TL"); + User tl = userService.register("Team Lead Kirill"); projectService.setTeamLead(project.id(), tl, manager); ticketService.createTicket(project.id(), m1Id, "Task", "Desc", tl); @@ -73,19 +73,19 @@ void testMilestoneLifecycle() { @Test void testTicketWorkflow() { - User manager = userService.register("Mgr"); - User tl = userService.register("TL"); - User dev = userService.register("Dev"); + User manager = userService.register("Manager Viktor"); + User tl = userService.register("Team Lead Kirill"); + User dev = userService.register("Dev Andrey"); - Project project = projectService.createProject("P1", manager); + 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(), "M1", now, now, manager); + 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", "D", tl); + 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); @@ -93,11 +93,11 @@ void testTicketWorkflow() { 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"); + 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, "TL Task", "D", manager); + 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); @@ -106,17 +106,17 @@ void testTicketWorkflow() { @Test void testBugReportWorkflow() { - User manager = userService.register("Mgr"); - User dev = userService.register("Dev"); - User tester = userService.register("Tester"); + User manager = userService.register("Manager Viktor"); + User dev = userService.register("Dev Andrey"); + User tester = userService.register("Tester Vladimir"); - Project project = projectService.createProject("P1", manager); + 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", tester); + bugReportService.createBugReport(project.id(), "Bug 1", "Desc 1", tester); - bugReportService.createBugReport(project.id(), "Bug 2", "Desc", dev); + bugReportService.createBugReport(project.id(), "Bug 2", "Desc 2", dev); Project p = projectService.getProject(project.id()); assertEquals(2, p.bugReports().size()); @@ -133,11 +133,11 @@ void testBugReportWorkflow() { @Test void testMultiRole() { - User u = userService.register("Multi"); - Project p1 = projectService.createProject("P1", u); + User u = userService.register("User Larisa"); + Project p1 = projectService.createProject("Demo project 1", u); - User u2 = userService.register("Other"); - Project p2 = projectService.createProject("P2", u2); + 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())); From 70dc6029475db9de0d67ebb1cf04f4743475ff8f Mon Sep 17 00:00:00 2001 From: Viktor Shamin Date: Thu, 25 Dec 2025 15:35:48 +0300 Subject: [PATCH 4/4] =?UTF-8?q?=D0=9F=D0=BE=D1=84=D0=B8=D0=BA=D1=81=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B1=D0=B0=D0=B3=20=D1=81=20=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D1=8B=D0=BC=D0=B8=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=B0=20project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradlew | 0 src/main/java/org/lab/Main.java | 39 +++++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) mode change 100644 => 100755 gradlew 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 d6ca133..ebfdaad 100644 --- a/src/main/java/org/lab/Main.java +++ b/src/main/java/org/lab/Main.java @@ -1,11 +1,14 @@ package org.lab; -import java.util.concurrent.Executors; +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 { @@ -27,19 +30,37 @@ public static void main(String[] args) { executor.submit(() -> { try { System.out.println("Ожидание базы данных..."); - ProjectRepository repo = dbInitTask.get(); + var repo = dbInitTask.get(); System.out.println("База данных готова. Приложение запущено"); - ProjectService service = new ProjectService(repo); - UserService userService = new UserService(); - - var userAdmin = userService.register("Admin"); - var proj = service.createProject("Demo project", userAdmin); + 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())); - System.out.println("Создан проект '" + proj.name() + "'"); - System.out.println("Роль администратора: " + service.getRoleDescription(proj.id(), userAdmin.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()); }