getListedOrders() {
19 | return $$("body > table tr").stream()
20 | .map(el -> el.findAll("td"))
21 | .filter(list -> !list.isEmpty())
22 | .map(list -> new Order(list.get(0).getText(), list.get(1).getText(), list.get(2).getText()))
23 | .collect(toList());
24 | }
25 |
26 | public OrderView followCreateOrderLink() {
27 | createOrderLink().click();
28 | return new OrderView();
29 | }
30 |
31 | private SelenideElement createOrderLink() {
32 | return $$("a").findBy(text("Create"));
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/systemtest-run-dev-env.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 |
4 | cd ${0%/*}/coffee-shop
5 |
6 | trap cleanup EXIT
7 |
8 | function cleanup() {
9 | echo stopping containers
10 | docker stop coffee-shop coffee-shop-db barista &> /dev/null || true
11 | }
12 |
13 |
14 | cleanup
15 |
16 | docker run -d --rm \
17 | --name coffee-shop-db \
18 | --network dkrnet \
19 | -p 5432:5432 \
20 | -e POSTGRES_USER=postgres \
21 | -e POSTGRES_PASSWORD=postgres \
22 | postgres:9.5
23 |
24 | docker run -d --rm \
25 | --name barista \
26 | --network dkrnet \
27 | -p 8002:8080 \
28 | rodolpheche/wiremock:2.6.0
29 |
30 |
31 | # coffee-shop
32 | mvn clean package -Dquarkus.package.type=mutable-jar
33 | docker build -f Dockerfile.dev -t tmp-builder .
34 |
35 | docker run -d --rm \
36 | --name coffee-shop \
37 | --network dkrnet \
38 | -p 8001:8080 \
39 | -p 5005:5005 \
40 | tmp-builder
41 |
42 | # wait for app startup
43 | while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:8001/health)" != "200" ]]; do
44 | sleep 2;
45 | done
46 |
47 | mvn quarkus:remote-dev -Ddebug=false -Dquarkus.live-reload.url=http://localhost:8001 -Dquarkus.live-reload.password=123 -Dquarkus.package.type=mutable-jar
--------------------------------------------------------------------------------
/coffee-shop/src/main/resources/templates/order.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Order coffee
8 |
9 |
10 |
11 |
12 | Order coffee
13 |
14 | Show all orders
15 |
16 | {#if failed}Could not create the order. Please select all values properly.
{/if}
17 |
18 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/it/CreateOrderQuarkusSmokeIT.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.it;
2 |
3 | import io.quarkus.test.junit.QuarkusTest;
4 | import org.junit.jupiter.api.AfterEach;
5 | import org.junit.jupiter.api.BeforeEach;
6 | import org.junit.jupiter.api.Disabled;
7 | import org.junit.jupiter.api.Test;
8 |
9 | import static org.assertj.core.api.Assertions.assertThat;
10 |
11 | @QuarkusTest
12 | @Disabled("too slow")
13 | class CreateOrderQuarkusSmokeIT {
14 |
15 | private CoffeeOrderSystem coffeeOrderSystem;
16 |
17 | @BeforeEach
18 | void setUp() {
19 | System.setProperty("coffee-shop.test.port", "8081");
20 | coffeeOrderSystem = new CoffeeOrderSystem();
21 | }
22 |
23 | @Test
24 | void testIsSystemUp() {
25 | assertThat(coffeeOrderSystem.isSystemUp()).isTrue();
26 | }
27 |
28 | @Test
29 | void testGetTypes() {
30 | assertThat(coffeeOrderSystem.getTypes()).containsExactlyInAnyOrder("Espresso", "Pour_over", "Latte");
31 | }
32 |
33 | @Test
34 | void testGetTypeOrigins() {
35 | assertThat(coffeeOrderSystem.getOrigins("Espresso")).containsExactlyInAnyOrder("Colombia", "Ethiopia");
36 | }
37 |
38 | @AfterEach
39 | void tearDown() {
40 | coffeeOrderSystem.close();
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/control/OrderProcessorTestDouble.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.control;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
4 | import com.sebastian_daschner.coffee_shop.orders.entity.OrderStatus;
5 | import org.assertj.core.api.Assertions;
6 | import org.mockito.ArgumentCaptor;
7 | import org.mockito.ArgumentMatcher;
8 | import org.mockito.Mockito;
9 |
10 | import javax.persistence.EntityManager;
11 | import java.util.List;
12 |
13 | import static org.assertj.core.api.Assertions.assertThat;
14 | import static org.mockito.ArgumentMatchers.any;
15 | import static org.mockito.Mockito.*;
16 |
17 | public class OrderProcessorTestDouble extends OrderProcessor {
18 |
19 | private final ArgumentCaptor orderCaptor;
20 |
21 | public OrderProcessorTestDouble() {
22 | entityManager = mock(EntityManager.class);
23 | barista = mock(Barista.class);
24 | orderCaptor = ArgumentCaptor.forClass(Order.class);
25 |
26 | when(barista.retrieveOrderStatus(orderCaptor.capture())).thenReturn(OrderStatus.PREPARING);
27 | }
28 |
29 | public void verifyProcessOrders(List orders) {
30 | verify(barista, times(orders.size())).retrieveOrderStatus(any());
31 | assertThat(orderCaptor.getAllValues()).containsExactlyElementsOf(orders);
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopTestDouble.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessorTestDouble;
4 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
5 |
6 | import javax.persistence.EntityManager;
7 | import javax.persistence.TypedQuery;
8 | import java.util.List;
9 |
10 | import static org.mockito.ArgumentMatchers.anyString;
11 | import static org.mockito.Mockito.*;
12 |
13 | public class CoffeeShopTestDouble extends CoffeeShop {
14 |
15 | public CoffeeShopTestDouble(OrderProcessorTestDouble orderProcessorTestDouble) {
16 | entityManager = mock(EntityManager.class);
17 | orderProcessor = orderProcessorTestDouble;
18 | }
19 |
20 | public void verifyCreateOrder(Order order) {
21 | verify(entityManager).merge(order);
22 | }
23 |
24 | public void verifyProcessUnfinishedOrders() {
25 | verify(entityManager).createNamedQuery(Order.FIND_UNFINISHED, Order.class);
26 | }
27 |
28 | public void answerForUnfinishedOrders(List orders) {
29 | @SuppressWarnings("unchecked")
30 | TypedQuery queryMock = mock(TypedQuery.class);
31 | when(entityManager.createNamedQuery(anyString(), eq(Order.class))).thenReturn(queryMock);
32 | when(queryMock.getResultList()).thenReturn(orders);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/entity/Origin.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.entity;
2 |
3 | import javax.persistence.*;
4 | import java.util.EnumSet;
5 | import java.util.Set;
6 |
7 | import static com.sebastian_daschner.coffee_shop.orders.entity.Origin.FIND_ALL;
8 |
9 | @Entity
10 | @Table(name = "origins")
11 | @NamedQuery(name = FIND_ALL, query = "select o from Origin o")
12 | public class Origin {
13 |
14 | public static final String FIND_ALL = "Origin.findAll";
15 |
16 | @Id
17 | private String name;
18 |
19 | @ElementCollection(fetch = FetchType.EAGER)
20 | @CollectionTable(name = "origin_coffee_types", joinColumns = @JoinColumn(name = "origin_name", nullable = false))
21 | @Column(name = "coffee_type", nullable = false)
22 | @Enumerated(value = EnumType.STRING)
23 | private Set coffeeTypes = EnumSet.noneOf(CoffeeType.class);
24 |
25 | public Origin() {
26 | }
27 |
28 | public Origin(final String name) {
29 | this.name = name;
30 | }
31 |
32 | public String getName() {
33 | return name;
34 | }
35 |
36 | public Set getCoffeeTypes() {
37 | return coffeeTypes;
38 | }
39 |
40 | @Override
41 | public String toString() {
42 | return "Origin{" +
43 | "name='" + name + '\'' +
44 | ", coffeeTypes=" + coffeeTypes +
45 | '}';
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/boundary/OriginsResource.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.control.EntityBuilder;
4 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType;
5 | import io.quarkus.arc.Unremovable;
6 |
7 | import javax.enterprise.context.RequestScoped;
8 | import javax.inject.Inject;
9 | import javax.json.Json;
10 | import javax.json.JsonArray;
11 | import javax.json.JsonArrayBuilder;
12 | import javax.ws.rs.GET;
13 | import javax.ws.rs.Path;
14 | import javax.ws.rs.PathParam;
15 | import javax.ws.rs.Produces;
16 | import javax.ws.rs.core.Context;
17 | import javax.ws.rs.core.MediaType;
18 | import javax.ws.rs.core.UriInfo;
19 |
20 | @Produces(MediaType.APPLICATION_JSON)
21 | @RequestScoped
22 | @Unremovable // https://github.com/quarkusio/quarkus/issues/5314
23 | public class OriginsResource {
24 |
25 | @Inject
26 | CoffeeShop coffeeShop;
27 |
28 | @PathParam("type")
29 | CoffeeType type;
30 |
31 | @Context
32 | UriInfo uriInfo;
33 |
34 | @Inject
35 | EntityBuilder entityBuilder;
36 |
37 | @GET
38 | public JsonArray getOrigins() {
39 | return coffeeShop.getOrigins(type).stream()
40 | .map(o -> entityBuilder.buildOrigin(uriInfo, o, type))
41 | .collect(Json::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::add).build();
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/boundary/TypesResource.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.control.EntityBuilder;
4 |
5 | import javax.enterprise.context.RequestScoped;
6 | import javax.inject.Inject;
7 | import javax.json.Json;
8 | import javax.json.JsonArray;
9 | import javax.json.JsonArrayBuilder;
10 | import javax.ws.rs.GET;
11 | import javax.ws.rs.Path;
12 | import javax.ws.rs.Produces;
13 | import javax.ws.rs.container.ResourceContext;
14 | import javax.ws.rs.core.Context;
15 | import javax.ws.rs.core.MediaType;
16 | import javax.ws.rs.core.UriInfo;
17 |
18 | @Path("types")
19 | @Produces(MediaType.APPLICATION_JSON)
20 | @RequestScoped
21 | public class TypesResource {
22 |
23 | @Inject
24 | CoffeeShop coffeeShop;
25 |
26 | @Inject
27 | EntityBuilder entityBuilder;
28 |
29 | @Context
30 | ResourceContext resourceContext;
31 |
32 | @Context
33 | UriInfo uriInfo;
34 |
35 | @GET
36 | public JsonArray getCoffeeTypes() {
37 | return coffeeShop.getCoffeeTypes().stream()
38 | .map(t -> entityBuilder.buildType(t, uriInfo))
39 | .collect(Json::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::add).build();
40 | }
41 |
42 | @Path("{type}/origins")
43 | public OriginsResource originsResource() {
44 | return resourceContext.getResource(OriginsResource.class);
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/TestData.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType;
4 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin;
6 |
7 | import java.util.EnumSet;
8 | import java.util.List;
9 | import java.util.Set;
10 | import java.util.UUID;
11 | import java.util.stream.Collectors;
12 | import java.util.stream.Stream;
13 |
14 | public final class TestData {
15 |
16 | private TestData() {
17 | }
18 |
19 | public static List unfinishedOrders() {
20 | Origin colombia = new Origin("Colombia");
21 | return List.of(
22 | new Order(UUID.randomUUID(), CoffeeType.ESPRESSO, colombia),
23 | new Order(UUID.randomUUID(), CoffeeType.LATTE, colombia),
24 | new Order(UUID.randomUUID(), CoffeeType.POUR_OVER, colombia)
25 | );
26 | }
27 |
28 | public static Set validCoffeeTypes() {
29 | return EnumSet.allOf(CoffeeType.class);
30 | }
31 |
32 | public static List validOrigins() {
33 | Set coffeeTypes = validCoffeeTypes();
34 |
35 | return Stream.of("Colombia", "Ethiopia")
36 | .map(Origin::new)
37 | .peek(o -> o.getCoffeeTypes().addAll(coffeeTypes))
38 | .collect(Collectors.toList());
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/coffee-shop/src/main/resources/META-INF/resources/form.js:
--------------------------------------------------------------------------------
1 | const typeSelect = document.querySelector('select[name=type]');
2 | const originSelect = document.querySelector('select[name=origin]');
3 | const submitButton = document.querySelector('button[type=submit]');
4 |
5 | function init() {
6 | typeSelect.addEventListener('change', ev => {
7 | const type = ev.target.value;
8 | if (type) updateOrigins(type);
9 | });
10 | }
11 |
12 | function updateOrigins(type) {
13 | fetch(`${window.location.origin}/types`)
14 | .then(res => res.json())
15 | .then(json => {
16 | const url = json.filter(t => t.type === type)
17 | .map(t => t['_links']['origins']);
18 | fetch(url)
19 | .then(res => res.json())
20 | .then(json => {
21 | originSelect.querySelectorAll('option').forEach(el => {
22 | if (el.value) el.remove();
23 | });
24 | originSelect.removeAttribute('disabled');
25 | submitButton.removeAttribute('disabled');
26 | json.map(t => t.origin)
27 | .forEach(origin => {
28 | const option = document.createElement('option');
29 | option.value = option.innerText = origin;
30 | originSelect.appendChild(option);
31 | })
32 | });
33 | });
34 | }
35 |
36 | init();
--------------------------------------------------------------------------------
/coffee-shop-st/src/test/java/cucumber/StepDefs.java:
--------------------------------------------------------------------------------
1 | package cucumber;
2 |
3 | import com.sebastian_daschner.coffee_shop.backend.entity.Order;
4 | import com.sebastian_daschner.coffee_shop.backend.systems.CoffeeOrderSystem;
5 | import cucumber.api.java.After;
6 | import cucumber.api.java.Before;
7 | import cucumber.api.java.en.Then;
8 | import cucumber.api.java.en.When;
9 |
10 | import java.net.URI;
11 |
12 | import static org.assertj.core.api.Assertions.assertThat;
13 |
14 | public class StepDefs {
15 |
16 | private CoffeeOrderSystem coffeeOrderSystem;
17 | private Order order;
18 |
19 | @Before
20 | public void setUp() {
21 | coffeeOrderSystem = new CoffeeOrderSystem();
22 | }
23 |
24 | @When("^I create an order with ([^ ]*) from ([^ ]*)$")
25 | public void i_create_an_order(String type, String origin) {
26 | order = new Order(type, origin);
27 | }
28 |
29 | @Then("^The order should be accepted$")
30 | public void accepted_order() {
31 | URI id = coffeeOrderSystem.createOrder(order);
32 | Order order = coffeeOrderSystem.getOrder(id);
33 | assertOrderMatches(this.order, order);
34 | }
35 |
36 | @Then("^The order should be rejected$")
37 | public void rejected_order() {
38 | coffeeOrderSystem.createInvalidOrder(order);
39 | }
40 |
41 | private void assertOrderMatches(Order actual, Order expected) {
42 | assertThat(actual).isEqualToComparingOnlyGivenFields(expected, "type", "origin");
43 | }
44 |
45 | @After
46 | public void close() {
47 | coffeeOrderSystem.close();
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopUseCaseTest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessorTestDouble;
4 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
6 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin;
7 | import org.junit.jupiter.api.BeforeEach;
8 | import org.junit.jupiter.api.Test;
9 |
10 | import java.util.Arrays;
11 | import java.util.List;
12 | import java.util.UUID;
13 |
14 | class CoffeeShopUseCaseTest {
15 |
16 | private CoffeeShopTestDouble coffeeShop;
17 | private OrderProcessorTestDouble orderProcessor;
18 |
19 | @BeforeEach
20 | void setUp() {
21 | orderProcessor = new OrderProcessorTestDouble();
22 | coffeeShop = new CoffeeShopTestDouble(orderProcessor);
23 | }
24 |
25 | @Test
26 | void testCreateOrder() {
27 | Order order = new Order();
28 | coffeeShop.createOrder(order);
29 | coffeeShop.verifyCreateOrder(order);
30 | }
31 |
32 | @Test
33 | void testProcessUnfinishedOrders() {
34 | List orders = Arrays.asList(new Order(UUID.randomUUID(), CoffeeType.ESPRESSO, new Origin("Colombia")),
35 | new Order(UUID.randomUUID(), CoffeeType.ESPRESSO, new Origin("Ethiopia")));
36 | coffeeShop.answerForUnfinishedOrders(orders);
37 |
38 | coffeeShop.processUnfinishedOrders();
39 |
40 | coffeeShop.verifyProcessUnfinishedOrders();
41 | orderProcessor.verifyProcessOrders(orders);
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/control/OrderValidator.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.control;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.boundary.CoffeeShop;
4 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin;
6 | import com.sebastian_daschner.coffee_shop.orders.entity.ValidOrder;
7 |
8 | import javax.enterprise.context.ApplicationScoped;
9 | import javax.inject.Inject;
10 | import javax.json.JsonObject;
11 | import javax.validation.ConstraintValidator;
12 | import javax.validation.ConstraintValidatorContext;
13 |
14 | @ApplicationScoped
15 | public class OrderValidator implements ConstraintValidator {
16 |
17 | @Inject
18 | CoffeeShop coffeeShop;
19 |
20 | public void initialize(ValidOrder constraint) {
21 | // nothing to do
22 | }
23 |
24 | public boolean isValid(JsonObject json, ConstraintValidatorContext context) {
25 |
26 | final String type = json.getString("type", null);
27 | final String origin = json.getString("origin", null);
28 |
29 | if (type == null || origin == null)
30 | return false;
31 |
32 | final CoffeeType coffeeType = coffeeShop.getCoffeeTypes().stream()
33 | .filter(t -> t.name().equalsIgnoreCase(type))
34 | .findAny().orElse(null);
35 |
36 | final Origin coffeeOrigin = coffeeShop.getOrigin(origin);
37 |
38 | if (coffeeOrigin == null || coffeeType == null)
39 | return false;
40 |
41 | return coffeeOrigin.getCoffeeTypes().contains(coffeeType);
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/coffee-shop-st/src/test/java/com/sebastian_daschner/coffee_shop/frontend/OrderView.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.frontend;
2 |
3 | import com.codeborne.selenide.SelenideElement;
4 | import org.openqa.selenium.Keys;
5 |
6 | import static com.codeborne.selenide.Condition.enabled;
7 | import static com.codeborne.selenide.Selenide.$;
8 | import static com.codeborne.selenide.Selenide.actions;
9 |
10 | public class OrderView {
11 |
12 | private final SelenideElement typeSelect = $("select[name=type]");
13 | private final SelenideElement originSelect = $("select[name=origin]");
14 | private final SelenideElement submitButton = $("button[type=submit]");
15 |
16 | public String getPageHeader() {
17 | return $("body > h1").text();
18 | }
19 |
20 | public IndexView orderCoffee(String type, String origin) {
21 | typeSelect.selectOptionContainingText(type);
22 | originSelect.shouldBe(enabled);
23 | originSelect.selectOptionContainingText(origin);
24 | submitButton.click();
25 | return new IndexView();
26 | }
27 |
28 | public IndexView orderCoffeeSelectWithKeyboard(String type, String origin) {
29 | selectWithKeyboard(type, typeSelect);
30 | originSelect.shouldBe(enabled);
31 | selectWithKeyboard(origin, originSelect);
32 |
33 | actions()
34 | .sendKeys(Keys.TAB)
35 | .sendKeys(Keys.ENTER)
36 | .perform();
37 |
38 | return new IndexView();
39 | }
40 |
41 | public void selectWithKeyboard(String type, SelenideElement select) {
42 | while (!type.equals(select.getSelectedText()))
43 | select.getWrappedElement().sendKeys(Keys.ARROW_DOWN);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/coffee-shop-st/src/test/java/com/sebastian_daschner/coffee_shop/backend/CreateOrderValidationTest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.backend;
2 |
3 | import com.sebastian_daschner.coffee_shop.backend.entity.Order;
4 | import com.sebastian_daschner.coffee_shop.backend.systems.CoffeeOrderSystem;
5 | import org.junit.jupiter.api.AfterEach;
6 | import org.junit.jupiter.api.BeforeEach;
7 | import org.junit.jupiter.api.Test;
8 |
9 | class CreateOrderValidationTest {
10 |
11 | private CoffeeOrderSystem coffeeOrderSystem;
12 |
13 | @BeforeEach
14 | void setUp() {
15 | coffeeOrderSystem = new CoffeeOrderSystem();
16 | }
17 |
18 | @Test
19 | void invalidEmptyOrder() {
20 | coffeeOrderSystem.createInvalidOrder(new Order());
21 | }
22 |
23 | @Test
24 | void invalidEmptyCoffeeType() {
25 | createOrder(null, "Colombia");
26 | }
27 |
28 | @Test
29 | void invalidEmptyOrigin() {
30 | createOrder("Espresso", null);
31 | }
32 |
33 | @Test
34 | void invalidCoffeeType() {
35 | createOrder("Siphon", "Colombia");
36 | }
37 |
38 | @Test
39 | void invalidCoffeeOrigin() {
40 | createOrder("Espresso", "Germany");
41 | }
42 |
43 | @Test
44 | void invalidEmptyCoffeeTypeInvalidOrigin() {
45 | createOrder(null, "Germany");
46 | }
47 |
48 | @Test
49 | void invalidEmptyOriginInvalidCoffeeType() {
50 | createOrder("Siphon", null);
51 | }
52 |
53 | private void createOrder(String o, String colombia) {
54 | Order order = new Order(o, colombia);
55 | coffeeOrderSystem.createInvalidOrder(order);
56 | }
57 |
58 | @AfterEach
59 | void close() {
60 | coffeeOrderSystem.close();
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/coffee-shop-st/src/test/java/com/sebastian_daschner/coffee_shop/frontend/CreateOrderUITest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.frontend;
2 |
3 | import org.junit.jupiter.api.BeforeEach;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import java.util.List;
7 |
8 | import static org.assertj.core.api.Assertions.assertThat;
9 |
10 | public class CreateOrderUITest {
11 |
12 | private final CoffeeShopUI coffeeShop = new CoffeeShopUI();
13 |
14 | @BeforeEach
15 | void setUp() {
16 | coffeeShop.init();
17 | }
18 |
19 | @Test
20 | void create_coffee_order() {
21 | IndexView index = coffeeShop.index();
22 | int numberOrders = index.getListedOrders().size();
23 |
24 | OrderView orderView = index.followCreateOrderLink();
25 | assertThat(orderView.getPageHeader()).isEqualTo("Order coffee");
26 | index = orderView.orderCoffee("Espresso", "Colombia");
27 |
28 | List orders = index.getListedOrders();
29 | assertThat(orders).hasSize(numberOrders + 1);
30 | Order order = orders.get(orders.size() - 1);
31 | assertThat(order.type).isEqualTo("Espresso");
32 | assertThat(order.origin).isEqualTo("Colombia");
33 | }
34 |
35 | @Test
36 | void create_coffee_order_keyboard_select() {
37 | IndexView index = coffeeShop.index();
38 | int numberOrders = index.getListedOrders().size();
39 |
40 | OrderView orderView = index.followCreateOrderLink();
41 | assertThat(orderView.getPageHeader()).isEqualTo("Order coffee");
42 | index = orderView.orderCoffeeSelectWithKeyboard("Espresso", "Colombia");
43 |
44 | List orders = index.getListedOrders();
45 | assertThat(orders).hasSize(numberOrders + 1);
46 | Order order = orders.get(orders.size() - 1);
47 | assertThat(order.type).isEqualTo("Espresso");
48 | assertThat(order.origin).isEqualTo("Colombia");
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopTest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.TestData;
4 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessor;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
6 | import org.junit.jupiter.api.BeforeEach;
7 | import org.junit.jupiter.api.Test;
8 | import org.mockito.ArgumentCaptor;
9 |
10 | import javax.persistence.EntityManager;
11 | import javax.persistence.TypedQuery;
12 | import java.util.List;
13 |
14 | import static com.sebastian_daschner.coffee_shop.orders.entity.OrderAssert.assertThat;
15 | import static org.assertj.core.api.Assertions.assertThat;
16 | import static org.mockito.Mockito.*;
17 |
18 | class CoffeeShopTest {
19 |
20 | private CoffeeShop testObject;
21 |
22 | @BeforeEach
23 | void setUp() {
24 | testObject = new CoffeeShop();
25 | testObject.entityManager = mock(EntityManager.class);
26 | testObject.orderProcessor = mock(OrderProcessor.class);
27 | }
28 |
29 | @Test
30 | @SuppressWarnings("unchecked")
31 | void testProcessUnfinishedOrders() {
32 | List desiredOrders = TestData.unfinishedOrders();
33 |
34 | TypedQuery mockQuery = mock(TypedQuery.class);
35 | when(testObject.entityManager.createNamedQuery(Order.FIND_UNFINISHED, Order.class)).thenReturn(mockQuery);
36 | when(mockQuery.getResultList()).thenReturn(desiredOrders);
37 | ArgumentCaptor orderCaptor = ArgumentCaptor.forClass(Order.class);
38 |
39 | testObject.processUnfinishedOrders();
40 |
41 | verify(testObject.entityManager).createNamedQuery(Order.FIND_UNFINISHED, Order.class);
42 | verify(testObject.orderProcessor, times(desiredOrders.size())).processOrder(orderCaptor.capture());
43 |
44 | assertThat(orderCaptor.getAllValues()).containsExactlyElementsOf(desiredOrders);
45 |
46 | orderCaptor.getAllValues().forEach(o -> assertThat(o).isPreparing());
47 | }
48 |
49 | }
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopMockitoTest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.TestData;
4 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessor;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
6 | import org.junit.jupiter.api.Test;
7 | import org.junit.jupiter.api.extension.ExtendWith;
8 | import org.mockito.ArgumentCaptor;
9 | import org.mockito.Captor;
10 | import org.mockito.InjectMocks;
11 | import org.mockito.Mock;
12 | import org.mockito.junit.jupiter.MockitoExtension;
13 |
14 | import javax.persistence.EntityManager;
15 | import javax.persistence.TypedQuery;
16 | import java.util.List;
17 |
18 | import static org.assertj.core.api.Assertions.assertThat;
19 | import static org.mockito.Mockito.*;
20 |
21 | @ExtendWith(MockitoExtension.class)
22 | class CoffeeShopMockitoTest {
23 |
24 | @InjectMocks
25 | CoffeeShop testObject;
26 |
27 | @Captor
28 | ArgumentCaptor orderCaptor;
29 |
30 | private EntityManager entityManager;
31 | private OrderProcessor orderProcessor;
32 |
33 | CoffeeShopMockitoTest(@Mock EntityManager entityManager, @Mock OrderProcessor orderProcessor) {
34 | this.entityManager = entityManager;
35 | this.orderProcessor = orderProcessor;
36 | }
37 |
38 | @Test
39 | void testProcessUnfinishedOrders(@Mock TypedQuery mockQuery) {
40 | when(entityManager.createNamedQuery(Order.FIND_UNFINISHED, Order.class)).thenReturn(mockQuery);
41 | List desiredOrders = TestData.unfinishedOrders();
42 | when(mockQuery.getResultList()).thenReturn(desiredOrders);
43 |
44 | testObject.processUnfinishedOrders();
45 |
46 | verify(entityManager).createNamedQuery(Order.FIND_UNFINISHED, Order.class);
47 | verify(orderProcessor, times(desiredOrders.size())).processOrder(orderCaptor.capture());
48 |
49 | assertThat(orderCaptor.getAllValues()).containsExactlyElementsOf(desiredOrders);
50 | }
51 |
52 | }
--------------------------------------------------------------------------------
/coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/boundary/OrderCoffeeController.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType;
4 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin;
6 | import io.quarkus.qute.Template;
7 | import io.quarkus.qute.TemplateInstance;
8 | import io.quarkus.qute.api.ResourcePath;
9 |
10 | import javax.inject.Inject;
11 | import javax.ws.rs.*;
12 | import javax.ws.rs.core.MediaType;
13 | import javax.ws.rs.core.Response;
14 | import java.net.URI;
15 | import java.util.Set;
16 | import java.util.UUID;
17 |
18 | @Path("order.html")
19 | @Produces(MediaType.TEXT_HTML)
20 | public class OrderCoffeeController {
21 |
22 | @Inject
23 | CoffeeShop coffeeShop;
24 |
25 | @ResourcePath("order.html")
26 | Template orderTemplate;
27 |
28 | @GET
29 | public TemplateInstance index() {
30 | Set types = coffeeShop.getCoffeeTypes();
31 | return orderTemplate.data("types", types);
32 | }
33 |
34 | @POST
35 | @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
36 | public Response submit(@FormParam("type") @DefaultValue("") String type, @FormParam("origin") @DefaultValue("") String originName) {
37 | CoffeeType coffeeType = CoffeeType.fromString(type);
38 | Origin origin = new Origin(originName);
39 | Order order = new Order(UUID.randomUUID(), coffeeType, origin);
40 |
41 | if (!orderIsValid(order)) {
42 | Set types = coffeeShop.getCoffeeTypes();
43 | return Response.ok(orderTemplate
44 | .data("failed", true)
45 | .data("types", types))
46 | .build();
47 | }
48 |
49 | coffeeShop.createOrder(order);
50 |
51 | return Response.seeOther(URI.create("/index.html")).build();
52 | }
53 |
54 | private boolean orderIsValid(Order order) {
55 | if (order.getType() == null || order.getOrigin() == null)
56 | return false;
57 | Origin origin = coffeeShop.getOrigin(order.getOrigin().getName());
58 | return origin != null;
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/control/StepDefs.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.control;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.TestData;
4 | import com.sebastian_daschner.coffee_shop.orders.boundary.CoffeeShop;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType;
6 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin;
7 | import cucumber.api.java.Before;
8 | import cucumber.api.java.en.Then;
9 | import cucumber.api.java.en.When;
10 |
11 | import javax.json.Json;
12 | import javax.json.JsonObject;
13 | import javax.validation.ConstraintValidatorContext;
14 | import java.util.List;
15 | import java.util.Set;
16 |
17 | import static org.assertj.core.api.Assertions.assertThat;
18 | import static org.mockito.ArgumentMatchers.anyString;
19 | import static org.mockito.Mockito.mock;
20 | import static org.mockito.Mockito.when;
21 |
22 | public class StepDefs {
23 |
24 | private OrderValidator testObject;
25 | private ConstraintValidatorContext context;
26 | private JsonObject jsonObject;
27 |
28 | @Before
29 | public void setUp() {
30 | testObject = new OrderValidator();
31 | testObject.coffeeShop = mock(CoffeeShop.class);
32 | context = mock(ConstraintValidatorContext.class);
33 |
34 | List origins = TestData.validOrigins();
35 | Set coffeeTypes = TestData.validCoffeeTypes();
36 |
37 | when(testObject.coffeeShop.getCoffeeTypes()).thenReturn(coffeeTypes);
38 | when(testObject.coffeeShop.getOrigin(anyString())).then(invocation -> origins.stream()
39 | .filter(o -> o.getName().equals(invocation.getArgument(0)))
40 | .findAny()
41 | .orElse(null));
42 | }
43 |
44 | @When("^I create an order with ([^ ]*) from ([^ ]*)$")
45 | public void i_create_an_order(String type, String origin) {
46 | jsonObject = Json.createObjectBuilder()
47 | .add("type", type)
48 | .add("origin", origin)
49 | .build();
50 | }
51 |
52 | @Then("^The order should be accepted$")
53 | public void accepted_order() {
54 | assertThat(testObject.isValid(jsonObject, context)).isTrue();
55 | }
56 |
57 | @Then("^The order should be rejected$")
58 | public void rejected_order() {
59 | assertThat(testObject.isValid(jsonObject, context)).isFalse();
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/coffee-shop-st/src/test/java/com/sebastian_daschner/coffee_shop/backend/CreateOrderTest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.backend;
2 |
3 | import com.sebastian_daschner.coffee_shop.backend.entity.Order;
4 | import com.sebastian_daschner.coffee_shop.backend.systems.BaristaSystem;
5 | import com.sebastian_daschner.coffee_shop.backend.systems.CoffeeOrderSystem;
6 | import org.assertj.core.api.Assertions;
7 | import org.junit.jupiter.api.AfterEach;
8 | import org.junit.jupiter.api.BeforeEach;
9 | import org.junit.jupiter.api.Test;
10 |
11 | import java.net.URI;
12 |
13 | import static org.assertj.core.api.Assertions.assertThat;
14 |
15 | class CreateOrderTest {
16 |
17 | private CoffeeOrderSystem coffeeOrderSystem;
18 | private BaristaSystem baristaSystem;
19 |
20 | @BeforeEach
21 | void setUp() {
22 | coffeeOrderSystem = new CoffeeOrderSystem();
23 | baristaSystem = new BaristaSystem();
24 | }
25 |
26 | @Test
27 | void createVerifyOrder() {
28 | Order order = new Order("Espresso", "Colombia");
29 | URI orderUri = coffeeOrderSystem.createOrder(order);
30 |
31 | Order loadedOrder = coffeeOrderSystem.getOrder(orderUri);
32 | Assertions.assertThat(loadedOrder).isEqualToComparingOnlyGivenFields(order, "type", "origin");
33 |
34 | Assertions.assertThat(coffeeOrderSystem.getOrders()).contains(orderUri);
35 | }
36 |
37 | @Test
38 | void createOrderCheckStatusUpdate() {
39 | Order order = new Order("Espresso", "Colombia");
40 | URI orderUri = coffeeOrderSystem.createOrder(order);
41 |
42 | baristaSystem.answerForOrder(orderUri, "PREPARING");
43 |
44 | Order loadedOrder = coffeeOrderSystem.getOrder(orderUri);
45 | Assertions.assertThat(loadedOrder).isEqualToComparingOnlyGivenFields(order, "type", "origin");
46 |
47 | loadedOrder = waitForProcessAndGet(orderUri, "PREPARING");
48 | Assertions.assertThat(loadedOrder.getStatus()).isEqualTo("Preparing");
49 |
50 | baristaSystem.answerForOrder(orderUri, "FINISHED");
51 |
52 | loadedOrder = waitForProcessAndGet(orderUri, "FINISHED");
53 | Assertions.assertThat(loadedOrder.getStatus()).isEqualTo("Finished");
54 | }
55 |
56 | private Order waitForProcessAndGet(URI orderUri, String requestedStatus) {
57 | baristaSystem.waitForInvocation(orderUri, requestedStatus);
58 | return coffeeOrderSystem.getOrder(orderUri);
59 | }
60 |
61 | @AfterEach
62 | void close() {
63 | coffeeOrderSystem.close();
64 | }
65 |
66 | }
--------------------------------------------------------------------------------
/coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/boundary/OrdersResource.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.control.EntityBuilder;
4 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.ValidOrder;
6 |
7 | import javax.inject.Inject;
8 | import javax.json.JsonArray;
9 | import javax.json.JsonObject;
10 | import javax.servlet.http.HttpServletRequest;
11 | import javax.validation.Valid;
12 | import javax.ws.rs.*;
13 | import javax.ws.rs.core.Context;
14 | import javax.ws.rs.core.MediaType;
15 | import javax.ws.rs.core.Response;
16 | import javax.ws.rs.core.UriInfo;
17 | import java.net.URI;
18 | import java.util.List;
19 | import java.util.UUID;
20 |
21 | @Path("orders")
22 | @Produces(MediaType.APPLICATION_JSON)
23 | @Consumes(MediaType.APPLICATION_JSON)
24 | public class OrdersResource {
25 |
26 | @Inject
27 | CoffeeShop coffeeShop;
28 |
29 | @Inject
30 | EntityBuilder entityBuilder;
31 |
32 | @Context
33 | UriInfo uriInfo;
34 |
35 | @Context
36 | HttpServletRequest request;
37 |
38 | @GET
39 | public JsonArray getOrders() {
40 | List orders = coffeeShop.getOrders();
41 | return entityBuilder.buildOrders(orders, uriInfo, request);
42 | }
43 |
44 | @PUT
45 | @Path("{id}")
46 | public void updateOrder(@PathParam("id") UUID id, JsonObject jsonObject) {
47 | Order order = entityBuilder.buildOrder(jsonObject);
48 | coffeeShop.updateOrder(id, order);
49 | }
50 |
51 | @GET
52 | @Path("{id}")
53 | public JsonObject getOrder(@PathParam("id") UUID id) {
54 | final Order order = coffeeShop.getOrder(id);
55 |
56 | if (order == null)
57 | throw new NotFoundException();
58 |
59 | return entityBuilder.buildOrder(order);
60 | }
61 |
62 | @POST
63 | public Response createOrder(@Valid @ValidOrder JsonObject json) {
64 | final Order order = entityBuilder.buildOrder(json);
65 |
66 | coffeeShop.createOrder(order);
67 |
68 | return Response.created(buildUri(order)).build();
69 | }
70 |
71 | private URI buildUri(Order order) {
72 | return uriInfo.getBaseUriBuilder()
73 | .host(request.getServerName())
74 | .port(request.getServerPort())
75 | .path(OrdersResource.class)
76 | .path(OrdersResource.class, "getOrder")
77 | .build(order.getId());
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/entity/Order.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.entity;
2 |
3 | import javax.persistence.*;
4 | import java.util.Objects;
5 | import java.util.UUID;
6 |
7 | import static com.sebastian_daschner.coffee_shop.orders.entity.Order.FIND_ALL;
8 | import static com.sebastian_daschner.coffee_shop.orders.entity.Order.FIND_UNFINISHED;
9 |
10 | @Entity
11 | @Table(name = "orders")
12 | @NamedQueries({
13 | @NamedQuery(name = FIND_UNFINISHED, query = "select o from Order o where o.status <> " +
14 | "com.sebastian_daschner.coffee_shop.orders.entity.OrderStatus.COLLECTED"),
15 | @NamedQuery(name = FIND_ALL, query = "select o from Order o")})
16 | public class Order {
17 |
18 | public static final String FIND_UNFINISHED = "Order.findUnfinished";
19 | public static final String FIND_ALL = "Order.findAll";
20 |
21 | @Id
22 | private String id;
23 |
24 | @Basic(optional = false)
25 | @Enumerated(EnumType.STRING)
26 | private CoffeeType type;
27 |
28 | @ManyToOne(optional = false)
29 | private Origin origin;
30 |
31 | @Basic(optional = false)
32 | @Enumerated(EnumType.STRING)
33 | private OrderStatus status = OrderStatus.PREPARING;
34 |
35 | public Order() {
36 | }
37 |
38 | public Order(UUID id, CoffeeType type, Origin origin) {
39 | Objects.requireNonNull(id);
40 | Objects.requireNonNull(type);
41 | Objects.requireNonNull(origin);
42 | this.id = id.toString();
43 | this.type = type;
44 | this.origin = origin;
45 | }
46 |
47 | public String getId() {
48 | return id;
49 | }
50 |
51 | public void setId(String id) {
52 | this.id = id;
53 | }
54 |
55 | public CoffeeType getType() {
56 | return type;
57 | }
58 |
59 | public void setType(CoffeeType type) {
60 | this.type = type;
61 | }
62 |
63 | public Origin getOrigin() {
64 | return origin;
65 | }
66 |
67 | public void setOrigin(Origin origin) {
68 | this.origin = origin;
69 | }
70 |
71 | public OrderStatus getStatus() {
72 | return status;
73 | }
74 |
75 | public void setStatus(OrderStatus status) {
76 | this.status = status;
77 | }
78 |
79 | @Override
80 | public String toString() {
81 | return "Order{" +
82 | "id='" + id + '\'' +
83 | ", type='" + type + '\'' +
84 | ", origin='" + origin + '\'' +
85 | ", status=" + status +
86 | '}';
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShop.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessor;
4 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
6 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin;
7 | import io.quarkus.scheduler.Scheduled;
8 |
9 | import javax.enterprise.context.ApplicationScoped;
10 | import javax.inject.Inject;
11 | import javax.persistence.EntityManager;
12 | import javax.persistence.PersistenceContext;
13 | import javax.transaction.Transactional;
14 | import java.util.EnumSet;
15 | import java.util.List;
16 | import java.util.Set;
17 | import java.util.UUID;
18 | import java.util.stream.Collectors;
19 |
20 | @ApplicationScoped
21 | @Transactional
22 | public class CoffeeShop {
23 |
24 | @PersistenceContext
25 | EntityManager entityManager;
26 |
27 | @Inject
28 | OrderProcessor orderProcessor;
29 |
30 | public Set getCoffeeTypes() {
31 | return EnumSet.of(CoffeeType.ESPRESSO, CoffeeType.LATTE, CoffeeType.POUR_OVER);
32 | }
33 |
34 | public Set getOrigins(final CoffeeType type) {
35 | return entityManager.createNamedQuery(Origin.FIND_ALL, Origin.class)
36 | .getResultList().stream()
37 | .filter(t -> t.getCoffeeTypes().contains(type))
38 | .collect(Collectors.toSet());
39 | }
40 |
41 | public Origin getOrigin(String name) {
42 | return entityManager.find(Origin.class, name);
43 | }
44 |
45 | public void createOrder(Order order) {
46 | entityManager.merge(order);
47 | entityManager.flush();
48 | }
49 |
50 | public Order getOrder(UUID id) {
51 | return entityManager.find(Order.class, id.toString());
52 | }
53 |
54 | public List getOrders() {
55 | return entityManager.createNamedQuery(Order.FIND_ALL, Order.class)
56 | .getResultList();
57 | }
58 |
59 | @Scheduled(every = "2s")
60 | public void processUnfinishedOrders() {
61 | entityManager.createNamedQuery(Order.FIND_UNFINISHED, Order.class)
62 | .getResultList()
63 | .forEach(orderProcessor::processOrder);
64 | }
65 |
66 | public void updateOrder(UUID id, Order order) {
67 | Order managedOrder = entityManager.find(Order.class, id.toString());
68 | managedOrder.setType(order.getType());
69 | managedOrder.setOrigin(order.getOrigin());
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/control/Barista.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.control;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
4 | import com.sebastian_daschner.coffee_shop.orders.entity.OrderStatus;
5 | import org.eclipse.microprofile.config.inject.ConfigProperty;
6 |
7 | import javax.annotation.PostConstruct;
8 | import javax.enterprise.context.ApplicationScoped;
9 | import javax.inject.Inject;
10 | import javax.json.Json;
11 | import javax.json.JsonObject;
12 | import javax.ws.rs.client.Client;
13 | import javax.ws.rs.client.ClientBuilder;
14 | import javax.ws.rs.client.Entity;
15 | import javax.ws.rs.client.WebTarget;
16 | import javax.ws.rs.core.Response;
17 |
18 | @ApplicationScoped
19 | public class Barista {
20 |
21 | WebTarget target;
22 |
23 | @Inject
24 | @ConfigProperty(name = "barista.url")
25 | String baristaUrl;
26 |
27 | @PostConstruct
28 | void initClient() {
29 | final Client client = ClientBuilder.newClient();
30 | target = client.target(baristaUrl);
31 | }
32 |
33 | public OrderStatus retrieveOrderStatus(Order order) {
34 | JsonObject requestJson = buildRequestJson(order);
35 |
36 | JsonObject responseJson = sendRequest(requestJson);
37 |
38 | return readStatus(responseJson);
39 | }
40 |
41 | private JsonObject buildRequestJson(Order order) {
42 | return Json.createObjectBuilder()
43 | .add("order", order.getId())
44 | .add("type", order.getType().name().toUpperCase())
45 | .add("origin", order.getOrigin().getName().toUpperCase())
46 | .add("status", order.getStatus().name().toUpperCase())
47 | .build();
48 | }
49 |
50 | private JsonObject sendRequest(final JsonObject requestBody) {
51 | Response response = target.request().buildPost(Entity.json(requestBody)).invoke();
52 |
53 | if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) {
54 | throw new RuntimeException("Could not successfully process order, response from " + target.getUri() + " was " + response.getStatus());
55 | }
56 |
57 | return response.readEntity(JsonObject.class);
58 | }
59 |
60 | private OrderStatus readStatus(final JsonObject responseJson) {
61 | final OrderStatus status = OrderStatus.fromString(responseJson.getString("status", null));
62 |
63 | if (status == null)
64 | throw new RuntimeException("Could not read known status from response" + responseJson);
65 |
66 | return status;
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/coffee-shop-st/src/test/java/com/sebastian_daschner/coffee_shop/backend/systems/BaristaSystem.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.backend.systems;
2 |
3 | import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
4 | import com.github.tomakehurst.wiremock.matching.ContentPattern;
5 | import com.github.tomakehurst.wiremock.verification.LoggedRequest;
6 |
7 | import java.net.URI;
8 | import java.util.Collections;
9 | import java.util.List;
10 | import java.util.concurrent.locks.LockSupport;
11 |
12 | import static com.github.tomakehurst.wiremock.client.WireMock.*;
13 |
14 | public class BaristaSystem {
15 |
16 | public BaristaSystem() {
17 | String host = System.getProperty("barista.test.host", "localhost");
18 | int port = Integer.parseInt(System.getProperty("barista.test.port", "8002"));
19 |
20 | configureFor(host, port);
21 |
22 | stubFor(post("/processes").willReturn(responseJson("PREPARING")));
23 | }
24 |
25 | private ResponseDefinitionBuilder responseJson(String status) {
26 | return ResponseDefinitionBuilder.okForJson(Collections.singletonMap("status", status));
27 | }
28 |
29 | public void answerForOrder(URI orderUri, String status) {
30 | String orderId = extractId(orderUri);
31 | stubFor(post("/processes")
32 | .withRequestBody(requestJson(orderId))
33 | .willReturn(responseJson(status)));
34 | }
35 |
36 | private String extractId(URI orderUri) {
37 | String string = orderUri.toString();
38 | return string.substring(string.lastIndexOf('/') + 1);
39 | }
40 |
41 | private ContentPattern> requestJson(String orderId) {
42 | return equalToJson("{\"order\":\"" + orderId + "\"}", true, true);
43 | }
44 |
45 | private ContentPattern> requestJson(String orderId, String status) {
46 | return equalToJson("{\"order\":\"" + orderId + "\",\"status\":\"" + status + "\"}", true, true);
47 | }
48 |
49 | public void waitForInvocation(URI orderUri, String status) {
50 | long timeout = System.currentTimeMillis() + 60_000L;
51 |
52 | String orderId = extractId(orderUri);
53 | while (!requestMatched(status, orderId)) {
54 | LockSupport.parkNanos(2_000_000_000L);
55 | if (System.currentTimeMillis() > timeout)
56 | throw new AssertionError("Invocation hasn't happened within timeout");
57 | }
58 | }
59 |
60 | private boolean requestMatched(String status, String orderId) {
61 | List requests = findAll(postRequestedFor(urlEqualTo("/processes"))
62 | .withRequestBody(requestJson(orderId, status)));
63 | return !requests.isEmpty();
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/control/OrderValidatorTest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.control;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.boundary.CoffeeShop;
4 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin;
6 | import org.junit.jupiter.api.BeforeEach;
7 | import org.junit.jupiter.params.ParameterizedTest;
8 | import org.junit.jupiter.params.provider.MethodSource;
9 |
10 | import javax.json.Json;
11 | import javax.json.JsonObject;
12 | import javax.validation.ConstraintValidatorContext;
13 | import java.io.StringReader;
14 | import java.util.Collection;
15 | import java.util.EnumSet;
16 | import java.util.List;
17 | import java.util.Set;
18 |
19 | import static org.assertj.core.api.Assertions.assertThat;
20 | import static org.mockito.Mockito.mock;
21 | import static org.mockito.Mockito.when;
22 |
23 | class OrderValidatorTest {
24 |
25 | private OrderValidator testObject;
26 | private ConstraintValidatorContext context;
27 |
28 | @BeforeEach
29 | void setUp() {
30 | testObject = new OrderValidator();
31 | testObject.coffeeShop = mock(CoffeeShop.class);
32 | context = mock(ConstraintValidatorContext.class);
33 |
34 | Set coffeeTypes = EnumSet.allOf(CoffeeType.class);
35 | Origin colombia = new Origin("Colombia");
36 | colombia.getCoffeeTypes().addAll(coffeeTypes);
37 |
38 | when(testObject.coffeeShop.getCoffeeTypes()).thenReturn(coffeeTypes);
39 | when(testObject.coffeeShop.getOrigin("Colombia")).thenReturn(colombia);
40 | }
41 |
42 | @ParameterizedTest
43 | @MethodSource("validData")
44 | void testIsValid(String json) {
45 | JsonObject jsonObject = Json.createReader(new StringReader(json)).readObject();
46 |
47 | assertThat(testObject.isValid(jsonObject, context)).isTrue();
48 | }
49 |
50 | private static Collection validData() {
51 | return List.of(
52 | "{\"type\":\"ESPRESSO\",\"origin\":\"Colombia\"}",
53 | "{\"type\":\"Espresso\",\"origin\":\"Colombia\"}",
54 | "{\"type\":\"LATTE\",\"origin\":\"Colombia\"}",
55 | "{\"type\":\"Latte\",\"origin\":\"Colombia\"}",
56 | "{\"type\":\"POUR_OVER\",\"origin\":\"Colombia\"}",
57 | "{\"type\":\"Pour_over\",\"origin\":\"Colombia\"}");
58 | }
59 |
60 | @ParameterizedTest
61 | @MethodSource("invalidData")
62 | void testIsInvalid(String json) {
63 | JsonObject jsonObject = Json.createReader(new StringReader(json)).readObject();
64 |
65 | assertThat(testObject.isValid(jsonObject, context)).isFalse();
66 | }
67 |
68 | private static Collection invalidData() {
69 | return List.of(
70 | "{\"type\":\"SIPHON\",\"origin\":\"Colombia\"}",
71 | "{\"type\":null,\"origin\":\"Colombia\"}",
72 | "{\"origin\":\"Colombia\"}",
73 | "{\"type\":\"ESPRESSO\",\"origin\":\"Ethiopia\"}");
74 | }
75 |
76 | }
--------------------------------------------------------------------------------
/coffee-shop-st/src/test/java/com/sebastian_daschner/coffee_shop/backend/systems/CoffeeOrderSystem.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.backend.systems;
2 |
3 | import com.sebastian_daschner.coffee_shop.backend.entity.Order;
4 |
5 | import javax.json.JsonArray;
6 | import javax.json.JsonObject;
7 | import javax.ws.rs.client.Client;
8 | import javax.ws.rs.client.ClientBuilder;
9 | import javax.ws.rs.client.Entity;
10 | import javax.ws.rs.client.WebTarget;
11 | import javax.ws.rs.core.MediaType;
12 | import javax.ws.rs.core.Response;
13 | import javax.ws.rs.core.UriBuilder;
14 | import java.net.URI;
15 | import java.util.List;
16 | import java.util.stream.Collectors;
17 |
18 | public class CoffeeOrderSystem {
19 |
20 | private final Client client;
21 | private final WebTarget ordersTarget;
22 | private final RequestJsonBuilder jsonBuilder;
23 |
24 | public CoffeeOrderSystem() {
25 | client = ClientBuilder.newClient();
26 | ordersTarget = client.target(buildUri());
27 | jsonBuilder = new RequestJsonBuilder();
28 | }
29 |
30 | private URI buildUri() {
31 | String host = System.getProperty("coffee-shop.test.host", "localhost");
32 | String port = System.getProperty("coffee-shop.test.port", "8001");
33 | return UriBuilder.fromUri("http://{host}:{port}/orders")
34 | .build(host, port);
35 | }
36 |
37 | public List getOrders() {
38 | return ordersTarget.request(MediaType.APPLICATION_JSON_TYPE)
39 | .get(JsonArray.class).getValuesAs(JsonObject.class).stream()
40 | .map(o -> o.getString("_self"))
41 | .map(URI::create)
42 | .collect(Collectors.toList());
43 | }
44 |
45 | public URI createOrder(Order order) {
46 | Response response = sendRequest(order);
47 | verifySuccess(response);
48 | return response.getLocation();
49 | }
50 |
51 | public void createInvalidOrder(Order order) {
52 | Response response = sendRequest(order);
53 | verifyClientError(response);
54 | }
55 |
56 | private Response sendRequest(Order order) {
57 | JsonObject requestBody = jsonBuilder.forOrder(order);
58 | return ordersTarget.request().post(Entity.json(requestBody));
59 | }
60 |
61 | private void verifySuccess(Response response) {
62 | verifyStatus(response, Response.Status.Family.SUCCESSFUL);
63 | }
64 |
65 | private void verifyClientError(Response response) {
66 | verifyStatus(response, Response.Status.Family.CLIENT_ERROR);
67 | }
68 |
69 | private void verifyStatus(Response response, Response.Status.Family clientError) {
70 | if (response.getStatusInfo().getFamily() != clientError)
71 | throw new AssertionError("Status was not successful: " + response.getStatus());
72 | }
73 |
74 | public Order getOrder(URI orderUri) {
75 | return client.target(orderUri)
76 | .request(MediaType.APPLICATION_JSON_TYPE)
77 | .get(Order.class);
78 | }
79 |
80 | public void close() {
81 | client.close();
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/coffee-shop-st/src/test/java/com/sebastian_daschner/coffee_shop/backend/CreateOrderNaiveTest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.backend;
2 |
3 | import com.sebastian_daschner.coffee_shop.backend.entity.Order;
4 | import org.assertj.core.api.Assertions;
5 | import org.junit.jupiter.api.BeforeEach;
6 | import org.junit.jupiter.api.Test;
7 |
8 | import javax.json.Json;
9 | import javax.json.JsonArray;
10 | import javax.json.JsonObject;
11 | import javax.json.JsonObjectBuilder;
12 | import javax.ws.rs.client.Client;
13 | import javax.ws.rs.client.ClientBuilder;
14 | import javax.ws.rs.client.Entity;
15 | import javax.ws.rs.client.WebTarget;
16 | import javax.ws.rs.core.MediaType;
17 | import javax.ws.rs.core.Response;
18 | import javax.ws.rs.core.UriBuilder;
19 | import java.net.URI;
20 | import java.util.List;
21 | import java.util.stream.Collectors;
22 |
23 | import static org.assertj.core.api.Assertions.assertThat;
24 |
25 | class CreateOrderNaiveTest {
26 |
27 | private Client client;
28 | private WebTarget ordersTarget;
29 |
30 | @BeforeEach
31 | void setUp() {
32 | client = ClientBuilder.newClient();
33 | ordersTarget = client.target(buildUri());
34 | }
35 |
36 | private URI buildUri() {
37 | String host = System.getProperty("coffee-shop.test.host", "localhost");
38 | String port = System.getProperty("coffee-shop.test.port", "8001");
39 | return UriBuilder.fromUri("http://{host}:{port}/orders")
40 | .build(host, port);
41 | }
42 |
43 | @Test
44 | void createVerifyOrder() {
45 | Order order = new Order("Espresso", "Colombia");
46 | JsonObject requestBody = createJson(order);
47 |
48 | Response response = ordersTarget.request().post(Entity.json(requestBody));
49 |
50 | if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL)
51 | throw new AssertionError("Status was not successful: " + response.getStatus());
52 |
53 | URI orderUri = response.getLocation();
54 |
55 | Order loadedOrder = client.target(orderUri)
56 | .request(MediaType.APPLICATION_JSON_TYPE)
57 | .get(Order.class);
58 |
59 | Assertions.assertThat(loadedOrder).isEqualToComparingOnlyGivenFields(order, "type", "origin");
60 |
61 | List orders = ordersTarget.request(MediaType.APPLICATION_JSON_TYPE)
62 | .get(JsonArray.class).getValuesAs(JsonObject.class).stream()
63 | .map(o -> o.getString("_self"))
64 | .map(URI::create)
65 | .collect(Collectors.toList());
66 |
67 | assertThat(orders).contains(orderUri);
68 | }
69 |
70 | JsonObject createJson(Order order) {
71 | JsonObjectBuilder builder = Json.createObjectBuilder();
72 |
73 | if (order.getType() != null)
74 | builder.add("type", order.getType());
75 | else
76 | builder.addNull("type");
77 | if (order.getOrigin() != null)
78 | builder.add("origin", order.getOrigin());
79 | else
80 | builder.addNull("origin");
81 |
82 | return builder.build();
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/it/CoffeeOrderSystem.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.it;
2 |
3 | import javax.json.JsonArray;
4 | import javax.json.JsonObject;
5 | import javax.ws.rs.client.Client;
6 | import javax.ws.rs.client.ClientBuilder;
7 | import javax.ws.rs.client.WebTarget;
8 | import javax.ws.rs.core.MediaType;
9 | import javax.ws.rs.core.UriBuilder;
10 | import java.net.URI;
11 | import java.util.List;
12 | import java.util.stream.Collectors;
13 |
14 | class CoffeeOrderSystem {
15 |
16 | private final Client client;
17 | private final WebTarget baseTarget;
18 |
19 | CoffeeOrderSystem() {
20 | client = ClientBuilder.newClient();
21 | baseTarget = client.target(buildBaseUri());
22 | }
23 |
24 | private URI buildBaseUri() {
25 | String host = System.getProperty("coffee-shop.test.host", "localhost");
26 | String port = System.getProperty("coffee-shop.test.port", "8001");
27 | return UriBuilder.fromUri("http://{host}:{port}/").build(host, port);
28 | }
29 |
30 | boolean isSystemUp() {
31 | JsonObject healthJson = retrieveHealthStatus();
32 |
33 | String status = healthJson.getString("status");
34 | if (!"UP".equalsIgnoreCase(status))
35 | return false;
36 |
37 | return "UP".equalsIgnoreCase(extractStatus(healthJson, "coffee-shop"));
38 | }
39 |
40 | private JsonObject retrieveHealthStatus() {
41 | return baseTarget.path("health")
42 | .request(MediaType.APPLICATION_JSON_TYPE)
43 | .get(JsonObject.class);
44 | }
45 |
46 | private String extractStatus(JsonObject healthJson, String name) {
47 | return healthJson.getJsonArray("checks")
48 | .getValuesAs(JsonObject.class)
49 | .stream()
50 | .filter(o -> o.getString("name").equalsIgnoreCase(name))
51 | .map(o -> o.getString("status"))
52 | .findAny().orElse(null);
53 | }
54 |
55 | List getTypes() {
56 | return baseTarget.path("types")
57 | .request(MediaType.APPLICATION_JSON_TYPE)
58 | .get(JsonArray.class).getValuesAs(JsonObject.class).stream()
59 | .map(o -> o.getString("type"))
60 | .collect(Collectors.toList());
61 | }
62 |
63 | List getOrigins(String type) {
64 | URI typeOriginsUri = retrieveTypeOriginsLink(type);
65 |
66 | return retrieveOriginsForType(typeOriginsUri);
67 | }
68 |
69 | private URI retrieveTypeOriginsLink(String type) {
70 | return baseTarget.path("types")
71 | .request(MediaType.APPLICATION_JSON_TYPE)
72 | .get(JsonArray.class).getValuesAs(JsonObject.class).stream()
73 | .filter(o -> o.getString("type").equalsIgnoreCase(type))
74 | .map(o -> o.getJsonObject("_links").getString("origins"))
75 | .map(URI::create)
76 | .findAny().orElseThrow(() -> new IllegalStateException("Could not get link to origins for " + type));
77 | }
78 |
79 | private List retrieveOriginsForType(URI typeOriginsUri) {
80 | return client.target(typeOriginsUri)
81 | .request(MediaType.APPLICATION_JSON_TYPE)
82 | .get(JsonArray.class).getValuesAs(JsonObject.class).stream()
83 | .map(o -> o.getString("origin"))
84 | .collect(Collectors.toList());
85 | }
86 |
87 | void close() {
88 | client.close();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopNaiveUseCaseTest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.control.Barista;
4 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessor;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType;
6 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
7 | import com.sebastian_daschner.coffee_shop.orders.entity.OrderStatus;
8 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin;
9 | import org.junit.jupiter.api.BeforeEach;
10 | import org.junit.jupiter.api.Test;
11 | import org.mockito.ArgumentCaptor;
12 |
13 | import javax.persistence.EntityManager;
14 | import javax.persistence.TypedQuery;
15 | import java.lang.reflect.Field;
16 | import java.util.Arrays;
17 | import java.util.List;
18 | import java.util.UUID;
19 |
20 | import static org.assertj.core.api.Assertions.assertThat;
21 | import static org.mockito.ArgumentMatchers.anyString;
22 | import static org.mockito.ArgumentMatchers.eq;
23 | import static org.mockito.Mockito.*;
24 |
25 | class CoffeeShopNaiveUseCaseTest {
26 |
27 | private CoffeeShop coffeeShop;
28 | private EntityManager entityManager;
29 | private OrderProcessor orderProcessor;
30 | private Barista barista;
31 | private ArgumentCaptor orderCaptor;
32 |
33 | @BeforeEach
34 | void setUp() {
35 | coffeeShop = new CoffeeShop();
36 | orderProcessor = new OrderProcessor();
37 |
38 | coffeeShop.orderProcessor = orderProcessor;
39 | entityManager = mock(EntityManager.class);
40 | coffeeShop.entityManager = entityManager;
41 |
42 | barista = mock(Barista.class);
43 | setReflectiveField(orderProcessor, "entityManager", entityManager);
44 | setReflectiveField(orderProcessor, "barista", barista);
45 | orderCaptor = ArgumentCaptor.forClass(Order.class);
46 |
47 | when(barista.retrieveOrderStatus(orderCaptor.capture())).thenReturn(OrderStatus.PREPARING);
48 | }
49 |
50 | @Test
51 | void testCreateOrder() {
52 | Order order = new Order();
53 | coffeeShop.createOrder(order);
54 | verify(entityManager).merge(order);
55 | }
56 |
57 | @Test
58 | @SuppressWarnings("unchecked")
59 | void testProcessUnfinishedOrders() {
60 | List orders = Arrays.asList(new Order(UUID.randomUUID(), CoffeeType.ESPRESSO, new Origin("Colombia")),
61 | new Order(UUID.randomUUID(), CoffeeType.ESPRESSO, new Origin("Ethiopia")));
62 |
63 | // answer for unfinished orders
64 | TypedQuery queryMock = mock(TypedQuery.class);
65 | when(entityManager.createNamedQuery(anyString(), eq(Order.class))).thenReturn(queryMock);
66 | when(queryMock.getResultList()).thenReturn(orders);
67 |
68 | coffeeShop.processUnfinishedOrders();
69 |
70 | verify(entityManager).createNamedQuery(Order.FIND_UNFINISHED, Order.class);
71 |
72 | verify(barista, times(orders.size())).retrieveOrderStatus(any());
73 | assertThat(orderCaptor.getAllValues()).containsExactlyElementsOf(orders);
74 | }
75 |
76 | private static void setReflectiveField(Object object, String fieldName, Object value) {
77 | try {
78 | Field f1 = object.getClass().getDeclaredField(fieldName);
79 | f1.setAccessible(true);
80 | f1.set(object, value);
81 | } catch (ReflectiveOperationException e) {
82 | throw new RuntimeException(e);
83 | }
84 | }
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/README.adoc:
--------------------------------------------------------------------------------
1 | = Effective Testing & Test Automation
2 |
3 | Some projects for my presentations on effective enterprise testing.
4 |
5 | The example projects contain a _coffee-shop_ application, which uses the _coffee-shop-db_ database, and a _barista_ application.
6 |
7 |
8 | == Running
9 |
10 | You run the environment, containing the _coffee-shop_, _barista_ applications and the _coffee-shop-db_ using Docker containers.
11 |
12 | In order to run the Docker containers locally, you need to create a Docker network first: +
13 | `docker network create --subnet=192.168.42.0/24 dkrnet`
14 |
15 | Then you can build the _coffee-shop_ project and run the environment as follows:
16 |
17 | [source,bash]
18 | ----
19 | ./local-build.sh
20 | ./local-run-env.sh
21 | ----
22 |
23 | You can access the _coffee-shop_ application using HTTP, after the applications have been started:
24 |
25 | [source,bash]
26 | ----
27 | curl localhost:8001/
28 | curl localhost:8001/orders
29 | ----
30 |
31 | You create new coffee orders by POST-ing the JSON representation of a new order:
32 |
33 | [source,bash]
34 | ----
35 | curl localhost:8001/orders -i \
36 | -XPOST \
37 | -H 'Content-Type: application/json' \
38 | -d '{"type":"Espresso","origin":"Colombia"}'
39 | ----
40 |
41 | INFO: To stop and clean up the containers again, run: `docker stop coffee-shop barista coffee-shop-db`
42 |
43 |
44 | == Running the tests
45 |
46 | You run the non-integration tests by executing `mvn test`, or `mvn package` (any goal that executes Maven's `test` phase).
47 |
48 | You run the integration tests by executing `mvn test-compile failsafe:integration-test failsafe:verify`.
49 |
50 | You can run the systems tests either in a Kubernetes environment or using plain Docker containers.
51 |
52 |
53 | === System tests on local Docker containers
54 |
55 | You run the system test environment by executing:
56 |
57 | [source,bash]
58 | ----
59 | ./systemtest-run-env.sh
60 | ----
61 |
62 | This starts up the _coffee-shop_ application, the _coffee-shop-db_ database, and a _barista_ mock server.
63 |
64 | The system tests contained in `coffee-shop-st/` will run against that deployed environment:
65 |
66 | [source,bash]
67 | ----
68 | cd coffee-shop-st/
69 | mvn verify
70 | ----
71 |
72 | INFO: To stop and clean up the system test containers again, run: `docker stop coffee-shop barista coffee-shop-db`
73 |
74 |
75 | === System tests with Quarkus dev mode
76 |
77 | The system test environment with Docker containers also works with the Quarkus `remote-dev` mode where we can see our code changes immediately being reflected in the running `coffee-shop` application.
78 |
79 | You run the system test environment with dev mode by executing:
80 |
81 | [source,bash]
82 | ----
83 | ./systemtest-run-dev-env.sh
84 | ----
85 |
86 | You can run the system tests like in the description before, and also you can change the sources under `coffee-shop/` and see the changes being reflected in the running coffee-shop application.
87 |
88 |
89 | === System tests on Kubernetes
90 |
91 | In order to run the system tests in Kubernetes & Istio you need to apply the `systemtest` Kubernetes resources: `kubectl apply -f coffee-shop/deployment/systemtest/`.
92 | The files assume that you have Istio installed on your cluster.
93 |
94 | You can point your integration & system tests to the remote environment using the following system variables:
95 |
96 | - for the smoke integration tests:
97 |
98 | [source,bash]
99 | ----
100 | cd coffee-shop/
101 | mvn test-compile failsafe:integration-test failsafe:verify \
102 | -Dcoffee-shop.test.host=1.2.3.4 \
103 | -Dcoffee-shop.test.port=80
104 | ----
105 |
106 | - for the system tests:
107 |
108 | [source,bash]
109 | ----
110 | cd coffee-shop-st/
111 | mvn verify \
112 | -Dcoffee-shop.test.host=1.2.3.4 \
113 | -Dcoffee-shop.test.port=80 \
114 | -Dbarista.test.host=1.2.3.4 \
115 | -Dbarista.test.port=80
116 | ----
117 |
--------------------------------------------------------------------------------
/coffee-shop-st/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 | com.sebastian-daschner.coffee
5 | coffee-shop-st
6 | 1.0
7 | jar
8 |
9 | Coffee shop system tests
10 |
11 |
12 |
13 |
14 | org.junit.jupiter
15 | junit-jupiter-api
16 | 5.7.1
17 | test
18 |
19 |
20 | org.junit.jupiter
21 | junit-jupiter-engine
22 | 5.7.1
23 | test
24 |
25 |
26 | org.junit.vintage
27 | junit-vintage-engine
28 | 5.7.1
29 | test
30 |
31 |
32 | org.mockito
33 | mockito-core
34 | 3.8.0
35 | test
36 |
37 |
38 | org.assertj
39 | assertj-core
40 | 3.19.0
41 | test
42 |
43 |
44 |
45 |
46 | io.cucumber
47 | cucumber-java
48 | 4.3.0
49 | test
50 |
51 |
52 | io.cucumber
53 | cucumber-junit
54 | 4.3.0
55 | test
56 |
57 |
58 |
59 |
60 | org.glassfish.jersey.core
61 | jersey-client
62 | 2.26
63 | test
64 |
65 |
66 | org.glassfish.jersey.inject
67 | jersey-hk2
68 | 2.26
69 | test
70 |
71 |
72 | org.glassfish.jersey.media
73 | jersey-media-json-jackson
74 | 2.26
75 | test
76 |
77 |
78 | org.glassfish.jersey.media
79 | jersey-media-json-processing
80 | 2.26
81 | test
82 |
83 |
84 | javax.xml.bind
85 | jaxb-api
86 | 2.3.1
87 | test
88 |
89 |
90 | javax.activation
91 | activation
92 | 1.1
93 | test
94 |
95 |
96 |
97 |
98 | com.codeborne
99 | selenide
100 | 5.19.0
101 | test
102 |
103 |
104 |
105 |
106 | com.github.tomakehurst
107 | wiremock
108 | 2.27.2
109 | test
110 |
111 |
112 |
113 |
114 | coffee-shop-st
115 |
116 |
117 | maven-surefire-plugin
118 | 2.22.2
119 |
120 |
121 |
122 |
123 |
124 | 14
125 | 14
126 | UTF-8
127 |
128 |
129 |
--------------------------------------------------------------------------------
/barista/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 | com.sebastian-daschner.coffee
5 | barista
6 | 1.0-SNAPSHOT
7 | Barista service
8 |
9 |
10 | 3.8.1
11 | true
12 | 11
13 | 11
14 | UTF-8
15 | UTF-8
16 | 1.7.5.Final-redhat-00007
17 | quarkus-universe-bom
18 | com.redhat.quarkus
19 | 1.7.5.Final-redhat-00007
20 | 3.0.0-M5
21 | 3.2.2
22 |
23 |
24 |
25 |
26 |
27 | ${quarkus.platform.group-id}
28 | ${quarkus.platform.artifact-id}
29 | ${quarkus.platform.version}
30 | pom
31 | import
32 |
33 |
34 |
35 |
36 |
37 |
38 | io.quarkus
39 | quarkus-resteasy
40 |
41 |
42 | io.quarkus
43 | quarkus-resteasy-jackson
44 |
45 |
46 | io.quarkus
47 | quarkus-resteasy-jsonb
48 |
49 |
50 | io.quarkus
51 | quarkus-jsonp
52 |
53 |
54 |
55 |
56 |
57 |
58 | io.quarkus
59 | quarkus-maven-plugin
60 | ${quarkus-plugin.version}
61 |
62 |
63 |
64 | build
65 |
66 |
67 |
68 |
69 | true
70 |
71 |
72 |
73 | maven-compiler-plugin
74 | ${compiler-plugin.version}
75 |
76 |
77 | maven-surefire-plugin
78 | ${surefire-plugin.version}
79 |
80 |
81 | org.jboss.logmanager.LogManager
82 |
83 |
84 |
85 |
86 | maven-war-plugin
87 | ${war-plugin.version}
88 |
89 |
90 |
91 |
92 |
93 | native
94 |
95 |
96 | native
97 |
98 |
99 |
100 |
101 |
102 | io.quarkus
103 | quarkus-maven-plugin
104 | ${quarkus-plugin.version}
105 |
106 |
107 |
108 | build
109 |
110 |
111 |
112 |
113 | false
114 |
115 |
116 |
117 | maven-failsafe-plugin
118 | ${surefire-plugin.version}
119 |
120 |
121 |
122 | integration-test
123 | verify
124 |
125 |
126 |
127 | ${project.build.directory}/${project.build.finalName}-runner
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | native
137 |
138 |
139 |
140 |
141 |
--------------------------------------------------------------------------------
/coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/control/EntityBuilder.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.control;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.boundary.OrdersResource;
4 | import com.sebastian_daschner.coffee_shop.orders.boundary.TypesResource;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType;
6 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
7 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin;
8 |
9 | import javax.enterprise.context.ApplicationScoped;
10 | import javax.json.*;
11 | import javax.servlet.http.HttpServletRequest;
12 | import javax.ws.rs.core.UriInfo;
13 | import java.net.URI;
14 | import java.util.List;
15 | import java.util.UUID;
16 |
17 | import static com.sebastian_daschner.coffee_shop.orders.control.StringExtensions.capitalize;
18 |
19 | @ApplicationScoped
20 | public class EntityBuilder {
21 |
22 | public JsonArray buildOrders(List orders, UriInfo uriInfo, HttpServletRequest request) {
23 | return orders.stream()
24 | .map(o -> buildOrderTeaser(o, uriInfo, request))
25 | .collect(Json::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::add)
26 | .build();
27 | }
28 |
29 | private JsonObject buildOrderTeaser(Order order, UriInfo uriInfo, HttpServletRequest request) {
30 | return Json.createObjectBuilder()
31 | .add("_self", uriInfo.getBaseUriBuilder()
32 | .host(request.getServerName())
33 | .port(request.getServerPort())
34 | .path(OrdersResource.class)
35 | .path(OrdersResource.class, "getOrder")
36 | .build(order.getId())
37 | .toString())
38 | .add("origin", order.getOrigin().getName())
39 | .add("status", capitalize(order.getStatus().name()))
40 | .build();
41 | }
42 |
43 | public JsonObject buildOrder(Order order) {
44 | return Json.createObjectBuilder()
45 | .add("type", capitalize(order.getType().name()))
46 | .add("origin", order.getOrigin().getName())
47 | .add("status", capitalize(order.getStatus().name()))
48 | .build();
49 | }
50 |
51 | public Order buildOrder(JsonObject json) {
52 | final CoffeeType type = CoffeeType.fromString(json.getString("type"));
53 | final Origin origin = new Origin(json.getString("origin"));
54 |
55 | return new Order(UUID.randomUUID(), type, origin);
56 | }
57 |
58 | public JsonObject buildIndex(UriInfo uriInfo) {
59 | final URI typesUri = uriInfo.getBaseUriBuilder().path(TypesResource.class).build();
60 | final URI ordersUri = uriInfo.getBaseUriBuilder().path(OrdersResource.class).build();
61 | return Json.createObjectBuilder()
62 | .add("_links", Json.createObjectBuilder()
63 | .add("types", typesUri.toString()))
64 | .add("_actions", Json.createObjectBuilder()
65 | .add("order-coffee", Json.createObjectBuilder()
66 | .add("method", "POST")
67 | .add("href", ordersUri.toString())
68 | .add("fields", Json.createArrayBuilder()
69 | .add(Json.createObjectBuilder()
70 | .add("name", "type")
71 | .add("type", "text"))
72 | .add(Json.createObjectBuilder()
73 | .add("name", "origin")
74 | .add("type", "text"))
75 | )))
76 | .build();
77 | }
78 |
79 | public JsonObject buildOrigin(UriInfo uriInfo, Origin origin, CoffeeType type) {
80 | final URI ordersUri = uriInfo.getBaseUriBuilder().path(OrdersResource.class).build();
81 | return Json.createObjectBuilder()
82 | .add("origin", origin.getName())
83 | .add("_actions", Json.createObjectBuilder()
84 | .add("order-coffee", Json.createObjectBuilder()
85 | .add("method", "POST")
86 | .add("href", ordersUri.toString())
87 | .add("fields", Json.createArrayBuilder()
88 | .add(Json.createObjectBuilder()
89 | .add("name", "type")
90 | .add("value", capitalize(type.name())))
91 | .add(Json.createObjectBuilder()
92 | .add("name", "origin")
93 | .add("type", origin.getName()))
94 | )))
95 | .build();
96 | }
97 |
98 | public JsonObjectBuilder buildType(CoffeeType type, UriInfo uriInfo) {
99 | return Json.createObjectBuilder()
100 | .add("type", capitalize(type.name()))
101 | .add("_links", Json.createObjectBuilder()
102 | .add("origins", uriInfo.getBaseUriBuilder()
103 | .path(TypesResource.class)
104 | .path(TypesResource.class, "originsResource")
105 | .build(type).toString().toLowerCase()));
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/coffee-shop/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 | com.sebastian-daschner.coffee
7 | coffee-shop
8 | 1.0-SNAPSHOT
9 | Coffee shop service
10 |
11 |
12 | 3.8.1
13 | true
14 | 11
15 | 11
16 | UTF-8
17 | UTF-8
18 | 1.7.5.Final-redhat-00007
19 | quarkus-universe-bom
20 | com.redhat.quarkus
21 | 1.7.5.Final-redhat-00007
22 | 3.0.0-M5
23 |
24 |
25 |
26 |
27 |
28 | ${quarkus.platform.group-id}
29 | ${quarkus.platform.artifact-id}
30 | ${quarkus.platform.version}
31 | pom
32 | import
33 |
34 |
35 |
36 |
37 |
38 |
39 | io.quarkus
40 | quarkus-resteasy-jsonb
41 |
42 |
43 | io.quarkus
44 | quarkus-junit5
45 | test
46 |
47 |
48 | io.rest-assured
49 | rest-assured
50 | test
51 |
52 |
53 | io.quarkus
54 | quarkus-hibernate-orm-panache
55 |
56 |
57 | io.quarkus
58 | quarkus-jdbc-h2
59 |
60 |
61 | io.quarkus
62 | quarkus-scheduler
63 |
64 |
65 | io.quarkus
66 | quarkus-resteasy-qute
67 |
68 |
69 | io.quarkus
70 | quarkus-jsonp
71 |
72 |
73 | io.quarkus
74 | quarkus-hibernate-validator
75 |
76 |
77 | io.quarkus
78 | quarkus-undertow
79 |
80 |
81 | io.quarkus
82 | quarkus-rest-client
83 |
84 |
85 | io.quarkus
86 | quarkus-hibernate-orm
87 |
88 |
89 | io.quarkus
90 | quarkus-jdbc-postgresql
91 |
92 |
93 | io.quarkus
94 | quarkus-undertow-websockets
95 |
96 |
97 | io.quarkus
98 | quarkus-smallrye-health
99 |
100 |
101 | io.quarkus
102 | quarkus-smallrye-metrics
103 |
104 |
105 |
106 |
107 | org.junit.jupiter
108 | junit-jupiter-api
109 | 5.6.0
110 | test
111 |
112 |
113 | org.junit.jupiter
114 | junit-jupiter-engine
115 | 5.6.0
116 | test
117 |
118 |
119 | org.junit.jupiter
120 | junit-jupiter-params
121 | 5.6.0
122 | test
123 |
124 |
125 | org.junit.vintage
126 | junit-vintage-engine
127 | 5.6.0
128 | test
129 |
130 |
131 | org.assertj
132 | assertj-core
133 | 3.15.0
134 | test
135 |
136 |
137 | org.mockito
138 | mockito-core
139 | 3.2.4
140 | test
141 |
142 |
143 | org.mockito
144 | mockito-junit-jupiter
145 | 3.2.4
146 | test
147 |
148 |
149 |
150 |
151 | io.cucumber
152 | cucumber-java
153 | 4.3.0
154 | test
155 |
156 |
157 | io.cucumber
158 | cucumber-junit
159 | 4.3.0
160 | test
161 |
162 |
163 |
164 |
165 | javax.xml.bind
166 | jaxb-api
167 | 2.3.1
168 | test
169 |
170 |
171 | javax.activation
172 | activation
173 | 1.1
174 | test
175 |
176 |
177 |
178 |
179 |
180 |
181 | io.quarkus
182 | quarkus-maven-plugin
183 | ${quarkus-plugin.version}
184 |
185 |
186 |
187 | build
188 |
189 |
190 |
191 |
192 | true
193 |
194 |
195 |
196 | maven-compiler-plugin
197 | ${compiler-plugin.version}
198 |
199 |
200 | maven-surefire-plugin
201 | ${surefire-plugin.version}
202 |
203 |
204 | org.jboss.logmanager.LogManager
205 |
206 |
207 |
208 |
209 | maven-failsafe-plugin
210 | 3.0.0-M5
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 | native
219 |
220 |
221 | native
222 |
223 |
224 |
225 |
226 |
227 | maven-failsafe-plugin
228 | ${surefire-plugin.version}
229 |
230 |
231 |
232 | integration-test
233 | verify
234 |
235 |
236 |
237 | ${project.build.directory}/${project.build.finalName}-runner
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 | native
247 |
248 |
249 |
250 |
251 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------