Commit 2b87bd84 authored by Colin DAMON's avatar Colin DAMON
Browse files

Merge branch '29-bored-api-integration' into 'master'

Resolve "Bored API integration"

Closes #29

See merge request !25
parents 32d3a134 4db6946e
......@@ -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).
......
package com.ippon.borestop.activity.domain;
public enum Category {
RELAXATION
COOKING,
RELAXATION,
DEFAULT
}
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());
}
}
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 {}
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);
}
}
}
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;
}
}
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());
}
}
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;
}
}
......@@ -169,3 +169,8 @@ jhipster:
# ===================================================================
# application:
bored-api:
url: 'https://www.boredapi.com/api/activity/'
connect-timeout: 500ms
read-timeout: 1s
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);
}
}
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);
}
}
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();
}
}
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" +
"}";
}
}
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);
}
}
......@@ -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)
......
......@@ -115,3 +115,8 @@ jhipster:
# ===================================================================
# application:
bored-api:
url: 'https://www.boredapi.com/api/activity/'
connect-timeout: 500ms
read-timeout: 1s
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment