diff --git a/pom.xml b/pom.xml index 03bc446d..4a67ba81 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent 3.3.5 - + com.emobile SpringToDo @@ -21,7 +21,21 @@ org.springframework.boot - spring-boot-starter + spring-boot-starter-actuator + + + io.micrometer + micrometer-core + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + + + + org.springframework.boot + spring-boot-starter-web @@ -29,6 +43,77 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-testcontainers + test + + + org.junit.vintage + junit-vintage-engine + 5.7.0 + + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.skyscreamer + jsonassert + 1.5.0 + test + + + org.junit.platform + junit-platform-launcher + test + + + org.projectlombok + lombok + true + + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-database-postgresql + + + org.postgresql + postgresql + runtime + + + org.testcontainers + testcontainers + 2.0.1 + test + + + + org.testcontainers + junit-jupiter + 1.20.1 + test + + + + org.testcontainers + postgresql + @@ -37,6 +122,32 @@ org.springframework.boot spring-boot-maven-plugin + + org.flywaydb + flyway-maven-plugin + 10.0.0 + + jdbc:postgresql://localhost:5432/todo_db + postgres + 1 + false + + + + org.postgresql + postgresql + 42.7.4 + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + true + + diff --git a/src/main/java/com/emobile/springtodo/SpringToDoApplication.java b/src/main/java/com/emobile/springtodo/SpringToDoApplication.java index 41980514..52d454a4 100644 --- a/src/main/java/com/emobile/springtodo/SpringToDoApplication.java +++ b/src/main/java/com/emobile/springtodo/SpringToDoApplication.java @@ -2,8 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.data.web.config.EnableSpringDataWebSupport; + +import static org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO; @SpringBootApplication +@EnableCaching +@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO) public class SpringToDoApplication { public static void main(String[] args) { 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..407fc114 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/controller/TodoController.java @@ -0,0 +1,65 @@ +package com.emobile.springtodo.controller; + + +import com.emobile.springtodo.model.Todo; +import com.emobile.springtodo.service.ToDoService; +import com.emobile.springtodo.swagger.ITodoController; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@CrossOrigin +public class TodoController implements ITodoController { + + private final ToDoService toDoService; + + + @Override + public void addToDo(Todo toDo) { + + + toDoService.createTodo(toDo); + + } + + @Override + public void updateToDoById(Todo todo, Long id) { + toDoService.updateToDoById(todo, id); + } + + + @Override + public List findAllToDo() { + return toDoService.findAllToDo(); + } + + @Override + public ResponseEntity> getToDo(Todo toDo, int size, int page) { + Pageable pageable = PageRequest.of(page, size); + return new ResponseEntity<>(toDoService.findAllToDoByFilter(toDo, pageable),HttpStatus.OK); + } + + + @Override + public void deleteToDoById(Long id) { + toDoService.deleteById(id); + } + + @Override + public void deleteAllToDo() { + toDoService.deleteAll(); + } + + + + + +} diff --git a/src/main/java/com/emobile/springtodo/exception/AppError.java b/src/main/java/com/emobile/springtodo/exception/AppError.java new file mode 100644 index 00000000..dc8b8084 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/exception/AppError.java @@ -0,0 +1,16 @@ +package com.emobile.springtodo.exception; + +import lombok.Data; + +@Data +public class AppError { + + private int httpStatus; + private String message; + + public AppError(int httpStatus, String message) { + this.httpStatus = httpStatus; + this.message = message; + } + +} diff --git a/src/main/java/com/emobile/springtodo/exception/GlobalExceptionHandler.java b/src/main/java/com/emobile/springtodo/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..dce73a26 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/exception/GlobalExceptionHandler.java @@ -0,0 +1,18 @@ +package com.emobile.springtodo.exception; + + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(TodoException.class) + public ResponseEntity todoException(TodoException ex) + { + return new ResponseEntity<>(new AppError(HttpStatus.NOT_FOUND.value(),ex.getMessage()),HttpStatus.BAD_REQUEST); + } + +} diff --git a/src/main/java/com/emobile/springtodo/exception/TodoException.java b/src/main/java/com/emobile/springtodo/exception/TodoException.java new file mode 100644 index 00000000..e8e44a9a --- /dev/null +++ b/src/main/java/com/emobile/springtodo/exception/TodoException.java @@ -0,0 +1,9 @@ +package com.emobile.springtodo.exception; + +public class TodoException extends RuntimeException{ + + public TodoException(String message) + { + super(message); + } +} 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..0382a218 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/mapper/TodoMapper.java @@ -0,0 +1,24 @@ +package com.emobile.springtodo.mapper; + +import com.emobile.springtodo.model.Todo; +import com.emobile.springtodo.model.TodoPriority; +import com.emobile.springtodo.model.TodoStatus; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class TodoMapper implements RowMapper { + @Override + public Todo mapRow(ResultSet rs, int rowNum) throws SQLException { + Todo toDo = new Todo(); + toDo.setId(rs.getLong("id")); + toDo.setToDoTitle((rs.getString("todo_title"))); + toDo.setDescription(rs.getString("description")); + toDo.setToDoPriority(TodoPriority.valueOf(rs.getString("todo_priority"))); + toDo.setToDoStatus(TodoStatus.valueOf(rs.getString("todo_status"))); + return toDo; + } + + +} \ No newline at end of file diff --git a/src/main/java/com/emobile/springtodo/model/Todo.java b/src/main/java/com/emobile/springtodo/model/Todo.java new file mode 100644 index 00000000..fcfb943f --- /dev/null +++ b/src/main/java/com/emobile/springtodo/model/Todo.java @@ -0,0 +1,17 @@ +package com.emobile.springtodo.model; + + +import lombok.*; + +import java.io.Serializable; + +@Data +public class Todo implements Serializable { + + private Long id; + private String toDoTitle; + private String description; + private TodoPriority toDoPriority; + private TodoStatus toDoStatus; + private String comment; +} \ No newline at end of file diff --git a/src/main/java/com/emobile/springtodo/model/TodoPriority.java b/src/main/java/com/emobile/springtodo/model/TodoPriority.java new file mode 100644 index 00000000..0fe2a76d --- /dev/null +++ b/src/main/java/com/emobile/springtodo/model/TodoPriority.java @@ -0,0 +1,8 @@ +package com.emobile.springtodo.model; + +public enum TodoPriority { + HIGH,MEDIUM,LOW +} + + + diff --git a/src/main/java/com/emobile/springtodo/model/TodoStatus.java b/src/main/java/com/emobile/springtodo/model/TodoStatus.java new file mode 100644 index 00000000..2fabf161 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/model/TodoStatus.java @@ -0,0 +1,6 @@ +package com.emobile.springtodo.model; + +public enum TodoStatus { + PENDING,IN_PROGRESS, + COMPLETED +} \ No newline at end of file diff --git a/src/main/java/com/emobile/springtodo/repository/JdbcTodoRepository.java b/src/main/java/com/emobile/springtodo/repository/JdbcTodoRepository.java new file mode 100644 index 00000000..44e5d9ae --- /dev/null +++ b/src/main/java/com/emobile/springtodo/repository/JdbcTodoRepository.java @@ -0,0 +1,142 @@ +package com.emobile.springtodo.repository; + +import com.emobile.springtodo.exception.TodoException; +import com.emobile.springtodo.mapper.TodoMapper; +import com.emobile.springtodo.model.Todo; +import com.emobile.springtodo.model.TodoStatus; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.dao.DataAccessException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +@Repository +public class JdbcTodoRepository implements TodoRepository { + + private final JdbcTemplate jdbcTemplate; + + private final AtomicInteger todoServiceCounter; + + public JdbcTodoRepository(JdbcTemplate jdbcTemplate, MeterRegistry registry) { + this.jdbcTemplate = jdbcTemplate; + todoServiceCounter = new AtomicInteger(0); + Gauge.builder("Completed tasks", todoServiceCounter::get).register(registry); + } + + @Override + public void saveToDo(Todo toDo) { + if (toDo.getToDoTitle() == null || toDo.getDescription() == null || toDo.getToDoPriority() == null) { + throw new TodoException("Please make sure description,title,priority are not empty"); + } + + if (toDo.getToDoStatus() != null && toDo.getToDoStatus() == TodoStatus.IN_PROGRESS || toDo.getToDoStatus() == TodoStatus.COMPLETED) { + throw new TodoException("You are not allowed to set status to 'COMPLETED' or 'IN_PROGRESS'. These parameters are set by the performer using 'updateToDoById' function to inform the admin about his/her progress and 'PENDING' is set by default"); + } + toDo.setToDoStatus(TodoStatus.PENDING); + + jdbcTemplate.update("INSERT INTO todo (todo_title,description,todo_priority,todo_status) VALUES(?,?,?,?)", + toDo.getToDoTitle(), + toDo.getDescription(), + toDo.getToDoPriority().name(), + toDo.getToDoStatus().name()); + } + + + @Override + public void updateToDoById(Todo toDo, Long toDoId) { + try { + + String newStatus = toDo.getToDoStatus().name(); + String currentStatus = getStatusByIdQuery(toDoId); + + int updatedRows = jdbcTemplate.update("UPDATE todo SET todo_status = ?::todo_status_enum WHERE id = ?", newStatus, toDoId); + + + if (updatedRows > 0 && "COMPLETED".equals(currentStatus) && !"COMPLETED".equals(newStatus)) { + + todoServiceCounter.decrementAndGet(); + + } else if (updatedRows > 0 && !"COMPLETED".equals(currentStatus) && "COMPLETED".equals(newStatus)) { + + todoServiceCounter.incrementAndGet(); + } + + if ("PENDING".equals(newStatus)) { + throw new TodoException("If todo is incomplete you can set status to IN_PROGRESS"); + } + + if (newStatus.equals(currentStatus)) { + throw new TodoException("Status already set to " + newStatus); + } + + + } catch (DataAccessException e) { + throw new TodoException("Todo for id " + toDoId + " is not found"); + } + + } + + private String getStatusByIdQuery(Long toDoId) { + return jdbcTemplate.queryForObject("SELECT todo_status FROM todo WHERE id = ?", String.class, toDoId); + } + + @Override + public List findAllToDo() { + List toDoList = jdbcTemplate.query("SELECT * FROM todo", new TodoMapper()); + if (toDoList.isEmpty()) { + throw new TodoException("No todos found"); + } + return toDoList; + } + + @Override + public Page findAllToDoByFilter(Todo todo, Pageable pageable) { + + int total = jdbcTemplate.queryForObject( + + "SELECT count(1) AS row_count FROM todo WHERE " + "todo_status = ?::todo_status_enum OR todo_priority = ?::todo_priority_enum", + + Integer.class, + + todo.getToDoStatus() != null ? todo.getToDoStatus().name() : null, + + todo.getToDoPriority() != null ? todo.getToDoPriority().name() : null); + + List toDoList = jdbcTemplate.query("SELECT * FROM todo WHERE " + "todo_status = ?::todo_status_enum OR todo_priority = ?::todo_priority_enum" + " LIMIT ? OFFSET ?", new TodoMapper(), todo.getToDoStatus() != null ? todo.getToDoStatus().name() : null, todo.getToDoPriority() != null ? todo.getToDoPriority().name() : null, pageable.getPageSize(), pageable.getOffset()); + + return new PageImpl<>(toDoList, pageable, total); + } + + @Override + public void deleteById(Long id) { + try { + + String status = getStatusByIdQuery(id); + int result = jdbcTemplate.update("DELETE FROM todo WHERE id=?", id); + + if (result > 0 && "COMPLETED".equals(status)) { + todoServiceCounter.decrementAndGet(); + } + } catch (DataAccessException e) { + throw new TodoException("todo by id " + id + " doesn't exist or has been deleted"); + } + } + + + @Override + public void deleteAllToDo() { + + jdbcTemplate.update("TRUNCATE todo"); + + todoServiceCounter.getAndSet(0); + + } + + +} \ 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 new file mode 100644 index 00000000..acb2951f --- /dev/null +++ b/src/main/java/com/emobile/springtodo/repository/TodoRepository.java @@ -0,0 +1,25 @@ +package com.emobile.springtodo.repository; + +import com.emobile.springtodo.model.Todo; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + + +public interface TodoRepository { + + void saveToDo(Todo toDo); + + void updateToDoById(Todo toDo, Long toDoId); + + Page findAllToDoByFilter(Todo todo, Pageable pageable); + + List findAllToDo(); + + void deleteById(Long id); + + void deleteAllToDo(); + + +} 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..b567a111 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/service/ToDoService.java @@ -0,0 +1,58 @@ +package com.emobile.springtodo.service; + +import com.emobile.springtodo.model.Todo; +import com.emobile.springtodo.repository.JdbcTodoRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.List; + + +@Service +@RequiredArgsConstructor +public class ToDoService { + + + private final JdbcTodoRepository jdbcToDoRepository; + + + + @CacheEvict(value = "todo", allEntries = true) + public void createTodo(Todo toDo) { + + jdbcToDoRepository.saveToDo(toDo); + + } + + @CacheEvict(value = "todo", key = "{#id,#toDo.toDoStatus != null ? #toDo.toDoStatus.name() : 'NULL'}") + public void updateToDoById(Todo toDo, Long id) { + jdbcToDoRepository.updateToDoById(toDo, id); + } + + @Cacheable(value = "todo", key = "{#toDo.toDoPriority != null ? #toDo.toDoPriority.name() : 'NULL',#toDo.toDoStatus != null ? #toDo.toDoStatus.name() : 'NULL',#pageable.pageNumber,#pageable.pageSize,#pageable.sort != null ? #pageable.sort.toString() : 'NOSORT' }") + public Page findAllToDoByFilter(Todo toDo, Pageable pageable) { + return jdbcToDoRepository.findAllToDoByFilter(toDo, pageable); + } + + public List findAllToDo() { + return jdbcToDoRepository.findAllToDo(); + } + + @CacheEvict(value = "todo", key = "#id") + public void deleteById(Long id) { + jdbcToDoRepository.deleteById(id); + } + + + @CacheEvict(value = "todo", allEntries = true) + public void deleteAll() { + jdbcToDoRepository.deleteAllToDo(); + } + + +} + 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..b4224c33 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/swagger/ITodoController.java @@ -0,0 +1,65 @@ +package com.emobile.springtodo.swagger; + +import com.emobile.springtodo.model.Todo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +@RequestMapping("/api") +@Tag(name = "Todo controller", description = "This controller is for accessing the Todo functions") +public interface ITodoController { + + + @Operation(summary = "Adds todo with status pending by default assuming the admin is creating todo for the performer",parameters ={ + @Parameter(name = "ToDo",description = "saves the info of Todo")}, + responses = { @ApiResponse(responseCode = "200", description = "If filling all the blank with optional comment is successful"), + @ApiResponse(responseCode = "400", description = "throws TodoException if description,title,priority are empty"), + @ApiResponse(responseCode = "400",description = "throws TodoException assuming the admin is not allowed to set the status of todo to 'COMPLETED' or 'IN_PROGRESS' which only should be done by the performer when he/she updates todo")}) + @PostMapping("/add_todo") + void addToDo(@RequestBody Todo toDo); + + + @Operation(summary = "Updates the status of todo assuming the performer is informing the admin about his/her status",parameters = { + @Parameter(name = "todo",description = "it saves the updated status of todo"), + @Parameter(name = "id",description = "searches for the id of todo that requires update")}, + responses = {@ApiResponse(responseCode = "200",description = "if status is update to different status like COMPLETED or IN_PROGRESS and also the metric gets incremented if COMPLETED status is used")}) + @ApiResponse(responseCode = "404",description = "throws TodoException if the searched id does not exists") + @ApiResponse(responseCode = "404",description = "throws TodoException if the status is updated like the previous one") + @PutMapping("/update_todo_by_id/{id}") + void updateToDoById(@RequestBody Todo todo, @PathVariable Long id); + + @Operation(summary = "Views the list of saved todos", + responses = {@ApiResponse(responseCode = "200",content = @Content(mediaType = "application/json", schema = @Schema(implementation = Todo.class)), description = "returns list of todos")}) + @ApiResponse(responseCode = "404",description = "throws TodoException if no saved todo exits") + @GetMapping("/view_todo_list") + List findAllToDo(); + + + @Operation(summary = "Views filtered list of todos",parameters = { + @Parameter(name = "todo",description = "used to work as filter by wither by it's status or priority"), + @Parameter(name = "size",description = "size limits results to be viewed in each page and it is set by default to 10"), + @Parameter(name = "page",description = "pages displays the amount of results that was set by the parameter size else it displays the whole results that was filtered by the parameter todo and here it is set by default to 0") + },responses = {@ApiResponse(responseCode = "200",content = @Content(mediaType = "application/json", schema = @Schema(implementation = Todo.class))), + @ApiResponse(responseCode = "500",description = "This response code is displayed if todo doesn't exits"), + }) + @GetMapping("/view_todo_list_filterBy") + ResponseEntity> getToDo(Todo toDo, @RequestParam(value = "size", defaultValue = "10") int size, @RequestParam(value = "page", defaultValue = "0") int page); + + + @Operation(summary = "Deletes todo by it's id",parameters = @Parameter(name = "id",description = "searches for id in todo and deletes it and also the metric gets decrement if todo with status 'COMPLETED' is deleted"), + responses = {@ApiResponse(responseCode = "200",description = "returns 200 if id was existed and it got deleted"), + @ApiResponse(responseCode = "404",description = "throws TodoException if id is already deleted or does not exist")}) + @DeleteMapping("/delete_by_id/{id}") + void deleteToDoById(@PathVariable Long id); + + @Operation(summary = "Deletes all todos",responses = @ApiResponse(responseCode = "200",description = "returns 200 if deleteAll operation was done successfully")) + @DeleteMapping("/delete_all") + void deleteAllToDo(); +} diff --git a/src/main/resources/actualCreateTodo.json b/src/main/resources/actualCreateTodo.json new file mode 100644 index 00000000..7b9e289d --- /dev/null +++ b/src/main/resources/actualCreateTodo.json @@ -0,0 +1,6 @@ +{ + "toDoStatus": "IN_PROGRESS", + "toDoTitle": "JAVADEV", + "description": "Creating app", + "toDoPriority": "MEDIUM" +} diff --git a/src/main/resources/actualUpdateTodoById.json b/src/main/resources/actualUpdateTodoById.json new file mode 100644 index 00000000..0eb6e1fb --- /dev/null +++ b/src/main/resources/actualUpdateTodoById.json @@ -0,0 +1,3 @@ +{ + "toDoStatus" : "PENDING" +} \ No newline at end of file diff --git a/src/main/resources/actual_view_todo_list.json b/src/main/resources/actual_view_todo_list.json new file mode 100644 index 00000000..db24da9e --- /dev/null +++ b/src/main/resources/actual_view_todo_list.json @@ -0,0 +1,10 @@ +[ + { + "id": 27, + "toDoTitle": "JAVADEV", + "description": "Creating app", + "toDoPriority": "MEDIUM", + "toDoStatus": "PENDING", + "comment": null + } +] \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 00000000..63bea3e0 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,36 @@ +spring: + application: + name: SpringToDo + + + + datasource: + url: jdbc:postgresql://localhost:5432/todo_db + username: postgres + password: 1 + + + + + flyway: + enabled: true + url: jdbc:postgresql://localhost:5432/todo_db + user: postgres + password: 1 + validate-migration-naming: true + baseline-on-migrate: true + + data: + redis: + host: localhost + port: 6379 + + cache: + type: redis +management: + endpoints: + web: + exposure: + include: "metrics,health" + + diff --git a/src/main/resources/db/migration/V1__Create_ToDo.sql b/src/main/resources/db/migration/V1__Create_ToDo.sql new file mode 100644 index 00000000..dbf4ae3a --- /dev/null +++ b/src/main/resources/db/migration/V1__Create_ToDo.sql @@ -0,0 +1,19 @@ +CREATE TYPE todo_priority_enum AS ENUM ('HIGH','MEDIUM','LOW'); +CREATE TYPE todo_status_enum AS ENUM ('PENDING','IN_PROGRESS','COMPLETED'); +CREATE CAST (varchar AS todo_priority_enum) WITH INOUT AS IMPLICIT; +CREATE CAST (varchar AS todo_status_enum) WITH INOUT AS IMPLICIT; +create table todo +( + id serial primary key, + todo_title varchar(255), + description varchar(255), + todo_priority todo_priority_enum, + todo_status todo_status_enum, + comment varchar(255) +); + + +delete from todo where todo_status = 'PENDING' + + + diff --git a/src/main/resources/expectedCreateTodo.json b/src/main/resources/expectedCreateTodo.json new file mode 100644 index 00000000..370f769d --- /dev/null +++ b/src/main/resources/expectedCreateTodo.json @@ -0,0 +1,6 @@ +{ + "toDoTitle": "JAVADEV", + "description": "Creating app", + "toDoPriority": "MEDIUM", + "toDoStatus": "IN_PROGRESS" +} diff --git a/src/main/resources/expectedUpdateTodoById.json b/src/main/resources/expectedUpdateTodoById.json new file mode 100644 index 00000000..0eb6e1fb --- /dev/null +++ b/src/main/resources/expectedUpdateTodoById.json @@ -0,0 +1,3 @@ +{ + "toDoStatus" : "PENDING" +} \ No newline at end of file diff --git a/src/main/resources/expected_view_todo_list.json b/src/main/resources/expected_view_todo_list.json new file mode 100644 index 00000000..db24da9e --- /dev/null +++ b/src/main/resources/expected_view_todo_list.json @@ -0,0 +1,10 @@ +[ + { + "id": 27, + "toDoTitle": "JAVADEV", + "description": "Creating app", + "toDoPriority": "MEDIUM", + "toDoStatus": "PENDING", + "comment": null + } +] \ No newline at end of file diff --git a/src/test/java/com/emobile/springtodo/SpringToDoApplicationTests.java b/src/test/java/com/emobile/springtodo/SpringTodoApplicationTests.java similarity index 84% rename from src/test/java/com/emobile/springtodo/SpringToDoApplicationTests.java rename to src/test/java/com/emobile/springtodo/SpringTodoApplicationTests.java index 3114a50b..bea412b3 100644 --- a/src/test/java/com/emobile/springtodo/SpringToDoApplicationTests.java +++ b/src/test/java/com/emobile/springtodo/SpringTodoApplicationTests.java @@ -4,7 +4,7 @@ import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class SpringToDoApplicationTests { +class SpringTodoApplicationTests { @Test void contextLoads() { diff --git a/src/test/java/com/emobile/springtodo/json/TodoJsonTests.java b/src/test/java/com/emobile/springtodo/json/TodoJsonTests.java new file mode 100644 index 00000000..7065ace0 --- /dev/null +++ b/src/test/java/com/emobile/springtodo/json/TodoJsonTests.java @@ -0,0 +1,76 @@ +package com.emobile.springtodo.json; + + +import lombok.SneakyThrows; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.Resource; + +@SpringBootTest +public class TodoJsonTests { + + + @Value("classpath:actualCreateToDo.json") + Resource actualCreateToDoJsonFromResourceFile; + + @Value("classpath:expectedCreateTodo.json") + Resource expectedCreateTodoJsonFromResourceFile; + + @Value("classpath:actualUpdateTodoById.json") + Resource actualUpdateTodoByIdJsonFromResourceFile; + + @Value("classpath:expectedUpdateTodoById.json") + Resource expectedUpdateTodoByIdJsonFromResourceFile; + + + @Value("classpath:expected_view_todo_list.json") + Resource expected_view_todo_list_from_resource_file; + + @Value("classpath:actual_view_todo_list.json") + Resource actual_view_todo_list_from_resource_file; + + + + @SneakyThrows + @Test + @DisplayName("Json comparison for create todo test") + public void createTodoJsonTest() { + String actualCreateToDoJson = readFromResourceFile(actualCreateToDoJsonFromResourceFile); + String expectedCreateTodoJson = readFromResourceFile(expectedCreateTodoJsonFromResourceFile); + JSONAssert.assertEquals(expectedCreateTodoJson, actualCreateToDoJson, JSONCompareMode.LENIENT); + } + + @SneakyThrows + @Test + @DisplayName("Json comparison for update status todo by id test") + public void updateStatusTodoByIdJsonTest() { + String actualUpdateTodoByIdJson = readFromResourceFile(actualUpdateTodoByIdJsonFromResourceFile); + String expectedUpdateTodoByIdJson = readFromResourceFile(expectedUpdateTodoByIdJsonFromResourceFile); + JSONAssert.assertEquals(expectedUpdateTodoByIdJson, actualUpdateTodoByIdJson , JSONCompareMode.LENIENT); + } + + @SneakyThrows + @Test + @DisplayName("Json comparison for view todo list test") + public void view_todo_listJsonTest() { + String actual_view_todo_list_json = readFromResourceFile(actual_view_todo_list_from_resource_file); + String expected_view_todo_list_json = readFromResourceFile(expected_view_todo_list_from_resource_file); + JSONAssert.assertEquals(expected_view_todo_list_json, actual_view_todo_list_json , JSONCompareMode.LENIENT); + } + + + + + @SneakyThrows + private String readFromResourceFile(Resource resource) + { + try(var inputStream = resource.getInputStream()) { + return new String(inputStream.readAllBytes()); + } + } + +} diff --git a/src/test/java/com/emobile/springtodo/repository/JdbcTodoRepositoryTest.java b/src/test/java/com/emobile/springtodo/repository/JdbcTodoRepositoryTest.java new file mode 100644 index 00000000..03e24bd3 --- /dev/null +++ b/src/test/java/com/emobile/springtodo/repository/JdbcTodoRepositoryTest.java @@ -0,0 +1,267 @@ +package com.emobile.springtodo.repository; + +import com.emobile.springtodo.exception.TodoException; +import com.emobile.springtodo.model.Todo; +import com.emobile.springtodo.model.TodoPriority; +import com.emobile.springtodo.model.TodoStatus; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.jdbc.Sql; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.Objects; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@Testcontainers +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@DisplayName("Testing the functions of JdbcToDoRepository") +class JdbcTodoRepositoryTest { + + @Container + static PostgreSQLContainer postgres = new PostgreSQLContainer<>(DockerImageName.parse("postgres:12.3")); + + @Autowired + private JdbcTemplate jdbcTemplate; + + private JdbcTodoRepository jdbcToDoRepository; + + private MeterRegistry meterRegistry; + + @BeforeEach + void setUp() { + this.meterRegistry = new SimpleMeterRegistry(); + this.jdbcToDoRepository = new JdbcTodoRepository(jdbcTemplate, meterRegistry); + } + + @Test + void connectionEstablished() { + assertThat(postgres.isCreated()).isTrue(); + assertThat(postgres.isRunning()).isTrue(); + } + + @Test + @DisplayName("Testing saveToDo without exception if not null") + public void saveToDoWithoutExceptionIfNotNullTest() { + + Todo todo = new Todo(); + + todo.setToDoTitle("ToDo"); + + todo.setDescription("Description"); + + todo.setToDoPriority(TodoPriority.HIGH); + + jdbcToDoRepository.saveToDo(todo); + + assertEquals("Description", todo.getDescription()); + + assertDoesNotThrow(() -> jdbcToDoRepository.saveToDo(todo)); + } + + @Test + @DisplayName("Testing saveToDo fails when setting status to completed or in progress") + public void saveToDoFailsWhenSettingStatusToCompletedOrInProgressTest() { + + Todo todo = new Todo(); + + todo.setToDoTitle("ToDo"); + + todo.setDescription("Description"); + + todo.setToDoStatus(TodoStatus.COMPLETED); + + todo.setToDoPriority(TodoPriority.HIGH); + + assertThrows(TodoException.class, () -> jdbcToDoRepository.saveToDo(todo)); + } + + + @Test + @Sql(statements = { + "TRUNCATE TABLE todo RESTART IDENTITY", + "INSERT INTO todo (id,todo_title,description,todo_priority,todo_status) VALUES(1,'JAVA_DEVELOPER','CREATING APP','HIGH','PENDING')", + }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @DisplayName("Testing updateToDoById metric is incremented for status complete") + public void updateToDoByIdIncrementForCompleteTest() { + + Todo todo = new Todo(); + + todo.setToDoTitle("ToDo"); + + todo.setDescription("Description"); + + todo.setToDoStatus(TodoStatus.COMPLETED); + + jdbcToDoRepository.updateToDoById(todo, 1L); + + assertEquals(1, Objects.requireNonNull(meterRegistry.find("Completed tasks").gauge()).value()); + } + + @Test + @Sql(statements = { + "TRUNCATE TABLE todo RESTART IDENTITY", + "INSERT INTO todo (id,todo_title,description,todo_priority,todo_status) VALUES(1,'JAVA_DEVELOPER','CREATING APP','HIGH','COMPLETED')", + }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @DisplayName("Testing updateToDoById metric is decremented if status is changed from complete to in progress") + public void updateToDoByIdDecrementWhenChangingFromCompleteToInProgressTest() { + + Todo toDo = new Todo(); + + toDo.setToDoTitle("ToDo"); + + toDo.setDescription("Description"); + + toDo.setToDoStatus(TodoStatus.IN_PROGRESS); + + assertEquals(0, Objects.requireNonNull(meterRegistry.find("Completed tasks").gauge()).value()); + + assertDoesNotThrow(() -> jdbcToDoRepository.updateToDoById(toDo, 1L)); + } + + + @Test + @Sql(statements = { + "TRUNCATE TABLE todo RESTART IDENTITY", + "INSERT INTO todo (id,todo_title,description,todo_priority,todo_status) VALUES(1,'JAVA_DEVELOPER','CREATING APP','HIGH','COMPLETED')", + }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @DisplayName("Testing updateToDoById should fails if it's setting the same status") + public void updateToDoById_fails_if_status_is_resetting_to_the_similar_status_test() { + + Todo toDo = new Todo(); + + toDo.setToDoTitle("ToDo"); + + toDo.setDescription("Description"); + + toDo.setToDoStatus(TodoStatus.COMPLETED); + + assertEquals(0, Objects.requireNonNull(meterRegistry.find("Completed tasks").gauge()).value()); + + assertThrows(TodoException.class, () -> jdbcToDoRepository.updateToDoById(toDo, 1L)); + } + + + @Test + @Sql(statements = { + "TRUNCATE TABLE todo RESTART IDENTITY", + "INSERT INTO todo (id,todo_title,description,todo_priority,todo_status) VALUES(1,'JAVA_DEVELOPER','CREATING APP','HIGH','COMPLETED')", + }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @DisplayName("Testing updateToDoById fails if id does not exists") + public void updateToDoById_fails_if_id_does_not_exists_test() { + + Todo toDo = new Todo(); + + toDo.setToDoTitle("ToDo"); + + toDo.setDescription("Description"); + + toDo.setToDoStatus(TodoStatus.COMPLETED); + + assertEquals(0, Objects.requireNonNull(meterRegistry.find("Completed tasks").gauge()).value()); + + assertThrows(TodoException.class, () -> jdbcToDoRepository.updateToDoById(toDo, 2L)); + } + + @Test + @DisplayName("Testing findAllToDo fails if data does not exists") + public void findAllToDo_fails_if_data_does_not_exists_test() { + assertThrows(TodoException.class, () -> jdbcToDoRepository.findAllToDo()); + } + + + @Test + @Sql(statements = { + "TRUNCATE TABLE todo RESTART IDENTITY", + "INSERT INTO todo (id,todo_title,description,todo_priority,todo_status) VALUES(1,'JAVA_DEVELOPER','CREATING APP','HIGH','COMPLETED')", + }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @DisplayName("Testing findAllToDo") + public void findAllToDo_succeeds_test() { + assertDoesNotThrow(() -> jdbcToDoRepository.findAllToDo()); + } + + + @Test + @Sql(statements = { + "TRUNCATE TABLE todo RESTART IDENTITY",}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @DisplayName("Testing findAllToDoByFilter") + public void findAllToDoByFilterTest() { + + Todo todo = new Todo(); + + todo.setToDoTitle("ToDo"); + + todo.setDescription("Description"); + + todo.setToDoPriority(TodoPriority.HIGH); + + jdbcToDoRepository.saveToDo(todo); + + Pageable pageable = PageRequest.of(0, 1); + + Page page = jdbcToDoRepository.findAllToDoByFilter(todo, pageable); + + assertEquals(1, page.getTotalElements()); + } + + + @Test + @Sql(statements = { + "TRUNCATE TABLE todo RESTART IDENTITY", + "INSERT INTO todo (id,todo_title,description,todo_priority,todo_status) VALUES(1,'JAVA_DEVELOPER','CREATING APP','HIGH','PENDING')", + }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @DisplayName("Testing deleteById metric is decremented if task with complete status is removed") + public void deleteById_and_decrement_if_todo_with_status_complete_is_removed_test() { + + Todo todo = new Todo(); + + todo.setToDoTitle("ToDo"); + + todo.setDescription("Description"); + + todo.setToDoPriority(TodoPriority.HIGH); + + todo.setToDoStatus(TodoStatus.COMPLETED); + + jdbcToDoRepository.updateToDoById(todo, 1L); + + assertEquals(1, Objects.requireNonNull(meterRegistry.find("Completed tasks").gauge()).value()); + + assertDoesNotThrow(() -> jdbcToDoRepository.deleteById(1L)); + + assertEquals(0, Objects.requireNonNull(meterRegistry.find("Completed tasks").gauge()).value()); + + } + + @Test + @DisplayName("Testing deleteById fails if no id exists") + public void deleteById_throws_exception_if_no_Id_exists_test() { + assertThrows(TodoException.class,() -> jdbcToDoRepository.deleteById(1L)); + } + + @Test + @Sql(statements = "TRUNCATE todo") + @DisplayName("Testing deleteAllTest") + public void deleteAllTest() + { + assertDoesNotThrow(() -> jdbcToDoRepository.deleteAllToDo()); + } + + +} \ No newline at end of file diff --git a/src/test/java/com/emobile/springtodo/service/TodoServiceTest.java b/src/test/java/com/emobile/springtodo/service/TodoServiceTest.java new file mode 100644 index 00000000..9f1581bb --- /dev/null +++ b/src/test/java/com/emobile/springtodo/service/TodoServiceTest.java @@ -0,0 +1,231 @@ +package com.emobile.springtodo.service; +/// / + +import com.emobile.springtodo.exception.TodoException; +import com.emobile.springtodo.model.Todo; +import com.emobile.springtodo.model.TodoPriority; +import com.emobile.springtodo.model.TodoStatus; +import com.emobile.springtodo.repository.JdbcTodoRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.Collections; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@SpringBootTest +@Testcontainers +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@DisplayName("Testing the functions of ToDoService") +class TodoServiceTest { + + @Container + static PostgreSQLContainer postgres = new PostgreSQLContainer<>(DockerImageName.parse("postgres:12.3")); + + @MockBean + JdbcTodoRepository jdbcToDoRepository; + + @Autowired + ToDoService toDoService; + + + @Test + void connectionEstablished() { + assertThat(postgres.isCreated()).isTrue(); + assertThat(postgres.isRunning()).isTrue(); + } + + + @Test + @DisplayName("Testing createTodo without exception") + public void whenCreateTodoWithoutExceptionTest() { + + Todo todo = new Todo(); + + todo.setToDoTitle("ToDo"); + + todo.setDescription("Description"); + + todo.setToDoStatus(TodoStatus.PENDING); + + todo.setToDoPriority(TodoPriority.HIGH); + + doNothing().when(jdbcToDoRepository).saveToDo(todo); + + assertDoesNotThrow( + () -> toDoService.createTodo(todo)); + } + + @Test + @DisplayName("Testing throw exception if createTodo is null") + public void throwExceptionIfCreateTodoIsNullTest() { + + Todo todo = new Todo(); + + todo.setToDoTitle("ToDo"); + + todo.setDescription("Description"); + + doThrow(new TodoException("Please make sure description,title,priority are not empty")).when(jdbcToDoRepository).saveToDo(todo); + + Exception todoException = assertThrows(TodoException.class, () -> toDoService.createTodo(todo)); + + assertEquals("Please make sure description,title,priority are not empty", todoException.getMessage()); + } + + @Test + @DisplayName("Testing throw exception if CreateTodo is setting status to complete or in progress") + public void throwException_IfCreateTodoIsSettingStatusToCompleteOrInProgressTest() { + + Todo todo = new Todo(); + + todo.setToDoTitle("ToDo"); + + todo.setDescription("Description"); + + todo.setToDoStatus(TodoStatus.COMPLETED); + + doThrow(new TodoException("You are allowed not set status to 'COMPLETED' or 'IN_PROGRESS' this parameters are set by the performer using 'updateToDoById' function to inform the admin about his/her progress and 'PENDING' is set by default")).when(jdbcToDoRepository).saveToDo(todo); + + Exception todoException = assertThrows(TodoException.class, () -> toDoService.createTodo(todo)); + + assertEquals("You are allowed not set status to 'COMPLETED' or 'IN_PROGRESS' this parameters are set by the performer using 'updateToDoById' function to inform the admin about his/her progress and 'PENDING' is set by default", todoException.getMessage()); + } + + @Test + @DisplayName("Testing when UpdateToDoById dont throw exception") + public void whenUpdateToDoByIdDontThrowExceptionTest() { + + Todo toDo = new Todo(); + + toDo.setToDoPriority(TodoPriority.HIGH); + + toDo.setToDoStatus(TodoStatus.COMPLETED); + + doNothing().when(jdbcToDoRepository).updateToDoById(toDo, 1L); + + assertDoesNotThrow( + () -> toDoService.updateToDoById(toDo, 1L)); + } + + @Test + @DisplayName("Testing throw exception if updateToDoById is resetting status to pending") + public void throwExceptionIfUpdateToDoByIdIsResettingStatusToPendingTest() { + + Todo toDo = new Todo(); + + toDo.setToDoPriority(TodoPriority.HIGH); + + toDo.setToDoStatus(TodoStatus.PENDING); + + doThrow(new TodoException("If todo is incomplete you can set status to IN_PROGRESS")).when(jdbcToDoRepository).updateToDoById(toDo, 1L); + + Exception todoException = assertThrows(TodoException.class, () -> toDoService.updateToDoById(toDo, 1L)); + + assertEquals("If todo is incomplete you can set status to IN_PROGRESS", todoException.getMessage()); + } + + + @Test + @DisplayName("Testing throw exception if UpdateToDoById is resetting the same status") + public void throwExceptionIfUpdateToDoByIdIsResettingTheSameStatusTest() { + + Todo toDo = new Todo(); + + toDo.setToDoPriority(TodoPriority.HIGH); + + toDo.setToDoStatus(TodoStatus.COMPLETED); + + doThrow(new TodoException("Status already set to COMPLETED")).when(jdbcToDoRepository).updateToDoById(toDo, 1L); + Exception todoException = assertThrows(TodoException.class, () -> toDoService.updateToDoById(toDo, 1L)); + assertEquals("Status already set to COMPLETED", todoException.getMessage()); + } + + + @Test + @DisplayName("Testing findAllToDoBy is filtering successfully") + public void findAllToDoByFilterTest() { + + Todo todo = new Todo(); + + todo.setToDoTitle("ToDo"); + + todo.setDescription("Description"); + + todo.setToDoStatus(TodoStatus.PENDING); + + todo.setToDoPriority(TodoPriority.HIGH); + + Pageable pageable = PageRequest.of(0, 1); + + Page page = new PageImpl<>(Collections.singletonList(todo), pageable, 1); + + when(jdbcToDoRepository.findAllToDoByFilter(todo, pageable)).thenReturn(page); + + assertEquals(toDoService.findAllToDoByFilter(todo, pageable), page); + } + + @Test + @DisplayName("Testing deleteById without exception if id exists") + public void deleteByIdWithoutExceptionIfIdExistsTest() { + + Todo todo = new Todo(); + + todo.setId(1L); + + todo.setToDoTitle("ToDo"); + + todo.setDescription("Description"); + + todo.setToDoStatus(TodoStatus.PENDING); + + todo.setToDoPriority(TodoPriority.HIGH); + + doNothing().when(jdbcToDoRepository).deleteById(1L); + + assertDoesNotThrow( + () -> toDoService.deleteById(1L)); + } + + @Test + @DisplayName("Testing throwException if deleteById is deleting a nonExisted id") + public void throwException_if_deleteById_is_deleting_a_nonExisted_id_test() { + + Todo toDo = new Todo(); + + toDo.setId(1L); + + toDo.setToDoTitle("ToDo"); + + toDo.setDescription("Description"); + + toDo.setToDoStatus(TodoStatus.PENDING); + + toDo.setToDoPriority(TodoPriority.HIGH); + + doThrow(new TodoException("todo by id " + 1L + " doesn't exist or has been deleted")).when(jdbcToDoRepository).updateToDoById(toDo, 1L); + + Exception todoException = assertThrows(TodoException.class, () -> toDoService.updateToDoById(toDo, 1L)); + + assertEquals("todo by id " + 1L + " doesn't exist or has been deleted", todoException.getMessage()); + } + +} + + + +