diff --git a/pom.xml b/pom.xml
index 03bc446d..9476f6e5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
org.springframework.boot
spring-boot-starter-parent
3.3.5
-
+
com.emobile
SpringToDo
@@ -19,18 +19,92 @@
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+
+
+ org.postgresql
+ postgresql
+ 42.7.4
+
+
+
org.springframework.boot
- spring-boot-starter
+ spring-boot-starter-cache
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.5.0
+
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
org.springframework.boot
spring-boot-starter-test
test
+
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+
+
+ org.testcontainers
+ postgresql
+ test
+
+
+
+
+ org.flywaydb
+ flyway-core
+ 11.3.2
+
+
+
+ org.flywaydb
+ flyway-database-postgresql
+ 11.3.2
+
+
+
+
diff --git a/src/main/java/com/emobile/springtodo/SpringToDoApplication.java b/src/main/java/com/emobile/springtodo/SpringToDoApplication.java
index 41980514..f9e0190e 100644
--- a/src/main/java/com/emobile/springtodo/SpringToDoApplication.java
+++ b/src/main/java/com/emobile/springtodo/SpringToDoApplication.java
@@ -3,6 +3,12 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+/**
+ * Главный класс приложения Spring Boot для ToDo.
+ * Запускает приложение.
+ *
+ * @author PavelOkhrimchuk
+ */
@SpringBootApplication
public class SpringToDoApplication {
diff --git a/src/main/java/com/emobile/springtodo/configuration/OpenApiConfig.java b/src/main/java/com/emobile/springtodo/configuration/OpenApiConfig.java
new file mode 100644
index 00000000..097fb9a0
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/configuration/OpenApiConfig.java
@@ -0,0 +1,30 @@
+package com.emobile.springtodo.configuration;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Конфигурация OpenAPI для документации API.
+ * Настроена информация о версии и описании API.
+ *
+ * @author PavelOkhrimchuk
+ */
+@Configuration
+public class OpenApiConfig {
+
+ /**
+ * Создает объект OpenAPI с информацией о приложении.
+ *
+ * @return OpenAPI объект с настроенной информацией.
+ */
+ @Bean
+ public OpenAPI customOpenAPI() {
+ return new OpenAPI()
+ .info(new Info()
+ .title("Spring ToDo API")
+ .version("1.0")
+ .description("API для управления задачами ToDo"));
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/configuration/RedisConfig.java b/src/main/java/com/emobile/springtodo/configuration/RedisConfig.java
new file mode 100644
index 00000000..6138e2b3
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/configuration/RedisConfig.java
@@ -0,0 +1,34 @@
+package com.emobile.springtodo.configuration;
+
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+
+/**
+ * Конфигурация Redis кеширования.
+ * Включает создание и настройку CacheManager для работы с Redis в приложении.
+ *
+ * @author PavelOkhrimchuk
+ */
+@Configuration
+@EnableCaching
+public class RedisConfig {
+
+ /**
+ * Создает и настраивает CacheManager для работы с Redis.
+ *
+ * @param connectionFactory Фабрика подключения к Redis.
+ * @return CacheManager, использующий Redis для кеширования.
+ */
+ @Bean
+ public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
+ RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig();
+ return RedisCacheManager.builder(connectionFactory)
+ .cacheDefaults(cacheConfig)
+ .build();
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/controller/GlobalExceptionHandler.java b/src/main/java/com/emobile/springtodo/controller/GlobalExceptionHandler.java
new file mode 100644
index 00000000..21e3d6b6
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/controller/GlobalExceptionHandler.java
@@ -0,0 +1,107 @@
+package com.emobile.springtodo.controller;
+
+import com.emobile.springtodo.dto.ErrorResponse;
+import com.emobile.springtodo.exception.InvalidToDoException;
+import com.emobile.springtodo.exception.ToDoNotFoundException;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Глобальный обработчик исключений для всего приложения.
+ * Обрабатывает различные исключения, такие как ToDoNotFoundException и InvalidToDoException,
+ * а также ошибки валидации.
+ *
+ * @author PavelOkhrimchuk
+ */
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ /**
+ * Строит ответ на основе переданного исключения, статуса HTTP и ошибки валидации.
+ *
+ * @param ex Исключение, которое было выброшено.
+ * @param status Статус HTTP-ответа.
+ * @param request HTTP-запрос, содержащий путь.
+ * @param validationErrors Ошибки валидации (если имеются).
+ * @return Ответ с ошибкой.
+ */
+ private ResponseEntity buildResponse(Exception ex, HttpStatus status, HttpServletRequest request, Map validationErrors) {
+ log.error("Exception: {} - {}", ex.getClass().getSimpleName(), ex.getMessage(), ex);
+
+ ErrorResponse response = ErrorResponse.builder()
+ .timestamp(LocalDateTime.now())
+ .status(status.value())
+ .error(status.getReasonPhrase())
+ .message(ex.getMessage())
+ .path(request.getRequestURI())
+ .build();
+
+ if (validationErrors != null && !validationErrors.isEmpty()) {
+ response.setDetails(validationErrors);
+ }
+
+ return ResponseEntity.status(status).body(response);
+ }
+
+ /**
+ * Обработчик исключения ToDoNotFoundException.
+ * @param ex Исключение, выброшенное при отсутствии задачи.
+ * @param request HTTP-запрос.
+ * @return Ответ с ошибкой 404.
+ */
+ @ExceptionHandler(ToDoNotFoundException.class)
+ public ResponseEntity handleNotFound(ToDoNotFoundException ex, HttpServletRequest request) {
+ return buildResponse(ex, HttpStatus.NOT_FOUND, request, null);
+ }
+
+ /**
+ * Обработчик исключения InvalidToDoException.
+ * @param ex Исключение, выброшенное при недействительной задаче.
+ * @param request HTTP-запрос.
+ * @return Ответ с ошибкой 400.
+ */
+ @ExceptionHandler(InvalidToDoException.class)
+ public ResponseEntity handleInvalidToDo(InvalidToDoException ex, HttpServletRequest request) {
+ return buildResponse(ex, HttpStatus.BAD_REQUEST, request, null);
+ }
+
+ /**
+ * Обработчик исключений валидации.
+ * @param ex Исключение с ошибками валидации.
+ * @param request HTTP-запрос.
+ * @return Ответ с ошибками валидации.
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex, HttpServletRequest request) {
+ Map validationErrors = new HashMap<>();
+ for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
+ validationErrors.put(fieldError.getField(), fieldError.getDefaultMessage());
+ }
+
+ log.error("Validation failed: {}", validationErrors);
+
+ return buildResponse(ex, HttpStatus.BAD_REQUEST, request, validationErrors);
+ }
+
+ /**
+ * Обработчик общих исключений.
+ * @param ex Общее исключение.
+ * @param request HTTP-запрос.
+ * @return Ответ с ошибкой 500.
+ */
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity handleGeneral(Exception ex, HttpServletRequest request) {
+ return buildResponse(ex, HttpStatus.INTERNAL_SERVER_ERROR, request, null);
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/controller/ToDoController.java b/src/main/java/com/emobile/springtodo/controller/ToDoController.java
new file mode 100644
index 00000000..3aad2d61
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/controller/ToDoController.java
@@ -0,0 +1,110 @@
+package com.emobile.springtodo.controller;
+
+import com.emobile.springtodo.dto.ToDoItemDto;
+import com.emobile.springtodo.service.ToDoService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * Контроллер для работы с задачами ToDo.
+ * Предоставляет API для получения, создания, обновления и удаления задач.
+ *
+ * @author PavelOkhrimchuk
+ */
+@RestController
+@RequestMapping("/api/todos")
+@RequiredArgsConstructor
+@Tag(name = "ToDo API", description = "API для управления задачами ToDo")
+public class ToDoController {
+
+ private final ToDoService toDoService;
+
+ /**
+ * Получить все задачи с возможностью пагинации.
+ *
+ * @param limit Количество задач для выборки.
+ * @param offset Смещение для выборки.
+ * @return Список задач.
+ */
+ @GetMapping
+ @Operation(summary = "Получить все задачи", description = "Получает список всех задач с возможностью пагинации")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Успешное получение списка задач"),
+ @ApiResponse(responseCode = "400", description = "Ошибка пагинации")
+ })
+ public List getAll(@RequestParam(defaultValue = "10") int limit,
+ @RequestParam(defaultValue = "0") int offset) {
+ return toDoService.getAll(limit, offset);
+ }
+
+ /**
+ * Получить задачу по уникальному идентификатору.
+ * @param id Идентификатор задачи.
+ * @return Задача с данным ID.
+ */
+ @GetMapping("/{id}")
+ @Operation(summary = "Получить задачу по ID", description = "Получает задачу по ее уникальному идентификатору")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Задача успешно получена"),
+ @ApiResponse(responseCode = "404", description = "Задача не найдена")
+ })
+ public ToDoItemDto getById(@PathVariable Long id) {
+ return toDoService.getById(id);
+ }
+
+ /**
+ * Создать новую задачу.
+ * @param dto Данные задачи для создания.
+ * @return Созданная задача.
+ */
+ @PostMapping
+ @Operation(summary = "Создать новую задачу", description = "Создает новую задачу")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Задача успешно создана"),
+ @ApiResponse(responseCode = "400", description = "Ошибка валидации данных")
+ })
+ public ResponseEntity create(@Valid @RequestBody ToDoItemDto dto) {
+ return ResponseEntity.ok(toDoService.create(dto));
+ }
+
+ /**
+ * Обновить задачу по ID.
+ * @param id Идентификатор задачи для обновления.
+ * @param dto Данные задачи для обновления.
+ * @return Обновленная задача.
+ */
+ @PutMapping("/{id}")
+ @Operation(summary = "Обновить задачу", description = "Обновляет существующую задачу по ее ID")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Задача успешно обновлена"),
+ @ApiResponse(responseCode = "404", description = "Задача не найдена")
+ })
+ public ResponseEntity update(@PathVariable Long id, @Valid @RequestBody ToDoItemDto dto) {
+ return ResponseEntity.ok(toDoService.update(id, dto));
+ }
+
+ /**
+ * Удалить задачу по ID.
+ * @param id Идентификатор задачи для удаления.
+ * @return Ответ без содержимого (204).
+ */
+ @DeleteMapping("/{id}")
+ @Operation(summary = "Удалить задачу", description = "Удаляет задачу по ее ID")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "204", description = "Задача успешно удалена"),
+ @ApiResponse(responseCode = "404", description = "Задача не найдена")
+ })
+ public ResponseEntity delete(@PathVariable Long id) {
+ toDoService.delete(id);
+ return ResponseEntity.noContent().build();
+ }
+
+}
diff --git a/src/main/java/com/emobile/springtodo/dto/ErrorResponse.java b/src/main/java/com/emobile/springtodo/dto/ErrorResponse.java
new file mode 100644
index 00000000..71d4e075
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/dto/ErrorResponse.java
@@ -0,0 +1,58 @@
+package com.emobile.springtodo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+/**
+ * Ответ с ошибкой, возвращаемый при возникновении исключения в приложении.
+ * Содержит информацию о времени возникновения ошибки, статусе HTTP, типе ошибки, сообщении, пути запроса и деталях ошибки.
+ *
+ * @author PavelOkhrimchuk
+ */
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@Schema(description = "Ответ с ошибкой")
+public class ErrorResponse {
+
+ /**
+ * Время возникновения ошибки.
+ */
+ @Schema(description = "Время возникновения ошибки", example = "2023-10-24T15:30:00")
+ private LocalDateTime timestamp;
+
+ /**
+ * HTTP статус код ошибки.
+ */
+ @Schema(description = "HTTP статус код ошибки", example = "400")
+ private int status;
+
+ /**
+ * Тип ошибки (например, Bad Request).
+ */
+ @Schema(description = "Тип ошибки", example = "Bad Request")
+ private String error;
+
+ /**
+ * Сообщение об ошибке.
+ */
+ @Schema(description = "Сообщение об ошибке", example = "Задача не найдена")
+ private String message;
+
+ /**
+ * Путь запроса, который вызвал ошибку.
+ */
+ @Schema(description = "Путь запроса, который вызвал ошибку", example = "/api/todos/1")
+ private String path;
+
+ /**
+ * Подробности ошибок валидации, если они есть.
+ */
+ @Schema(description = "Подробности ошибок валидации, если есть", example = "{\"field\":\"Title cannot be blank\"}")
+ private Map details;
+}
diff --git a/src/main/java/com/emobile/springtodo/dto/ToDoItemDto.java b/src/main/java/com/emobile/springtodo/dto/ToDoItemDto.java
new file mode 100644
index 00000000..f0e89026
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/dto/ToDoItemDto.java
@@ -0,0 +1,52 @@
+package com.emobile.springtodo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.*;
+
+import java.io.Serializable;
+
+/**
+ * DTO для задачи (ToDoItem).
+ * Содержит поля задачи и использует аннотации для валидации данных.
+ *
+ * @author PavelOkhrimchuk
+ */
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ToDoItemDto implements Serializable {
+
+ /**
+ * Уникальный идентификатор задачи.
+ */
+ @Schema(description = "Уникальный идентификатор задачи", example = "1")
+ private Long id;
+
+ /**
+ * Название задачи.
+ * Не может быть пустым и должно быть не более 100 символов.
+ */
+ @NotBlank(message = "Title cannot be blank")
+ @Size(max = 100, message = "Title must be at most 100 characters long")
+ @Schema(description = "Название задачи", example = "Сделать уборку", maxLength = 100)
+ private String title;
+
+ /**
+ * Описание задачи.
+ * Должно быть не более 1000 символов.
+ */
+ @Size(max = 1000, message = "Description must be at most 1000 characters long")
+ @Schema(description = "Описание задачи", example = "Убрать кухню и зал", maxLength = 1000)
+ private String description;
+
+ /**
+ * Статус выполнения задачи.
+ * true — задача выполнена, false — задача не выполнена.
+ */
+ @Schema(description = "Статус выполнения задачи", example = "false")
+ private boolean completed;
+}
diff --git a/src/main/java/com/emobile/springtodo/entity/ToDoItem.java b/src/main/java/com/emobile/springtodo/entity/ToDoItem.java
new file mode 100644
index 00000000..80fc7ee1
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/entity/ToDoItem.java
@@ -0,0 +1,47 @@
+package com.emobile.springtodo.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * Класс, представляющий задачу (ToDoItem) в системе.
+ * Содержит информацию о задаче, такую как идентификатор, название, описание, статус выполнения и дата создания.
+ *
+ * @author PavelOkhrimchuk
+ */
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+public class ToDoItem {
+
+ /**
+ * Уникальный идентификатор задачи.
+ */
+ private Long id;
+
+ /**
+ * Название задачи.
+ */
+ private String title;
+
+ /**
+ * Описание задачи.
+ */
+ private String description;
+
+ /**
+ * Статус выполнения задачи.
+ */
+ private boolean completed;
+
+ /**
+ * Дата и время создания задачи.
+ */
+ private LocalDateTime createdAt;
+
+}
diff --git a/src/main/java/com/emobile/springtodo/exception/InvalidToDoException.java b/src/main/java/com/emobile/springtodo/exception/InvalidToDoException.java
new file mode 100644
index 00000000..3a2b77ac
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/exception/InvalidToDoException.java
@@ -0,0 +1,18 @@
+package com.emobile.springtodo.exception;
+
+/**
+ * Исключение, которое выбрасывается, если задача ToDo является недействительной.
+ *
+ * @author PavelOkhrimchuk
+ */
+public class InvalidToDoException extends RuntimeException {
+
+ /**
+ * Конструктор исключения.
+ *
+ * @param message Сообщение об ошибке.
+ */
+ public InvalidToDoException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/exception/ToDoNotFoundException.java b/src/main/java/com/emobile/springtodo/exception/ToDoNotFoundException.java
new file mode 100644
index 00000000..3efc1180
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/exception/ToDoNotFoundException.java
@@ -0,0 +1,18 @@
+package com.emobile.springtodo.exception;
+
+/**
+ * Исключение, которое выбрасывается, если задача ToDo не найдена по ID.
+ *
+ * @author PavelOkhrimchuk
+ */
+public class ToDoNotFoundException extends RuntimeException {
+
+ /**
+ * Конструктор исключения с ID задачи.
+ *
+ * @param id Идентификатор задачи.
+ */
+ public ToDoNotFoundException(Long id) {
+ super("ToDo item with id " + id + " not found");
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/mapper/ToDoItemMapper.java b/src/main/java/com/emobile/springtodo/mapper/ToDoItemMapper.java
new file mode 100644
index 00000000..c11d6a07
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/mapper/ToDoItemMapper.java
@@ -0,0 +1,27 @@
+package com.emobile.springtodo.mapper;
+
+import com.emobile.springtodo.dto.ToDoItemDto;
+import com.emobile.springtodo.entity.ToDoItem;
+
+/**
+ * Интерфейс для маппинга между сущностью ToDoItem и DTO ToDoItemDto.
+ *
+ * @author PavelOkhrimchuk
+ */
+public interface ToDoItemMapper {
+
+ /**
+ * Преобразует DTO в сущность.
+ *
+ * @param dto Объект ToDoItemDto, который нужно преобразовать в сущность.
+ * @return Сущность ToDoItem.
+ */
+ ToDoItem toEntity(ToDoItemDto dto);
+
+ /**
+ * Преобразует сущность в DTO.
+ * @param entity Объект ToDoItem, который нужно преобразовать в DTO.
+ * @return DTO ToDoItemDto.
+ */
+ ToDoItemDto toDto(ToDoItem entity);
+}
diff --git a/src/main/java/com/emobile/springtodo/mapper/ToDoItemMapperImpl.java b/src/main/java/com/emobile/springtodo/mapper/ToDoItemMapperImpl.java
new file mode 100644
index 00000000..ef3846a3
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/mapper/ToDoItemMapperImpl.java
@@ -0,0 +1,48 @@
+package com.emobile.springtodo.mapper;
+
+import com.emobile.springtodo.dto.ToDoItemDto;
+import com.emobile.springtodo.entity.ToDoItem;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+/**
+ * Реализация маппера для преобразования сущности ToDoItem в DTO и наоборот.
+ *
+ * @author PavelOkhrimchuk
+ */
+@Component
+public class ToDoItemMapperImpl implements ToDoItemMapper {
+
+ /**
+ * Преобразует DTO в сущность ToDoItem.
+ *
+ * @param dto DTO для преобразования.
+ * @return Сущность ToDoItem.
+ */
+ @Override
+ public ToDoItem toEntity(ToDoItemDto dto) {
+ return new ToDoItem(
+ dto.getId(),
+ dto.getTitle(),
+ dto.getDescription(),
+ dto.isCompleted(),
+ LocalDateTime.now()
+ );
+ }
+
+ /**
+ * Преобразует сущность ToDoItem в DTO.
+ * @param entity Сущность ToDoItem для преобразования.
+ * @return DTO ToDoItemDto.
+ */
+ @Override
+ public ToDoItemDto toDto(ToDoItem entity) {
+ return ToDoItemDto.builder()
+ .id(entity.getId())
+ .title(entity.getTitle())
+ .description(entity.getDescription())
+ .completed(entity.isCompleted())
+ .build();
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java b/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java
new file mode 100644
index 00000000..3f71a9f4
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java
@@ -0,0 +1,49 @@
+package com.emobile.springtodo.repository;
+
+import com.emobile.springtodo.entity.ToDoItem;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Репозиторий для работы с задачами ToDo.
+ * Содержит методы для поиска, сохранения, обновления и удаления задач.
+ *
+ * @author PavelOkhrimchuk
+ */
+public interface ToDoRepository {
+
+ /**
+ * Получить все задачи с поддержкой пагинации.
+ *
+ * @param limit Количество задач для выборки.
+ * @param offset Смещение для выборки.
+ * @return Список задач.
+ */
+ List findAll(int limit, int offset);
+
+ /**
+ * Найти задачу по ID.
+ * @param id Идентификатор задачи.
+ * @return Опциональная задача.
+ */
+ Optional findById(Long id);
+
+ /**
+ * Сохранить новую задачу.
+ * @param toDoItem Задача, которую нужно сохранить.
+ */
+ void save(ToDoItem toDoItem);
+
+ /**
+ * Обновить задачу.
+ * @param toDoItem Задача с обновленными данными.
+ */
+ void update(ToDoItem toDoItem);
+
+ /**
+ * Удалить задачу по ID.
+ * @param id Идентификатор задачи.
+ */
+ void deleteById(Long id);
+}
diff --git a/src/main/java/com/emobile/springtodo/repository/ToDoRepositoryImpl.java b/src/main/java/com/emobile/springtodo/repository/ToDoRepositoryImpl.java
new file mode 100644
index 00000000..8970ccf1
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/repository/ToDoRepositoryImpl.java
@@ -0,0 +1,91 @@
+package com.emobile.springtodo.repository;
+
+import com.emobile.springtodo.entity.ToDoItem;
+import lombok.RequiredArgsConstructor;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Реализация репозитория для работы с задачами ToDo с использованием JdbcTemplate.
+ * Реализует методы поиска, сохранения, обновления и удаления задач в базе данных.
+ *
+ * @author PavelOkhrimchuk
+ */
+@Repository
+@RequiredArgsConstructor
+public class ToDoRepositoryImpl implements ToDoRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ /**
+ * Маппер для преобразования строки результата SQL запроса в объект ToDoItem.
+ */
+ private final RowMapper rowMapper = (rs, rowNum) -> new ToDoItem(
+ rs.getLong("id"),
+ rs.getString("title"),
+ rs.getString("description"),
+ rs.getBoolean("completed"),
+ rs.getTimestamp("created_at").toLocalDateTime()
+ );
+
+ /**
+ * Получить все задачи с поддержкой пагинации.
+ *
+ * @param limit Количество задач для выборки.
+ * @param offset Смещение для выборки.
+ * @return Список задач.
+ */
+ @Override
+ public List findAll(int limit, int offset) {
+ String sql = "SELECT * FROM todo_items ORDER BY created_at DESC LIMIT ? OFFSET ?";
+ return jdbcTemplate.query(sql, rowMapper, limit, offset);
+ }
+
+ /**
+ * Найти задачу по ID.
+ * @param id Идентификатор задачи.
+ * @return Опциональная задача.
+ */
+ @Override
+ public Optional findById(Long id) {
+ String sql = "SELECT * FROM todo_items WHERE id = ?";
+ List result = jdbcTemplate.query(sql, rowMapper, id);
+ return result.isEmpty() ? Optional.empty() : Optional.of(result.get(0));
+ }
+
+ /**
+ * Сохранить новую задачу в базе данных.
+ * @param toDoItem Задача для сохранения.
+ */
+ @Override
+ public void save(ToDoItem toDoItem) {
+ String sql = "INSERT INTO todo_items (title, description, completed, created_at) VALUES (?,?,?,?) RETURNING id";
+ Long generatedId = jdbcTemplate.queryForObject(sql, Long.class,
+ toDoItem.getTitle(), toDoItem.getDescription(), toDoItem.isCompleted(), toDoItem.getCreatedAt());
+ toDoItem.setId(generatedId);
+ }
+
+ /**
+ * Обновить существующую задачу.
+ * @param toDoItem Задача с обновленными данными.
+ */
+ @Override
+ public void update(ToDoItem toDoItem) {
+ String sql = "UPDATE todo_items SET title = ?, description = ?, completed = ? WHERE id = ?";
+ jdbcTemplate.update(sql, toDoItem.getTitle(), toDoItem.getDescription(), toDoItem.isCompleted(), toDoItem.getId());
+ }
+
+ /**
+ * Удалить задачу по ID.
+ * @param id Идентификатор задачи.
+ */
+ @Override
+ public void deleteById(Long id) {
+ String sql = "DELETE FROM todo_items WHERE id = ?";
+ jdbcTemplate.update(sql, id);
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/service/ToDoService.java b/src/main/java/com/emobile/springtodo/service/ToDoService.java
new file mode 100644
index 00000000..14c3f279
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/service/ToDoService.java
@@ -0,0 +1,51 @@
+package com.emobile.springtodo.service;
+
+import com.emobile.springtodo.dto.ToDoItemDto;
+
+import java.util.List;
+
+/**
+ * Сервис для управления задачами ToDo.
+ * Содержит методы для получения, создания, обновления и удаления задач.
+ *
+ * @author PavelOkhrimchuk
+ */
+public interface ToDoService {
+
+ /**
+ * Получить все задачи с поддержкой пагинации.
+ *
+ * @param limit Количество задач для выборки.
+ * @param offset Смещение для выборки.
+ * @return Список задач в виде DTO.
+ */
+ List getAll(int limit, int offset);
+
+ /**
+ * Получить задачу по ID.
+ * @param id Идентификатор задачи.
+ * @return DTO задачи.
+ */
+ ToDoItemDto getById(Long id);
+
+ /**
+ * Создать новую задачу.
+ * @param dto DTO задачи для создания.
+ * @return Созданная задача в виде DTO.
+ */
+ ToDoItemDto create(ToDoItemDto dto);
+
+ /**
+ * Обновить задачу по ID.
+ * @param id Идентификатор задачи для обновления.
+ * @param dto DTO с новыми данными задачи.
+ * @return Обновленная задача в виде DTO.
+ */
+ ToDoItemDto update(Long id, ToDoItemDto dto);
+
+ /**
+ * Удалить задачу по ID.
+ * @param id Идентификатор задачи для удаления.
+ */
+ void delete(Long id);
+}
diff --git a/src/main/java/com/emobile/springtodo/service/ToDoServiceImpl.java b/src/main/java/com/emobile/springtodo/service/ToDoServiceImpl.java
new file mode 100644
index 00000000..a03f6b2f
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/service/ToDoServiceImpl.java
@@ -0,0 +1,120 @@
+package com.emobile.springtodo.service;
+
+import com.emobile.springtodo.dto.ToDoItemDto;
+import com.emobile.springtodo.entity.ToDoItem;
+import com.emobile.springtodo.exception.ToDoNotFoundException;
+import com.emobile.springtodo.mapper.ToDoItemMapper;
+import com.emobile.springtodo.repository.ToDoRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Реализация сервиса для управления задачами ToDo.
+ * Включает методы для получения задач, создания, обновления и удаления задач с логированием.
+ *
+ * @author PavelOkhrimchuk
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ToDoServiceImpl implements ToDoService {
+
+ private final ToDoRepository toDoRepository;
+ private final ToDoItemMapper toDoItemMapper;
+
+ /**
+ * Получить все задачи с пагинацией.
+ *
+ * @param limit Количество задач для выборки.
+ * @param offset Смещение для выборки.
+ * @return Список задач в виде DTO.
+ */
+ @Override
+ public List getAll(int limit, int offset) {
+ log.info("Fetching ToDo items with limit={} and offset={}", limit, offset);
+ return toDoRepository.findAll(limit, offset)
+ .stream()
+ .map(toDoItemMapper::toDto)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Получить задачу по ID с кешированием.
+ * @param id Идентификатор задачи.
+ * @return DTO задачи.
+ */
+ @Cacheable(value = "todos", key = "#id")
+ @Override
+ public ToDoItemDto getById(Long id) {
+ log.info("Fetching ToDo item with id={}", id);
+ ToDoItem item = toDoRepository.findById(id)
+ .orElseThrow(() -> {
+ log.warn("ToDo item with id={} not found", id);
+ return new ToDoNotFoundException(id);
+ });
+ return toDoItemMapper.toDto(item);
+ }
+
+ /**
+ * Создание новой задачи.
+ * @param dto DTO задачи для создания.
+ * @return Созданная задача в виде DTO.
+ */
+ @Transactional
+ @Override
+ public ToDoItemDto create(ToDoItemDto dto) {
+ log.info("Creating new ToDo item: {}", dto);
+ ToDoItem entity = toDoItemMapper.toEntity(dto);
+ toDoRepository.save(entity);
+ return toDoItemMapper.toDto(entity);
+ }
+
+ /**
+ * Обновление задачи по ID с кешированием.
+ * @param id Идентификатор задачи для обновления.
+ * @param dto DTO с новыми данными задачи.
+ * @return Обновленная задача в виде DTO.
+ */
+ @Transactional
+ @CachePut(value = "todos", key = "#id")
+ @Override
+ public ToDoItemDto update(Long id, ToDoItemDto dto) {
+ log.info("Updating ToDo item with id={}", id);
+ ToDoItem item = toDoRepository.findById(id)
+ .orElseThrow(() -> {
+ log.warn("Cannot update - ToDo item with id={} not found", id);
+ return new ToDoNotFoundException(id);
+ });
+
+ item.setTitle(dto.getTitle());
+ item.setDescription(dto.getDescription());
+ item.setCompleted(dto.isCompleted());
+
+ toDoRepository.update(item);
+ return toDoItemMapper.toDto(item);
+ }
+
+ /**
+ * Удаление задачи по ID с кешированием.
+ * @param id Идентификатор задачи для удаления.
+ */
+ @Transactional
+ @CacheEvict(value = "todos", key = "#id")
+ @Override
+ public void delete(Long id) {
+ log.info("Deleting ToDo item with id={}", id);
+ if (toDoRepository.findById(id).isEmpty()) {
+ log.warn("Cannot delete - ToDo item with id={} not found", id);
+ throw new ToDoNotFoundException(id);
+ }
+ toDoRepository.deleteById(id);
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 5c3e5461..ffc59196 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1 +1,13 @@
spring.application.name=SpringToDo
+spring.datasource.url=jdbc:postgresql://localhost:5432/todo_db
+spring.datasource.username=todo_user
+spring.datasource.password=todo_pass
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.flyway.enabled=true
+spring.cache.type=redis
+spring.data.redis.host=localhost
+spring.data.redis.port=6379
+spring.data.redis.timeout=6000
+spring.data.redis.client-type=lettuce
+spring.data.redis.password=
+spring.data.redis.database=0
diff --git a/src/main/resources/db/migration/V1__Create_initial_schema.sql b/src/main/resources/db/migration/V1__Create_initial_schema.sql
new file mode 100644
index 00000000..2c797986
--- /dev/null
+++ b/src/main/resources/db/migration/V1__Create_initial_schema.sql
@@ -0,0 +1,8 @@
+CREATE TABLE todo_items
+(
+ id SERIAL PRIMARY KEY,
+ title VARCHAR(255) NOT NULL,
+ description TEXT,
+ completed BOOLEAN DEFAULT FALSE,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
diff --git a/src/test/java/com/emobile/springtodo/controller/BaseIntegrationTest.java b/src/test/java/com/emobile/springtodo/controller/BaseIntegrationTest.java
new file mode 100644
index 00000000..af665ba9
--- /dev/null
+++ b/src/test/java/com/emobile/springtodo/controller/BaseIntegrationTest.java
@@ -0,0 +1,50 @@
+package com.emobile.springtodo.controller;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.PostgreSQLContainer;
+
+
+/**
+ * Базовый класс для интеграционных тестов.
+ * Настроены контейнеры для PostgreSQL и Redis.
+ *
+ * @author PavelOkhrimchuk
+ */
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+public abstract class BaseIntegrationTest {
+
+ protected static PostgreSQLContainer> postgresContainer = new PostgreSQLContainer<>("postgres:latest")
+ .withDatabaseName("test_db")
+ .withUsername("test")
+ .withPassword("test");
+
+ protected static GenericContainer> redisContainer = new GenericContainer<>("redis:latest")
+ .withExposedPorts(6379);
+
+ @LocalServerPort
+ protected int port;
+
+
+ @BeforeAll
+ public static void setup() {
+ postgresContainer.start();
+ redisContainer.start();
+
+ System.setProperty("spring.datasource.url", postgresContainer.getJdbcUrl());
+ System.setProperty("spring.datasource.username", postgresContainer.getUsername());
+ System.setProperty("spring.datasource.password", postgresContainer.getPassword());
+
+ System.setProperty("spring.data.redis.host", redisContainer.getHost());
+ System.setProperty("spring.data.redis.port", redisContainer.getMappedPort(6379).toString());
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ postgresContainer.stop();
+ redisContainer.stop();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/emobile/springtodo/controller/ToDoControllerIntegrationTest.java b/src/test/java/com/emobile/springtodo/controller/ToDoControllerIntegrationTest.java
new file mode 100644
index 00000000..cba123fe
--- /dev/null
+++ b/src/test/java/com/emobile/springtodo/controller/ToDoControllerIntegrationTest.java
@@ -0,0 +1,72 @@
+package com.emobile.springtodo.controller;
+
+
+import com.emobile.springtodo.dto.ToDoItemDto;
+import com.emobile.springtodo.service.ToDoServiceImpl;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Интеграционные тесты для контроллера ToDo.
+ * Проверяются основные операции с задачами (создание, получение).
+ *
+ * @author PavelOkhrimchuk
+ */
+@ExtendWith(SpringExtension.class)
+@Import(ToDoServiceImpl.class)
+class ToDoControllerIntegrationTest extends BaseIntegrationTest {
+
+ @Autowired
+ private TestRestTemplate restTemplate;
+
+ @Test
+ void testCreateToDo() {
+ ToDoItemDto dto = new ToDoItemDto(1L, "New Task", "Description of the task", false);
+
+
+ ToDoItemDto response = restTemplate.postForObject("http://localhost:" + port + "/api/todos", dto, ToDoItemDto.class);
+
+
+ assertThat(response).isNotNull();
+ assertThat(response.getTitle()).isEqualTo(dto.getTitle());
+ assertThat(response.getDescription()).isEqualTo(dto.getDescription());
+ assertThat(response.isCompleted()).isEqualTo(dto.isCompleted());
+ }
+
+ @Test
+ void testGetAllToDos() {
+
+ ToDoItemDto dto = new ToDoItemDto(1L, "New Task", "Description of the task", false);
+ restTemplate.postForObject("http://localhost:" + port + "/api/todos", dto, ToDoItemDto.class);
+
+
+ ToDoItemDto[] response = restTemplate.getForObject("http://localhost:" + port + "/api/todos", ToDoItemDto[].class);
+
+
+ assertThat(response).isNotEmpty();
+ }
+
+ @Test
+ void testGetByIdToDo() {
+
+ ToDoItemDto dto = new ToDoItemDto(1L, "New Task", "Description of the task", false);
+ ToDoItemDto createdDto = restTemplate.postForObject("http://localhost:" + port + "/api/todos", dto, ToDoItemDto.class);
+
+
+ ToDoItemDto response = restTemplate.getForObject("http://localhost:" + port + "/api/todos/" + createdDto.getId(), ToDoItemDto.class);
+
+
+ assertThat(response).isNotNull();
+ assertThat(response.getTitle()).isEqualTo(dto.getTitle());
+ assertThat(response.getDescription()).isEqualTo(dto.getDescription());
+ assertThat(response.isCompleted()).isEqualTo(dto.isCompleted());
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/emobile/springtodo/repository/ToDoRepositoryTest.java b/src/test/java/com/emobile/springtodo/repository/ToDoRepositoryTest.java
new file mode 100644
index 00000000..266273eb
--- /dev/null
+++ b/src/test/java/com/emobile/springtodo/repository/ToDoRepositoryTest.java
@@ -0,0 +1,66 @@
+package com.emobile.springtodo.repository;
+
+import com.emobile.springtodo.entity.ToDoItem;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.time.LocalDateTime;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+/**
+ * Тесты для репозитория ToDo.
+ * Включает тестирование операций сохранения, обновления и удаления задач.
+ *
+ * @author PavelOkhrimchuk
+ */
+@ExtendWith(MockitoExtension.class)
+class ToDoRepositoryTest {
+
+ @Mock
+ private JdbcTemplate jdbcTemplate;
+
+ @InjectMocks
+ private ToDoRepositoryImpl toDoRepository;
+
+
+ @Test
+ void testSave() {
+ ToDoItem toDoItem = new ToDoItem(null, "Title", "Description", false, LocalDateTime.now());
+ String sql = "INSERT INTO todo_items (title, description, completed, created_at) VALUES (?,?,?,?) RETURNING id";
+ Long generatedId = 1L;
+
+ when(jdbcTemplate.queryForObject(eq(sql), eq(Long.class), any(), any(), any(), any()))
+ .thenReturn(generatedId);
+
+ toDoRepository.save(toDoItem);
+
+ assertEquals(generatedId, toDoItem.getId());
+ verify(jdbcTemplate).queryForObject(eq(sql), eq(Long.class), any(), any(), any(), any());
+ }
+
+ @Test
+ void testUpdate() {
+ ToDoItem toDoItem = new ToDoItem(1L, "Updated Title", "Updated Description", true, LocalDateTime.now());
+ String sql = "UPDATE todo_items SET title = ?, description = ?, completed = ? WHERE id = ?";
+
+ toDoRepository.update(toDoItem);
+
+ verify(jdbcTemplate).update(eq(sql), any(), any(), any(), eq(toDoItem.getId()));
+ }
+
+ @Test
+ void testDeleteById() {
+ Long id = 1L;
+ String sql = "DELETE FROM todo_items WHERE id = ?";
+
+ toDoRepository.deleteById(id);
+
+ verify(jdbcTemplate).update(eq(sql), eq(id));
+ }
+}
diff --git a/src/test/java/com/emobile/springtodo/service/ToDoServiceTest.java b/src/test/java/com/emobile/springtodo/service/ToDoServiceTest.java
new file mode 100644
index 00000000..3538a8cc
--- /dev/null
+++ b/src/test/java/com/emobile/springtodo/service/ToDoServiceTest.java
@@ -0,0 +1,133 @@
+package com.emobile.springtodo.service;
+
+
+import com.emobile.springtodo.dto.ToDoItemDto;
+import com.emobile.springtodo.entity.ToDoItem;
+import com.emobile.springtodo.exception.ToDoNotFoundException;
+import com.emobile.springtodo.mapper.ToDoItemMapper;
+import com.emobile.springtodo.repository.ToDoRepository;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.*;
+
+/**
+ * Тесты для сервиса ToDo.
+ * Включает тестирование всех CRUD операций (получение, создание, обновление, удаление).
+ *
+ * @author PavelOkhrimchuk
+ */
+@ExtendWith(MockitoExtension.class)
+class ToDoServiceTest {
+
+ @Mock
+ private ToDoRepository toDoRepository;
+
+ @Mock
+ private ToDoItemMapper toDoItemMapper;
+
+ @InjectMocks
+ private ToDoServiceImpl toDoService;
+
+ @Test
+ void testGetAll() {
+ ToDoItem item1 = new ToDoItem(1L, "Task 1", "Description 1", false, LocalDateTime.now());
+ ToDoItem item2 = new ToDoItem(2L, "Task 2", "Description 2", true, LocalDateTime.now());
+ ToDoItemDto dto1 = new ToDoItemDto(1L, "Task 1", "Description 1", false);
+ ToDoItemDto dto2 = new ToDoItemDto(1L, "Task 2", "Description 2", true);
+
+ when(toDoRepository.findAll(10, 0)).thenReturn(List.of(item1, item2));
+ when(toDoItemMapper.toDto(item1)).thenReturn(dto1);
+ when(toDoItemMapper.toDto(item2)).thenReturn(dto2);
+
+ List result = toDoService.getAll(10, 0);
+
+ assertEquals(2, result.size());
+ assertEquals(dto1, result.get(0));
+ assertEquals(dto2, result.get(1));
+
+ verify(toDoRepository).findAll(10, 0);
+ verify(toDoItemMapper, times(2)).toDto(any(ToDoItem.class));
+ }
+
+ @Test
+ void testGetById_Success() {
+ Long id = 1L;
+ ToDoItem item = new ToDoItem(id, "Task", "Description", false, LocalDateTime.now());
+ ToDoItemDto dto = new ToDoItemDto(1L, "Task", "Description", false);
+
+ when(toDoRepository.findById(id)).thenReturn(Optional.of(item));
+ when(toDoItemMapper.toDto(item)).thenReturn(dto);
+
+ ToDoItemDto result = toDoService.getById(id);
+
+ assertEquals(dto, result);
+ verify(toDoRepository).findById(id);
+ verify(toDoItemMapper).toDto(item);
+ }
+
+ @Test
+ void testGetById_NotFound() {
+ Long id = 1L;
+
+ when(toDoRepository.findById(id)).thenReturn(Optional.empty());
+
+ assertThrows(ToDoNotFoundException.class, () -> toDoService.getById(id));
+ verify(toDoRepository).findById(id);
+ }
+
+ @Test
+ void testCreate() {
+ ToDoItemDto dto = new ToDoItemDto(1L, "New Task", "New Description", false);
+ ToDoItem entity = new ToDoItem(null, "New Task", "New Description", false, LocalDateTime.now());
+ ToDoItemDto savedDto = new ToDoItemDto(1L, "New Task", "New Description", false);
+
+ when(toDoItemMapper.toEntity(dto)).thenReturn(entity);
+ when(toDoItemMapper.toDto(any(ToDoItem.class))).thenReturn(savedDto);
+
+ doAnswer(invocation -> {
+ entity.setId(1L);
+ return null;
+ }).when(toDoRepository).save(entity);
+
+ ToDoItemDto result = toDoService.create(dto);
+
+ assertEquals(savedDto, result);
+ verify(toDoRepository).save(entity);
+ verify(toDoItemMapper).toEntity(dto);
+ verify(toDoItemMapper).toDto(any(ToDoItem.class));
+ }
+
+
+ @Test
+ void testDelete_Success() {
+ Long id = 1L;
+ ToDoItem item = new ToDoItem(id, "Task", "Description", false, LocalDateTime.now());
+
+ when(toDoRepository.findById(id)).thenReturn(Optional.of(item));
+
+ toDoService.delete(id);
+
+ verify(toDoRepository).findById(id);
+ verify(toDoRepository).deleteById(id);
+ }
+
+ @Test
+ void testDelete_NotFound() {
+ Long id = 1L;
+
+ when(toDoRepository.findById(id)).thenReturn(Optional.empty());
+
+ assertThrows(ToDoNotFoundException.class, () -> toDoService.delete(id));
+ verify(toDoRepository).findById(id);
+ }
+}