diff --git a/2021-02/spring-20/.gitignore b/2021-02/spring-20/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2021-02/spring-20/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2021-02/spring-20/pom.xml b/2021-02/spring-20/pom.xml
new file mode 100644
index 00000000..8997be90
--- /dev/null
+++ b/2021-02/spring-20/pom.xml
@@ -0,0 +1,17 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-20
+ 1.0
+
+ pom
+
+
+ spring-20-exercise
+ spring-20-solution
+
+
diff --git a/2021-02/spring-20/spring-20-exercise/pom.xml b/2021-02/spring-20/spring-20-exercise/pom.xml
new file mode 100644
index 00000000..7fb51891
--- /dev/null
+++ b/2021-02/spring-20/spring-20-exercise/pom.xml
@@ -0,0 +1,61 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-20-exercise
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.4.5
+
+
+
+
+ 11
+ 11
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb-reactive
+
+
+ de.flapdoodle.embed
+ de.flapdoodle.embed.mongo
+
+
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/Main.java b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..7b3c42bf
--- /dev/null
+++ b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/Main.java
@@ -0,0 +1,69 @@
+package ru.otus.spring;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.ServerRequest;
+import org.springframework.web.reactive.function.server.ServerResponse;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.repository.PersonRepository;
+
+import java.util.Arrays;
+
+import static org.springframework.http.MediaType.APPLICATION_JSON;
+import static org.springframework.web.reactive.function.BodyInserters.fromObject;
+import static org.springframework.web.reactive.function.BodyInserters.fromValue;
+import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
+import static org.springframework.web.reactive.function.server.RequestPredicates.queryParam;
+import static org.springframework.web.reactive.function.server.RouterFunctions.route;
+import static org.springframework.web.reactive.function.server.ServerResponse.*;
+
+@SpringBootApplication
+public class Main {
+
+ public static void main(String[] args) {
+ ApplicationContext context = SpringApplication.run(Main.class);
+ PersonRepository repository = context.getBean(PersonRepository.class);
+
+ repository.saveAll(Arrays.asList(
+ new Person("Pushkin", 22),
+ new Person("Lermontov", 22),
+ new Person("Tolstoy", 60)
+ )).subscribe(p -> System.out.println(p.getLastName()));
+
+ }
+
+ @Bean
+ public RouterFunction composedRoutes(PersonRepository repository) {
+ return route()
+ // Обратите внимание на использование хэндлера
+ .GET("/func/person", accept(APPLICATION_JSON), new PersonHandler(repository)::list)
+ // Обратите внимание на использование pathVariable
+ .GET("/func/person/{id}", accept(APPLICATION_JSON),
+ request -> repository.findById(request.pathVariable("id"))
+ .flatMap(person -> ok().contentType(APPLICATION_JSON).body(fromValue(person)))
+ .switchIfEmpty(notFound().build())
+ ).build();
+ }
+
+ // Это пример хэндлера, который даже не бин
+ static class PersonHandler {
+
+ private final PersonRepository repository;
+
+ PersonHandler(PersonRepository repository) {
+ this.repository = repository;
+ }
+
+ Mono list(ServerRequest request) {
+ // Обратите внимание на пример другого порядка создания response от Flux
+ return ok().contentType(APPLICATION_JSON).body(repository.findAll(), Person.class);
+ }
+ }
+}
+
+
diff --git a/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/domain/Person.java b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/domain/Person.java
new file mode 100644
index 00000000..d40218ec
--- /dev/null
+++ b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/domain/Person.java
@@ -0,0 +1,55 @@
+package ru.otus.spring.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Field;
+
+@Document
+public class Person {
+
+ @Id
+ private String id;
+
+ @JsonProperty("name")
+ @Field("name")
+ private String lastName;
+
+ private int age;
+
+ public Person() {
+ }
+
+ public Person(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public Person(String lastName, int age) {
+ this.lastName = lastName;
+ this.age = age;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+}
diff --git a/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/repository/PersonRepository.java b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/repository/PersonRepository.java
new file mode 100644
index 00000000..0584f0cb
--- /dev/null
+++ b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/repository/PersonRepository.java
@@ -0,0 +1,20 @@
+package ru.otus.spring.repository;
+
+import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+
+public interface PersonRepository
+ extends ReactiveMongoRepository {
+
+ Flux findAll();
+
+ Mono findById(String id);
+
+ Mono save(Mono person);
+
+ Flux findAllByLastName(String lastName);
+
+ Flux findAllByAge(int age);
+}
diff --git a/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/rest/AnnotatedController.java b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/rest/AnnotatedController.java
new file mode 100644
index 00000000..fc3bded8
--- /dev/null
+++ b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/rest/AnnotatedController.java
@@ -0,0 +1,33 @@
+package ru.otus.spring.rest;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+
+@RestController
+public class AnnotatedController {
+
+ @GetMapping("/flux/one")
+ public Mono one() {
+ return Mono.just("one");
+ }
+
+ @GetMapping("/flux/ten")
+ public Flux list() {
+ return Flux.range(1, 10).delayElements(Duration.ofSeconds(1));
+ }
+
+ @GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ public Flux stream() {
+ return Flux.generate(() -> 0, (state, emitter) -> {
+ emitter.next(state);
+ return state + 1;
+ })
+ .delayElements(Duration.ofSeconds(1L))
+ .map(i -> "" + i);
+ }
+}
diff --git a/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/rest/PersonController.java b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/rest/PersonController.java
new file mode 100644
index 00000000..c6f2df03
--- /dev/null
+++ b/2021-02/spring-20/spring-20-exercise/src/main/java/ru/otus/spring/rest/PersonController.java
@@ -0,0 +1,37 @@
+package ru.otus.spring.rest;
+
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.repository.PersonRepository;
+
+@RestController
+public class PersonController {
+
+ private final PersonRepository repository;
+
+ public PersonController(PersonRepository repository) {
+ this.repository = repository;
+ }
+
+ @GetMapping("/person")
+ public Flux all() {
+ return repository.findAll();
+ }
+
+ @GetMapping("/person/{id}")
+ public Mono byId(@PathVariable("id") String id) {
+ return repository.findById(id);
+ }
+
+ @PostMapping("/person")
+ public Mono save(@RequestBody Mono dto) {
+ return repository.save(dto);
+ }
+
+ @GetMapping("/person/find")
+ public Flux byName(@RequestParam("name") String name) {
+ return repository.findAllByLastName(name);
+ }
+}
diff --git a/2021-02/spring-20/spring-20-exercise/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java b/2021-02/spring-20/spring-20-exercise/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
new file mode 100644
index 00000000..2f373b60
--- /dev/null
+++ b/2021-02/spring-20/spring-20-exercise/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
@@ -0,0 +1,28 @@
+package ru.otus.spring.repository;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+import ru.otus.spring.domain.Person;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@DataMongoTest
+public class PersonRepositoryTest {
+
+ @Autowired
+ private PersonRepository repository;
+
+ @Test
+ public void shouldSetIdOnSave() {
+ Mono personMono = repository.save(new Person("Bill", 12));
+
+ StepVerifier
+ .create(personMono)
+ .assertNext(person -> assertNotNull(person.getId()))
+ .expectComplete()
+ .verify();
+ }
+}
diff --git a/2021-02/spring-20/spring-20-exercise/src/test/java/ru/otus/spring/rest/PersonControllerTest.java b/2021-02/spring-20/spring-20-exercise/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
new file mode 100644
index 00000000..d1504547
--- /dev/null
+++ b/2021-02/spring-20/spring-20-exercise/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
@@ -0,0 +1,28 @@
+package ru.otus.spring.rest;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.ServerResponse;
+
+@SpringBootTest
+public class PersonControllerTest {
+
+ @Autowired
+ private RouterFunction route;
+
+ @Test
+ public void testRoute() {
+ WebTestClient client = WebTestClient
+ .bindToRouterFunction(route)
+ .build();
+
+ client.get()
+ .uri("/func/person")
+ .exchange()
+ .expectStatus()
+ .isOk();
+ }
+}
diff --git a/2021-02/spring-20/spring-20-solution/pom.xml b/2021-02/spring-20/spring-20-solution/pom.xml
new file mode 100644
index 00000000..60a6d129
--- /dev/null
+++ b/2021-02/spring-20/spring-20-solution/pom.xml
@@ -0,0 +1,61 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-20-solution
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.4.5
+
+
+
+
+ 11
+ 11
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb-reactive
+
+
+ de.flapdoodle.embed
+ de.flapdoodle.embed.mongo
+
+
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/Main.java b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..8bad8f3d
--- /dev/null
+++ b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/Main.java
@@ -0,0 +1,82 @@
+package ru.otus.spring;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.ServerRequest;
+import org.springframework.web.reactive.function.server.ServerResponse;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.repository.PersonRepository;
+
+import java.util.Arrays;
+
+import static org.springframework.http.MediaType.APPLICATION_JSON;
+import static org.springframework.web.reactive.function.BodyInserters.fromObject;
+import static org.springframework.web.reactive.function.BodyInserters.fromValue;
+import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
+import static org.springframework.web.reactive.function.server.RequestPredicates.queryParam;
+import static org.springframework.web.reactive.function.server.RouterFunctions.route;
+import static org.springframework.web.reactive.function.server.ServerResponse.*;
+
+@SpringBootApplication
+public class Main {
+
+ public static void main(String[] args) {
+ ApplicationContext context = SpringApplication.run(Main.class);
+ PersonRepository repository = context.getBean(PersonRepository.class);
+
+ repository.saveAll(Arrays.asList(
+ new Person("Pushkin", 22),
+ new Person("Lermontov", 22),
+ new Person("Tolstoy", 60)
+ )).subscribe(p -> System.out.println(p.getLastName()));
+ }
+
+ @Bean
+ public RouterFunction composedRoutes(PersonRepository repository) {
+ return route()
+ // эта функция должна стоять раньше findAll - порядок следования роутов - важен
+ .GET("/func/person", queryParam("name", StringUtils::isNotEmpty),
+ request -> request.queryParam("name")
+ .map(repository::findAllByLastName)
+ .map(persons -> ok().body(persons, Person.class))
+ .orElse(badRequest().build())
+ )
+ // пример другой реализации - начиная с запроса репозитория
+ .GET("/func/person", queryParam("age", StringUtils::isNotEmpty),
+ req -> repository.findAllByLastName(
+ req.queryParam("age").orElseThrow(IllegalArgumentException::new)
+ )
+ .collectList()
+ .flatMap(persons -> ok().body(persons, Person.class)))
+ // Обратите внимание на использование хэндлера
+ .GET("/func/person", accept(APPLICATION_JSON), new PersonHandler(repository)::list)
+ // Обратите внимание на использование pathVariable
+ .GET("/func/person/{id}", accept(APPLICATION_JSON),
+ request -> repository.findById(request.pathVariable("id"))
+ .flatMap(person -> ok().contentType(APPLICATION_JSON).body(fromValue(person)))
+ .switchIfEmpty(notFound().build())
+ ).build();
+ }
+
+ // Это пример хэндлера, который даже не бин
+ static class PersonHandler {
+
+ private final PersonRepository repository;
+
+ PersonHandler(PersonRepository repository) {
+ this.repository = repository;
+ }
+
+ Mono list(ServerRequest request) {
+ // Обратите внимание на пример другого порядка создания response от Flux
+ return ok().contentType(APPLICATION_JSON).body(repository.findAll(), Person.class);
+ }
+ }
+}
+
+
diff --git a/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/domain/Person.java b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/domain/Person.java
new file mode 100644
index 00000000..d40218ec
--- /dev/null
+++ b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/domain/Person.java
@@ -0,0 +1,55 @@
+package ru.otus.spring.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Field;
+
+@Document
+public class Person {
+
+ @Id
+ private String id;
+
+ @JsonProperty("name")
+ @Field("name")
+ private String lastName;
+
+ private int age;
+
+ public Person() {
+ }
+
+ public Person(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public Person(String lastName, int age) {
+ this.lastName = lastName;
+ this.age = age;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+}
diff --git a/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/repository/PersonRepository.java b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/repository/PersonRepository.java
new file mode 100644
index 00000000..0584f0cb
--- /dev/null
+++ b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/repository/PersonRepository.java
@@ -0,0 +1,20 @@
+package ru.otus.spring.repository;
+
+import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+
+public interface PersonRepository
+ extends ReactiveMongoRepository {
+
+ Flux findAll();
+
+ Mono findById(String id);
+
+ Mono save(Mono person);
+
+ Flux findAllByLastName(String lastName);
+
+ Flux findAllByAge(int age);
+}
diff --git a/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/rest/AnnotatedController.java b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/rest/AnnotatedController.java
new file mode 100644
index 00000000..184da872
--- /dev/null
+++ b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/rest/AnnotatedController.java
@@ -0,0 +1,34 @@
+package ru.otus.spring.rest;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+
+@RestController
+public class AnnotatedController {
+
+ @GetMapping("/flux/one")
+ public Mono one() {
+ return Mono.just("one")
+ .map(String::toUpperCase);
+ }
+
+ @GetMapping("/flux/ten")
+ public Flux list() {
+ return Flux.range(1, 10);
+ }
+
+ @GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ public Flux stream() {
+ return Flux.generate(() -> 0, (state, emitter) -> {
+ emitter.next(state);
+ return state + 1;
+ })
+ .delayElements(Duration.ofSeconds(1L))
+ .map(i -> "" + i);
+ }
+}
diff --git a/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/rest/PersonController.java b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/rest/PersonController.java
new file mode 100644
index 00000000..2edcaea9
--- /dev/null
+++ b/2021-02/spring-20/spring-20-solution/src/main/java/ru/otus/spring/rest/PersonController.java
@@ -0,0 +1,42 @@
+package ru.otus.spring.rest;
+
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.repository.PersonRepository;
+
+@RestController
+public class PersonController {
+
+ private PersonRepository repository;
+
+ public PersonController(PersonRepository repository) {
+ this.repository = repository;
+ }
+
+ @GetMapping("/person")
+ public Flux all() {
+ return repository.findAll();
+ }
+
+ @GetMapping("/person/{id}")
+ public Mono byId(@PathVariable("id") String id) {
+ return repository.findById(id);
+ }
+
+ @GetMapping("/person/byname")
+ public Flux byName(@RequestParam("name") String lastName) {
+ return repository.findAllByLastName(lastName);
+ }
+
+ @GetMapping("/person/byage")
+ public Flux byAge(@RequestParam int age) {
+ return repository.findAllByAge(age);
+ }
+
+ @PostMapping("/person")
+ public Mono save(@RequestBody Mono dto) {
+ return repository.save(dto);
+ }
+}
diff --git a/2021-02/spring-20/spring-20-solution/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java b/2021-02/spring-20/spring-20-solution/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
new file mode 100644
index 00000000..2f373b60
--- /dev/null
+++ b/2021-02/spring-20/spring-20-solution/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
@@ -0,0 +1,28 @@
+package ru.otus.spring.repository;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+import ru.otus.spring.domain.Person;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@DataMongoTest
+public class PersonRepositoryTest {
+
+ @Autowired
+ private PersonRepository repository;
+
+ @Test
+ public void shouldSetIdOnSave() {
+ Mono personMono = repository.save(new Person("Bill", 12));
+
+ StepVerifier
+ .create(personMono)
+ .assertNext(person -> assertNotNull(person.getId()))
+ .expectComplete()
+ .verify();
+ }
+}
diff --git a/2021-02/spring-20/spring-20-solution/src/test/java/ru/otus/spring/rest/PersonControllerTest.java b/2021-02/spring-20/spring-20-solution/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
new file mode 100644
index 00000000..d1504547
--- /dev/null
+++ b/2021-02/spring-20/spring-20-solution/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
@@ -0,0 +1,28 @@
+package ru.otus.spring.rest;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.ServerResponse;
+
+@SpringBootTest
+public class PersonControllerTest {
+
+ @Autowired
+ private RouterFunction route;
+
+ @Test
+ public void testRoute() {
+ WebTestClient client = WebTestClient
+ .bindToRouterFunction(route)
+ .build();
+
+ client.get()
+ .uri("/func/person")
+ .exchange()
+ .expectStatus()
+ .isOk();
+ }
+}