diff --git a/.idea/libraries/gson_2_9_0.xml b/.idea/libraries/gson_2_9_0.xml
new file mode 100644
index 0000000..2377008
--- /dev/null
+++ b/.idea/libraries/gson_2_9_0.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index be7c42d..9e28842 100644
--- a/README.md
+++ b/README.md
@@ -1,43 +1,147 @@
-# java-kanban
-### Основа для менеджера задач
+# Java Kanban - Менеджер задач
----
-## Содержание
-- #### Main - пример добавления задачи
-- #### TaskManager - основные методы работы программы
-- #### Task - родитель всех задач
-- #### Epic - задача, способная хранить другие задачи
-- #### SubTask - задача, принадлежащая эпику
-- #### Status - статусы задач
+## Описание проекта
----
+**Java Kanban** - это бэкенд-система для управления задачами, реализующая менеджер задач и HTTP-сервер.
-## Особенности задач
-### Каждая задача обладает:
+Система поддерживает три типа задач: обычные задачи(Task), эпики(Epic) и подзадачи(SubTask).
-- #### уникальным id
-- #### названием
-- #### описанием
-- #### статусом
+## Функциональные возможности
-#### Эпик хранит каждую подзадачу, которая в него входит
-#### Подзадача хранит id эпика, которому принадлежит
+### Управление задачами
+- Создание, обновление и удаление задач всех типов
+- Хранение истории просмотров задач
+- Очистка всей истории просмотров
+- Обработка ошибок при создании и обновлении задач
----
+### Получение информации
+- Получение списков всех задач/эпиков/подзадач
+- Поиск задач по ID
+- Получение всех подзадач конкретного эпика
+- Получение задач в порядке приоритета (по времени начала)
-## TaskManager
-Здесь хранятся основным методы работы с задачами и сами задачи
-в таблицах (id=задача)
+## Технологический стек
+- **Java** - основной язык программирования
+- **JUnit 5** - фреймворк для модульного тестирования
+- **Gson** - библиотека для работы с JSON
-- Добавление задачи в таблицу
-- Получение задачи по id
-- Удаление задачи из таблицы
-- Удаление всех задач или всех задач из определенной задачи
-- Печать задач в разных форматах
-- Обновление задачи (задача меняет свое название или описание)
+## API
-Пользователь способен обновить статус задачи, обновив саму задачу.
-При это обновление статуса эпика происходит независимо от пользователя,
-а благодаря вычислениям
+### Поддерживаемые форматы времени
+Система поддерживает следующие форматы для полей `startTime`:
+- `DateTimeFormatter.ISO_LOCAL_DATE_TIME` - `2023-10-15T14:30:00`
+- `DateTimeFormatter.ISO_DATE_TIME` - `2023-10-15T14:30:00.000+03:00`
+- `yyyy-MM-dd HH:mm:ss.SSS` - `2023-10-15 14:30:00.000`
+- `yyyy-MM-dd HH:mm:ss` - `2023-10-15 14:30:00`
+- `dd.MM.yyyy HH:mm:ss` - `15.10.2023 14:30:00`
+
+### Коды состояния HTTP
+
+Система использует следующие HTTP коды состояния для обработки запросов:
+
+| Код | Описание |
+|---------|-----------------------------------------------------------------------------------------------------------|
+| **200** | Успешный запрос (GET, HEAD, DELETE, OPTIONS) |
+| **201** | Успешное создание или обновление ресурса (POST) |
+| **400** | Неверный формат JSON или отсутствуют обязательные поля, неверный формат времени, id не число или меньше 0 |
+| **404** | Задача не найдена или неверный путь |
+| **405** | Метод не разрешен для ресурса |
+| **406** | Пересечение временных интервалов с существующими задачами |
+| **500** | Внутренняя ошибка сервера |
+
+### Задачи (Tasks)
+
+| Метод | Путь | Действие | Коды ответа |
+|-------|------|----------|-------------|
+| HEAD | `/tasks` или `/tasks/{id}` | Получить заголовки | 200, 404 |
+| GET | `/tasks` | Получить все задачи | 200 |
+| GET | `/tasks/{id}` | Получить задачу по ID | 200, 404 |
+| POST | `/tasks` | Создать новую задачу | 201, 400, 406 |
+| DELETE | `/tasks` | Удалить все задачи | 200 |
+| DELETE | `/tasks/{id}` | Удалить задачу по ID | 200, 404 |
+| OPTIONS | `/tasks` или `/tasks/{id}` | Получить разрешенные методы | 200 |
+
+#### Формат JSON для задачи
+```json
+{
+ "taskId": 0, // 0 - для создания, >0 - для обновления (обязательное)
+ "title": "string", // обязательное поле
+ "description": "string", // обязательное поле
+ "status": "NEW|IN_PROGRESS|DONE", // обязательное поле
+ "duration": 10, // продолжительность в минутах (опционально)
+ "startTime": "1970-01-01T00:00:00.000" // дата и время ISO (опционально)
+}
+```
+
+### Эпики (Epics)
+
+| Метод | Путь | Действие | Коды ответа |
+|-------|------|----------|-------------|
+| HEAD | любой путь | Получить заголовки | 200, 404 |
+| GET | `/epics` | Получить все эпики | 200 |
+| GET | `/epics/{id}` | Получить эпик по ID | 200, 404 |
+| GET | `/epics/{id}/subtasks` | Получить все подзадачи эпика | 200, 404 |
+| POST | `/epics` | Создать новый эпик | 201, 400, 406 |
+| DELETE | `/epics` | Удалить все эпики | 200 |
+| DELETE | `/epics/{id}` | Удалить эпик по ID | 200, 404 |
+| DELETE | `/epics/{id}/subtasks` | Удалить все подзадачи эпика | 200, 404 |
+| OPTIONS | любой путь | Получить разрешенные методы | 200 |
+
+#### Формат JSON для эпика
+```json
+{
+ "taskId": 0, //0 - для создания, >0 - для обновления (обязательное)
+ "title": "string", // обязательное поле
+ "description": "string", // обязательное поле
+ "status": "NEW|IN_PROGRESS|DONE", // только для чтения
+ "subtasks": [1, 2, 3], // только для чтения
+ "duration": 10, // только для чтения
+ "startTime": "1970-01-01T00:00:00.000" // только для чтения
+}
+```
+
+### Подзадачи (Subtasks)
+
+| Метод | Путь | Действие | Коды ответа |
+|-------|------|----------|-------------|
+| HEAD | `/subtasks` или `/subtasks/{id}` | Получить заголовки | 200, 404 |
+| GET | `/subtasks` | Получить все подзадачи | 200 |
+| GET | `/subtasks/{id}` | Получить подзадачу по ID | 200, 404 |
+| POST | `/subtasks` | Создать новую подзадачу | 201, 400, 406 |
+| DELETE | `/subtasks` | Удалить все подзадачи | 200 |
+| DELETE | `/subtasks/{id}` | Удалить подзадачу по ID | 200, 404 |
+
+#### Формат JSON для подзадачи
+```json
+{
+ "taskId": 0, // 0 - для создания, >0 - для обновления (обязательное)
+ "title": "string", // обязательное поле
+ "description": "string", // обязательное поле
+ "status": "NEW|IN_PROGRESS|DONE", // обязательное поле
+ "epicId": 123, // обязательное поле - ID родительского эпика
+ "duration": 10, // продолжительность в минутах (опционально)
+ "startTime": "1970-01-01T00:00:00.000" // дата и время ISO (опционально)
+}
+```
+
+## История просмотров (History)
+
+| Метод | Путь | Действие | Коды ответа |
+|-------|------|----------|-------------|
+| HEAD | `/history` | Получить заголовки | 200 |
+| GET | `/history` | Получить историю просмотров | 200 |
+| OPTIONS | `/history` | Получить разрешенные методы | 200 |
+
+## Список приоритетов (Prioritized)
+
+| Метод | Путь | Действие | Коды ответа |
+|-------|------|----------|-------------|
+| HEAD | `/prioritized` | Получить заголовки | 200 |
+| GET | `/prioritized` | Получить отсортированный список задач | 200 |
+| OPTIONS | `/prioritized` | Получить разрешенные методы | 200 |
+
+## Тестирование
+
+Проект включает модульные тесты с использованием JUnit 5 для проверки функциональности менеджера задач и HTTP-сервера.
\ No newline at end of file
diff --git a/java-kanban.iml b/java-kanban.iml
index 66c15d2..48a7097 100644
--- a/java-kanban.iml
+++ b/java-kanban.iml
@@ -24,5 +24,6 @@
+
\ No newline at end of file
diff --git a/src/http/HttpTaskServer.java b/src/http/HttpTaskServer.java
new file mode 100644
index 0000000..0939d82
--- /dev/null
+++ b/src/http/HttpTaskServer.java
@@ -0,0 +1,67 @@
+package http;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.sun.net.httpserver.HttpServer;
+import http.handlers.*;
+import managers.InMemoryTaskManager;
+import managers.TaskManager;
+import util.gsonadapters.DurationAdapter;
+import util.gsonadapters.LocalDateTimeAdapter;
+import util.http.JsonBuilder;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+public class HttpTaskServer {
+ private final int port = 8080;
+ private HttpServer httpServer;
+ private final TaskManager manager;
+
+ private final Gson gson;
+ private final JsonBuilder jsonBuilder;
+
+ public static void main(String[] args) {
+ new HttpTaskServer(new InMemoryTaskManager()).start();
+ }
+
+ public HttpTaskServer(TaskManager manager) {
+ this.manager = manager;
+ this.gson = new GsonBuilder()
+ .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
+ .registerTypeAdapter(Duration.class, new DurationAdapter())
+ .create();
+ this.jsonBuilder = new JsonBuilder(gson);
+ createServer();
+ }
+
+ private void createServer() {
+ try {
+ httpServer = HttpServer.create(new InetSocketAddress(port), 0);
+ httpServer.createContext("/tasks", new TaskHandler(manager, gson, jsonBuilder));
+ httpServer.createContext("/epics", new EpicsHandler(manager, gson, jsonBuilder));
+ httpServer.createContext("/subtasks", new SubTasksHandler(manager, gson, jsonBuilder));
+ httpServer.createContext("/history", new HistoryHandler(manager, gson, jsonBuilder));
+ httpServer.createContext("/prioritized", new PrioritizedHandler(manager, gson, jsonBuilder));
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void start() {
+ try {
+ httpServer.start();
+ System.out.println("Server is running: " + httpServer.getAddress().getPort());
+ } catch (IllegalStateException e) {
+ System.err.println("Server is already running");
+ }
+ }
+
+ public void stop() {
+ httpServer.stop(0);
+ System.out.println("Server has been stopped");
+ }
+}
diff --git a/src/http/handlers/BaseHttpHandler.java b/src/http/handlers/BaseHttpHandler.java
new file mode 100644
index 0000000..37a1f5e
--- /dev/null
+++ b/src/http/handlers/BaseHttpHandler.java
@@ -0,0 +1,236 @@
+package http.handlers;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import managers.TaskManager;
+import util.http.JsonBuilder;
+import util.http.RequestSegments;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+import static util.enums.Endpoint.*;
+import static util.http.RequestSegments.getRequestSegments;
+
+/**
+ * Абстрактный базовый класс для обработки HTTP-запросов.
+ *
+ * Предоставляет общую функциональность для обработки HTTP-запросов, включая:
+ *
+ *
+ * - Парсинг и валидацию сегментов URI
+ * - Маршрутизацию запросов по HTTP-методам
+ * - Стандартизированную отправку ответов
+ * - Обработку ошибок и исключений
+ *
+ *
+ * Жизненный цикл обработки запроса:
+ *
+ * - Парсинг сегментов пути из URI
+ * - Валидация сегментов и HTTP-метода
+ * - Проверка ресурсов (абстрактный метод)
+ * - Маршрутизация на соответствующий обработчик метода
+ * - Обработка исключений и отправка ошибок
+ *
+ *
+ * Поддерживаемые HTTP-методы: GET, POST, DELETE, OPTIONS, HEAD
+ *
+ * Особенности обработки:
+ *
+ * - Метод HEAD обрабатывается как GET, но без отправки тела ответа
+ * - OPTIONS возвращает разрешенные методы для ресурса
+ * - Некорректные запросы возвращают соответствующие HTTP-статусы
+ * - Все ответы отправляются в формате JSON с UTF-8 кодировкой
+ *
+ *
+ * Наследование: Классы-наследники должны реализовать абстрактные методы
+ * для конкретной логики обработки ресурсов.
+ */
+public abstract class BaseHttpHandler implements HttpHandler {
+ private final Charset defaultCharset = StandardCharsets.UTF_8;
+ protected final TaskManager manager;
+
+ protected Gson gson;
+ protected JsonBuilder jsonBuilder;
+
+ public BaseHttpHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) {
+ this.manager = manager;
+ this.gson = gson;
+
+ this.jsonBuilder = jsonBuilder;
+ }
+
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ try {
+ RequestSegments segments = getPreparedSegments(exchange);
+
+ if (segments == null || validateResources(exchange, segments)) {
+ return;
+ }
+
+ mapEndpoints(exchange, segments);
+
+ } catch (JsonSyntaxException e) {
+ sendText(exchange, jsonBuilder.badRequest("Invalid Json"), 400);
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (Exception e) {
+ try {
+ sendServerError(exchange);
+ e.printStackTrace();
+ } catch (IOException ex) {
+ System.err.println("Failed to send response");
+ ex.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Подготавливает и валидирует сегменты запроса.
+ *
+ * @param exchange HTTP-обмен для анализа
+ * @return подготовленные сегменты запроса или {@code null} если валидация не пройдена
+ * @throws IOException если возникает ошибка при обработке запроса
+ */
+ private RequestSegments getPreparedSegments(HttpExchange exchange) throws IOException {
+ RequestSegments segments = getRequestSegments(exchange);
+
+ jsonBuilder.setSegments(segments);
+
+ if (!isSegmentsValid(exchange, segments)) {
+ return null;
+ }
+
+ return segments;
+ }
+
+ /**
+ * Маршрутизирует запрос на соответствующий обработчик метода.
+ *
+ * @param exchange HTTP-обмен для обработки
+ * @param segments разобранные сегменты запроса
+ * @throws IOException если возникает ошибка при обработке запроса
+ */
+ private void mapEndpoints(HttpExchange exchange, RequestSegments segments) throws IOException {
+ switch (segments.endpoint()) {
+ case OPTIONS -> handleOptions(exchange, segments);
+ case GET -> handleGet(exchange, segments);
+ case POST -> handlePost(exchange, segments);
+ case DELETE -> handleDelete(exchange, segments);
+ default -> sendText(exchange, jsonBuilder.badRequest("No such endpoint"), 400);
+ }
+ }
+
+ /**
+ * Проверяет валидность сегментов запроса.
+ * Отправляет ответ, если проверка не пройдена.
+ *
+ * Выполняет следующие проверки:
+ *
+ *
+ * - Поддержка HTTP-метода
+ * - Корректность endpoint
+ * - Валидность структуры подресурсов
+ * - Корректность числового идентификатора
+ * - Разрешение подресурсов только для эпиков
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты для валидации
+ * @return {@code true} если сегменты валидны, иначе {@code false}
+ * @throws IOException если возникает ошибка при отправке ответа об ошибке
+ */
+ private boolean isSegmentsValid(HttpExchange exchange, RequestSegments segments) throws IOException {
+
+ if (segments.endpoint() == INVALID_METHOD) {
+ exchange.getResponseHeaders().add("Allow", "GET, POST, DELETE, HEAD, OPTIONS");
+ sendMethodNotAllowed(exchange);
+ return false;
+ } else if (segments.endpoint() == INVALID) {
+ sendText(exchange, jsonBuilder.badRequest("No such endpoint"), 400);
+ return false;
+ } else if (segments.endpoint() == INVALID_SUBRESOURCE) {
+ sendNotFound(exchange, jsonBuilder.tooMuchSubResources());
+ return false;
+ } else if (segments.id() == -1) {
+ sendText(exchange, jsonBuilder.invalidId(), 400);
+ return false;
+ } else if (segments.subResource().isPresent() && !segments.resource().equals("epics")) {
+ sendNotFound(exchange, jsonBuilder.subresourceNotFound());
+ return false;
+ }
+
+ return true;
+ }
+
+
+ protected void sendText(HttpExchange exchange, String responseString, int responseCode) throws IOException {
+ byte[] resp = responseString.getBytes(defaultCharset);
+ exchange.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8");
+ exchange.getResponseHeaders().add("Content-Length", String.valueOf(resp.length));
+
+ if (!"HEAD".equals(exchange.getRequestMethod())) {
+ exchange.sendResponseHeaders(responseCode, resp.length);
+ exchange.getResponseBody().write(resp);
+ } else {
+ exchange.sendResponseHeaders(responseCode, -1);
+ }
+
+ exchange.close();
+ }
+
+ protected void sendNotFound(HttpExchange exchange, String responseString) throws IOException {
+ sendText(exchange, responseString, 404);
+ }
+
+ protected void sendHasOverlaps(HttpExchange exchange, String responseString) throws IOException {
+ sendText(exchange, responseString, 406);
+ }
+
+ protected void sendMethodNotAllowed(HttpExchange exchange) throws IOException {
+ exchange.sendResponseHeaders(405, -1);
+ exchange.close();
+ }
+
+ protected void sendServerError(HttpExchange exchange) throws IOException {
+ exchange.sendResponseHeaders(500, -1);
+ exchange.close();
+ }
+
+ /**
+ * Проверяет запрет метода POST для ресурсов с идентификатором.
+ * Отправляет ответ, если метод запрещен
+ *
+ * POST запросы разрешены только для формата пути /resource.
+ * Для форматов /resource/id и /resource/id/subresource
+ * ресурсов разрешены только GET, DELETE, OPTIONS, HEAD.
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @return true если операция POST запрещена, false если разрешена
+ * @throws IOException если возникает ошибка при отправке ответа об ошибке
+ */
+ protected boolean isPostForbidden(HttpExchange exchange, RequestSegments segments) throws IOException {
+ if (segments.id() != 0) {
+ exchange.getResponseHeaders().add("Allow", "GET, DELETE, OPTIONS, HEAD");
+ sendMethodNotAllowed(exchange);
+ return true;
+ }
+ return false;
+ }
+
+ protected abstract void handleOptions(HttpExchange exchange, RequestSegments segments) throws IOException;
+
+ protected abstract void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException;
+
+ protected abstract void handlePost(HttpExchange exchange, RequestSegments segments) throws IOException;
+
+ protected abstract void handleDelete(HttpExchange exchange, RequestSegments segments) throws IOException;
+
+ protected abstract boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException;
+}
diff --git a/src/http/handlers/EpicsHandler.java b/src/http/handlers/EpicsHandler.java
new file mode 100644
index 0000000..0eead44
--- /dev/null
+++ b/src/http/handlers/EpicsHandler.java
@@ -0,0 +1,269 @@
+package http.handlers;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.sun.net.httpserver.HttpExchange;
+import managers.TaskManager;
+import model.Epic;
+import model.Task;
+import util.enums.Endpoint;
+import util.exceptions.TaskNotFound;
+import util.http.JsonBuilder;
+import util.http.RequestSegments;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.format.DateTimeParseException;
+
+/**
+ * Обработчик HTTP-запросов для управления эпиками (Epic) и их подзадачами.
+ *
+ * Расширяет функциональность {@link TaskHandler} для работы с эпиками.
+ * Обеспечивает REST API для операций CRUD над эпиками и их подзадачами.
+ *
+ *
+ * Поддерживаемые эндпоинты для эпиков:
+ *
+ * | Метод | Путь | Действие |
+ * | HEAD | любой путь | Получить заголовки |
+ * | GET | /epics | Получить все эпики |
+ * | GET | /epics/{id} | Получить эпик по Id |
+ * | GET | /epics/{id}/subtasks | Получить все подзадачи эпика |
+ * | POST | /epics | Создать новый эпик |
+ * | POST | /epics/{id} | Запрещено - метод не разрешен |
+ * | POST | /epics/{id}/subtasks | Запрещено - метод не разрешен |
+ * | DELETE | /epics | Удалить все эпики |
+ * | DELETE | /epics/{id} | Удалить эпик по Id |
+ * | DELETE | /epics/{id}/subtasks | Удалить все подзадачи эпика |
+ * | OPTIONS | любой путь | Получить разрешенные методы |
+ *
+ *
+ * Формат JSON для эпика:
+ *
+ * {
+ * "taskId": 0, // 0 для создания, >0 для обновления
+ * "title": "string", // обязательное поле
+ * "description": "string", // обязательное поле
+ * "status": "NEW|IN_PROGRESS|DONE", // (только для чтения)
+ * "subtasks": [1, 2, 3], // (только для чтения)
+ * "duration": 10, // (только для чтения)
+ * "startTime": "1970-01-01T00:00:00.000" //(только для чтения)
+ * }
+ *
+ *
+ * Обработка ошибок:
+ *
+ * - 400 - Неверный формат JSON или отсутствуют обязательные поля
+ * - 404 - Эпик не найден или неверный путь
+ * - 405 - Метод не разрешен для ресурса
+ * - 500 - Внутренняя ошибка сервера
+ *
+ */
+public class EpicsHandler extends TaskHandler {
+
+ public EpicsHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) {
+ super(manager, gson, jsonBuilder);
+ }
+
+ /**
+ * Обрабатывает HTTP-метод OPTIONS для эпиков.
+ *
+ * Возвращает разрешенные методы в зависимости от контекста:
+ *
+ *
+ * - Для /epics: GET, POST, DELETE, OPTIONS, HEAD
+ * - Для /epics/{id} и /epics/{id}/subtasks: GET, DELETE, OPTIONS, HEAD
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ */
+ @Override
+ protected void handleOptions(HttpExchange exchange, RequestSegments segments) throws IOException {
+ if (segments.id() == 0 && segments.subResource().isEmpty()) {
+ exchange.getResponseHeaders().add("Allow", "GET, POST, DELETE, OPTIONS, HEAD");
+ } else {
+ exchange.getResponseHeaders().add("Allow", "GET, DELETE, OPTIONS, HEAD");
+ }
+ exchange.sendResponseHeaders(204, -1);
+ }
+
+ /**
+ * Обрабатывает HTTP-метод GET для эпиков.
+ *
+ * В зависимости от пути запроса:
+ *
+ *
+ * - /epics - возвращает список всех эпиков
+ * - /epics/{id} - возвращает конкретный эпик
+ * - /epics/{id}/subtasks - возвращает все подзадачи указанного эпика
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ */
+ @Override
+ protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException {
+ int id = segments.id();
+
+ if (segments.subResource().isPresent()) {
+ if (manager.getEpicWithoutHistory(id) != null) {
+ sendText(exchange, gson.toJson(manager.getSubTasksFromEpic(id)), 200);
+ } else {
+ sendNotFound(exchange, jsonBuilder.notFound("Epic with id '" + id + "'not found"));
+ }
+ } else if (id == 0) {
+ sendText(exchange, gson.toJson(manager.getEpics()), 200);
+ } else {
+ Epic epic = manager.getEpic(id);
+ if (epic != null) {
+ sendText(exchange, gson.toJson(epic), 200);
+ } else {
+ sendNotFound(exchange, jsonBuilder.notFound("Epic with id '" + id + "'not found"));
+ }
+ }
+ }
+
+ /**
+ * Обрабатывает HTTP-метод POST для эпиков.
+ *
+ * Создает новый эпик или обновляет существующий:
+ *
+ *
+ * - taskId = 0: создает новый эпик через конструктор
+ * - taskId > 0: обновляет существующий эпик
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ */
+ @Override
+ protected void handlePost(HttpExchange exchange, RequestSegments segments) throws IOException {
+ if (isPostForbidden(exchange, segments)) {
+ return;
+ }
+ String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
+
+ if (body.isBlank()) {
+ sendText(exchange, jsonBuilder.badRequest("Body is required"), 400);
+ }
+ try {
+ Epic epic = gson.fromJson(body, Epic.class);
+ isTaskValid(epic);
+
+ if (epic.getTaskId() == 0) {
+ //новый эпик для безопасного создания
+ epic = new Epic(epic.getTitle(), epic.getDescription(), epic.getStatus());
+ manager.addEpic(epic);
+ } else {
+ manager.updateEpic(epic);
+ }
+ sendText(exchange, gson.toJson(manager.getEpicWithoutHistory(epic.getTaskId())), 201);
+ } catch (DateTimeParseException e) {
+ sendText(exchange, jsonBuilder.badRequest("Invalid date format"), 400);
+ } catch (TaskNotFound e) {
+ sendNotFound(exchange, jsonBuilder.notFound(e.getMessage()));
+ }
+ }
+
+ /**
+ * Обрабатывает HTTP-метод DELETE для эпиков.
+ *
+ * В зависимости от пути запроса:
+ *
+ *
+ * - /epics - удаляет все эпики
+ * - /epics/{id} - удаляет конкретный эпик
+ * - /epics/{id}/subtasks - удаляет все подзадачи указанного эпика
+ *
+ *
+ * Удаление эпика автоматически удаляет все его подзадачи.
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ */
+ @Override
+ protected void handleDelete(HttpExchange exchange, RequestSegments segments) throws IOException {
+
+ if (segments.subResource().isPresent()) {
+ try {
+ manager.clearSubTasksFromEpic(segments.id());
+ sendText(exchange,
+ jsonBuilder.message("Subtasks from epic: '" + segments.id() + "' were deleted"),
+ 200);
+ } catch (TaskNotFound e) {
+ sendNotFound(exchange, jsonBuilder.notFound(e.getMessage()));
+ }
+ } else if (segments.id() == 0) {
+ manager.clearEpics();
+ sendText(exchange, jsonBuilder.message("Epics were deleted"), 200);
+ } else {
+ manager.deleteEpic(segments.id());
+ sendText(exchange, jsonBuilder.message("Epic: '" + segments.id() + "' was deleted"), 200);
+ }
+ }
+
+ /**
+ * Проверяет корректность ресурса и подресурсов в запросе.
+ *
+ * Выполняет следующие проверки:
+ *
+ *
+ * - Убеждается, что запрос адресован эпикам ("epics")
+ * - Проверяет валидность подресурса (только "subtasks")
+ * - Запрещает POST запросы для подресурсов
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @return true если ресурс неверен или запрос запрещен, иначе false
+ * @throws IOException если возникает ошибка при отправке ответа об ошибке
+ */
+ @Override
+ protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException {
+ if (!"epics".equals(segments.resource())) {
+ sendNotFound(exchange, jsonBuilder.resourceNotFound());
+ return true;
+ }
+
+ if (segments.subResource().isPresent()) {
+ if (!segments.subResource().get().equals("subtasks")) {
+ sendNotFound(exchange, jsonBuilder.subresourceNotFound());
+ return true;
+ }
+ if (segments.endpoint() == Endpoint.POST) {
+ exchange.getResponseHeaders().add("Allow", "GET, DELETE, HEAD, OPTIONS");
+ sendMethodNotAllowed(exchange);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Проверяет валидность объекта эпика.
+ *
+ * Проверяет наличие обязательных полей:
+ *
+ *
+ * - title - заголовок эпика
+ * - description - описание эпика
+ *
+ *
+ * Отличие от TaskHandler: для эпиков статус не является обязательным полем,
+ * так как он рассчитывается автоматически на основе подзадач.
+ *
+ * @param task эпик для валидации
+ * @throws JsonSyntaxException если отсутствуют обязательные поля
+ */
+ @Override
+ protected void isTaskValid(Task task) {
+ if (task.getTitle() == null || task.getDescription() == null) {
+ throw new JsonSyntaxException("Epic has invalid fields");
+ }
+ }
+}
diff --git a/src/http/handlers/HistoryHandler.java b/src/http/handlers/HistoryHandler.java
new file mode 100644
index 0000000..876980b
--- /dev/null
+++ b/src/http/handlers/HistoryHandler.java
@@ -0,0 +1,152 @@
+package http.handlers;
+
+import com.google.gson.Gson;
+import com.sun.net.httpserver.HttpExchange;
+import managers.TaskManager;
+import util.http.JsonBuilder;
+import util.http.RequestSegments;
+
+import java.io.IOException;
+
+/**
+ * Обработчик HTTP-запросов для получения истории просмотров задач.
+ *
+ * Предоставляет доступ к истории последних просмотренных задач.
+ *
+ *
+ * Поддерживаемые эндпоинты:
+ *
+ * | Метод | Путь | Действие |
+ * | HEAD | /history | Получить заголовки |
+ * | GET | /history | Получить историю просмотров |
+ * | OPTIONS | /history | Получить разрешенные методы |
+ * | POST | /history | Запрещено - метод не разрешен |
+ * | DELETE | /history | Запрещено - метод не разрешен |
+ * | GET | /history/{id} | Запрещено - метод не разрешен |
+ *
+ *
+ * Формат ответа:
+ *
+ * [
+ * {
+ * "taskId": 1,
+ * "title": "Название задачи",
+ * "description": "Описание задачи",
+ * "status": "NEW",
+ * "type": "TASK|EPIC|SUBTASK"
+ * // ... другие поля задачи в зависимости от типа
+ * },
+ * {
+ * "taskId": 2,
+ * "title": "Другая задача",
+ * // ...
+ * }
+ * ]
+ *
+ *
+ * Обработка ошибок:
+ *
+ * - 404 - Неверный путь
+ * - 405 - Метод не разрешен для ресурса
+ * - 500 - Внутренняя ошибка сервера
+ *
+ */
+public class HistoryHandler extends BaseHttpHandler {
+
+ public HistoryHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) {
+ super(manager, gson, jsonBuilder);
+ }
+
+ /**
+ * Обрабатывает HTTP-метод OPTIONS для истории.
+ *
+ * Возвращает разрешенные методы для ресурса истории:
+ * GET, OPTIONS, HEAD
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ */
+ @Override
+ protected void handleOptions(HttpExchange exchange, RequestSegments segments) throws IOException {
+ exchange.getResponseHeaders().add("Allow", "GET, OPTIONS, HEAD");
+ exchange.sendResponseHeaders(204, -1);
+ }
+
+ /**
+ * Обрабатывает HTTP-метод GET для истории.
+ *
+ * Возвращает полную историю просмотров задач в формате JSON.
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ */
+ @Override
+ protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException {
+ sendText(exchange, gson.toJson(manager.getHistory()), 200);
+ }
+
+ /**
+ * Обрабатывает HTTP-метод POST для истории или списка приоритетов.
+ *
+ * Ресурс только для чтения, поэтому POST запросы запрещены.
+ * Возвращает статус 405 Method Not Allowed с заголовком Allow.
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ */
+ @Override
+ protected void handlePost(HttpExchange exchange, RequestSegments segments) throws IOException {
+ exchange.getResponseHeaders().add("Allow", "GET, HEAD, OPTIONS");
+ sendMethodNotAllowed(exchange);
+ }
+
+ /**
+ * Обрабатывает HTTP-метод DELETE для истории или списка приоритетов.
+ *
+ * Ресурс только для чтения, поэтому DELETE запросы запрещены.
+ * Возвращает статус 405 Method Not Allowed с заголовком Allow.
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ */
+ @Override
+ protected void handleDelete(HttpExchange exchange, RequestSegments segments) throws IOException {
+ exchange.getResponseHeaders().add("Allow", "GET, HEAD, OPTIONS");
+ sendMethodNotAllowed(exchange);
+ }
+
+ /**
+ * Проверяет корректность ресурса и пути запроса для истории.
+ *
+ * Выполняет следующие проверки:
+ *
+ *
+ * - Убеждается, что запрос адресован истории ("history")
+ * - Проверяет, что в пути нет идентификатора и подресурсов
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @return true если путь неверен, иначе false
+ * @throws IOException если возникает ошибка при отправке ответа 404
+ */
+ @Override
+ protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException {
+ if (!"history".equals(segments.resource())) {
+ sendNotFound(exchange, jsonBuilder.resourceNotFound());
+ return true;
+ } else if (segments.id() != 0) {
+ sendNotFound(exchange, jsonBuilder.subresourceNotFound());
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/http/handlers/PrioritizedHandler.java b/src/http/handlers/PrioritizedHandler.java
new file mode 100644
index 0000000..4b7e85d
--- /dev/null
+++ b/src/http/handlers/PrioritizedHandler.java
@@ -0,0 +1,103 @@
+package http.handlers;
+
+import com.google.gson.Gson;
+import com.sun.net.httpserver.HttpExchange;
+import managers.TaskManager;
+import util.http.JsonBuilder;
+import util.http.RequestSegments;
+
+import java.io.IOException;
+
+/**
+ * Обработчик HTTP-запросов для получения отсортированного списка задач по приоритету.
+ *
+ * Расширяет функциональность {@link HistoryHandler} для обработки запрещенных методов (POST, DELETE).
+ *
+ *
+ * Поддерживаемые эндпоинты:
+ *
+ * | Метод | Путь | Действие |
+ * | HEAD | /prioritized | Получить заголовки |
+ * | GET | /prioritized | Получить отсортированный список задач |
+ * | OPTIONS | /prioritized | Получить разрешенные методы |
+ * | POST | /prioritized | Запрещено - метод не разрешен |
+ * | DELETE | /prioritized | Запрещено - метод не разрешен |
+ * | GET | /prioritized/{id} | Запрещено - метод не разрешен |
+ *
+ *
+ * Формат ответа:
+ *
+ * [
+ * {
+ * "taskId": 3,
+ * "title": "Ранняя задача",
+ * "startTime": "1970-01-01T00:00:00.000",
+ * "duration": 10,
+ * // ... другие поля задачи
+ * },
+ * {
+ * "taskId": 1,
+ * "title": "Задача с временем",
+ * "startTime": "2000-01-01T00:00:00.000",
+ * "duration": 10,
+ * // ...
+ * }
+ * ]
+ *
+ *
+ * Обработка ошибок:
+ *
+ * - 404 - Неверный путь
+ * - 405 - Метод не разрешен для ресурса
+ * - 500 - Внутренняя ошибка сервера
+ *
+ */
+public class PrioritizedHandler extends HistoryHandler {
+
+ public PrioritizedHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) {
+ super(manager, gson, jsonBuilder);
+ }
+
+ /**
+ * Обрабатывает HTTP-метод GET для приоритизированного списка.
+ *
+ * Возвращает задачи, отсортированные по времени начала (startTime).
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ * @see util.TaskTimeController#getPrioritizedTasks()
+ */
+ @Override
+ protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException {
+ sendText(exchange, gson.toJson(manager.getPrioritizedTasks()), 200);
+ }
+
+ /**
+ * Проверяет корректность ресурса и пути запроса для приоритизированного списка.
+ *
+ * Выполняет следующие проверки:
+ *
+ *
+ * - Убеждается, что запрос адресован приоритизированному списку ("prioritized")
+ * - Проверяет, что в пути нет идентификатора и подресурсов
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @return true если путь неверен, иначе false
+ * @throws IOException если возникает ошибка при отправке ответа 404
+ */
+ @Override
+ protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException {
+ if (!"prioritized".equals(segments.resource())) {
+ sendNotFound(exchange, jsonBuilder.resourceNotFound());
+ return true;
+ } else if (segments.id() != 0) {
+ sendNotFound(exchange, jsonBuilder.subresourceNotFound());
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/http/handlers/SubTasksHandler.java b/src/http/handlers/SubTasksHandler.java
new file mode 100644
index 0000000..4b43e5d
--- /dev/null
+++ b/src/http/handlers/SubTasksHandler.java
@@ -0,0 +1,215 @@
+package http.handlers;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.sun.net.httpserver.HttpExchange;
+import managers.TaskManager;
+import model.SubTask;
+import model.Task;
+import util.exceptions.TaskNotFound;
+import util.exceptions.TaskTimeOverlapException;
+import util.http.JsonBuilder;
+import util.http.RequestSegments;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.format.DateTimeParseException;
+
+/**
+ * Обработчик HTTP-запросов для управления подзадачами (SubTask).
+ *
+ * Расширяет функциональность {@link TaskHandler} для работы с подзадачами.
+ * Обеспечивает REST API для операций CRUD над подзадачами.
+ *
+ *
+ * Поддерживаемые эндпоинты для подзадач:
+ *
+ * | Метод | Путь | Действие |
+ * | HEAD | /subtasks или /subtasks/{id} | Получить заголовки |
+ * | GET | /subtasks | Получить все подзадачи |
+ * | GET | /subtasks/{id} | Получить подзадачу по Id |
+ * | POST | /subtasks | Создать новую подзадачу |
+ * | POST | /subtasks/{id} | Запрещено - метод не разрешен |
+ * | DELETE | /subtasks | Удалить все подзадачи |
+ * | DELETE | /subtasks/{id} | Удалить подзадачу по Id |
+ *
+ *
+ * Формат JSON для подзадачи:
+ *
+ * {
+ * "taskId": 0, // 0 для создания, >0 для обновления
+ * "title": "string", // обязательное поле
+ * "description": "string", // обязательное поле
+ * "status": "NEW|IN_PROGRESS|DONE", // обязательное поле
+ * "epicId": 123, // обязательное поле - ID родительского эпика
+ * "duration": 10, // продолжительность в минутах (опционально)
+ * "startTime": "1970-01-01T00:00:00.000" // дата и время ISO (опционально)
+ * }
+ *
+ *
+ * Обработка ошибок:
+ *
+ * - 400 - Неверный формат JSON или отсутствуют обязательные поля
+ * - 404 - Подзадача или родительский эпик не найдены или неверный путь
+ * - 405 - Метод не разрешен для ресурса
+ * - 406 - Пересечение временных интервалов с существующими задачами
+ * - 500 - Внутренняя ошибка сервера
+ *
+ */
+public class SubTasksHandler extends TaskHandler {
+
+ public SubTasksHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) {
+ super(manager, gson, jsonBuilder);
+ }
+
+ /**
+ * Обрабатывает HTTP-метод GET для подзадач.
+ *
+ * В зависимости от наличия идентификатора:
+ *
+ *
+ * - Без ID: возвращает список всех подзадач
+ * - С ID: возвращает конкретную подзадачу
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ */
+ @Override
+ protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException {
+ if (segments.id() == 0) {
+ sendText(exchange, gson.toJson(manager.getSubTasks()), 200);
+ } else {
+ SubTask subtask = manager.getSubTask(segments.id());
+ if (subtask != null) {
+ sendText(exchange, gson.toJson(subtask), 200);
+ } else {
+ sendNotFound(exchange, jsonBuilder.notFound("SubTask with id '" + segments.id() + "'not found"));
+ }
+ }
+ }
+
+ /**
+ * Обрабатывает HTTP-метод POST для подзадач.
+ *
+ * Создает новую подзадачу или обновляет существующую в зависимости от taskId:
+ *
+ *
+ * - taskId = 0: создание новой подзадачи
+ * - taskId > 0: обновление существующей подзадачи
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ * @throws TaskNotFound если родительский эпик или подзадача для обновления не существует
+ * @throws TaskTimeOverlapException если обнаружено пересечение временных интервалов
+ */
+ @Override
+ protected void handlePost(HttpExchange exchange, RequestSegments segments) throws IOException {
+ if (isPostForbidden(exchange, segments)) {
+ return;
+ }
+ String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
+
+ if (body.isBlank()) {
+ sendText(exchange, jsonBuilder.badRequest("Body is required"), 400);
+ }
+
+ try {
+ SubTask subTask = gson.fromJson(body, SubTask.class);
+ isTaskValid(subTask);
+
+ if (subTask.getTaskId() == 0) {
+ manager.addSubTask(subTask);
+ } else {
+ manager.updateSubTask(subTask);
+ }
+ sendText(exchange, gson.toJson(manager.getSubTaskWithoutHistory(subTask.getTaskId())), 201);
+ } catch (TaskNotFound e) {
+ sendNotFound(exchange, jsonBuilder.notFound(e.getMessage()));
+ } catch (DateTimeParseException e) {
+ sendText(exchange, jsonBuilder.badRequest("Invalid date format"), 400);
+ } catch (TaskTimeOverlapException e) {
+ sendHasOverlaps(exchange, jsonBuilder.hasOverlaps());
+ }
+
+ }
+
+ /**
+ * Обрабатывает HTTP-метод DELETE для подзадач.
+ *
+ * В зависимости от наличия идентификатора:
+ *
+ *
+ * - Без ID: удаляет все подзадачи
+ * - С ID: удаляет конкретную подзадачу
+ *
+ *
+ * Удаление подзадачи автоматически обновляет статус и поля времени родительского эпика.
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ */
+ @Override
+ protected void handleDelete(HttpExchange exchange, RequestSegments segments) throws IOException {
+ if (segments.id() == 0) {
+ manager.clearSubTasks();
+ sendText(exchange, jsonBuilder.message("Subtasks were deleted"), 200);
+ } else {
+ manager.deleteSubTask(segments.id());
+ sendText(exchange, jsonBuilder.message("Subtask: '" + segments.id() + "' was deleted"), 200);
+ }
+ }
+
+ /**
+ * Проверяет корректность ресурса в запросе.
+ *
+ * Убеждается, что запрос адресован именно подзадачам ("subtasks").
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @return true если ресурс неверен (не "subtasks"), иначе false
+ * @throws IOException если возникает ошибка при отправке ответа 404
+ */
+ @Override
+ protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException {
+ if (!"subtasks".equals(segments.resource())) {
+ sendNotFound(exchange, jsonBuilder.resourceNotFound());
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Проверяет валидность объекта подзадачи.
+ *
+ * Проверяет наличие обязательных полей для подзадачи:
+ *
+ *
+ * - title - заголовок подзадачи
+ * - description - описание подзадачи
+ * - status - статус подзадачи
+ * - epicId - идентификатор родительского эпика (должен быть > 0)
+ *
+ *
+ * Отличие от TaskHandler: для подзадач дополнительно проверяется наличие epicId.
+ *
+ * @param task подзадача для валидации (приводится к SubTask)
+ * @throws JsonSyntaxException если отсутствуют обязательные поля или epicId = 0
+ */
+ @Override
+ protected void isTaskValid(Task task) {
+ SubTask subTask = (SubTask) task;
+ if (subTask.getTitle() == null ||
+ subTask.getDescription() == null ||
+ subTask.getStatus() == null ||
+ subTask.getEpicId() == 0) {
+ throw new JsonSyntaxException("Subtask has invalid fields");
+ }
+ }
+
+}
diff --git a/src/http/handlers/TaskHandler.java b/src/http/handlers/TaskHandler.java
new file mode 100644
index 0000000..84d011c
--- /dev/null
+++ b/src/http/handlers/TaskHandler.java
@@ -0,0 +1,241 @@
+package http.handlers;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.sun.net.httpserver.HttpExchange;
+import managers.TaskManager;
+import model.Task;
+import util.exceptions.TaskNotFound;
+import util.exceptions.TaskTimeOverlapException;
+import util.http.JsonBuilder;
+import util.http.RequestSegments;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.format.DateTimeParseException;
+
+/**
+ * Обработчик HTTP-запросов для управления задачами (Task).
+ *
+ * Обеспечивает REST API для операций CRUD над задачами:
+ *
+ *
+ * - Создание новых задач
+ * - Получение задач (всех или по идентификатору)
+ * - Обновление существующих задач
+ * - Удаление задач (всех или по идентификатору)
+ *
+ *
+ * Поддерживаемые эндпоинты:
+ *
+ * | Метод | Путь | Действие |
+ * | HEAD | /tasks или /tasks/{id} | Получить заголовки |
+ * | GET | /tasks | Получить все задачи |
+ * | GET | /tasks/{id} | Получить задачу по Id |
+ * | POST | /tasks | Создать новую задачу |
+ * | POST | /tasks/{id} | Запрещено - метод не разрешен |
+ * | DELETE | /tasks | Удалить все задачи |
+ * | DELETE | /tasks/{id} | Удалить задачу по Id |
+ * | OPTIONS | /tasks или /tasks/{id} | Получить разрешенные методы |
+ *
+ *
+ * Формат JSON для задачи:
+ *
+ * {
+ * "taskId": 0, // 0 для создания, >0 для обновления
+ * "title": "string", // обязательное поле
+ * "description": "string", // обязательное поле
+ * "status": "NEW|IN_PROGRESS|DONE", // обязательное поле
+ * "duration": 10, // продолжительность в минутах (опционально)
+ * "startTime": "1970-01-01T00:00:00.000" // дата и время ISO (опционально)
+ * }
+ *
+ *
+ * Обработка ошибок:
+ *
+ * - 400 - Неверный формат JSON или отсутствуют обязательные поля
+ * - 404 - Задача не найдена или неверный путь
+ * - 405 - Метод не разрешен для ресурса
+ * - 406 - Пересечение временных интервалов с существующими задачами
+ * - 500 - Внутренняя ошибка сервера
+ *
+ *
+ * @see java.time.format.DateTimeFormatter#ISO_LOCAL_DATE_TIME
+ */
+public class TaskHandler extends BaseHttpHandler {
+
+ public TaskHandler(TaskManager manager, Gson gson, JsonBuilder jsonBuilder) {
+ super(manager, gson, jsonBuilder);
+ }
+
+ /**
+ * Обрабатывает HTTP-метод OPTIONS для задач.
+ *
+ * Возвращает разрешенные методы в зависимости от контекста:
+ *
+ *
+ * - Для /tasks или /subtasks: GET, POST, DELETE, OPTIONS, HEAD
+ * - Для /tasks/{id} или /subtasks/{id}: GET, DELETE, OPTIONS, HEAD
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ */
+ @Override
+ protected void handleOptions(HttpExchange exchange, RequestSegments segments) throws IOException {
+ if (segments.id() == 0) {
+ exchange.getResponseHeaders().add("Allow", "GET, POST, DELETE, OPTIONS, HEAD");
+ } else {
+ exchange.getResponseHeaders().add("Allow", "GET, DELETE, OPTIONS, HEAD");
+ }
+ exchange.sendResponseHeaders(204, -1);
+ }
+
+ /**
+ * Обрабатывает HTTP-метод GET для задач.
+ *
+ * В зависимости от наличия идентификатора:
+ *
+ *
+ * - Без Id: возвращает список всех задач
+ * - С Id: возвращает конкретную задачу
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ */
+ @Override
+ protected void handleGet(HttpExchange exchange, RequestSegments segments) throws IOException {
+
+ if (segments.id() == 0) {
+ sendText(exchange, gson.toJson(manager.getTasks()), 200);
+ } else {
+ Task task = manager.getTask(segments.id());
+ if (task != null) {
+ sendText(exchange, gson.toJson(task), 200);
+ } else {
+ sendNotFound(exchange, jsonBuilder.notFound("Task with id '" + segments.id() + "'not found"));
+ }
+ }
+ }
+
+ /**
+ * Обрабатывает HTTP-метод POST для задач.
+ *
+ * Создает новую задачу или обновляет существующую в зависимости от taskId:
+ *
+ *
+ * - taskId = 0: создание новой задачи
+ * - taskId > 0: обновление существующей задачи
+ *
+ * Возвращает задачу в теле ответа
+ *
+ * Валидация:
+ *
+ * - Проверка
+ * - Проверка наличия обязательных полей
+ * - Проверка формата даты и времени
+ * - Проверка на пересечение временных интервалов
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ */
+ @Override
+ protected void handlePost(HttpExchange exchange, RequestSegments segments) throws IOException {
+ if (isPostForbidden(exchange, segments)) {
+ return;
+ }
+ String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
+
+ if (body.isBlank()) {
+ sendText(exchange, jsonBuilder.badRequest("Body is required"), 400);
+ }
+
+ try {
+ Task task = gson.fromJson(body, Task.class);
+ isTaskValid(task);
+
+ if (task.getTaskId() == 0) {
+ manager.addTask(task);
+ } else {
+ manager.updateTask(task);
+ }
+ sendText(exchange, gson.toJson(manager.getTaskWithoutHistory(task.getTaskId())), 201);
+ } catch (TaskNotFound e) {
+ sendNotFound(exchange, jsonBuilder.notFound(e.getMessage()));
+ } catch (DateTimeParseException e) {
+ sendText(exchange, jsonBuilder.badRequest("Invalid date format"), 400);
+ } catch (TaskTimeOverlapException e) {
+ sendHasOverlaps(exchange, jsonBuilder.hasOverlaps());
+ }
+ }
+
+ /**
+ * Обрабатывает HTTP-метод DELETE для задач.
+ *
+ * В зависимости от наличия идентификатора:
+ *
+ *
+ * - Без ID: удаляет все задачи
+ * - С ID: удаляет конкретную задачу
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @throws IOException если возникает ошибка при отправке ответа
+ */
+ @Override
+ protected void handleDelete(HttpExchange exchange, RequestSegments segments) throws IOException {
+ if (segments.id() == 0) {
+ manager.clearTasks();
+ sendText(exchange, jsonBuilder.message("Tasks were deleted"), 200);
+ } else {
+ manager.deleteTask(segments.id());
+ sendText(exchange, jsonBuilder.message("Task: '" + segments.id() + "' was deleted"), 200);
+ }
+ }
+
+ /**
+ * Проверяет корректность ресурса в запросе.
+ *
+ * Убеждается, что запрос адресован именно задачам ("tasks").
+ *
+ *
+ * @param exchange HTTP-обмен
+ * @param segments сегменты запроса
+ * @return true если ресурс неверен (не "tasks"), false если корректно
+ * @throws IOException если возникает ошибка при отправке ответа 404
+ */
+ @Override
+ protected boolean validateResources(HttpExchange exchange, RequestSegments segments) throws IOException {
+ if (!"tasks".equals(segments.resource())) {
+ sendNotFound(exchange, jsonBuilder.resourceNotFound());
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Проверяет валидность объекта задачи.
+ *
+ * Проверяет наличие обязательных полей:
+ *
+ *
+ * - title - заголовок задачи
+ * - description - описание задачи
+ * - status - статус задачи
+ *
+ *
+ * @param task задача для валидации
+ * @throws JsonSyntaxException если отсутствуют обязательные поля
+ */
+ protected void isTaskValid(Task task) {
+ if (task.getTitle() == null || task.getDescription() == null || task.getStatus() == null) {
+ throw new JsonSyntaxException("Task has invalid fields");
+ }
+ }
+}
diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java
index 56537ee..26984a8 100644
--- a/src/managers/InMemoryTaskManager.java
+++ b/src/managers/InMemoryTaskManager.java
@@ -4,8 +4,9 @@
import model.Epic;
import model.SubTask;
import model.Task;
-import util.Status;
import util.TaskTimeController;
+import util.enums.Status;
+import util.exceptions.TaskNotFound;
import util.exceptions.TaskTimeOverlapException;
import java.util.ArrayList;
@@ -43,7 +44,7 @@ public class InMemoryTaskManager implements TaskManager {
@Override
public void addTask(Task task) throws TaskTimeOverlapException {
if (taskTimeController.isTimeOverlapping(task)) {
- throw new TaskTimeOverlapException("OVERLAP! Can`t add task: " + task);
+ throw new TaskTimeOverlapException("Time overlap! Can`t add task: " + task);
}
task.setTaskId(++idCount);
tasks.put(task.getTaskId(), task);
@@ -59,7 +60,7 @@ public void addEpic(Epic epic) {
@Override
public void addSubTask(SubTask subTask) throws TaskTimeOverlapException {
if (taskTimeController.isTimeOverlapping(subTask)) {
- throw new TaskTimeOverlapException("OVERLAP! Can`t add task: " + subTask);
+ throw new TaskTimeOverlapException("Time overlap! Can`t add subtask: " + subTask);
}
subTask.setTaskId(++idCount);
subtasks.put(subTask.getTaskId(), subTask);
@@ -90,6 +91,21 @@ public SubTask getSubTask(int id) {
return subtasks.get(id);
}
+ @Override
+ public Task getTaskWithoutHistory(int id) {
+ return tasks.get(id);
+ }
+
+ @Override
+ public Epic getEpicWithoutHistory(int id) {
+ return epics.get(id);
+ }
+
+ @Override
+ public SubTask getSubTaskWithoutHistory(int id) {
+ return subtasks.get(id);
+ }
+
@Override
public List getTasks() {
return List.copyOf(tasks.values());
@@ -122,30 +138,139 @@ public List getPrioritizedTasks() {
return taskTimeController.getPrioritizedTasks();
}
+ /**
+ * Обновляет существующую задачу (Task) новыми данными.
+ *
+ * Обновляемые поля:
+ *
+ * - Заголовок (title)
+ * - Описание (description)
+ * - Статус (status)
+ * - Продолжительность (duration)
+ * - Время начала (startTime)
+ *
+ *
+ * Временная логика:
+ *
+ * - Старая задача временно удаляется из временного контроллера
+ * - Проверяется пересечение временных интервалов для новой версии задачи
+ * - Обновленная задача добавляется обратно во временной контроллер
+ * - Если найдено пересечение времени старая задача добавляется обратно во временной контроллер
+ *
+ *
+ * @param task задача с обновленными данными (должна содержать корректный taskId)
+ * @throws TaskNotFound если задача с указанным Id не найдена
+ * @throws TaskTimeOverlapException если новое время задачи пересекается с существующими задачами
+ */
@Override
public void updateTask(Task task) {
- tasks.put(task.getTaskId(), task);
+ int id = task.getTaskId();
+ if (!tasks.containsKey(id)) {
+ throw new TaskNotFound("Task with id: " + id + " not found");
+ }
+ Task oldTask = tasks.get(id);
+ taskTimeController.remove(oldTask);
+
+ if (taskTimeController.isTimeOverlapping(task)) {
+ taskTimeController.add(oldTask);
+ throw new TaskTimeOverlapException("Time overlap! Can`t add task: " + task);
+ }
+
+ oldTask.setTitle(task.getTitle());
+ oldTask.setDescription(task.getDescription());
+ oldTask.setStatus(task.getStatus());
+ oldTask.setDuration(task.getDuration());
+ oldTask.setStartTime(task.getStartTime());
+
+ taskTimeController.add(oldTask);
}
+ /**
+ * Обновляет существующий эпик (Epic) новыми данными.
+ * Метод обновляет только основные данные эпика.
+ *
В отличие от обычных задач, для эпиков:
+ *
+ *
+ * - Не проверяются временные интервалы
+ * - Не обновляется статус (статус эпика рассчитывается автоматически на основе подзадач)
+ * - Не обновляются временные характеристики (duration, startTime)
+ *
+ *
+ * Обновляемые поля:
+ *
+ * - Заголовок (title)
+ * - Описание (description)
+ *
+ *
+ * @param epic эпик с обновленными данными (должен содержать корректный taskId)
+ * @throws TaskNotFound если эпик с указанным Id не найдена
+ */
@Override
public void updateEpic(Epic epic) {
int id = epic.getTaskId();
+ if (!epics.containsKey(id)) {
+ throw new TaskNotFound("Epic with id: " + id + " not found");
+ }
Epic oldEpic = epics.get(id);
oldEpic.setTitle(epic.getTitle());
oldEpic.setDescription(epic.getDescription());
}
+ /**
+ * Обновляет существующую подзадачу (SubTask) новыми данными.
+ *
+ * Обновляемые поля:
+ *
+ * - Заголовок (title)
+ * - Описание (description)
+ * - Статус (status)
+ * - Продолжительность (duration)
+ * - Время начала (startTime)
+ *
+ *
+ * Вторичные эффекты:
+ *
+ * - Автоматическое обновление статуса родительского эпика
+ * - Пересчет временных характеристик родительского эпика
+ *
+ *
+ * Временная логика:
+ *
+ * - Старая подзадача временно удаляется из временного контроллера
+ * - Проверяется пересечение временных интервалов для новой версии задачи
+ * - Обновленная подзадача добавляется обратно во временной контроллер
+ * - Если найдено пересечение времени старая подзадача добавляется обратно во временной контроллер
+ *
+ *
+ * @param subTask подзадача с обновленными данными (должна содержать корректный taskId)
+ * @throws TaskNotFound если подзадача с указанным Id не найдена
+ * @throws TaskTimeOverlapException если новое время подзадачи пересекается с существующими задачами
+ * @see #updateEpicStatus(Epic)
+ * @see TaskTimeController#isTimeOverlapping(Task)
+ */
@Override
public void updateSubTask(SubTask subTask) {
int id = subTask.getTaskId();
+ if (!subtasks.containsKey(id)) {
+ throw new TaskNotFound("SubTask with id: " + id + " not found");
+ }
SubTask oldSubTask = subtasks.get(id);
+ taskTimeController.remove(oldSubTask);
+
+ if (taskTimeController.isTimeOverlapping(subTask)) {
+ taskTimeController.add(oldSubTask);
+ throw new TaskTimeOverlapException("Time overlap! Can`t add subtask: " + subTask);
+ }
oldSubTask.setTitle(subTask.getTitle());
oldSubTask.setDescription(subTask.getDescription());
oldSubTask.setStatus(subTask.getStatus());
+ oldSubTask.setDuration(subTask.getDuration());
+ oldSubTask.setStartTime(subTask.getStartTime());
- updateEpicStatus(epics.get(oldSubTask.getEpicId())); //обновляем статус эпика
+ taskTimeController.add(oldSubTask);
+ updateEpicStatus(epics.get(oldSubTask.getEpicId()));
}
/**
@@ -304,6 +429,22 @@ public void clearSubTasks() {
subtasks.clear();
}
+ @Override
+ public void clearSubTasksFromEpic(int id) {
+ if (!epics.containsKey(id)) {
+ throw new TaskNotFound("Epic with id: '" + id + "' not found");
+ }
+ Epic epic = epics.get(id);
+ List subtasksFromEpic = epic.getSubtaskIds();
+
+ for (Integer key : subtasksFromEpic) {
+ taskTimeController.remove(subtasks.get(key));
+ historyManager.remove(key);
+ subtasks.remove(key);
+ }
+ epic.clearSubtasks();
+ }
+
public int getIdCount() {
return idCount;
}
diff --git a/src/managers/TaskManager.java b/src/managers/TaskManager.java
index 8450f89..fc81db6 100644
--- a/src/managers/TaskManager.java
+++ b/src/managers/TaskManager.java
@@ -24,6 +24,12 @@ public interface TaskManager {
SubTask getSubTask(int id);
+ Task getTaskWithoutHistory(int id);
+
+ Epic getEpicWithoutHistory(int id);
+
+ SubTask getSubTaskWithoutHistory(int id);
+
List getTasks();
List getEpics();
@@ -52,5 +58,7 @@ public interface TaskManager {
void clearSubTasks();
+ void clearSubTasksFromEpic(int id);
+
List getHistory();
}
diff --git a/src/managers/filedbacked/FileBackedTaskManager.java b/src/managers/filedbacked/FileBackedTaskManager.java
index 7174fcc..8036425 100644
--- a/src/managers/filedbacked/FileBackedTaskManager.java
+++ b/src/managers/filedbacked/FileBackedTaskManager.java
@@ -4,8 +4,8 @@
import model.Epic;
import model.SubTask;
import model.Task;
-import util.Status;
-import util.Type;
+import util.enums.Status;
+import util.enums.Type;
import util.exceptions.ManagerLoadException;
import util.exceptions.ManagerSaveException;
@@ -17,8 +17,8 @@
import java.util.List;
import static managers.filedbacked.ParserHelper.*;
-import static util.CsvField.*;
-import static util.Type.*;
+import static util.enums.CsvField.*;
+import static util.enums.Type.*;
/**
* Менеджер задач с сохранением состояния в файл типа csv.
@@ -277,4 +277,10 @@ public void clearSubTasks() {
super.clearSubTasks();
save();
}
+
+ @Override
+ public void clearSubTasksFromEpic(int id) {
+ super.clearSubTasks();
+ save();
+ }
}
diff --git a/src/managers/filedbacked/ParserHelper.java b/src/managers/filedbacked/ParserHelper.java
index 6a80e9e..6823009 100644
--- a/src/managers/filedbacked/ParserHelper.java
+++ b/src/managers/filedbacked/ParserHelper.java
@@ -1,7 +1,7 @@
package managers.filedbacked;
-import util.Status;
-import util.Type;
+import util.enums.Status;
+import util.enums.Type;
import util.exceptions.ManagerLoadException;
import java.time.Duration;
diff --git a/src/model/Epic.java b/src/model/Epic.java
index f451675..5a8a095 100644
--- a/src/model/Epic.java
+++ b/src/model/Epic.java
@@ -1,7 +1,7 @@
package model;
-import util.Status;
-import util.Type;
+import util.enums.Status;
+import util.enums.Type;
import java.time.Duration;
import java.time.LocalDateTime;
diff --git a/src/model/SubTask.java b/src/model/SubTask.java
index 439a05f..5e406a7 100644
--- a/src/model/SubTask.java
+++ b/src/model/SubTask.java
@@ -1,7 +1,7 @@
package model;
-import util.Status;
-import util.Type;
+import util.enums.Status;
+import util.enums.Type;
import java.time.LocalDateTime;
diff --git a/src/model/Task.java b/src/model/Task.java
index 7e31d1f..0aca7ca 100644
--- a/src/model/Task.java
+++ b/src/model/Task.java
@@ -1,7 +1,7 @@
package model;
-import util.Status;
-import util.Type;
+import util.enums.Status;
+import util.enums.Type;
import java.time.Duration;
import java.time.LocalDateTime;
@@ -26,7 +26,7 @@ public Task(String title, String description, Status status, long durationInMinu
this.title = title;
this.description = description;
this.status = status;
- this.duration = durationInMinutes < 0 ? null : Duration.ofMinutes(durationInMinutes);
+ this.duration = durationInMinutes <= 0 ? null : Duration.ofMinutes(durationInMinutes);
this.startTime = startTime;
}
@@ -34,7 +34,7 @@ public Task(String title, String description, Status status, long durationInMinu
this.title = title;
this.description = description;
this.status = status;
- this.duration = Duration.ofMinutes(durationInMinutes);
+ this.duration = durationInMinutes <= 0 ? null : Duration.ofMinutes(durationInMinutes);
}
public int getTaskId() {
diff --git a/src/util/TaskTimeController.java b/src/util/TaskTimeController.java
index 4194747..4643860 100644
--- a/src/util/TaskTimeController.java
+++ b/src/util/TaskTimeController.java
@@ -3,6 +3,7 @@
import model.Epic;
import model.SubTask;
import model.Task;
+import util.enums.Type;
import java.time.LocalDateTime;
import java.util.Comparator;
diff --git a/src/util/CsvField.java b/src/util/enums/CsvField.java
similarity index 93%
rename from src/util/CsvField.java
rename to src/util/enums/CsvField.java
index 3f3b79f..31370d2 100644
--- a/src/util/CsvField.java
+++ b/src/util/enums/CsvField.java
@@ -1,4 +1,4 @@
-package util;
+package util.enums;
public enum CsvField {
ID(0),
diff --git a/src/util/enums/Endpoint.java b/src/util/enums/Endpoint.java
new file mode 100644
index 0000000..4dda49c
--- /dev/null
+++ b/src/util/enums/Endpoint.java
@@ -0,0 +1,13 @@
+package util.enums;
+
+public enum Endpoint {
+ INVALID_SUBRESOURCE,
+
+ INVALID,
+ INVALID_METHOD,
+
+ OPTIONS,
+ GET,
+ POST,
+ DELETE
+}
diff --git a/src/util/Status.java b/src/util/enums/Status.java
similarity index 74%
rename from src/util/Status.java
rename to src/util/enums/Status.java
index 9e4a21d..1b22897 100644
--- a/src/util/Status.java
+++ b/src/util/enums/Status.java
@@ -1,4 +1,4 @@
-package util;
+package util.enums;
public enum Status {
NEW,
diff --git a/src/util/Type.java b/src/util/enums/Type.java
similarity index 72%
rename from src/util/Type.java
rename to src/util/enums/Type.java
index dff2b72..9259e98 100644
--- a/src/util/Type.java
+++ b/src/util/enums/Type.java
@@ -1,4 +1,4 @@
-package util;
+package util.enums;
public enum Type {
TASK,
diff --git a/src/util/exceptions/TaskNotFound.java b/src/util/exceptions/TaskNotFound.java
new file mode 100644
index 0000000..59e0307
--- /dev/null
+++ b/src/util/exceptions/TaskNotFound.java
@@ -0,0 +1,16 @@
+package util.exceptions;
+
+public class TaskNotFound extends RuntimeException {
+
+ public TaskNotFound() {
+ super();
+ }
+
+ public TaskNotFound(String message) {
+ super(message);
+ }
+
+ public TaskNotFound(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/util/gsonadapters/DurationAdapter.java b/src/util/gsonadapters/DurationAdapter.java
new file mode 100644
index 0000000..9bb25bd
--- /dev/null
+++ b/src/util/gsonadapters/DurationAdapter.java
@@ -0,0 +1,22 @@
+package util.gsonadapters;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+
+import java.io.IOException;
+import java.time.Duration;
+
+public class DurationAdapter extends TypeAdapter {
+
+ @Override
+ public void write(JsonWriter jsonWriter, Duration duration) throws IOException {
+ jsonWriter.value(duration == null ? null : duration.toMinutes());
+ }
+
+ @Override
+ public Duration read(JsonReader jsonReader) throws IOException {
+ String string = jsonReader.nextString();
+ return Duration.ofMinutes(Long.parseLong(string));
+ }
+}
diff --git a/src/util/gsonadapters/LocalDateTimeAdapter.java b/src/util/gsonadapters/LocalDateTimeAdapter.java
new file mode 100644
index 0000000..532077a
--- /dev/null
+++ b/src/util/gsonadapters/LocalDateTimeAdapter.java
@@ -0,0 +1,40 @@
+package util.gsonadapters;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.List;
+
+public class LocalDateTimeAdapter extends TypeAdapter {
+ private final List formats =
+ List.of(DateTimeFormatter.ISO_LOCAL_DATE_TIME,
+ DateTimeFormatter.ISO_DATE_TIME,
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"),
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
+ DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")
+ );
+
+ @Override
+ public void write(JsonWriter jsonWriter, LocalDateTime localDateTime) throws IOException {
+ jsonWriter.value(localDateTime == null ? null : localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
+ }
+
+ @Override
+ public LocalDateTime read(JsonReader jsonReader) throws IOException {
+ String date = jsonReader.nextString();
+ for (DateTimeFormatter format : formats) {
+ try {
+ return LocalDateTime.parse(date, format);
+ } catch (DateTimeParseException e) {
+ continue;
+ }
+ }
+
+ throw new DateTimeParseException("Cannot parse date: " + date, date, 0);
+ }
+}
diff --git a/src/util/http/ErrorResponse.java b/src/util/http/ErrorResponse.java
new file mode 100644
index 0000000..3f0196a
--- /dev/null
+++ b/src/util/http/ErrorResponse.java
@@ -0,0 +1,17 @@
+package util.http;
+
+import com.google.gson.Gson;
+
+public class ErrorResponse {
+ private final String error;
+ private final String message;
+
+ private ErrorResponse(String error, String message) {
+ this.error = error;
+ this.message = message;
+ }
+
+ public static String errorToJson(Gson gson, String error, String message) {
+ return gson.toJson(new ErrorResponse(error, message));
+ }
+}
\ No newline at end of file
diff --git a/src/util/http/JsonBuilder.java b/src/util/http/JsonBuilder.java
new file mode 100644
index 0000000..32c730c
--- /dev/null
+++ b/src/util/http/JsonBuilder.java
@@ -0,0 +1,67 @@
+package util.http;
+
+import com.google.gson.Gson;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class JsonBuilder {
+ private final Gson gson;
+ private RequestSegments segments;
+
+ public JsonBuilder(Gson gson) {
+ this.gson = gson;
+ }
+
+ public void setSegments(RequestSegments segments) {
+ this.segments = segments;
+ }
+
+ public String message(String message) {
+ Map map = new HashMap<>();
+ map.put("message", message);
+ return gson.toJson(map);
+ }
+
+ public String tooMuchSubResources() {
+ return notFound("Subresources do not exist");
+ }
+
+ public String badRequest(String message) {
+ return ErrorResponse.errorToJson(
+ gson,
+ "Bad Request",
+ message
+ );
+ }
+
+ public String notFound(String message) {
+ return ErrorResponse.errorToJson(
+ gson,
+ "Not Found",
+ message
+ );
+ }
+
+ public String resourceNotFound() {
+ return notFound("Resource '" + segments.resource() + "' does not exist");
+ }
+
+ public String subresourceNotFound() {
+ return notFound("Subresource '" + segments.subResource().orElse("") + "' does not exist");
+ }
+
+ public String hasOverlaps() {
+ return ErrorResponse.errorToJson(gson, "Task time is overlapping",
+ "This task cannot be added due to overlap"
+ );
+ }
+
+ public String invalidId() {
+ return ErrorResponse.errorToJson(
+ gson,
+ "Bad Request",
+ "Id must be a positive integer"
+ );
+ }
+}
diff --git a/src/util/http/RequestSegments.java b/src/util/http/RequestSegments.java
new file mode 100644
index 0000000..4bb51ab
--- /dev/null
+++ b/src/util/http/RequestSegments.java
@@ -0,0 +1,145 @@
+package util.http;
+
+import com.sun.net.httpserver.HttpExchange;
+import util.enums.Endpoint;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Класс для парсинга сегментов HTTP-запроса.
+ *
+ * Разбирает URI пути запроса на составные части: endpoint, ресурс,
+ * идентификатор и опциональный подресурс. Также выполняет валидацию HTTP-метода.
+ *
+ *
+ * Формат пути:
+ *
+ * - {@code /resource} - ресурс без идентификатора
+ * - {@code /resource/123} - ресурс с числовым идентификатором
+ * - {@code /resource/123/subresource} - ресурс с идентификатором и подресурсом
+ *
+ *
+ * Поддерживаемые HTTP-методы: GET, POST, DELETE, HEAD, OPTIONS
+ *
+ * Особенности обработки:
+ *
+ * - Метод HEAD обрабатывается как GET
+ * - Некорректные идентификаторы преобразуются в -1
+ * - Неподдерживаемые методы возвращают соответствующие
+ * значения Endpoint.INVALID_METHOD
+ * - Ошбика парсинга Endpoint возвращает Enpoint.INVALID
+ *
+ *
+ * @param endpoint конечная точка запроса (на основе HTTP-метода)
+ * @param resource название основного ресурса (первый сегмент пути после /)
+ * @param id числовой идентификатор ресурса (второй сегмент пути)
+ * @param subResource опциональный подресурс (третий сегмент пути)
+ */
+public record RequestSegments(
+ Endpoint endpoint,
+ String resource,
+ int id,
+ Optional subResource) {
+
+ /**
+ * Создает экземпляр RequestSegments на основе HttpExchange.
+ *
+ * Извлекает метод запроса и путь из переданного HttpExchange и делегирует
+ * парсинг методу {@link #parse(String, String)}.
+ *
+ *
+ * @param exchange HTTP-обмен для парсинга
+ * @return новый экземпляр RequestSegments с разобранными сегментами пути
+ * @see #parse(String, String)
+ */
+ public static RequestSegments getRequestSegments(HttpExchange exchange) {
+ String path = exchange.getRequestURI().getPath();
+ return RequestSegments.parse(exchange.getRequestMethod(), path);
+ }
+
+ /**
+ * Парсит HTTP-метод и путь на составляющие сегменты.
+ *
+ * В зависимости от количества сегментов пути создает соответствующий экземпляр:
+ *
+ *
+ * | Сегментов | Формат | Результат |
+ * | 2 | /resource | resource, id=0, subResource=empty |
+ * | 3 | /resource/123 | resource, id=123, subResource=empty |
+ * | 4 | /resource/123/sub | resource, id=123, subResource=sub |
+ * | другое | любой | Endpoint.INVALID_SUBRESOURCE |
+ *
+ *
+ * @param method HTTP-метод запроса
+ * @param path путь URI запроса
+ * @return экземпляр RequestSegments с разобранными сегментами
+ */
+ private static RequestSegments parse(String method, String path) {
+ String[] parts = path.split("/");
+
+ return switch (parts.length) {
+ case 2 -> new RequestSegments(
+ parseEndpoint(method),
+ parts[1],
+ 0,
+ Optional.empty());
+ case 3 -> new RequestSegments(
+ parseEndpoint(method),
+ parts[1],
+ parseInt(parts[2]),
+ Optional.empty());
+ case 4 -> new RequestSegments(
+ parseEndpoint(method),
+ parts[1],
+ parseInt(parts[2]),
+ Optional.of(parts[3]));
+ default -> new RequestSegments(
+ Endpoint.INVALID_SUBRESOURCE,
+ parts[1],
+ 0,
+ Optional.empty());
+ };
+ }
+
+ private static int parseInt(String idString) {
+ try {
+ int id = Integer.parseInt(idString);
+ if (id < 0) {
+ throw new NumberFormatException();
+ } else {
+ return id;
+ }
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+ }
+
+ private static Endpoint parseEndpoint(String method) {
+ HashSet validMethods = new HashSet<>(List.of(
+ "GET",
+ "POST",
+ "DELETE",
+ "HEAD",
+ "OPTIONS")
+ );
+ if (!validMethods.contains(method)) {
+ return Endpoint.INVALID_METHOD;
+ }
+
+ Endpoint endpoint;
+
+ if (method.equals("HEAD")) {
+ method = "GET";
+ }
+
+ try {
+ endpoint = Endpoint.valueOf(method);
+ } catch (IllegalArgumentException e) {
+ endpoint = Endpoint.INVALID;
+ }
+
+ return endpoint;
+ }
+}
diff --git a/test/http/HttpTaskServerTest.java b/test/http/HttpTaskServerTest.java
new file mode 100644
index 0000000..31b14d7
--- /dev/null
+++ b/test/http/HttpTaskServerTest.java
@@ -0,0 +1,151 @@
+package http;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import managers.InMemoryTaskManager;
+import managers.TaskManager;
+import model.Epic;
+import model.SubTask;
+import model.Task;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import util.enums.Status;
+import util.gsonadapters.DurationAdapter;
+import util.gsonadapters.LocalDateTimeAdapter;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class HttpTaskServerTest {
+
+ protected HttpClient client;
+ protected HttpTaskServer server;
+ protected TaskManager manager;
+ protected final Gson gson = new GsonBuilder()
+ .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
+ .registerTypeAdapter(Duration.class, new DurationAdapter())
+ .create();
+
+ protected final LocalDateTime epochTime =
+ LocalDateTime.of(1970, 1, 1, 0, 0, 0);
+
+ protected Task task;
+ protected Epic epic;
+ protected SubTask subTask;
+
+ @BeforeEach
+ public void initServer() {
+ client = HttpClient.newHttpClient();
+ manager = new InMemoryTaskManager();
+ server = new HttpTaskServer(manager);
+
+ task = new Task("task1", "demo", Status.NEW);
+ epic = new Epic("epic1", "demo", Status.NEW);
+ subTask = new SubTask("subtask1", "demo", Status.NEW, 1);
+ server.start();
+ }
+
+ @AfterEach
+ public void stopServer() {
+ server.stop();
+ }
+
+ protected HttpRequest getMethod(String method, String path) {
+ URI url = URI.create(path);
+
+ HttpRequest.Builder builder = HttpRequest.newBuilder().uri(url);
+
+ switch (method) {
+ case "HEAD" -> builder.HEAD();
+ case "OPTIONS" -> builder.method("OPTIONS", HttpRequest.BodyPublishers.noBody());
+ case "GET" -> builder.GET();
+ case "DELETE" -> builder.DELETE();
+ }
+
+ return builder.build();
+ }
+
+ protected HttpRequest getPost(String value, String taskJson) {
+ URI url = URI.create(value);
+
+ return HttpRequest
+ .newBuilder()
+ .POST(HttpRequest.BodyPublishers.ofString(taskJson))
+ .uri(url)
+ .build();
+ }
+
+ @Nested
+ class BaseHandlerTest {
+ private HttpRequest getInvalidMethod() {
+ URI url = URI.create("http://localhost:8080/tasks");
+
+ return HttpRequest
+ .newBuilder()
+ .PUT(HttpRequest.BodyPublishers.ofString(""))
+ .uri(url)
+ .build();
+ }
+
+ @Test
+ public void shouldReturn405WhenForbiddenMethod() throws IOException, InterruptedException {
+
+ HttpResponse response = client.send(
+ getInvalidMethod(),
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ assertEquals(405, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn404WhenTooManySubs() throws IOException, InterruptedException {
+
+ HttpResponse response = client.send(
+ getMethod("GET", "http://localhost:8080/tasks/1/sub/sub"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn405WhenInvalidMethod() throws IOException, InterruptedException {
+ HttpRequest request = HttpRequest
+ .newBuilder()
+ .method("BlaBlaBla", HttpRequest.BodyPublishers.noBody())
+ .uri(URI.create("http://localhost:8080/tasks"))
+ .build();
+
+
+ HttpResponse response = client.send(
+ request,
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ assertEquals(405, response.statusCode());
+ }
+ }
+
+ public class ListTaskTypeToken extends TypeToken> {
+ }
+
+ public class ListEpicTypeToken extends TypeToken> {
+ }
+
+ public class ListSubTaskTypeToken extends TypeToken> {
+ }
+}
+
+
diff --git a/test/http/handlers/EpicsHandlerTest.java b/test/http/handlers/EpicsHandlerTest.java
new file mode 100644
index 0000000..1aac956
--- /dev/null
+++ b/test/http/handlers/EpicsHandlerTest.java
@@ -0,0 +1,393 @@
+package http.handlers;
+
+import http.HttpTaskServerTest;
+import model.Epic;
+import model.SubTask;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import util.enums.Status;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class EpicsHandlerTest extends HttpTaskServerTest {
+
+ @Nested
+ class EpicHeadTest {
+ private HttpRequest getRequest(String value) {
+ URI url = URI.create(value);
+
+ return HttpRequest
+ .newBuilder()
+ .HEAD()
+ .uri(url)
+ .build();
+ }
+
+ @Test
+ public void shouldReturn200AndRightHeadersWhenHeadWithEmptyEpics() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics"),
+ HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.headers().firstValue("Content-type"));
+ assertEquals(200, response.statusCode());
+
+ assertEquals("application/json;charset=utf-8",
+ response.headers().firstValue("Content-type").get());
+ assertEquals("2",
+ response.headers().firstValue("Content-length").get());
+ }
+
+ @Test
+ public void shouldReturn404AndRightHeadersWhenHeadWithInvalidEpic() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics/1"),
+ HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.headers().firstValue("Content-type"));
+ assertEquals(404, response.statusCode());
+
+ assertEquals("application/json;charset=utf-8",
+ response.headers().firstValue("Content-type").get());
+ assertEquals("69",
+ response.headers().firstValue("Content-length").get());
+ }
+
+ @Test
+ public void shouldReturn200AndRightHeadersWhenHeadWithSubResource() throws IOException, InterruptedException {
+ manager.addEpic(epic);
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics/1/subtasks"),
+ HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.headers().firstValue("Content-type"));
+ assertEquals(200, response.statusCode());
+
+ assertEquals("application/json;charset=utf-8",
+ response.headers().firstValue("Content-type").get());
+ assertEquals("2",
+ response.headers().firstValue("Content-length").get());
+ }
+
+ @Test
+ public void shouldReturn400AndRightHeadersWhenHeadWithInvalidId() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics/-1"),
+ HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.headers().firstValue("Content-type"));
+ assertEquals(400, response.statusCode());
+
+ assertEquals("application/json;charset=utf-8",
+ response.headers().firstValue("Content-type").get());
+ assertEquals("65",
+ response.headers().firstValue("Content-length").get());
+ }
+
+ }
+
+ @Nested
+ class EpicGetTest {
+
+ private HttpRequest getRequest(String value) {
+ return getMethod("GET", value);
+ }
+
+ @Test
+ public void shouldReturn404WhenInvalidResource() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epicsABC"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldBeEqualAndCode200() throws IOException, InterruptedException {
+ manager.addEpic(epic);
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics/1"),
+ HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.body());
+ assertEquals(200, response.statusCode());
+ assertEquals(epic, gson.fromJson(response.body(), Epic.class));
+ }
+
+ @Test
+ public void shouldSameAmountAndCode200() throws IOException, InterruptedException {
+ manager.addEpic(epic);
+ manager.addEpic(new Epic("epic2", "demo", Status.NEW));
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics"),
+ HttpResponse.BodyHandlers.ofString());
+
+ List list = gson.fromJson(response.body(), new ListEpicTypeToken().getType());
+
+ assertEquals(200, response.statusCode());
+ assertEquals(2, list.size());
+ }
+
+ @Test
+ public void shouldReturn404WhenNoId() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics/1"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn404WhenSubResourcePresent() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics/1/sub"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn404WhenIdInvalid() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics/-2a"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(400, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn200AndSubtasks() throws IOException, InterruptedException {
+ manager.addEpic(epic);
+ manager.addSubTask(subTask);
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics/1/subtasks"),
+ HttpResponse.BodyHandlers.ofString());
+
+ List list = gson.fromJson(response.body(), new ListSubTaskTypeToken().getType());
+
+ assertEquals(200, response.statusCode());
+ assertEquals(1, list.size());
+ assertEquals(subTask, list.getFirst());
+
+ }
+
+ @Test
+ public void shouldReturn404WhenNoIdAndSubTasks() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics/1/subtasks"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+ }
+
+ @Nested
+ class EpicPostTest {
+
+ @Test
+ public void shouldBeEqualAndCode201() throws IOException, InterruptedException {
+ String taskJson = gson.toJson(epic);
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/epics", taskJson),
+ HttpResponse.BodyHandlers.ofString());
+
+ epic.setTaskId(1);
+
+ assertEquals(201, response.statusCode());
+ assertEquals(epic, manager.getEpic(1));
+ }
+
+ @Test
+ public void shouldBeUpdatedAndCode201() throws IOException, InterruptedException {
+ manager.addEpic(epic);
+
+ epic.setTitle("new title");
+ String taskJson = gson.toJson(epic);
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/epics", taskJson),
+ HttpResponse.BodyHandlers.ofString());
+
+
+ assertEquals(201, response.statusCode());
+ assertEquals(epic, manager.getEpic(1));
+ }
+
+ @Test
+ public void shouldNotBeUpdatedAndCode404WhenNotFound() throws IOException, InterruptedException {
+
+ epic.setTaskId(1);
+ String taskJson = gson.toJson(epic);
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/epics", taskJson),
+ HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.body());
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn405AndNotPostWhenIdAndSubIsPresent() throws IOException, InterruptedException {
+ String taskJson = gson.toJson(epic);
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/epics/1/subtasks", taskJson),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ epic.setTaskId(1);
+
+ assertEquals(405, response.statusCode());
+ assertNull(manager.getEpic(1));
+ }
+
+ @Test
+ public void shouldReturn400WhenNoBody() throws IOException, InterruptedException {
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/epics", ""),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ epic.setTaskId(1);
+
+ assertEquals(400, response.statusCode());
+ assertNull(manager.getEpic(1));
+ }
+
+ @Test
+ public void shouldReturn400WhenInvalidBody() throws IOException, InterruptedException {
+ String taskJson = "{blablabla:blabla}";
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/epics", taskJson),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ epic.setTaskId(1);
+
+ assertEquals(400, response.statusCode());
+ assertNull(manager.getEpic(1));
+ }
+
+ }
+
+ @Nested
+ class EpicDeleteTest {
+ private HttpRequest getRequest(String value) {
+ return getMethod("DELETE", value);
+ }
+
+ @Test
+ public void shouldReturn404WhenIdIsNotPresentAndDeletingSubtasks() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics/1/subtasks"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn200AndDeleteAllEpics() throws IOException, InterruptedException {
+ manager.addEpic(epic);
+ manager.addEpic(new Epic("epic2", "demo", Status.NEW));
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ assertEquals(200, response.statusCode());
+ assertEquals(0, manager.getEpics().size());
+ }
+
+ @Test
+ public void shouldReturn200AndDeleteEpic() throws IOException, InterruptedException {
+ manager.addEpic(epic);
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics/1"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ assertEquals(200, response.statusCode());
+ assertNull(manager.getEpic(1));
+ }
+
+ @Test
+ public void shouldReturn200AndDeleteSubtasksFromEpic() throws IOException, InterruptedException {
+ manager.addEpic(epic);
+ manager.addSubTask(subTask);
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics/1/subtasks"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ assertEquals(200, response.statusCode());
+ assertEquals(0, manager.getSubTasksFromEpic(1).size());
+ }
+
+
+ }
+
+ @Nested
+ class EpicOptionsTest {
+
+ private HttpRequest getRequest(String value) {
+ return getMethod("OPTIONS", value);
+ }
+
+ @Test
+ public void shouldReturn204AndValidMethods() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ List expected = List.of("GET, POST, DELETE, OPTIONS, HEAD");
+
+ List methods = response.headers().allValues("Allow");
+
+ assertEquals(204, response.statusCode());
+ assertEquals(expected, methods);
+ }
+
+ @Test
+ public void shouldReturn204AndValidMethodsWhenIdIsPresent() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasks/1"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ List expected = List.of("GET, DELETE, OPTIONS, HEAD");
+
+ List methods = response.headers().allValues("Allow");
+
+ assertEquals(204, response.statusCode());
+ assertEquals(expected, methods);
+ }
+
+
+ @Test
+ public void shouldReturn204AndValidMethodsWhenIdAndSubTasksIsPresent() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/epics/1/subtasks"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ List expected = List.of("GET, DELETE, OPTIONS, HEAD");
+
+ List methods = response.headers().allValues("Allow");
+
+ assertEquals(204, response.statusCode());
+ assertEquals(expected, methods);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/http/handlers/HistoryHandlerTest.java b/test/http/handlers/HistoryHandlerTest.java
new file mode 100644
index 0000000..c7530df
--- /dev/null
+++ b/test/http/handlers/HistoryHandlerTest.java
@@ -0,0 +1,203 @@
+package http.handlers;
+
+import http.HttpTaskServerTest;
+import model.Task;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class HistoryHandlerTest extends HttpTaskServerTest {
+
+ @BeforeEach
+ public void addTasks() {
+ manager.addEpic(epic);
+ manager.addTask(task);
+ manager.addSubTask(subTask);
+ }
+
+ @Nested
+ class HistoryHeadTest {
+
+ private HttpRequest getRequest(String value) {
+ return getMethod("HEAD", value);
+ }
+
+ @Test
+ public void shouldReturn200AndRightHeadersWhenHeadWithEmptyTasks() throws IOException, InterruptedException {
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/history"),
+ HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.headers().firstValue("Content-type"));
+ assertEquals(200, response.statusCode());
+
+ assertEquals("application/json;charset=utf-8",
+ response.headers().firstValue("Content-type").get());
+ assertEquals("2",
+ response.headers().firstValue("Content-length").get());
+ }
+
+ @Test
+ public void shouldReturn404AndRightHeadersWhenHeadWithInvalidId() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/history/1"),
+ HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.headers().firstValue("Content-type"));
+ assertEquals(404, response.statusCode());
+
+ assertEquals("application/json;charset=utf-8",
+ response.headers().firstValue("Content-type").get());
+ assertEquals("73",
+ response.headers().firstValue("Content-length").get());
+ }
+
+ @Test
+ public void shouldReturn404AndRightHeadersWhenHeadWithInvalidSubResource() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/history/1/sub"),
+ HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.headers().firstValue("Content-type"));
+ assertEquals(404, response.statusCode());
+
+ assertEquals("application/json;charset=utf-8",
+ response.headers().firstValue("Content-type").get());
+ assertEquals("76",
+ response.headers().firstValue("Content-length").get());
+ }
+
+ @Test
+ public void shouldReturn400AndRightHeadersWhenHeadWithInvalidId() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/history/-1"),
+ HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.headers().firstValue("Content-type"));
+ assertEquals(400, response.statusCode());
+
+ assertEquals("application/json;charset=utf-8",
+ response.headers().firstValue("Content-type").get());
+ assertEquals("65",
+ response.headers().firstValue("Content-length").get());
+ }
+
+ }
+
+ @Nested
+ class HistoryGetTest {
+
+ private HttpRequest getRequest(String value) {
+ return getMethod("GET", value);
+ }
+
+ @Test
+ public void shouldBeEqualAndCode200() throws IOException, InterruptedException {
+
+ manager.getEpic(1);
+ manager.getTask(2);
+ manager.getSubTask(3);
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/history"),
+ HttpResponse.BodyHandlers.ofString());
+
+
+ List list = gson.fromJson(response.body(), new ListTaskTypeToken().getType());
+ assertEquals(200, response.statusCode());
+ assertEquals(3, list.size());
+ }
+
+ @Test
+ public void shouldReturn404WithInvalidId() throws IOException, InterruptedException {
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/history/1"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn404WithInvalidIdAndSub() throws IOException, InterruptedException {
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/history/1/sub"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn404WithInvalidResource() throws IOException, InterruptedException {
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/historyABC"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ }
+
+ @Nested
+ class HistoryPostTest {
+
+ @Test
+ public void shouldReturn405() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/history", ""),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(405, response.statusCode());
+ }
+ }
+
+ @Nested
+ class HistoryDeleteTest {
+
+ private HttpRequest getRequest(String value) {
+ return getMethod("DELETE", value);
+ }
+
+ @Test
+ public void shouldReturn405() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/history"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(405, response.statusCode());
+ }
+ }
+
+ @Nested
+ class SubTaskOptionsTest {
+
+ private HttpRequest getRequest(String value) {
+ return getMethod("OPTIONS", value);
+ }
+
+ @Test
+ public void shouldReturn204AndValidMethods() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/history"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ List expected = List.of("GET, OPTIONS, HEAD");
+
+ List methods = response.headers().allValues("Allow");
+
+ assertEquals(204, response.statusCode());
+ assertEquals(expected, methods);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/test/http/handlers/PrioritizedHandlerTest.java b/test/http/handlers/PrioritizedHandlerTest.java
new file mode 100644
index 0000000..cb9edd4
--- /dev/null
+++ b/test/http/handlers/PrioritizedHandlerTest.java
@@ -0,0 +1,73 @@
+package http.handlers;
+
+import http.HttpTaskServerTest;
+import model.Task;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import util.enums.Status;
+
+import java.io.IOException;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class PrioritizedHandlerTest extends HttpTaskServerTest {
+
+ @Nested
+ class HistoryGetTest {
+
+ private HttpRequest getRequest(String value) {
+ return getMethod("GET", value);
+ }
+
+ @Test
+ public void shouldBeEqualAndCode200() throws IOException, InterruptedException {
+ manager.addTask(new Task("task1", "demo", Status.NEW, 10, epochTime));
+ manager.addTask(new Task("task2", "demo", Status.NEW, 10, epochTime.plusMinutes(10)));
+ manager.addTask(task);
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/prioritized"),
+ HttpResponse.BodyHandlers.ofString());
+
+
+ List list = gson.fromJson(response.body(), new ListTaskTypeToken().getType());
+ assertEquals(200, response.statusCode());
+ assertEquals(2, list.size());
+ }
+
+ @Test
+ public void shouldReturn404WithInvalidId() throws IOException, InterruptedException {
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/prioritized/1"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn404WithInvalidIdAndSub() throws IOException, InterruptedException {
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/prioritized/1/sub"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn404WithInvalidResource() throws IOException, InterruptedException {
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/prioritizedABC"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/test/http/handlers/SubTasksHandlerTest.java b/test/http/handlers/SubTasksHandlerTest.java
new file mode 100644
index 0000000..801bfa9
--- /dev/null
+++ b/test/http/handlers/SubTasksHandlerTest.java
@@ -0,0 +1,245 @@
+package http.handlers;
+
+import http.HttpTaskServerTest;
+import model.SubTask;
+import model.Task;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import util.enums.Status;
+
+import java.io.IOException;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class SubTasksHandlerTest extends HttpTaskServerTest {
+
+ @BeforeEach
+ public void addEpic() {
+ manager.addEpic(epic);
+ }
+
+ @Nested
+ class SubTaskGetTest {
+
+ private HttpRequest getRequest(String value) {
+ return getMethod("GET", value);
+ }
+
+ @Test
+ public void shouldReturn404WhenInvalidResource() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/subtasksABC"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldBeEqualAndCode200() throws IOException, InterruptedException {
+ manager.addSubTask(subTask);
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/subtasks/2"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(200, response.statusCode());
+ assertEquals(subTask, gson.fromJson(response.body(), SubTask.class));
+ }
+
+ @Test
+ public void shouldSameAmountAndCode200() throws IOException, InterruptedException {
+ manager.addSubTask(subTask);
+ manager.addSubTask(new SubTask("subtask2", "demo", Status.NEW, 1));
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/subtasks"),
+ HttpResponse.BodyHandlers.ofString());
+
+ List list = gson.fromJson(response.body(), new ListSubTaskTypeToken().getType());
+
+ assertEquals(200, response.statusCode());
+ assertEquals(2, list.size());
+ }
+
+ @Test
+ public void shouldReturn404WhenNoId() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/subtasks/2"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn404WhenSubResourcePresent() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/subtasks/2/sub"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn404WhenIdInvalid() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/subtasks/-2a"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(400, response.statusCode());
+ }
+
+ }
+
+ @Nested
+ class SubTaskPostTest {
+
+ @Test
+ public void shouldBeEqualAndCode201() throws IOException, InterruptedException {
+ String taskJson = gson.toJson(subTask);
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/subtasks", taskJson),
+ HttpResponse.BodyHandlers.ofString());
+
+ subTask.setTaskId(2);
+
+ assertEquals(201, response.statusCode());
+ assertEquals(subTask, manager.getSubTask(2));
+ }
+
+ @Test
+ public void shouldBeUpdatedAndCode201() throws IOException, InterruptedException {
+ manager.addSubTask(subTask);
+
+ subTask.setTitle("new title");
+ String taskJson = gson.toJson(subTask);
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/subtasks", taskJson),
+ HttpResponse.BodyHandlers.ofString());
+
+
+ assertEquals(201, response.statusCode());
+ assertEquals(subTask, manager.getSubTask(2));
+ }
+
+ @Test
+ public void shouldNotBeUpdatedAndCode404WhenNotFound() throws IOException, InterruptedException {
+
+ subTask.setTaskId(2);
+ String taskJson = gson.toJson(subTask);
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/subtasks", taskJson),
+ HttpResponse.BodyHandlers.ofString());
+
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn405AndNotPostWhenIdIsPresent() throws IOException, InterruptedException {
+ String taskJson = gson.toJson(subTask);
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/subtasks/2", taskJson),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ subTask.setTaskId(2);
+
+ assertEquals(405, response.statusCode());
+ assertNull(manager.getSubTask(2));
+ }
+
+ @Test
+ public void shouldReturn400WhenNoBody() throws IOException, InterruptedException {
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/subtasks", ""),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ subTask.setTaskId(2);
+
+ assertEquals(400, response.statusCode());
+ assertNull(manager.getSubTask(2));
+ }
+
+ @Test
+ public void shouldReturn400WhenInvalidBody() throws IOException, InterruptedException {
+ String taskJson = "{blablabla:blabla}";
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/subtasks", taskJson),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ subTask.setTaskId(2);
+
+ assertEquals(400, response.statusCode());
+ assertNull(manager.getSubTask(2));
+ }
+
+ @Test
+ public void shouldReturn406WhenTimeOverlap() throws IOException, InterruptedException {
+
+ manager.addSubTask(new SubTask("subtask1",
+ "demo",
+ Status.NEW,
+ 1,
+ 10,
+ epochTime));
+
+ String taskJson = gson.toJson(new SubTask("subtask2",
+ "demo",
+ Status.NEW,
+ 1,
+ 5,
+ epochTime));
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/subtasks", taskJson),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(406, response.statusCode());
+ }
+ }
+
+ @Nested
+ class SubTaskDeleteTest {
+
+ private HttpRequest getRequest(String value) {
+ return getMethod("DELETE", value);
+ }
+
+ @Test
+ public void shouldReturn200AndDeleteAllTasks() throws IOException, InterruptedException {
+ manager.addSubTask(subTask);
+ manager.addSubTask(new SubTask("subtask2", "demo", Status.NEW, 1));
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/subtasks"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ assertEquals(200, response.statusCode());
+ assertEquals(0, manager.getSubTasks().size());
+ }
+
+ @Test
+ public void shouldReturn200AndDeleteTask() throws IOException, InterruptedException {
+ manager.addSubTask(subTask);
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/subtasks/2"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ assertEquals(200, response.statusCode());
+ assertNull(manager.getSubTask(2));
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/test/http/handlers/TaskHandlerTest.java b/test/http/handlers/TaskHandlerTest.java
new file mode 100644
index 0000000..29b1692
--- /dev/null
+++ b/test/http/handlers/TaskHandlerTest.java
@@ -0,0 +1,332 @@
+package http.handlers;
+
+import http.HttpTaskServerTest;
+import model.Task;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import util.enums.Status;
+
+import java.io.IOException;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+
+class TaskHandlerTest extends HttpTaskServerTest {
+
+ @Nested
+ class TaskHeadTest {
+
+ private HttpRequest getRequest(String value) {
+ return getMethod("HEAD", value);
+ }
+
+ @Test
+ public void shouldReturn200AndRightHeadersWhenHeadWithEmptyTasks() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasks"),
+ HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.headers().firstValue("Content-type"));
+ assertEquals(200, response.statusCode());
+
+ assertEquals("application/json;charset=utf-8",
+ response.headers().firstValue("Content-type").get());
+ assertEquals("2",
+ response.headers().firstValue("Content-length").get());
+ }
+
+ @Test
+ public void shouldReturn404AndRightHeadersWhenHeadWithInvalidTask() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasks/1"),
+ HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.headers().firstValue("Content-type"));
+ assertEquals(404, response.statusCode());
+
+ assertEquals("application/json;charset=utf-8",
+ response.headers().firstValue("Content-type").get());
+ assertEquals("69",
+ response.headers().firstValue("Content-length").get());
+ }
+
+ @Test
+ public void shouldReturn404AndRightHeadersWhenHeadWithInvalidSubResource() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasks/1/sub"),
+ HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.headers().firstValue("Content-type"));
+ assertEquals(404, response.statusCode());
+
+ assertEquals("application/json;charset=utf-8",
+ response.headers().firstValue("Content-type").get());
+ assertEquals("76",
+ response.headers().firstValue("Content-length").get());
+ }
+
+ @Test
+ public void shouldReturn400AndRightHeadersWhenHeadWithInvalidId() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasks/-1"),
+ HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.headers().firstValue("Content-type"));
+ assertEquals(400, response.statusCode());
+
+ assertEquals("application/json;charset=utf-8",
+ response.headers().firstValue("Content-type").get());
+ assertEquals("65",
+ response.headers().firstValue("Content-length").get());
+ }
+ }
+
+ @Nested
+ class TaskGetTest {
+
+ private HttpRequest getRequest(String value) {
+ return getMethod("GET", value);
+ }
+
+ @Test
+ public void shouldReturn404WhenInvalidResource() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasksABC"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldBeEqualAndCode200() throws IOException, InterruptedException {
+ manager.addTask(task);
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasks/1"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(200, response.statusCode());
+ assertEquals(task, gson.fromJson(response.body(), Task.class));
+ }
+
+ @Test
+ public void shouldSameAmountAndCode200() throws IOException, InterruptedException {
+ manager.addTask(task);
+ manager.addTask(new Task("task2", "demo", Status.NEW));
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasks"),
+ HttpResponse.BodyHandlers.ofString());
+
+ List list = gson.fromJson(response.body(), new ListSubTaskTypeToken().getType());
+
+ assertEquals(200, response.statusCode());
+ assertEquals(2, list.size());
+ }
+
+ @Test
+ public void shouldReturn404WhenNoId() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasks/1"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn404WhenSubResourcePresent() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasks/1/sub"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn404WhenIdInvalid() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasks/-2a"),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(400, response.statusCode());
+ }
+
+ }
+
+ @Nested
+ class TaskPostTest {
+
+ @Test
+ public void shouldBeEqualAndCode201() throws IOException, InterruptedException {
+ String taskJson = gson.toJson(task);
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/tasks", taskJson),
+ HttpResponse.BodyHandlers.ofString());
+
+ task.setTaskId(1);
+
+ assertEquals(201, response.statusCode());
+ assertEquals(task, manager.getTask(1));
+ }
+
+ @Test
+ public void shouldBeUpdatedAndCode201() throws IOException, InterruptedException {
+ manager.addTask(task);
+
+ task.setTitle("new title");
+ String taskJson = gson.toJson(task);
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/tasks", taskJson),
+ HttpResponse.BodyHandlers.ofString());
+
+
+ assertEquals(201, response.statusCode());
+ assertEquals(task, manager.getTask(1));
+ }
+
+ @Test
+ public void shouldNotBeUpdatedAndCode404WhenNotFound() throws IOException, InterruptedException {
+
+ task.setTaskId(1);
+ String taskJson = gson.toJson(task);
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/tasks", taskJson),
+ HttpResponse.BodyHandlers.ofString());
+
+
+ assertEquals(404, response.statusCode());
+ }
+
+ @Test
+ public void shouldReturn405AndNotPostWhenIdIsPresent() throws IOException, InterruptedException {
+ String taskJson = gson.toJson(task);
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/tasks/1", taskJson),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ task.setTaskId(1);
+
+ assertEquals(405, response.statusCode());
+ assertNull(manager.getTask(1));
+ }
+
+ @Test
+ public void shouldReturn400WhenNoBody() throws IOException, InterruptedException {
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/tasks", ""),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ task.setTaskId(1);
+
+ assertEquals(400, response.statusCode());
+ assertNull(manager.getTask(1));
+ }
+
+ @Test
+ public void shouldReturn400WhenInvalidBody() throws IOException, InterruptedException {
+ String taskJson = "{blablabla:blabla}";
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/tasks", taskJson),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ task.setTaskId(1);
+
+ assertEquals(400, response.statusCode());
+ assertNull(manager.getTask(1));
+ }
+
+ @Test
+ public void shouldReturn406WhenTimeOverlap() throws IOException, InterruptedException {
+ manager.addTask(new Task("task1", "demo", Status.NEW, 10, epochTime));
+
+ String taskJson = gson.toJson(new Task("task2", "demo", Status.NEW, 5, epochTime));
+
+ HttpResponse response = client.send(
+ getPost("http://localhost:8080/tasks", taskJson),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(406, response.statusCode());
+ }
+ }
+
+ @Nested
+ class TaskDeleteTest {
+
+ private HttpRequest getRequest(String value) {
+ return getMethod("DELETE", value);
+ }
+
+ @Test
+ public void shouldReturn200AndDeleteAllTasks() throws IOException, InterruptedException {
+ manager.addTask(task);
+ manager.addTask(new Task("task2", "demo", Status.NEW));
+
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasks"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ assertEquals(200, response.statusCode());
+ assertEquals(0, manager.getTasks().size());
+ }
+
+ @Test
+ public void shouldReturn200AndDeleteTask() throws IOException, InterruptedException {
+ manager.addTask(task);
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasks/1"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ assertEquals(200, response.statusCode());
+ assertNull(manager.getTask(1));
+ }
+ }
+
+ @Nested
+ class TaskOptionsTest {
+
+ private HttpRequest getRequest(String value) {
+ return getMethod("OPTIONS", value);
+ }
+
+ @Test
+ public void shouldReturn204AndValidMethods() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasks"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ List expected = List.of("GET, POST, DELETE, OPTIONS, HEAD");
+
+ List methods = response.headers().allValues("Allow");
+
+ assertEquals(204, response.statusCode());
+ assertEquals(expected, methods);
+ }
+
+ @Test
+ public void shouldReturn204AndValidMethodsWhenIdIsPresent() throws IOException, InterruptedException {
+ HttpResponse response = client.send(
+ getRequest("http://localhost:8080/tasks/1"),
+ HttpResponse.BodyHandlers.ofString()
+ );
+ List expected = List.of("GET, DELETE, OPTIONS, HEAD");
+
+ List methods = response.headers().allValues("Allow");
+
+ assertEquals(204, response.statusCode());
+ assertEquals(expected, methods);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/test/managers/FileBackedTaskManagerTest.java b/test/managers/FileBackedTaskManagerTest.java
index eb4a9c7..0d2d48f 100644
--- a/test/managers/FileBackedTaskManagerTest.java
+++ b/test/managers/FileBackedTaskManagerTest.java
@@ -6,7 +6,7 @@
import model.Task;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import util.Status;
+import util.enums.Status;
import util.exceptions.ManagerLoadException;
import util.exceptions.ManagerSaveException;
diff --git a/test/managers/InMemoryHistoryManagerTest.java b/test/managers/InMemoryHistoryManagerTest.java
index eb1f007..c4e8c6d 100644
--- a/test/managers/InMemoryHistoryManagerTest.java
+++ b/test/managers/InMemoryHistoryManagerTest.java
@@ -5,7 +5,7 @@
import model.SubTask;
import model.Task;
import org.junit.jupiter.api.Test;
-import util.Status;
+import util.enums.Status;
import static org.junit.jupiter.api.Assertions.*;
diff --git a/test/managers/InMemoryTaskManagerTest.java b/test/managers/InMemoryTaskManagerTest.java
index 174f55f..8aa53cb 100644
--- a/test/managers/InMemoryTaskManagerTest.java
+++ b/test/managers/InMemoryTaskManagerTest.java
@@ -5,7 +5,7 @@
import model.Task;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import util.Status;
+import util.enums.Status;
import util.exceptions.TaskTimeOverlapException;
import java.time.LocalDateTime;
diff --git a/test/model/EpicTest.java b/test/model/EpicTest.java
index 4dc9a59..eaa2377 100644
--- a/test/model/EpicTest.java
+++ b/test/model/EpicTest.java
@@ -1,7 +1,7 @@
package model;
import org.junit.jupiter.api.Test;
-import util.Status;
+import util.enums.Status;
import java.time.Duration;
import java.time.LocalDateTime;
diff --git a/test/model/SubTaskTest.java b/test/model/SubTaskTest.java
index dce8d68..23b0160 100644
--- a/test/model/SubTaskTest.java
+++ b/test/model/SubTaskTest.java
@@ -2,7 +2,7 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
-import util.Status;
+import util.enums.Status;
public class SubTaskTest {
diff --git a/test/model/TaskTest.java b/test/model/TaskTest.java
index d0b0d98..5e2794a 100644
--- a/test/model/TaskTest.java
+++ b/test/model/TaskTest.java
@@ -1,8 +1,8 @@
package model;
import org.junit.jupiter.api.Test;
-import util.Status;
-import util.Type;
+import util.enums.Status;
+import util.enums.Type;
import java.time.Duration;
import java.time.LocalDateTime;
diff --git a/test/util/TaskTimeControllerTest.java b/test/util/TaskTimeControllerTest.java
index b8e7a3b..74ce7ba 100644
--- a/test/util/TaskTimeControllerTest.java
+++ b/test/util/TaskTimeControllerTest.java
@@ -6,6 +6,8 @@
import model.SubTask;
import model.Task;
import org.junit.jupiter.api.Test;
+import util.enums.Status;
+import util.enums.Type;
import java.time.Duration;
import java.time.LocalDateTime;