From 4db6946e3b6146cff86e271b8dbc6071384317fe Mon Sep 17 00:00:00 2001
From: Hippolyte Durix <hdurix@ippon.fr>
Date: Wed, 2 Sep 2020 14:12:19 +0200
Subject: [PATCH] Advancing in kata implementation

---
 borestop/README.md                            | 22 ++++++++
 .../borestop/activity/domain/Category.java    |  4 +-
 .../secondary/BoredApiCategory.java           | 37 +++++++++++++
 .../secondary/BoredApiConfiguration.java      |  8 +++
 .../secondary/BoredApiIdeasRepository.java    | 32 +++++++++++
 .../secondary/BoredApiProperties.java         | 42 +++++++++++++++
 .../secondary/BoredApiResponse.java           | 26 +++++++++
 .../secondary/RestIdeasRepository.java        | 15 ------
 .../src/main/resources/config/application.yml |  5 ++
 .../secondary/BoredApiCategoryUnitTest.java   | 29 ++++++++++
 ...piIdeasRepositoryCommunicationIntTest.java | 54 +++++++++++++++++++
 .../BoredApiIdeasRepositoryIntTest.java       | 23 ++++++++
 .../secondary/BoredApiResponseUnitTest.java   | 41 ++++++++++++++
 .../secondary/FakeBoredApi.java               | 31 +++++++++++
 .../borestop/common/BorestopIntTest.java      |  9 ++--
 .../src/test/resources/config/application.yml |  5 ++
 16 files changed, 362 insertions(+), 21 deletions(-)
 create mode 100644 borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiCategory.java
 create mode 100644 borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiConfiguration.java
 create mode 100644 borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiIdeasRepository.java
 create mode 100644 borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiProperties.java
 create mode 100644 borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiResponse.java
 delete mode 100644 borestop/src/main/java/com/ippon/borestop/activity/infrastructure/secondary/RestIdeasRepository.java
 create mode 100644 borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiCategoryUnitTest.java
 create mode 100644 borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiIdeasRepositoryCommunicationIntTest.java
 create mode 100644 borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiIdeasRepositoryIntTest.java
 create mode 100644 borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/BoredApiResponseUnitTest.java
 create mode 100644 borestop/src/test/java/com/ippon/borestop/activity/infrastructure/secondary/FakeBoredApi.java

diff --git a/borestop/README.md b/borestop/README.md
index 29c3fde7..c19e3819 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 101af047..48a9df30 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 00000000..231ff3dd
--- /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 00000000..54eb299b
--- /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 00000000..d386bfbc
--- /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 00000000..54313eb2
--- /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 00000000..a255c320
--- /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 ff375789..00000000
--- 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 c6f99b1b..7d74a71d 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 00000000..34c0772f
--- /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 00000000..2349c073
--- /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 00000000..721370b1
--- /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 00000000..08b90785
--- /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 00000000..8e22a06b
--- /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 48faae98..7590f516 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 5eb3945b..34c9cdd5 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
-- 
GitLab