├── .gitignore ├── LICENSE ├── README.adoc ├── barista ├── Dockerfile ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── sebastian_daschner │ └── barista │ ├── HealthCheckResource.java │ ├── IllegalArgumentExceptionMapper.java │ ├── OrderStatusProcessor.java │ └── ProcessesResource.java ├── coffee-shop-st ├── pom.xml └── src │ ├── main │ └── java │ │ └── Dummy.java │ └── test │ ├── java │ ├── com │ │ └── sebastian_daschner │ │ │ └── coffee_shop │ │ │ ├── backend │ │ │ ├── CreateOrderNaiveTest.java │ │ │ ├── CreateOrderTest.java │ │ │ ├── CreateOrderValidationTest.java │ │ │ ├── entity │ │ │ │ ├── Order.java │ │ │ │ └── OrderAssert.java │ │ │ └── systems │ │ │ │ ├── BaristaSystem.java │ │ │ │ ├── CoffeeOrderSystem.java │ │ │ │ └── RequestJsonBuilder.java │ │ │ └── frontend │ │ │ ├── CoffeeShopSmokeUITest.java │ │ │ ├── CoffeeShopUI.java │ │ │ ├── CreateOrderUITest.java │ │ │ ├── IndexView.java │ │ │ ├── Order.java │ │ │ └── OrderView.java │ └── cucumber │ │ ├── RunCucumberTest.java │ │ └── StepDefs.java │ └── resources │ └── cucumber │ ├── validating-coffee-orders-using-params.feature │ └── validating-coffee-orders.feature ├── coffee-shop ├── Dockerfile ├── create-order.sh ├── deployment │ ├── production │ │ ├── barista.yaml │ │ ├── coffee-shop-db.yaml │ │ ├── coffee-shop.yaml │ │ └── gateway.yaml │ └── systemtest │ │ ├── barista.yaml │ │ ├── coffee-shop-db.yaml │ │ ├── coffee-shop.yaml │ │ └── gateway.yaml ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── sebastian_daschner │ │ │ └── coffee_shop │ │ │ ├── Health.java │ │ │ ├── RootResource.java │ │ │ └── orders │ │ │ ├── boundary │ │ │ ├── CoffeeShop.java │ │ │ ├── IndexController.java │ │ │ ├── OrderCoffeeController.java │ │ │ ├── OrdersResource.java │ │ │ ├── OriginsResource.java │ │ │ └── TypesResource.java │ │ │ ├── control │ │ │ ├── Barista.java │ │ │ ├── EntityBuilder.java │ │ │ ├── OrderProcessor.java │ │ │ ├── OrderRepository.java │ │ │ ├── OrderValidator.java │ │ │ ├── OriginRepository.java │ │ │ └── StringExtensions.java │ │ │ └── entity │ │ │ ├── CoffeeType.java │ │ │ ├── Order.java │ │ │ ├── OrderStatus.java │ │ │ ├── Origin.java │ │ │ └── ValidOrder.java │ └── resources │ │ ├── META-INF │ │ └── resources │ │ │ ├── form.js │ │ │ └── style.css │ │ ├── application.properties │ │ ├── scripts │ │ ├── load-data.sql │ │ ├── schema-load-data.sql │ │ └── schema.sql │ │ └── templates │ │ ├── index.html │ │ └── order.html │ └── test │ ├── java │ └── com │ │ └── sebastian_daschner │ │ └── coffee_shop │ │ ├── it │ │ ├── CoffeeOrderSystem.java │ │ ├── CreateOrderQuarkusSmokeIT.java │ │ └── CreateOrderSmokeIT.java │ │ └── orders │ │ ├── ReflectionSupport.java │ │ ├── TestData.java │ │ ├── boundary │ │ ├── CoffeeShopMockitoTest.java │ │ ├── CoffeeShopNaiveTest.java │ │ ├── CoffeeShopQuarkusTest.java │ │ ├── CoffeeShopTestDouble.java │ │ └── CoffeeShopUseCaseTest.java │ │ ├── control │ │ ├── OrderProcessorTestDouble.java │ │ ├── OrderValidatorTest.java │ │ ├── RunCucumberTest.java │ │ └── StepDefs.java │ │ └── entity │ │ └── OrderAssert.java │ └── resources │ └── com │ └── sebastian_daschner │ └── coffee_shop │ └── orders │ └── control │ └── validating-coffee-orders.feature ├── local-build.sh ├── local-run-env.sh ├── presentation ├── slide001 ├── slide002 ├── slide003 ├── slide004 ├── slide005 ├── slide006 ├── slide007 ├── slide008 ├── slide009 ├── slide010 ├── slide011 └── slide012 ├── prod-run-env.sh ├── systemtest-run-dev-env.sh └── systemtest-run-env.sh /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | build/ 3 | pom.xml.tag 4 | pom.xml.releaseBackup 5 | pom.xml.versionsBackup 6 | pom.xml.next 7 | release.properties 8 | 9 | *.iml 10 | .idea/ 11 | 12 | *.log 13 | *.jar 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 118 | 119 | == Further resources 120 | 121 | - https://blog.sebastian-daschner.com/entries/efficient-testing-video-course[Efficient enterprise testing (video course)^] 122 | - https://blog.sebastian-daschner.com/entries/thoughts-on-efficient-testing[Efficient enterprise testing (article series)^] 123 | - https://blog.sebastian-daschner.com/entries/tests-that-spark-flow-poem[Tests that spark flow — A poem^] 124 | -------------------------------------------------------------------------------- /barista/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM adoptopenjdk/openjdk16-openj9:x86_64-alpine-jre-16.0.1_9_openj9-0.26.0 2 | RUN apk add curl 3 | 4 | COPY target/quarkus-app/lib/ /deployments/lib/ 5 | COPY target/quarkus-app/*.jar /deployments/ 6 | COPY target/quarkus-app/quarkus/ /deployments/quarkus/ 7 | COPY target/quarkus-app/app/ /deployments/app/ 8 | 9 | CMD ["java", "-jar", "-Dquarkus.http.host=0.0.0.0", "-Djava.util.logging.manager=org.jboss.logmanager.LogManager", "/deployments/quarkus-run.jar"] 10 | -------------------------------------------------------------------------------- /barista/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.sebastian-daschner.coffee 5 | barista 6 | 1.0 7 | 8 | 9 | 10 | 11 | io.quarkus 12 | quarkus-bom 13 | ${quarkus.version} 14 | pom 15 | import 16 | 17 | 18 | 19 | 20 | 21 | 22 | io.quarkus 23 | quarkus-resteasy 24 | 25 | 26 | io.quarkus 27 | quarkus-resteasy-jackson 28 | 29 | 30 | io.quarkus 31 | quarkus-resteasy-jsonb 32 | 33 | 34 | io.quarkus 35 | quarkus-jsonp 36 | 37 | 38 | 39 | 40 | barista 41 | 42 | 43 | io.quarkus 44 | quarkus-maven-plugin 45 | ${quarkus.version} 46 | 47 | 48 | 49 | build 50 | 51 | 52 | 53 | 54 | 55 | maven-surefire-plugin 56 | 3.0.0 57 | 58 | 59 | maven-failsafe-plugin 60 | 3.0.0 61 | 62 | 63 | 64 | 65 | 66 | 3.0.1.Final 67 | 16 68 | 16 69 | UTF-8 70 | 71 | 72 | -------------------------------------------------------------------------------- /barista/src/main/java/com/sebastian_daschner/barista/HealthCheckResource.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.barista; 2 | 3 | import jakarta.ws.rs.GET; 4 | import jakarta.ws.rs.Path; 5 | 6 | @Path("health") 7 | public class HealthCheckResource { 8 | 9 | @GET 10 | public String healthCheck() { 11 | return "OK"; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /barista/src/main/java/com/sebastian_daschner/barista/IllegalArgumentExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.barista; 2 | 3 | import jakarta.ws.rs.core.Response; 4 | import jakarta.ws.rs.ext.ExceptionMapper; 5 | import jakarta.ws.rs.ext.Provider; 6 | 7 | @Provider 8 | public class IllegalArgumentExceptionMapper implements ExceptionMapper { 9 | 10 | @Override 11 | public Response toResponse(final IllegalArgumentException exception) { 12 | return Response 13 | .status(Response.Status.BAD_REQUEST) 14 | .entity(exception.getMessage()) 15 | .build(); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /barista/src/main/java/com/sebastian_daschner/barista/OrderStatusProcessor.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.barista; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | 5 | @ApplicationScoped 6 | public class OrderStatusProcessor { 7 | 8 | public String process(final String status) { 9 | switch (status) { 10 | case "PREPARING": 11 | return "FINISHED"; 12 | case "FINISHED": 13 | return "COLLECTED"; 14 | case "COLLECTED": 15 | return "COLLECTED"; 16 | default: 17 | throw new IllegalArgumentException("Unknown status " + status); 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /barista/src/main/java/com/sebastian_daschner/barista/ProcessesResource.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.barista; 2 | 3 | import jakarta.inject.Inject; 4 | import jakarta.json.Json; 5 | import jakarta.json.JsonObject; 6 | import jakarta.ws.rs.POST; 7 | import jakarta.ws.rs.Path; 8 | 9 | @Path("processes") 10 | public class ProcessesResource { 11 | 12 | @Inject 13 | OrderStatusProcessor processor; 14 | 15 | @POST 16 | public JsonObject process(JsonObject order) { 17 | final String status = order.getString("status", null); 18 | String newStatus = processor.process(status); 19 | return Json.createObjectBuilder().add("status", newStatus).build(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /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 | 15 | org.junit.jupiter 16 | junit-jupiter-api 17 | 5.7.2 18 | test 19 | 20 | 21 | org.junit.jupiter 22 | junit-jupiter-engine 23 | 5.7.2 24 | test 25 | 26 | 27 | org.junit.vintage 28 | junit-vintage-engine 29 | 5.7.2 30 | test 31 | 32 | 33 | org.mockito 34 | mockito-core 35 | 3.8.0 36 | test 37 | 38 | 39 | org.assertj 40 | assertj-core 41 | 3.19.0 42 | test 43 | 44 | 45 | 46 | 47 | io.cucumber 48 | cucumber-java 49 | 4.3.0 50 | test 51 | 52 | 53 | io.cucumber 54 | cucumber-junit 55 | 4.3.0 56 | test 57 | 58 | 59 | 60 | 61 | org.glassfish.jersey.core 62 | jersey-client 63 | 2.26 64 | test 65 | 66 | 67 | org.glassfish.jersey.inject 68 | jersey-hk2 69 | 2.26 70 | test 71 | 72 | 73 | org.glassfish.jersey.media 74 | jersey-media-json-jackson 75 | 2.26 76 | test 77 | 78 | 79 | org.glassfish.jersey.media 80 | jersey-media-json-processing 81 | 2.26 82 | test 83 | 84 | 85 | javax.xml.bind 86 | jaxb-api 87 | 2.3.1 88 | test 89 | 90 | 91 | javax.activation 92 | activation 93 | 1.1 94 | test 95 | 96 | 97 | 98 | 99 | com.codeborne 100 | selenide 101 | 5.19.0 102 | test 103 | 104 | 105 | 106 | 107 | com.github.tomakehurst 108 | wiremock 109 | 2.27.2 110 | test 111 | 112 | 113 | 114 | 115 | coffee-shop-st 116 | 117 | 118 | maven-surefire-plugin 119 | 3.0.0-M5 120 | 121 | 122 | 123 | 124 | 125 | 126 | test 127 | 128 | 2.15.2.Final 129 | false 130 | ignored 131 | 132 | 133 | 134 | 135 | io.quarkus 136 | quarkus-bom 137 | ${quarkus.version} 138 | pom 139 | import 140 | 141 | 142 | 143 | 144 | 145 | io.quarkus 146 | quarkus-core 147 | 148 | 149 | 150 | 151 | 152 | io.quarkus 153 | quarkus-maven-plugin 154 | true 155 | ${quarkus.version} 156 | 157 | 158 | 159 | build 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 16 171 | 16 172 | UTF-8 173 | 174 | 175 | -------------------------------------------------------------------------------- /coffee-shop-st/src/main/java/Dummy.java: -------------------------------------------------------------------------------- 1 | // required so that the quarkus:dev process wouldn't fail 2 | public class Dummy { 3 | 4 | } -------------------------------------------------------------------------------- /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.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import javax.json.Json; 8 | import javax.json.JsonArray; 9 | import javax.json.JsonObject; 10 | import javax.json.JsonObjectBuilder; 11 | import javax.ws.rs.client.Client; 12 | import javax.ws.rs.client.ClientBuilder; 13 | import javax.ws.rs.client.Entity; 14 | import javax.ws.rs.client.WebTarget; 15 | import javax.ws.rs.core.MediaType; 16 | import javax.ws.rs.core.Response; 17 | import javax.ws.rs.core.UriBuilder; 18 | import java.net.URI; 19 | import java.util.List; 20 | import java.util.stream.Collectors; 21 | 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | 24 | class CreateOrderNaiveTest { 25 | 26 | private Client client; 27 | private WebTarget ordersTarget; 28 | 29 | @BeforeEach 30 | void setUp() { 31 | client = ClientBuilder.newClient(); 32 | ordersTarget = client.target(buildUri()); 33 | } 34 | 35 | private URI buildUri() { 36 | String host = System.getProperty("coffee-shop.test.host", "localhost"); 37 | String port = System.getProperty("coffee-shop.test.port", "8001"); 38 | return UriBuilder.fromUri("http://{host}:{port}/orders") 39 | .build(host, port); 40 | } 41 | 42 | @Test 43 | void createVerifyOrder() { 44 | Order order = new Order("Espresso", "Colombia"); 45 | JsonObject requestBody = createJson(order); 46 | 47 | Response response = ordersTarget.request().post(Entity.json(requestBody)); 48 | 49 | if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) 50 | throw new AssertionError("Status was not successful: " + response.getStatus()); 51 | 52 | URI orderUri = response.getLocation(); 53 | 54 | Order loadedOrder = client.target(orderUri) 55 | .request(MediaType.APPLICATION_JSON_TYPE) 56 | .get(Order.class); 57 | 58 | assertThat(loadedOrder.getType()).isEqualTo(order.getType()); 59 | assertThat(loadedOrder.getOrigin()).isEqualTo(order.getOrigin()); 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-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.entity.OrderAssert; 5 | import com.sebastian_daschner.coffee_shop.backend.systems.BaristaSystem; 6 | import com.sebastian_daschner.coffee_shop.backend.systems.CoffeeOrderSystem; 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 | OrderAssert.assertThat(loadedOrder).matchesOrderedData(order); 33 | 34 | 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 | OrderAssert.assertThat(loadedOrder).matchesOrderedData(order); 46 | 47 | loadedOrder = waitForProcessAndGet(orderUri, "PREPARING"); 48 | assertThat(loadedOrder.getStatus()).isEqualTo("Preparing"); 49 | 50 | baristaSystem.answerForOrder(orderUri, "FINISHED"); 51 | 52 | loadedOrder = waitForProcessAndGet(orderUri, "FINISHED"); 53 | 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-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/backend/entity/Order.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.backend.entity; 2 | 3 | public class Order { 4 | 5 | private String id; 6 | private String type; 7 | private String origin; 8 | private String status; 9 | 10 | public Order() { 11 | } 12 | 13 | public Order(String type, String origin) { 14 | this.type = type; 15 | this.origin = origin; 16 | } 17 | 18 | public String getId() { 19 | return id; 20 | } 21 | 22 | public void setId(String id) { 23 | this.id = id; 24 | } 25 | 26 | public String getType() { 27 | return type; 28 | } 29 | 30 | public void setType(String type) { 31 | this.type = type; 32 | } 33 | 34 | public String getOrigin() { 35 | return origin; 36 | } 37 | 38 | public void setOrigin(String origin) { 39 | this.origin = origin; 40 | } 41 | 42 | public String getStatus() { 43 | return status; 44 | } 45 | 46 | public void setStatus(String status) { 47 | this.status = status; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /coffee-shop-st/src/test/java/com/sebastian_daschner/coffee_shop/backend/entity/OrderAssert.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.backend.entity; 2 | 3 | import org.assertj.core.api.AbstractAssert; 4 | import org.assertj.core.api.Assertions; 5 | 6 | public class OrderAssert extends AbstractAssert { 7 | 8 | public OrderAssert(Order order) { 9 | super(order, OrderAssert.class); 10 | } 11 | 12 | public static OrderAssert assertThat(Order actual) { 13 | return new OrderAssert(actual); 14 | } 15 | 16 | public OrderAssert matchesOrderedData(Order other) { 17 | isNotNull(); 18 | Assertions.assertThat(actual.getType()).isEqualTo(other.getType()); 19 | Assertions.assertThat(actual.getOrigin()).isEqualTo(other.getOrigin()); 20 | return this; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /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-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/systems/RequestJsonBuilder.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.Json; 6 | import javax.json.JsonObject; 7 | import javax.json.JsonObjectBuilder; 8 | 9 | class RequestJsonBuilder { 10 | 11 | JsonObject forOrder(Order order) { 12 | JsonObjectBuilder builder = Json.createObjectBuilder(); 13 | 14 | if (order.getType() != null) 15 | builder.add("type", order.getType()); 16 | else 17 | builder.addNull("type"); 18 | if (order.getOrigin() != null) 19 | builder.add("origin", order.getOrigin()); 20 | else 21 | builder.addNull("origin"); 22 | 23 | return builder.build(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /coffee-shop-st/src/test/java/com/sebastian_daschner/coffee_shop/frontend/CoffeeShopSmokeUITest.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 static org.assertj.core.api.Assertions.assertThat; 7 | 8 | public class CoffeeShopSmokeUITest { 9 | 10 | private final CoffeeShopUI coffeeShop = new CoffeeShopUI(); 11 | 12 | @BeforeEach 13 | void setUp() { 14 | coffeeShop.init(); 15 | } 16 | 17 | @Test 18 | void index_view_page_header_empty_table() { 19 | IndexView index = coffeeShop.index(); 20 | index.assertPageHeader("All coffee orders"); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /coffee-shop-st/src/test/java/com/sebastian_daschner/coffee_shop/frontend/CoffeeShopUI.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.frontend; 2 | 3 | import org.openqa.selenium.Cookie; 4 | 5 | import javax.ws.rs.core.UriBuilder; 6 | 7 | import static com.codeborne.selenide.Selenide.open; 8 | import static com.codeborne.selenide.WebDriverRunner.getWebDriver; 9 | 10 | public class CoffeeShopUI { 11 | 12 | public void init() { 13 | open(uriBuilder().path("index.html").toString()); 14 | getWebDriver().manage().addCookie(new Cookie("session", "123")); 15 | } 16 | 17 | private UriBuilder uriBuilder() { 18 | String host = System.getProperty("coffee-shop.test.host", "localhost"); 19 | String port = System.getProperty("coffee-shop.test.port", "8001"); 20 | return UriBuilder.fromUri("http://{host}:{port}/") 21 | .resolveTemplate("host", host) 22 | .resolveTemplate("port", port); 23 | } 24 | 25 | public IndexView index() { 26 | open(uriBuilder().path("index.html").toString()); 27 | return new IndexView(); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /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-st/src/test/java/com/sebastian_daschner/coffee_shop/frontend/IndexView.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.frontend; 2 | 3 | import java.util.List; 4 | 5 | import static com.codeborne.selenide.Condition.text; 6 | import static com.codeborne.selenide.Selenide.$; 7 | import static com.codeborne.selenide.Selenide.$$; 8 | import static java.util.stream.Collectors.toList; 9 | 10 | public class IndexView { 11 | 12 | public String getPageHeader() { 13 | return $("body > h1").text(); 14 | } 15 | 16 | public void assertPageHeader(String header) { 17 | $("body > h1").shouldHave(text(header)); 18 | } 19 | 20 | public List getListedOrders() { 21 | return $$("body > table tr").stream() 22 | .map(el -> el.findAll("td")) 23 | .filter(list -> !list.isEmpty()) 24 | .map(list -> new Order(list.get(0).getText(), list.get(1).getText(), list.get(2).getText())) 25 | .collect(toList()); 26 | } 27 | 28 | public OrderView followCreateOrderLink() { 29 | $$("a").findBy(text("Create")).click(); 30 | return new OrderView(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /coffee-shop-st/src/test/java/com/sebastian_daschner/coffee_shop/frontend/Order.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.frontend; 2 | 3 | public class Order { 4 | 5 | public final String status; 6 | public final String type; 7 | public final String origin; 8 | 9 | public Order(String status, String type, String origin) { 10 | this.status = status; 11 | this.type = type; 12 | this.origin = origin; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /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/cucumber/RunCucumberTest.java: -------------------------------------------------------------------------------- 1 | package cucumber; 2 | 3 | import cucumber.api.CucumberOptions; 4 | import cucumber.api.junit.Cucumber; 5 | import org.junit.runner.RunWith; 6 | 7 | @RunWith(Cucumber.class) 8 | @CucumberOptions 9 | public class RunCucumberTest { 10 | 11 | 12 | } 13 | -------------------------------------------------------------------------------- /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.entity.OrderAssert; 5 | import com.sebastian_daschner.coffee_shop.backend.systems.CoffeeOrderSystem; 6 | import cucumber.api.java.After; 7 | import cucumber.api.java.Before; 8 | import cucumber.api.java.en.Then; 9 | import cucumber.api.java.en.When; 10 | 11 | import java.net.URI; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | public class StepDefs { 16 | 17 | private CoffeeOrderSystem coffeeOrderSystem; 18 | private Order order; 19 | 20 | @Before 21 | public void setUp() { 22 | coffeeOrderSystem = new CoffeeOrderSystem(); 23 | } 24 | 25 | @When("^I create an order with ([^ ]*) from ([^ ]*)$") 26 | public void i_create_an_order(String type, String origin) { 27 | order = new Order(type, origin); 28 | } 29 | 30 | @Then("^The order should be accepted$") 31 | public void accepted_order() { 32 | URI id = coffeeOrderSystem.createOrder(order); 33 | Order order = coffeeOrderSystem.getOrder(id); 34 | assertOrderMatches(this.order, order); 35 | } 36 | 37 | @Then("^The order should be rejected$") 38 | public void rejected_order() { 39 | coffeeOrderSystem.createInvalidOrder(order); 40 | } 41 | 42 | private void assertOrderMatches(Order actual, Order expected) { 43 | OrderAssert.assertThat(actual).matchesOrderedData(expected); 44 | } 45 | 46 | @After 47 | public void close() { 48 | coffeeOrderSystem.close(); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /coffee-shop-st/src/test/resources/cucumber/validating-coffee-orders-using-params.feature: -------------------------------------------------------------------------------- 1 | Feature: Validating coffee order 2 | 3 | Scenario Outline: Creating from , will be 4 | When I create an order with from 5 | Then The order should be 6 | 7 | Examples: 8 | | type | origin | result | 9 | | Espresso | Colombia | accepted | 10 | | Pour_over | Colombia | accepted | 11 | | Espresso | Ethiopia | accepted | 12 | | Latte | Ethiopia | accepted | 13 | | Pour_over | Ethiopia | accepted | 14 | | Espresso | Germany | rejected | 15 | | Siphon | Colombia | rejected | 16 | | Siphon | Germany | rejected | 17 | -------------------------------------------------------------------------------- /coffee-shop-st/src/test/resources/cucumber/validating-coffee-orders.feature: -------------------------------------------------------------------------------- 1 | Feature: Validating coffee order 2 | 3 | Scenario: Creating valid order 4 | When I create an order with Espresso from Colombia 5 | Then The order should be accepted 6 | 7 | Scenario: Creating invalid order, wrong origin 8 | When I create an order with Espresso from Germany 9 | Then The order should be rejected 10 | 11 | Scenario: Creating invalid order, wrong type 12 | When I create an order with Siphon from Colombia 13 | Then The order should be rejected 14 | 15 | Scenario: Creating invalid order, wrong type and origin 16 | When I create an order with Siphon from Germany 17 | Then The order should be rejected 18 | -------------------------------------------------------------------------------- /coffee-shop/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM adoptopenjdk/openjdk16-openj9:x86_64-alpine-jre-16.0.1_9_openj9-0.26.0 2 | RUN apk add curl 3 | 4 | COPY target/quarkus-app/lib/ /deployments/lib/ 5 | COPY target/quarkus-app/*.jar /deployments/ 6 | COPY target/quarkus-app/quarkus/ /deployments/quarkus/ 7 | COPY target/quarkus-app/app/ /deployments/app/ 8 | 9 | CMD ["java", "-jar", "-Dquarkus.http.host=0.0.0.0", "-Djava.util.logging.manager=org.jboss.logmanager.LogManager", "/deployments/quarkus-run.jar"] 10 | -------------------------------------------------------------------------------- /coffee-shop/create-order.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | curl http://localhost:8080/orders -XPOST -i -H 'Content-Type: application/json' -d '{"origin":"Colombia","type":"Espresso"}' 5 | -------------------------------------------------------------------------------- /coffee-shop/deployment/production/barista.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: barista 5 | spec: 6 | selector: 7 | app: barista 8 | ports: 9 | - port: 8080 10 | --- 11 | 12 | kind: Deployment 13 | apiVersion: apps/v1 14 | metadata: 15 | name: barista 16 | spec: 17 | replicas: 1 18 | selector: 19 | matchLabels: 20 | app: barista 21 | version: v1 22 | template: 23 | metadata: 24 | labels: 25 | app: barista 26 | version: v1 27 | spec: 28 | containers: 29 | - name: barista 30 | image: sdaschner/barista:quarkus-testing-1 31 | imagePullPolicy: Always 32 | readinessProbe: 33 | httpGet: 34 | path: /health 35 | port: 8080 36 | restartPolicy: Always 37 | --- 38 | 39 | apiVersion: networking.istio.io/v1alpha3 40 | kind: VirtualService 41 | metadata: 42 | name: barista 43 | spec: 44 | hosts: 45 | - barista 46 | http: 47 | - route: 48 | - destination: 49 | host: barista 50 | subset: v1 51 | --- 52 | 53 | apiVersion: networking.istio.io/v1alpha3 54 | kind: DestinationRule 55 | metadata: 56 | name: barista 57 | spec: 58 | host: barista 59 | subsets: 60 | - name: v1 61 | labels: 62 | version: v1 63 | --- 64 | -------------------------------------------------------------------------------- /coffee-shop/deployment/production/coffee-shop-db.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: coffee-shop-db 5 | spec: 6 | selector: 7 | app: coffee-shop-db 8 | ports: 9 | - port: 5432 10 | --- 11 | 12 | kind: Deployment 13 | apiVersion: apps/v1 14 | metadata: 15 | name: coffee-shop-db 16 | spec: 17 | replicas: 1 18 | selector: 19 | matchLabels: 20 | app: coffee-shop-db 21 | template: 22 | metadata: 23 | labels: 24 | app: coffee-shop-db 25 | spec: 26 | containers: 27 | - name: coffee-shop-db 28 | image: postgres:9.5 29 | imagePullPolicy: Always 30 | env: 31 | - name: POSTGRES_USER 32 | value: postgres 33 | - name: POSTGRES_PASSWORD 34 | value: postgres 35 | restartPolicy: Always 36 | --- -------------------------------------------------------------------------------- /coffee-shop/deployment/production/coffee-shop.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: coffee-shop 5 | spec: 6 | selector: 7 | app: coffee-shop 8 | ports: 9 | - port: 8080 10 | --- 11 | 12 | kind: Deployment 13 | apiVersion: apps/v1 14 | metadata: 15 | name: coffee-shop 16 | spec: 17 | replicas: 1 18 | selector: 19 | matchLabels: 20 | app: coffee-shop 21 | version: v1 22 | template: 23 | metadata: 24 | labels: 25 | app: coffee-shop 26 | version: v1 27 | spec: 28 | containers: 29 | - name: coffee-shop 30 | image: sdaschner/coffee-shop:quarkus-testing-1 31 | imagePullPolicy: Always 32 | readinessProbe: 33 | httpGet: 34 | path: / 35 | port: 8080 36 | restartPolicy: Always 37 | --- 38 | 39 | apiVersion: networking.istio.io/v1alpha3 40 | kind: VirtualService 41 | metadata: 42 | name: coffee-shop 43 | spec: 44 | hosts: 45 | - coffee-shop 46 | http: 47 | - route: 48 | - destination: 49 | host: coffee-shop 50 | subset: v1 51 | --- 52 | 53 | apiVersion: networking.istio.io/v1alpha3 54 | kind: DestinationRule 55 | metadata: 56 | name: coffee-shop 57 | spec: 58 | host: coffee-shop 59 | subsets: 60 | - name: v1 61 | labels: 62 | version: v1 63 | --- 64 | -------------------------------------------------------------------------------- /coffee-shop/deployment/production/gateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: Gateway 3 | metadata: 4 | name: coffee-gateway 5 | spec: 6 | selector: 7 | istio: ingressgateway 8 | servers: 9 | - port: 10 | number: 80 11 | name: http 12 | protocol: HTTP 13 | hosts: 14 | - "*" 15 | --- 16 | 17 | apiVersion: networking.istio.io/v1alpha3 18 | kind: VirtualService 19 | metadata: 20 | name: coffee-gateway 21 | spec: 22 | hosts: 23 | - "*" 24 | gateways: 25 | - coffee-gateway 26 | http: 27 | - match: 28 | - uri: 29 | prefix: "/processes" 30 | route: 31 | - destination: 32 | host: barista 33 | subset: v1 34 | port: 35 | number: 8080 36 | - match: 37 | - uri: 38 | prefix: "/orders" 39 | - uri: 40 | prefix: "/types" 41 | - uri: 42 | prefix: "/health" 43 | route: 44 | - destination: 45 | host: coffee-shop 46 | subset: v1 47 | port: 48 | number: 8080 49 | --- 50 | 51 | -------------------------------------------------------------------------------- /coffee-shop/deployment/systemtest/barista.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: barista 5 | spec: 6 | selector: 7 | app: barista 8 | ports: 9 | - port: 8080 10 | --- 11 | 12 | kind: Deployment 13 | apiVersion: apps/v1 14 | metadata: 15 | name: barista 16 | spec: 17 | replicas: 1 18 | selector: 19 | matchLabels: 20 | app: barista 21 | version: v1 22 | template: 23 | metadata: 24 | labels: 25 | app: barista 26 | version: v1 27 | spec: 28 | containers: 29 | - name: barista 30 | image: rodolpheche/wiremock:2.6.0 31 | imagePullPolicy: IfNotPresent 32 | restartPolicy: Always 33 | --- 34 | 35 | apiVersion: networking.istio.io/v1alpha3 36 | kind: VirtualService 37 | metadata: 38 | name: barista 39 | spec: 40 | hosts: 41 | - barista 42 | http: 43 | - route: 44 | - destination: 45 | host: barista 46 | subset: v1 47 | --- 48 | 49 | apiVersion: networking.istio.io/v1alpha3 50 | kind: DestinationRule 51 | metadata: 52 | name: barista 53 | spec: 54 | host: barista 55 | subsets: 56 | - name: v1 57 | labels: 58 | version: v1 59 | --- 60 | -------------------------------------------------------------------------------- /coffee-shop/deployment/systemtest/coffee-shop-db.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: coffee-shop-db 5 | spec: 6 | selector: 7 | app: coffee-shop-db 8 | ports: 9 | - port: 5432 10 | --- 11 | 12 | kind: Deployment 13 | apiVersion: apps/v1 14 | metadata: 15 | name: coffee-shop-db 16 | spec: 17 | replicas: 1 18 | selector: 19 | matchLabels: 20 | app: coffee-shop-db 21 | template: 22 | metadata: 23 | labels: 24 | app: coffee-shop-db 25 | spec: 26 | containers: 27 | - name: coffee-shop-db 28 | image: postgres:9.5 29 | imagePullPolicy: Always 30 | env: 31 | - name: POSTGRES_USER 32 | value: postgres 33 | - name: POSTGRES_PASSWORD 34 | value: postgres 35 | restartPolicy: Always 36 | --- -------------------------------------------------------------------------------- /coffee-shop/deployment/systemtest/coffee-shop.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: coffee-shop 5 | spec: 6 | selector: 7 | app: coffee-shop 8 | ports: 9 | - port: 8080 10 | --- 11 | 12 | kind: Deployment 13 | apiVersion: apps/v1 14 | metadata: 15 | name: coffee-shop 16 | spec: 17 | replicas: 1 18 | selector: 19 | matchLabels: 20 | app: coffee-shop 21 | version: v1 22 | template: 23 | metadata: 24 | labels: 25 | app: coffee-shop 26 | version: v1 27 | spec: 28 | containers: 29 | - name: coffee-shop 30 | image: sdaschner/coffee-shop:quarkus-testing-1 31 | imagePullPolicy: Always 32 | readinessProbe: 33 | httpGet: 34 | path: / 35 | port: 8080 36 | restartPolicy: Always 37 | --- 38 | 39 | apiVersion: networking.istio.io/v1alpha3 40 | kind: VirtualService 41 | metadata: 42 | name: coffee-shop 43 | spec: 44 | hosts: 45 | - coffee-shop 46 | http: 47 | - route: 48 | - destination: 49 | host: coffee-shop 50 | subset: v1 51 | --- 52 | 53 | apiVersion: networking.istio.io/v1alpha3 54 | kind: DestinationRule 55 | metadata: 56 | name: coffee-shop 57 | spec: 58 | host: coffee-shop 59 | subsets: 60 | - name: v1 61 | labels: 62 | version: v1 63 | --- 64 | -------------------------------------------------------------------------------- /coffee-shop/deployment/systemtest/gateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: Gateway 3 | metadata: 4 | name: coffee-gateway 5 | spec: 6 | selector: 7 | istio: ingressgateway 8 | servers: 9 | - port: 10 | number: 80 11 | name: http 12 | protocol: HTTP 13 | hosts: 14 | - "*" 15 | --- 16 | 17 | apiVersion: networking.istio.io/v1alpha3 18 | kind: VirtualService 19 | metadata: 20 | name: coffee-gateway 21 | spec: 22 | hosts: 23 | - "*" 24 | gateways: 25 | - coffee-gateway 26 | http: 27 | - match: 28 | - uri: 29 | prefix: "/processes" 30 | - uri: 31 | prefix: "/__admin" 32 | route: 33 | - destination: 34 | host: barista 35 | subset: v1 36 | port: 37 | number: 8080 38 | - match: 39 | - uri: 40 | prefix: "/orders" 41 | - uri: 42 | prefix: "/types" 43 | - uri: 44 | prefix: "/health" 45 | route: 46 | - destination: 47 | host: coffee-shop 48 | subset: v1 49 | port: 50 | number: 8080 51 | --- 52 | 53 | -------------------------------------------------------------------------------- /coffee-shop/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.sebastian-daschner.coffee 5 | coffee-shop 6 | 1.0 7 | 8 | 9 | 10 | 11 | io.quarkus 12 | quarkus-bom 13 | ${quarkus.version} 14 | pom 15 | import 16 | 17 | 18 | 19 | 20 | Coffee shop application 21 | 22 | 23 | 24 | io.quarkus 25 | quarkus-resteasy 26 | 27 | 28 | io.quarkus 29 | quarkus-scheduler 30 | 31 | 32 | io.quarkus 33 | quarkus-resteasy-jackson 34 | 35 | 36 | io.quarkus 37 | quarkus-resteasy-jsonb 38 | 39 | 40 | io.quarkus 41 | quarkus-resteasy-qute 42 | 43 | 44 | io.quarkus 45 | quarkus-jsonp 46 | 47 | 48 | io.quarkus 49 | quarkus-hibernate-validator 50 | 51 | 52 | io.quarkus 53 | quarkus-undertow 54 | 55 | 56 | io.quarkus 57 | quarkus-rest-client 58 | 59 | 60 | io.quarkus 61 | quarkus-hibernate-orm-panache 62 | 63 | 64 | io.quarkus 65 | quarkus-jdbc-postgresql 66 | 67 | 68 | io.quarkus 69 | quarkus-smallrye-health 70 | 71 | 72 | io.quarkus 73 | quarkus-smallrye-metrics 74 | 75 | 76 | 77 | 78 | org.junit.jupiter 79 | junit-jupiter-api 80 | 5.9.3 81 | test 82 | 83 | 84 | org.junit.jupiter 85 | junit-jupiter-engine 86 | 5.9.3 87 | test 88 | 89 | 90 | org.junit.jupiter 91 | junit-jupiter-params 92 | 5.9.3 93 | test 94 | 95 | 96 | org.junit.vintage 97 | junit-vintage-engine 98 | 5.9.3 99 | test 100 | 101 | 102 | org.assertj 103 | assertj-core 104 | 3.24.2 105 | test 106 | 107 | 108 | org.mockito 109 | mockito-core 110 | 5.3.1 111 | test 112 | 113 | 114 | org.mockito 115 | mockito-junit-jupiter 116 | 5.3.1 117 | test 118 | 119 | 120 | 121 | io.quarkus 122 | quarkus-junit5-mockito 123 | test 124 | 125 | 126 | 127 | 128 | io.cucumber 129 | cucumber-java 130 | 4.3.0 131 | test 132 | 133 | 134 | io.cucumber 135 | cucumber-junit 136 | 4.3.0 137 | test 138 | 139 | 140 | 141 | 142 | jakarta.xml.bind 143 | jakarta.xml.bind-api 144 | 4.0.0 145 | test 146 | 147 | 148 | javax.activation 149 | activation 150 | 1.1 151 | test 152 | 153 | 154 | 155 | 156 | 157 | coffee-shop 158 | 159 | 160 | io.quarkus 161 | quarkus-maven-plugin 162 | ${quarkus.version} 163 | 164 | 165 | 166 | build 167 | 168 | 169 | 170 | 171 | 172 | maven-surefire-plugin 173 | 3.0.0 174 | 175 | 176 | maven-failsafe-plugin 177 | 3.0.0 178 | 179 | 180 | 181 | 182 | 183 | 3.0.1.Final 184 | 16 185 | 16 186 | UTF-8 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/Health.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop; 2 | 3 | import org.eclipse.microprofile.health.HealthCheck; 4 | import org.eclipse.microprofile.health.HealthCheckResponse; 5 | import org.eclipse.microprofile.health.Readiness; 6 | 7 | import jakarta.enterprise.context.ApplicationScoped; 8 | 9 | @Readiness 10 | @ApplicationScoped 11 | public class Health implements HealthCheck { 12 | 13 | @Override 14 | public HealthCheckResponse call() { 15 | return HealthCheckResponse.up("coffee-shop"); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/RootResource.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.control.EntityBuilder; 4 | 5 | import jakarta.inject.Inject; 6 | import jakarta.json.JsonObject; 7 | import jakarta.ws.rs.GET; 8 | import jakarta.ws.rs.Path; 9 | import jakarta.ws.rs.core.Context; 10 | import jakarta.ws.rs.core.UriInfo; 11 | 12 | @Path("/") 13 | public class RootResource { 14 | 15 | @Context 16 | UriInfo uriInfo; 17 | 18 | @Inject 19 | EntityBuilder entityBuilder; 20 | 21 | @GET 22 | public JsonObject getIndex() { 23 | return entityBuilder.buildIndex(this.uriInfo); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /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.control.OrderRepository; 5 | import com.sebastian_daschner.coffee_shop.orders.control.OriginRepository; 6 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType; 7 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 8 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin; 9 | import io.quarkus.scheduler.Scheduled; 10 | 11 | import jakarta.enterprise.context.ApplicationScoped; 12 | import jakarta.inject.Inject; 13 | import jakarta.transaction.Transactional; 14 | import java.util.EnumSet; 15 | import java.util.List; 16 | import java.util.Set; 17 | import java.util.UUID; 18 | 19 | @ApplicationScoped 20 | @Transactional 21 | public class CoffeeShop { 22 | 23 | @Inject 24 | OrderRepository orderRepository; 25 | 26 | @Inject 27 | OriginRepository originRepository; 28 | 29 | @Inject 30 | OrderProcessor orderProcessor; 31 | 32 | public Set getCoffeeTypes() { 33 | return EnumSet.of(CoffeeType.ESPRESSO, CoffeeType.LATTE, CoffeeType.POUR_OVER); 34 | } 35 | 36 | public Set getOrigins(CoffeeType type) { 37 | return originRepository.listForCoffeeType(type); 38 | } 39 | 40 | public Origin getOrigin(String name) { 41 | return originRepository.findById(name); 42 | } 43 | 44 | public void createOrder(Order order) { 45 | orderRepository.persist(order); 46 | } 47 | 48 | public Order getOrder(UUID id) { 49 | return orderRepository.findById(id); 50 | } 51 | 52 | public List getOrders() { 53 | return orderRepository.listAll(); 54 | } 55 | 56 | @Scheduled(every = "2s") 57 | public void processUnfinishedOrders() { 58 | orderRepository.listUnfinishedOrders().forEach(orderProcessor::processOrder); 59 | } 60 | 61 | public void updateOrder(UUID id, Order order) { 62 | Order managedOrder = orderRepository.findById(id); 63 | managedOrder.setType(order.getType()); 64 | managedOrder.setOrigin(order.getOrigin()); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/boundary/IndexController.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.boundary; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 4 | import io.quarkus.qute.Location; 5 | import io.quarkus.qute.Template; 6 | import io.quarkus.qute.TemplateInstance; 7 | 8 | import jakarta.inject.Inject; 9 | import jakarta.ws.rs.GET; 10 | import jakarta.ws.rs.Path; 11 | import jakarta.ws.rs.Produces; 12 | import jakarta.ws.rs.core.MediaType; 13 | import java.util.List; 14 | 15 | @Path("index.html") 16 | @Produces(MediaType.TEXT_HTML) 17 | public class IndexController { 18 | 19 | @Inject 20 | CoffeeShop coffeeShop; 21 | 22 | @Location("index.html") 23 | Template index; 24 | 25 | @GET 26 | public TemplateInstance index() { 27 | List orders = coffeeShop.getOrders(); 28 | return index.data("orders", orders); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /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.Location; 7 | import io.quarkus.qute.Template; 8 | import io.quarkus.qute.TemplateInstance; 9 | 10 | import jakarta.inject.Inject; 11 | import jakarta.ws.rs.*; 12 | import jakarta.ws.rs.core.MediaType; 13 | import jakarta.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 | @Location("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/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 jakarta.inject.Inject; 8 | import jakarta.json.JsonArray; 9 | import jakarta.json.JsonObject; 10 | import jakarta.servlet.http.HttpServletRequest; 11 | import jakarta.validation.Valid; 12 | import jakarta.ws.rs.*; 13 | import jakarta.ws.rs.core.Context; 14 | import jakarta.ws.rs.core.MediaType; 15 | import jakarta.ws.rs.core.Response; 16 | import jakarta.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/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 jakarta.enterprise.context.RequestScoped; 8 | import jakarta.inject.Inject; 9 | import jakarta.json.Json; 10 | import jakarta.json.JsonArray; 11 | import jakarta.json.JsonArrayBuilder; 12 | import jakarta.ws.rs.GET; 13 | import jakarta.ws.rs.Path; 14 | import jakarta.ws.rs.PathParam; 15 | import jakarta.ws.rs.Produces; 16 | import jakarta.ws.rs.core.Context; 17 | import jakarta.ws.rs.core.MediaType; 18 | import jakarta.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 jakarta.enterprise.context.RequestScoped; 6 | import jakarta.inject.Inject; 7 | import jakarta.json.Json; 8 | import jakarta.json.JsonArray; 9 | import jakarta.json.JsonArrayBuilder; 10 | import jakarta.ws.rs.GET; 11 | import jakarta.ws.rs.Path; 12 | import jakarta.ws.rs.Produces; 13 | import jakarta.ws.rs.container.ResourceContext; 14 | import jakarta.ws.rs.core.Context; 15 | import jakarta.ws.rs.core.MediaType; 16 | import jakarta.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/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 jakarta.annotation.PostConstruct; 6 | import org.eclipse.microprofile.config.inject.ConfigProperty; 7 | 8 | import jakarta.enterprise.context.ApplicationScoped; 9 | import jakarta.inject.Inject; 10 | import jakarta.json.Json; 11 | import jakarta.json.JsonObject; 12 | import jakarta.ws.rs.client.Client; 13 | import jakarta.ws.rs.client.ClientBuilder; 14 | import jakarta.ws.rs.client.Entity; 15 | import jakarta.ws.rs.client.WebTarget; 16 | import jakarta.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().toString()) 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/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 jakarta.enterprise.context.ApplicationScoped; 10 | import jakarta.json.*; 11 | import jakarta.servlet.http.HttpServletRequest; 12 | import jakarta.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/src/main/java/com/sebastian_daschner/coffee_shop/orders/control/OrderProcessor.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 | 6 | import jakarta.enterprise.context.ApplicationScoped; 7 | import jakarta.inject.Inject; 8 | import jakarta.transaction.Transactional; 9 | 10 | @ApplicationScoped 11 | public class OrderProcessor { 12 | 13 | @Inject 14 | OrderRepository orderRepository; 15 | 16 | @Inject 17 | Barista barista; 18 | 19 | @Transactional(Transactional.TxType.REQUIRES_NEW) 20 | public void processOrder(Order order) { 21 | Order managedOrder = orderRepository.findById(order.getId()); 22 | OrderStatus status = barista.retrieveOrderStatus(managedOrder); 23 | managedOrder.setStatus(status); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/control/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.control; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 4 | import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; 5 | 6 | import jakarta.enterprise.context.ApplicationScoped; 7 | import java.util.List; 8 | import java.util.UUID; 9 | 10 | @ApplicationScoped 11 | public class OrderRepository implements PanacheRepositoryBase { 12 | 13 | public List listUnfinishedOrders() { 14 | return list("status <> 'COLLECTED'"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /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 jakarta.enterprise.context.ApplicationScoped; 9 | import jakarta.inject.Inject; 10 | import jakarta.json.JsonObject; 11 | import jakarta.validation.ConstraintValidator; 12 | import jakarta.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/src/main/java/com/sebastian_daschner/coffee_shop/orders/control/OriginRepository.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.control; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType; 4 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin; 5 | import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; 6 | 7 | import jakarta.enterprise.context.ApplicationScoped; 8 | import java.util.Set; 9 | import java.util.stream.Collectors; 10 | 11 | @ApplicationScoped 12 | public class OriginRepository implements PanacheRepositoryBase { 13 | 14 | public Set listForCoffeeType(CoffeeType type) { 15 | return streamAll() 16 | .filter(t -> t.getCoffeeTypes().contains(type)) 17 | .collect(Collectors.toSet()); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/control/StringExtensions.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.control; 2 | 3 | import io.quarkus.qute.TemplateExtension; 4 | 5 | public class StringExtensions { 6 | 7 | @TemplateExtension(namespace = "string") 8 | public static String capitalize(String word) { 9 | return Character.toUpperCase(word.charAt(0)) + word.substring(1).toLowerCase(); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/entity/CoffeeType.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.entity; 2 | 3 | import java.util.stream.Stream; 4 | 5 | public enum CoffeeType { 6 | 7 | ESPRESSO("Espresso"), 8 | POUR_OVER("Pour over"), 9 | LATTE("Latte"); 10 | 11 | private final String description; 12 | 13 | CoffeeType(String description) { 14 | this.description = description; 15 | } 16 | 17 | public String getDescription() { 18 | return description; 19 | } 20 | 21 | public static CoffeeType fromString(String string) { 22 | return Stream.of(CoffeeType.values()) 23 | .filter(t -> t.name().equalsIgnoreCase(string)) 24 | .findAny().orElse(null); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /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 jakarta.persistence.*; 4 | import java.util.Objects; 5 | import java.util.UUID; 6 | 7 | @Entity 8 | @Table(name = "orders") 9 | public class Order { 10 | 11 | @Id 12 | private UUID id; 13 | 14 | @Basic(optional = false) 15 | @Enumerated(EnumType.STRING) 16 | private CoffeeType type; 17 | 18 | @ManyToOne(optional = false) 19 | private Origin origin; 20 | 21 | @Basic(optional = false) 22 | @Enumerated(EnumType.STRING) 23 | private OrderStatus status = OrderStatus.PREPARING; 24 | 25 | public Order() { 26 | } 27 | 28 | public Order(UUID id, CoffeeType type, Origin origin) { 29 | Objects.requireNonNull(id); 30 | Objects.requireNonNull(type); 31 | Objects.requireNonNull(origin); 32 | this.id = id; 33 | this.type = type; 34 | this.origin = origin; 35 | } 36 | 37 | public UUID getId() { 38 | return id; 39 | } 40 | 41 | public void setId(UUID id) { 42 | this.id = id; 43 | } 44 | 45 | public CoffeeType getType() { 46 | return type; 47 | } 48 | 49 | public void setType(CoffeeType type) { 50 | this.type = type; 51 | } 52 | 53 | public Origin getOrigin() { 54 | return origin; 55 | } 56 | 57 | public void setOrigin(Origin origin) { 58 | this.origin = origin; 59 | } 60 | 61 | public OrderStatus getStatus() { 62 | return status; 63 | } 64 | 65 | public void setStatus(OrderStatus status) { 66 | this.status = status; 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | return "Order{" + 72 | "id='" + id + '\'' + 73 | ", type='" + type + '\'' + 74 | ", origin='" + origin + '\'' + 75 | ", status=" + status + 76 | '}'; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/entity/OrderStatus.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.entity; 2 | 3 | import java.util.stream.Stream; 4 | 5 | public enum OrderStatus { 6 | 7 | PREPARING, 8 | FINISHED, 9 | COLLECTED; 10 | 11 | public static OrderStatus fromString(String string) { 12 | return Stream.of(OrderStatus.values()) 13 | .filter(t -> t.name().equalsIgnoreCase(string)) 14 | .findAny().orElse(null); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /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 jakarta.persistence.*; 4 | import java.util.EnumSet; 5 | import java.util.Set; 6 | 7 | @Entity 8 | @Table(name = "origins") 9 | public class Origin { 10 | 11 | @Id 12 | private String name; 13 | 14 | @ElementCollection(fetch = FetchType.EAGER) 15 | @CollectionTable(name = "origin_coffee_types", joinColumns = @JoinColumn(name = "origin_name", nullable = false)) 16 | @Column(name = "coffee_type", nullable = false) 17 | @Enumerated(value = EnumType.STRING) 18 | private Set coffeeTypes = EnumSet.noneOf(CoffeeType.class); 19 | 20 | public Origin() { 21 | } 22 | 23 | public Origin(final String name) { 24 | this.name = name; 25 | } 26 | 27 | public String getName() { 28 | return name; 29 | } 30 | 31 | public Set getCoffeeTypes() { 32 | return coffeeTypes; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "Origin{" + 38 | "name='" + name + '\'' + 39 | ", coffeeTypes=" + coffeeTypes + 40 | '}'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/entity/ValidOrder.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.entity; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.control.OrderValidator; 4 | 5 | import jakarta.validation.Constraint; 6 | import jakarta.validation.Payload; 7 | import jakarta.validation.constraints.NotNull; 8 | import java.lang.annotation.Documented; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import static java.lang.annotation.ElementType.*; 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | @Target({FIELD, METHOD, ANNOTATION_TYPE, PARAMETER}) 16 | @Retention(RUNTIME) 17 | @NotNull 18 | @Constraint(validatedBy = OrderValidator.class) 19 | @Documented 20 | public @interface ValidOrder { 21 | 22 | String message() default ""; 23 | 24 | Class[] groups() default {}; 25 | 26 | Class[] payload() default {}; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /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/src/main/resources/META-INF/resources/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 1rem auto; 3 | width: 40rem; 4 | font-family: 'IBM Plex Sans', Arial, sans-serif; 5 | } 6 | 7 | a { 8 | color: #000; 9 | } 10 | 11 | th { 12 | text-align: left 13 | } 14 | 15 | td, th { 16 | padding-right: 0.8rem; 17 | padding-bottom: 0.4rem 18 | } 19 | 20 | .grid { 21 | display: grid; 22 | grid-auto-flow: column; 23 | grid-column-gap: 1rem; 24 | grid-auto-columns: max-content; 25 | } 26 | 27 | .error { 28 | color: red; 29 | font-size: 0.9rem; 30 | } -------------------------------------------------------------------------------- /coffee-shop/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #quarkus.datasource.devservices.init-script-path=scripts/schema-load-data.sql 2 | #quarkus.datasource.devservices.init-script-path=scripts/load-data.sql 3 | 4 | barista.url=http://barista:8080/processes 5 | %dev.barista.url=http://localhost:8002/processes 6 | #%test.barista.url=http://localhost:8002/processes 7 | 8 | #quarkus.hibernate-orm.database.generation=none 9 | #%dev.quarkus.hibernate-orm.database.generation=drop-and-create 10 | #%dev.quarkus.hibernate-orm.sql-load-script=scripts/load-data.sql 11 | 12 | 13 | quarkus.datasource.db-kind=postgresql 14 | quarkus.datasource.jdbc.url=jdbc:postgresql://coffee-shop-db:5432/postgres 15 | %dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres 16 | quarkus.datasource.username=postgres 17 | quarkus.datasource.password=postgres 18 | 19 | #quarkus.hibernate-orm.database.generation=none 20 | -------------------------------------------------------------------------------- /coffee-shop/src/main/resources/scripts/load-data.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- insert data 3 | 4 | INSERT INTO origins VALUES ('Ethiopia'); 5 | INSERT INTO origin_coffee_types VALUES ('Ethiopia', 'ESPRESSO'); 6 | INSERT INTO origin_coffee_types VALUES ('Ethiopia', 'LATTE'); 7 | INSERT INTO origin_coffee_types VALUES ('Ethiopia', 'POUR_OVER'); 8 | 9 | INSERT INTO origins VALUES ('Colombia'); 10 | INSERT INTO origin_coffee_types VALUES ('Colombia', 'ESPRESSO'); 11 | INSERT INTO origin_coffee_types VALUES ('Colombia', 'POUR_OVER'); -------------------------------------------------------------------------------- /coffee-shop/src/main/resources/scripts/schema-load-data.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE public.orders ( 2 | id uuid NOT NULL, 3 | status character varying(255) NOT NULL, 4 | type character varying(255) NOT NULL, 5 | origin_name character varying(255) NOT NULL 6 | ); 7 | 8 | CREATE TABLE public.origin_coffee_types ( 9 | origin_name character varying(255) NOT NULL, 10 | coffee_type character varying(255) NOT NULL 11 | ); 12 | 13 | CREATE TABLE public.origins ( 14 | name character varying(255) NOT NULL 15 | ); 16 | 17 | ALTER TABLE ONLY public.orders 18 | ADD CONSTRAINT orders_pkey PRIMARY KEY (id); 19 | 20 | ALTER TABLE ONLY public.origin_coffee_types 21 | ADD CONSTRAINT origin_coffee_types_pkey PRIMARY KEY (origin_name, coffee_type); 22 | 23 | ALTER TABLE ONLY public.origins 24 | ADD CONSTRAINT origins_pkey PRIMARY KEY (name); 25 | 26 | ALTER TABLE ONLY public.origin_coffee_types 27 | ADD CONSTRAINT fksny5vf8j30otg213opmbp0ab9 FOREIGN KEY (origin_name) REFERENCES public.origins(name); 28 | 29 | ALTER TABLE ONLY public.orders 30 | ADD CONSTRAINT fkt474abpx5pxojjm61tb1d64vp FOREIGN KEY (origin_name) REFERENCES public.origins(name); 31 | 32 | -- 33 | -- insert data 34 | 35 | INSERT INTO origins VALUES ('Ethiopia'); 36 | INSERT INTO origin_coffee_types VALUES ('Ethiopia', 'ESPRESSO'); 37 | INSERT INTO origin_coffee_types VALUES ('Ethiopia', 'LATTE'); 38 | INSERT INTO origin_coffee_types VALUES ('Ethiopia', 'POUR_OVER'); 39 | 40 | INSERT INTO origins VALUES ('Colombia'); 41 | INSERT INTO origin_coffee_types VALUES ('Colombia', 'ESPRESSO'); 42 | INSERT INTO origin_coffee_types VALUES ('Colombia', 'POUR_OVER'); 43 | -------------------------------------------------------------------------------- /coffee-shop/src/main/resources/scripts/schema.sql: -------------------------------------------------------------------------------- 1 | -- Name: orders; Type: TABLE; Schema: public; Owner: postgres 2 | -- 3 | 4 | CREATE TABLE public.orders ( 5 | id uuid NOT NULL, 6 | status character varying(255) NOT NULL, 7 | type character varying(255) NOT NULL, 8 | origin_name character varying(255) NOT NULL 9 | ); 10 | 11 | 12 | ALTER TABLE public.orders OWNER TO postgres; 13 | 14 | -- 15 | -- Name: origin_coffee_types; Type: TABLE; Schema: public; Owner: postgres 16 | -- 17 | 18 | CREATE TABLE public.origin_coffee_types ( 19 | origin_name character varying(255) NOT NULL, 20 | coffee_type character varying(255) NOT NULL 21 | ); 22 | 23 | 24 | ALTER TABLE public.origin_coffee_types OWNER TO postgres; 25 | 26 | -- 27 | -- Name: origins; Type: TABLE; Schema: public; Owner: postgres 28 | -- 29 | 30 | CREATE TABLE public.origins ( 31 | name character varying(255) NOT NULL 32 | ); 33 | 34 | 35 | ALTER TABLE public.origins OWNER TO postgres; 36 | 37 | -- 38 | -- Name: orders orders_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres 39 | -- 40 | 41 | ALTER TABLE ONLY public.orders 42 | ADD CONSTRAINT orders_pkey PRIMARY KEY (id); 43 | 44 | 45 | -- 46 | -- Name: origin_coffee_types origin_coffee_types_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres 47 | -- 48 | 49 | ALTER TABLE ONLY public.origin_coffee_types 50 | ADD CONSTRAINT origin_coffee_types_pkey PRIMARY KEY (origin_name, coffee_type); 51 | 52 | 53 | -- 54 | -- Name: origins origins_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres 55 | -- 56 | 57 | ALTER TABLE ONLY public.origins 58 | ADD CONSTRAINT origins_pkey PRIMARY KEY (name); 59 | 60 | 61 | -- 62 | -- Name: origin_coffee_types fksny5vf8j30otg213opmbp0ab9; Type: FK CONSTRAINT; Schema: public; Owner: postgres 63 | -- 64 | 65 | ALTER TABLE ONLY public.origin_coffee_types 66 | ADD CONSTRAINT fksny5vf8j30otg213opmbp0ab9 FOREIGN KEY (origin_name) REFERENCES public.origins(name); 67 | 68 | 69 | -- 70 | -- Name: orders fkt474abpx5pxojjm61tb1d64vp; Type: FK CONSTRAINT; Schema: public; Owner: postgres 71 | -- 72 | 73 | ALTER TABLE ONLY public.orders 74 | ADD CONSTRAINT fkt474abpx5pxojjm61tb1d64vp FOREIGN KEY (origin_name) REFERENCES public.origins(name); 75 | 76 | 77 | -- 78 | -- PostgreSQL database dump complete 79 | -- 80 | 81 | -------------------------------------------------------------------------------- /coffee-shop/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Coffee orders 8 | 9 | 10 | 11 | 12 |

