Skip to content

Commit 649a97a

Browse files
authored
Add simple TODO REST API (#1)
1 parent 038e814 commit 649a97a

10 files changed

+252
-18
lines changed

pom.xml

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
<groupId>org.springframework.boot</groupId>
77
<artifactId>spring-boot-starter-parent</artifactId>
88
<version>3.3.6</version>
9-
<relativePath/> <!-- lookup parent from repository -->
109
</parent>
10+
1111
<groupId>pl.desz.todo</groupId>
1212
<artifactId>todo-service</artifactId>
1313
<version>0.0.1-SNAPSHOT</version>
@@ -84,6 +84,11 @@
8484
<artifactId>mongodb</artifactId>
8585
<scope>test</scope>
8686
</dependency>
87+
<dependency>
88+
<groupId>org.mockito</groupId>
89+
<artifactId>mockito-core</artifactId>
90+
<scope>test</scope>
91+
</dependency>
8792
</dependencies>
8893

8994
<build>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package pl.desz.todo.api;
2+
3+
import org.springframework.web.bind.annotation.DeleteMapping;
4+
import org.springframework.web.bind.annotation.GetMapping;
5+
import org.springframework.web.bind.annotation.PathVariable;
6+
import org.springframework.web.bind.annotation.PostMapping;
7+
import org.springframework.web.bind.annotation.RequestMapping;
8+
import org.springframework.web.bind.annotation.RestController;
9+
import pl.desz.todo.model.Todo;
10+
import pl.desz.todo.service.TodoService;
11+
import reactor.core.publisher.Flux;
12+
import reactor.core.publisher.Mono;
13+
14+
@RestController
15+
@RequestMapping("/todos")
16+
public class TodoController {
17+
18+
private final TodoService todoService;
19+
20+
public TodoController(TodoService todoService) {
21+
this.todoService = todoService;
22+
}
23+
24+
@GetMapping
25+
public Flux<Todo> getTodos() {
26+
return todoService.getAllTodos();
27+
}
28+
29+
@GetMapping("/{id}")
30+
public Mono<Todo> getTodoById(@PathVariable Long id) {
31+
return todoService.getById(id);
32+
}
33+
34+
@PostMapping
35+
public Mono<Todo> addTodo(Mono<Todo> todoMono) {
36+
return todoService.saveTodo(todoMono);
37+
}
38+
39+
@DeleteMapping("/{id}")
40+
public Mono<Void> deleteTodo(@PathVariable Long id) {
41+
return todoService.deleteTodoById(id);
42+
}
43+
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package pl.desz.todo.model;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Data;
5+
import lombok.NoArgsConstructor;
6+
import org.springframework.data.annotation.Id;
7+
import org.springframework.data.mongodb.core.mapping.Document;
8+
9+
@NoArgsConstructor
10+
@AllArgsConstructor
11+
@Data
12+
@Document(collection = "todos")
13+
public class Todo {
14+
15+
@Id
16+
Long id;
17+
String description;
18+
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package pl.desz.todo.persistence;
2+
3+
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
4+
import org.springframework.stereotype.Repository;
5+
import pl.desz.todo.model.Todo;
6+
7+
@Repository
8+
public interface TodoRepository extends ReactiveCrudRepository<Todo, Long> {
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package pl.desz.todo.service;
2+
3+
import org.springframework.stereotype.Service;
4+
import pl.desz.todo.model.Todo;
5+
import pl.desz.todo.persistence.TodoRepository;
6+
import reactor.core.publisher.Flux;
7+
import reactor.core.publisher.Mono;
8+
9+
@Service
10+
public class TodoService {
11+
12+
private final TodoRepository todoRepository;
13+
14+
public TodoService(TodoRepository todoRepository) {
15+
this.todoRepository = todoRepository;
16+
}
17+
18+
public Flux<Todo> getAllTodos() {
19+
return todoRepository.findAll();
20+
}
21+
22+
public Mono<Todo> getById(Long id) {
23+
return todoRepository.findById(id);
24+
}
25+
26+
public Mono<Todo> saveTodo(Mono<Todo> todoMono) {
27+
return todoMono.flatMap(todoRepository::save);
28+
}
29+
30+
public Mono<Void> deleteTodoById(Long id) {
31+
return todoRepository.deleteById(id);
32+
}
33+
}

src/main/resources/application.yaml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
spring:
22
application:
3-
name: todo-service
3+
name: todo-service
4+
data:
5+
mongodb:
6+
uri: mongodb://test:thepass@localhost:27017/test?authSource=admin

src/test/java/pl/desz/todo/TestcontainersConfiguration.java src/test/java/pl/desz/todo/MongoDBContainerConf.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
import org.testcontainers.utility.DockerImageName;
88

99
@TestConfiguration(proxyBeanMethods = false)
10-
class TestcontainersConfiguration {
10+
class MongoDBContainerConf {
1111

1212
@Bean
1313
@ServiceConnection
1414
MongoDBContainer mongoDbContainer() {
15-
return new MongoDBContainer(DockerImageName.parse("mongo:latest"));
15+
return new MongoDBContainer(DockerImageName.parse("mongo:8.0.3"));
1616
}
1717

1818
}

src/test/java/pl/desz/todo/TestTodoServiceApplication.java

-11
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package pl.desz.todo;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.extension.ExtendWith;
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
7+
import org.springframework.boot.test.mock.mockito.MockBean;
8+
import org.springframework.test.context.junit.jupiter.SpringExtension;
9+
import org.springframework.test.web.reactive.server.WebTestClient;
10+
import pl.desz.todo.api.TodoController;
11+
import pl.desz.todo.model.Todo;
12+
import pl.desz.todo.service.TodoService;
13+
import reactor.core.publisher.Flux;
14+
import reactor.core.publisher.Mono;
15+
16+
import static org.mockito.ArgumentMatchers.any;
17+
import static org.mockito.Mockito.when;
18+
19+
@ExtendWith(SpringExtension.class)
20+
@WebFluxTest(controllers = TodoController.class)
21+
public class TodoApiTest {
22+
23+
@Autowired
24+
private WebTestClient webTestClient;
25+
26+
@MockBean
27+
private TodoService todoService;
28+
29+
@Test
30+
void shouldGetAllTodos() {
31+
when(todoService.getAllTodos())
32+
.thenReturn(Flux.just(
33+
new Todo(1L, "DESC"),
34+
new Todo(2L, "DESC")));
35+
36+
webTestClient.get()
37+
.uri("/todos")
38+
.exchange()
39+
.expectStatus().isOk()
40+
.expectBodyList(Todo.class)
41+
.hasSize(2);
42+
}
43+
44+
@Test
45+
void shouldGetTodoById() {
46+
final Todo todo = new Todo(1L, "DESC");
47+
48+
when(todoService.getById(1L))
49+
.thenReturn(Mono.just(todo));
50+
51+
webTestClient.get()
52+
.uri("/todos/1")
53+
.exchange()
54+
.expectStatus().isOk()
55+
.expectBody(Todo.class)
56+
.isEqualTo(todo);
57+
}
58+
59+
@Test
60+
void shouldSaveTodo() {
61+
when(todoService.saveTodo(any()))
62+
.thenReturn(Mono.just(new Todo(1L, "DESC")));
63+
64+
webTestClient.post()
65+
.uri("/todos")
66+
.exchange()
67+
.expectStatus().isOk()
68+
.expectBody(Todo.class);
69+
}
70+
71+
@Test
72+
void shouldDeleteTodo() {
73+
when(todoService.deleteTodoById(1L))
74+
.thenReturn(Mono.empty().then());
75+
76+
webTestClient.delete()
77+
.uri("/todos/1")
78+
.exchange()
79+
.expectStatus().isOk();
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,67 @@
11
package pl.desz.todo;
22

33
import org.junit.jupiter.api.Test;
4+
import org.springframework.beans.factory.annotation.Autowired;
45
import org.springframework.boot.test.context.SpringBootTest;
56
import org.springframework.context.annotation.Import;
7+
import pl.desz.todo.model.Todo;
8+
import pl.desz.todo.persistence.TodoRepository;
9+
import reactor.core.publisher.Mono;
10+
import reactor.test.StepVerifier;
611

7-
@Import(TestcontainersConfiguration.class)
12+
@Import(MongoDBContainerConf.class)
813
@SpringBootTest
914
class TodoServiceApplicationTests {
1015

16+
private static final Long TODO_ID = 1L;
17+
private static final String TODO_DESCRIPTION = "DESCRIPTION";
18+
19+
@Autowired
20+
private TodoRepository todoRepository;
21+
22+
@Test
23+
void shouldSaveTodo() {
24+
final Todo todo = new Todo(TODO_ID, TODO_DESCRIPTION);
25+
final Mono<Todo> todoMono = todoRepository.save(todo);
26+
27+
StepVerifier.create(todoMono)
28+
.expectNext(todo)
29+
.verifyComplete();
30+
}
31+
32+
@Test
33+
void shouldFindTodoById() {
34+
final Todo todo = new Todo(TODO_ID, TODO_DESCRIPTION);
35+
todoRepository.save(todo).block();
36+
37+
StepVerifier.create(todoRepository.findById(TODO_ID))
38+
.expectNextMatches(foundTodo -> foundTodo.getId().equals(TODO_ID) && foundTodo.getDescription().equals(TODO_DESCRIPTION))
39+
.verifyComplete();
40+
}
41+
1142
@Test
12-
void contextLoads() {
43+
void shouldUpdateTodo() {
44+
final Todo todo = new Todo(TODO_ID, TODO_DESCRIPTION);
45+
todoRepository.save(todo).block();
46+
47+
final Todo updatedTodo = new Todo(TODO_ID, "Updated");
48+
final Mono<Todo> monoUpdatedTodo = todoRepository.save(updatedTodo);
49+
50+
StepVerifier.create(monoUpdatedTodo)
51+
.expectNextMatches(t -> t.getId().equals(TODO_ID) && t.getDescription().equals("Updated"))
52+
.verifyComplete();
1353
}
1454

15-
}
55+
@Test
56+
void shouldDeleteTodo() {
57+
final Todo todo = new Todo(TODO_ID, TODO_DESCRIPTION);
58+
todoRepository.save(todo).block();
59+
60+
StepVerifier.create(todoRepository.deleteById(TODO_ID))
61+
.verifyComplete();
62+
63+
StepVerifier.create(todoRepository.findById(TODO_ID))
64+
.expectNextCount(0)
65+
.verifyComplete();
66+
}
67+
}

0 commit comments

Comments
 (0)