This is an automated email from the ASF dual-hosted git repository. jamesnetherton pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
The following commit(s) were added to refs/heads/main by this push: new 5cb73cad45 Fixes #7373 extend saga coverage 5cb73cad45 is described below commit 5cb73cad45972b1b1cd0db6bb0004ee973434039 Author: Jiri Ondrusek <ondrusek.j...@gmail.com> AuthorDate: Fri May 16 16:12:37 2025 +0200 Fixes #7373 extend saga coverage --- integration-tests/saga/pom.xml | 96 +++++++++++++++ .../quarkus/component/saga/it/SagaResource.java | 99 +++++++++++++++ .../camel/quarkus/component/saga/it/SagaRoute.java | 133 +++++++++++++++++++++ .../component/saga/it/lra/LraCreditService.java | 61 ++++++++++ .../quarkus/component/saga/it/lra/LraService.java | 58 +++++++++ .../component/saga/it/lra/LraTicketService.java | 70 +++++++++++ .../saga/it/lra/LraTicketServiceStatus.java} | 29 +---- .../saga/src/main/resources/application.properties | 19 +++ .../saga/src/main/resources/routes/saga-routes.xml | 54 +++++++++ .../camel/quarkus/component/saga/it/SagaTest.java | 132 ++++++++++++++++++++ 10 files changed, 728 insertions(+), 23 deletions(-) diff --git a/integration-tests/saga/pom.xml b/integration-tests/saga/pom.xml index fe4c9c06c2..5753aa6511 100644 --- a/integration-tests/saga/pom.xml +++ b/integration-tests/saga/pom.xml @@ -39,6 +39,14 @@ <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-direct</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-seda</artifactId> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-lra</artifactId> + </dependency> <dependency> <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-bean</artifactId> @@ -47,6 +55,26 @@ <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy</artifactId> </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-resteasy-jackson</artifactId> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-xml-io-dsl</artifactId> + </dependency> + + <!-- Messaging extension to test --> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-jms</artifactId> + </dependency> + + <!-- The JMS client library to test with --> + <dependency> + <groupId>io.quarkiverse.artemis</groupId> + <artifactId>quarkus-artemis-jms</artifactId> + </dependency> <!-- test dependencies --> <dependency> @@ -59,6 +87,11 @@ <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.awaitility</groupId> + <artifactId>awaitility</artifactId> + <scope>test</scope> + </dependency> </dependencies> <profiles> @@ -124,6 +157,45 @@ </exclusion> </exclusions> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-jms-deployment</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-seda-deployment</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-lra-deployment</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> <dependency> <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-saga-deployment</artifactId> @@ -137,8 +209,32 @@ </exclusion> </exclusions> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-xml-io-dsl-deployment</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> </dependencies> </profile> + <profile> + <id>skip-testcontainers-tests</id> + <activation> + <property> + <name>skip-testcontainers-tests</name> + </property> + </activation> + <properties> + <skipTests>true</skipTests> + </properties> + </profile> </profiles> </project> diff --git a/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/SagaResource.java b/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/SagaResource.java index e2ad657750..f7cc8c8763 100644 --- a/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/SagaResource.java +++ b/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/SagaResource.java @@ -16,14 +16,20 @@ */ package org.apache.camel.quarkus.component.saga.it; +import java.util.Map; + import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.apache.camel.CamelContext; +import org.apache.camel.quarkus.component.saga.it.lra.LraCreditService; +import org.apache.camel.quarkus.component.saga.it.lra.LraService; +import org.apache.camel.quarkus.component.saga.it.lra.LraTicketService; import org.jboss.logging.Logger; @Path("/saga") @@ -43,6 +49,15 @@ public class SagaResource { @Inject CreditService creditService; + @Inject + LraCreditService lraCreditService; + + @Inject + LraTicketService lraTicketService; + + @Inject + LraService lraService; + @Path("/load/component/saga") @GET @Produces(MediaType.TEXT_PLAIN) @@ -94,4 +109,88 @@ public class SagaResource { } } } + + @Path("/lraSaga/{id}/{credit}/{trainCost}/{flightCost}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response lraSaga(@PathParam("id") int id, + @PathParam("credit") int credit, + @PathParam("trainCost") int trainCost, + @PathParam("flightCost") int flightCost) throws InterruptedException { + + //initialize total credit + lraCreditService.setTotalCredit(credit); + lraTicketService.reset(); + + try { + context.createFluentProducerTemplate().to("direct:lraSaga") + .withHeader("id", id) + .withHeader("trainCost", trainCost) + .withHeader("flightCost", flightCost) + .request(); + } catch (Exception e) { + return getResponse(500); + } + + return getResponse(200); + } + + private Response getResponse(int status) throws InterruptedException { + // wait for the orders being cancelled + Thread.sleep(500); + Map result = Map.of("creditBalance", lraCreditService.getCredit(), "train", + lraTicketService.getTrain(), + "flight", lraTicketService.getFlight()); + + return Response.status(status).entity(result).build(); + } + + @Path("/timeout/{timeout}") + @GET + @Produces(MediaType.TEXT_PLAIN) + public Response timeout(@PathParam("timeout") int timeout) throws InterruptedException { + Object o = context.createFluentProducerTemplate().to("direct:newOrderTimeout5sec") + .withHeader("timeout", timeout) + .request(); + return Response.ok().entity(o).build(); + } + + @Path("/manualSaga/{complete}") + @GET + @Produces(MediaType.TEXT_PLAIN) + public Response manualSaga(@PathParam("complete") boolean complete) throws InterruptedException { + + lraService.setCompleted(false); + + context.createFluentProducerTemplate().to("direct:manualSaga") + .withHeader("shouldComplete", complete) + .request(); + + return Response.ok().build(); + } + + @Path("/manualCompleted") + @GET + @Produces(MediaType.TEXT_PLAIN) + public Response manualCompensated() throws InterruptedException { + + return Response.ok().entity(lraService.isCompleted()).build(); + } + + @Path("/xmlSaga/{complete}") + @GET + @Produces(MediaType.TEXT_PLAIN) + public Response xmlSaga(@PathParam("complete") boolean complete) throws InterruptedException { + lraService.setXmlCompleted(false); + try { + context.createFluentProducerTemplate().to("direct:xmlSaga") + .withHeader("complete", complete) + .request(); + } catch (Exception e) { + return Response.status(500).entity(e.getCause().getMessage()).build(); + } + //time to complete + Thread.sleep(500); + return Response.ok().entity(lraService.isXmlCompleted()).build(); + } } diff --git a/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/SagaRoute.java b/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/SagaRoute.java index d6dcea6124..8fe8498c3a 100644 --- a/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/SagaRoute.java +++ b/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/SagaRoute.java @@ -16,11 +16,17 @@ */ package org.apache.camel.quarkus.component.saga.it; +import java.util.concurrent.TimeUnit; + import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.apache.camel.Exchange; import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.SagaCompletionMode; import org.apache.camel.model.SagaPropagation; +import org.apache.camel.quarkus.component.saga.it.lra.LraCreditService; +import org.apache.camel.quarkus.component.saga.it.lra.LraService; +import org.apache.camel.quarkus.component.saga.it.lra.LraTicketService; import org.apache.camel.saga.CamelSagaService; import org.apache.camel.saga.InMemorySagaService; @@ -32,6 +38,15 @@ public class SagaRoute extends RouteBuilder { @Inject CreditService creditService; + @Inject + LraService lraService; + + @Inject + LraCreditService lraCreditService; + + @Inject + LraTicketService lraTicketService; + @Override public void configure() throws Exception { CamelSagaService sagaService = new InMemorySagaService(); @@ -63,5 +78,123 @@ public class SagaRoute extends RouteBuilder { from("direct:finalize").saga().propagation(SagaPropagation.MANDATORY).choice() .when(header("fail").isEqualTo(true)).to("saga:COMPENSATE").end(); + // ---------------------- saga with JMS using custom ids / timeouts / completion / Long-Running-Action header + + from("direct:lraSaga") + .saga() + .compensation("direct:lraCancelOrder") + .completion("direct:lraCompleted") + .log("Executing saga #${header.id} with LRA ${header.Long-Running-Action}") + .setHeader("payFor", constant("train")) + .setHeader("amount", header("trainCost")) + .to("jms:queue:train?exchangePattern=InOut" + + "&replyTo=train.reply") + .log("train seat reserved for saga #${header.id} with payment transaction: ${body}") + .setHeader("payFor", constant("flight")) + .setHeader("amount", header("flightCost")) + .to("jms:queue:flight?exchangePattern=InOut" + + "&replyTo=flight.reply") + .log("flight booked for saga #${header.id} with payment transaction: ${body}") + .setBody(header("Long-Running-Action")) + .end(); + + from("direct:lraCancelOrder") + .log("Transaction ${header.Long-Running-Action} has been cancelled due to flight or train insufficient payment, refunding all tickets") + .bean(lraCreditService, "refundCredit") + .bean(lraTicketService, "setTicketsRefunded") + .log("Credit for action ${body} refunded"); + + from("direct:lraCompleted") + .log("Transaction ${header.Long-Running-Action} has been completed.") + .bean(lraTicketService, "setTicketsReserved"); + + //train + from("jms:queue:train") + .saga() + .propagation(SagaPropagation.MANDATORY) + .option("id", header("id")) + .compensation("direct:lraTrainCancelPurchase") + .log("Buying train #${header.id}") + .to("jms:queue:payment?exchangePattern=InOut" + + "&replyTo=payment.train.reply") + .log("Payment for train #${header.id} done with transaction ${body}") + .end(); + + from("direct:lraTrainCancelPurchase") + .log("Train purchase #${header.id} has been cancelled due to payment failure"); + + //flight + from("jms:queue:flight") + .saga() + .propagation(SagaPropagation.MANDATORY) + .option("id", header("id")) + .compensation("direct:lraFlightCancelPurchase") + .log("Buying flight #${header.id}") + .to("jms:queue:payment?exchangePattern=InOut" + + "&replyTo={payment.flight.reply") + .log("Payment for flight #${header.id} done with transaction ${body}") + .end(); + + from("direct:lraFlightCancelPurchase") + .log("Flight purchase #${header.id} has been cancelled due to payment failure"); + + from("jms:queue:payment") + .routeId("payment-service") + .saga() + .propagation(SagaPropagation.MANDATORY) + .option("id", header("id")) + .option("payFor", header("payFor")) + .compensation("direct:lraCancelPayment") + .log("Paying ${header.payFor} (${header.amount}) for order #${header.id}") + .bean(lraCreditService, "reserveCredit") + .setBody(header("JMSCorrelationID")) + .log("Payment ${header.payFor} done for order #${header.id} with payment transaction ${body}") + .end(); + + from("direct:lraCancelPayment") + .routeId("payment-cancel") + .choice() + .when(header("payFor").contains("train")).bean(lraTicketService, "setTrainError") + .when(header("payFor").contains("flight")).bean(lraTicketService, "setFlightError") + .endChoice() + .log("Payment for order #${header.id} did not finish (insufficient credit)"); + + // ----------------- timeout ------------------------------ + + from("direct:newOrderTimeout5sec") + .saga() + .timeout(5, TimeUnit.SECONDS) // newOrder requires that the saga is completed within 5 seconds + .propagation(SagaPropagation.REQUIRES_NEW) + .compensation("direct:cancelOrderTimeout5sec") + .bean(lraService, "sleep") + .setBody(constant("success")) + .log("Order ${body} created"); + + from("direct:cancelOrderTimeout5sec") + .saga() + .propagation(SagaPropagation.MANDATORY) + .setBody(constant("failure")); + + // ----------------- manual ---------------------------------- + + from("direct:manualSaga") + .saga() + .completionMode(SagaCompletionMode.MANUAL) + .completion("seda:manualSagaComplete") + .to("seda:manualSagaProcessOrder"); + + from("seda:manualSagaProcessOrder") // an asynchronous callback + .saga() + .propagation(SagaPropagation.MANDATORY) + .log("Processing manual saga order with complete set to ${header.shouldComplete}") + .choice() + .when(header("shouldComplete").isEqualTo("true")) + .to("saga:complete") // complete the current saga manually (saga component) + .end(); + + from("seda:manualSagaComplete") // an asynchronous callback + .log("Manual saga marked as completed") + .bean(lraService, "complete"); + } } diff --git a/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/lra/LraCreditService.java b/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/lra/LraCreditService.java new file mode 100644 index 0000000000..2f10075711 --- /dev/null +++ b/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/lra/LraCreditService.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.saga.it.lra; + +import java.util.HashMap; +import java.util.Map; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.camel.Header; + +@ApplicationScoped +@RegisterForReflection +public class LraCreditService { + + private int totalCredit; + + private Map<String, Integer> reservations = new HashMap<>(); + + public LraCreditService() { + this.totalCredit = 100; + } + + public synchronized void reserveCredit(@Header("Long-Running-Action") String id, @Header("amount") int amount) { + int credit = getCredit(); + if (amount > credit) { + throw new IllegalStateException("Insufficient credit"); + } + if (reservations.containsKey(id)) { + reservations.put(id, reservations.get(id) + amount); + } else { + reservations.put(id, amount); + } + } + + public synchronized void refundCredit(@Header("Long-Running-Action") String id) { + reservations.remove(id); + } + + public synchronized int getCredit() { + return totalCredit - reservations.values().stream().reduce(0, (a, b) -> a + b); + } + + public void setTotalCredit(int totalCredit) { + this.totalCredit = totalCredit; + } +} diff --git a/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/lra/LraService.java b/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/lra/LraService.java new file mode 100644 index 0000000000..c51b98a9ef --- /dev/null +++ b/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/lra/LraService.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.saga.it.lra; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Named; +import org.apache.camel.Header; + +@ApplicationScoped +@Named("lraService") +@RegisterForReflection +public class LraService { + + private Boolean completed, xmlCompleted; + + public void sleep(@Header("timeout") long timeout) throws InterruptedException { + Thread.sleep(timeout); + } + + public void setCompleted(boolean completed) { + this.completed = completed; + } + + public void setXmlCompleted(boolean xmlCompleted) { + this.xmlCompleted = xmlCompleted; + } + + public boolean isCompleted() { + return completed; + } + + public void complete() { + this.completed = true; + } + + public void xmlComplete() { + this.xmlCompleted = true; + } + + public boolean isXmlCompleted() { + return xmlCompleted; + } +} diff --git a/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/lra/LraTicketService.java b/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/lra/LraTicketService.java new file mode 100644 index 0000000000..b68969c929 --- /dev/null +++ b/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/lra/LraTicketService.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.saga.it.lra; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +@RegisterForReflection +public class LraTicketService { + + private LraTicketServiceStatus train = LraTicketServiceStatus.nothing; + + private LraTicketServiceStatus flight = LraTicketServiceStatus.nothing; + + public LraTicketServiceStatus getTrain() { + return train; + } + + public void setTrainError() { + this.train = LraTicketServiceStatus.error; + } + + public void setFlightError() { + this.flight = LraTicketServiceStatus.error; + } + + public LraTicketServiceStatus getFlight() { + return flight; + } + + public void setTicketsRefunded() { + //all tickets without error are refunded now + if (this.flight == LraTicketServiceStatus.nothing) { + this.flight = LraTicketServiceStatus.refunded; + } + if (this.train == LraTicketServiceStatus.nothing) { + this.train = LraTicketServiceStatus.refunded; + } + } + + public void setTicketsReserved() { + //all tickets without error are refunded now + if (this.flight == LraTicketServiceStatus.nothing) { + this.flight = LraTicketServiceStatus.reserved; + } + if (this.train == LraTicketServiceStatus.nothing) { + this.train = LraTicketServiceStatus.reserved; + } + } + + public void reset() { + this.train = LraTicketServiceStatus.nothing; + this.flight = LraTicketServiceStatus.nothing; + } +} diff --git a/integration-tests/saga/src/test/java/org/apache/camel/quarkus/component/saga/it/SagaTest.java b/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/lra/LraTicketServiceStatus.java similarity index 59% copy from integration-tests/saga/src/test/java/org/apache/camel/quarkus/component/saga/it/SagaTest.java copy to integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/lra/LraTicketServiceStatus.java index 9a382d982a..31fc5d256f 100644 --- a/integration-tests/saga/src/test/java/org/apache/camel/quarkus/component/saga/it/SagaTest.java +++ b/integration-tests/saga/src/main/java/org/apache/camel/quarkus/component/saga/it/lra/LraTicketServiceStatus.java @@ -14,28 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.camel.quarkus.component.saga.it; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; - -@QuarkusTest -class SagaTest { - - @Test - public void loadComponentSaga() { - /* A simple autogenerated test */ - RestAssured.get("/saga/load/component/saga") - .then() - .statusCode(200); - } - - @Test - public void testCreditExhausted() { - RestAssured.get("/saga/test") - .then() - .statusCode(200); - } +package org.apache.camel.quarkus.component.saga.it.lra; +public enum LraTicketServiceStatus { + nothing, //credit is enough + refunded, //the other ticket failed, therefore current ticket was refunded + error, //purchase of ticket failed due to insufficient credit + reserved; } diff --git a/integration-tests/saga/src/main/resources/application.properties b/integration-tests/saga/src/main/resources/application.properties new file mode 100644 index 0000000000..af4a30346b --- /dev/null +++ b/integration-tests/saga/src/main/resources/application.properties @@ -0,0 +1,19 @@ +## --------------------------------------------------------------------------- +## Licensed to the Apache Software Foundation (ASF) under one or more +## contributor license agreements. See the NOTICE file distributed with +## this work for additional information regarding copyright ownership. +## The ASF licenses this file to You under the Apache License, Version 2.0 +## (the "License"); you may not use this file except in compliance with +## the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## --------------------------------------------------------------------------- +quarkus.artemis.devservices.extra-args=--no-autotune --mapped --no-fsync --java-options=-Dbrokerconfig.maxDiskUsage=-1 +quarkus.artemis.enabled=true +camel.main.routes-include-pattern = routes/saga-routes.xml \ No newline at end of file diff --git a/integration-tests/saga/src/main/resources/routes/saga-routes.xml b/integration-tests/saga/src/main/resources/routes/saga-routes.xml new file mode 100644 index 0000000000..49306533e4 --- /dev/null +++ b/integration-tests/saga/src/main/resources/routes/saga-routes.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--> +<routes> + <route> + <from uri="direct:xmlSaga"/> + <saga> + <compensation uri="direct:xmlCompensation" /> + <completion uri="direct:xmlCompletion" /> + <option key="shouldComplete"> + <header>complete</header> + </option> + </saga> + <to uri="direct:xmlAction" /> + </route> + + <route> + <from uri="direct:xmlAction" /> + <log message="Xml action" /> + <choice> + <when> + <simple>${header.complete} == false</simple> + <throwException exceptionType="java.lang.RuntimeException" message="Intended xml exception"/> + </when> + </choice> + </route> + + <route> + <from uri="direct:xmlCompensation" /> + <log message="Xml compensation" /> + </route> + + <route> + <from uri="direct:xmlCompletion" /> + <log message="Xml completion" /> + <to uri="bean:lraService?method=xmlComplete"/> + </route> +</routes> diff --git a/integration-tests/saga/src/test/java/org/apache/camel/quarkus/component/saga/it/SagaTest.java b/integration-tests/saga/src/test/java/org/apache/camel/quarkus/component/saga/it/SagaTest.java index 9a382d982a..d45427e066 100644 --- a/integration-tests/saga/src/test/java/org/apache/camel/quarkus/component/saga/it/SagaTest.java +++ b/integration-tests/saga/src/test/java/org/apache/camel/quarkus/component/saga/it/SagaTest.java @@ -16,8 +16,13 @@ */ package org.apache.camel.quarkus.component.saga.it; +import java.util.concurrent.TimeUnit; + import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; +import org.apache.camel.quarkus.component.saga.it.lra.LraTicketServiceStatus; +import org.awaitility.Awaitility; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; @QuarkusTest @@ -38,4 +43,131 @@ class SagaTest { .statusCode(200); } + //long-run-actions using jms. + // Scenario - buying train and flight ticket. + // If credit is not sufficient, the purchase fails. + // All payments are refunded, the reason of refundment is saved in ticket service + // + // rest endpoint saga/lraSaga/1/100/50/50 has following attributes: #orderId/#initialCredit/#trabCost/#flightCost + + @Test + public void testLRACompletedScenario() { + //successful transaction + RestAssured.get("/saga/lraSaga/1/100/50/50") + .then() + .statusCode(200) + .body("creditBalance", Matchers.is(0)) + .body("train", Matchers.is(LraTicketServiceStatus.reserved.name())) + .body("flight", Matchers.is(LraTicketServiceStatus.reserved.name())); + } + + @Test + public void testLRAInsufficientCredit01() { + //successful transaction + RestAssured.get("/saga/lraSaga/2/50/50/100") + .then() + .statusCode(500) + .body("creditBalance", Matchers.is(50)) + .body("train", Matchers.is(LraTicketServiceStatus.refunded.name())) + .body("flight", Matchers.is(LraTicketServiceStatus.error.name())); + } + + @Test + public void testLRAInsufficientCredit02() { + //successful transaction + RestAssured.get("/saga/lraSaga/3/50/100/50") + .then() + .statusCode(500) + .body("creditBalance", Matchers.is(50)) + .body("train", Matchers.is(LraTicketServiceStatus.error.name())) + .body("flight", Matchers.is(LraTicketServiceStatus.refunded.name())); + } + + @Test + public void testLRAInsufficientCredit03() { + //successful transaction + RestAssured.get("/saga/lraSaga/4/50/100/100") + .then() + .statusCode(500) + .body("creditBalance", Matchers.is(50)) + .body("train", Matchers.is(LraTicketServiceStatus.error.name())) + .body("flight", Matchers.is(LraTicketServiceStatus.refunded.name())); //bought of flight ticket is not attempted + } + + @Test + public void testLRAInsufficientCredit04() { + //successful transaction + RestAssured.get("/saga/lraSaga/5/50/50/50") + .then() + .statusCode(500) + .body("creditBalance", Matchers.is(50)) + .body("train", Matchers.is(LraTicketServiceStatus.refunded.name())) + .body("flight", Matchers.is(LraTicketServiceStatus.error.name())); //the second buy action fails + } + + @Test + public void testTimeoutSuccessful() { + //successful transaction + RestAssured.get("/saga/timeout/2000") + .then() + .body(Matchers.is("success")) + .statusCode(200); + } + + @Test + public void testTimeoutFailure() { + //successful transaction + RestAssured.get("/saga/timeout/10000") + .then() + .statusCode(500); + } + + @Test + public void testManualSuccess() { + + //start saga action, which won't complete + RestAssured.given().get("/saga/manualSaga/true").then() + .statusCode(200); + + Awaitility.await().pollInterval(1, TimeUnit.SECONDS).atMost(10, TimeUnit.MINUTES).untilAsserted( + () -> RestAssured.get("/saga/manualCompleted") + .then() + .statusCode(200) + .body(Matchers.is("true"))); + + } + + @Test + public void testManualFailure() throws InterruptedException { + + //start saga action, which won't complete + RestAssured.given().get("/saga/manualSaga/false").then() + .statusCode(200); + + //wait some time and the saga should not be completed + Thread.sleep(10000); + + RestAssured.get("/saga/manualCompleted") + .then() + .statusCode(200) + .body(Matchers.is("false")); + } + + @Test + public void testXmlDslSuccess() throws InterruptedException { + + RestAssured.get("/saga/xmlSaga/true") + .then() + .statusCode(200) + .body(Matchers.is("true")); + } + + @Test + public void testXmlDslFailure() throws InterruptedException { + RestAssured.get("/saga/xmlSaga/false") + .then() + .statusCode(500) + .body(Matchers.is("Intended xml exception")); + } + }