All coffee orders

13 | 14 |

Create coffee order

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {#for order in orders} 23 | 24 | 25 | 26 | 27 | 28 | {/for} 29 |
StatusTypeOrigin
{string:capitalize(order.status.name)}{order.type.description}{order.origin.name}
30 | 31 | 32 | -------------------------------------------------------------------------------- /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 |
19 |
20 | 26 | 29 | 30 |
31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/it/CoffeeOrderSystem.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.it; 2 | 3 | import jakarta.json.JsonArray; 4 | import jakarta.json.JsonObject; 5 | import jakarta.ws.rs.client.Client; 6 | import jakarta.ws.rs.client.ClientBuilder; 7 | import jakarta.ws.rs.client.WebTarget; 8 | import jakarta.ws.rs.core.MediaType; 9 | import jakarta.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/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/it/CreateOrderSmokeIT.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.it; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | class CreateOrderSmokeIT { 10 | 11 | private CoffeeOrderSystem coffeeOrderSystem; 12 | 13 | @BeforeEach 14 | void setUp() { 15 | coffeeOrderSystem = new CoffeeOrderSystem(); 16 | } 17 | 18 | @Test 19 | void testIsSystemUp() { 20 | assertThat(coffeeOrderSystem.isSystemUp()).isTrue(); 21 | } 22 | 23 | @Test 24 | void testGetTypes() { 25 | assertThat(coffeeOrderSystem.getTypes()).containsExactlyInAnyOrder("Espresso", "Pour_over", "Latte"); 26 | } 27 | 28 | @Test 29 | void testGetTypeOrigins() { 30 | assertThat(coffeeOrderSystem.getOrigins("Espresso")).containsExactlyInAnyOrder("Colombia", "Ethiopia"); 31 | } 32 | 33 | @AfterEach 34 | void tearDown() { 35 | coffeeOrderSystem.close(); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/ReflectionSupport.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders; 2 | 3 | import java.lang.reflect.Field; 4 | 5 | public final class ReflectionSupport { 6 | 7 | private ReflectionSupport() { 8 | } 9 | 10 | public static void setReflectiveField(Object object, String fieldName, Object value) { 11 | try { 12 | Field f1 = object.getClass().getDeclaredField(fieldName); 13 | f1.setAccessible(true); 14 | f1.set(object, value); 15 | } catch (ReflectiveOperationException e) { 16 | throw new RuntimeException(e); 17 | } 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /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/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.control.OrderRepository; 6 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.ArgumentCaptor; 10 | import org.mockito.Captor; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | 15 | import java.util.List; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | import static org.mockito.Mockito.*; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | class CoffeeShopMockitoTest { 22 | 23 | @InjectMocks 24 | CoffeeShop testObject; 25 | 26 | @Captor 27 | ArgumentCaptor orderCaptor; 28 | 29 | private final OrderRepository orderRepository; 30 | private final OrderProcessor orderProcessor; 31 | 32 | CoffeeShopMockitoTest(@Mock OrderRepository orderRepository, @Mock OrderProcessor orderProcessor) { 33 | this.orderRepository = orderRepository; 34 | this.orderProcessor = orderProcessor; 35 | } 36 | 37 | @Test 38 | void testProcessUnfinishedOrders() { 39 | List orders = TestData.unfinishedOrders(); 40 | when(orderRepository.listUnfinishedOrders()).thenReturn(orders); 41 | 42 | testObject.processUnfinishedOrders(); 43 | 44 | verify(orderRepository).listUnfinishedOrders(); 45 | 46 | verify(orderProcessor, times(orders.size())).processOrder(orderCaptor.capture()); 47 | assertThat(orderCaptor.getAllValues()).containsExactlyElementsOf(orders); 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopNaiveTest.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.control.OrderRepository; 6 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType; 7 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 8 | import com.sebastian_daschner.coffee_shop.orders.entity.OrderStatus; 9 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.mockito.ArgumentCaptor; 13 | 14 | import java.util.Arrays; 15 | import java.util.List; 16 | import java.util.UUID; 17 | 18 | import static com.sebastian_daschner.coffee_shop.orders.ReflectionSupport.setReflectiveField; 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.mockito.Mockito.*; 21 | 22 | class CoffeeShopNaiveTest { 23 | 24 | private CoffeeShop coffeeShop; 25 | private OrderRepository orderRepository; 26 | private Barista barista; 27 | private ArgumentCaptor orderCaptor; 28 | 29 | @BeforeEach 30 | void setUp() { 31 | coffeeShop = new CoffeeShop(); 32 | OrderProcessor orderProcessor = new OrderProcessor(); 33 | 34 | coffeeShop.orderProcessor = orderProcessor; 35 | orderRepository = mock(OrderRepository.class); 36 | coffeeShop.orderRepository = orderRepository; 37 | 38 | barista = mock(Barista.class); 39 | setReflectiveField(orderProcessor, "orderRepository", orderRepository); 40 | setReflectiveField(orderProcessor, "barista", barista); 41 | orderCaptor = ArgumentCaptor.forClass(Order.class); 42 | 43 | when(barista.retrieveOrderStatus(orderCaptor.capture())).thenReturn(OrderStatus.PREPARING); 44 | } 45 | 46 | @Test 47 | void testCreateOrder() { 48 | Order order = new Order(); 49 | coffeeShop.createOrder(order); 50 | verify(orderRepository).persist(order); 51 | } 52 | 53 | @Test 54 | void testProcessUnfinishedOrders() { 55 | List orders = Arrays.asList(new Order(UUID.randomUUID(), CoffeeType.ESPRESSO, new Origin("Colombia")), 56 | new Order(UUID.randomUUID(), CoffeeType.ESPRESSO, new Origin("Ethiopia"))); 57 | 58 | when(orderRepository.listUnfinishedOrders()).thenReturn(orders); 59 | orders.forEach(o -> when(orderRepository.findById(o.getId())).thenReturn(o)); 60 | 61 | coffeeShop.processUnfinishedOrders(); 62 | 63 | verify(orderRepository).listUnfinishedOrders(); 64 | 65 | verify(barista, times(orders.size())).retrieveOrderStatus(any()); 66 | assertThat(orderCaptor.getAllValues()).containsExactlyElementsOf(orders); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopQuarkusTest.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.control.OrderRepository; 6 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 7 | import io.quarkus.test.junit.QuarkusTest; 8 | import io.quarkus.test.junit.mockito.InjectMock; 9 | import io.quarkus.test.junit.mockito.InjectSpy; 10 | import org.junit.jupiter.api.Disabled; 11 | import org.junit.jupiter.api.Test; 12 | 13 | import jakarta.inject.Inject; 14 | import java.util.List; 15 | 16 | import static org.mockito.Mockito.*; 17 | 18 | @QuarkusTest 19 | @Disabled("too slow") 20 | class CoffeeShopQuarkusTest { 21 | 22 | @Inject 23 | CoffeeShop coffeeShop; 24 | 25 | @InjectMock 26 | OrderRepository orderRepository; 27 | 28 | @InjectSpy 29 | OrderProcessor orderProcessor; 30 | 31 | @Test 32 | void testProcessUnfinishedOrders() { 33 | List orders = TestData.unfinishedOrders(); 34 | when(orderRepository.listUnfinishedOrders()).thenReturn(orders); 35 | orders.forEach(o -> when(orderRepository.findById(o.getId())).thenReturn(o)); 36 | 37 | coffeeShop.processUnfinishedOrders(); 38 | 39 | verify(orderProcessor, times(orders.size())).processOrder(any()); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /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.control.OrderRepository; 5 | import com.sebastian_daschner.coffee_shop.orders.control.OriginRepository; 6 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 7 | 8 | import java.util.List; 9 | 10 | import static org.mockito.Mockito.*; 11 | 12 | public class CoffeeShopTestDouble extends CoffeeShop { 13 | 14 | public CoffeeShopTestDouble(OrderProcessorTestDouble orderProcessorTestDouble) { 15 | orderRepository = mock(OrderRepository.class); 16 | originRepository = mock(OriginRepository.class); 17 | orderProcessor = orderProcessorTestDouble; 18 | } 19 | 20 | public void verifyCreateOrder(Order order) { 21 | verify(orderRepository).persist(order); 22 | } 23 | 24 | public void verifyProcessUnfinishedOrders(List orders) { 25 | verify(orderRepository).listUnfinishedOrders(); 26 | ((OrderProcessorTestDouble) orderProcessor).verifyProcessOrders(orders); 27 | } 28 | 29 | public void answerForUnfinishedOrders(List orders) { 30 | when(orderRepository.listUnfinishedOrders()).thenReturn(orders); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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.TestData; 4 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessorTestDouble; 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 | 9 | import java.util.List; 10 | 11 | class CoffeeShopUseCaseTest { 12 | 13 | private CoffeeShopTestDouble coffeeShop; 14 | 15 | @BeforeEach 16 | void setUp() { 17 | OrderProcessorTestDouble orderProcessor = new OrderProcessorTestDouble(); 18 | coffeeShop = new CoffeeShopTestDouble(orderProcessor); 19 | } 20 | 21 | @Test 22 | void testCreateOrder() { 23 | Order order = new Order(); 24 | coffeeShop.createOrder(order); 25 | coffeeShop.verifyCreateOrder(order); 26 | } 27 | 28 | @Test 29 | void testProcessUnfinishedOrders() { 30 | List orders = TestData.unfinishedOrders(); 31 | 32 | coffeeShop.answerForUnfinishedOrders(orders); 33 | 34 | coffeeShop.processUnfinishedOrders(); 35 | coffeeShop.verifyProcessUnfinishedOrders(orders); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /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.mockito.ArgumentCaptor; 6 | 7 | import java.util.List; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.mockito.ArgumentMatchers.any; 11 | import static org.mockito.Mockito.*; 12 | 13 | public class OrderProcessorTestDouble extends OrderProcessor { 14 | 15 | private final ArgumentCaptor orderCaptor; 16 | 17 | public OrderProcessorTestDouble() { 18 | orderRepository = mock(OrderRepository.class); 19 | barista = mock(Barista.class); 20 | orderCaptor = ArgumentCaptor.forClass(Order.class); 21 | 22 | when(barista.retrieveOrderStatus(orderCaptor.capture())).thenReturn(OrderStatus.PREPARING); 23 | } 24 | 25 | public void verifyProcessOrders(List orders) { 26 | verify(barista, times(orders.size())).retrieveOrderStatus(any()); 27 | assertThat(orderCaptor.getAllValues()).containsExactlyElementsOf(orders); 28 | } 29 | 30 | @Override 31 | public void processOrder(Order order) { 32 | when(orderRepository.findById(order.getId())).thenReturn(order); 33 | super.processOrder(order); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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 jakarta.json.Json; 11 | import jakarta.json.JsonObject; 12 | import jakarta.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/src/test/java/com/sebastian_daschner/coffee_shop/orders/control/RunCucumberTest.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.control; 2 | 3 | import cucumber.api.CucumberOptions; 4 | import cucumber.api.junit.Cucumber; 5 | import org.junit.runner.RunWith; 6 | 7 | @RunWith(Cucumber.class) 8 | @CucumberOptions 9 | public class RunCucumberTest { 10 | 11 | 12 | 13 | } 14 | -------------------------------------------------------------------------------- /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 jakarta.json.Json; 12 | import jakarta.json.JsonObject; 13 | import jakarta.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/src/test/java/com/sebastian_daschner/coffee_shop/orders/entity/OrderAssert.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.entity; 2 | 3 | import org.assertj.core.api.AbstractAssert; 4 | 5 | public class OrderAssert extends AbstractAssert { 6 | 7 | public OrderAssert(Order order) { 8 | super(order, OrderAssert.class); 9 | } 10 | 11 | public static OrderAssert assertThat(Order actual) { 12 | return new OrderAssert(actual); 13 | } 14 | 15 | public OrderAssert isPreparing() { 16 | isNotNull(); 17 | if (actual.getStatus() != OrderStatus.PREPARING) { 18 | failWithMessage("Expected the order to be in status PREPARING but was %s", actual.getStatus()); 19 | } 20 | return this; 21 | } 22 | 23 | public OrderAssert containsMilk() { 24 | isNotNull(); 25 | if (actual.getType() != CoffeeType.LATTE) { 26 | failWithMessage("Expected the coffee order to contain milk but the coffee type was %s", actual.getType()); 27 | } 28 | return this; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /coffee-shop/src/test/resources/com/sebastian_daschner/coffee_shop/orders/control/validating-coffee-orders.feature: -------------------------------------------------------------------------------- 1 | Feature: Validating coffee order 2 | 3 | Scenario Outline: Creating from , will be 4 | When I create an order with from 5 | Then The order should be 6 | 7 | Examples: 8 | | type | origin | result | 9 | | Espresso | Colombia | accepted | 10 | | Pour_over | Colombia | accepted | 11 | | Espresso | Ethiopia | accepted | 12 | | Latte | Ethiopia | accepted | 13 | | Pour_over | Ethiopia | accepted | 14 | | Espresso | Germany | rejected | 15 | | Siphon | Colombia | rejected | 16 | | Siphon | Germany | rejected | 17 | -------------------------------------------------------------------------------- /local-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | cd ${0%/*}/coffee-shop 4 | 5 | mvn clean package 6 | 7 | docker build -t coffee-shop . 8 | -------------------------------------------------------------------------------- /local-run-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | cd ${0%/*}/coffee-shop 4 | 5 | docker stop coffee-shop coffee-shop-db barista &> /dev/null || true 6 | 7 | docker run -d --rm \ 8 | --name coffee-shop-db \ 9 | --network dkrnet \ 10 | -p 5432:5432 \ 11 | -e POSTGRES_USER=postgres \ 12 | -e POSTGRES_PASSWORD=postgres \ 13 | -v $(pwd)/src/main/resources/scripts:/scripts:ro \ 14 | postgres:15.2 15 | 16 | docker run -d --rm \ 17 | --name barista \ 18 | --network dkrnet \ 19 | -p 8002:8080 \ 20 | sdaschner/barista:quarkus-testing-1 21 | 22 | echo 'waiting for db startup' 23 | until docker exec coffee-shop-db pg_isready -h localhost > /dev/null; do 24 | sleep 0.5 25 | done; 26 | 27 | echo 'starting coffee-shop' 28 | docker run -d --rm \ 29 | --name coffee-shop \ 30 | --network dkrnet \ 31 | -p 8001:8080 \ 32 | coffee-shop 33 | 34 | until [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:8001/q/health)" == "200" ]]; do 35 | sleep 0.5 36 | done; 37 | echo 'all containers started' 38 | 39 | #echo 'loading data into database' 40 | #docker exec coffee-shop-db psql -U postgres -f /scripts/load-data.sql -------------------------------------------------------------------------------- /presentation/slide001: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ___ __ __ _ _ _ _ _ 5 | | __|/ _|/ _|___ __| |_(_)_ _____ | |_ ___ __| |_(_)_ _ __ _ 6 | | _|| _| _/ -_) _| _| \ V / -_) | _/ -_|_-< _| | ' \/ _` | 7 | |___|_| |_| \___\__|\__|_|\_/\___| \__\___/__/\__|_|_||_\__, | 8 | |___/ 9 | _ _ _ _ 10 | | |_| |_ __ _| |_ (_)___ 11 | | _| ' \/ _` | _| | (_-< 12 | \__|_||_\__,_|\__| |_/__/ 13 | 14 | _ _ _ __ 15 | __ _ __| |_ _ _ __ _| | |_ _ / _|_ _ _ _ 16 | / _` / _| _| || / _` | | | || | | _| || | ' \ 17 | \__,_\__|\__|\_,_\__,_|_|_|\_, | |_| \_,_|_||_| 18 | |__/ 19 | 20 | 21 | Sebastian Daschner 22 | 23 | 24 | -------------------------------------------------------------------------------- /presentation/slide002: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sebastian Daschner 7 | 8 | • Conference speaker, trainer, book author 9 | • Jakarta EE Committer, JCP Expert Group member 10 | • Java Champion 11 | • Oracle Developer Champion (Alumni) 12 | • JavaOne Rockstar speaker 13 | -------------------------------------------------------------------------------- /presentation/slide003: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | user 6 | \ 7 | ┘ 8 | .──. ┌───────────┐ 9 | │DB│<-->│coffee-shop│ 10 | └──┘ └───────────┘ 11 | -------------------------------------------------------------------------------- /presentation/slide004: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | user 6 | \ 7 | ┘ 8 | .──. ┌───────────┐ 9 | │DB│<-->│coffee-shop│ 10 | └──┘ └───────────┘ 11 | \ async 12 | \ 13 | ┘ 14 | ┌─────────┐ 15 | │ barista │ 16 | └─────────┘ 17 | -------------------------------------------------------------------------------- /presentation/slide005: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | _ _ _ _ _ 10 | /_\ | |__ __| |_ _ _ __ _ __| |_(_)___ _ _ 11 | / _ \| '_ (_-< _| '_/ _` / _| _| / _ \ ' \ 12 | /_/ \_\_.__/__/\__|_| \__,_\__|\__|_\___/_||_| 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /presentation/slide006: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | system test 6 | | 7 | v 8 | .──. ┌───────────┐ 9 | │DB│<-->│coffee-shop│ 10 | └──┘ └───────────┘ 11 | | 12 | | 13 | v 14 | ┌─────────┐ 15 | │ barista │ 16 | │ (mock) │ 17 | └─────────┘ 18 | -------------------------------------------------------------------------------- /presentation/slide007: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | system test ---. 6 | | | 7 | v | 8 | .──. ┌───────────┐ | 9 | │DB│<-->│coffee-shop│ | control/verify 10 | └──┘ └───────────┘ | 11 | | | 12 | | | 13 | v | 14 | ┌─────────┐ | 15 | │ barista │ | 16 | │ (mock) │<---’ 17 | └─────────┘ 18 | 19 | 20 | -------------------------------------------------------------------------------- /presentation/slide008: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src/ system test --. 6 | \ | | 7 | \ | | 8 | ┘ v | 9 | .──. ┌───────────┐ | 10 | │DB│<-->│coffee-shop│ | control/verify 11 | └──┘ │ (:dev) │ | 12 | └───────────┘ | 13 | | | 14 | | | 15 | v | 16 | ┌─────────┐ | 17 | │ barista │ | 18 | │ (mock) │<---’ 19 | └─────────┘ 20 | 21 | 22 | -------------------------------------------------------------------------------- /presentation/slide009: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Use case tests 5 | 6 | 7 | ┌──────────┐ ┌──────────────┐ 8 | │CoffeeShop│ ----> │OrderProcessor│ 9 | └──────────┘ └──────────────┘ 10 | ^ ^ 11 | main | | 12 | ----------------------|--------------------|------- 13 | test | | 14 | ┌────────────┐ ┌───────────┐ 15 | │CoffeeShopTD│ ----> │OrderProcTD│ 16 | └────────────┘ └───────────┘ 17 | \ ┌────┐ 18 | ----> │Mock│ 19 | └────┘ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /presentation/slide010: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Local development: 5 | 6 | ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌────────────────┐ 7 | │ │ │ │ │ │ │ │ 8 | │ build/UT │-->│ deploy local │-->│ smoke ITs │-->│ system tests │ 9 | │(coffee-shop)│ │ (:dev mode) │ │(coffee-shop)│ │(coffee-shop-st)│ 10 | └─────────────┘ └──────────────┘ └─────────────┘ └────────────────┘ 11 | 12 | 13 | CI/CD: 14 | 15 | ┌─────────────┐ ┌────────────┐ ┌──────────────┐ ┌────────────────┐ 16 | │ │ │ │ │ │ │ │ 17 | │ build/UT │-->│ deploy │-->│ smoke ITs │-->│ system tests │-->... 18 | │(coffee-shop)│ │ (e.g. k8s) │ │(point to k8s)│ │ (point to k8s) │ 19 | └─────────────┘ └────────────┘ └──────────────┘ └────────────────┘ 20 | -------------------------------------------------------------------------------- /presentation/slide011: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Key Takeaways 7 | 8 | • Test code quality matters 9 | • Reusable test components 10 | • Fast feedback, turnaround cycles 11 | • Separate life cycles 12 | • Reusable, idempotent tests scenarios 13 | • Test code quality > test frameworks 14 | 15 | -------------------------------------------------------------------------------- /presentation/slide012: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Thank you for your attention! 7 | 8 | • daschner.dev/effective-tests 9 | • @DaschnerS 10 | • Book: Architecting Modern Java EE Applications 11 | 12 | • quarkus.io 13 | • wiremock.org 14 | -------------------------------------------------------------------------------- /prod-run-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | cd ${0%/*}/coffee-shop 4 | 5 | docker stop coffee-shop coffee-shop-db barista &> /dev/null || true 6 | 7 | docker run -d --rm \ 8 | --name coffee-shop-db \ 9 | --network dkrnet \ 10 | -p 5432:5432 \ 11 | -e POSTGRES_USER=postgres \ 12 | -e POSTGRES_PASSWORD=postgres \ 13 | -v $(pwd)/src/main/resources/scripts:/scripts:ro \ 14 | postgres:15.2 15 | 16 | docker run -d --rm \ 17 | --name barista \ 18 | --network dkrnet \ 19 | -p 8002:8080 \ 20 | sdaschner/barista:quarkus-testing-1 21 | 22 | echo 'waiting for db startup' 23 | until docker exec coffee-shop-db pg_isready -h localhost > /dev/null; do 24 | sleep 0.5 25 | done; 26 | 27 | echo 'creating schema' 28 | docker exec coffee-shop-db psql -U postgres -f /scripts/schema.sql 29 | echo 'loading data into database' 30 | docker exec coffee-shop-db psql -U postgres -f /scripts/load-data.sql 31 | 32 | echo 'starting coffee-shop' 33 | docker run -d --rm \ 34 | --name coffee-shop \ 35 | --network dkrnet \ 36 | -p 8001:8080 \ 37 | coffee-shop 38 | 39 | until [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:8001/q/health)" == "200" ]]; do 40 | sleep 0.5 41 | done; -------------------------------------------------------------------------------- /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 -t coffee-shop:tmp . 34 | 35 | docker run -d \ 36 | --name coffee-shop \ 37 | --network dkrnet \ 38 | -e QUARKUS_LAUNCH_DEVMODE=true \ 39 | -p 8001:8080 \ 40 | -p 5005:5005 \ 41 | coffee-shop:tmp \ 42 | java -jar \ 43 | "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" \ 44 | -Dquarkus.live-reload.password=123 \ 45 | -Dquarkus.http.host=0.0.0.0 \ 46 | /deployments/quarkus-run.jar 47 | 48 | # wait for app startup 49 | wget --quiet --tries=30 --waitretry=2 --retry-connrefused -O /dev/null http://localhost:8001/q/health 50 | 51 | mvn quarkus:remote-dev -Ddebug=false -Dquarkus.live-reload.url=http://localhost:8001 -Dquarkus.live-reload.password=123 -Dquarkus.package.type=mutable-jar 52 | -------------------------------------------------------------------------------- /systemtest-run-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | cd ${0%/*}/coffee-shop 4 | 5 | docker stop coffee-shop coffee-shop-db barista &> /dev/null || true 6 | 7 | docker run -d --rm \ 8 | --name coffee-shop-db \ 9 | --network dkrnet \ 10 | -p 5432:5432 \ 11 | -e POSTGRES_USER=postgres \ 12 | -e POSTGRES_PASSWORD=postgres \ 13 | -v $(pwd)/src/main/resources/scripts:/scripts:ro \ 14 | postgres:15.2 15 | 16 | docker run -d --rm \ 17 | --name barista \ 18 | --network dkrnet \ 19 | -p 8002:8080 \ 20 | rodolpheche/wiremock:2.6.0 21 | 22 | echo 'waiting for db startup' 23 | until docker exec coffee-shop-db pg_isready -h localhost > /dev/null; do 24 | sleep 0.5 25 | done; 26 | 27 | echo 'creating schema' 28 | docker exec coffee-shop-db psql -U postgres -f /scripts/schema.sql 29 | echo 'loading data into database' 30 | docker exec coffee-shop-db psql -U postgres -f /scripts/load-data.sql 31 | 32 | echo 'starting coffee-shop' 33 | docker run -d --rm \ 34 | --name coffee-shop \ 35 | --network dkrnet \ 36 | -p 8001:8080 \ 37 | coffee-shop 38 | 39 | until [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:8001/q/health)" == "200" ]]; do 40 | sleep 0.5 41 | done; 42 | --------------------------------------------------------------------------------