diff --git a/lib/gson-2.9.0.jar b/lib/gson-2.9.0.jar new file mode 100644 index 0000000..fb62e05 Binary files /dev/null and b/lib/gson-2.9.0.jar differ diff --git a/src/http/HttpTaskServer.java b/src/http/HttpTaskServer.java new file mode 100644 index 0000000..9dd61c8 --- /dev/null +++ b/src/http/HttpTaskServer.java @@ -0,0 +1,76 @@ +package http; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.sun.net.httpserver.HttpServer; +import http.adapter.DurationAdapter; +import http.adapter.LocalDateTimeAdapter; +import http.handler.EpicsHandler; +import http.handler.HistoryHandler; +import http.handler.PrioritizedHandler; +import http.handler.SubtasksHandler; +import http.handler.TasksHandler; +import service.Managers; +import service.TaskManager; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.time.Duration; +import java.time.LocalDateTime; + +public class HttpTaskServer { + + // Порт, на котором будет работать сервер + private static final int PORT = 8080; + + // Экземпляр HTTP-сервера из стандартной библиотеки Java + private final HttpServer httpServer; + + // Менеджер задач, с которым будут работать обработчики запросов + private final TaskManager taskManager; + + // Gson нужен для преобразования объектов Java в JSON и обратно + private static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(Duration.class, new DurationAdapter()) + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter()) + .create(); + + // Конструктор для обычного запуска приложения и для тестов + public HttpTaskServer(TaskManager taskManager) throws IOException { + this.taskManager = taskManager; + this.httpServer = HttpServer.create(new InetSocketAddress(PORT), 0); + + // Регистрируем обработчики для каждого базового пути API + httpServer.createContext("/tasks", new TasksHandler(this.taskManager)); + httpServer.createContext("/subtasks", new SubtasksHandler(this.taskManager)); + httpServer.createContext("/epics", new EpicsHandler(this.taskManager)); + httpServer.createContext("/history", new HistoryHandler(this.taskManager)); + httpServer.createContext("/prioritized", new PrioritizedHandler(this.taskManager)); + } + + // Метод запуска сервера + public void start() { + httpServer.start(); + System.out.println("HTTP task server started on port " + PORT); + } + + // Метод остановки сервера + public void stop() { + httpServer.stop(0); + System.out.println("HTTP task server stopped on port " + PORT); + } + + // Общий Gson для всего HTTP-слоя + public static Gson getGson() { + return GSON; + } + + // Точка входа в приложение + public static void main(String[] args) throws IOException { + TaskManager manager = Managers.getDefault(); + HttpTaskServer httpTaskServer = new HttpTaskServer(manager); + httpTaskServer.start(); + } + + +} \ No newline at end of file diff --git a/src/http/adapter/DurationAdapter.java b/src/http/adapter/DurationAdapter.java new file mode 100644 index 0000000..9712624 --- /dev/null +++ b/src/http/adapter/DurationAdapter.java @@ -0,0 +1,38 @@ +package http.adapter; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; +import java.time.Duration; + +// Адаптер для преобразования Duration в JSON и обратно. +// В JSON будет храниться продолжительность как количество минут. +public class DurationAdapter implements JsonSerializer, JsonDeserializer { + + // Преобразование объекта Duration в JSON + @Override + public JsonElement serialize(Duration duration, Type typeOfSrc, JsonSerializationContext context) { + if (duration == null) { + return null; + } + + return new JsonPrimitive(duration.toMinutes()); + } + + // Преобразование JSON обратно в объект Duration + @Override + public Duration deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + if (json == null || json.isJsonNull()) { + return null; + } + + return Duration.ofMinutes(json.getAsLong()); + } +} \ No newline at end of file diff --git a/src/http/adapter/LocalDateTimeAdapter.java b/src/http/adapter/LocalDateTimeAdapter.java new file mode 100644 index 0000000..d4041d2 --- /dev/null +++ b/src/http/adapter/LocalDateTimeAdapter.java @@ -0,0 +1,38 @@ +package http.adapter; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; +import java.time.LocalDateTime; + +// Адаптер для преобразования LocalDateTime в JSON и обратно. +// В JSON дата и время будут храниться в строковом ISO-формате. +public class LocalDateTimeAdapter implements JsonSerializer, JsonDeserializer { + + // Преобразование LocalDateTime в JSON + @Override + public JsonElement serialize(LocalDateTime localDateTime, Type typeOfSrc, JsonSerializationContext context) { + if (localDateTime == null) { + return null; + } + + return new JsonPrimitive(localDateTime.toString()); + } + + // Преобразование JSON обратно в LocalDateTime + @Override + public LocalDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + if (json == null || json.isJsonNull()) { + return null; + } + + return LocalDateTime.parse(json.getAsString()); + } +} \ No newline at end of file diff --git a/src/http/handler/BaseHttpHandler.java b/src/http/handler/BaseHttpHandler.java new file mode 100644 index 0000000..1cb3bcf --- /dev/null +++ b/src/http/handler/BaseHttpHandler.java @@ -0,0 +1,57 @@ +package http.handler; + +import com.sun.net.httpserver.HttpExchange; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +// Базовый класс для всех HTTP-обработчиков. +// Содержит общие методы для отправки и чтения HTTP-данных. +public class BaseHttpHandler { + + // HTTP-статусы, используемые в обработчиках. + protected static final int STATUS_OK = 200; + protected static final int STATUS_CREATED = 201; + protected static final int STATUS_NOT_FOUND = 404; + protected static final int STATUS_NOT_ACCEPTABLE = 406; + protected static final int STATUS_INTERNAL_ERROR = 500; + + // Отправка ответа с текстом в формате JSON. + protected void sendText(HttpExchange exchange, String text, int statusCode) throws IOException { + byte[] response = text.getBytes(StandardCharsets.UTF_8); + + exchange.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); + exchange.sendResponseHeaders(statusCode, response.length); + exchange.getResponseBody().write(response); + exchange.close(); + } + + // Отправка ответа 201 без тела. + protected void sendCreated(HttpExchange exchange) throws IOException { + exchange.sendResponseHeaders(STATUS_CREATED, -1); + exchange.close(); + } + + // Чтение тела HTTP-запроса в виде строки. + protected String readText(HttpExchange exchange) throws IOException { + return new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + } + + // Ответ 404 — объект не найден. + protected void sendNotFound(HttpExchange exchange) throws IOException { + exchange.sendResponseHeaders(STATUS_NOT_FOUND, -1); + exchange.close(); + } + + // Ответ 406 — задача пересекается по времени. + protected void sendHasInteractions(HttpExchange exchange) throws IOException { + exchange.sendResponseHeaders(STATUS_NOT_ACCEPTABLE, -1); + exchange.close(); + } + + // Ответ 500 — внутренняя ошибка сервера. + protected void sendInternalError(HttpExchange exchange) throws IOException { + exchange.sendResponseHeaders(STATUS_INTERNAL_ERROR, -1); + exchange.close(); + } +} \ No newline at end of file diff --git a/src/http/handler/EpicsHandler.java b/src/http/handler/EpicsHandler.java new file mode 100644 index 0000000..8d4b96b --- /dev/null +++ b/src/http/handler/EpicsHandler.java @@ -0,0 +1,153 @@ +package http.handler; + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import http.HttpTaskServer; +import model.Epic; +import model.Subtask; +import service.TaskManager; +import service.exception.NotFoundException; + +import java.io.IOException; +import java.util.List; + +// Обработчик запросов по пути /epics. +// Поддерживает получение списка эпиков, получение эпика по id, +// получение подзадач эпика, создание/обновление эпика и удаление эпика. +public class EpicsHandler extends BaseHttpHandler implements HttpHandler { + + // Менеджер задач, с которым работает обработчик. + private final TaskManager taskManager; + + // Общий Gson для преобразования объектов в JSON и обратно. + private final Gson gson = HttpTaskServer.getGson(); + + public EpicsHandler(TaskManager taskManager) { + this.taskManager = taskManager; + } + + // Главный метод обработки HTTP-запроса. + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + String method = exchange.getRequestMethod(); + String path = exchange.getRequestURI().getPath(); + + if ("GET".equals(method)) { + handleGet(exchange, path); + return; + } + + if ("POST".equals(method)) { + handlePost(exchange, path); + return; + } + + if ("DELETE".equals(method)) { + handleDelete(exchange, path); + return; + } + + sendInternalError(exchange); + + } catch (NotFoundException e) { + sendNotFound(exchange); + } catch (IllegalArgumentException e) { + sendHasInteractions(exchange); + } catch (Exception e) { + sendInternalError(exchange); + } + } + + // Обработка GET-запросов: + // GET /epics -> список всех эпиков + // GET /epics/{id} -> один эпик по id + // GET /epics/{id}/subtasks -> список подзадач эпика + private void handleGet(HttpExchange exchange, String path) throws IOException { + if ("/epics".equals(path)) { + List epics = taskManager.getEpics(); + String response = gson.toJson(epics); + sendText(exchange, response, STATUS_OK); + return; + } + + if (path.endsWith("/subtasks")) { + int epicId = extractEpicIdForSubtasks(path); + List subtasks = taskManager.getEpicSubtasks(epicId); + String response = gson.toJson(subtasks); + sendText(exchange, response, STATUS_OK); + return; + } + + int id = extractId(path); + Epic epic = taskManager.getEpic(id); + String response = gson.toJson(epic); + sendText(exchange, response, STATUS_OK); + } + + // Обработка POST-запроса: + // POST /epics -> создание нового эпика или обновление существующего. + private void handlePost(HttpExchange exchange, String path) throws IOException { + if (!"/epics".equals(path)) { + throw new NotFoundException("Некорректный путь для POST /epics"); + } + + String body = readText(exchange); + Epic epic = gson.fromJson(body, Epic.class); + + if (epic == null) { + throw new IOException("Тело запроса пустое."); + } + + if (epic.getId() == 0) { + taskManager.createEpic(epic); + } else { + taskManager.updateEpic(epic); + } + + sendCreated(exchange); + } + + // Обработка DELETE-запроса: + // DELETE /epics/{id} -> удаление эпика по id. + private void handleDelete(HttpExchange exchange, String path) throws IOException { + int id = extractId(path); + taskManager.deleteEpic(id); + sendText(exchange, "", STATUS_OK); + } + + // Извлечение id эпика из пути вида /epics/{id}. + private int extractId(String path) { + String[] pathParts = path.split("/"); + + if (pathParts.length != 3) { + throw new NotFoundException("Некорректный путь запроса."); + } + + try { + return Integer.parseInt(pathParts[2]); + } catch (NumberFormatException e) { + throw new NotFoundException("Некорректный идентификатор эпика."); + } + } + + // Извлечение id эпика из пути вида /epics/{id}/subtasks. + private int extractEpicIdForSubtasks(String path) { + String[] pathParts = path.split("/"); + + if (pathParts.length != 4) { + throw new NotFoundException("Некорректный путь запроса."); + } + + if (!"subtasks".equals(pathParts[3])) { + throw new NotFoundException("Некорректный путь запроса."); + } + + try { + return Integer.parseInt(pathParts[2]); + } catch (NumberFormatException e) { + throw new NotFoundException("Некорректный идентификатор эпика."); + } + } +} \ No newline at end of file diff --git a/src/http/handler/HistoryHandler.java b/src/http/handler/HistoryHandler.java new file mode 100644 index 0000000..8e2902d --- /dev/null +++ b/src/http/handler/HistoryHandler.java @@ -0,0 +1,52 @@ +package http.handler; + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import http.HttpTaskServer; +import model.Task; +import service.TaskManager; + +import java.io.IOException; +import java.util.List; + +// Обработчик запросов по пути /history. +// Поддерживает получение истории просмотров задач. +public class HistoryHandler extends BaseHttpHandler implements HttpHandler { + + // Менеджер задач, с которым работает обработчик. + private final TaskManager taskManager; + + // Общий Gson для преобразования объектов в JSON. + private final Gson gson = HttpTaskServer.getGson(); + + public HistoryHandler(TaskManager taskManager) { + this.taskManager = taskManager; + } + + // Главный метод обработки HTTP-запроса. + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + String method = exchange.getRequestMethod(); + String path = exchange.getRequestURI().getPath(); + + if (!"GET".equals(method)) { + sendInternalError(exchange); + return; + } + + if (!"/history".equals(path)) { + sendNotFound(exchange); + return; + } + + List history = taskManager.getHistory(); + String response = gson.toJson(history); + sendText(exchange, response, STATUS_OK); + + } catch (Exception e) { + sendInternalError(exchange); + } + } +} \ No newline at end of file diff --git a/src/http/handler/PrioritizedHandler.java b/src/http/handler/PrioritizedHandler.java new file mode 100644 index 0000000..5d451f1 --- /dev/null +++ b/src/http/handler/PrioritizedHandler.java @@ -0,0 +1,52 @@ +package http.handler; + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import http.HttpTaskServer; +import model.Task; +import service.TaskManager; + +import java.io.IOException; +import java.util.List; + +// Обработчик запросов по пути /prioritized. +// Поддерживает получение списка задач в порядке приоритета. +public class PrioritizedHandler extends BaseHttpHandler implements HttpHandler { + + // Менеджер задач, с которым работает обработчик. + private final TaskManager taskManager; + + // Общий Gson для преобразования объектов в JSON. + private final Gson gson = HttpTaskServer.getGson(); + + public PrioritizedHandler(TaskManager taskManager) { + this.taskManager = taskManager; + } + + // Главный метод обработки HTTP-запроса. + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + String method = exchange.getRequestMethod(); + String path = exchange.getRequestURI().getPath(); + + if (!"GET".equals(method)) { + sendInternalError(exchange); + return; + } + + if (!"/prioritized".equals(path)) { + sendNotFound(exchange); + return; + } + + List prioritizedTasks = taskManager.getPrioritizedTasks(); + String response = gson.toJson(prioritizedTasks); + sendText(exchange, response, STATUS_OK); + + } catch (Exception e) { + sendInternalError(exchange); + } + } +} \ No newline at end of file diff --git a/src/http/handler/SubtasksHandler.java b/src/http/handler/SubtasksHandler.java new file mode 100644 index 0000000..e8b6892 --- /dev/null +++ b/src/http/handler/SubtasksHandler.java @@ -0,0 +1,125 @@ +package http.handler; + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import http.HttpTaskServer; +import model.Subtask; +import service.TaskManager; +import service.exception.NotFoundException; + +import java.io.IOException; +import java.util.List; + +// Обработчик запросов по пути /subtasks. +// Поддерживает получение списка подзадач, получение подзадачи по id, +// создание/обновление подзадачи и удаление подзадачи. +public class SubtasksHandler extends BaseHttpHandler implements HttpHandler { + + // Менеджер задач, с которым работает обработчик. + private final TaskManager taskManager; + + // Общий Gson для преобразования объектов в JSON и обратно. + private final Gson gson = HttpTaskServer.getGson(); + + public SubtasksHandler(TaskManager taskManager) { + this.taskManager = taskManager; + } + + // Главный метод обработки HTTP-запроса. + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + String method = exchange.getRequestMethod(); + String path = exchange.getRequestURI().getPath(); + + if ("GET".equals(method)) { + handleGet(exchange, path); + return; + } + + if ("POST".equals(method)) { + handlePost(exchange, path); + return; + } + + if ("DELETE".equals(method)) { + handleDelete(exchange, path); + return; + } + + sendInternalError(exchange); + + } catch (NotFoundException e) { + sendNotFound(exchange); + } catch (IllegalArgumentException e) { + sendHasInteractions(exchange); + } catch (Exception e) { + sendInternalError(exchange); + } + } + + // Обработка GET-запросов: + // GET /subtasks -> список всех подзадач + // GET /subtasks/{id} -> одна подзадача по id + private void handleGet(HttpExchange exchange, String path) throws IOException { + if ("/subtasks".equals(path)) { + List subtasks = taskManager.getSubtasks(); + String response = gson.toJson(subtasks); + sendText(exchange, response, STATUS_OK); + return; + } + + int id = extractId(path); + Subtask subtask = taskManager.getSubtask(id); + String response = gson.toJson(subtask); + sendText(exchange, response, STATUS_OK); + } + + // Обработка POST-запроса: + // POST /subtasks -> создание новой подзадачи или обновление существующей. + private void handlePost(HttpExchange exchange, String path) throws IOException { + if (!"/subtasks".equals(path)) { + throw new NotFoundException("Некорректный путь для POST /subtasks"); + } + + String body = readText(exchange); + Subtask subtask = gson.fromJson(body, Subtask.class); + + if (subtask == null) { + throw new IOException("Тело запроса пустое."); + } + + if (subtask.getId() == 0) { + taskManager.createSubtask(subtask); + } else { + taskManager.updateSubtask(subtask); + } + + sendCreated(exchange); + } + + // Обработка DELETE-запроса: + // DELETE /subtasks/{id} -> удаление подзадачи по id. + private void handleDelete(HttpExchange exchange, String path) throws IOException { + int id = extractId(path); + taskManager.deleteSubtask(id); + sendText(exchange, "", STATUS_OK); + } + + // Извлечение id подзадачи из пути. + // Ожидается путь вида /subtasks/{id}. + private int extractId(String path) { + String[] pathParts = path.split("/"); + + if (pathParts.length != 3) { + throw new NotFoundException("Некорректный путь запроса."); + } + + try { + return Integer.parseInt(pathParts[2]); + } catch (NumberFormatException e) { + throw new NotFoundException("Некорректный идентификатор подзадачи."); + } + } +} \ No newline at end of file diff --git a/src/http/handler/TasksHandler.java b/src/http/handler/TasksHandler.java new file mode 100644 index 0000000..c10e538 --- /dev/null +++ b/src/http/handler/TasksHandler.java @@ -0,0 +1,125 @@ +package http.handler; + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import http.HttpTaskServer; +import model.Task; +import service.TaskManager; +import service.exception.NotFoundException; + +import java.io.IOException; +import java.util.List; + +// Обработчик запросов по пути /tasks. +// Поддерживает получение списка задач, получение задачи по id, +// создание/обновление задачи и удаление задачи. +public class TasksHandler extends BaseHttpHandler implements HttpHandler { + + // Менеджер задач, с которым работает обработчик. + private final TaskManager taskManager; + + // Общий Gson для преобразования объектов в JSON и обратно. + private final Gson gson = HttpTaskServer.getGson(); + + public TasksHandler(TaskManager taskManager) { + this.taskManager = taskManager; + } + + // Главный метод обработчика HTTP-запроса. + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + String method = exchange.getRequestMethod(); + String path = exchange.getRequestURI().getPath(); + + if ("GET".equals(method)) { + handleGet(exchange, path); + return; + } + + if ("POST".equals(method)) { + handlePost(exchange, path); + return; + } + + if ("DELETE".equals(method)) { + handleDelete(exchange, path); + return; + } + + sendInternalError(exchange); + + } catch (NotFoundException e) { + sendNotFound(exchange); + } catch (IllegalArgumentException e) { + sendHasInteractions(exchange); + } catch (Exception e) { + sendInternalError(exchange); + } + } + + // Обработка GET-запросов: + // GET /tasks -> список всех задач + // GET /tasks/{id} -> одна задача по id + private void handleGet(HttpExchange exchange, String path) throws IOException { + if ("/tasks".equals(path)) { + List tasks = taskManager.getTasks(); + String response = gson.toJson(tasks); + sendText(exchange, response, STATUS_OK); + return; + } + + int id = extractId(path); + Task task = taskManager.getTask(id); + String response = gson.toJson(task); + sendText(exchange, response, STATUS_OK); + } + + // Обработка POST-запроса: + // POST /tasks -> создание новой задачи или обновление существующей. + private void handlePost(HttpExchange exchange, String path) throws IOException { + if (!"/tasks".equals(path)) { + throw new NotFoundException("Некорректный путь для POST /tasks"); + } + + String body = readText(exchange); + Task task = gson.fromJson(body, Task.class); + + if (task == null) { + throw new IOException("Тело запроса пустое."); + } + + if (task.getId() == 0) { + taskManager.createTask(task); + } else { + taskManager.updateTask(task); + } + + sendCreated(exchange); + } + + // Обработка DELETE-запроса: + // DELETE /tasks/{id} -> удаление задачи по id. + private void handleDelete(HttpExchange exchange, String path) throws IOException { + int id = extractId(path); + taskManager.deleteTask(id); + sendText(exchange, "", STATUS_OK); + } + + // Извлечение id задачи из пути. + // Ожидается путь вида /tasks/{id}. + private int extractId(String path) { + String[] pathParts = path.split("/"); + + if (pathParts.length != 3) { + throw new NotFoundException("Некорректный путь запроса."); + } + + try { + return Integer.parseInt(pathParts[2]); + } catch (NumberFormatException e) { + throw new NotFoundException("Некорректный идентификатор задачи."); + } + } +} \ No newline at end of file diff --git a/src/model/Epic.java b/src/model/Epic.java index 2cd2cae..239719b 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -1,9 +1,12 @@ package model; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.ArrayList; public class Epic extends Task { private ArrayList subtaskIds = new ArrayList<>(); + private LocalDateTime endTime; public Epic(String name, String description) { super(name, description, Status.NEW); @@ -34,6 +37,23 @@ public void removeSubtaskId(Integer subtaskId) { subtaskIds.remove(subtaskId); } + public void setEpicDuration(Duration duration) { + setDuration(duration); + } + + public void setEpicStartTime(LocalDateTime startTime) { + setStartTime(startTime); + } + + public void setEpicEndTime(LocalDateTime endTime) { + this.endTime = endTime; + } + + @Override + public LocalDateTime getEndTime() { + return endTime; + } + @Override public TaskType getType() { return TaskType.EPIC; @@ -41,12 +61,15 @@ public TaskType getType() { @Override public String toString() { - return "Epic{" + - "id=" + getId() + - ", name='" + getName() + '\'' + - ", description='" + getDescription() + '\'' + - ", status=" + getStatus() + - ", subtaskIds=" + subtaskIds + - '}'; - } -} + return "Epic{" + + "id=" + getId() + + ", name='" + getName() + '\'' + + ", description='" + getDescription() + '\'' + + ", status=" + getStatus() + + ", duration=" + getDuration() + + ", startTime=" + getStartTime() + + ", endTime=" + getEndTime() + + ", subtaskIds=" + subtaskIds + + '}'; + } +} \ No newline at end of file diff --git a/src/model/Subtask.java b/src/model/Subtask.java index e888412..0e2d5a6 100644 --- a/src/model/Subtask.java +++ b/src/model/Subtask.java @@ -1,12 +1,19 @@ package model; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.Objects; public class Subtask extends Task { private int epicId; public Subtask(String name, String description, Status status, int epicId) { - super(name, description, status); + this(name, description, status, null, null, epicId); + } + + public Subtask(String name, String description, Status status, + Duration duration, LocalDateTime startTime, int epicId) { + super(name, description, status, duration, startTime); this.epicId = epicId; } @@ -49,12 +56,15 @@ public int hashCode() { @Override public String toString() { - return "Subtask{" + - "id=" + getId() + - ", name='" + getName() + '\'' + - ", description='" + getDescription() + '\'' + - ", status=" + getStatus() + - ", epicId=" + epicId + - '}'; + return "Subtask{" + + "id=" + getId() + + ", name='" + getName() + '\'' + + ", description='" + getDescription() + '\'' + + ", status=" + getStatus() + + ", duration=" + getDuration() + + ", startTime=" + getStartTime() + + ", endTime=" + getEndTime() + + ", epicId=" + epicId + + '}'; } } diff --git a/src/model/Task.java b/src/model/Task.java index 0e96c0f..da0ffb8 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -1,5 +1,7 @@ package model; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.Objects; public class Task { @@ -7,11 +9,19 @@ public class Task { private String name; private String description; private Status status; + private Duration duration; + private LocalDateTime startTime; public Task(String name, String description, Status status) { + this(name, description, status, null, null); + } + + public Task(String name, String description, Status status, Duration duration, LocalDateTime startTime) { this.name = name; this.description = description; this.status = status; + this.duration = duration; + this.startTime = startTime; } public TaskType getType() { @@ -50,6 +60,29 @@ public void setStatus(Status status) { this.status = status; } + public Duration getDuration() { + return duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public LocalDateTime getEndTime() { + if (startTime == null || duration == null) { + return null; + } + return startTime.plus(duration); + } + // Переопределение методов equals и hashCode для ID @Override public boolean equals(Object obj) { @@ -71,6 +104,9 @@ public String toString() { + ", name='" + name + '\'' + ", description='" + description + '\'' + ", status=" + status + + ", duration=" + duration + + ", startTime=" + startTime + + ", endTime=" + getEndTime() + '}'; } } diff --git a/src/service/FileBackedTaskManager.java b/src/service/FileBackedTaskManager.java index 44e50ce..63328b2 100644 --- a/src/service/FileBackedTaskManager.java +++ b/src/service/FileBackedTaskManager.java @@ -12,6 +12,8 @@ import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.TreeMap; public class FileBackedTaskManager extends InMemoryTaskManager { @@ -24,7 +26,7 @@ public FileBackedTaskManager(File file) { // Сохраняет текущее состояние менеджера в CSV-файл private void save() { try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { - writer.write("id,type,name,status,description,epic"); + writer.write("id,type,name,status,description,duration,startTime,epic"); writer.newLine(); TreeMap allTasks = new TreeMap<>(); @@ -50,18 +52,27 @@ private void save() { } } - // Преобразует задачу в строку формата CSV + // Преобразует задачу в строку формата CSV с учетом времени и продолжительности private String toString(Task task) { StringBuilder builder = new StringBuilder(); builder.append(task.getId()).append(","); builder.append(task.getType()).append(","); - builder.append(task.getName()).append(","); builder.append(task.getStatus()).append(","); builder.append(task.getDescription()).append(","); - if (task instanceof Subtask) { + if (task.getDuration() != null) { + builder.append(task.getDuration().toMinutes()); + } + builder.append(","); + + if (task.getStartTime() != null) { + builder.append(task.getStartTime()); + } + builder.append(","); + + if (task.getType() == TaskType.SUBTASK) { Subtask subtask = (Subtask) task; builder.append(subtask.getEpicId()); } @@ -69,7 +80,7 @@ private String toString(Task task) { return builder.toString(); } - // Преобразует строку CSV в объект задачи + // Преобразует строку CSV в объект задачи с учетом времени и продолжительности private static Task fromString(String value) { String[] fields = value.split(",", -1); @@ -79,16 +90,26 @@ private static Task fromString(String value) { Status status = Status.valueOf(fields[3]); String description = fields[4]; + Duration duration = null; + if (!fields[5].isBlank()) { + duration = Duration.ofMinutes(Long.parseLong(fields[5])); + } + + LocalDateTime startTime = null; + if (!fields[6].isBlank()) { + startTime = LocalDateTime.parse(fields[6]); + } + Task task; if (taskType == TaskType.TASK) { - task = new Task(name, description, status); + task = new Task(name, description, status, duration, startTime); } else if (taskType == TaskType.EPIC) { task = new Epic(name, description); task.setStatus(status); } else { - int epicId = Integer.parseInt(fields[5]); - task = new Subtask(name, description, status, epicId); + int epicId = Integer.parseInt(fields[7]); + task = new Subtask(name, description, status, duration, startTime, epicId); } task.setId(id); diff --git a/src/service/InMemoryHistoryManager.java b/src/service/InMemoryHistoryManager.java index 1a4ad28..7a60012 100644 --- a/src/service/InMemoryHistoryManager.java +++ b/src/service/InMemoryHistoryManager.java @@ -25,6 +25,8 @@ private Task makeSnapshot(Task task) { original.getName(), original.getDescription(), original.getStatus(), + original.getDuration(), + original.getStartTime(), original.getEpicId() ); copy.setId(original.getId()); @@ -35,9 +37,18 @@ private Task makeSnapshot(Task task) { copy.setId(original.getId()); copy.setStatus(original.getStatus()); copy.setSubtaskIds(original.getSubtaskIds()); + copy.setEpicDuration(original.getDuration()); + copy.setEpicStartTime(original.getStartTime()); + copy.setEpicEndTime(original.getEndTime()); return copy; } else { - Task copy = new Task(task.getName(), task.getDescription(), task.getStatus()); + Task copy = new Task( + task.getName(), + task.getDescription(), + task.getStatus(), + task.getDuration(), + task.getStartTime() + ); copy.setId(task.getId()); return copy; } diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index b2d6126..8fac7c7 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -4,16 +4,27 @@ import model.Task; import model.Subtask; import model.Epic; +import service.exception.NotFoundException; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.List; +import java.util.TreeSet; public class InMemoryTaskManager implements service.TaskManager { // Хеш-таблицы для хранения задач для классов Task, Epic, Subtask - private HashMap tasks = new HashMap<>(); - private HashMap epics = new HashMap<>(); - private HashMap subtasks = new HashMap<>(); + private final HashMap tasks = new HashMap<>(); + private final HashMap epics = new HashMap<>(); + private final HashMap subtasks = new HashMap<>(); + + // Хранение задач и подзадач в отсортированном порядке по времени начала + private final TreeSet prioritizedTasks = new TreeSet<>( + Comparator.comparing(Task::getStartTime) + .thenComparing(Task::getId) + ); // Менеджер истории private final HistoryManager historyManager = Managers.getDefaultHistory(); @@ -21,12 +32,39 @@ public class InMemoryTaskManager implements service.TaskManager { // Счетчик для генерации ID private int nextId = 1; - // Метод для увеличения ID + // Получение следующего идентификатора задачи private int getNextId() { return nextId++; } - // Получение списка всех задач для каждого из типов задач(Задача/Эпик/Подзадача) + // Поиск обычной задачи по id + private Task findTaskById(int id) { + Task task = tasks.get(id); + if (task == null) { + throw new NotFoundException("Задача с id=" + id + " не найдена."); + } + return task; + } + + // Поиск эпика по id + private Epic findEpicById(int id) { + Epic epic = epics.get(id); + if (epic == null) { + throw new NotFoundException("Эпик с id=" + id + " не найден."); + } + return epic; + } + + // Поиск подзадачи по id + private Subtask findSubtaskById(int id) { + Subtask subtask = subtasks.get(id); + if (subtask == null) { + throw new NotFoundException("Подзадача с id=" + id + " не найдена."); + } + return subtask; + } + + // Получение списков задач, эпиков и подзадач @Override public ArrayList getTasks() { return new ArrayList<>(tasks.values()); @@ -42,22 +80,25 @@ public ArrayList getSubtasks() { return new ArrayList<>(subtasks.values()); } - // Удаление всех задач для каждого из типов задач(Задача/Эпик/Подзадача) + // Удаление всех задач @Override public void clearTasks() { - // Удаление каждой задачи из истории перед очисткой хранилища - for (int id : tasks.keySet()) { - historyManager.remove(id); + // Удаление каждой задачи из истории и списка приоритетов перед очисткой хранилища + for (Task task : tasks.values()) { + prioritizedTasks.remove(task); + historyManager.remove(task.getId()); } tasks.clear(); } @Override public void clearEpics() { - // Сначала удаление всех подзадач из истории - for (int id : subtasks.keySet()) { - historyManager.remove(id); + // Сначала удаление всех подзадач из истории и списка приоритетов + for (Subtask subtask : subtasks.values()) { + prioritizedTasks.remove(subtask); + historyManager.remove(subtask.getId()); } + // Затем самих эпиков for (int id : epics.keySet()) { historyManager.remove(id); @@ -69,29 +110,36 @@ public void clearEpics() { @Override public void clearSubtasks() { - // Удаление каждой подзадачи из истории - for (int id : subtasks.keySet()) { - historyManager.remove(id); + // Удаление каждой подзадачи из истории и списка приоритетов + for (Subtask subtask : subtasks.values()) { + prioritizedTasks.remove(subtask); + historyManager.remove(subtask.getId()); } subtasks.clear(); - // Чистка Эпиков, если подзадач больше нет + // Очистка эпиков, если подзадач больше нет for (Epic epic : epics.values()) { epic.clearSubtaskIds(); epic.setStatus(Status.NEW); + updateEpicTime(epic); } } - // Получение по идентификатору для каждого из типов задач(Задача/Эпик/Подзадача) + // Получение задачи, эпика или подзадачи по идентификатору @Override public Task getTask(int id) { - Task task = tasks.get(id); - if (task == null) return null; + Task task = findTaskById(id); historyManager.add(task); - Task copy = new Task(task.getName(), task.getDescription(), task.getStatus()); + Task copy = new Task( + task.getName(), + task.getDescription(), + task.getStatus(), + task.getDuration(), + task.getStartTime() + ); copy.setId(task.getId()); return copy; @@ -99,8 +147,7 @@ public Task getTask(int id) { @Override public Epic getEpic(int id) { - Epic epic = epics.get(id); - if (epic == null) return null; + Epic epic = findEpicById(id); historyManager.add(epic); @@ -108,14 +155,16 @@ public Epic getEpic(int id) { copy.setId(epic.getId()); copy.setStatus(epic.getStatus()); copy.setSubtaskIds(epic.getSubtaskIds()); + copy.setEpicDuration(epic.getDuration()); + copy.setEpicStartTime(epic.getStartTime()); + copy.setEpicEndTime(epic.getEndTime()); return copy; } @Override public Subtask getSubtask(int id) { - Subtask subtask = subtasks.get(id); - if (subtask == null) return null; + Subtask subtask = findSubtaskById(id); historyManager.add(subtask); @@ -123,6 +172,8 @@ public Subtask getSubtask(int id) { subtask.getName(), subtask.getDescription(), subtask.getStatus(), + subtask.getDuration(), + subtask.getStartTime(), subtask.getEpicId() ); copy.setId(subtask.getId()); @@ -130,18 +181,55 @@ public Subtask getSubtask(int id) { return copy; } - // Возвращение списка истории + // Получение списка истории @Override public List getHistory() { return historyManager.getHistory(); } - // Создание для каждого из типов задач(Задача/Эпик/Подзадача) + // Получение задач и подзадач в порядке приоритета по времени начала + @Override + public List getPrioritizedTasks() { + return new ArrayList<>(prioritizedTasks); + } + + // Проверка пересечения двух задач по времени выполнения + protected boolean isTaskOverlapping(Task firstTask, Task secondTask) { + return firstTask.getStartTime().isBefore(secondTask.getEndTime()) + && secondTask.getStartTime().isBefore(firstTask.getEndTime()); + } + + // Проверка пересечения задачи с уже существующими задачами и подзадачами + protected boolean hasTimeOverlap(Task task) { + if (task.getStartTime() == null || task.getEndTime() == null) { + return false; + } + + return prioritizedTasks.stream() + .anyMatch(prioritizedTask -> + prioritizedTask.getId() != task.getId() + && isTaskOverlapping(task, prioritizedTask) + ); + } + + // Добавление задачи или подзадачи в список приоритетов, если задано время начала + protected void addTaskToPrioritizedTasks(Task task) { + if (task.getStartTime() != null) { + prioritizedTasks.add(task); + } + } + + // Создание задач, эпиков и подзадач @Override public Task createTask(Task task) { + if (hasTimeOverlap(task)) { + throw new IllegalArgumentException("Задача пересекается по времени с другой задачей."); + } + task.setId(getNextId()); - task.setStatus(Status.NEW); tasks.put(task.getId(), task); + addTaskToPrioritizedTasks(task); + return task; } @@ -155,59 +243,69 @@ public Epic createEpic(Epic epic) { @Override public Subtask createSubtask(Subtask subtask) { + Epic epic = findEpicById(subtask.getEpicId()); + + if (hasTimeOverlap(subtask)) { + throw new IllegalArgumentException("Подзадача пересекается по времени с другой задачей."); + } + subtask.setId(getNextId()); subtask.setStatus(Status.NEW); subtasks.put(subtask.getId(), subtask); + addTaskToPrioritizedTasks(subtask); - // Добавляем в Эпик ID новой Подзадачи - Epic epic = epics.get(subtask.getEpicId()); - if (epic != null) { - epic.addSubtaskId(subtask.getId()); - updateEpicStatus(epic); - } + epic.addSubtaskId(subtask.getId()); + updateEpicStatus(epic); + updateEpicTime(epic); return subtask; } - // Обновление задач для каждого из типов задач(Задача/Эпик/Подзадача) + // Обновление задачи с синхронизацией списка приоритетов @Override public void updateTask(Task task) { - // Проверка на наличие задачи - if (tasks.containsKey(task.getId())) { - tasks.put(task.getId(), task); + Task oldTask = findTaskById(task.getId()); + + prioritizedTasks.remove(oldTask); + + if (hasTimeOverlap(task)) { + addTaskToPrioritizedTasks(oldTask); + throw new IllegalArgumentException("Обновлённая задача пересекается по времени с другой задачей."); } + + tasks.put(task.getId(), task); + addTaskToPrioritizedTasks(task); } @Override public void updateEpic(Epic epic) { - if (epics.containsKey(epic.getId())) { - // Создание копии списка задач во избежание потери данных при обновлении - Epic oldEpic = epics.get(epic.getId()); - epic.setSubtaskIds(oldEpic.getSubtaskIds()); - epic.setStatus(oldEpic.getStatus()); + // Создание копии списка задач во избежание потери данных при обновлении + Epic oldEpic = findEpicById(epic.getId()); + epic.setSubtaskIds(oldEpic.getSubtaskIds()); + epic.setStatus(oldEpic.getStatus()); + epic.setEpicDuration(oldEpic.getDuration()); + epic.setEpicStartTime(oldEpic.getStartTime()); + epic.setEpicEndTime(oldEpic.getEndTime()); - epics.put(epic.getId(), epic); - } + epics.put(epic.getId(), epic); } - // Метод для обновления статуса Эпик при обновлении Подзадач + // Пересчёт статуса эпика на основе его подзадач protected void updateEpicStatus(Epic epic) { if (epic.getSubtaskIds().isEmpty()) { epic.setStatus(Status.NEW); return; } - int countNew = 0; - int countDone = 0; + long countNew = epic.getSubtaskIds().stream() + .map(subtasks::get) + .filter(subtask -> subtask.getStatus() == Status.NEW) + .count(); - for (int subtaskId : epic.getSubtaskIds()) { - Subtask subtask = subtasks.get(subtaskId); - if (subtask.getStatus() == Status.NEW) { - countNew++; - } else if (subtask.getStatus() == Status.DONE) { - countDone++; - } - } + long countDone = epic.getSubtaskIds().stream() + .map(subtasks::get) + .filter(subtask -> subtask.getStatus() == Status.DONE) + .count(); if (countNew == epic.getSubtaskIds().size()) { epic.setStatus(Status.NEW); @@ -218,68 +316,121 @@ protected void updateEpicStatus(Epic epic) { } } + // Пересчёт времени эпика на основе его подзадач + protected void updateEpicTime(Epic epic) { + if (epic.getSubtaskIds().isEmpty()) { + epic.setEpicDuration(null); + epic.setEpicStartTime(null); + epic.setEpicEndTime(null); + return; + } + + long totalDurationInMinutes = epic.getSubtaskIds().stream() + .map(subtasks::get) + .filter(subtask -> subtask.getDuration() != null) + .mapToLong(subtask -> subtask.getDuration().toMinutes()) + .sum(); + + LocalDateTime earliestStartTime = epic.getSubtaskIds().stream() + .map(subtasks::get) + .map(Subtask::getStartTime) + .filter(start -> start != null) + .min(LocalDateTime::compareTo) + .orElse(null); + + LocalDateTime latestEndTime = epic.getSubtaskIds().stream() + .map(subtasks::get) + .map(Subtask::getEndTime) + .filter(end -> end != null) + .max(LocalDateTime::compareTo) + .orElse(null); + + epic.setEpicDuration(Duration.ofMinutes(totalDurationInMinutes)); + epic.setEpicStartTime(earliestStartTime); + epic.setEpicEndTime(latestEndTime); + } + @Override public void updateSubtask(Subtask subtask) { - if (subtasks.containsKey(subtask.getId())) { - subtasks.put(subtask.getId(), subtask); + Subtask oldSubtask = findSubtaskById(subtask.getId()); - // Пересчёт статуса Эпик после обновления Подзадач - Epic epic = epics.get(subtask.getEpicId()); - if (epic != null) { - updateEpicStatus(epic); - } + subtask.setEpicId(oldSubtask.getEpicId()); + + Epic epic = findEpicById(subtask.getEpicId()); + + prioritizedTasks.remove(oldSubtask); + + if (hasTimeOverlap(subtask)) { + addTaskToPrioritizedTasks(oldSubtask); + throw new IllegalArgumentException("Обновлённая подзадача пересекается по времени с другой задачей."); } + + subtasks.put(subtask.getId(), subtask); + addTaskToPrioritizedTasks(subtask); + + updateEpicStatus(epic); + updateEpicTime(epic); } - // Удаление по идентификатору для каждого из типов задач(Задача/Эпик/Подзадача) + // Удаление задачи, эпика или подзадачи по идентификатору @Override public void deleteTask(int id) { + Task removedTask = findTaskById(id); + tasks.remove(id); + prioritizedTasks.remove(removedTask); historyManager.remove(id); } @Override public void deleteEpic(int id) { - Epic epic = epics.remove(id); - if (epic != null) { - // Удаление подзадач, связанных с этим Эпиком - for (Integer subtaskId : epic.getSubtaskIds()) { - subtasks.remove(subtaskId); - historyManager.remove(subtaskId); + Epic epic = findEpicById(id); + + epics.remove(id); + + // Удаление подзадач, связанных с этим эпиком + for (Integer subtaskId : epic.getSubtaskIds()) { + Subtask removedSubtask = subtasks.remove(subtaskId); + if (removedSubtask != null) { + prioritizedTasks.remove(removedSubtask); } + historyManager.remove(subtaskId); } + historyManager.remove(id); } @Override public void deleteSubtask(int id) { - Subtask subtask = subtasks.remove(id); - if (subtask != null) { - // Удаление ID Подзадачи из Эпика - Epic epic = epics.get(subtask.getEpicId()); - if (epic != null) { - epic.removeSubtaskId(id); - updateEpicStatus(epic); - } - } + Subtask subtask = findSubtaskById(id); + + subtasks.remove(id); + prioritizedTasks.remove(subtask); + + // Удаление id подзадачи из эпика + Epic epic = findEpicById(subtask.getEpicId()); + epic.removeSubtaskId(id); + updateEpicStatus(epic); + updateEpicTime(epic); + historyManager.remove(id); } - // Получение списка всех Подзадач для определённого Эпика - public ArrayList getEpicSubtasks(int epicId) { - ArrayList result = new ArrayList<>(); - Epic epic = epics.get(epicId); - if (epic != null) { - for (int subtaskId : epic.getSubtaskIds()) { - result.add(subtasks.get(subtaskId)); - } - } - return result; + // Получение списка подзадач эпика с использованием Stream API + @Override + public List getEpicSubtasks(int epicId) { + Epic epic = findEpicById(epicId); + + return epic.getSubtaskIds().stream() + .map(subtasks::get) + .filter(subtask -> subtask != null) + .toList(); } // Служебные методы для восстановления менеджера из файла protected void putLoadedTask(Task task) { tasks.put(task.getId(), task); + addTaskToPrioritizedTasks(task); } protected void putLoadedEpic(Epic epic) { @@ -288,10 +439,13 @@ protected void putLoadedEpic(Epic epic) { protected void putLoadedSubtask(Subtask subtask) { subtasks.put(subtask.getId(), subtask); + addTaskToPrioritizedTasks(subtask); Epic epic = epics.get(subtask.getEpicId()); if (epic != null) { epic.addSubtaskId(subtask.getId()); + updateEpicStatus(epic); + updateEpicTime(epic); } } diff --git a/src/service/TaskManager.java b/src/service/TaskManager.java index b70e41d..85c6c17 100644 --- a/src/service/TaskManager.java +++ b/src/service/TaskManager.java @@ -53,6 +53,9 @@ public interface TaskManager { void deleteSubtask(int id); - // Получение списка всех Подзадач для определённого Эпика - ArrayList getEpicSubtasks(int epicId); + // Получение подзадач эпика по id + List getEpicSubtasks(int epicId); + + // Получение задач и подзадач в порядке приоритета (по startTime) + List getPrioritizedTasks(); } diff --git a/src/service/exception/NotFoundException.java b/src/service/exception/NotFoundException.java new file mode 100644 index 0000000..513f525 --- /dev/null +++ b/src/service/exception/NotFoundException.java @@ -0,0 +1,10 @@ +package service.exception; + +// Исключение, которое выбрасывается, если объект не найден. +// Используется в HTTP-обработчиках, чтобы вернуть статус 404. +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } +} diff --git a/test/http/HttpTaskManagerEpicsTest.java b/test/http/HttpTaskManagerEpicsTest.java new file mode 100644 index 0000000..85430a4 --- /dev/null +++ b/test/http/HttpTaskManagerEpicsTest.java @@ -0,0 +1,238 @@ +package http; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import model.Epic; +import model.Subtask; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +// Тесты для endpoint'а /epics. +public class HttpTaskManagerEpicsTest extends HttpTaskServerTestBase { + + // Проверка: GET /epics должен возвращать пустой список, + // если эпиков в менеджере нет. + @Test + public void shouldReturnEmptyEpicsList() throws IOException, InterruptedException { + URI url = createUri("/epics"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode(), "Некорректный код ответа."); + assertEquals("[]", response.body(), "Список эпиков должен быть пустым."); + } + + // Проверка: POST /epics должен создавать новый эпик. + @Test + public void shouldCreateEpic() throws IOException, InterruptedException { + Epic epic = createTestEpic("Покупки", "Список покупок на вечер"); + String epicJson = gson.toJson(epic); + + URI url = createUri("/epics"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(epicJson)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + List epicsFromManager = manager.getEpics(); + + assertEquals(201, response.statusCode(), "Некорректный код ответа при создании эпика."); + assertNotNull(epicsFromManager, "Список эпиков не должен быть null."); + assertEquals(1, epicsFromManager.size(), "Некорректное количество эпиков."); + assertEquals("Покупки", epicsFromManager.get(0).getName(), "Некорректное имя эпика."); + } + + // Проверка: POST /epics должен обновлять эпик, + // если в JSON указан существующий id. + @Test + public void shouldUpdateEpic() throws IOException, InterruptedException { + Epic epic = createTestEpic("Учёба", "Старое описание"); + manager.createEpic(epic); + + Epic savedEpic = manager.getEpics().get(0); + + Epic updatedEpic = createTestEpic("Учёба и практика", "Новое описание"); + updatedEpic.setId(savedEpic.getId()); + + String updatedEpicJson = gson.toJson(updatedEpic); + + URI url = createUri("/epics"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(updatedEpicJson)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + Epic epicFromManager = manager.getEpic(savedEpic.getId()); + + assertEquals(201, response.statusCode(), "Некорректный код ответа при обновлении эпика."); + assertEquals("Учёба и практика", epicFromManager.getName(), "Имя эпика не обновилось."); + assertEquals("Новое описание", epicFromManager.getDescription(), "Описание эпика не обновилось."); + } + + // Проверка: POST /epics должен возвращать 404, + // если в JSON указан id несуществующего эпика. + @Test + public void shouldReturn404WhenUpdatingMissingEpic() throws IOException, InterruptedException { + Epic epic = createTestEpic("Несуществующий эпик", "Не должен обновиться"); + epic.setId(999); + + String epicJson = gson.toJson(epic); + + URI url = createUri("/epics"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(epicJson)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode(), + "Должен вернуться статус 404 при попытке обновить несуществующий эпик."); + } + + // Проверка: GET /epics/{id} должен возвращать эпик по id. + @Test + public void shouldReturnEpicById() throws IOException, InterruptedException { + Epic epic = createTestEpic("Учёба", "Подготовка к экзамену"); + manager.createEpic(epic); + + int epicId = manager.getEpics().get(0).getId(); + + URI url = createUri("/epics/" + epicId); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + Epic epicFromResponse = gson.fromJson(response.body(), Epic.class); + + assertEquals(200, response.statusCode(), "Некорректный код ответа при получении эпика по id."); + assertNotNull(epicFromResponse, "Эпик не пришёл в ответе."); + assertEquals(epicId, epicFromResponse.getId(), "Некорректный id эпика."); + assertEquals("Учёба", epicFromResponse.getName(), "Некорректное имя эпика."); + } + + // Проверка: GET /epics/{id} должен возвращать 404, + // если эпика с таким id не существует. + @Test + public void shouldReturn404WhenEpicNotFound() throws IOException, InterruptedException { + URI url = createUri("/epics/" + MISSING_ID); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode(), "Должен вернуться статус 404, если эпик не найден."); + } + + // Проверка: GET /epics/{id}/subtasks должен возвращать список подзадач эпика. + @Test + public void shouldReturnEpicSubtasks() throws IOException, InterruptedException { + Epic epic = createTestEpic("Переезд", "Подготовка к переезду"); + manager.createEpic(epic); + + int epicId = manager.getEpics().get(0).getId(); + + Subtask subtask = createTestSubtask( + "Собрать коробки", + "Упаковать вещи", + 60, + LocalDateTime.of(2026, 4, 22, 10, 0), + epicId + ); + manager.createSubtask(subtask); + + URI url = createUri("/epics/" + epicId + "/subtasks"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + JsonArray jsonArray = JsonParser.parseString(response.body()).getAsJsonArray(); + JsonObject firstSubtask = jsonArray.get(0).getAsJsonObject(); + + assertEquals(200, response.statusCode(), + "Некорректный код ответа при получении подзадач эпика."); + assertEquals(1, jsonArray.size(), "У эпика должна быть одна подзадача."); + assertEquals("Собрать коробки", firstSubtask.get("name").getAsString(), + "Некорректное имя подзадачи в ответе."); + } + + // Проверка: GET /epics/{id}/subtasks должен возвращать 404, + // если эпика с таким id не существует. + @Test + public void shouldReturn404WhenEpicSubtasksRequestedForMissingEpic() throws IOException, InterruptedException { + URI url = createUri("/epics/" + MISSING_ID + "/subtasks"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode(), + "Должен вернуться статус 404, если подзадачи запрашиваются у несуществующего эпика."); + } + + // Проверка: DELETE /epics/{id} должен удалять эпик. + @Test + public void shouldDeleteEpicById() throws IOException, InterruptedException { + Epic epic = createTestEpic("Отпуск", "Подготовка к отпуску"); + manager.createEpic(epic); + + int epicId = manager.getEpics().get(0).getId(); + + URI url = createUri("/epics/" + epicId); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .DELETE() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode(), "Некорректный код ответа при удалении эпика."); + assertTrue(manager.getEpics().isEmpty(), "Эпик не был удалён из менеджера."); + } + + // Проверка: DELETE /epics/{id} должен возвращать 404, + // если эпика с таким id не существует. + @Test + public void shouldReturn404WhenDeletingMissingEpic() throws IOException, InterruptedException { + URI url = createUri("/epics/" + MISSING_ID); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .DELETE() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode(), + "Должен вернуться статус 404 при удалении несуществующего эпика."); + } +} \ No newline at end of file diff --git a/test/http/HttpTaskManagerHistoryTest.java b/test/http/HttpTaskManagerHistoryTest.java new file mode 100644 index 0000000..6232298 --- /dev/null +++ b/test/http/HttpTaskManagerHistoryTest.java @@ -0,0 +1,106 @@ +package http; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import model.Epic; +import model.Subtask; +import model.Task; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +// Тесты для endpoint'а /history. +public class HttpTaskManagerHistoryTest extends HttpTaskServerTestBase { + + // Проверка: GET /history должен возвращать пустой список, + // если история просмотров пуста. + @Test + public void shouldReturnEmptyHistory() throws IOException, InterruptedException { + URI url = createUri("/history"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode(), "Некорректный код ответа."); + assertEquals("[]", response.body(), "История должна быть пустой."); + } + + // Проверка: GET /history должен возвращать просмотренные задачи. + @Test + public void shouldReturnViewedTasksInHistory() throws IOException, InterruptedException { + Task task = createTestTask( + "Обычная задача", + "Описание задачи", + 30, + LocalDateTime.of(2026, 4, 26, 10, 0) + ); + manager.createTask(task); + + Epic epic = createTestEpic("Эпик", "Описание эпика"); + manager.createEpic(epic); + + int epicId = manager.getEpics().get(0).getId(); + + Subtask subtask = createTestSubtask( + "Подзадача", + "Описание подзадачи", + 40, + LocalDateTime.of(2026, 4, 26, 12, 0), + epicId + ); + manager.createSubtask(subtask); + + int taskId = manager.getTasks().get(0).getId(); + int subtaskId = manager.getSubtasks().get(0).getId(); + + manager.getTask(taskId); + manager.getEpic(epicId); + manager.getSubtask(subtaskId); + + URI url = createUri("/history"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + JsonArray jsonArray = JsonParser.parseString(response.body()).getAsJsonArray(); + + assertEquals(200, response.statusCode(), "Некорректный код ответа при получении истории."); + assertEquals(3, jsonArray.size(), "В истории должно быть три просмотренные сущности."); + + JsonObject firstObject = jsonArray.get(0).getAsJsonObject(); + JsonObject secondObject = jsonArray.get(1).getAsJsonObject(); + JsonObject thirdObject = jsonArray.get(2).getAsJsonObject(); + + assertEquals(taskId, firstObject.get("id").getAsInt(), "Первой в истории должна быть обычная задача."); + assertEquals(epicId, secondObject.get("id").getAsInt(), "Вторым в истории должен быть эпик."); + assertEquals(subtaskId, thirdObject.get("id").getAsInt(), "Третьей в истории должна быть подзадача."); + } + + // Проверка: GET /history должен возвращать только GET-запросы. + @Test + public void shouldReturn500ForPostHistoryRequest() throws IOException, InterruptedException { + URI url = createUri("/history"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .POST(HttpRequest.BodyPublishers.ofString("")) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(500, response.statusCode(), + "Для неподдерживаемого POST-запроса к /history должен вернуться 500."); + } +} \ No newline at end of file diff --git a/test/http/HttpTaskManagerPrioritizedTest.java b/test/http/HttpTaskManagerPrioritizedTest.java new file mode 100644 index 0000000..2000d6a --- /dev/null +++ b/test/http/HttpTaskManagerPrioritizedTest.java @@ -0,0 +1,110 @@ +package http; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import model.Epic; +import model.Subtask; +import model.Task; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +// Тесты для endpoint'а /prioritized. +public class HttpTaskManagerPrioritizedTest extends HttpTaskServerTestBase { + + // Проверка: GET /prioritized должен возвращать пустой список, + // если задач с временем начала нет. + @Test + public void shouldReturnEmptyPrioritizedList() throws IOException, InterruptedException { + URI url = createUri("/prioritized"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode(), "Некорректный код ответа."); + assertEquals("[]", response.body(), "Список приоритетных задач должен быть пустым."); + } + + // Проверка: GET /prioritized должен возвращать задачи в порядке приоритета. + @Test + public void shouldReturnTasksInPrioritizedOrder() throws IOException, InterruptedException { + Task lateTask = createTestTask( + "Поздняя задача", + "Будет второй", + 30, + LocalDateTime.of(2026, 4, 27, 18, 0) + ); + manager.createTask(lateTask); + + Task earlyTask = createTestTask( + "Ранняя задача", + "Будет первой", + 30, + LocalDateTime.of(2026, 4, 27, 9, 0) + ); + manager.createTask(earlyTask); + + Epic epic = createTestEpic("Эпик", "Описание эпика"); + manager.createEpic(epic); + + int epicId = manager.getEpics().get(0).getId(); + + Subtask middleSubtask = createTestSubtask( + "Средняя подзадача", + "Будет между задачами", + 45, + LocalDateTime.of(2026, 4, 27, 13, 0), + epicId + ); + manager.createSubtask(middleSubtask); + + URI url = createUri("/prioritized"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + JsonArray jsonArray = JsonParser.parseString(response.body()).getAsJsonArray(); + + assertEquals(200, response.statusCode(), "Некорректный код ответа при получении приоритетных задач."); + assertEquals(3, jsonArray.size(), "В списке приоритетных задач должно быть три элемента."); + + JsonObject firstObject = jsonArray.get(0).getAsJsonObject(); + JsonObject secondObject = jsonArray.get(1).getAsJsonObject(); + JsonObject thirdObject = jsonArray.get(2).getAsJsonObject(); + + assertEquals("Ранняя задача", firstObject.get("name").getAsString(), + "Первой должна быть самая ранняя задача."); + assertEquals("Средняя подзадача", secondObject.get("name").getAsString(), + "Второй должна быть подзадача со средним временем."); + assertEquals("Поздняя задача", thirdObject.get("name").getAsString(), + "Третьей должна быть самая поздняя задача."); + } + + // Проверка: GET /prioritized должен возвращать только GET-запросы. + @Test + public void shouldReturn500ForPostPrioritizedRequest() throws IOException, InterruptedException { + URI url = createUri("/prioritized"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .POST(HttpRequest.BodyPublishers.ofString("")) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(500, response.statusCode(), + "Для неподдерживаемого POST-запроса к /prioritized должен вернуться 500."); + } +} \ No newline at end of file diff --git a/test/http/HttpTaskManagerSubtasksTest.java b/test/http/HttpTaskManagerSubtasksTest.java new file mode 100644 index 0000000..b4b0bfd --- /dev/null +++ b/test/http/HttpTaskManagerSubtasksTest.java @@ -0,0 +1,318 @@ +package http; + +import model.Epic; +import model.Subtask; +import model.Task; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +// Тесты для endpoint'а /subtasks. +public class HttpTaskManagerSubtasksTest extends HttpTaskServerTestBase { + + // Проверка: GET /subtasks должен возвращать пустой список, + // если подзадач в менеджере нет. + @Test + public void shouldReturnEmptySubtasksList() throws IOException, InterruptedException { + URI url = createUri("/subtasks"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode(), "Некорректный код ответа."); + assertEquals("[]", response.body(), "Список подзадач должен быть пустым."); + } + + // Проверка: POST /subtasks должен создавать новую подзадачу, + // если эпик существует. + @Test + public void shouldCreateSubtask() throws IOException, InterruptedException { + Epic epic = createTestEpic("Домашние дела", "Список домашних дел"); + manager.createEpic(epic); + + int epicId = manager.getEpics().get(0).getId(); + + Subtask subtask = createTestSubtask( + "Помыть окна", + "На кухне и в комнате", + 45, + LocalDateTime.of(2026, 4, 23, 14, 0), + epicId + ); + + String subtaskJson = gson.toJson(subtask); + + URI url = createUri("/subtasks"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(subtaskJson)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + List subtasksFromManager = manager.getSubtasks(); + + assertEquals(201, response.statusCode(), "Некорректный код ответа при создании подзадачи."); + assertNotNull(subtasksFromManager, "Список подзадач не должен быть null."); + assertEquals(1, subtasksFromManager.size(), "Некорректное количество подзадач."); + assertEquals("Помыть окна", subtasksFromManager.get(0).getName(), "Некорректное имя подзадачи."); + } + + // Проверка: POST /subtasks должен обновлять подзадачу, + // если в JSON указан существующий id. + @Test + public void shouldUpdateSubtask() throws IOException, InterruptedException { + Epic epic = createTestEpic("Учёба", "Подготовка к занятиям"); + manager.createEpic(epic); + + int epicId = manager.getEpics().get(0).getId(); + + Subtask subtask = createTestSubtask( + "Сделать домашку", + "Первая версия", + 60, + LocalDateTime.of(2026, 4, 25, 16, 0), + epicId + ); + manager.createSubtask(subtask); + + Subtask savedSubtask = manager.getSubtasks().get(0); + + Subtask updatedSubtask = createTestSubtask( + "Сделать домашку по Java", + "Обновлённая версия", + 90, + LocalDateTime.of(2026, 4, 25, 18, 0), + epicId + ); + updatedSubtask.setId(savedSubtask.getId()); + + String updatedSubtaskJson = gson.toJson(updatedSubtask); + + URI url = createUri("/subtasks"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(updatedSubtaskJson)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + Subtask subtaskFromManager = manager.getSubtask(savedSubtask.getId()); + + assertEquals(201, response.statusCode(), "Некорректный код ответа при обновлении подзадачи."); + assertEquals("Сделать домашку по Java", subtaskFromManager.getName(), "Имя подзадачи не обновилось."); + assertEquals("Обновлённая версия", subtaskFromManager.getDescription(), + "Описание подзадачи не обновилось."); + } + + // Проверка: POST /subtasks должен возвращать 404, + // если в JSON указан id несуществующей подзадачи. + @Test + public void shouldReturn404WhenUpdatingMissingSubtask() throws IOException, InterruptedException { + Epic epic = createTestEpic("Работа", "Рабочий эпик"); + manager.createEpic(epic); + + int epicId = manager.getEpics().get(0).getId(); + + Subtask subtask = createTestSubtask( + "Несуществующая подзадача", + "Не должна обновиться", + 40, + LocalDateTime.of(2026, 4, 26, 11, 0), + epicId + ); + subtask.setId(999); + + String subtaskJson = gson.toJson(subtask); + + URI url = createUri("/subtasks"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(subtaskJson)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode(), + "Должен вернуться статус 404 при попытке обновить несуществующую подзадачу."); + } + + // Проверка: GET /subtasks/{id} должен возвращать подзадачу по id. + @Test + public void shouldReturnSubtaskById() throws IOException, InterruptedException { + Epic epic = createTestEpic("Учёба", "Подготовка к занятиям"); + manager.createEpic(epic); + + int epicId = manager.getEpics().get(0).getId(); + + Subtask subtask = createTestSubtask( + "Решить задачи", + "Практика по Java", + 90, + LocalDateTime.of(2026, 4, 23, 18, 0), + epicId + ); + manager.createSubtask(subtask); + + int subtaskId = manager.getSubtasks().get(0).getId(); + + URI url = createUri("/subtasks/" + subtaskId); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + Subtask subtaskFromResponse = gson.fromJson(response.body(), Subtask.class); + + assertEquals(200, response.statusCode(), "Некорректный код ответа при получении подзадачи по id."); + assertNotNull(subtaskFromResponse, "Подзадача не пришла в ответе."); + assertEquals(subtaskId, subtaskFromResponse.getId(), "Некорректный id подзадачи."); + assertEquals("Решить задачи", subtaskFromResponse.getName(), "Некорректное имя подзадачи."); + } + + // Проверка: GET /subtasks/{id} должен возвращать 404, + // если подзадачи с таким id не существует. + @Test + public void shouldReturn404WhenSubtaskNotFound() throws IOException, InterruptedException { + URI url = createUri("/subtasks/" + MISSING_ID); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode(), "Должен вернуться статус 404, если подзадача не найдена."); + } + + // Проверка: POST /subtasks должен возвращать 404, + // если у подзадачи указан несуществующий epicId. + @Test + public void shouldReturn404WhenCreatingSubtaskWithoutExistingEpic() throws IOException, InterruptedException { + Subtask subtask = createTestSubtask( + "Несуществующий эпик", + "Подзадача не должна создаться", + 30, + LocalDateTime.of(2026, 4, 24, 10, 0), + 999 + ); + + String subtaskJson = gson.toJson(subtask); + + URI url = createUri("/subtasks"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(subtaskJson)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode(), + "Должен вернуться статус 404, если подзадача создаётся без существующего эпика."); + assertTrue(manager.getSubtasks().isEmpty(), "Подзадача не должна сохраниться в менеджере."); + } + + // Проверка: POST /subtasks должен возвращать 406, + // если новая подзадача пересекается по времени с существующей задачей. + @Test + public void shouldReturn406WhenSubtaskHasTimeOverlap() throws IOException, InterruptedException { + Epic epic = createTestEpic("Работа", "Рабочие задачи"); + manager.createEpic(epic); + + int epicId = manager.getEpics().get(0).getId(); + + Task task = createTestTask( + "Основная задача", + "Занимает время", + 60, + LocalDateTime.of(2026, 4, 24, 12, 0) + ); + manager.createTask(task); + + Subtask overlappingSubtask = createTestSubtask( + "Пересекающаяся подзадача", + "Не должна сохраниться", + 30, + LocalDateTime.of(2026, 4, 24, 12, 30), + epicId + ); + + String subtaskJson = gson.toJson(overlappingSubtask); + + URI url = createUri("/subtasks"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(subtaskJson)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(406, response.statusCode(), + "Должен вернуться статус 406 при пересечении подзадачи по времени."); + assertTrue(manager.getSubtasks().isEmpty(), "Пересекающаяся подзадача не должна сохраниться."); + } + + // Проверка: DELETE /subtasks/{id} должен удалять подзадачу. + @Test + public void shouldDeleteSubtaskById() throws IOException, InterruptedException { + Epic epic = createTestEpic("Проект", "Работа над проектом"); + manager.createEpic(epic); + + int epicId = manager.getEpics().get(0).getId(); + + Subtask subtask = createTestSubtask( + "Написать код", + "Основной модуль", + 120, + LocalDateTime.of(2026, 4, 25, 11, 0), + epicId + ); + manager.createSubtask(subtask); + + int subtaskId = manager.getSubtasks().get(0).getId(); + + URI url = createUri("/subtasks/" + subtaskId); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .DELETE() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode(), "Некорректный код ответа при удалении подзадачи."); + assertTrue(manager.getSubtasks().isEmpty(), "Подзадача не была удалена из менеджера."); + } + + // Проверка: DELETE /subtasks/{id} должен возвращать 404, + // если подзадачи с таким id не существует. + @Test + public void shouldReturn404WhenDeletingMissingSubtask() throws IOException, InterruptedException { + URI url = createUri("/subtasks/" + MISSING_ID); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .DELETE() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode(), + "Должен вернуться статус 404 при удалении несуществующей подзадачи."); + } +} \ No newline at end of file diff --git a/test/http/HttpTaskManagerTasksTest.java b/test/http/HttpTaskManagerTasksTest.java new file mode 100644 index 0000000..21080ea --- /dev/null +++ b/test/http/HttpTaskManagerTasksTest.java @@ -0,0 +1,246 @@ +package http; + +import model.Task; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +// Тесты для endpoint'а /tasks. +public class HttpTaskManagerTasksTest extends HttpTaskServerTestBase { + + // Проверка: GET /tasks должен возвращать пустой список, + // если задач в менеджере нет. + @Test + public void shouldReturnEmptyTasksList() throws IOException, InterruptedException { + URI url = createUri("/tasks"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode(), "Некорректный код ответа."); + assertEquals("[]", response.body(), "Список задач должен быть пустым."); + } + + // Проверка: POST /tasks должен создавать новую задачу. + @Test + public void shouldCreateTask() throws IOException, InterruptedException { + Task task = createTestTask( + "Закодить 9 спринт", + "по Java", + 30, + LocalDateTime.of(2026, 4, 18, 19, 0) + ); + + String taskJson = gson.toJson(task); + + URI url = createUri("/tasks"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .POST(HttpRequest.BodyPublishers.ofString(taskJson)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + List tasksFromManager = manager.getTasks(); + + assertEquals(201, response.statusCode(), "Некорректный код ответа при создании задачи."); + assertNotNull(tasksFromManager, "Задачи не возвращаются."); + assertEquals(1, tasksFromManager.size(), "Некорректное количество задач."); + assertEquals("Закодить 9 спринт", tasksFromManager.get(0).getName(), "Некорректное имя задачи."); + } + + // Проверка: GET /tasks/{id} должен возвращать задачу по id. + @Test + public void shouldReturnTaskById() throws IOException, InterruptedException { + Task task = createTestTask( + "Почитать книгу", + "Перед сном", + 40, + LocalDateTime.of(2026, 4, 18, 21, 0) + ); + manager.createTask(task); + + int taskId = manager.getTasks().get(0).getId(); + + URI url = createUri("/tasks/" + taskId); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + Task taskFromResponse = gson.fromJson(response.body(), Task.class); + + assertEquals(200, response.statusCode(), "Некорректный код ответа при получении задачи по id."); + assertNotNull(taskFromResponse, "Задача не пришла в ответе."); + assertEquals(taskId, taskFromResponse.getId(), "Некорректный id задачи."); + assertEquals("Почитать книгу", taskFromResponse.getName(), "Некорректное имя задачи."); + } + + // Проверка: GET /tasks/{id} должен возвращать 404, + // если задачи с таким id не существует. + @Test + public void shouldReturn404WhenTaskNotFound() throws IOException, InterruptedException { + URI url = createUri("/tasks/" + MISSING_ID); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode(), "Должен вернуться статус 404, если задача не найдена."); + } + + // Проверка: POST /tasks должен обновлять задачу, + // если в JSON указан существующий id. + @Test + public void shouldUpdateTask() throws IOException, InterruptedException { + Task task = createTestTask( + "Сделать зарядку", + "Утром", + 15, + LocalDateTime.of(2026, 4, 19, 8, 0) + ); + manager.createTask(task); + + Task savedTask = manager.getTasks().get(0); + + Task updatedTask = createTestTask( + "Сделать зарядку дома", + "Утром после пробуждения", + 20, + LocalDateTime.of(2026, 4, 19, 8, 30) + ); + updatedTask.setId(savedTask.getId()); + + String updatedTaskJson = gson.toJson(updatedTask); + + URI url = createUri("/tasks"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .POST(HttpRequest.BodyPublishers.ofString(updatedTaskJson)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + Task taskFromManager = manager.getTask(savedTask.getId()); + + assertEquals(201, response.statusCode(), "Некорректный код ответа при обновлении задачи."); + assertEquals("Сделать зарядку дома", taskFromManager.getName(), "Имя задачи не обновилось."); + assertEquals("Утром после пробуждения", taskFromManager.getDescription(), + "Описание задачи не обновилось."); + } + + // Проверка: POST /tasks должен возвращать 404, + // если в JSON указан id несуществующей задачи. + @Test + public void shouldReturn404WhenUpdatingMissingTask() throws IOException, InterruptedException { + Task task = createTestTask( + "Несуществующая задача", + "Не должна обновиться", + 20, + LocalDateTime.of(2026, 4, 20, 15, 0) + ); + task.setId(999); + + String taskJson = gson.toJson(task); + + URI url = createUri("/tasks"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .POST(HttpRequest.BodyPublishers.ofString(taskJson)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode(), + "Должен вернуться статус 404 при попытке обновить несуществующую задачу."); + } + + // Проверка: POST /tasks должен возвращать 406, + // если новая задача пересекается по времени с существующей. + @Test + public void shouldReturn406WhenTaskHasTimeOverlap() throws IOException, InterruptedException { + Task firstTask = createTestTask( + "Первая задача", + "Описание первой задачи", + 60, + LocalDateTime.of(2026, 4, 20, 10, 0) + ); + manager.createTask(firstTask); + + Task secondTask = createTestTask( + "Вторая задача", + "Описание второй задачи", + 30, + LocalDateTime.of(2026, 4, 20, 10, 30) + ); + + String secondTaskJson = gson.toJson(secondTask); + + URI url = createUri("/tasks"); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .POST(HttpRequest.BodyPublishers.ofString(secondTaskJson)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(406, response.statusCode(), + "Должен вернуться статус 406 при пересечении задач по времени."); + assertEquals(1, manager.getTasks().size(), "В менеджере не должна сохраниться пересекающаяся задача."); + } + + // Проверка: DELETE /tasks/{id} должен удалять задачу. + @Test + public void shouldDeleteTaskById() throws IOException, InterruptedException { + Task task = createTestTask( + "Удаляемая задача", + "Нужно удалить", + 25, + LocalDateTime.of(2026, 4, 21, 12, 0) + ); + manager.createTask(task); + + int taskId = manager.getTasks().get(0).getId(); + + URI url = createUri("/tasks/" + taskId); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .DELETE() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode(), "Некорректный код ответа при удалении задачи."); + assertTrue(manager.getTasks().isEmpty(), "Задача не была удалена из менеджера."); + } + + // Проверка: DELETE /tasks/{id} должен возвращать 404, + // если задачи с таким id не существует. + @Test + public void shouldReturn404WhenDeletingMissingTask() throws IOException, InterruptedException { + URI url = createUri("/tasks/" + MISSING_ID); + HttpRequest request = HttpRequest.newBuilder() + .uri(url) + .DELETE() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(404, response.statusCode(), + "Должен вернуться статус 404 при удалении несуществующей задачи."); + } +} \ No newline at end of file diff --git a/test/http/HttpTaskServerTestBase.java b/test/http/HttpTaskServerTestBase.java new file mode 100644 index 0000000..02aea9a --- /dev/null +++ b/test/http/HttpTaskServerTestBase.java @@ -0,0 +1,90 @@ +package http; + +import com.google.gson.Gson; +import model.Epic; +import model.Status; +import model.Subtask; +import model.Task; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import service.InMemoryTaskManager; +import service.TaskManager; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.time.Duration; +import java.time.LocalDateTime; + +// Базовый класс для HTTP-тестов. +// Содержит общий код запуска и остановки сервера, +// а также общие объекты, которые пригодятся в наследниках. +public abstract class HttpTaskServerTestBase { + + // Менеджер задач, который будет использоваться сервером в тестах. + protected TaskManager manager; + + // Экземпляр HTTP-сервера. + protected HttpTaskServer taskServer; + + // Gson сервера для преобразования объектов в JSON и обратно. + protected Gson gson; + + // HTTP-клиент для отправки запросов к локальному серверу. + protected HttpClient client; + + // Общие константы для HTTP-тестов. + protected static final String BASE_URL = "http://localhost:8080"; + protected static final int MISSING_ID = 999; + + // Запуск нового чистого окружения перед каждым тестом. + @BeforeEach + public void setUp() throws IOException { + manager = new InMemoryTaskManager(); + taskServer = new HttpTaskServer(manager); + gson = HttpTaskServer.getGson(); + client = HttpClient.newHttpClient(); + + taskServer.start(); + } + + // Остановка сервера после каждого теста. + @AfterEach + public void shutDown() { + taskServer.stop(); + } + + // Вспомогательный метод для создания URI к локальному тестовому серверу. + protected URI createUri(String path) { + return URI.create(BASE_URL + path); + } + + // Вспомогательный метод для быстрого создания обычной задачи. + protected Task createTestTask(String name, String description, int durationMinutes, LocalDateTime startTime) { + return new Task( + name, + description, + Status.NEW, + Duration.ofMinutes(durationMinutes), + startTime + ); + } + + // Вспомогательный метод для быстрого создания эпика. + protected Epic createTestEpic(String name, String description) { + return new Epic(name, description); + } + + // Вспомогательный метод для быстрого создания подзадачи. + protected Subtask createTestSubtask(String name, String description, int durationMinutes, + LocalDateTime startTime, int epicId) { + return new Subtask( + name, + description, + Status.NEW, + Duration.ofMinutes(durationMinutes), + startTime, + epicId + ); + } +} \ No newline at end of file diff --git a/test/service/FileBackedTaskManagerTest.java b/test/service/FileBackedTaskManagerTest.java index 2e2bbe1..e6cd829 100644 --- a/test/service/FileBackedTaskManagerTest.java +++ b/test/service/FileBackedTaskManagerTest.java @@ -6,95 +6,111 @@ import model.Task; import org.junit.jupiter.api.Test; -import java.io.BufferedWriter; import java.io.File; -import java.io.FileWriter; import java.io.IOException; +import java.time.Duration; +import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.*; -class FileBackedTaskManagerTest { +class FileBackedTaskManagerTest extends TaskManagerTest { - // Тест на сохранение и загрузку пустого менеджера - @Test - void shouldSaveAndLoadEmptyManager() throws IOException { - File tempFile = File.createTempFile("tasks", ".csv"); + private File file; - try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile))) { - writer.write("id,type,name,status,description,epic"); - writer.newLine(); + @Override + protected FileBackedTaskManager createTaskManager() { + try { + file = File.createTempFile("tasks", ".csv"); + return new FileBackedTaskManager(file); + } catch (IOException e) { + throw new RuntimeException("Не удалось создать временный файл для теста.", e); } - - FileBackedTaskManager loadedManager = FileBackedTaskManager.loadFromFile(tempFile); - - assertTrue(loadedManager.getTasks().isEmpty(), "Список задач должен быть пустым"); - assertTrue(loadedManager.getEpics().isEmpty(), "Список эпиков должен быть пустым"); - assertTrue(loadedManager.getSubtasks().isEmpty(), "Список подзадач должен быть пустым"); } - // Тест на сохранение нескольких задач + // Проверка сохранения и загрузки пустого менеджера @Test - void shouldSaveMultipleTasks() throws IOException { - File tempFile = File.createTempFile("tasks", ".csv"); - - FileBackedTaskManager manager = new FileBackedTaskManager(tempFile); - - Task task = new Task("Task1", "Description1", Status.NEW); - manager.createTask(task); - - Epic epic = new Epic("Epic1", "Description epic"); - manager.createEpic(epic); - - Subtask subtask = new Subtask("Subtask1", "Description sub", Status.NEW, epic.getId()); - manager.createSubtask(subtask); - - FileBackedTaskManager loadedManager = FileBackedTaskManager.loadFromFile(tempFile); - - assertEquals(1, loadedManager.getTasks().size(), "Должна быть 1 задача"); - assertEquals(1, loadedManager.getEpics().size(), "Должен быть 1 эпик"); - assertEquals(1, loadedManager.getSubtasks().size(), "Должна быть 1 подзадача"); + void shouldSaveAndLoadEmptyManager() { + assertDoesNotThrow(() -> { + FileBackedTaskManager manager = createTaskManager(); + FileBackedTaskManager loadedManager = FileBackedTaskManager.loadFromFile(file); + + assertTrue(loadedManager.getTasks().isEmpty(), "Список задач должен быть пустым."); + assertTrue(loadedManager.getEpics().isEmpty(), "Список эпиков должен быть пустым."); + assertTrue(loadedManager.getSubtasks().isEmpty(), "Список подзадач должен быть пустым."); + }, "Загрузка пустого менеджера не должна выбрасывать исключение."); } - // Тест на загрузку нескольких задач из файла + // Проверка сохранения и загрузки задачи с полями времени @Test - void shouldLoadMultipleTasks() throws IOException { - File tempFile = File.createTempFile("tasks", ".csv"); - - FileBackedTaskManager manager = new FileBackedTaskManager(tempFile); - - Task task = new Task("Task1", "Description1", Status.NEW); - manager.createTask(task); - - Epic epic = new Epic("Epic1", "Description epic"); - manager.createEpic(epic); - - Subtask subtask = new Subtask("Subtask1", "Description sub", Status.NEW, epic.getId()); - manager.createSubtask(subtask); - - subtask.setStatus(Status.DONE); - manager.updateSubtask(subtask); - - FileBackedTaskManager loadedManager = FileBackedTaskManager.loadFromFile(tempFile); - - Task loadedTask = loadedManager.getTasks().get(0); - Epic loadedEpic = loadedManager.getEpics().get(0); - Subtask loadedSubtask = loadedManager.getSubtasks().get(0); - - assertEquals("Task1", loadedTask.getName(), "Имя задачи должно совпадать"); - assertEquals("Description1", loadedTask.getDescription(), "Описание задачи должно совпадать"); - assertEquals(Status.NEW, loadedTask.getStatus(), "Статус задачи должен совпадать"); - - assertEquals("Epic1", loadedEpic.getName(), "Имя эпика должно совпадать"); - assertEquals("Description epic", loadedEpic.getDescription(), "Описание эпика должно совпадать"); - assertEquals(Status.DONE, loadedEpic.getStatus(), "Статус эпика должен пересчитаться по подзадаче"); + void shouldSaveAndLoadTaskWithTimeFields() { + FileBackedTaskManager manager = createTaskManager(); + + Task task = new Task( + "Task", + "Task description", + Status.NEW, + Duration.ofMinutes(45), + LocalDateTime.of(2026, 4, 16, 10, 0) + ); + + Task createdTask = manager.createTask(task); + + FileBackedTaskManager loadedManager = FileBackedTaskManager.loadFromFile(file); + Task loadedTask = loadedManager.getTask(createdTask.getId()); + + assertNotNull(loadedTask, "Задача должна загрузиться из файла."); + assertEquals(Duration.ofMinutes(45), loadedTask.getDuration(), + "Продолжительность задачи должна сохраниться."); + assertEquals(LocalDateTime.of(2026, 4, 16, 10, 0), loadedTask.getStartTime(), + "Время начала задачи должно сохраниться."); + assertEquals(LocalDateTime.of(2026, 4, 16, 10, 45), loadedTask.getEndTime(), + "Время окончания задачи должно корректно рассчитываться после загрузки."); + } - assertEquals("Subtask1", loadedSubtask.getName(), "Имя подзадачи должно совпадать"); - assertEquals("Description sub", loadedSubtask.getDescription(), "Описание подзадачи должно совпадать"); - assertEquals(Status.DONE, loadedSubtask.getStatus(), "Статус подзадачи должен совпадать"); - assertEquals(epic.getId(), loadedSubtask.getEpicId(), "Epic ID подзадачи должен совпадать"); + // Проверка сохранения и загрузки времени эпика через подзадачи + @Test + void shouldSaveAndLoadEpicTimeCalculatedFromSubtasks() { + FileBackedTaskManager manager = createTaskManager(); + + Epic epic = manager.createEpic(new Epic("Epic", "Epic description")); + + manager.createSubtask(new Subtask( + "Subtask 1", + "Description 1", + Status.NEW, + Duration.ofMinutes(60), + LocalDateTime.of(2026, 4, 16, 9, 0), + epic.getId() + )); + + manager.createSubtask(new Subtask( + "Subtask 2", + "Description 2", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 12, 0), + epic.getId() + )); + + FileBackedTaskManager loadedManager = FileBackedTaskManager.loadFromFile(file); + Epic loadedEpic = loadedManager.getEpic(epic.getId()); + + assertNotNull(loadedEpic, "Эпик должен загрузиться из файла."); + assertEquals(Duration.ofMinutes(90), loadedEpic.getDuration(), + "Продолжительность эпика должна восстановиться по подзадачам."); + assertEquals(LocalDateTime.of(2026, 4, 16, 9, 0), loadedEpic.getStartTime(), + "Время начала эпика должно восстановиться по самой ранней подзадаче."); + assertEquals(LocalDateTime.of(2026, 4, 16, 12, 30), loadedEpic.getEndTime(), + "Время окончания эпика должно восстановиться по самой поздней подзадаче."); + } - assertEquals(task.getId(), loadedTask.getId(), "ID задачи должен совпадать"); - assertEquals(epic.getId(), loadedEpic.getId(), "ID эпика должен совпадать"); - assertEquals(subtask.getId(), loadedSubtask.getId(), "ID подзадачи должен совпадать"); + // Проверка исключения при загрузке несуществующего файла + @Test + void shouldThrowExceptionWhenLoadingFromInvalidFile() { + assertThrows( + ManagerSaveException.class, + () -> FileBackedTaskManager.loadFromFile(new File("file_does_not_exist.csv")), + "Загрузка несуществующего файла должна приводить к ManagerSaveException." + ); } -} +} \ No newline at end of file diff --git a/test/service/InMemoryHistoryManagerTest.java b/test/service/InMemoryHistoryManagerTest.java index 35de938..be0c84d 100644 --- a/test/service/InMemoryHistoryManagerTest.java +++ b/test/service/InMemoryHistoryManagerTest.java @@ -1,180 +1,173 @@ package service; import model.Status; +import model.Subtask; import model.Task; - -import java.util.List; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; class InMemoryHistoryManagerTest { - private HistoryManager historyManager; + private InMemoryHistoryManager historyManager; @BeforeEach void setUp() { historyManager = new InMemoryHistoryManager(); } - // Задачи, добавляемые в HistoryManager, сохраняют предыдущую версию задачи и её данных + // Проверка пустой истории @Test - void add() { - Task task = new Task("Task", "Task Description", Status.NEW); - task.setId(1); - - historyManager.add(task); - - final List history = historyManager.getHistory(); - - assertNotNull(history, "После добавления задачи, история не должна быть пустой."); - assertEquals(1, history.size(), "После добавления задачи, история не должна быть пустой."); + void shouldReturnEmptyHistoryWhenNoTasksWereAdded() { + List history = historyManager.getHistory(); - assertEquals(task.getName(), history.get(0).getName(), "Имя задачи в истории не совпадает"); - assertEquals(task.getDescription(), history.get(0).getDescription(), "Описание задачи в истории не совпадает"); - assertEquals(task.getStatus(), history.get(0).getStatus(), "Статус задачи в истории не совпадает"); - assertEquals(task.getId(), history.get(0).getId(), "ID задачи в истории не совпадает"); + assertNotNull(history, "История не должна быть null."); + assertTrue(history.isEmpty(), "История должна быть пустой."); } - // При повторном добавлении задачи в историю дубликат не создастся + // Проверка добавления задачи в историю @Test - void addShouldNotCreateDuplicates() { - Task task = new Task("Task", "Description", Status.NEW); + void shouldAddTaskToHistory() { + Task task = new Task( + "Task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0) + ); task.setId(1); historyManager.add(task); - historyManager.add(task); // добавляем второй раз - historyManager.add(task); // и третий - assertEquals(1, historyManager.getHistory().size(), "История не должна содержать дубликаты"); - } + List history = historyManager.getHistory(); - // История не должна ограничиваться 10 элементами - @Test - void historyShouldBeUnlimited() { - for (int i = 1; i <= 15; i++) { - Task task = new Task("Task " + i, "Description", Status.NEW); - task.setId(i); - historyManager.add(task); - } - - assertEquals(15, historyManager.getHistory().size(), "История должна хранить более 10 элементов"); + assertEquals(1, history.size(), "История должна содержать одну задачу."); + assertEquals(task.getId(), history.get(0).getId(), "В истории должна быть добавленная задача."); } - // Удаление из начала истории + // Проверка удаления дубликатов в истории @Test - void removeShouldDeleteFromBeginning() { - Task task1 = new Task("Task1", "Desc", Status.NEW); - task1.setId(1); - Task task2 = new Task("Task2", "Desc", Status.NEW); - task2.setId(2); - Task task3 = new Task("Task3", "Desc", Status.NEW); - task3.setId(3); - - historyManager.add(task1); - historyManager.add(task2); - historyManager.add(task3); + void shouldKeepOnlyOneTaskWhenTaskAddedTwice() { + Task task = new Task( + "Task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0) + ); + task.setId(1); - historyManager.remove(1); // удаление первой - головы + historyManager.add(task); + historyManager.add(task); List history = historyManager.getHistory(); - assertEquals(2, history.size(), "В истории должно остаться 2 задачи"); - assertEquals(2, history.get(0).getId(), "Первой должна быть task2"); + + assertEquals(1, history.size(), "Дубликаты не должны сохраняться в истории."); + assertEquals(task.getId(), history.get(0).getId(), "В истории должна остаться одна задача."); } - // Удаление из середины истории + // Проверка удаления задачи из начала истории @Test - void removeShouldDeleteFromMiddle() { - Task task1 = new Task("Task1", "Desc", Status.NEW); + void shouldRemoveTaskFromBeginningOfHistory() { + Task task1 = new Task("Task 1", "Description 1", Status.NEW, + Duration.ofMinutes(10), LocalDateTime.of(2026, 4, 16, 9, 0)); task1.setId(1); - Task task2 = new Task("Task2", "Desc", Status.NEW); + + Task task2 = new Task("Task 2", "Description 2", Status.NEW, + Duration.ofMinutes(20), LocalDateTime.of(2026, 4, 16, 10, 0)); task2.setId(2); - Task task3 = new Task("Task3", "Desc", Status.NEW); - task3.setId(3); historyManager.add(task1); historyManager.add(task2); - historyManager.add(task3); - historyManager.remove(2); // удаление из середины + historyManager.remove(1); List history = historyManager.getHistory(); - assertEquals(2, history.size(), "В истории должно остаться 2 задачи"); - assertEquals(1, history.get(0).getId(), "Первой должна быть task1"); - assertEquals(3, history.get(1).getId(), "Второй должна быть task3"); + + assertEquals(1, history.size(), "После удаления в истории должна остаться одна задача."); + assertEquals(2, history.get(0).getId(), "В истории должна остаться вторая задача."); } - // Удаление из конца истории + // Проверка удаления задачи из середины истории @Test - void removeShouldDeleteFromEnd() { - Task task1 = new Task("Task1", "Desc", Status.NEW); + void shouldRemoveTaskFromMiddleOfHistory() { + Task task1 = new Task("Task 1", "Description 1", Status.NEW, + Duration.ofMinutes(10), LocalDateTime.of(2026, 4, 16, 9, 0)); task1.setId(1); - Task task2 = new Task("Task2", "Desc", Status.NEW); + + Task task2 = new Task("Task 2", "Description 2", Status.NEW, + Duration.ofMinutes(20), LocalDateTime.of(2026, 4, 16, 10, 0)); task2.setId(2); - Task task3 = new Task("Task3", "Desc", Status.NEW); + + Task task3 = new Task("Task 3", "Description 3", Status.NEW, + Duration.ofMinutes(30), LocalDateTime.of(2026, 4, 16, 11, 0)); task3.setId(3); historyManager.add(task1); historyManager.add(task2); historyManager.add(task3); - historyManager.remove(3); // удаление последней - хвоста + historyManager.remove(2); List history = historyManager.getHistory(); - assertEquals(2, history.size(), "В истории должно остаться 2 задачи"); - assertEquals(2, history.get(1).getId(), "Последней должна быть task2"); + + assertEquals(2, history.size(), "После удаления в истории должно остаться две задачи."); + assertEquals(1, history.get(0).getId(), "Первая задача должна остаться."); + assertEquals(3, history.get(1).getId(), "Третья задача должна остаться."); } - // Повторный просмотр задачи должен переместить её в конец истории + // Проверка удаления задачи из конца истории @Test - void repeatedViewShouldMoveTaskToEnd() { - Task task1 = new Task("Task1", "Desc", Status.NEW); + void shouldRemoveTaskFromEndOfHistory() { + Task task1 = new Task("Task 1", "Description 1", Status.NEW, + Duration.ofMinutes(10), LocalDateTime.of(2026, 4, 16, 9, 0)); task1.setId(1); - Task task2 = new Task("Task2", "Desc", Status.NEW); + + Task task2 = new Task("Task 2", "Description 2", Status.NEW, + Duration.ofMinutes(20), LocalDateTime.of(2026, 4, 16, 10, 0)); task2.setId(2); - Task task3 = new Task("Task3", "Desc", Status.NEW); - task3.setId(3); historyManager.add(task1); historyManager.add(task2); - historyManager.add(task3); - historyManager.add(task1); // task1 повторно — должна уйти в конец - List history = historyManager.getHistory(); - - assertEquals(3, history.size(), "Дубликатов быть не должно"); - assertEquals(2, history.get(0).getId(), "Первой должна быть task2"); - assertEquals(3, history.get(1).getId(), "Второй должна быть task3"); - assertEquals(1, history.get(2).getId(), "Последней должна быть task1"); - } + historyManager.remove(2); - // Пустая история должна возвращать пустой список - @Test - void emptyHistoryShouldReturnEmptyList() { List history = historyManager.getHistory(); - assertNotNull(history, "getHistory() не должен возвращать null"); - assertEquals(0, history.size(), "Пустая история должна содержать 0 элементов"); + + assertEquals(1, history.size(), "После удаления в истории должна остаться одна задача."); + assertEquals(1, history.get(0).getId(), "В истории должна остаться первая задача."); } - // Статус задачи в истории не должен меняться при изменении оригинала через сеттер + // Проверка сохранения новых полей во snapshot истории @Test - void historyShouldStoreSnapshotNotReference() { - Task task = new Task("Task", "Desc", Status.NEW); - task.setId(1); + void shouldSaveTimeFieldsInHistorySnapshot() { + Subtask subtask = new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(40), + LocalDateTime.of(2026, 4, 16, 13, 0), + 100 + ); + subtask.setId(1); + + historyManager.add(subtask); - historyManager.add(task); + List history = historyManager.getHistory(); - // Меняем оригинал после добавления в историю - task.setName("Изменённое имя"); - task.setStatus(Status.DONE); + assertEquals(1, history.size(), "История должна содержать одну подзадачу."); + Task savedTask = history.get(0); - Task inHistory = historyManager.getHistory().get(0); - assertEquals("Task", inHistory.getName(), - "История должна хранить снимок: имя не должно измениться"); - assertEquals(Status.NEW, inHistory.getStatus(), - "История должна хранить снимок: статус не должен измениться"); + assertEquals(Duration.ofMinutes(40), savedTask.getDuration(), + "Продолжительность должна сохраниться в snapshot истории."); + assertEquals(LocalDateTime.of(2026, 4, 16, 13, 0), savedTask.getStartTime(), + "Время начала должно сохраниться в snapshot истории."); + assertEquals(LocalDateTime.of(2026, 4, 16, 13, 40), savedTask.getEndTime(), + "Время окончания должно корректно рассчитываться в snapshot истории."); } } \ No newline at end of file diff --git a/test/service/InMemoryTaskManagerTest.java b/test/service/InMemoryTaskManagerTest.java index 8a43bf2..bc0f78c 100644 --- a/test/service/InMemoryTaskManagerTest.java +++ b/test/service/InMemoryTaskManagerTest.java @@ -1,271 +1,9 @@ package service; -import model.Status; -import model.Task; -import model.Epic; -import model.Subtask; +class InMemoryTaskManagerTest extends TaskManagerTest { -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -class InMemoryTaskManagerTest { - - private TaskManager taskManager; - - @BeforeEach - void setUp() { - taskManager = new InMemoryTaskManager(); - } - - //InMemoryTaskManager действительно добавляет задачи разного типа и может найти их по id - @Test - void addNewTask() { - Task task = new Task("Task", "Task Description", Status.NEW); - taskManager.createTask(task); - - final Task savedTask = taskManager.getTask(task.getId()); - - assertNotNull(savedTask, "Задача не найдена."); - assertEquals(task, savedTask, "Задачи не совпадают."); - - final List tasks = taskManager.getTasks(); - assertNotNull(tasks, "Задачи не возвращаются."); - assertEquals(1, tasks.size(), "Неверное количество задач."); - assertEquals(task, tasks.get(0), "Задачи не совпадают"); - } - - @Test - void addNewEpic() { - Epic epic = new Epic("Epic", "Epic Description"); - taskManager.createEpic(epic); - - final Epic savedEpic = taskManager.getEpic(epic.getId()); - - assertNotNull(savedEpic, "Эпик не найден."); - assertEquals(epic, savedEpic, "Эпики не совпадают."); - - final List epics = taskManager.getEpics(); - assertEquals(1, epics.size(), "Неверное количество эпиков."); - } - - @Test - void addNewSubtask() { - Epic epic = new Epic("Epic", "Epic Description"); - taskManager.createEpic(epic); - - Subtask subtask = new Subtask("Subtask", "Subtask Description", Status.NEW, epic.getId()); - taskManager.createSubtask(subtask); - - final Subtask savedSubtask = taskManager.getSubtask(subtask.getId()); - - assertNotNull(savedSubtask, "Подзадача не найдена."); - assertEquals(subtask, savedSubtask, "Подзадачи не совпадают."); - assertEquals(epic.getId(), savedSubtask.getEpicId(), "У Подзадачи неверный EpicID"); - - final List subtasks = taskManager.getSubtasks(); - assertEquals(1, subtasks.size(), "Неверное количество подзадач."); - } - - // Задачи с заданным id и сгенерированным id не конфликтуют внутри менеджера - @Test - void taskWithGeneratedIdShouldNotConflict() { - Task task = new Task("Task", "Task Description", Status.NEW); - task.setId(999); - - taskManager.createTask(task); - - assertNotEquals(999, task.getId(), - "Менеджер должен игнорировать заданный вручную ID и генерировать уникальный"); - - Task savedTask = taskManager.getTask(task.getId()); - assertNotNull(savedTask, "Задача должна быть найдена по сгенерированному ID"); - assertEquals(task, savedTask); - } - - // Tест, в котором проверяется неизменность задачи (по всем полям) при добавлении задачи в менеджер - @Test - void taskShouldBeUnchangedAfterAddingToManager() { - // Эталонные данные - String expectedName = "Orig Name"; - String expectedDescription = "Orig Description"; - Status expectedStatus = Status.NEW; - - Task task = new Task(expectedName, expectedDescription, expectedStatus); - - // Добавляем её - taskManager.createTask(task); - - // Забираем - Task savedTask = taskManager.getTask(task.getId()); - - // Проверяем - assertEquals(expectedName, savedTask.getName(), "Имя задачи изменилось при сохранении"); - assertEquals(expectedDescription, savedTask.getDescription(), - "Описание задачи изменилось при сохранении"); - assertEquals(expectedStatus, savedTask.getStatus(), "Статус задачи изменился при сохранении"); - } - - // Если удалена задача, то она должна исчезнуть из истории - @Test - void deletedTaskShouldBeRemovedFromHistory() { - Task task = new Task("Task", "Description", Status.NEW); - taskManager.createTask(task); - - taskManager.getTask(task.getId()); - assertEquals(1, taskManager.getHistory().size(), "Задача должна быть в истории"); - - taskManager.deleteTask(task.getId()); // удаляем - assertEquals(0, taskManager.getHistory().size(), - "Удалённая задача не должна оставаться в истории"); - } - - // При удалении эпика из истории удаляется и сам эпик, и все его подзадачи - @Test - void deletedEpicShouldBeRemovedFromHistoryWithSubtasks() { - Epic epic = new Epic("Epic", "Description"); - taskManager.createEpic(epic); - - Subtask subtask1 = new Subtask("Sub1", "Desc", Status.NEW, epic.getId()); - Subtask subtask2 = new Subtask("Sub2", "Desc", Status.NEW, epic.getId()); - taskManager.createSubtask(subtask1); - taskManager.createSubtask(subtask2); - - // Просматриваем всё — всё попадает в историю - taskManager.getEpic(epic.getId()); - taskManager.getSubtask(subtask1.getId()); - taskManager.getSubtask(subtask2.getId()); - assertEquals(3, taskManager.getHistory().size(), "В истории должно быть 3 элемента"); - - taskManager.deleteEpic(epic.getId()); // удаляем эпик - assertEquals(0, taskManager.getHistory().size(), - "После удаления эпика история должна быть пустой"); - } - - // Если удалена подзадача, то её id не должен оставаться внутри эпика - @Test - void deletedSubtaskShouldBeRemovedFromEpic() { - Epic epic = new Epic("Epic", "Description"); - taskManager.createEpic(epic); - - Subtask subtask = new Subtask("Sub", "Desc", Status.NEW, epic.getId()); - taskManager.createSubtask(subtask); - - int subtaskId = subtask.getId(); - taskManager.deleteSubtask(subtaskId); - - assertEquals(0, taskManager.getEpicSubtasks(epic.getId()).size(), - "После удаления подзадачи эпик не должен содержать её id"); - } - - // Изменение задачи через сеттер не должно влиять на данные внутри менеджера - @Test - void taskShouldNotChangeInManagerAfterSetterCall() { - Task task = new Task("Оригинальное имя", "Описание", Status.NEW); - taskManager.createTask(task); - - Task savedTask = taskManager.getTask(task.getId()); - savedTask.setName("Изменённое имя"); - - assertEquals("Оригинальное имя", - taskManager.getTask(task.getId()).getName(), - "Сеттер не должен менять данные задачи внутри менеджера"); - } - - // clearTasks() должен удалять задачи из истории - @Test - void clearTasksShouldRemoveTasksFromHistory() { - Task task1 = new Task("Task1", "Desc", Status.NEW); - Task task2 = new Task("Task2", "Desc", Status.NEW); - taskManager.createTask(task1); - taskManager.createTask(task2); - - taskManager.getTask(task1.getId()); - taskManager.getTask(task2.getId()); - assertEquals(2, taskManager.getHistory().size(), - "Перед очисткой в истории должно быть 2 задачи"); - - taskManager.clearTasks(); - - assertEquals(0, taskManager.getHistory().size(), - "После clearTasks история должна быть пустой"); - } - - // clearEpics() должен удалять эпики и их подзадачи из истории - @Test - void clearEpicsShouldRemoveEpicsAndSubtasksFromHistory() { - Epic epic = new Epic("Epic", "Desc"); - taskManager.createEpic(epic); - - Subtask subtask = new Subtask("Sub", "Desc", Status.NEW, epic.getId()); - taskManager.createSubtask(subtask); - - taskManager.getEpic(epic.getId()); - taskManager.getSubtask(subtask.getId()); - assertEquals(2, taskManager.getHistory().size(), - "Перед очисткой в истории должно быть 2 элемента"); - - taskManager.clearEpics(); - - assertEquals(0, taskManager.getHistory().size(), - "После clearEpics история должна быть пустой"); - } - - // Изменение подзадачи через сеттер не должно влиять на данные внутри менеджера - @Test - void subtaskShouldNotChangeInManagerAfterSetterCall() { - Epic epic = new Epic("Epic", "Desc"); - taskManager.createEpic(epic); - - Subtask subtask = new Subtask("Оригинальное имя", "Описание", Status.NEW, epic.getId()); - taskManager.createSubtask(subtask); - - Subtask savedSubtask = taskManager.getSubtask(subtask.getId()); - savedSubtask.setName("Изменённое имя"); - - assertEquals("Оригинальное имя", - taskManager.getSubtask(subtask.getId()).getName(), - "Сеттер не должен менять данные подзадачи внутри менеджера"); - } - - // clearSubtasks() должен удалять подзадачи из истории - @Test - void clearSubtasksShouldRemoveSubtasksFromHistory() { - Epic epic = new Epic("Epic", "Desc"); - taskManager.createEpic(epic); - - Subtask subtask = new Subtask("Sub", "Desc", Status.NEW, epic.getId()); - taskManager.createSubtask(subtask); - - taskManager.getSubtask(subtask.getId()); // добавление в историю - assertEquals(1, taskManager.getHistory().size(), - "Перед очисткой в истории должна быть 1 подзадача"); - - taskManager.clearSubtasks(); - - assertEquals(0, taskManager.getHistory().size(), - "После clearSubtasks история должна быть пустой"); - } - - // Проверка неизменности эпика по всем полям при добавлении в менеджер - @Test - void epicShouldBeUnchangedAfterAddingToManager() { - String expectedName = "Оригинальный эпик"; - String expectedDescription = "Описание эпика"; - - Epic epic = new Epic(expectedName, expectedDescription); - taskManager.createEpic(epic); - - Epic savedEpic = taskManager.getEpic(epic.getId()); - - assertEquals(expectedName, savedEpic.getName(), - "Имя эпика изменилось при сохранении"); - assertEquals(expectedDescription, savedEpic.getDescription(), - "Описание эпика изменилось при сохранении"); + @Override + protected InMemoryTaskManager createTaskManager() { + return new InMemoryTaskManager(); } } \ No newline at end of file diff --git a/test/service/TaskManagerTest.java b/test/service/TaskManagerTest.java new file mode 100644 index 0000000..a45d295 --- /dev/null +++ b/test/service/TaskManagerTest.java @@ -0,0 +1,728 @@ +package service; + +import model.Epic; +import model.Status; +import model.Subtask; +import model.Task; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import service.exception.NotFoundException; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +abstract class TaskManagerTest { + + protected T taskManager; + + // Каждый наследник сам создаёт свою реализацию менеджера + protected abstract T createTaskManager(); + + @BeforeEach + void setUp() { + taskManager = createTaskManager(); + } + + // Проверка создания обычной задачи + @Test + void shouldCreateTask() { + Task task = new Task( + "Task name", + "Task description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0) + ); + + Task createdTask = taskManager.createTask(task); + + assertNotNull(createdTask, "Задача должна создаться."); + assertNotNull(taskManager.getTask(createdTask.getId()), "Задача должна находиться по id."); + assertEquals("Task name", createdTask.getName(), "Имя задачи должно сохраниться."); + assertEquals(Duration.ofMinutes(30), createdTask.getDuration(), + "Продолжительность задачи должна сохраниться."); + assertEquals(LocalDateTime.of(2026, 4, 16, 10, 0), createdTask.getStartTime(), + "Время начала задачи должно сохраниться."); + } + + // Проверка создания эпика + @Test + void shouldCreateEpic() { + Epic epic = new Epic("Epic name", "Epic description"); + + Epic createdEpic = taskManager.createEpic(epic); + + assertNotNull(createdEpic, "Эпик должен создаться."); + assertNotNull(taskManager.getEpic(createdEpic.getId()), "Эпик должен находиться по id."); + assertEquals("Epic name", createdEpic.getName(), "Имя эпика должно сохраниться."); + } + + // Проверка создания подзадачи и связи с эпиком + @Test + void shouldCreateSubtaskWithEpic() { + Epic epic = new Epic("Epic", "Epic description"); + Epic createdEpic = taskManager.createEpic(epic); + + Subtask subtask = new Subtask( + "Subtask", + "Subtask description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 12, 0), + createdEpic.getId() + ); + + Subtask createdSubtask = taskManager.createSubtask(subtask); + + assertNotNull(createdSubtask, "Подзадача должна создаться."); + assertEquals(createdEpic.getId(), createdSubtask.getEpicId(), + "Подзадача должна хранить id связанного эпика."); + + List epicSubtasks = taskManager.getEpicSubtasks(createdEpic.getId()); + assertEquals(1, epicSubtasks.size(), "У эпика должна быть одна подзадача."); + assertEquals(createdSubtask.getId(), epicSubtasks.get(0).getId(), + "Подзадача должна входить в список подзадач эпика."); + } + + // Проверка расчёта статуса эпика: все подзадачи NEW + @Test + void shouldSetEpicStatusNewWhenAllSubtasksAreNew() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + taskManager.createSubtask(new Subtask( + "Subtask 1", + "Description 1", + Status.NEW, + Duration.ofMinutes(10), + LocalDateTime.of(2026, 4, 16, 9, 0), + epic.getId() + )); + + taskManager.createSubtask(new Subtask( + "Subtask 2", + "Description 2", + Status.NEW, + Duration.ofMinutes(15), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + assertEquals(Status.NEW, savedEpic.getStatus(), + "Статус эпика должен быть NEW, если все подзадачи NEW."); + } + + // Проверка расчёта статуса эпика: все подзадачи DONE + @Test + void shouldSetEpicStatusDoneWhenAllSubtasksAreDone() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + Subtask subtask1 = taskManager.createSubtask(new Subtask( + "Subtask 1", + "Description 1", + Status.NEW, + Duration.ofMinutes(10), + LocalDateTime.of(2026, 4, 16, 9, 0), + epic.getId() + )); + + Subtask subtask2 = taskManager.createSubtask(new Subtask( + "Subtask 2", + "Description 2", + Status.NEW, + Duration.ofMinutes(15), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + subtask1.setStatus(Status.DONE); + subtask2.setStatus(Status.DONE); + + taskManager.updateSubtask(subtask1); + taskManager.updateSubtask(subtask2); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + assertEquals(Status.DONE, savedEpic.getStatus(), + "Статус эпика должен быть DONE, если все подзадачи DONE."); + } + + // Проверка расчёта статуса эпика: подзадачи NEW и DONE + @Test + void shouldSetEpicStatusInProgressWhenSubtasksAreNewAndDone() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + Subtask subtask1 = taskManager.createSubtask(new Subtask( + "Subtask 1", + "Description 1", + Status.NEW, + Duration.ofMinutes(10), + LocalDateTime.of(2026, 4, 16, 9, 0), + epic.getId() + )); + + Subtask subtask2 = taskManager.createSubtask(new Subtask( + "Subtask 2", + "Description 2", + Status.NEW, + Duration.ofMinutes(15), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + subtask2.setStatus(Status.DONE); + + taskManager.updateSubtask(subtask1); + taskManager.updateSubtask(subtask2); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + assertEquals(Status.IN_PROGRESS, savedEpic.getStatus(), + "Статус эпика должен быть IN_PROGRESS, если подзадачи NEW и DONE."); + } + + // Проверка расчёта статуса эпика: есть подзадача IN_PROGRESS + @Test + void shouldSetEpicStatusInProgressWhenSubtaskIsInProgress() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + Subtask subtask = taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 9, 0), + epic.getId() + )); + + subtask.setStatus(Status.IN_PROGRESS); + taskManager.updateSubtask(subtask); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + assertEquals(Status.IN_PROGRESS, savedEpic.getStatus(), + "Статус эпика должен быть IN_PROGRESS, если есть подзадача IN_PROGRESS."); + } + + // Проверка приоритетного списка задач + @Test + void shouldReturnPrioritizedTasksSortedByStartTime() { + Task laterTask = taskManager.createTask(new Task( + "Later task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 12, 0) + )); + + Task earlierTask = taskManager.createTask(new Task( + "Earlier task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 9, 0) + )); + + List prioritizedTasks = taskManager.getPrioritizedTasks(); + + assertEquals(2, prioritizedTasks.size(), "Должно быть две задачи в списке приоритетов."); + assertEquals(earlierTask.getId(), prioritizedTasks.get(0).getId(), + "Первая задача должна быть с более ранним startTime."); + assertEquals(laterTask.getId(), prioritizedTasks.get(1).getId(), + "Вторая задача должна быть с более поздним startTime."); + } + + // Проверка, что задача без startTime не попадает в приоритетный список + @Test + void shouldNotAddTaskWithoutStartTimeToPrioritizedTasks() { + Task taskWithoutTime = new Task( + "Task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + null + ); + + taskManager.createTask(taskWithoutTime); + + List prioritizedTasks = taskManager.getPrioritizedTasks(); + assertTrue(prioritizedTasks.isEmpty(), + "Задача без startTime не должна попадать в список приоритетов."); + } + + // Проверка пересечения интервалов при создании задачи + @Test + void shouldThrowExceptionWhenCreatingOverlappingTask() { + taskManager.createTask(new Task( + "Task 1", + "Description", + Status.NEW, + Duration.ofMinutes(60), + LocalDateTime.of(2026, 4, 16, 10, 0) + )); + + assertThrows( + IllegalArgumentException.class, + () -> taskManager.createTask(new Task( + "Task 2", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 30) + )), + "Создание пересекающейся задачи должно приводить к исключению." + ); + } + + // Проверка времени эпика по подзадачам + @Test + void shouldCalculateEpicTimeFromSubtasks() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + taskManager.createSubtask(new Subtask( + "Subtask 1", + "Description 1", + Status.NEW, + Duration.ofMinutes(60), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + taskManager.createSubtask(new Subtask( + "Subtask 2", + "Description 2", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 12, 0), + epic.getId() + )); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + + assertEquals(Duration.ofMinutes(90), savedEpic.getDuration(), + "Продолжительность эпика должна быть суммой продолжительностей подзадач."); + assertEquals(LocalDateTime.of(2026, 4, 16, 10, 0), savedEpic.getStartTime(), + "Время начала эпика должно быть временем начала самой ранней подзадачи."); + assertEquals(LocalDateTime.of(2026, 4, 16, 12, 30), savedEpic.getEndTime(), + "Время окончания эпика должно быть временем окончания самой поздней подзадачи."); + } + + // Проверка очистки всех задач + @Test + void shouldClearAllTasks() { + Task task = taskManager.createTask(new Task( + "Task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0) + )); + + taskManager.getTask(task.getId()); + taskManager.clearTasks(); + + assertTrue(taskManager.getTasks().isEmpty(), "Список задач должен быть пустым."); + assertThrows( + NotFoundException.class, + () -> taskManager.getTask(task.getId()), + "После очистки задача должна выбрасывать NotFoundException." + ); + assertTrue(taskManager.getHistory().isEmpty(), "История должна очищаться от удалённых задач."); + assertTrue(taskManager.getPrioritizedTasks().isEmpty(), "Приоритетный список должен очищаться."); + } + + // Проверка очистки всех подзадач и сброса полей эпика + @Test + void shouldClearAllSubtasksAndResetEpicFields() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + taskManager.clearSubtasks(); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + + assertTrue(taskManager.getSubtasks().isEmpty(), "Список подзадач должен быть пустым."); + assertTrue(savedEpic.getSubtaskIds().isEmpty(), "У эпика не должно остаться подзадач."); + assertEquals(Status.NEW, savedEpic.getStatus(), "Статус эпика должен стать NEW."); + assertNull(savedEpic.getStartTime(), "Время начала эпика должно стать null."); + assertNull(savedEpic.getEndTime(), "Время окончания эпика должно стать null."); + assertNull(savedEpic.getDuration(), "Продолжительность эпика должна стать null."); + } + + // Проверка очистки всех эпиков и связанных подзадач + @Test + void shouldClearAllEpicsAndSubtasks() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + Subtask subtask = taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 11, 0), + epic.getId() + )); + + taskManager.getEpic(epic.getId()); + taskManager.getSubtask(subtask.getId()); + + taskManager.clearEpics(); + + assertTrue(taskManager.getEpics().isEmpty(), "Список эпиков должен быть пустым."); + assertTrue(taskManager.getSubtasks().isEmpty(), "Список подзадач должен быть пустым."); + assertThrows( + NotFoundException.class, + () -> taskManager.getEpic(epic.getId()), + "После очистки эпик должен выбрасывать NotFoundException." + ); + + assertThrows( + NotFoundException.class, + () -> taskManager.getSubtask(subtask.getId()), + "После очистки подзадача должна выбрасывать NotFoundException." + ); + assertTrue(taskManager.getHistory().isEmpty(), "История должна очищаться."); + } + + // Проверка обновления задач, подзадач и эпиков + @Test + void shouldUpdateTask() { + Task task = taskManager.createTask(new Task( + "Old name", + "Old description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0) + )); + + Task updatedTask = new Task( + "New name", + "New description", + Status.IN_PROGRESS, + Duration.ofMinutes(45), + LocalDateTime.of(2026, 4, 16, 12, 0) + ); + updatedTask.setId(task.getId()); + + taskManager.updateTask(updatedTask); + + Task savedTask = taskManager.getTask(task.getId()); + + assertEquals("New name", savedTask.getName()); + assertEquals("New description", savedTask.getDescription()); + assertEquals(Status.IN_PROGRESS, savedTask.getStatus()); + assertEquals(Duration.ofMinutes(45), savedTask.getDuration()); + assertEquals(LocalDateTime.of(2026, 4, 16, 12, 0), savedTask.getStartTime()); + } + + @Test + void shouldUpdateEpicWithoutLosingCalculatedFields() { + Epic epic = taskManager.createEpic(new Epic("Old epic", "Old description")); + + taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + Epic updatedEpic = new Epic("New epic", "New description"); + updatedEpic.setId(epic.getId()); + + taskManager.updateEpic(updatedEpic); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + + assertEquals("New epic", savedEpic.getName()); + assertEquals("New description", savedEpic.getDescription()); + assertEquals(1, savedEpic.getSubtaskIds().size(), "Связь с подзадачами должна сохраниться."); + assertEquals(Status.NEW, savedEpic.getStatus(), "Статус не должен теряться."); + assertEquals(Duration.ofMinutes(30), savedEpic.getDuration(), "Продолжительность не должна теряться."); + assertEquals(LocalDateTime.of(2026, 4, 16, 10, 0), savedEpic.getStartTime(), "StartTime не должен теряться."); + assertEquals(LocalDateTime.of(2026, 4, 16, 10, 30), savedEpic.getEndTime(), "EndTime не должен теряться."); + } + + @Test + void shouldUpdateSubtaskAndRecalculateEpic() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + Subtask subtask = taskManager.createSubtask(new Subtask( + "Old subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 9, 0), + epic.getId() + )); + + Subtask updatedSubtask = new Subtask( + "New subtask", + "New description", + Status.DONE, + Duration.ofMinutes(40), + LocalDateTime.of(2026, 4, 16, 11, 0), + epic.getId() + ); + updatedSubtask.setId(subtask.getId()); + + taskManager.updateSubtask(updatedSubtask); + + Subtask savedSubtask = taskManager.getSubtask(subtask.getId()); + Epic savedEpic = taskManager.getEpic(epic.getId()); + + assertEquals("New subtask", savedSubtask.getName()); + assertEquals(Status.DONE, savedSubtask.getStatus()); + assertEquals(Duration.ofMinutes(40), savedSubtask.getDuration()); + assertEquals(LocalDateTime.of(2026, 4, 16, 11, 0), savedSubtask.getStartTime()); + + assertEquals(Status.DONE, savedEpic.getStatus(), "Статус эпика должен пересчитаться."); + assertEquals(Duration.ofMinutes(40), savedEpic.getDuration(), "Продолжительность эпика должна пересчитаться."); + } + + // Проверка получения списка подзадач эпика + @Test + void shouldReturnEmptyListForEpicWithoutSubtasks() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + List epicSubtasks = taskManager.getEpicSubtasks(epic.getId()); + + assertTrue(epicSubtasks.isEmpty(), "У нового эпика список подзадач должен быть пустым."); + } + + @Test + void shouldReturnEmptyListForNonExistingEpicSubtasks() { + assertThrows( + NotFoundException.class, + () -> taskManager.getEpicSubtasks(999), + "Для несуществующего эпика должен выбрасываться NotFoundException." + ); + } + + // Проверка сортировки задач и подзадач в приоритетном списке + @Test + void shouldReturnTasksAndSubtasksSortedByStartTime() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + Task task = taskManager.createTask(new Task( + "Task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 12, 0) + )); + + Subtask subtask = taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + List prioritizedTasks = taskManager.getPrioritizedTasks(); + + assertEquals(2, prioritizedTasks.size(), "В приоритетном списке должны быть задача и подзадача."); + assertEquals(subtask.getId(), prioritizedTasks.get(0).getId(), "Подзадача с более ранним startTime должна быть первой."); + assertEquals(task.getId(), prioritizedTasks.get(1).getId(), "Задача с более поздним startTime должна быть второй."); + } + + // Проверка удаления задач, подзадач и эпиков + @Test + void shouldDeleteTaskById() { + Task task = taskManager.createTask(new Task( + "Task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0) + )); + + taskManager.getTask(task.getId()); + taskManager.deleteTask(task.getId()); + + assertThrows( + NotFoundException.class, + () -> taskManager.getTask(task.getId()), + "После удаления задача должна выбрасывать NotFoundException." + ); + assertTrue(taskManager.getTasks().isEmpty(), "Список задач должен быть пустым."); + assertTrue(taskManager.getHistory().isEmpty(), "История должна очищаться от удалённой задачи."); + } + + @Test + void shouldDeleteSubtaskAndUpdateEpic() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + Subtask subtask = taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + taskManager.deleteSubtask(subtask.getId()); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + + assertThrows( + NotFoundException.class, + () -> taskManager.getSubtask(subtask.getId()), + "После удаления подзадача должна выбрасывать NotFoundException." + ); + assertTrue(savedEpic.getSubtaskIds().isEmpty(), "У эпика не должно остаться подзадач."); + assertEquals(Status.NEW, savedEpic.getStatus(), "Статус эпика должен пересчитаться."); + assertNull(savedEpic.getDuration(), "Продолжительность эпика должна стать null."); + } + + @Test + void shouldDeleteEpicWithItsSubtasks() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + Subtask subtask = taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + taskManager.deleteEpic(epic.getId()); + + assertThrows( + NotFoundException.class, + () -> taskManager.getEpic(epic.getId()), + "После удаления эпик должен выбрасывать NotFoundException." + ); + + assertThrows( + NotFoundException.class, + () -> taskManager.getSubtask(subtask.getId()), + "После удаления эпика его подзадача тоже должна выбрасывать NotFoundException." + ); + assertTrue(taskManager.getEpics().isEmpty(), "Список эпиков должен быть пустым."); + assertTrue(taskManager.getSubtasks().isEmpty(), "Список подзадач должен быть пустым."); + } + + // Проверка граничных случаев поиска задач, эпиков и подзадач + @Test + void shouldThrowNotFoundExceptionWhenTaskNotFound() { + assertThrows( + NotFoundException.class, + () -> taskManager.getTask(999), + "Несуществующая задача должна выбрасывать NotFoundException." + ); + } + + @Test + void shouldThrowNotFoundExceptionWhenEpicNotFound() { + assertThrows( + NotFoundException.class, + () -> taskManager.getEpic(999), + "Несуществующий эпик должен выбрасывать NotFoundException." + ); + } + + @Test + void shouldThrowNotFoundExceptionWhenSubtaskNotFound() { + assertThrows( + NotFoundException.class, + () -> taskManager.getSubtask(999), + "Несуществующая подзадача должна выбрасывать NotFoundException." + ); + } + + // Проверка добавления задач в историю просмотров + @Test + void shouldAddViewedTaskToHistory() { + Task task = taskManager.createTask(new Task( + "Task", + "Description", + Status.NEW, + Duration.ofMinutes(15), + LocalDateTime.of(2026, 4, 16, 8, 0) + )); + + taskManager.getTask(task.getId()); + + List history = taskManager.getHistory(); + + assertEquals(1, history.size(), "После просмотра задача должна попасть в историю."); + assertEquals(task.getId(), history.get(0).getId(), "В истории должна быть просмотренная задача."); + } + + // Проверка создания подзадачи без существующего эпика + @Test + void shouldNotCreateSubtaskWithoutExistingEpic() { + Subtask subtask = new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 10, 0), + 999 + ); + + assertThrows( + NotFoundException.class, + () -> taskManager.createSubtask(subtask), + "Подзадача без существующего эпика должна выбрасывать NotFoundException." + ); + + assertTrue(taskManager.getSubtasks().isEmpty(), "Список подзадач должен остаться пустым."); + } + + // Проверка пересечения временных интервалов задач и подзадач + @Test + void shouldAllowTasksThatTouchBordersButDoNotOverlap() { + taskManager.createTask(new Task( + "Task 1", + "Description", + Status.NEW, + Duration.ofMinutes(60), + LocalDateTime.of(2026, 4, 16, 10, 0) + )); + + assertDoesNotThrow(() -> taskManager.createTask(new Task( + "Task 2", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 11, 0) + )), "Задачи, соприкасающиеся границами, не должны считаться пересекающимися."); + } + + @Test + void shouldThrowExceptionWhenCreatingOverlappingSubtask() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + taskManager.createTask(new Task( + "Task 1", + "Description", + Status.NEW, + Duration.ofMinutes(60), + LocalDateTime.of(2026, 4, 16, 10, 0) + )); + + assertThrows(IllegalArgumentException.class, () -> taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 10, 30), + epic.getId() + )), "Подзадача не должна создаваться, если пересекается по времени с другой задачей."); + } +} \ No newline at end of file