From 98a50cf9162fcaf435c62ed61805990f336178e0 Mon Sep 17 00:00:00 2001 From: Sergey Kovalev Date: Sun, 15 Jun 2025 18:15:59 +0300 Subject: [PATCH 01/10] repository layer. --- docker-compose.yaml | 7 ++ pom.xml | 111 ++++++++++++++++-- .../com/emobile/springtodo/entity/ToDo.java | 37 ++++++ .../exception/ToDoNotFoundException.java | 11 ++ .../springtodo/mapper/JDBCToDoMapper.java | 24 ++++ .../springtodo/repository/ToDoRepository.java | 15 +++ .../repository/impl/ToDoRepositoryImpl.java | 90 ++++++++++++++ src/main/resources/application.properties | 1 - src/main/resources/application.yaml | 29 +++++ .../db/changelog/01.000.00/db.changelog.yaml | 11 ++ .../db/changelog/01.000.00/todos.sql | 10 ++ .../db/changelog/db.changelog-master.yaml | 4 + 12 files changed, 339 insertions(+), 11 deletions(-) create mode 100644 docker-compose.yaml create mode 100644 src/main/java/com/emobile/springtodo/entity/ToDo.java create mode 100644 src/main/java/com/emobile/springtodo/exception/ToDoNotFoundException.java create mode 100644 src/main/java/com/emobile/springtodo/mapper/JDBCToDoMapper.java create mode 100644 src/main/java/com/emobile/springtodo/repository/ToDoRepository.java create mode 100644 src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yaml create mode 100644 src/main/resources/db/changelog/01.000.00/db.changelog.yaml create mode 100644 src/main/resources/db/changelog/01.000.00/todos.sql create mode 100644 src/main/resources/db/changelog/db.changelog-master.yaml diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..601fc6a6 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,7 @@ +services: + redis: + container_name: redis + image: redis:latest + restart: always + ports: + - "6379:6379" \ No newline at end of file diff --git a/pom.xml b/pom.xml index 03bc446d..b6918eac 100644 --- a/pom.xml +++ b/pom.xml @@ -5,23 +5,79 @@ org.springframework.boot spring-boot-starter-parent - 3.3.5 + 3.5.0 - com.emobile - SpringToDo + com.example + demo 0.0.1-SNAPSHOT - SpringToDo - SpringToDo - + demo + Demo project for Spring Boot + + + + + + + + + + + + + - 21 + 17 + 1.6.3 - org.springframework.boot - spring-boot-starter + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.liquibase + liquibase-core + + + org.mapstruct + mapstruct + ${mapstructVersion} + + + org.mapstruct + mapstruct-processor + ${mapstructVersion} + + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-data-jdbc @@ -29,15 +85,50 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + org.springframework.boot spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + - + \ No newline at end of file diff --git a/src/main/java/com/emobile/springtodo/entity/ToDo.java b/src/main/java/com/emobile/springtodo/entity/ToDo.java new file mode 100644 index 00000000..243a5eef --- /dev/null +++ b/src/main/java/com/emobile/springtodo/entity/ToDo.java @@ -0,0 +1,37 @@ +package com.emobile.springtodo.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "todos") +public class ToDo { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column(name = "id") + private UUID id; + + @Column(name = "description", nullable = false) + private String description; + + @Column(name = "expiration_date") + private LocalDateTime expirationDate; + + @Column(name = "is_done") + private boolean isDone; +} 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..080d51d4 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/exception/ToDoNotFoundException.java @@ -0,0 +1,11 @@ +package com.emobile.springtodo.exception; + +import java.util.UUID; + +public class ToDoNotFoundException extends RuntimeException { + private static final String DEFAULT_MESSAGE = "ToDo with id %s not found for update"; + + public ToDoNotFoundException(UUID id) { + super(String.format(DEFAULT_MESSAGE, id.toString())); + } +} diff --git a/src/main/java/com/emobile/springtodo/mapper/JDBCToDoMapper.java b/src/main/java/com/emobile/springtodo/mapper/JDBCToDoMapper.java new file mode 100644 index 00000000..0eafd1ee --- /dev/null +++ b/src/main/java/com/emobile/springtodo/mapper/JDBCToDoMapper.java @@ -0,0 +1,24 @@ +package com.emobile.springtodo.mapper; + +import com.emobile.springtodo.entity.ToDo; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.util.UUID; + +@Component +public class JDBCToDoMapper implements RowMapper { + + @Override + public ToDo mapRow(ResultSet rs, int rowNum) throws SQLException { + return ToDo.builder() + .id(UUID.fromString(rs.getString("id"))) + .description(rs.getString("description")) + .expirationDate(rs.getObject("expiration_date", LocalDateTime.class)) + .isDone(rs.getBoolean("is_done")) + .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..d8880d3d --- /dev/null +++ b/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java @@ -0,0 +1,15 @@ +package com.emobile.springtodo.repository; + +import com.emobile.springtodo.entity.ToDo; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface ToDoRepository { + Optional findById(UUID id); + List findAll(); + ToDo save(ToDo todo); + ToDo update(ToDo todo, UUID id); + void delete(ToDo todo); +} diff --git a/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java b/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java new file mode 100644 index 00000000..56b7eb49 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java @@ -0,0 +1,90 @@ +package com.emobile.springtodo.repository.impl; + +import com.emobile.springtodo.entity.ToDo; +import com.emobile.springtodo.exception.ToDoNotFoundException; +import com.emobile.springtodo.mapper.JDBCToDoMapper; +import com.emobile.springtodo.repository.ToDoRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Repository +@RequiredArgsConstructor +public class ToDoRepositoryImpl implements ToDoRepository { + + private final JdbcTemplate jdbcTemplate; + private final JDBCToDoMapper mapper; + + + @Override + public Optional findById(UUID id) { + String query = "SELECT * FROM todos WHERE id = ?"; + + try { + ToDo toDo = jdbcTemplate.queryForObject(query, mapper, id.toString()); + return Optional.ofNullable(toDo); + } catch (DataAccessException e) { + return Optional.empty(); + } + } + + @Override + public List findAll() { + String query = "SELECT * FROM todos"; + + try { + return jdbcTemplate.query(query, mapper); + } catch (DataAccessException e) { + return new ArrayList<>(); + } + } + + @Override + public ToDo save(ToDo todo) { + if (todo.getId() == null) { + todo.setId(UUID.randomUUID()); + } + + String query = "INSERT INTO todos (id, description, expiration_date, is_done) VALUES (?, ?, ?, ?)"; + + jdbcTemplate.update(query, + todo.getId().toString(), + todo.getDescription(), + todo.getExpirationDate(), + todo.isDone()); + return todo; + } + + @Override + public ToDo update(ToDo todo, UUID id) { + String sql = "UPDATE todos SET description = ?, expiration_date = ?, is_done = ? WHERE id = ?"; + + int rowsChanged = jdbcTemplate.update(sql, + todo.getDescription(), + todo.getExpirationDate(), + todo.isDone(), + id.toString()); + + if (rowsChanged == 0) { + throw new ToDoNotFoundException(id); + } + + return todo; } + + @Override + public void delete(ToDo todo) { + String sql = "DELETE FROM todos WHERE id = ?"; + + int rowsAffected = jdbcTemplate.update(sql, todo.getId().toString()); + + if (rowsAffected == 0) { + throw new RuntimeException("ToDo with id " + todo.getId() + " not found for deletion"); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 5c3e5461..00000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=SpringToDo diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 00000000..fbf7c5e4 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,29 @@ +server: + port: 9009 + +spring: + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://localhost:5432/todos_db +# url: jdbc:postgresql://PostgreSQL:5432/socks_db + username: postgres + password: postgres + hikari: + schema: public + + jpa: + database: postgresql + database-platform: org.hibernate.dialect.PostgreSQLDialect + generate-ddl: false + show-sql: true + hibernate: + ddl-auto: none + + liquibase: + enabled: true + change-log: classpath:/db/changelog/db.changelog-master.yaml + + data: + redis: + host: localhost + port: 6379 \ No newline at end of file diff --git a/src/main/resources/db/changelog/01.000.00/db.changelog.yaml b/src/main/resources/db/changelog/01.000.00/db.changelog.yaml new file mode 100644 index 00000000..d3e9821b --- /dev/null +++ b/src/main/resources/db/changelog/01.000.00/db.changelog.yaml @@ -0,0 +1,11 @@ +databaseChangeLog: + - include: + file: todos.sql + relativeToChangelogFile: true + - changeSet: + id: 01.000.00 + author: Siarhei_Kavaleu + logicalFilePath: 01.000.00/db.changelog.yaml + changes: + - tagDatabase: + tag: 01.000.00 \ No newline at end of file diff --git a/src/main/resources/db/changelog/01.000.00/todos.sql b/src/main/resources/db/changelog/01.000.00/todos.sql new file mode 100644 index 00000000..b9d1d4cc --- /dev/null +++ b/src/main/resources/db/changelog/01.000.00/todos.sql @@ -0,0 +1,10 @@ +--liquibase formatted sql +--changeset Siarhei_Kavaleu:db localFilePath:01.000.00/todos.sql +CREATE TABLE todos +( + id UUID NOT NULL, + description VARCHAR(255) NOT NULL, + expiration_date DATE NOT NULL, + is_done BOOLEAN NOT NULL, + CONSTRAINT pk_todos PRIMARY KEY (id) +); \ No newline at end of file diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml new file mode 100644 index 00000000..d8edc901 --- /dev/null +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -0,0 +1,4 @@ +databaseChangeLog: + - include: + file: 01.000.00/db.changelog.yaml + relativeToChangelogFile: true \ No newline at end of file From 1e8341eb34ce4770e5a2719f42c632cd1cd95c7d Mon Sep 17 00:00:00 2001 From: Sergey Kovalev Date: Sun, 15 Jun 2025 18:28:30 +0300 Subject: [PATCH 02/10] repository layer little changes. --- .../com/emobile/springtodo/repository/ToDoRepository.java | 2 +- .../springtodo/repository/impl/ToDoRepositoryImpl.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java b/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java index d8880d3d..2a7c237c 100644 --- a/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java +++ b/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java @@ -8,7 +8,7 @@ public interface ToDoRepository { Optional findById(UUID id); - List findAll(); + List findAll(int limit, int offset); ToDo save(ToDo todo); ToDo update(ToDo todo, UUID id); void delete(ToDo todo); diff --git a/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java b/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java index 56b7eb49..f45b0c6a 100644 --- a/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java +++ b/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java @@ -35,11 +35,11 @@ public Optional findById(UUID id) { } @Override - public List findAll() { - String query = "SELECT * FROM todos"; + public List findAll(int limit, int offset) { + String query = "SELECT * FROM todos ORDER BY id LIMIT ? OFFSET ?"; try { - return jdbcTemplate.query(query, mapper); + return jdbcTemplate.query(query, mapper, limit, offset); } catch (DataAccessException e) { return new ArrayList<>(); } @@ -84,7 +84,7 @@ public void delete(ToDo todo) { int rowsAffected = jdbcTemplate.update(sql, todo.getId().toString()); if (rowsAffected == 0) { - throw new RuntimeException("ToDo with id " + todo.getId() + " not found for deletion"); + throw new ToDoNotFoundException(todo.getId()); } } } From 3efdd31d33716bd8b08aab773a90edcee84ff815 Mon Sep 17 00:00:00 2001 From: Sergey Kovalev Date: Sun, 15 Jun 2025 19:18:50 +0300 Subject: [PATCH 03/10] service layer --- pom.xml | 4 ++ .../dto/request/ToDoRequestDto.java | 20 +++++++ .../dto/response/ToDoResponseDto.java | 11 ++++ .../emobile/springtodo/mapper/ToDoMapper.java | 13 +++++ .../springtodo/repository/ToDoRepository.java | 2 +- .../repository/impl/ToDoRepositoryImpl.java | 10 ++-- .../springtodo/service/ToDoService.java | 15 +++++ .../service/impl/ToDoServiceImpl.java | 57 +++++++++++++++++++ 8 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/emobile/springtodo/dto/request/ToDoRequestDto.java create mode 100644 src/main/java/com/emobile/springtodo/dto/response/ToDoResponseDto.java create mode 100644 src/main/java/com/emobile/springtodo/mapper/ToDoMapper.java create mode 100644 src/main/java/com/emobile/springtodo/service/ToDoService.java create mode 100644 src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java diff --git a/pom.xml b/pom.xml index b6918eac..97bc67a5 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-validation + org.springframework.boot spring-boot-starter-actuator diff --git a/src/main/java/com/emobile/springtodo/dto/request/ToDoRequestDto.java b/src/main/java/com/emobile/springtodo/dto/request/ToDoRequestDto.java new file mode 100644 index 00000000..62f180da --- /dev/null +++ b/src/main/java/com/emobile/springtodo/dto/request/ToDoRequestDto.java @@ -0,0 +1,20 @@ +package com.emobile.springtodo.dto.request; + +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.hibernate.validator.constraints.Length; + +import java.time.LocalDateTime; + +public record ToDoRequestDto( + @Length(min = 1, max = 255, message = "Description must contain from 1 to 255 characters") + @NotBlank(message = "Description can't be blank") + String description, + + @NotNull(message = "Expiration date can't be null") + @FutureOrPresent(message = "Expiration date must be in future") + LocalDateTime expirationDate, + + boolean isDone) { +} diff --git a/src/main/java/com/emobile/springtodo/dto/response/ToDoResponseDto.java b/src/main/java/com/emobile/springtodo/dto/response/ToDoResponseDto.java new file mode 100644 index 00000000..dc0d8c89 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/dto/response/ToDoResponseDto.java @@ -0,0 +1,11 @@ +package com.emobile.springtodo.dto.response; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record ToDoResponseDto( + UUID id, + String description, + LocalDateTime expirationDate, + boolean isDone) { +} diff --git a/src/main/java/com/emobile/springtodo/mapper/ToDoMapper.java b/src/main/java/com/emobile/springtodo/mapper/ToDoMapper.java new file mode 100644 index 00000000..859bb1c3 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/mapper/ToDoMapper.java @@ -0,0 +1,13 @@ +package com.emobile.springtodo.mapper; + +import com.emobile.springtodo.dto.request.ToDoRequestDto; +import com.emobile.springtodo.dto.response.ToDoResponseDto; +import com.emobile.springtodo.entity.ToDo; +import org.mapstruct.Mapper; + +@Mapper +public interface ToDoMapper { + ToDo fromRequestToEntity(ToDoRequestDto toDoRequestDto); + + ToDoResponseDto fromEntityToResponseDto(ToDo toDo); +} diff --git a/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java b/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java index 2a7c237c..c9a15200 100644 --- a/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java +++ b/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java @@ -11,5 +11,5 @@ public interface ToDoRepository { List findAll(int limit, int offset); ToDo save(ToDo todo); ToDo update(ToDo todo, UUID id); - void delete(ToDo todo); + void delete(UUID id); } diff --git a/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java b/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java index f45b0c6a..7e2d88d2 100644 --- a/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java +++ b/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java @@ -70,7 +70,6 @@ public ToDo update(ToDo todo, UUID id) { todo.getExpirationDate(), todo.isDone(), id.toString()); - if (rowsChanged == 0) { throw new ToDoNotFoundException(id); } @@ -78,13 +77,12 @@ public ToDo update(ToDo todo, UUID id) { return todo; } @Override - public void delete(ToDo todo) { + public void delete(UUID id) { String sql = "DELETE FROM todos WHERE id = ?"; - int rowsAffected = jdbcTemplate.update(sql, todo.getId().toString()); - - if (rowsAffected == 0) { - throw new ToDoNotFoundException(todo.getId()); + int rowsChanged = jdbcTemplate.update(sql, id.toString()); + if (rowsChanged == 0) { + throw new ToDoNotFoundException(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..13f4987b --- /dev/null +++ b/src/main/java/com/emobile/springtodo/service/ToDoService.java @@ -0,0 +1,15 @@ +package com.emobile.springtodo.service; + +import com.emobile.springtodo.dto.request.ToDoRequestDto; +import com.emobile.springtodo.dto.response.ToDoResponseDto; + +import java.util.List; +import java.util.UUID; + +public interface ToDoService { + ToDoResponseDto findById(UUID id); + List findAll(int limit, int offset); + ToDoResponseDto save(ToDoRequestDto toDoRequestDto); + ToDoResponseDto update(ToDoRequestDto toDoRequestDto, UUID id); + String delete(UUID id); +} diff --git a/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java b/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java new file mode 100644 index 00000000..25564cfc --- /dev/null +++ b/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java @@ -0,0 +1,57 @@ +package com.emobile.springtodo.service.impl; + +import com.emobile.springtodo.dto.request.ToDoRequestDto; +import com.emobile.springtodo.dto.response.ToDoResponseDto; +import com.emobile.springtodo.entity.ToDo; +import com.emobile.springtodo.exception.ToDoNotFoundException; +import com.emobile.springtodo.mapper.ToDoMapper; +import com.emobile.springtodo.repository.ToDoRepository; +import com.emobile.springtodo.service.ToDoService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class ToDoServiceImpl implements ToDoService { + private final ToDoRepository toDoRepository; + private final ToDoMapper toDoMapper; + + + @Override + public ToDoResponseDto findById(UUID id) { + return toDoRepository.findById(id) + .map(toDoMapper::fromEntityToResponseDto) + .orElseThrow(() -> new ToDoNotFoundException(id)); + } + + @Override + public List findAll(int limit, int offset) { + return toDoRepository.findAll(limit, offset) + .stream() + .map(toDoMapper::fromEntityToResponseDto) + .toList(); + } + + @Override + public ToDoResponseDto save(ToDoRequestDto toDoRequestDto) { + ToDo forSave = toDoMapper.fromRequestToEntity(toDoRequestDto); + ToDo saved = toDoRepository.save(forSave); + return toDoMapper.fromEntityToResponseDto(saved); + } + + @Override + public ToDoResponseDto update(ToDoRequestDto toDoRequestDto, UUID id) { + ToDo forUpdate = toDoMapper.fromRequestToEntity(toDoRequestDto); + ToDo updated = toDoRepository.update(forUpdate, id); + return toDoMapper.fromEntityToResponseDto(updated); + } + + @Override + public String delete(UUID id) { + toDoRepository.delete(id); + return String.format("ToDo with id: %s was deleted", id); + } +} From 6b1357b597c8f5c997297d9a24eac395cf6fd828 Mon Sep 17 00:00:00 2001 From: Sergey Kovalev Date: Sun, 15 Jun 2025 21:02:52 +0300 Subject: [PATCH 04/10] controller layer + exc.handler. --- .../springtodo/controller/ToDoController.java | 55 +++++++++++++++++++ .../handler/ControllersExceptionHandler.java | 44 +++++++++++++++ .../handler/CustomErrorResponse.java | 25 +++++++++ .../springtodo/repository/ToDoRepository.java | 2 +- .../repository/impl/ToDoRepositoryImpl.java | 6 +- .../springtodo/service/ToDoService.java | 9 ++- .../service/impl/ToDoServiceImpl.java | 18 +++--- 7 files changed, 142 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/emobile/springtodo/controller/ToDoController.java create mode 100644 src/main/java/com/emobile/springtodo/exception/handler/ControllersExceptionHandler.java create mode 100644 src/main/java/com/emobile/springtodo/exception/handler/CustomErrorResponse.java 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..4e4cbda0 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/controller/ToDoController.java @@ -0,0 +1,55 @@ +package com.emobile.springtodo.controller; + +import com.emobile.springtodo.dto.request.ToDoRequestDto; +import com.emobile.springtodo.dto.response.ToDoResponseDto; +import com.emobile.springtodo.service.ToDoService; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import org.hibernate.validator.constraints.UUID; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@Validated +@RequestMapping("/todo") +@RequiredArgsConstructor +public class ToDoController { + private final ToDoService toDoService; + + @GetMapping("/{id}") + public ToDoResponseDto findById(@UUID @PathVariable String id) { + return toDoService.findById(id); + } + + @GetMapping("/all") + public List findAll(@RequestParam @Positive int pageSize, + @RequestParam @Positive int pageNumber) { + return toDoService.findAll(pageSize, pageNumber); + } + + @PostMapping + public ToDoResponseDto save(@RequestBody ToDoRequestDto requestDto) { + return toDoService.save(requestDto); + } + + @PutMapping("/{id}") + public ToDoResponseDto update(@RequestBody @Validated ToDoRequestDto requestDto, + @PathVariable @UUID String id) { + return toDoService.update(requestDto, id); + } + + @DeleteMapping("/{id}") + public String delete(@PathVariable @UUID String id) { + return toDoService.delete(id); + } +} diff --git a/src/main/java/com/emobile/springtodo/exception/handler/ControllersExceptionHandler.java b/src/main/java/com/emobile/springtodo/exception/handler/ControllersExceptionHandler.java new file mode 100644 index 00000000..7c88d0c0 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/exception/handler/ControllersExceptionHandler.java @@ -0,0 +1,44 @@ +package com.emobile.springtodo.exception.handler; + +import com.emobile.springtodo.exception.ToDoNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; + +import java.util.stream.Collectors; + +@RestControllerAdvice +@RequiredArgsConstructor +public class ControllersExceptionHandler { + + @ExceptionHandler(ToDoNotFoundException.class) + public ResponseEntity handleNotFoundException(Exception e, WebRequest request) { + CustomErrorResponse errorResponse = new CustomErrorResponse(e.getMessage(), HttpStatus.NOT_FOUND, request); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException( + MethodArgumentNotValidException e, WebRequest request) { + String message = e.getBindingResult().getAllErrors().stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining("; ", "", ".")); + + CustomErrorResponse errorResponse = new CustomErrorResponse(message, HttpStatus.BAD_REQUEST, request); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler({ + IllegalArgumentException.class, + HttpMessageNotReadableException.class}) + public ResponseEntity handleIllegalArgumentException(Exception e, WebRequest request) { + CustomErrorResponse errorResponse = new CustomErrorResponse(e.getMessage(), HttpStatus.BAD_REQUEST, request); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } +} diff --git a/src/main/java/com/emobile/springtodo/exception/handler/CustomErrorResponse.java b/src/main/java/com/emobile/springtodo/exception/handler/CustomErrorResponse.java new file mode 100644 index 00000000..a6bf0334 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/exception/handler/CustomErrorResponse.java @@ -0,0 +1,25 @@ +package com.emobile.springtodo.exception.handler; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.springframework.http.HttpStatus; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +public class CustomErrorResponse { + private String timestamp; + private int status; + private String error; + private String path; + + public CustomErrorResponse(String message, HttpStatus status, WebRequest request) { + this.timestamp = LocalDateTime.now().toString(); + this.status = status.value(); + this.error = message; + this.path = ((ServletWebRequest) request).getRequest().getRequestURI(); + } +} diff --git a/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java b/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java index c9a15200..95411334 100644 --- a/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java +++ b/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java @@ -8,7 +8,7 @@ public interface ToDoRepository { Optional findById(UUID id); - List findAll(int limit, int offset); + List findAll(int pageSize, int pageNumber); ToDo save(ToDo todo); ToDo update(ToDo todo, UUID id); void delete(UUID id); diff --git a/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java b/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java index 7e2d88d2..711ad49e 100644 --- a/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java +++ b/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java @@ -35,11 +35,13 @@ public Optional findById(UUID id) { } @Override - public List findAll(int limit, int offset) { + public List findAll(int pageSize, int pageNumber) { String query = "SELECT * FROM todos ORDER BY id LIMIT ? OFFSET ?"; + int offset = (pageNumber - 1) * pageSize; + try { - return jdbcTemplate.query(query, mapper, limit, offset); + return jdbcTemplate.query(query, mapper, pageSize, offset); } catch (DataAccessException e) { return new ArrayList<>(); } diff --git a/src/main/java/com/emobile/springtodo/service/ToDoService.java b/src/main/java/com/emobile/springtodo/service/ToDoService.java index 13f4987b..0b184f8e 100644 --- a/src/main/java/com/emobile/springtodo/service/ToDoService.java +++ b/src/main/java/com/emobile/springtodo/service/ToDoService.java @@ -4,12 +4,11 @@ import com.emobile.springtodo.dto.response.ToDoResponseDto; import java.util.List; -import java.util.UUID; public interface ToDoService { - ToDoResponseDto findById(UUID id); - List findAll(int limit, int offset); + ToDoResponseDto findById(String id); + List findAll(int pageSize, int pageNumber); ToDoResponseDto save(ToDoRequestDto toDoRequestDto); - ToDoResponseDto update(ToDoRequestDto toDoRequestDto, UUID id); - String delete(UUID id); + ToDoResponseDto update(ToDoRequestDto toDoRequestDto, String id); + String delete(String id); } diff --git a/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java b/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java index 25564cfc..6ebc9493 100644 --- a/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java +++ b/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java @@ -21,15 +21,15 @@ public class ToDoServiceImpl implements ToDoService { @Override - public ToDoResponseDto findById(UUID id) { - return toDoRepository.findById(id) + public ToDoResponseDto findById(String id) { + return toDoRepository.findById(UUID.fromString(id)) .map(toDoMapper::fromEntityToResponseDto) - .orElseThrow(() -> new ToDoNotFoundException(id)); + .orElseThrow(() -> new ToDoNotFoundException(UUID.fromString(id))); } @Override - public List findAll(int limit, int offset) { - return toDoRepository.findAll(limit, offset) + public List findAll(int pageSize, int pageNumber) { + return toDoRepository.findAll(pageSize, pageNumber) .stream() .map(toDoMapper::fromEntityToResponseDto) .toList(); @@ -43,15 +43,15 @@ public ToDoResponseDto save(ToDoRequestDto toDoRequestDto) { } @Override - public ToDoResponseDto update(ToDoRequestDto toDoRequestDto, UUID id) { + public ToDoResponseDto update(ToDoRequestDto toDoRequestDto, String id) { ToDo forUpdate = toDoMapper.fromRequestToEntity(toDoRequestDto); - ToDo updated = toDoRepository.update(forUpdate, id); + ToDo updated = toDoRepository.update(forUpdate, UUID.fromString(id)); return toDoMapper.fromEntityToResponseDto(updated); } @Override - public String delete(UUID id) { - toDoRepository.delete(id); + public String delete(String id) { + toDoRepository.delete(UUID.fromString(id)); return String.format("ToDo with id: %s was deleted", id); } } From 2701e161b6dcc6ace6fa44c1a96afb735eb71198 Mon Sep 17 00:00:00 2001 From: Sergey Kovalev Date: Sun, 15 Jun 2025 22:20:22 +0300 Subject: [PATCH 05/10] Little refactoring & bugs fixes. --- pom.xml | 20 ++++++++++--------- .../springtodo/controller/ToDoController.java | 2 +- .../dto/request/ToDoRequestDto.java | 2 +- .../dto/response/ToDoResponseDto.java | 2 +- .../com/emobile/springtodo/entity/ToDo.java | 2 +- .../exception/ToDoNotFoundException.java | 2 +- .../handler/ControllersExceptionHandler.java | 4 +++- .../springtodo/mapper/JDBCToDoMapper.java | 2 +- .../emobile/springtodo/mapper/ToDoMapper.java | 6 +++++- .../repository/impl/ToDoRepositoryImpl.java | 15 +++++++------- .../service/impl/ToDoServiceImpl.java | 2 +- .../db/changelog/01.000.01/db.changelog.yaml | 11 ++++++++++ .../db/changelog/01.000.01/todos.sql | 3 +++ .../db/changelog/db.changelog-master.yaml | 3 +++ 14 files changed, 51 insertions(+), 25 deletions(-) create mode 100644 src/main/resources/db/changelog/01.000.01/db.changelog.yaml create mode 100644 src/main/resources/db/changelog/01.000.01/todos.sql diff --git a/pom.xml b/pom.xml index 97bc67a5..1fd9bae3 100644 --- a/pom.xml +++ b/pom.xml @@ -73,7 +73,8 @@ org.projectlombok lombok - true + 1.18.34 + provided org.springframework.boot @@ -111,11 +112,20 @@ org.apache.maven.plugins maven-compiler-plugin + 3.14.0 + ${java.version} + ${java.version} org.projectlombok lombok + 1.18.34 + + + org.mapstruct + mapstruct-processor + ${mapstructVersion} @@ -123,14 +133,6 @@ org.springframework.boot spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - diff --git a/src/main/java/com/emobile/springtodo/controller/ToDoController.java b/src/main/java/com/emobile/springtodo/controller/ToDoController.java index 4e4cbda0..7f503586 100644 --- a/src/main/java/com/emobile/springtodo/controller/ToDoController.java +++ b/src/main/java/com/emobile/springtodo/controller/ToDoController.java @@ -38,7 +38,7 @@ public List findAll(@RequestParam @Positive int pageSize, } @PostMapping - public ToDoResponseDto save(@RequestBody ToDoRequestDto requestDto) { + public ToDoResponseDto save(@RequestBody @Validated ToDoRequestDto requestDto) { return toDoService.save(requestDto); } diff --git a/src/main/java/com/emobile/springtodo/dto/request/ToDoRequestDto.java b/src/main/java/com/emobile/springtodo/dto/request/ToDoRequestDto.java index 62f180da..0cde5336 100644 --- a/src/main/java/com/emobile/springtodo/dto/request/ToDoRequestDto.java +++ b/src/main/java/com/emobile/springtodo/dto/request/ToDoRequestDto.java @@ -16,5 +16,5 @@ public record ToDoRequestDto( @FutureOrPresent(message = "Expiration date must be in future") LocalDateTime expirationDate, - boolean isDone) { + boolean done) { } diff --git a/src/main/java/com/emobile/springtodo/dto/response/ToDoResponseDto.java b/src/main/java/com/emobile/springtodo/dto/response/ToDoResponseDto.java index dc0d8c89..8a02abe1 100644 --- a/src/main/java/com/emobile/springtodo/dto/response/ToDoResponseDto.java +++ b/src/main/java/com/emobile/springtodo/dto/response/ToDoResponseDto.java @@ -7,5 +7,5 @@ public record ToDoResponseDto( UUID id, String description, LocalDateTime expirationDate, - boolean isDone) { + boolean done) { } diff --git a/src/main/java/com/emobile/springtodo/entity/ToDo.java b/src/main/java/com/emobile/springtodo/entity/ToDo.java index 243a5eef..6275ffb0 100644 --- a/src/main/java/com/emobile/springtodo/entity/ToDo.java +++ b/src/main/java/com/emobile/springtodo/entity/ToDo.java @@ -33,5 +33,5 @@ public class ToDo { private LocalDateTime expirationDate; @Column(name = "is_done") - private boolean isDone; + private boolean done; } diff --git a/src/main/java/com/emobile/springtodo/exception/ToDoNotFoundException.java b/src/main/java/com/emobile/springtodo/exception/ToDoNotFoundException.java index 080d51d4..4f0d8baf 100644 --- a/src/main/java/com/emobile/springtodo/exception/ToDoNotFoundException.java +++ b/src/main/java/com/emobile/springtodo/exception/ToDoNotFoundException.java @@ -3,7 +3,7 @@ import java.util.UUID; public class ToDoNotFoundException extends RuntimeException { - private static final String DEFAULT_MESSAGE = "ToDo with id %s not found for update"; + private static final String DEFAULT_MESSAGE = "ToDo with id %s not found"; public ToDoNotFoundException(UUID id) { super(String.format(DEFAULT_MESSAGE, id.toString())); diff --git a/src/main/java/com/emobile/springtodo/exception/handler/ControllersExceptionHandler.java b/src/main/java/com/emobile/springtodo/exception/handler/ControllersExceptionHandler.java index 7c88d0c0..89d38052 100644 --- a/src/main/java/com/emobile/springtodo/exception/handler/ControllersExceptionHandler.java +++ b/src/main/java/com/emobile/springtodo/exception/handler/ControllersExceptionHandler.java @@ -1,6 +1,7 @@ package com.emobile.springtodo.exception.handler; import com.emobile.springtodo.exception.ToDoNotFoundException; +import jakarta.validation.ConstraintViolationException; import lombok.RequiredArgsConstructor; import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.HttpStatus; @@ -36,7 +37,8 @@ public ResponseEntity handleMethodArgumentNotValidException @ExceptionHandler({ IllegalArgumentException.class, - HttpMessageNotReadableException.class}) + HttpMessageNotReadableException.class, + ConstraintViolationException.class}) public ResponseEntity handleIllegalArgumentException(Exception e, WebRequest request) { CustomErrorResponse errorResponse = new CustomErrorResponse(e.getMessage(), HttpStatus.BAD_REQUEST, request); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); diff --git a/src/main/java/com/emobile/springtodo/mapper/JDBCToDoMapper.java b/src/main/java/com/emobile/springtodo/mapper/JDBCToDoMapper.java index 0eafd1ee..69058e83 100644 --- a/src/main/java/com/emobile/springtodo/mapper/JDBCToDoMapper.java +++ b/src/main/java/com/emobile/springtodo/mapper/JDBCToDoMapper.java @@ -18,7 +18,7 @@ public ToDo mapRow(ResultSet rs, int rowNum) throws SQLException { .id(UUID.fromString(rs.getString("id"))) .description(rs.getString("description")) .expirationDate(rs.getObject("expiration_date", LocalDateTime.class)) - .isDone(rs.getBoolean("is_done")) + .done(rs.getBoolean("is_done")) .build(); } } diff --git a/src/main/java/com/emobile/springtodo/mapper/ToDoMapper.java b/src/main/java/com/emobile/springtodo/mapper/ToDoMapper.java index 859bb1c3..e6522e86 100644 --- a/src/main/java/com/emobile/springtodo/mapper/ToDoMapper.java +++ b/src/main/java/com/emobile/springtodo/mapper/ToDoMapper.java @@ -4,10 +4,14 @@ import com.emobile.springtodo.dto.response.ToDoResponseDto; import com.emobile.springtodo.entity.ToDo; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; -@Mapper +@Mapper(componentModel = "spring") public interface ToDoMapper { + @Mapping(target = "id", ignore = true) + @Mapping(source = "done", target = "done") ToDo fromRequestToEntity(ToDoRequestDto toDoRequestDto); + @Mapping(source = "done", target = "done") ToDoResponseDto fromEntityToResponseDto(ToDo toDo); } diff --git a/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java b/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java index 711ad49e..a1c99d2b 100644 --- a/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java +++ b/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java @@ -25,9 +25,9 @@ public class ToDoRepositoryImpl implements ToDoRepository { @Override public Optional findById(UUID id) { String query = "SELECT * FROM todos WHERE id = ?"; - + System.out.println(id); try { - ToDo toDo = jdbcTemplate.queryForObject(query, mapper, id.toString()); + ToDo toDo = jdbcTemplate.queryForObject(query, mapper, id); return Optional.ofNullable(toDo); } catch (DataAccessException e) { return Optional.empty(); @@ -56,7 +56,7 @@ public ToDo save(ToDo todo) { String query = "INSERT INTO todos (id, description, expiration_date, is_done) VALUES (?, ?, ?, ?)"; jdbcTemplate.update(query, - todo.getId().toString(), + todo.getId(), todo.getDescription(), todo.getExpirationDate(), todo.isDone()); @@ -71,18 +71,19 @@ public ToDo update(ToDo todo, UUID id) { todo.getDescription(), todo.getExpirationDate(), todo.isDone(), - id.toString()); + id); if (rowsChanged == 0) { throw new ToDoNotFoundException(id); } - - return todo; } + todo.setId(id); + return todo; + } @Override public void delete(UUID id) { String sql = "DELETE FROM todos WHERE id = ?"; - int rowsChanged = jdbcTemplate.update(sql, id.toString()); + int rowsChanged = jdbcTemplate.update(sql, id); if (rowsChanged == 0) { throw new ToDoNotFoundException(id); } diff --git a/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java b/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java index 6ebc9493..8a2bebd9 100644 --- a/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java +++ b/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java @@ -52,6 +52,6 @@ public ToDoResponseDto update(ToDoRequestDto toDoRequestDto, String id) { @Override public String delete(String id) { toDoRepository.delete(UUID.fromString(id)); - return String.format("ToDo with id: %s was deleted", id); + return String.format("ToDo with id: %s has been deleted", id); } } diff --git a/src/main/resources/db/changelog/01.000.01/db.changelog.yaml b/src/main/resources/db/changelog/01.000.01/db.changelog.yaml new file mode 100644 index 00000000..6a550b13 --- /dev/null +++ b/src/main/resources/db/changelog/01.000.01/db.changelog.yaml @@ -0,0 +1,11 @@ +databaseChangeLog: + - include: + file: todos.sql + relativeToChangelogFile: true + - changeSet: + id: 01.000.01 + author: Siarhei_Kavaleu + logicalFilePath: 01.000.01/db.changelog.yaml + changes: + - tagDatabase: + tag: 01.000.01 \ No newline at end of file diff --git a/src/main/resources/db/changelog/01.000.01/todos.sql b/src/main/resources/db/changelog/01.000.01/todos.sql new file mode 100644 index 00000000..d806da40 --- /dev/null +++ b/src/main/resources/db/changelog/01.000.01/todos.sql @@ -0,0 +1,3 @@ +--liquibase formatted sql +--changeset Siarhei_Kavaleu:db localFilePath:01.000.01/todos.sql +ALTER TABLE todos ALTER COLUMN expiration_date TYPE timestamp; \ No newline at end of file diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index d8edc901..5a7e2f0c 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -1,4 +1,7 @@ databaseChangeLog: - include: file: 01.000.00/db.changelog.yaml + relativeToChangelogFile: true + - include: + file: 01.000.01/db.changelog.yaml relativeToChangelogFile: true \ No newline at end of file From b81d50ca4b12e7fc202a1735f3749d7fe9d88b88 Mon Sep 17 00:00:00 2001 From: Sergey Kovalev Date: Mon, 16 Jun 2025 00:56:24 +0300 Subject: [PATCH 06/10] Unit tests added. --- pom.xml | 6 + .../dto/request/ToDoRequestDto.java | 2 + .../dto/response/ToDoResponseDto.java | 3 + .../controller/ToDoControllerTest.java | 211 ++++++++++++++++++ .../springtodo/mapper/ToDoMapperTest.java | 64 ++++++ .../service/impl/ToDoServiceImplTest.java | 192 ++++++++++++++++ 6 files changed, 478 insertions(+) create mode 100644 src/test/java/com/emobile/springtodo/controller/ToDoControllerTest.java create mode 100644 src/test/java/com/emobile/springtodo/mapper/ToDoMapperTest.java create mode 100644 src/test/java/com/emobile/springtodo/service/impl/ToDoServiceImplTest.java diff --git a/pom.xml b/pom.xml index 1fd9bae3..a7362dcf 100644 --- a/pom.xml +++ b/pom.xml @@ -100,6 +100,12 @@ junit-jupiter test + + org.assertj + assertj-core + 3.27.3 + test + org.testcontainers postgresql diff --git a/src/main/java/com/emobile/springtodo/dto/request/ToDoRequestDto.java b/src/main/java/com/emobile/springtodo/dto/request/ToDoRequestDto.java index 0cde5336..6f8b8e67 100644 --- a/src/main/java/com/emobile/springtodo/dto/request/ToDoRequestDto.java +++ b/src/main/java/com/emobile/springtodo/dto/request/ToDoRequestDto.java @@ -3,10 +3,12 @@ import jakarta.validation.constraints.FutureOrPresent; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.Builder; import org.hibernate.validator.constraints.Length; import java.time.LocalDateTime; +@Builder public record ToDoRequestDto( @Length(min = 1, max = 255, message = "Description must contain from 1 to 255 characters") @NotBlank(message = "Description can't be blank") diff --git a/src/main/java/com/emobile/springtodo/dto/response/ToDoResponseDto.java b/src/main/java/com/emobile/springtodo/dto/response/ToDoResponseDto.java index 8a02abe1..6a54cfba 100644 --- a/src/main/java/com/emobile/springtodo/dto/response/ToDoResponseDto.java +++ b/src/main/java/com/emobile/springtodo/dto/response/ToDoResponseDto.java @@ -1,8 +1,11 @@ package com.emobile.springtodo.dto.response; +import lombok.Builder; + import java.time.LocalDateTime; import java.util.UUID; +@Builder public record ToDoResponseDto( UUID id, String description, diff --git a/src/test/java/com/emobile/springtodo/controller/ToDoControllerTest.java b/src/test/java/com/emobile/springtodo/controller/ToDoControllerTest.java new file mode 100644 index 00000000..d4e57a75 --- /dev/null +++ b/src/test/java/com/emobile/springtodo/controller/ToDoControllerTest.java @@ -0,0 +1,211 @@ +package com.emobile.springtodo.controller; + +import com.emobile.springtodo.dto.request.ToDoRequestDto; +import com.emobile.springtodo.dto.response.ToDoResponseDto; +import com.emobile.springtodo.exception.ToDoNotFoundException; +import com.emobile.springtodo.exception.handler.ControllersExceptionHandler; +import com.emobile.springtodo.service.ToDoService; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +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.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@AutoConfigureMockMvc +@ExtendWith(MockitoExtension.class) +class ToDoControllerTest { + private MockMvc mockMvc; + private ObjectMapper objectMapper; + + @Mock + private ToDoService service; + @InjectMocks + private ToDoController controller; + + private String id; + private ToDoRequestDto requestDto; + private ToDoResponseDto responseDto; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders + .standaloneSetup(controller) + .setControllerAdvice(new ControllersExceptionHandler()) + .build(); + objectMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()); + + id = "1fcc470d-a691-4d0d-9a23-b8b455c5f586"; + String description = "bla bla bla"; + LocalDateTime expirationDate = LocalDateTime.now().plusYears(1); + boolean done = false; + + requestDto = ToDoRequestDto.builder() + .description(description) + .expirationDate(expirationDate) + .done(done) + .build(); + + responseDto = ToDoResponseDto.builder() + .id(UUID.fromString(id)) + .description(description) + .expirationDate(expirationDate) + .done(done) + .build(); + } + + @Test + @DisplayName("should return response when found by id") + void findById_whenPresent() throws Exception { + String jsonResponseQuery = objectMapper.writeValueAsString(responseDto); + + when(service.findById(id)).thenReturn(responseDto); + + mockMvc.perform(MockMvcRequestBuilders.get("/todo/{id}", id) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(jsonResponseQuery)) + .andDo(print()); + + verify(service).findById(id); + } + + @Test + @DisplayName("should return error when not found by id") + void findById_whenNotPresent() throws Exception { + when(service.findById(id)).thenThrow(new ToDoNotFoundException(UUID.fromString(id))); + + mockMvc.perform(MockMvcRequestBuilders.get("/todo/{id}", id) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.error").value(String.format("ToDo with id %s not found", id))) + .andDo(print()); + } + + @Test + @DisplayName("should return list of all founded todos") + void findAll() throws Exception { + int pageSize = 5; + int pageNumber = 1; + List responseDtoList = List.of(responseDto, responseDto); + + when(service.findAll(pageSize, pageNumber)).thenReturn(responseDtoList); + + String expectedJson = objectMapper.writeValueAsString(responseDtoList); + + mockMvc.perform(MockMvcRequestBuilders.get("/todo/all") + .param("pageSize", String.valueOf(pageSize)) + .param("pageNumber", String.valueOf(pageNumber)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(content().json(expectedJson)) + .andDo(print()); + } + + @Test + @DisplayName("should save ToDo and return it") + void save() throws Exception { + when(service.save(any(ToDoRequestDto.class))).thenReturn(responseDto); + + String requestJson = objectMapper.writeValueAsString(requestDto); + String responseJson = objectMapper.writeValueAsString(responseDto); + + mockMvc.perform(MockMvcRequestBuilders.post("/todo") + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(content().json(responseJson)) + .andDo(print()); + } + + @Test + @DisplayName("should update todo by id when it present in db") + void update_whenPresent() throws Exception { + when(service.update(any(ToDoRequestDto.class), eq(id))).thenReturn(responseDto); + + String requestJson = objectMapper.writeValueAsString(requestDto); + String responseJson = objectMapper.writeValueAsString(responseDto); + + mockMvc.perform(MockMvcRequestBuilders.put("/todo/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(content().json(responseJson)); + } + + @Test + @DisplayName("should return error of updating todo when id not present in db") + void update_whenNotPresent() throws Exception { + when(service.update(any(ToDoRequestDto.class), eq(id))) + .thenThrow(new ToDoNotFoundException(UUID.fromString(id))); + + String requestJson = objectMapper.writeValueAsString(requestDto); + + mockMvc.perform(MockMvcRequestBuilders.put("/todo/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andExpect(status().isNotFound()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.error").value(String.format("ToDo with id %s not found", id))) + .andDo(print()); + } + + @Test + @DisplayName("should delete todo by id when it present in db") + void delete_whenPresent() throws Exception { + String expectedMessage = String.format("ToDo with id: %s has been deleted", id); + + when(service.delete(id)).thenReturn(expectedMessage); + + mockMvc.perform(MockMvcRequestBuilders.delete("/todo/{id}", id) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string(expectedMessage)) + .andDo(print()); + + verify(service).delete(id); + } + + @Test + @DisplayName("should return error of deleting todo when id not present in db") + void delete_whenNotPresent() throws Exception { + when(service.delete(id)).thenThrow(new ToDoNotFoundException(UUID.fromString(id))); + + mockMvc.perform(MockMvcRequestBuilders.delete("/todo/{id}", id) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.error").value(String.format("ToDo with id %s not found", id))) + .andDo(print()); + + verify(service).delete(id); + } +} \ No newline at end of file diff --git a/src/test/java/com/emobile/springtodo/mapper/ToDoMapperTest.java b/src/test/java/com/emobile/springtodo/mapper/ToDoMapperTest.java new file mode 100644 index 00000000..58b4d69d --- /dev/null +++ b/src/test/java/com/emobile/springtodo/mapper/ToDoMapperTest.java @@ -0,0 +1,64 @@ +package com.emobile.springtodo.mapper; + +import com.emobile.springtodo.dto.request.ToDoRequestDto; +import com.emobile.springtodo.dto.response.ToDoResponseDto; +import com.emobile.springtodo.entity.ToDo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class ToDoMapperTest { + + private final ToDoMapper mapper = new ToDoMapperImpl(); + + private ToDo todo; + private ToDoRequestDto requestDto; + private ToDoResponseDto responseDto; + + @BeforeEach + void setUp() { + String id = "e487f331-5b7d-40b9-b49d-b41f94b2c960"; + todo = ToDo.builder() + .id(UUID.fromString(id)) + .description("bla bla bla") + .expirationDate(LocalDateTime.now().plusYears(1)) + .done(false) + .build(); + + responseDto = ToDoResponseDto.builder() + .id(todo.getId()) + .description(todo.getDescription()) + .expirationDate(todo.getExpirationDate()) + .done(todo.isDone()) + .build(); + + requestDto = ToDoRequestDto.builder() + .description(todo.getDescription()) + .expirationDate(todo.getExpirationDate()) + .done(todo.isDone()) + .build(); + } + + @Test + @DisplayName("should convert requestDto to Entity") + void fromRequestToEntity() { + todo.setId(null); + + ToDo actual = mapper.fromRequestToEntity(requestDto); + + assertThat(actual).isEqualTo(todo); + } + + @Test + @DisplayName("should convert Entity to responseDto") + void fromEntityToResponseDto() { + ToDoResponseDto actual = mapper.fromEntityToResponseDto(todo); + + assertThat(actual).isEqualTo(responseDto); + } +} \ No newline at end of file diff --git a/src/test/java/com/emobile/springtodo/service/impl/ToDoServiceImplTest.java b/src/test/java/com/emobile/springtodo/service/impl/ToDoServiceImplTest.java new file mode 100644 index 00000000..1d457b76 --- /dev/null +++ b/src/test/java/com/emobile/springtodo/service/impl/ToDoServiceImplTest.java @@ -0,0 +1,192 @@ +package com.emobile.springtodo.service.impl; + +import com.emobile.springtodo.dto.request.ToDoRequestDto; +import com.emobile.springtodo.dto.response.ToDoResponseDto; +import com.emobile.springtodo.entity.ToDo; +import com.emobile.springtodo.exception.ToDoNotFoundException; +import com.emobile.springtodo.mapper.ToDoMapper; +import com.emobile.springtodo.repository.ToDoRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +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 java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ToDoServiceImplTest { + @Mock + private ToDoRepository repository; + @Mock + private ToDoMapper mapper; + @InjectMocks + private ToDoServiceImpl service; + + private String id; + private ToDo todo; + private ToDoResponseDto responseDto; + private ToDoRequestDto requestDto; + + @BeforeEach + void setUp() { + id = "e487f331-5b7d-40b9-b49d-b41f94b2c960"; + todo = ToDo.builder() + .id(UUID.fromString(id)) + .description("bla bla bla") + .expirationDate(LocalDateTime.now().plusYears(1)) + .done(false) + .build(); + + responseDto = ToDoResponseDto.builder() + .id(todo.getId()) + .description(todo.getDescription()) + .expirationDate(todo.getExpirationDate()) + .done(todo.isDone()) + .build(); + + requestDto = ToDoRequestDto.builder() + .description(todo.getDescription()) + .expirationDate(todo.getExpirationDate()) + .done(todo.isDone()) + .build(); + } + + @Test + @DisplayName("should find ToDo by ID when present") + void findById_whenPresent() { + when(repository.findById(any(UUID.class))) + .thenReturn(Optional.of(todo)); + when(mapper.fromEntityToResponseDto(todo)) + .thenReturn(responseDto); + + ToDoResponseDto actual = service.findById(id); + + assertThat(actual).isEqualTo(responseDto); + } + + @Test + @DisplayName("should throw ToDoNotFoundException when ToDo not found by ID") + void findById_whenNotPresent() { + when(repository.findById(any(UUID.class))) + .thenReturn(Optional.empty()); + + assertThatThrownBy(() -> service.findById(id)) + .isInstanceOf(ToDoNotFoundException.class) + .hasMessageContaining(String.format("ToDo with id %s not found", id)); + + verify(mapper, never()).fromEntityToResponseDto(any()); + } + + @Test + @DisplayName("should return all ToDos as responseDTOs") + void findAll() { + List expected = List.of(responseDto, responseDto, responseDto); + + int pageSize = 5, pageNumber = 1; + List toDos = List.of(todo, todo, todo); + + when(repository.findAll(pageSize, pageNumber)) + .thenReturn(toDos); + when(mapper.fromEntityToResponseDto(any(ToDo.class))) + .thenReturn(responseDto); + + List actual = service.findAll(pageSize, pageNumber); + + assertThat(actual) + .isNotEmpty() + .containsExactlyElementsOf(expected) + .hasSameSizeAs(expected); + } + + @Test + @DisplayName("should save requestToDo and return responseDTO") + void save() { + when(mapper.fromRequestToEntity(any(ToDoRequestDto.class))) + .thenReturn(todo); + when(repository.save(any(ToDo.class))) + .thenReturn(todo); + when(mapper.fromEntityToResponseDto(todo)) + .thenReturn(responseDto); + + ToDoResponseDto actual = service.save(requestDto); + + assertThat(actual).isEqualTo(responseDto); + + verify(mapper, times(1)).fromEntityToResponseDto(any(ToDo.class)); + verify(mapper, times(1)).fromRequestToEntity(any(ToDoRequestDto.class)); + } + + @Test + @DisplayName("should update an existing ToDo when present") + void update_whenPresent() { + when(mapper.fromRequestToEntity(any(ToDoRequestDto.class))) + .thenReturn(todo); + when(repository.update(any(ToDo.class), any(UUID.class))) + .thenReturn(todo); + when(mapper.fromEntityToResponseDto(todo)) + .thenReturn(responseDto); + + ToDoResponseDto actual = service.update(requestDto, id); + + assertThat(actual).isEqualTo(responseDto); + + verify(mapper, times(1)).fromRequestToEntity(any(ToDoRequestDto.class)); + verify(mapper, times(1)).fromEntityToResponseDto(any(ToDo.class)); + } + + @Test + @DisplayName("should throw ToDoNotFoundException when updating a non-existent ToDo") + void update_whenNotPresent() { + when(mapper.fromRequestToEntity(any(ToDoRequestDto.class))) + .thenReturn(todo); + when(repository.update(any(ToDo.class), any(UUID.class))) + .thenThrow(new ToDoNotFoundException(UUID.fromString(id))); + + assertThatThrownBy(() -> service.update(requestDto, id)) + .isInstanceOf(ToDoNotFoundException.class) + .hasMessageContaining(String.format("ToDo with id %s not found", id)); + + verify(mapper, times(1)).fromRequestToEntity(any(ToDoRequestDto.class)); + verify(mapper, never()).fromEntityToResponseDto(any(ToDo.class)); + } + + @Test + @DisplayName("should delete ToDo by ID when present") + void delete_whenPresent() { + doNothing().when(repository).delete(any(UUID.class)); + + String actual = service.delete(id); + + assertThat(actual).isEqualTo(String.format("ToDo with id: %s has been deleted", id)); + verify(repository, times(1)).delete(any(UUID.class)); + } + + @Test + @DisplayName("should throw ToDoNotFoundException when deleting a non-existent ToDo") + void delete_whenNotPresent() { + doThrow(new ToDoNotFoundException(UUID.fromString(id))) + .when(repository).delete(any(UUID.class)); + + assertThatThrownBy(() -> repository.delete(UUID.fromString(id))) + .isInstanceOf(ToDoNotFoundException.class) + .hasMessageContaining(String.format("ToDo with id %s not found", id)); + + verify(repository, times(1)).delete(any(UUID.class)); + } +} \ No newline at end of file From 13e64193244f86241a121e8b587c8ad7dea44cb5 Mon Sep 17 00:00:00 2001 From: Sergey Kovalev Date: Mon, 16 Jun 2025 14:02:02 +0300 Subject: [PATCH 07/10] Integration tests added. --- .../repository/impl/ToDoRepositoryImpl.java | 1 - .../ToDoControllerIntegrationTest.java | 234 ++++++++++++++++++ src/test/resources/todo-data-init.sql | 4 + 3 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/emobile/springtodo/integration/ToDoControllerIntegrationTest.java create mode 100644 src/test/resources/todo-data-init.sql diff --git a/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java b/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java index a1c99d2b..118c3138 100644 --- a/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java +++ b/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java @@ -25,7 +25,6 @@ public class ToDoRepositoryImpl implements ToDoRepository { @Override public Optional findById(UUID id) { String query = "SELECT * FROM todos WHERE id = ?"; - System.out.println(id); try { ToDo toDo = jdbcTemplate.queryForObject(query, mapper, id); return Optional.ofNullable(toDo); diff --git a/src/test/java/com/emobile/springtodo/integration/ToDoControllerIntegrationTest.java b/src/test/java/com/emobile/springtodo/integration/ToDoControllerIntegrationTest.java new file mode 100644 index 00000000..70f19581 --- /dev/null +++ b/src/test/java/com/emobile/springtodo/integration/ToDoControllerIntegrationTest.java @@ -0,0 +1,234 @@ +package com.emobile.springtodo.integration; + +import com.emobile.springtodo.dto.request.ToDoRequestDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.http.MediaType; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.LocalDateTime; +import java.time.Month; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Testcontainers +@AutoConfigureMockMvc +@Transactional +@Sql({"/todo-data-init.sql"}) +public class ToDoControllerIntegrationTest { + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + + + @Container + @ServiceConnection + static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer<>("postgres:16.0"); + + @BeforeEach + void setUp() { + objectMapper.registerModule(new JavaTimeModule()); + } + + @Test + @DisplayName("should return response when found by id") + void getToDo() throws Exception { + String id = "989b3265-ccc7-4480-bb9a-cf1c7026ed39"; + String expectedJson = """ + { + "id": "989b3265-ccc7-4480-bb9a-cf1c7026ed39", + "description": "bla bla bla", + "expirationDate": '2025-07-18T00:00:00.546', + "done": false + }"""; + + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/todo/" + id)) + .andExpect(status().isOk()) + .andReturn(); + + JSONAssert.assertEquals(expectedJson, result.getResponse().getContentAsString(), false); + } + + @Test + @DisplayName("should return error when not found by id") + void getToDo_notFound() throws Exception { + String id = "859b3265-ccc7-4480-bb9a-cf1c7026ed39"; + String expectedJson = """ + { + "status":404, + "error":"ToDo with id 859b3265-ccc7-4480-bb9a-cf1c7026ed39 not found", + "path":"/todo/859b3265-ccc7-4480-bb9a-cf1c7026ed39" + }"""; + + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/todo/" + id)) + .andExpect(status().isNotFound()) + .andReturn(); + + JSONAssert.assertEquals(expectedJson, result.getResponse().getContentAsString(), false); + } + + @Test + @DisplayName("should return list of all founded todos") + void findAll() throws Exception { + String expectedJson = """ + [ + { + "id":"989b3265-ccc7-4480-bb9a-cf1c7026ed39", + "description":"bla bla bla", + "expirationDate":"2025-07-18T00:00:00.546", + "done":false + }, + { + "id":"cd3d29fe-6991-4cb3-992d-cef17e91decb", + "description":"bla2 bla bla", + "expirationDate":"2025-07-15T00:00:00", + "done":true + } + ]"""; + + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/todo/all") + .param("pageSize", "5") + .param("pageNumber", "1")) + .andExpect(status().isOk()) + .andReturn(); + + JSONAssert.assertEquals(expectedJson, result.getResponse().getContentAsString(), false); + } + + @Test + @DisplayName("should save ToDo and return it") + void save() throws Exception { + ToDoRequestDto requestDto = ToDoRequestDto.builder() + .description("Hello World") + .expirationDate(LocalDateTime.of(2025, Month.NOVEMBER, 25, 12, 59, 10)) + .done(false) + .build(); + String requestStr = objectMapper.writeValueAsString(requestDto); + + String expectedJson = """ + { + "description":"Hello World", + "expirationDate":"2025-11-25T12:59:10", + "done":false + }"""; + + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/todo") + .contentType(MediaType.APPLICATION_JSON) + .content(requestStr)) + .andExpect(status().isOk()) + .andReturn(); + + JSONAssert.assertEquals(expectedJson, result.getResponse().getContentAsString(), false); + } + + @Test + @DisplayName("should update todo by id when it present in db") + void update_whenPresent() throws Exception { + String id = "cd3d29fe-6991-4cb3-992d-cef17e91decb"; + + ToDoRequestDto requestDto = ToDoRequestDto.builder() + .description("Hello World") + .expirationDate(LocalDateTime.of(2025, Month.NOVEMBER, 25, 12, 59, 10)) + .done(false) + .build(); + + String requestStr = objectMapper.writeValueAsString(requestDto); + + String expectedJson = """ + { + "id":"cd3d29fe-6991-4cb3-992d-cef17e91decb", + "description":"Hello World", + "expirationDate":"2025-11-25T12:59:10", + "done":false + }"""; + + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.put("/todo/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(requestStr)) + .andExpect(status().isOk()) + .andReturn(); + + JSONAssert.assertEquals(expectedJson, result.getResponse().getContentAsString(), false); + } + + @Test + @DisplayName("should return error of updating todo when id not present in db") + void update_whenNotPresent() throws Exception { + String id = "859b3265-ccc7-4480-bb9a-cf1c7026ed39"; + + ToDoRequestDto requestDto = ToDoRequestDto.builder() + .description("Hello World") + .expirationDate(LocalDateTime.of(2025, Month.NOVEMBER, 25, 12, 59, 10)) + .done(false) + .build(); + + String requestStr = objectMapper.writeValueAsString(requestDto); + + String expectedJson = """ + { + "status":404, + "error":"ToDo with id 859b3265-ccc7-4480-bb9a-cf1c7026ed39 not found", + "path":"/todo/859b3265-ccc7-4480-bb9a-cf1c7026ed39" + }"""; + + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.put("/todo/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(requestStr)) + .andExpect(status().isNotFound()) + .andReturn(); + JSONAssert.assertEquals(expectedJson, result.getResponse().getContentAsString(), false); + } + + @Test + @DisplayName("should delete todo by id when it present in db") + void delete_whenPresent() throws Exception { + String id = "cd3d29fe-6991-4cb3-992d-cef17e91decb"; + + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.delete("/todo/{id}", id)) + .andExpect(status().isOk()) + .andReturn(); + + String response = result.getResponse().getContentAsString(); + + String expectedMessage = String.format("ToDo with id: %s has been deleted", id); + + assertThat(response).isEqualTo(expectedMessage); + } + + @Test + @DisplayName("should return error of deleting todo when id not present in db") + void delete_whenNotPresent() throws Exception { + String id = "859b3265-ccc7-4480-bb9a-cf1c7026ed39"; + + String expectedJson = """ + { + "status":404, + "error":"ToDo with id 859b3265-ccc7-4480-bb9a-cf1c7026ed39 not found", + "path":"/todo/859b3265-ccc7-4480-bb9a-cf1c7026ed39" + }"""; + + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.delete("/todo/{id}", id)) + .andExpect(status().isNotFound()) + .andReturn(); + + JSONAssert.assertEquals(expectedJson, result.getResponse().getContentAsString(), false); + } +} diff --git a/src/test/resources/todo-data-init.sql b/src/test/resources/todo-data-init.sql new file mode 100644 index 00000000..a83ffc30 --- /dev/null +++ b/src/test/resources/todo-data-init.sql @@ -0,0 +1,4 @@ +INSERT INTO todos (id, description, expiration_date, is_done) +VALUES + ('989b3265-ccc7-4480-bb9a-cf1c7026ed39', 'bla bla bla', '2025-07-18 00:00:00.546', false), + ('cd3d29fe-6991-4cb3-992d-cef17e91decb', 'bla2 bla bla', '2025-07-15 00:00:00.000', true); \ No newline at end of file From e265bf38bafd3b2d3ad69f1397175322bb6e2439 Mon Sep 17 00:00:00 2001 From: Sergey Kovalev Date: Mon, 16 Jun 2025 15:01:03 +0300 Subject: [PATCH 08/10] redis caching added. Integration tests fixed. --- .../springtodo/SpringToDoApplication.java | 2 + .../springtodo/config/CacheConfig.java | 41 +++++++++++++++++++ .../service/impl/ToDoServiceImpl.java | 14 +++++++ src/main/resources/application.yaml | 5 ++- .../ToDoControllerIntegrationTest.java | 13 ++++++ 5 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/emobile/springtodo/config/CacheConfig.java diff --git a/src/main/java/com/emobile/springtodo/SpringToDoApplication.java b/src/main/java/com/emobile/springtodo/SpringToDoApplication.java index 41980514..0a6a54a3 100644 --- a/src/main/java/com/emobile/springtodo/SpringToDoApplication.java +++ b/src/main/java/com/emobile/springtodo/SpringToDoApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; @SpringBootApplication +@EnableCaching public class SpringToDoApplication { public static void main(String[] args) { diff --git a/src/main/java/com/emobile/springtodo/config/CacheConfig.java b/src/main/java/com/emobile/springtodo/config/CacheConfig.java new file mode 100644 index 00000000..216eedb3 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/config/CacheConfig.java @@ -0,0 +1,41 @@ +package com.emobile.springtodo.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +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; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; + +import java.time.Duration; + +@Configuration +@EnableCaching +public class CacheConfig { + + @Bean + public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + GenericJackson2JsonRedisSerializer serializer = + new GenericJackson2JsonRedisSerializer(objectMapper); + + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(serializer) + ) + .entryTtl(Duration.ofMinutes(10)) + .disableCachingNullValues(); + + return RedisCacheManager.builder(connectionFactory) + .cacheDefaults(config) + .build(); + } +} diff --git a/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java b/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java index 8a2bebd9..197be92c 100644 --- a/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java +++ b/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java @@ -8,6 +8,10 @@ import com.emobile.springtodo.repository.ToDoRepository; import com.emobile.springtodo.service.ToDoService; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; import org.springframework.stereotype.Service; import java.util.List; @@ -21,6 +25,7 @@ public class ToDoServiceImpl implements ToDoService { @Override + @Cacheable(value = "todos", key = "#id") public ToDoResponseDto findById(String id) { return toDoRepository.findById(UUID.fromString(id)) .map(toDoMapper::fromEntityToResponseDto) @@ -28,6 +33,7 @@ public ToDoResponseDto findById(String id) { } @Override + @Cacheable(value = "todosList", key = "'pageSize:' + #pageSize + ':pageNumber:' + #pageNumber") public List findAll(int pageSize, int pageNumber) { return toDoRepository.findAll(pageSize, pageNumber) .stream() @@ -36,6 +42,8 @@ public List findAll(int pageSize, int pageNumber) { } @Override + @CachePut(value = "todos", key = "#result.id") + @CacheEvict(value = "todosList", allEntries = true) public ToDoResponseDto save(ToDoRequestDto toDoRequestDto) { ToDo forSave = toDoMapper.fromRequestToEntity(toDoRequestDto); ToDo saved = toDoRepository.save(forSave); @@ -43,6 +51,8 @@ public ToDoResponseDto save(ToDoRequestDto toDoRequestDto) { } @Override + @CachePut(value = "todos", key = "#result.id") + @CacheEvict(value = "todosList", allEntries = true) public ToDoResponseDto update(ToDoRequestDto toDoRequestDto, String id) { ToDo forUpdate = toDoMapper.fromRequestToEntity(toDoRequestDto); ToDo updated = toDoRepository.update(forUpdate, UUID.fromString(id)); @@ -50,6 +60,10 @@ public ToDoResponseDto update(ToDoRequestDto toDoRequestDto, String id) { } @Override + @Caching(evict = { + @CacheEvict(value = "todos", key = "#id"), + @CacheEvict(value = "todosList", allEntries = true) + }) public String delete(String id) { toDoRepository.delete(UUID.fromString(id)); return String.format("ToDo with id: %s has been deleted", id); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index fbf7c5e4..13f121cd 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -26,4 +26,7 @@ spring: data: redis: host: localhost - port: 6379 \ No newline at end of file + port: 6379 + jedis: + pool: + time-between-eviction-runs: 10m \ No newline at end of file diff --git a/src/test/java/com/emobile/springtodo/integration/ToDoControllerIntegrationTest.java b/src/test/java/com/emobile/springtodo/integration/ToDoControllerIntegrationTest.java index 70f19581..2c741964 100644 --- a/src/test/java/com/emobile/springtodo/integration/ToDoControllerIntegrationTest.java +++ b/src/test/java/com/emobile/springtodo/integration/ToDoControllerIntegrationTest.java @@ -11,12 +11,15 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.http.MediaType; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -37,15 +40,25 @@ public class ToDoControllerIntegrationTest { private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; + @Autowired + private RedisConnectionFactory redisConnectionFactory; + @Container + @ServiceConnection + static GenericContainer redisContainer = new GenericContainer<>("redis:8.0.2") + .withExposedPorts(6379); @Container @ServiceConnection static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer<>("postgres:16.0"); + @BeforeEach void setUp() { objectMapper.registerModule(new JavaTimeModule()); + try (RedisConnection connection = redisConnectionFactory.getConnection()) { + connection.flushAll(); + } } @Test From 6f1b332422661b72af925875b1b7bd46891e6baa Mon Sep 17 00:00:00 2001 From: Sergey Kovalev Date: Mon, 16 Jun 2025 18:25:33 +0300 Subject: [PATCH 09/10] swagger and metrics added. --- pom.xml | 5 + .../springtodo/config/SwaggerConfig.java | 18 ++ .../springtodo/controller/ToDoController.java | 5 +- .../springtodo/metric/ToDoMetrics.java | 22 ++ .../springtodo/repository/ToDoRepository.java | 1 + .../repository/impl/ToDoRepositoryImpl.java | 11 + .../service/impl/ToDoServiceImpl.java | 1 - .../springtodo/swagger/IToDoController.java | 204 ++++++++++++++++++ src/main/resources/application.yaml | 26 ++- 9 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/emobile/springtodo/config/SwaggerConfig.java create mode 100644 src/main/java/com/emobile/springtodo/metric/ToDoMetrics.java create mode 100644 src/main/java/com/emobile/springtodo/swagger/IToDoController.java diff --git a/pom.xml b/pom.xml index a7362dcf..90900ba2 100644 --- a/pom.xml +++ b/pom.xml @@ -84,6 +84,11 @@ org.springframework.boot spring-boot-starter-data-jdbc + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.8 + org.springframework.boot diff --git a/src/main/java/com/emobile/springtodo/config/SwaggerConfig.java b/src/main/java/com/emobile/springtodo/config/SwaggerConfig.java new file mode 100644 index 00000000..90d70984 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/config/SwaggerConfig.java @@ -0,0 +1,18 @@ +package com.emobile.springtodo.config; + +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; + +@Configuration +public class SwaggerConfig { + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("ToDo API") + .version("1.0") + .description("API для управления списком дел")); + } +} diff --git a/src/main/java/com/emobile/springtodo/controller/ToDoController.java b/src/main/java/com/emobile/springtodo/controller/ToDoController.java index 7f503586..cebe3266 100644 --- a/src/main/java/com/emobile/springtodo/controller/ToDoController.java +++ b/src/main/java/com/emobile/springtodo/controller/ToDoController.java @@ -3,6 +3,7 @@ import com.emobile.springtodo.dto.request.ToDoRequestDto; import com.emobile.springtodo.dto.response.ToDoResponseDto; import com.emobile.springtodo.service.ToDoService; +import com.emobile.springtodo.swagger.IToDoController; import jakarta.validation.constraints.Positive; import lombok.RequiredArgsConstructor; import org.hibernate.validator.constraints.UUID; @@ -20,10 +21,10 @@ import java.util.List; @RestController -@Validated @RequestMapping("/todo") @RequiredArgsConstructor -public class ToDoController { +@Validated +public class ToDoController implements IToDoController { private final ToDoService toDoService; @GetMapping("/{id}") diff --git a/src/main/java/com/emobile/springtodo/metric/ToDoMetrics.java b/src/main/java/com/emobile/springtodo/metric/ToDoMetrics.java new file mode 100644 index 00000000..d2390d99 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/metric/ToDoMetrics.java @@ -0,0 +1,22 @@ +package com.emobile.springtodo.metric; + +import com.emobile.springtodo.repository.ToDoRepository; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.stereotype.Component; + +@Component +public class ToDoMetrics { + private final MeterRegistry meterRegistry; + private final ToDoRepository toDoRepository; + + public ToDoMetrics(MeterRegistry meterRegistry, ToDoRepository toDoRepository) { + this.meterRegistry = meterRegistry; + this.toDoRepository = toDoRepository; + + Gauge.builder("todo.tasks.done.count", + () -> toDoRepository.countByDone(true)) // Предполагается, что такой метод есть в репозитории + .description("Количество выполненных задач (done = true)") + .register(meterRegistry); + } +} \ No newline at end of file diff --git a/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java b/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java index 95411334..8fe349fa 100644 --- a/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java +++ b/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java @@ -12,4 +12,5 @@ public interface ToDoRepository { ToDo save(ToDo todo); ToDo update(ToDo todo, UUID id); void delete(UUID id); + int countByDone(boolean done); } diff --git a/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java b/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java index 118c3138..9de89465 100644 --- a/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java +++ b/src/main/java/com/emobile/springtodo/repository/impl/ToDoRepositoryImpl.java @@ -87,4 +87,15 @@ public void delete(UUID id) { throw new ToDoNotFoundException(id); } } + + @Override + public int countByDone(boolean done) { + String query = "SELECT COUNT(*) FROM todos WHERE is_done = ?"; + try { + Integer count = jdbcTemplate.queryForObject(query, Integer.class, done); + return count != null ? count : 0; + } catch (DataAccessException e) { + return 0; + } + } } diff --git a/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java b/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java index 197be92c..8b6d3481 100644 --- a/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java +++ b/src/main/java/com/emobile/springtodo/service/impl/ToDoServiceImpl.java @@ -23,7 +23,6 @@ public class ToDoServiceImpl implements ToDoService { private final ToDoRepository toDoRepository; private final ToDoMapper toDoMapper; - @Override @Cacheable(value = "todos", key = "#id") public ToDoResponseDto findById(String id) { diff --git a/src/main/java/com/emobile/springtodo/swagger/IToDoController.java b/src/main/java/com/emobile/springtodo/swagger/IToDoController.java new file mode 100644 index 00000000..e707fc11 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/swagger/IToDoController.java @@ -0,0 +1,204 @@ +package com.emobile.springtodo.swagger; + +import com.emobile.springtodo.dto.request.ToDoRequestDto; +import com.emobile.springtodo.dto.response.ToDoResponseDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.Positive; +import org.hibernate.validator.constraints.UUID; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +@Tag(name = "ToDo API", description = "API для управления списком дел") +public interface IToDoController { + + @Operation( + summary = "Получение задачи", + + description = "Получение задачи по id", + responses = { + @ApiResponse( + description = "Задача найдена", + responseCode = "200", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject( + value = """ + { + "id": "989b3265-ccc7-4480-bb9a-cf1c7026ed39", + "description": "bla bla bla", + "expirationDate": "2025-07-18T00:00:00.546", + "done": false + } + """ + ) + } + + )), + @ApiResponse( + description = "Задача не найдена", + responseCode = "404", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject( + value = """ + { + "timestamp": "2023-10-31T18:09:02.598535700Z" + "status":404, + "error":"ToDo with id 859b3265-ccc7-4480-bb9a-cf1c7026ed39 not found", + "path":"/todo/859b3265-ccc7-4480-bb9a-cf1c7026ed39" + } + """ + ) + } + )) + } + ) + ToDoResponseDto findById(@UUID @PathVariable String id); + + @Operation( + summary = "Получение всего списка дел", + description = "Получение всего списка дел с пагинацией", + responses = { + @ApiResponse( + description = "Список дел получен", + responseCode = "200", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject( + value = """ + [ + { + "id":"989b3265-ccc7-4480-bb9a-cf1c7026ed39", + "description":"bla bla bla", + "expirationDate":"2025-07-18T00:00:00.546", + "done":false + }, + { + "id":"cd3d29fe-6991-4cb3-992d-cef17e91decb", + "description":"bla2 bla bla", + "expirationDate":"2025-07-15T00:00:00", + "done":true + } + ]""" + ) + } + )) + } + ) + List findAll(@RequestParam @Positive int pageSize, @RequestParam @Positive int pageNumber); + + @Operation( + summary = "Сохранение новой задачи", + description = "Сохраняет задачу", + responses = { + @ApiResponse( + description = "Задача сохранена", + responseCode = "200", + content = @Content( + examples = { + @ExampleObject( + value = """ + { + "id":"cd3d29fe-6991-4cb3-992d-cef17e91decb", + "description":"Hello World", + "expirationDate":"2025-11-25T12:59:10", + "done":false + }""" + ) + } + )) + } + ) + ToDoResponseDto save(@RequestBody @Validated ToDoRequestDto requestDto); + + @Operation( + summary = "Обновление задачи", + description = "Обновление задачи по id", + responses = { + @ApiResponse( + description = "Задача обновлена", + responseCode = "200", + content = @Content( + examples = { + @ExampleObject( + value = """ + { + "id":"cd3d29fe-6991-4cb3-992d-cef17e91decb", + "description":"Hello World", + "expirationDate":"2025-11-25T12:59:10", + "done":false + }""" + ) + } + + )), + @ApiResponse( + description = "Задача не найдена", + responseCode = "404", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject( + value = """ + { + "timestamp": "2023-10-31T18:09:02.598535700Z" + "status":404, + "error":"ToDo with id 859b3265-ccc7-4480-bb9a-cf1c7026ed39 not found", + "path":"/todo/859b3265-ccc7-4480-bb9a-cf1c7026ed39" + } + """ + ) + } + )) + } + ) + ToDoResponseDto update(@RequestBody @Validated ToDoRequestDto requestDto, @PathVariable @UUID String id); + + @Operation( + summary = "Удаление задачи", + description = "Удаление задачи по id", + responses = { + @ApiResponse( + description = "Задача удалена", + responseCode = "200", + content = @Content( + examples = { + @ExampleObject( + value = "ToDo with id: 859b3265-ccc7-4480-bb9a-cf1c7026ed39 has been deleted" + ) + } + + )), + @ApiResponse( + description = "Задача не найдена", + responseCode = "404", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject( + value = """ + { + "timestamp": "2023-10-31T18:09:02.598535700Z" + "status":404, + "error":"ToDo with id 859b3265-ccc7-4480-bb9a-cf1c7026ed39 not found", + "path":"/todo/859b3265-ccc7-4480-bb9a-cf1c7026ed39" + } + """ + ) + } + )) + } + ) + String delete(@PathVariable @UUID String id); +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 13f121cd..49111eda 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -11,6 +11,9 @@ spring: hikari: schema: public + autoconfigure: + exclude: + jpa: database: postgresql database-platform: org.hibernate.dialect.PostgreSQLDialect @@ -29,4 +32,25 @@ spring: port: 6379 jedis: pool: - time-between-eviction-runs: 10m \ No newline at end of file + time-between-eviction-runs: 10m + +springdoc: + api-docs: + enabled: true + swagger-ui: + enabled: true + +management: + endpoints: + web: + exposure: + include: health, metrics, info + endpoint: + health: + show-details: always + +info: + app: + name: SpringToDo + version: 1.0.0 + description: week3 1st task \ No newline at end of file From bec3a7afb1fbd6a50b00e1d0084b3c0eb406628d Mon Sep 17 00:00:00 2001 From: Sergey Kovalev Date: Thu, 24 Jul 2025 11:45:56 +0300 Subject: [PATCH 10/10] little refactoring. --- pom.xml | 30 ++++++++++++++++++- .../springtodo/config/CacheConfig.java | 3 +- .../springtodo/metric/ToDoMetrics.java | 4 --- .../SpringToDoApplicationTests.java | 13 -------- .../ToDoControllerIntegrationTest.java | 4 +-- .../service/impl/ToDoServiceImplTest.java | 4 ++- 6 files changed, 35 insertions(+), 23 deletions(-) delete mode 100644 src/test/java/com/emobile/springtodo/SpringToDoApplicationTests.java diff --git a/pom.xml b/pom.xml index 90900ba2..8bbdf4ee 100644 --- a/pom.xml +++ b/pom.xml @@ -141,11 +141,39 @@ + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + prepare-agent + + prepare-agent + + + + report + test + + report + + + ${project.build.directory}/jacoco-report + + XML + HTML + + + + + + org.springframework.boot spring-boot-maven-plugin - \ No newline at end of file diff --git a/src/main/java/com/emobile/springtodo/config/CacheConfig.java b/src/main/java/com/emobile/springtodo/config/CacheConfig.java index 216eedb3..18bbdf94 100644 --- a/src/main/java/com/emobile/springtodo/config/CacheConfig.java +++ b/src/main/java/com/emobile/springtodo/config/CacheConfig.java @@ -29,8 +29,7 @@ public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith( - RedisSerializationContext.SerializationPair.fromSerializer(serializer) - ) + RedisSerializationContext.SerializationPair.fromSerializer(serializer)) .entryTtl(Duration.ofMinutes(10)) .disableCachingNullValues(); diff --git a/src/main/java/com/emobile/springtodo/metric/ToDoMetrics.java b/src/main/java/com/emobile/springtodo/metric/ToDoMetrics.java index d2390d99..3d2d233a 100644 --- a/src/main/java/com/emobile/springtodo/metric/ToDoMetrics.java +++ b/src/main/java/com/emobile/springtodo/metric/ToDoMetrics.java @@ -7,12 +7,8 @@ @Component public class ToDoMetrics { - private final MeterRegistry meterRegistry; - private final ToDoRepository toDoRepository; public ToDoMetrics(MeterRegistry meterRegistry, ToDoRepository toDoRepository) { - this.meterRegistry = meterRegistry; - this.toDoRepository = toDoRepository; Gauge.builder("todo.tasks.done.count", () -> toDoRepository.countByDone(true)) // Предполагается, что такой метод есть в репозитории diff --git a/src/test/java/com/emobile/springtodo/SpringToDoApplicationTests.java b/src/test/java/com/emobile/springtodo/SpringToDoApplicationTests.java deleted file mode 100644 index 3114a50b..00000000 --- a/src/test/java/com/emobile/springtodo/SpringToDoApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.emobile.springtodo; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SpringToDoApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/com/emobile/springtodo/integration/ToDoControllerIntegrationTest.java b/src/test/java/com/emobile/springtodo/integration/ToDoControllerIntegrationTest.java index 2c741964..4faf891a 100644 --- a/src/test/java/com/emobile/springtodo/integration/ToDoControllerIntegrationTest.java +++ b/src/test/java/com/emobile/springtodo/integration/ToDoControllerIntegrationTest.java @@ -35,7 +35,7 @@ @AutoConfigureMockMvc @Transactional @Sql({"/todo-data-init.sql"}) -public class ToDoControllerIntegrationTest { +class ToDoControllerIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired @@ -57,7 +57,7 @@ public class ToDoControllerIntegrationTest { void setUp() { objectMapper.registerModule(new JavaTimeModule()); try (RedisConnection connection = redisConnectionFactory.getConnection()) { - connection.flushAll(); + connection.execute("FLUSHDB"); } } diff --git a/src/test/java/com/emobile/springtodo/service/impl/ToDoServiceImplTest.java b/src/test/java/com/emobile/springtodo/service/impl/ToDoServiceImplTest.java index 1d457b76..a8d6cd75 100644 --- a/src/test/java/com/emobile/springtodo/service/impl/ToDoServiceImplTest.java +++ b/src/test/java/com/emobile/springtodo/service/impl/ToDoServiceImplTest.java @@ -183,7 +183,9 @@ void delete_whenNotPresent() { doThrow(new ToDoNotFoundException(UUID.fromString(id))) .when(repository).delete(any(UUID.class)); - assertThatThrownBy(() -> repository.delete(UUID.fromString(id))) + UUID uuid = UUID.fromString(id); + + assertThatThrownBy(() -> repository.delete(uuid)) .isInstanceOf(ToDoNotFoundException.class) .hasMessageContaining(String.format("ToDo with id %s not found", id));