diff --git a/borestop/README.md b/borestop/README.md index 29c3fde7ef3aac06fbd1693b4fb2e6f50b7ce623..c19e38198fc72dc593d31b236a3aca1d35e535cf 100644 --- a/borestop/README.md +++ b/borestop/README.md @@ -10,6 +10,28 @@ Création en live d'une application pour montrer l'apport de valeur pas différe - **Niveau** : Débutant - **Replay** : [La pyramide de tests de Kheops (partie 1) - Hippolyte et Colin](https://www.youtube.com/watch?v=rfRgJk251pw) +## Partie 2 + +API utilisée : + +- https://www.boredapi.com/api/activity/ + +- **Auteurs** : Hippolyte DURIX && Colin DAMON +- **Date** : 02/09/2020 +- **Langage** : Java +- **Niveau** : Moyen +- **Replay** : [Appeler une API externe avec Hippolyte et Colin](https://www.youtube.com/watch?v=E-5mrsesZHk) + +## Partie 3 + +Appel à boredapi avec des [patterns anti-fragiles](https://github.com/resilience4j/resilience4j) + +- **Auteurs** : Hippolyte DURIX && Colin DAMON +- **Date** : 18/11/2020 +- **Langage** : Java +- **Niveau** : Moyen +- **Replay** : TODO + ## JHipster This application was generated using JHipster 6.10.1, you can find documentation and help at [https://www.jhipster.tech/documentation-archive/v6.10.1](https://www.jhipster.tech/documentation-archive/v6.10.1). diff --git a/borestop/src/main/java/com/ippon/borestop/activity/domain/Category.java b/borestop/src/main/java/com/ippon/borestop/activity/domain/Category.java index 101af047b4e633712528b897e4c26ca5c56901c5..48a9df3071f164db9d4cbabd1751259c8fe71b72 100644 --- a/borestop/src/main/java/com/ippon/borestop/activity/domain/Category.java +++ b/borestop/src/main/java/com/ippon/borestop/activity/domain/Category.java @@ -1,5 +1,7 @@ package com.ippon.borestop.activity.domain; public enum Category { - RELAXATION + COOKING, + RELAXATION, + DEFAULT } diff --git a/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiCategory.java b/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiCategory.java new file mode 100644 index 0000000000000000000000000000000000000000..231ff3ddbe573d1de82b93e84726c54eb77bf9a1 --- /dev/null +++ b/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiCategory.java @@ -0,0 +1,37 @@ +package com.ippon.borestop.activity.infrastructure.secondary; + +import com.ippon.borestop.activity.domain.Category; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; + +enum BoredApiCategory { + COOKING("cooking"), + RELAXATION("relaxation"), + DEFAULT("default"); + private static final Map<String, BoredApiCategory> categories = buildCategories(); + + private static Map<String, BoredApiCategory> buildCategories() { + return Arrays.stream(values()).collect(Collectors.toMap(BoredApiCategory::getLabel, Function.identity())); + } + + private final String label; + + BoredApiCategory(String label) { + this.label = label; + } + + private String getLabel() { + return label; + } + + static BoredApiCategory from(String type) { + return categories.getOrDefault(StringUtils.lowerCase(type), DEFAULT); + } + + Category toDomain() { + return Category.valueOf(name()); + } +} diff --git a/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiConfiguration.java b/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..54eb299b63d5150219da96b2bb311dc3b7f14537 --- /dev/null +++ b/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiConfiguration.java @@ -0,0 +1,8 @@ +package com.ippon.borestop.activity.infrastructure.secondary; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(BoredApiProperties.class) +class BoredApiConfiguration {} diff --git a/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiIdeasRepository.java b/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiIdeasRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..d386bfbc828f06f3a1811cdd63d5f8898f2b0562 --- /dev/null +++ b/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiIdeasRepository.java @@ -0,0 +1,32 @@ +package com.ippon.borestop.activity.infrastructure.secondary; + +import com.ippon.borestop.activity.domain.Category; +import com.ippon.borestop.activity.domain.Idea; +import com.ippon.borestop.activity.domain.IdeasRepository; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +@Service +class BoredApiIdeasRepository implements IdeasRepository { + private final RestTemplate rest; + private final String url; + + BoredApiIdeasRepository(RestTemplateBuilder restBuilder, BoredApiProperties properties) { + this.rest = restBuilder.setConnectTimeout(properties.getConnectTimeout()).setReadTimeout(properties.getReadTimeout()).build(); + + url = properties.getUrl(); + } + + @Override + public Idea next() { + try { + BoredApiResponse apiResponse = rest.getForEntity(url, BoredApiResponse.class).getBody(); + + return apiResponse.toDomain(); + } catch (RestClientException e) { + return new Idea("Go grab a beer", Category.RELAXATION); + } + } +} diff --git a/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiProperties.java b/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..54313eb23d56370838dded39c9b222b04d268063 --- /dev/null +++ b/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiProperties.java @@ -0,0 +1,42 @@ +package com.ippon.borestop.activity.infrastructure.secondary; + +import java.time.Duration; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +@Validated +@ConfigurationProperties("bored-api") +class BoredApiProperties { + private String url; + private Duration connectTimeout; + private Duration readTimeout; + + @NotBlank + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + @NotNull + public Duration getConnectTimeout() { + return connectTimeout; + } + + public void setConnectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + } + + @NotNull + public Duration getReadTimeout() { + return readTimeout; + } + + public void setReadTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + } +} diff --git a/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiResponse.java b/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..a255c320591721277fd191b70286e20b005e5063 --- /dev/null +++ b/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiResponse.java @@ -0,0 +1,26 @@ +package com.ippon.borestop.activity.infrastructure.secondary; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.ippon.borestop.activity.domain.Idea; + +class BoredApiResponse { + private final String activity; + private final String type; + + public BoredApiResponse(@JsonProperty("activity") String activity, @JsonProperty("type") String type) { + this.activity = activity; + this.type = type; + } + + public String getActivity() { + return activity; + } + + public String getType() { + return type; + } + + public Idea toDomain() { + return new Idea(this.activity, BoredApiCategory.from(type).toDomain()); + } +} diff --git a/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/RestIdeasRepository.java b/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/RestIdeasRepository.java deleted file mode 100644 index ff375789573795c70719fa57eed0ee468d2e26dc..0000000000000000000000000000000000000000 --- a/borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/RestIdeasRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.ippon.borestop.activity.infrastructure.secondary; - -import com.ippon.borestop.activity.domain.Idea; -import com.ippon.borestop.activity.domain.IdeasRepository; -import org.springframework.stereotype.Service; - -@Service -class RestIdeasRepository implements IdeasRepository { - - @Override - public Idea next() { - // TODO Auto-generated method stub - return null; - } -} diff --git a/borestop/src/main/resources/config/application.yml b/borestop/src/main/resources/config/application.yml index c6f99b1b771a056e0547bc661ed80c1924f5fd1a..7d74a71de30a55ee76a2b1876821f728d41940f5 100644 --- a/borestop/src/main/resources/config/application.yml +++ b/borestop/src/main/resources/config/application.yml @@ -169,3 +169,8 @@ jhipster: # =================================================================== # application: + +bored-api: + url: 'https://www.boredapi.com/api/activity/' + connect-timeout: 500ms + read-timeout: 1s diff --git a/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiCategoryUnitTest.java b/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiCategoryUnitTest.java new file mode 100644 index 0000000000000000000000000000000000000000..34c0772f4a2edc8a349d4c62fd81fa55fd50b6ab --- /dev/null +++ b/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiCategoryUnitTest.java @@ -0,0 +1,29 @@ +package com.ippon.borestop.activity.infrastructure.secondary; + +import static org.assertj.core.api.Assertions.*; + +import com.ippon.borestop.activity.domain.Category; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +class BoredApiCategoryUnitTest { + + @Test + void shouldHaveValueForAllDomainCategories() { + Set<Category> mappedCategories = Arrays.stream(BoredApiCategory.values()).map(BoredApiCategory::toDomain).collect(Collectors.toSet()); + + assertThat(mappedCategories).containsExactlyInAnyOrder(Category.values()); + } + + @Test + void shouldConvertFromType() { + assertThat(BoredApiCategory.from("Cooking")).isEqualTo(BoredApiCategory.COOKING); + } + + @Test + void shouldConvertFromUnknownType() { + assertThat(BoredApiCategory.from("unknown")).isEqualTo(BoredApiCategory.DEFAULT); + } +} diff --git a/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiIdeasRepositoryCommunicationIntTest.java b/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiIdeasRepositoryCommunicationIntTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2349c07376309104c522dc93d80c8353407cec24 --- /dev/null +++ b/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiIdeasRepositoryCommunicationIntTest.java @@ -0,0 +1,54 @@ +package com.ippon.borestop.activity.infrastructure.secondary; + +import static com.ippon.borestop.activity.domain.Category.*; +import static org.assertj.core.api.Assertions.*; + +import com.ippon.borestop.activity.domain.Idea; +import com.ippon.borestop.common.BorestopIntTest; +import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.web.server.LocalServerPort; + +@BorestopIntTest +class BoredApiIdeasRepositoryCommunicationIntTest { + @LocalServerPort + private int port; + + @Autowired + private FakeBoredApi fake; + + @Autowired + private RestTemplateBuilder restBuilder; + + private BoredApiIdeasRepository repository; + + @BeforeEach + void loadRepository() { + fake.reset(); + + repository = new BoredApiIdeasRepository(restBuilder, properties()); + } + + private BoredApiProperties properties() { + BoredApiProperties properties = new BoredApiProperties(); + + properties.setUrl("http://localhost:" + port + "/fake-bored-api"); + properties.setConnectTimeout(Duration.ofMillis(10)); + properties.setReadTimeout(Duration.ofMillis(50)); + + return properties; + } + + @Test + void shouldGracefullyHandleTimeouts() { + fake.slowDown(); + + Idea idea = repository.next(); + + assertThat(idea.getLabel()).isEqualTo("Go grab a beer"); + assertThat(idea.getCategory()).isEqualTo(RELAXATION); + } +} diff --git a/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiIdeasRepositoryIntTest.java b/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiIdeasRepositoryIntTest.java new file mode 100644 index 0000000000000000000000000000000000000000..721370b16d026fd0de6aadde2ac33fec13375b9a --- /dev/null +++ b/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiIdeasRepositoryIntTest.java @@ -0,0 +1,23 @@ +package com.ippon.borestop.activity.infrastructure.secondary; + +import static org.assertj.core.api.Assertions.*; + +import com.ippon.borestop.activity.domain.Idea; +import com.ippon.borestop.activity.domain.IdeasRepository; +import com.ippon.borestop.common.BorestopIntTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@BorestopIntTest +class BoredApiIdeasRepositoryIntTest { + @Autowired + private IdeasRepository ideasRepository; + + @Test + void shouldGetNextIdea() { + Idea idea = ideasRepository.next(); + + assertThat(idea.getCategory()).isNotNull(); + assertThat(idea.getLabel()).isNotNull(); + } +} diff --git a/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiResponseUnitTest.java b/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiResponseUnitTest.java new file mode 100644 index 0000000000000000000000000000000000000000..08b9078594ba48b083096be5c141ebd097ef5663 --- /dev/null +++ b/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiResponseUnitTest.java @@ -0,0 +1,41 @@ +package com.ippon.borestop.activity.infrastructure.secondary; + +import com.ippon.borestop.activity.domain.Category; +import com.ippon.borestop.activity.domain.Idea; +import com.ippon.borestop.common.infrastructure.primary.TestJson; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class BoredApiResponseUnitTest { + + @Test + void shouldDeserializeFromJson() { + BoredApiResponse response = TestJson.readFromJson(json(), BoredApiResponse.class); + + assertThat(response.getActivity()).isEqualTo("Cook something together with someone"); + assertThat(response.getType()).isEqualTo("cooking"); + } + + @Test + void shouldConvertToDomain() { + BoredApiResponse response = new BoredApiResponse("activity", "cooking"); + + Idea idea = response.toDomain(); + + assertThat(idea.getLabel()).isEqualTo("activity"); + assertThat(idea.getCategory()).isEqualTo(Category.COOKING); + } + + private String json() { + return "{\n" + + " \"activity\": \"Cook something together with someone\",\n" + + " \"type\": \"cooking\",\n" + + " \"participants\": 2,\n" + + " \"price\": 0.3,\n" + + " \"link\": \"\",\n" + + " \"key\": \"1799120\",\n" + + " \"accessibility\": 0.8\n" + + "}"; + } +} diff --git a/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/FakeBoredApi.java b/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/FakeBoredApi.java new file mode 100644 index 0000000000000000000000000000000000000000..8e22a06b7e1006596ac4765b89b70a8dd7e172c7 --- /dev/null +++ b/borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/FakeBoredApi.java @@ -0,0 +1,31 @@ +package com.ippon.borestop.activity.infrastructure.secondary; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/fake-bored-api") +class FakeBoredApi { + private boolean slow; + + void reset() { + slow = false; + } + + void slowDown() { + slow = true; + } + + @GetMapping + ResponseEntity<BoredApiResponse> getResponse() throws InterruptedException { + if (slow) { + Thread.sleep(500); + } + + BoredApiResponse response = new BoredApiResponse("Hey", "cooking"); + + return ResponseEntity.ok(response); + } +} diff --git a/borestop/src/test/java/com/ippon/borestop/common/BorestopIntTest.java b/borestop/src/test/java/com/ippon/borestop/common/BorestopIntTest.java index 48faae9879284552740e7353d60987b2e36b4f4b..7590f5161aa46635e1a7e7b18048835d0568ddbb 100644 --- a/borestop/src/test/java/com/ippon/borestop/common/BorestopIntTest.java +++ b/borestop/src/test/java/com/ippon/borestop/common/BorestopIntTest.java @@ -2,16 +2,15 @@ package com.ippon.borestop.common; import com.ippon.borestop.BorestopApp; import io.github.jhipster.config.JHipsterConstants; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.ActiveProfiles; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import javax.transaction.Transactional; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.test.context.ActiveProfiles; -@Transactional @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @ActiveProfiles(JHipsterConstants.SPRING_PROFILE_TEST) diff --git a/borestop/src/test/resources/config/application.yml b/borestop/src/test/resources/config/application.yml index 5eb3945b2edf299e0e70cef33f924d4b2ce18a06..34c9cdd536e36bd8980fd4a62f4753c00e5423b7 100644 --- a/borestop/src/test/resources/config/application.yml +++ b/borestop/src/test/resources/config/application.yml @@ -115,3 +115,8 @@ jhipster: # =================================================================== # application: + +bored-api: + url: 'https://www.boredapi.com/api/activity/' + connect-timeout: 500ms + read-timeout: 1s