├── .gitignore ├── LICENSE ├── README.adoc ├── barista ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── sebastian_daschner │ └── barista │ ├── HealthCheckResource.java │ ├── IllegalArgumentExceptionMapper.java │ ├── OrderStatusProcessor.java │ └── ProcessesResource.java ├── coffee-shop-st ├── pom.xml └── src │ └── test │ ├── java │ ├── com │ │ └── sebastian_daschner │ │ │ └── coffee_shop │ │ │ ├── backend │ │ │ ├── CreateOrderNaiveTest.java │ │ │ ├── CreateOrderTest.java │ │ │ ├── CreateOrderValidationTest.java │ │ │ ├── entity │ │ │ │ └── Order.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 ├── create-order.sh ├── 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 │ │ │ ├── OrderValidator.java │ │ │ └── StringExtensions.java │ │ │ └── entity │ │ │ ├── CoffeeType.java │ │ │ ├── Order.java │ │ │ ├── OrderStatus.java │ │ │ ├── Origin.java │ │ │ └── ValidOrder.java │ └── resources │ │ ├── META-INF │ │ └── resources │ │ │ ├── form.js │ │ │ └── style.css │ │ ├── application.properties │ │ ├── load-data.sql │ │ └── templates │ │ ├── index.html │ │ └── order.html │ └── test │ ├── java │ └── com │ │ └── sebastian_daschner │ │ └── coffee_shop │ │ ├── it │ │ ├── CoffeeOrderSystem.java │ │ ├── CreateOrderQuarkusSmokeIT.java │ │ └── CreateOrderSmokeIT.java │ │ └── orders │ │ ├── TestData.java │ │ ├── boundary │ │ ├── CoffeeShopMockitoTest.java │ │ ├── CoffeeShopNaiveUseCaseTest.java │ │ ├── CoffeeShopTest.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 ├── nexus_settings.xml ├── nexus_settings_openshift.xml ├── 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 | 15 | .project 16 | .settings 17 | .classpath 18 | .settings/ 19 | 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /barista/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.sebastian-daschner.coffee 5 | barista 6 | 1.0-SNAPSHOT 7 | Barista service 8 | 9 | 10 | 3.8.1 11 | true 12 | 11 13 | 11 14 | UTF-8 15 | UTF-8 16 | 1.7.5.Final-redhat-00007 17 | quarkus-universe-bom 18 | com.redhat.quarkus 19 | 1.7.5.Final-redhat-00007 20 | 3.0.0-M5 21 | 3.2.2 22 | 23 | 24 | 25 | 26 | 27 | ${quarkus.platform.group-id} 28 | ${quarkus.platform.artifact-id} 29 | ${quarkus.platform.version} 30 | pom 31 | import 32 | 33 | 34 | 35 | 36 | 37 | 38 | io.quarkus 39 | quarkus-resteasy 40 | 41 | 42 | io.quarkus 43 | quarkus-resteasy-jackson 44 | 45 | 46 | io.quarkus 47 | quarkus-resteasy-jsonb 48 | 49 | 50 | io.quarkus 51 | quarkus-jsonp 52 | 53 | 54 | 55 | 56 | 57 | 58 | io.quarkus 59 | quarkus-maven-plugin 60 | ${quarkus-plugin.version} 61 | 62 | 63 | 64 | build 65 | 66 | 67 | 68 | 69 | true 70 | 71 | 72 | 73 | maven-compiler-plugin 74 | ${compiler-plugin.version} 75 | 76 | 77 | maven-surefire-plugin 78 | ${surefire-plugin.version} 79 | 80 | 81 | org.jboss.logmanager.LogManager 82 | 83 | 84 | 85 | 86 | maven-war-plugin 87 | ${war-plugin.version} 88 | 89 | 90 | 91 | 92 | 93 | native 94 | 95 | 96 | native 97 | 98 | 99 | 100 | 101 | 102 | io.quarkus 103 | quarkus-maven-plugin 104 | ${quarkus-plugin.version} 105 | 106 | 107 | 108 | build 109 | 110 | 111 | 112 | 113 | false 114 | 115 | 116 | 117 | maven-failsafe-plugin 118 | ${surefire-plugin.version} 119 | 120 | 121 | 122 | integration-test 123 | verify 124 | 125 | 126 | 127 | ${project.build.directory}/${project.build.finalName}-runner 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | native 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /barista/src/main/java/com/sebastian_daschner/barista/HealthCheckResource.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.barista; 2 | 3 | import javax.ws.rs.GET; 4 | import javax.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 javax.ws.rs.core.Response; 4 | import javax.ws.rs.ext.ExceptionMapper; 5 | import javax.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 javax.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 javax.inject.Inject; 4 | import javax.json.Json; 5 | import javax.json.JsonObject; 6 | import javax.ws.rs.POST; 7 | import javax.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 | org.junit.jupiter 15 | junit-jupiter-api 16 | 5.7.1 17 | test 18 | 19 | 20 | org.junit.jupiter 21 | junit-jupiter-engine 22 | 5.7.1 23 | test 24 | 25 | 26 | org.junit.vintage 27 | junit-vintage-engine 28 | 5.7.1 29 | test 30 | 31 | 32 | org.mockito 33 | mockito-core 34 | 3.8.0 35 | test 36 | 37 | 38 | org.assertj 39 | assertj-core 40 | 3.19.0 41 | test 42 | 43 | 44 | 45 | 46 | io.cucumber 47 | cucumber-java 48 | 4.3.0 49 | test 50 | 51 | 52 | io.cucumber 53 | cucumber-junit 54 | 4.3.0 55 | test 56 | 57 | 58 | 59 | 60 | org.glassfish.jersey.core 61 | jersey-client 62 | 2.26 63 | test 64 | 65 | 66 | org.glassfish.jersey.inject 67 | jersey-hk2 68 | 2.26 69 | test 70 | 71 | 72 | org.glassfish.jersey.media 73 | jersey-media-json-jackson 74 | 2.26 75 | test 76 | 77 | 78 | org.glassfish.jersey.media 79 | jersey-media-json-processing 80 | 2.26 81 | test 82 | 83 | 84 | javax.xml.bind 85 | jaxb-api 86 | 2.3.1 87 | test 88 | 89 | 90 | javax.activation 91 | activation 92 | 1.1 93 | test 94 | 95 | 96 | 97 | 98 | com.codeborne 99 | selenide 100 | 5.19.0 101 | test 102 | 103 | 104 | 105 | 106 | com.github.tomakehurst 107 | wiremock 108 | 2.27.2 109 | test 110 | 111 | 112 | 113 | 114 | coffee-shop-st 115 | 116 | 117 | maven-surefire-plugin 118 | 2.22.2 119 | 120 | 121 | 122 | 123 | 124 | 14 125 | 14 126 | UTF-8 127 | 128 | 129 | -------------------------------------------------------------------------------- /coffee-shop-st/src/test/java/com/sebastian_daschner/coffee_shop/backend/CreateOrderNaiveTest.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.backend; 2 | 3 | import com.sebastian_daschner.coffee_shop.backend.entity.Order; 4 | import org.assertj.core.api.Assertions; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import javax.json.Json; 9 | import javax.json.JsonArray; 10 | import javax.json.JsonObject; 11 | import javax.json.JsonObjectBuilder; 12 | import javax.ws.rs.client.Client; 13 | import javax.ws.rs.client.ClientBuilder; 14 | import javax.ws.rs.client.Entity; 15 | import javax.ws.rs.client.WebTarget; 16 | import javax.ws.rs.core.MediaType; 17 | import javax.ws.rs.core.Response; 18 | import javax.ws.rs.core.UriBuilder; 19 | import java.net.URI; 20 | import java.util.List; 21 | import java.util.stream.Collectors; 22 | 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | 25 | class CreateOrderNaiveTest { 26 | 27 | private Client client; 28 | private WebTarget ordersTarget; 29 | 30 | @BeforeEach 31 | void setUp() { 32 | client = ClientBuilder.newClient(); 33 | ordersTarget = client.target(buildUri()); 34 | } 35 | 36 | private URI buildUri() { 37 | String host = System.getProperty("coffee-shop.test.host", "localhost"); 38 | String port = System.getProperty("coffee-shop.test.port", "8001"); 39 | return UriBuilder.fromUri("http://{host}:{port}/orders") 40 | .build(host, port); 41 | } 42 | 43 | @Test 44 | void createVerifyOrder() { 45 | Order order = new Order("Espresso", "Colombia"); 46 | JsonObject requestBody = createJson(order); 47 | 48 | Response response = ordersTarget.request().post(Entity.json(requestBody)); 49 | 50 | if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) 51 | throw new AssertionError("Status was not successful: " + response.getStatus()); 52 | 53 | URI orderUri = response.getLocation(); 54 | 55 | Order loadedOrder = client.target(orderUri) 56 | .request(MediaType.APPLICATION_JSON_TYPE) 57 | .get(Order.class); 58 | 59 | Assertions.assertThat(loadedOrder).isEqualToComparingOnlyGivenFields(order, "type", "origin"); 60 | 61 | List orders = ordersTarget.request(MediaType.APPLICATION_JSON_TYPE) 62 | .get(JsonArray.class).getValuesAs(JsonObject.class).stream() 63 | .map(o -> o.getString("_self")) 64 | .map(URI::create) 65 | .collect(Collectors.toList()); 66 | 67 | assertThat(orders).contains(orderUri); 68 | } 69 | 70 | JsonObject createJson(Order order) { 71 | JsonObjectBuilder builder = Json.createObjectBuilder(); 72 | 73 | if (order.getType() != null) 74 | builder.add("type", order.getType()); 75 | else 76 | builder.addNull("type"); 77 | if (order.getOrigin() != null) 78 | builder.add("origin", order.getOrigin()); 79 | else 80 | builder.addNull("origin"); 81 | 82 | return builder.build(); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /coffee-shop-st/src/test/java/com/sebastian_daschner/coffee_shop/backend/CreateOrderTest.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.backend; 2 | 3 | import com.sebastian_daschner.coffee_shop.backend.entity.Order; 4 | import com.sebastian_daschner.coffee_shop.backend.systems.BaristaSystem; 5 | import com.sebastian_daschner.coffee_shop.backend.systems.CoffeeOrderSystem; 6 | import org.assertj.core.api.Assertions; 7 | import org.junit.jupiter.api.AfterEach; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.net.URI; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | class CreateOrderTest { 16 | 17 | private CoffeeOrderSystem coffeeOrderSystem; 18 | private BaristaSystem baristaSystem; 19 | 20 | @BeforeEach 21 | void setUp() { 22 | coffeeOrderSystem = new CoffeeOrderSystem(); 23 | baristaSystem = new BaristaSystem(); 24 | } 25 | 26 | @Test 27 | void createVerifyOrder() { 28 | Order order = new Order("Espresso", "Colombia"); 29 | URI orderUri = coffeeOrderSystem.createOrder(order); 30 | 31 | Order loadedOrder = coffeeOrderSystem.getOrder(orderUri); 32 | Assertions.assertThat(loadedOrder).isEqualToComparingOnlyGivenFields(order, "type", "origin"); 33 | 34 | Assertions.assertThat(coffeeOrderSystem.getOrders()).contains(orderUri); 35 | } 36 | 37 | @Test 38 | void createOrderCheckStatusUpdate() { 39 | Order order = new Order("Espresso", "Colombia"); 40 | URI orderUri = coffeeOrderSystem.createOrder(order); 41 | 42 | baristaSystem.answerForOrder(orderUri, "PREPARING"); 43 | 44 | Order loadedOrder = coffeeOrderSystem.getOrder(orderUri); 45 | Assertions.assertThat(loadedOrder).isEqualToComparingOnlyGivenFields(order, "type", "origin"); 46 | 47 | loadedOrder = waitForProcessAndGet(orderUri, "PREPARING"); 48 | Assertions.assertThat(loadedOrder.getStatus()).isEqualTo("Preparing"); 49 | 50 | baristaSystem.answerForOrder(orderUri, "FINISHED"); 51 | 52 | loadedOrder = waitForProcessAndGet(orderUri, "FINISHED"); 53 | Assertions.assertThat(loadedOrder.getStatus()).isEqualTo("Finished"); 54 | } 55 | 56 | private Order waitForProcessAndGet(URI orderUri, String requestedStatus) { 57 | baristaSystem.waitForInvocation(orderUri, requestedStatus); 58 | return coffeeOrderSystem.getOrder(orderUri); 59 | } 60 | 61 | @AfterEach 62 | void close() { 63 | coffeeOrderSystem.close(); 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /coffee-shop-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/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 | assertThat(index.getPageHeader()).isEqualTo("All coffee orders"); 21 | assertThat(index.getListedOrders()).isEmpty(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /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 com.codeborne.selenide.SelenideElement; 4 | 5 | import java.util.List; 6 | 7 | import static com.codeborne.selenide.Condition.text; 8 | import static com.codeborne.selenide.Selenide.$; 9 | import static com.codeborne.selenide.Selenide.$$; 10 | import static java.util.stream.Collectors.toList; 11 | 12 | public class IndexView { 13 | 14 | public String getPageHeader() { 15 | return $("body > h1").text(); 16 | } 17 | 18 | public List getListedOrders() { 19 | return $$("body > table tr").stream() 20 | .map(el -> el.findAll("td")) 21 | .filter(list -> !list.isEmpty()) 22 | .map(list -> new Order(list.get(0).getText(), list.get(1).getText(), list.get(2).getText())) 23 | .collect(toList()); 24 | } 25 | 26 | public OrderView followCreateOrderLink() { 27 | createOrderLink().click(); 28 | return new OrderView(); 29 | } 30 | 31 | private SelenideElement createOrderLink() { 32 | return $$("a").findBy(text("Create")); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /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 | } 14 | -------------------------------------------------------------------------------- /coffee-shop-st/src/test/java/cucumber/StepDefs.java: -------------------------------------------------------------------------------- 1 | package cucumber; 2 | 3 | import com.sebastian_daschner.coffee_shop.backend.entity.Order; 4 | import com.sebastian_daschner.coffee_shop.backend.systems.CoffeeOrderSystem; 5 | import cucumber.api.java.After; 6 | import cucumber.api.java.Before; 7 | import cucumber.api.java.en.Then; 8 | import cucumber.api.java.en.When; 9 | 10 | import java.net.URI; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | public class StepDefs { 15 | 16 | private CoffeeOrderSystem coffeeOrderSystem; 17 | private Order order; 18 | 19 | @Before 20 | public void setUp() { 21 | coffeeOrderSystem = new CoffeeOrderSystem(); 22 | } 23 | 24 | @When("^I create an order with ([^ ]*) from ([^ ]*)$") 25 | public void i_create_an_order(String type, String origin) { 26 | order = new Order(type, origin); 27 | } 28 | 29 | @Then("^The order should be accepted$") 30 | public void accepted_order() { 31 | URI id = coffeeOrderSystem.createOrder(order); 32 | Order order = coffeeOrderSystem.getOrder(id); 33 | assertOrderMatches(this.order, order); 34 | } 35 | 36 | @Then("^The order should be rejected$") 37 | public void rejected_order() { 38 | coffeeOrderSystem.createInvalidOrder(order); 39 | } 40 | 41 | private void assertOrderMatches(Order actual, Order expected) { 42 | assertThat(actual).isEqualToComparingOnlyGivenFields(expected, "type", "origin"); 43 | } 44 | 45 | @After 46 | public void close() { 47 | coffeeOrderSystem.close(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /coffee-shop-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/create-order.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | curl http://$(oc get route coffee-shop --template='{{ .spec.host }}'/orders -XPOST -i -H 'Content-Type: application/json' -d '{"origin":"Colombia","type":"Espresso"}' 5 | -------------------------------------------------------------------------------- /coffee-shop/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | com.sebastian-daschner.coffee 7 | coffee-shop 8 | 1.0-SNAPSHOT 9 | Coffee shop service 10 | 11 | 12 | 3.8.1 13 | true 14 | 11 15 | 11 16 | UTF-8 17 | UTF-8 18 | 1.7.5.Final-redhat-00007 19 | quarkus-universe-bom 20 | com.redhat.quarkus 21 | 1.7.5.Final-redhat-00007 22 | 3.0.0-M5 23 | 24 | 25 | 26 | 27 | 28 | ${quarkus.platform.group-id} 29 | ${quarkus.platform.artifact-id} 30 | ${quarkus.platform.version} 31 | pom 32 | import 33 | 34 | 35 | 36 | 37 | 38 | 39 | io.quarkus 40 | quarkus-resteasy-jsonb 41 | 42 | 43 | io.quarkus 44 | quarkus-junit5 45 | test 46 | 47 | 48 | io.rest-assured 49 | rest-assured 50 | test 51 | 52 | 53 | io.quarkus 54 | quarkus-hibernate-orm-panache 55 | 56 | 57 | io.quarkus 58 | quarkus-jdbc-h2 59 | 60 | 61 | io.quarkus 62 | quarkus-scheduler 63 | 64 | 65 | io.quarkus 66 | quarkus-resteasy-qute 67 | 68 | 69 | io.quarkus 70 | quarkus-jsonp 71 | 72 | 73 | io.quarkus 74 | quarkus-hibernate-validator 75 | 76 | 77 | io.quarkus 78 | quarkus-undertow 79 | 80 | 81 | io.quarkus 82 | quarkus-rest-client 83 | 84 | 85 | io.quarkus 86 | quarkus-hibernate-orm 87 | 88 | 89 | io.quarkus 90 | quarkus-jdbc-postgresql 91 | 92 | 93 | io.quarkus 94 | quarkus-undertow-websockets 95 | 96 | 97 | io.quarkus 98 | quarkus-smallrye-health 99 | 100 | 101 | io.quarkus 102 | quarkus-smallrye-metrics 103 | 104 | 105 | 106 | 107 | org.junit.jupiter 108 | junit-jupiter-api 109 | 5.6.0 110 | test 111 | 112 | 113 | org.junit.jupiter 114 | junit-jupiter-engine 115 | 5.6.0 116 | test 117 | 118 | 119 | org.junit.jupiter 120 | junit-jupiter-params 121 | 5.6.0 122 | test 123 | 124 | 125 | org.junit.vintage 126 | junit-vintage-engine 127 | 5.6.0 128 | test 129 | 130 | 131 | org.assertj 132 | assertj-core 133 | 3.15.0 134 | test 135 | 136 | 137 | org.mockito 138 | mockito-core 139 | 3.2.4 140 | test 141 | 142 | 143 | org.mockito 144 | mockito-junit-jupiter 145 | 3.2.4 146 | test 147 | 148 | 149 | 150 | 151 | io.cucumber 152 | cucumber-java 153 | 4.3.0 154 | test 155 | 156 | 157 | io.cucumber 158 | cucumber-junit 159 | 4.3.0 160 | test 161 | 162 | 163 | 164 | 165 | javax.xml.bind 166 | jaxb-api 167 | 2.3.1 168 | test 169 | 170 | 171 | javax.activation 172 | activation 173 | 1.1 174 | test 175 | 176 | 177 | 178 | 179 | 180 | 181 | io.quarkus 182 | quarkus-maven-plugin 183 | ${quarkus-plugin.version} 184 | 185 | 186 | 187 | build 188 | 189 | 190 | 191 | 192 | true 193 | 194 | 195 | 196 | maven-compiler-plugin 197 | ${compiler-plugin.version} 198 | 199 | 200 | maven-surefire-plugin 201 | ${surefire-plugin.version} 202 | 203 | 204 | org.jboss.logmanager.LogManager 205 | 206 | 207 | 208 | 209 | maven-failsafe-plugin 210 | 3.0.0-M5 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | native 219 | 220 | 221 | native 222 | 223 | 224 | 225 | 226 | 227 | maven-failsafe-plugin 228 | ${surefire-plugin.version} 229 | 230 | 231 | 232 | integration-test 233 | verify 234 | 235 | 236 | 237 | ${project.build.directory}/${project.build.finalName}-runner 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | native 247 | 248 | 249 | 250 | 251 | -------------------------------------------------------------------------------- /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 javax.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 javax.inject.Inject; 6 | import javax.json.JsonObject; 7 | import javax.ws.rs.GET; 8 | import javax.ws.rs.Path; 9 | import javax.ws.rs.core.Context; 10 | import javax.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.entity.CoffeeType; 5 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 6 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin; 7 | import io.quarkus.scheduler.Scheduled; 8 | 9 | import javax.enterprise.context.ApplicationScoped; 10 | import javax.inject.Inject; 11 | import javax.persistence.EntityManager; 12 | import javax.persistence.PersistenceContext; 13 | import javax.transaction.Transactional; 14 | import java.util.EnumSet; 15 | import java.util.List; 16 | import java.util.Set; 17 | import java.util.UUID; 18 | import java.util.stream.Collectors; 19 | 20 | @ApplicationScoped 21 | @Transactional 22 | public class CoffeeShop { 23 | 24 | @PersistenceContext 25 | EntityManager entityManager; 26 | 27 | @Inject 28 | OrderProcessor orderProcessor; 29 | 30 | public Set getCoffeeTypes() { 31 | return EnumSet.of(CoffeeType.ESPRESSO, CoffeeType.LATTE, CoffeeType.POUR_OVER); 32 | } 33 | 34 | public Set getOrigins(final CoffeeType type) { 35 | return entityManager.createNamedQuery(Origin.FIND_ALL, Origin.class) 36 | .getResultList().stream() 37 | .filter(t -> t.getCoffeeTypes().contains(type)) 38 | .collect(Collectors.toSet()); 39 | } 40 | 41 | public Origin getOrigin(String name) { 42 | return entityManager.find(Origin.class, name); 43 | } 44 | 45 | public void createOrder(Order order) { 46 | entityManager.merge(order); 47 | entityManager.flush(); 48 | } 49 | 50 | public Order getOrder(UUID id) { 51 | return entityManager.find(Order.class, id.toString()); 52 | } 53 | 54 | public List getOrders() { 55 | return entityManager.createNamedQuery(Order.FIND_ALL, Order.class) 56 | .getResultList(); 57 | } 58 | 59 | @Scheduled(every = "2s") 60 | public void processUnfinishedOrders() { 61 | entityManager.createNamedQuery(Order.FIND_UNFINISHED, Order.class) 62 | .getResultList() 63 | .forEach(orderProcessor::processOrder); 64 | } 65 | 66 | public void updateOrder(UUID id, Order order) { 67 | Order managedOrder = entityManager.find(Order.class, id.toString()); 68 | managedOrder.setType(order.getType()); 69 | managedOrder.setOrigin(order.getOrigin()); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/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.Template; 5 | import io.quarkus.qute.TemplateInstance; 6 | import io.quarkus.qute.api.ResourcePath; 7 | 8 | import javax.inject.Inject; 9 | import javax.ws.rs.GET; 10 | import javax.ws.rs.Path; 11 | import javax.ws.rs.Produces; 12 | import javax.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 | @ResourcePath("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.Template; 7 | import io.quarkus.qute.TemplateInstance; 8 | import io.quarkus.qute.api.ResourcePath; 9 | 10 | import javax.inject.Inject; 11 | import javax.ws.rs.*; 12 | import javax.ws.rs.core.MediaType; 13 | import javax.ws.rs.core.Response; 14 | import java.net.URI; 15 | import java.util.Set; 16 | import java.util.UUID; 17 | 18 | @Path("order.html") 19 | @Produces(MediaType.TEXT_HTML) 20 | public class OrderCoffeeController { 21 | 22 | @Inject 23 | CoffeeShop coffeeShop; 24 | 25 | @ResourcePath("order.html") 26 | Template orderTemplate; 27 | 28 | @GET 29 | public TemplateInstance index() { 30 | Set types = coffeeShop.getCoffeeTypes(); 31 | return orderTemplate.data("types", types); 32 | } 33 | 34 | @POST 35 | @Consumes(MediaType.APPLICATION_FORM_URLENCODED) 36 | public Response submit(@FormParam("type") @DefaultValue("") String type, @FormParam("origin") @DefaultValue("") String originName) { 37 | CoffeeType coffeeType = CoffeeType.fromString(type); 38 | Origin origin = new Origin(originName); 39 | Order order = new Order(UUID.randomUUID(), coffeeType, origin); 40 | 41 | if (!orderIsValid(order)) { 42 | Set types = coffeeShop.getCoffeeTypes(); 43 | return Response.ok(orderTemplate 44 | .data("failed", true) 45 | .data("types", types)) 46 | .build(); 47 | } 48 | 49 | coffeeShop.createOrder(order); 50 | 51 | return Response.seeOther(URI.create("/index.html")).build(); 52 | } 53 | 54 | private boolean orderIsValid(Order order) { 55 | if (order.getType() == null || order.getOrigin() == null) 56 | return false; 57 | Origin origin = coffeeShop.getOrigin(order.getOrigin().getName()); 58 | return origin != null; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/boundary/OrdersResource.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.boundary; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.control.EntityBuilder; 4 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 5 | import com.sebastian_daschner.coffee_shop.orders.entity.ValidOrder; 6 | 7 | import javax.inject.Inject; 8 | import javax.json.JsonArray; 9 | import javax.json.JsonObject; 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.validation.Valid; 12 | import javax.ws.rs.*; 13 | import javax.ws.rs.core.Context; 14 | import javax.ws.rs.core.MediaType; 15 | import javax.ws.rs.core.Response; 16 | import javax.ws.rs.core.UriInfo; 17 | import java.net.URI; 18 | import java.util.List; 19 | import java.util.UUID; 20 | 21 | @Path("orders") 22 | @Produces(MediaType.APPLICATION_JSON) 23 | @Consumes(MediaType.APPLICATION_JSON) 24 | public class OrdersResource { 25 | 26 | @Inject 27 | CoffeeShop coffeeShop; 28 | 29 | @Inject 30 | EntityBuilder entityBuilder; 31 | 32 | @Context 33 | UriInfo uriInfo; 34 | 35 | @Context 36 | HttpServletRequest request; 37 | 38 | @GET 39 | public JsonArray getOrders() { 40 | List orders = coffeeShop.getOrders(); 41 | return entityBuilder.buildOrders(orders, uriInfo, request); 42 | } 43 | 44 | @PUT 45 | @Path("{id}") 46 | public void updateOrder(@PathParam("id") UUID id, JsonObject jsonObject) { 47 | Order order = entityBuilder.buildOrder(jsonObject); 48 | coffeeShop.updateOrder(id, order); 49 | } 50 | 51 | @GET 52 | @Path("{id}") 53 | public JsonObject getOrder(@PathParam("id") UUID id) { 54 | final Order order = coffeeShop.getOrder(id); 55 | 56 | if (order == null) 57 | throw new NotFoundException(); 58 | 59 | return entityBuilder.buildOrder(order); 60 | } 61 | 62 | @POST 63 | public Response createOrder(@Valid @ValidOrder JsonObject json) { 64 | final Order order = entityBuilder.buildOrder(json); 65 | 66 | coffeeShop.createOrder(order); 67 | 68 | return Response.created(buildUri(order)).build(); 69 | } 70 | 71 | private URI buildUri(Order order) { 72 | return uriInfo.getBaseUriBuilder() 73 | .host(request.getServerName()) 74 | .port(request.getServerPort()) 75 | .path(OrdersResource.class) 76 | .path(OrdersResource.class, "getOrder") 77 | .build(order.getId()); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/boundary/OriginsResource.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.boundary; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.control.EntityBuilder; 4 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType; 5 | import io.quarkus.arc.Unremovable; 6 | 7 | import javax.enterprise.context.RequestScoped; 8 | import javax.inject.Inject; 9 | import javax.json.Json; 10 | import javax.json.JsonArray; 11 | import javax.json.JsonArrayBuilder; 12 | import javax.ws.rs.GET; 13 | import javax.ws.rs.Path; 14 | import javax.ws.rs.PathParam; 15 | import javax.ws.rs.Produces; 16 | import javax.ws.rs.core.Context; 17 | import javax.ws.rs.core.MediaType; 18 | import javax.ws.rs.core.UriInfo; 19 | 20 | @Produces(MediaType.APPLICATION_JSON) 21 | @RequestScoped 22 | @Unremovable // https://github.com/quarkusio/quarkus/issues/5314 23 | public class OriginsResource { 24 | 25 | @Inject 26 | CoffeeShop coffeeShop; 27 | 28 | @PathParam("type") 29 | CoffeeType type; 30 | 31 | @Context 32 | UriInfo uriInfo; 33 | 34 | @Inject 35 | EntityBuilder entityBuilder; 36 | 37 | @GET 38 | public JsonArray getOrigins() { 39 | return coffeeShop.getOrigins(type).stream() 40 | .map(o -> entityBuilder.buildOrigin(uriInfo, o, type)) 41 | .collect(Json::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::add).build(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/boundary/TypesResource.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.boundary; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.control.EntityBuilder; 4 | 5 | import javax.enterprise.context.RequestScoped; 6 | import javax.inject.Inject; 7 | import javax.json.Json; 8 | import javax.json.JsonArray; 9 | import javax.json.JsonArrayBuilder; 10 | import javax.ws.rs.GET; 11 | import javax.ws.rs.Path; 12 | import javax.ws.rs.Produces; 13 | import javax.ws.rs.container.ResourceContext; 14 | import javax.ws.rs.core.Context; 15 | import javax.ws.rs.core.MediaType; 16 | import javax.ws.rs.core.UriInfo; 17 | 18 | @Path("types") 19 | @Produces(MediaType.APPLICATION_JSON) 20 | @RequestScoped 21 | public class TypesResource { 22 | 23 | @Inject 24 | CoffeeShop coffeeShop; 25 | 26 | @Inject 27 | EntityBuilder entityBuilder; 28 | 29 | @Context 30 | ResourceContext resourceContext; 31 | 32 | @Context 33 | UriInfo uriInfo; 34 | 35 | @GET 36 | public JsonArray getCoffeeTypes() { 37 | return coffeeShop.getCoffeeTypes().stream() 38 | .map(t -> entityBuilder.buildType(t, uriInfo)) 39 | .collect(Json::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::add).build(); 40 | } 41 | 42 | @Path("{type}/origins") 43 | public OriginsResource originsResource() { 44 | return resourceContext.getResource(OriginsResource.class); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/control/Barista.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.control; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 4 | import com.sebastian_daschner.coffee_shop.orders.entity.OrderStatus; 5 | import org.eclipse.microprofile.config.inject.ConfigProperty; 6 | 7 | import javax.annotation.PostConstruct; 8 | import javax.enterprise.context.ApplicationScoped; 9 | import javax.inject.Inject; 10 | import javax.json.Json; 11 | import javax.json.JsonObject; 12 | import javax.ws.rs.client.Client; 13 | import javax.ws.rs.client.ClientBuilder; 14 | import javax.ws.rs.client.Entity; 15 | import javax.ws.rs.client.WebTarget; 16 | import javax.ws.rs.core.Response; 17 | 18 | @ApplicationScoped 19 | public class Barista { 20 | 21 | WebTarget target; 22 | 23 | @Inject 24 | @ConfigProperty(name = "barista.url") 25 | String baristaUrl; 26 | 27 | @PostConstruct 28 | void initClient() { 29 | final Client client = ClientBuilder.newClient(); 30 | target = client.target(baristaUrl); 31 | } 32 | 33 | public OrderStatus retrieveOrderStatus(Order order) { 34 | JsonObject requestJson = buildRequestJson(order); 35 | 36 | JsonObject responseJson = sendRequest(requestJson); 37 | 38 | return readStatus(responseJson); 39 | } 40 | 41 | private JsonObject buildRequestJson(Order order) { 42 | return Json.createObjectBuilder() 43 | .add("order", order.getId()) 44 | .add("type", order.getType().name().toUpperCase()) 45 | .add("origin", order.getOrigin().getName().toUpperCase()) 46 | .add("status", order.getStatus().name().toUpperCase()) 47 | .build(); 48 | } 49 | 50 | private JsonObject sendRequest(final JsonObject requestBody) { 51 | Response response = target.request().buildPost(Entity.json(requestBody)).invoke(); 52 | 53 | if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) { 54 | throw new RuntimeException("Could not successfully process order, response from " + target.getUri() + " was " + response.getStatus()); 55 | } 56 | 57 | return response.readEntity(JsonObject.class); 58 | } 59 | 60 | private OrderStatus readStatus(final JsonObject responseJson) { 61 | final OrderStatus status = OrderStatus.fromString(responseJson.getString("status", null)); 62 | 63 | if (status == null) 64 | throw new RuntimeException("Could not read known status from response" + responseJson); 65 | 66 | return status; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/control/EntityBuilder.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.control; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.boundary.OrdersResource; 4 | import com.sebastian_daschner.coffee_shop.orders.boundary.TypesResource; 5 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType; 6 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 7 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin; 8 | 9 | import javax.enterprise.context.ApplicationScoped; 10 | import javax.json.*; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.ws.rs.core.UriInfo; 13 | import java.net.URI; 14 | import java.util.List; 15 | import java.util.UUID; 16 | 17 | import static com.sebastian_daschner.coffee_shop.orders.control.StringExtensions.capitalize; 18 | 19 | @ApplicationScoped 20 | public class EntityBuilder { 21 | 22 | public JsonArray buildOrders(List orders, UriInfo uriInfo, HttpServletRequest request) { 23 | return orders.stream() 24 | .map(o -> buildOrderTeaser(o, uriInfo, request)) 25 | .collect(Json::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::add) 26 | .build(); 27 | } 28 | 29 | private JsonObject buildOrderTeaser(Order order, UriInfo uriInfo, HttpServletRequest request) { 30 | return Json.createObjectBuilder() 31 | .add("_self", uriInfo.getBaseUriBuilder() 32 | .host(request.getServerName()) 33 | .port(request.getServerPort()) 34 | .path(OrdersResource.class) 35 | .path(OrdersResource.class, "getOrder") 36 | .build(order.getId()) 37 | .toString()) 38 | .add("origin", order.getOrigin().getName()) 39 | .add("status", capitalize(order.getStatus().name())) 40 | .build(); 41 | } 42 | 43 | public JsonObject buildOrder(Order order) { 44 | return Json.createObjectBuilder() 45 | .add("type", capitalize(order.getType().name())) 46 | .add("origin", order.getOrigin().getName()) 47 | .add("status", capitalize(order.getStatus().name())) 48 | .build(); 49 | } 50 | 51 | public Order buildOrder(JsonObject json) { 52 | final CoffeeType type = CoffeeType.fromString(json.getString("type")); 53 | final Origin origin = new Origin(json.getString("origin")); 54 | 55 | return new Order(UUID.randomUUID(), type, origin); 56 | } 57 | 58 | public JsonObject buildIndex(UriInfo uriInfo) { 59 | final URI typesUri = uriInfo.getBaseUriBuilder().path(TypesResource.class).build(); 60 | final URI ordersUri = uriInfo.getBaseUriBuilder().path(OrdersResource.class).build(); 61 | return Json.createObjectBuilder() 62 | .add("_links", Json.createObjectBuilder() 63 | .add("types", typesUri.toString())) 64 | .add("_actions", Json.createObjectBuilder() 65 | .add("order-coffee", Json.createObjectBuilder() 66 | .add("method", "POST") 67 | .add("href", ordersUri.toString()) 68 | .add("fields", Json.createArrayBuilder() 69 | .add(Json.createObjectBuilder() 70 | .add("name", "type") 71 | .add("type", "text")) 72 | .add(Json.createObjectBuilder() 73 | .add("name", "origin") 74 | .add("type", "text")) 75 | ))) 76 | .build(); 77 | } 78 | 79 | public JsonObject buildOrigin(UriInfo uriInfo, Origin origin, CoffeeType type) { 80 | final URI ordersUri = uriInfo.getBaseUriBuilder().path(OrdersResource.class).build(); 81 | return Json.createObjectBuilder() 82 | .add("origin", origin.getName()) 83 | .add("_actions", Json.createObjectBuilder() 84 | .add("order-coffee", Json.createObjectBuilder() 85 | .add("method", "POST") 86 | .add("href", ordersUri.toString()) 87 | .add("fields", Json.createArrayBuilder() 88 | .add(Json.createObjectBuilder() 89 | .add("name", "type") 90 | .add("value", capitalize(type.name()))) 91 | .add(Json.createObjectBuilder() 92 | .add("name", "origin") 93 | .add("type", origin.getName())) 94 | ))) 95 | .build(); 96 | } 97 | 98 | public JsonObjectBuilder buildType(CoffeeType type, UriInfo uriInfo) { 99 | return Json.createObjectBuilder() 100 | .add("type", capitalize(type.name())) 101 | .add("_links", Json.createObjectBuilder() 102 | .add("origins", uriInfo.getBaseUriBuilder() 103 | .path(TypesResource.class) 104 | .path(TypesResource.class, "originsResource") 105 | .build(type).toString().toLowerCase())); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /coffee-shop/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 javax.enterprise.context.ApplicationScoped; 7 | import javax.inject.Inject; 8 | import javax.persistence.EntityManager; 9 | import javax.persistence.PersistenceContext; 10 | import javax.transaction.Transactional; 11 | 12 | @ApplicationScoped 13 | public class OrderProcessor { 14 | 15 | @PersistenceContext 16 | EntityManager entityManager; 17 | 18 | @Inject 19 | Barista barista; 20 | 21 | @Transactional(Transactional.TxType.REQUIRES_NEW) 22 | public void processOrder(Order order) { 23 | OrderStatus status = barista.retrieveOrderStatus(order); 24 | order.setStatus(status); 25 | entityManager.merge(order); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/control/OrderValidator.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.control; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.boundary.CoffeeShop; 4 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType; 5 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin; 6 | import com.sebastian_daschner.coffee_shop.orders.entity.ValidOrder; 7 | 8 | import javax.enterprise.context.ApplicationScoped; 9 | import javax.inject.Inject; 10 | import javax.json.JsonObject; 11 | import javax.validation.ConstraintValidator; 12 | import javax.validation.ConstraintValidatorContext; 13 | 14 | @ApplicationScoped 15 | public class OrderValidator implements ConstraintValidator { 16 | 17 | @Inject 18 | CoffeeShop coffeeShop; 19 | 20 | public void initialize(ValidOrder constraint) { 21 | // nothing to do 22 | } 23 | 24 | public boolean isValid(JsonObject json, ConstraintValidatorContext context) { 25 | 26 | final String type = json.getString("type", null); 27 | final String origin = json.getString("origin", null); 28 | 29 | if (type == null || origin == null) 30 | return false; 31 | 32 | final CoffeeType coffeeType = coffeeShop.getCoffeeTypes().stream() 33 | .filter(t -> t.name().equalsIgnoreCase(type)) 34 | .findAny().orElse(null); 35 | 36 | final Origin coffeeOrigin = coffeeShop.getOrigin(origin); 37 | 38 | if (coffeeOrigin == null || coffeeType == null) 39 | return false; 40 | 41 | return coffeeOrigin.getCoffeeTypes().contains(coffeeType); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /coffee-shop/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 javax.persistence.*; 4 | import java.util.Objects; 5 | import java.util.UUID; 6 | 7 | import static com.sebastian_daschner.coffee_shop.orders.entity.Order.FIND_ALL; 8 | import static com.sebastian_daschner.coffee_shop.orders.entity.Order.FIND_UNFINISHED; 9 | 10 | @Entity 11 | @Table(name = "orders") 12 | @NamedQueries({ 13 | @NamedQuery(name = FIND_UNFINISHED, query = "select o from Order o where o.status <> " + 14 | "com.sebastian_daschner.coffee_shop.orders.entity.OrderStatus.COLLECTED"), 15 | @NamedQuery(name = FIND_ALL, query = "select o from Order o")}) 16 | public class Order { 17 | 18 | public static final String FIND_UNFINISHED = "Order.findUnfinished"; 19 | public static final String FIND_ALL = "Order.findAll"; 20 | 21 | @Id 22 | private String id; 23 | 24 | @Basic(optional = false) 25 | @Enumerated(EnumType.STRING) 26 | private CoffeeType type; 27 | 28 | @ManyToOne(optional = false) 29 | private Origin origin; 30 | 31 | @Basic(optional = false) 32 | @Enumerated(EnumType.STRING) 33 | private OrderStatus status = OrderStatus.PREPARING; 34 | 35 | public Order() { 36 | } 37 | 38 | public Order(UUID id, CoffeeType type, Origin origin) { 39 | Objects.requireNonNull(id); 40 | Objects.requireNonNull(type); 41 | Objects.requireNonNull(origin); 42 | this.id = id.toString(); 43 | this.type = type; 44 | this.origin = origin; 45 | } 46 | 47 | public String getId() { 48 | return id; 49 | } 50 | 51 | public void setId(String id) { 52 | this.id = id; 53 | } 54 | 55 | public CoffeeType getType() { 56 | return type; 57 | } 58 | 59 | public void setType(CoffeeType type) { 60 | this.type = type; 61 | } 62 | 63 | public Origin getOrigin() { 64 | return origin; 65 | } 66 | 67 | public void setOrigin(Origin origin) { 68 | this.origin = origin; 69 | } 70 | 71 | public OrderStatus getStatus() { 72 | return status; 73 | } 74 | 75 | public void setStatus(OrderStatus status) { 76 | this.status = status; 77 | } 78 | 79 | @Override 80 | public String toString() { 81 | return "Order{" + 82 | "id='" + id + '\'' + 83 | ", type='" + type + '\'' + 84 | ", origin='" + origin + '\'' + 85 | ", status=" + status + 86 | '}'; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/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 javax.persistence.*; 4 | import java.util.EnumSet; 5 | import java.util.Set; 6 | 7 | import static com.sebastian_daschner.coffee_shop.orders.entity.Origin.FIND_ALL; 8 | 9 | @Entity 10 | @Table(name = "origins") 11 | @NamedQuery(name = FIND_ALL, query = "select o from Origin o") 12 | public class Origin { 13 | 14 | public static final String FIND_ALL = "Origin.findAll"; 15 | 16 | @Id 17 | private String name; 18 | 19 | @ElementCollection(fetch = FetchType.EAGER) 20 | @CollectionTable(name = "origin_coffee_types", joinColumns = @JoinColumn(name = "origin_name", nullable = false)) 21 | @Column(name = "coffee_type", nullable = false) 22 | @Enumerated(value = EnumType.STRING) 23 | private Set coffeeTypes = EnumSet.noneOf(CoffeeType.class); 24 | 25 | public Origin() { 26 | } 27 | 28 | public Origin(final String name) { 29 | this.name = name; 30 | } 31 | 32 | public String getName() { 33 | return name; 34 | } 35 | 36 | public Set getCoffeeTypes() { 37 | return coffeeTypes; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return "Origin{" + 43 | "name='" + name + '\'' + 44 | ", coffeeTypes=" + coffeeTypes + 45 | '}'; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/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 javax.validation.Constraint; 6 | import javax.validation.Payload; 7 | import javax.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.db-kind=postgresql 2 | quarkus.datasource.jdbc.url=jdbc:postgresql://coffee-shop-db:5432/postgres 3 | #%dev.quarkus.datasource.jdbc.url=jdbc:postgresql://coffee-shop-db:5432/postgres 4 | quarkus.datasource.username=postgres 5 | quarkus.datasource.password=postgres 6 | quarkus.hibernate-orm.database.generation=drop-and-create 7 | quarkus.hibernate-orm.sql-load-script=load-data.sql 8 | 9 | barista.url=http://barista:8080/processes 10 | #%dev.barista.url=http://barista:8002/processes 11 | -------------------------------------------------------------------------------- /coffee-shop/src/main/resources/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/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 javax.json.JsonArray; 4 | import javax.json.JsonObject; 5 | import javax.ws.rs.client.Client; 6 | import javax.ws.rs.client.ClientBuilder; 7 | import javax.ws.rs.client.WebTarget; 8 | import javax.ws.rs.core.MediaType; 9 | import javax.ws.rs.core.UriBuilder; 10 | import java.net.URI; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | 14 | class CoffeeOrderSystem { 15 | 16 | private final Client client; 17 | private final WebTarget baseTarget; 18 | 19 | CoffeeOrderSystem() { 20 | client = ClientBuilder.newClient(); 21 | baseTarget = client.target(buildBaseUri()); 22 | } 23 | 24 | private URI buildBaseUri() { 25 | String host = System.getProperty("coffee-shop.test.host", "localhost"); 26 | String port = System.getProperty("coffee-shop.test.port", "8001"); 27 | return UriBuilder.fromUri("http://{host}:{port}/").build(host, port); 28 | } 29 | 30 | boolean isSystemUp() { 31 | JsonObject healthJson = retrieveHealthStatus(); 32 | 33 | String status = healthJson.getString("status"); 34 | if (!"UP".equalsIgnoreCase(status)) 35 | return false; 36 | 37 | return "UP".equalsIgnoreCase(extractStatus(healthJson, "coffee-shop")); 38 | } 39 | 40 | private JsonObject retrieveHealthStatus() { 41 | return baseTarget.path("health") 42 | .request(MediaType.APPLICATION_JSON_TYPE) 43 | .get(JsonObject.class); 44 | } 45 | 46 | private String extractStatus(JsonObject healthJson, String name) { 47 | return healthJson.getJsonArray("checks") 48 | .getValuesAs(JsonObject.class) 49 | .stream() 50 | .filter(o -> o.getString("name").equalsIgnoreCase(name)) 51 | .map(o -> o.getString("status")) 52 | .findAny().orElse(null); 53 | } 54 | 55 | List getTypes() { 56 | return baseTarget.path("types") 57 | .request(MediaType.APPLICATION_JSON_TYPE) 58 | .get(JsonArray.class).getValuesAs(JsonObject.class).stream() 59 | .map(o -> o.getString("type")) 60 | .collect(Collectors.toList()); 61 | } 62 | 63 | List getOrigins(String type) { 64 | URI typeOriginsUri = retrieveTypeOriginsLink(type); 65 | 66 | return retrieveOriginsForType(typeOriginsUri); 67 | } 68 | 69 | private URI retrieveTypeOriginsLink(String type) { 70 | return baseTarget.path("types") 71 | .request(MediaType.APPLICATION_JSON_TYPE) 72 | .get(JsonArray.class).getValuesAs(JsonObject.class).stream() 73 | .filter(o -> o.getString("type").equalsIgnoreCase(type)) 74 | .map(o -> o.getJsonObject("_links").getString("origins")) 75 | .map(URI::create) 76 | .findAny().orElseThrow(() -> new IllegalStateException("Could not get link to origins for " + type)); 77 | } 78 | 79 | private List retrieveOriginsForType(URI typeOriginsUri) { 80 | return client.target(typeOriginsUri) 81 | .request(MediaType.APPLICATION_JSON_TYPE) 82 | .get(JsonArray.class).getValuesAs(JsonObject.class).stream() 83 | .map(o -> o.getString("origin")) 84 | .collect(Collectors.toList()); 85 | } 86 | 87 | void close() { 88 | client.close(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/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/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.entity.Order; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.ArgumentCaptor; 9 | import org.mockito.Captor; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | 14 | import javax.persistence.EntityManager; 15 | import javax.persistence.TypedQuery; 16 | import java.util.List; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.mockito.Mockito.*; 20 | 21 | @ExtendWith(MockitoExtension.class) 22 | class CoffeeShopMockitoTest { 23 | 24 | @InjectMocks 25 | CoffeeShop testObject; 26 | 27 | @Captor 28 | ArgumentCaptor orderCaptor; 29 | 30 | private EntityManager entityManager; 31 | private OrderProcessor orderProcessor; 32 | 33 | CoffeeShopMockitoTest(@Mock EntityManager entityManager, @Mock OrderProcessor orderProcessor) { 34 | this.entityManager = entityManager; 35 | this.orderProcessor = orderProcessor; 36 | } 37 | 38 | @Test 39 | void testProcessUnfinishedOrders(@Mock TypedQuery mockQuery) { 40 | when(entityManager.createNamedQuery(Order.FIND_UNFINISHED, Order.class)).thenReturn(mockQuery); 41 | List desiredOrders = TestData.unfinishedOrders(); 42 | when(mockQuery.getResultList()).thenReturn(desiredOrders); 43 | 44 | testObject.processUnfinishedOrders(); 45 | 46 | verify(entityManager).createNamedQuery(Order.FIND_UNFINISHED, Order.class); 47 | verify(orderProcessor, times(desiredOrders.size())).processOrder(orderCaptor.capture()); 48 | 49 | assertThat(orderCaptor.getAllValues()).containsExactlyElementsOf(desiredOrders); 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopNaiveUseCaseTest.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.boundary; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.control.Barista; 4 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessor; 5 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType; 6 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 7 | import com.sebastian_daschner.coffee_shop.orders.entity.OrderStatus; 8 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.mockito.ArgumentCaptor; 12 | 13 | import javax.persistence.EntityManager; 14 | import javax.persistence.TypedQuery; 15 | import java.lang.reflect.Field; 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import java.util.UUID; 19 | 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | import static org.mockito.ArgumentMatchers.anyString; 22 | import static org.mockito.ArgumentMatchers.eq; 23 | import static org.mockito.Mockito.*; 24 | 25 | class CoffeeShopNaiveUseCaseTest { 26 | 27 | private CoffeeShop coffeeShop; 28 | private EntityManager entityManager; 29 | private OrderProcessor orderProcessor; 30 | private Barista barista; 31 | private ArgumentCaptor orderCaptor; 32 | 33 | @BeforeEach 34 | void setUp() { 35 | coffeeShop = new CoffeeShop(); 36 | orderProcessor = new OrderProcessor(); 37 | 38 | coffeeShop.orderProcessor = orderProcessor; 39 | entityManager = mock(EntityManager.class); 40 | coffeeShop.entityManager = entityManager; 41 | 42 | barista = mock(Barista.class); 43 | setReflectiveField(orderProcessor, "entityManager", entityManager); 44 | setReflectiveField(orderProcessor, "barista", barista); 45 | orderCaptor = ArgumentCaptor.forClass(Order.class); 46 | 47 | when(barista.retrieveOrderStatus(orderCaptor.capture())).thenReturn(OrderStatus.PREPARING); 48 | } 49 | 50 | @Test 51 | void testCreateOrder() { 52 | Order order = new Order(); 53 | coffeeShop.createOrder(order); 54 | verify(entityManager).merge(order); 55 | } 56 | 57 | @Test 58 | @SuppressWarnings("unchecked") 59 | void testProcessUnfinishedOrders() { 60 | List orders = Arrays.asList(new Order(UUID.randomUUID(), CoffeeType.ESPRESSO, new Origin("Colombia")), 61 | new Order(UUID.randomUUID(), CoffeeType.ESPRESSO, new Origin("Ethiopia"))); 62 | 63 | // answer for unfinished orders 64 | TypedQuery queryMock = mock(TypedQuery.class); 65 | when(entityManager.createNamedQuery(anyString(), eq(Order.class))).thenReturn(queryMock); 66 | when(queryMock.getResultList()).thenReturn(orders); 67 | 68 | coffeeShop.processUnfinishedOrders(); 69 | 70 | verify(entityManager).createNamedQuery(Order.FIND_UNFINISHED, Order.class); 71 | 72 | verify(barista, times(orders.size())).retrieveOrderStatus(any()); 73 | assertThat(orderCaptor.getAllValues()).containsExactlyElementsOf(orders); 74 | } 75 | 76 | private static void setReflectiveField(Object object, String fieldName, Object value) { 77 | try { 78 | Field f1 = object.getClass().getDeclaredField(fieldName); 79 | f1.setAccessible(true); 80 | f1.set(object, value); 81 | } catch (ReflectiveOperationException e) { 82 | throw new RuntimeException(e); 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopTest.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.boundary; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.TestData; 4 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessor; 5 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.mockito.ArgumentCaptor; 9 | 10 | import javax.persistence.EntityManager; 11 | import javax.persistence.TypedQuery; 12 | import java.util.List; 13 | 14 | import static com.sebastian_daschner.coffee_shop.orders.entity.OrderAssert.assertThat; 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.mockito.Mockito.*; 17 | 18 | class CoffeeShopTest { 19 | 20 | private CoffeeShop testObject; 21 | 22 | @BeforeEach 23 | void setUp() { 24 | testObject = new CoffeeShop(); 25 | testObject.entityManager = mock(EntityManager.class); 26 | testObject.orderProcessor = mock(OrderProcessor.class); 27 | } 28 | 29 | @Test 30 | @SuppressWarnings("unchecked") 31 | void testProcessUnfinishedOrders() { 32 | List desiredOrders = TestData.unfinishedOrders(); 33 | 34 | TypedQuery mockQuery = mock(TypedQuery.class); 35 | when(testObject.entityManager.createNamedQuery(Order.FIND_UNFINISHED, Order.class)).thenReturn(mockQuery); 36 | when(mockQuery.getResultList()).thenReturn(desiredOrders); 37 | ArgumentCaptor orderCaptor = ArgumentCaptor.forClass(Order.class); 38 | 39 | testObject.processUnfinishedOrders(); 40 | 41 | verify(testObject.entityManager).createNamedQuery(Order.FIND_UNFINISHED, Order.class); 42 | verify(testObject.orderProcessor, times(desiredOrders.size())).processOrder(orderCaptor.capture()); 43 | 44 | assertThat(orderCaptor.getAllValues()).containsExactlyElementsOf(desiredOrders); 45 | 46 | orderCaptor.getAllValues().forEach(o -> assertThat(o).isPreparing()); 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopTestDouble.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.boundary; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessorTestDouble; 4 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 5 | 6 | import javax.persistence.EntityManager; 7 | import javax.persistence.TypedQuery; 8 | import java.util.List; 9 | 10 | import static org.mockito.ArgumentMatchers.anyString; 11 | import static org.mockito.Mockito.*; 12 | 13 | public class CoffeeShopTestDouble extends CoffeeShop { 14 | 15 | public CoffeeShopTestDouble(OrderProcessorTestDouble orderProcessorTestDouble) { 16 | entityManager = mock(EntityManager.class); 17 | orderProcessor = orderProcessorTestDouble; 18 | } 19 | 20 | public void verifyCreateOrder(Order order) { 21 | verify(entityManager).merge(order); 22 | } 23 | 24 | public void verifyProcessUnfinishedOrders() { 25 | verify(entityManager).createNamedQuery(Order.FIND_UNFINISHED, Order.class); 26 | } 27 | 28 | public void answerForUnfinishedOrders(List orders) { 29 | @SuppressWarnings("unchecked") 30 | TypedQuery queryMock = mock(TypedQuery.class); 31 | when(entityManager.createNamedQuery(anyString(), eq(Order.class))).thenReturn(queryMock); 32 | when(queryMock.getResultList()).thenReturn(orders); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopUseCaseTest.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.boundary; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessorTestDouble; 4 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType; 5 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 6 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import java.util.UUID; 13 | 14 | class CoffeeShopUseCaseTest { 15 | 16 | private CoffeeShopTestDouble coffeeShop; 17 | private OrderProcessorTestDouble orderProcessor; 18 | 19 | @BeforeEach 20 | void setUp() { 21 | orderProcessor = new OrderProcessorTestDouble(); 22 | coffeeShop = new CoffeeShopTestDouble(orderProcessor); 23 | } 24 | 25 | @Test 26 | void testCreateOrder() { 27 | Order order = new Order(); 28 | coffeeShop.createOrder(order); 29 | coffeeShop.verifyCreateOrder(order); 30 | } 31 | 32 | @Test 33 | void testProcessUnfinishedOrders() { 34 | List orders = Arrays.asList(new Order(UUID.randomUUID(), CoffeeType.ESPRESSO, new Origin("Colombia")), 35 | new Order(UUID.randomUUID(), CoffeeType.ESPRESSO, new Origin("Ethiopia"))); 36 | coffeeShop.answerForUnfinishedOrders(orders); 37 | 38 | coffeeShop.processUnfinishedOrders(); 39 | 40 | coffeeShop.verifyProcessUnfinishedOrders(); 41 | orderProcessor.verifyProcessOrders(orders); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/control/OrderProcessorTestDouble.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.control; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.entity.Order; 4 | import com.sebastian_daschner.coffee_shop.orders.entity.OrderStatus; 5 | import org.assertj.core.api.Assertions; 6 | import org.mockito.ArgumentCaptor; 7 | import org.mockito.ArgumentMatcher; 8 | import org.mockito.Mockito; 9 | 10 | import javax.persistence.EntityManager; 11 | import java.util.List; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.mockito.ArgumentMatchers.any; 15 | import static org.mockito.Mockito.*; 16 | 17 | public class OrderProcessorTestDouble extends OrderProcessor { 18 | 19 | private final ArgumentCaptor orderCaptor; 20 | 21 | public OrderProcessorTestDouble() { 22 | entityManager = mock(EntityManager.class); 23 | barista = mock(Barista.class); 24 | orderCaptor = ArgumentCaptor.forClass(Order.class); 25 | 26 | when(barista.retrieveOrderStatus(orderCaptor.capture())).thenReturn(OrderStatus.PREPARING); 27 | } 28 | 29 | public void verifyProcessOrders(List orders) { 30 | verify(barista, times(orders.size())).retrieveOrderStatus(any()); 31 | assertThat(orderCaptor.getAllValues()).containsExactlyElementsOf(orders); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/control/OrderValidatorTest.java: -------------------------------------------------------------------------------- 1 | package com.sebastian_daschner.coffee_shop.orders.control; 2 | 3 | import com.sebastian_daschner.coffee_shop.orders.boundary.CoffeeShop; 4 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType; 5 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.params.ParameterizedTest; 8 | import org.junit.jupiter.params.provider.MethodSource; 9 | 10 | import javax.json.Json; 11 | import javax.json.JsonObject; 12 | import javax.validation.ConstraintValidatorContext; 13 | import java.io.StringReader; 14 | import java.util.Collection; 15 | import java.util.EnumSet; 16 | import java.util.List; 17 | import java.util.Set; 18 | 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.mockito.Mockito.mock; 21 | import static org.mockito.Mockito.when; 22 | 23 | class OrderValidatorTest { 24 | 25 | private OrderValidator testObject; 26 | private ConstraintValidatorContext context; 27 | 28 | @BeforeEach 29 | void setUp() { 30 | testObject = new OrderValidator(); 31 | testObject.coffeeShop = mock(CoffeeShop.class); 32 | context = mock(ConstraintValidatorContext.class); 33 | 34 | Set coffeeTypes = EnumSet.allOf(CoffeeType.class); 35 | Origin colombia = new Origin("Colombia"); 36 | colombia.getCoffeeTypes().addAll(coffeeTypes); 37 | 38 | when(testObject.coffeeShop.getCoffeeTypes()).thenReturn(coffeeTypes); 39 | when(testObject.coffeeShop.getOrigin("Colombia")).thenReturn(colombia); 40 | } 41 | 42 | @ParameterizedTest 43 | @MethodSource("validData") 44 | void testIsValid(String json) { 45 | JsonObject jsonObject = Json.createReader(new StringReader(json)).readObject(); 46 | 47 | assertThat(testObject.isValid(jsonObject, context)).isTrue(); 48 | } 49 | 50 | private static Collection validData() { 51 | return List.of( 52 | "{\"type\":\"ESPRESSO\",\"origin\":\"Colombia\"}", 53 | "{\"type\":\"Espresso\",\"origin\":\"Colombia\"}", 54 | "{\"type\":\"LATTE\",\"origin\":\"Colombia\"}", 55 | "{\"type\":\"Latte\",\"origin\":\"Colombia\"}", 56 | "{\"type\":\"POUR_OVER\",\"origin\":\"Colombia\"}", 57 | "{\"type\":\"Pour_over\",\"origin\":\"Colombia\"}"); 58 | } 59 | 60 | @ParameterizedTest 61 | @MethodSource("invalidData") 62 | void testIsInvalid(String json) { 63 | JsonObject jsonObject = Json.createReader(new StringReader(json)).readObject(); 64 | 65 | assertThat(testObject.isValid(jsonObject, context)).isFalse(); 66 | } 67 | 68 | private static Collection invalidData() { 69 | return List.of( 70 | "{\"type\":\"SIPHON\",\"origin\":\"Colombia\"}", 71 | "{\"type\":null,\"origin\":\"Colombia\"}", 72 | "{\"origin\":\"Colombia\"}", 73 | "{\"type\":\"ESPRESSO\",\"origin\":\"Ethiopia\"}"); 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /coffee-shop/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 javax.json.Json; 12 | import javax.json.JsonObject; 13 | import javax.validation.ConstraintValidatorContext; 14 | import java.util.List; 15 | import java.util.Set; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | import static org.mockito.ArgumentMatchers.anyString; 19 | import static org.mockito.Mockito.mock; 20 | import static org.mockito.Mockito.when; 21 | 22 | public class StepDefs { 23 | 24 | private OrderValidator testObject; 25 | private ConstraintValidatorContext context; 26 | private JsonObject jsonObject; 27 | 28 | @Before 29 | public void setUp() { 30 | testObject = new OrderValidator(); 31 | testObject.coffeeShop = mock(CoffeeShop.class); 32 | context = mock(ConstraintValidatorContext.class); 33 | 34 | List origins = TestData.validOrigins(); 35 | Set coffeeTypes = TestData.validCoffeeTypes(); 36 | 37 | when(testObject.coffeeShop.getCoffeeTypes()).thenReturn(coffeeTypes); 38 | when(testObject.coffeeShop.getOrigin(anyString())).then(invocation -> origins.stream() 39 | .filter(o -> o.getName().equals(invocation.getArgument(0))) 40 | .findAny() 41 | .orElse(null)); 42 | } 43 | 44 | @When("^I create an order with ([^ ]*) from ([^ ]*)$") 45 | public void i_create_an_order(String type, String origin) { 46 | jsonObject = Json.createObjectBuilder() 47 | .add("type", type) 48 | .add("origin", origin) 49 | .build(); 50 | } 51 | 52 | @Then("^The order should be accepted$") 53 | public void accepted_order() { 54 | assertThat(testObject.isValid(jsonObject, context)).isTrue(); 55 | } 56 | 57 | @Then("^The order should be rejected$") 58 | public void rejected_order() { 59 | assertThat(testObject.isValid(jsonObject, context)).isFalse(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /coffee-shop/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 task) { 8 | super(task, 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 | -------------------------------------------------------------------------------- /nexus_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | nexus 6 | Nexus Public Mirror 7 | https://nexus-nexus.OCP_APPS_DOMAIN/repository/maven-all-public/ 8 | * 9 | 10 | 11 | 12 | 13 | nexus 14 | admin 15 | redhat 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /nexus_settings_openshift.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | nexus 6 | Nexus Public Mirror 7 | http://nexus.nexus.svc:8081/repository/maven-all-public/ 8 | * 9 | 10 | 11 | 12 | 13 | nexus 14 | admin 15 | redhat 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /systemtest-run-dev-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | cd ${0%/*}/coffee-shop 5 | 6 | trap cleanup EXIT 7 | 8 | function cleanup() { 9 | echo stopping containers 10 | docker stop coffee-shop coffee-shop-db barista &> /dev/null || true 11 | } 12 | 13 | 14 | cleanup 15 | 16 | docker run -d --rm \ 17 | --name coffee-shop-db \ 18 | --network dkrnet \ 19 | -p 5432:5432 \ 20 | -e POSTGRES_USER=postgres \ 21 | -e POSTGRES_PASSWORD=postgres \ 22 | postgres:9.5 23 | 24 | docker run -d --rm \ 25 | --name barista \ 26 | --network dkrnet \ 27 | -p 8002:8080 \ 28 | rodolpheche/wiremock:2.6.0 29 | 30 | 31 | # coffee-shop 32 | mvn clean package -Dquarkus.package.type=mutable-jar 33 | docker build -f Dockerfile.dev -t tmp-builder . 34 | 35 | docker run -d --rm \ 36 | --name coffee-shop \ 37 | --network dkrnet \ 38 | -p 8001:8080 \ 39 | -p 5005:5005 \ 40 | tmp-builder 41 | 42 | # wait for app startup 43 | while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:8001/health)" != "200" ]]; do 44 | sleep 2; 45 | done 46 | 47 | mvn quarkus:remote-dev -Ddebug=false -Dquarkus.live-reload.url=http://localhost:8001 -Dquarkus.live-reload.password=123 -Dquarkus.package.type=mutable-jar -------------------------------------------------------------------------------- /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 | postgres:9.5 14 | 15 | docker run -d --rm \ 16 | --name barista \ 17 | --network dkrnet \ 18 | -p 8002:8080 \ 19 | rodolpheche/wiremock:2.6.0 20 | 21 | # wait for db startup 22 | sleep 5 23 | 24 | docker run -d --rm \ 25 | --name coffee-shop \ 26 | --network dkrnet \ 27 | -p 8001:8080 \ 28 | coffee-shop 29 | --------------------------------------------------------------------------------