From 6f3a20a9d11cf28dc3915a38ea95b706aff638bf Mon Sep 17 00:00:00 2001 From: pablo gonzalez granados Date: Tue, 24 Jan 2023 19:21:11 +0100 Subject: [PATCH] Add transaction-API classic scenario --- README.md | 7 + pom.xml | 1 + sql-db/narayana-transactions/pom.xml | 108 +++++++++ .../AccountEntity.java | 105 +++++++++ .../AccountService.java | 54 +++++ .../JournalEntity.java | 96 ++++++++ .../JournalService.java | 18 ++ .../TransferDTO.java | 33 +++ .../TransferProcessor.java | 20 ++ .../TransferResource.java | 98 +++++++++ .../TransferTopUpService.java | 48 ++++ .../TransferTransactionService.java | 52 +++++ .../TransferWithdrawalService.java | 51 +++++ .../src/main/resources/application.properties | 6 + .../src/main/resources/import.sql | 6 + .../MariaDbTransactionGeneralUsageIT.java | 28 +++ .../MssqlTransactionGeneralUsageIT.java | 27 +++ .../MysqlTransactionGeneralUsageIT.java | 30 +++ ...ShiftMariaDbTransactionGeneralUsageIT.java | 26 +++ ...penShiftMsqlTransactionGeneralUsageIT.java | 25 +++ ...enShiftMssqlTransactionGeneralUsageIT.java | 10 + ...nShiftOracleTransactionGeneralUsageIT.java | 10 + ...ftPostgresqlTransactionGeneralUsageIT.java | 7 + .../OracleTransactionGeneralUsageIT.java | 27 +++ .../PostgresqlTransactionGeneralUsageIT.java | 28 +++ .../quarkus/ts/transactions/SwaggerUiIT.java | 28 +++ .../ts/transactions/TransactionCommons.java | 208 ++++++++++++++++++ .../container-license-acceptance.txt | 5 + .../src/test/resources/mariadb_app.properties | 6 + .../src/test/resources/mariadb_import.sql | 7 + .../src/test/resources/mssql.properties | 7 + .../src/test/resources/mssql_import.sql | 6 + .../src/test/resources/mysql.properties | 6 + .../src/test/resources/oracle.properties | 5 + .../src/test/resources/oracle_import.sql | 5 + .../src/test/resources/test.properties | 4 + 36 files changed, 1208 insertions(+) create mode 100644 sql-db/narayana-transactions/pom.xml create mode 100644 sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/AccountEntity.java create mode 100644 sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/AccountService.java create mode 100644 sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/JournalEntity.java create mode 100644 sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/JournalService.java create mode 100644 sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferDTO.java create mode 100644 sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferProcessor.java create mode 100644 sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferResource.java create mode 100644 sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferTopUpService.java create mode 100644 sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferTransactionService.java create mode 100644 sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferWithdrawalService.java create mode 100644 sql-db/narayana-transactions/src/main/resources/application.properties create mode 100644 sql-db/narayana-transactions/src/main/resources/import.sql create mode 100644 sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MariaDbTransactionGeneralUsageIT.java create mode 100644 sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MssqlTransactionGeneralUsageIT.java create mode 100644 sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MysqlTransactionGeneralUsageIT.java create mode 100644 sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMariaDbTransactionGeneralUsageIT.java create mode 100644 sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMsqlTransactionGeneralUsageIT.java create mode 100644 sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMssqlTransactionGeneralUsageIT.java create mode 100644 sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftOracleTransactionGeneralUsageIT.java create mode 100644 sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftPostgresqlTransactionGeneralUsageIT.java create mode 100644 sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OracleTransactionGeneralUsageIT.java create mode 100644 sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/PostgresqlTransactionGeneralUsageIT.java create mode 100644 sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/SwaggerUiIT.java create mode 100644 sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/TransactionCommons.java create mode 100644 sql-db/narayana-transactions/src/test/resources/container-license-acceptance.txt create mode 100644 sql-db/narayana-transactions/src/test/resources/mariadb_app.properties create mode 100644 sql-db/narayana-transactions/src/test/resources/mariadb_import.sql create mode 100644 sql-db/narayana-transactions/src/test/resources/mssql.properties create mode 100644 sql-db/narayana-transactions/src/test/resources/mssql_import.sql create mode 100644 sql-db/narayana-transactions/src/test/resources/mysql.properties create mode 100644 sql-db/narayana-transactions/src/test/resources/oracle.properties create mode 100644 sql-db/narayana-transactions/src/test/resources/oracle_import.sql create mode 100644 sql-db/narayana-transactions/src/test/resources/test.properties diff --git a/README.md b/README.md index 554985bdb..613e87e46 100644 --- a/README.md +++ b/README.md @@ -538,6 +538,13 @@ Base application is reduced to two REST resources: Tests cover the supported functionality of `rest-data-panache`: CRUD operations, `json` and `hal+json` data types, invalid input, filtering, sorting, pagination. +### `sql-db/narayana-transactions` + +Verifies Quarkus transaction programmatic API. +Base application contains REST resource `TransferResource` and three main services: `TransferTransactionService`, `TransferWithdrawalService` +and `TransferTopUpService` which implement various bank transactions. The main scenario is implemented in `TransactionGeneralUsageIT` +and checks whether transactions and rollbacks always done in full. + ### `security/basic` Verifies the simplest way of doing authn/authz. diff --git a/pom.xml b/pom.xml index 957381a04..e5f2b88b2 100644 --- a/pom.xml +++ b/pom.xml @@ -557,6 +557,7 @@ sql-db/hibernate-reactive sql-db/reactive-vanilla sql-db/hibernate-fulltext-search + sql-db/narayana-transactions diff --git a/sql-db/narayana-transactions/pom.xml b/sql-db/narayana-transactions/pom.xml new file mode 100644 index 000000000..da2c1e748 --- /dev/null +++ b/sql-db/narayana-transactions/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + io.quarkus.ts.qe + parent + 1.0.0-SNAPSHOT + ../.. + + narayana-transactions + jar + Quarkus QE TS: SQL Database: Narayana-transactions + + + io.quarkus + quarkus-hibernate-orm-panache + + + io.quarkus + quarkus-resteasy-jackson + + + io.quarkus + quarkus-jdbc-mariadb + + + io.quarkus + quarkus-jdbc-mssql + + + io.quarkus + quarkus-jdbc-mysql + + + io.quarkus + quarkus-jdbc-postgresql + + + io.quarkus + quarkus-jdbc-oracle + + + io.quarkus + quarkus-smallrye-health + + + + io.quarkus + quarkus-smallrye-openapi + + + + io.quarkus + quarkus-opentelemetry + + + + io.quarkus + quarkus-micrometer-registry-prometheus + + + + io.quarkus.qe + quarkus-test-service-database + test + + + io.quarkus.qe + quarkus-test-service-jaeger + test + + + + + native + + + native + + + + + + skip-tests-on-windows + + + windows + + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + + + + diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/AccountEntity.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/AccountEntity.java new file mode 100644 index 000000000..f1368e2bc --- /dev/null +++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/AccountEntity.java @@ -0,0 +1,105 @@ +package io.quarkus.ts.transactions; + +import java.sql.Timestamp; +import java.util.List; +import java.util.Objects; + +import javax.persistence.Column; +import javax.persistence.Entity; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.panache.common.Parameters; +import io.quarkus.panache.common.Sort; + +@Entity(name = "account") +public class AccountEntity extends PanacheEntity { + @Column(nullable = false) + private String name; + @Column(nullable = false) + private String lastName; + @Column(unique = true, nullable = false) + private String accountNumber; + @Column(precision = 10, scale = 2, nullable = false) + private int amount; + private Timestamp updatedAt; + @Column(nullable = false) + private Timestamp createdAt; + + public static boolean exist(String accountNumber) { + return Objects.nonNull(findAccount(accountNumber)); + } + + public static AccountEntity findAccount(String accountNumber) { + return find("accountNumber", accountNumber).firstResult(); + } + + public static int updateAmount(String accountNumber, int amount) { + Timestamp currentTime = new Timestamp(System.currentTimeMillis()); + int updatedRecordsAmount = update("amount = :amount, updatedAt = :updatedAt where accountNumber = :account", + Parameters.with("amount", amount) + .and("updatedAt", currentTime) + .and("account", accountNumber)); + flush(); + return updatedRecordsAmount; + } + + public static List getAllAccountsRecords() { + return findAll(Sort.by("createdAt").descending()).list(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public int getAmount() { + return amount; + } + + public void setAmount(int amount) { + this.amount = amount; + } + + public Timestamp getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Timestamp updatedAt) { + this.updatedAt = updatedAt; + } + + public Timestamp getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Timestamp createdAt) { + this.createdAt = createdAt; + } + + public String getAccountNumber() { + return accountNumber; + } + + public void setAccountNumber(String accountNumber) { + this.accountNumber = accountNumber; + } +} diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/AccountService.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/AccountService.java new file mode 100644 index 000000000..bd9aa13e2 --- /dev/null +++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/AccountService.java @@ -0,0 +1,54 @@ +package io.quarkus.ts.transactions; + +import static io.quarkus.ts.transactions.AccountEntity.exist; + +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.NotFoundException; + +import org.jboss.logging.Logger; + +@ApplicationScoped +public class AccountService { + + private static final Logger LOG = Logger.getLogger(AccountService.class); + + public boolean isPresent(String accountNumber) { + if (!exist(accountNumber)) { + String msg = String.format("Account %s doesn't exist", accountNumber); + LOG.warn(msg); + throw new NotFoundException(msg); + } + + return true; + } + + public int increaseBalance(String account, int amount) { + AccountEntity accountEntity = AccountEntity.findAccount(account); + int updatedAmount = accountEntity.getAmount() + amount; + AccountEntity.updateAmount(account, updatedAmount); + return AccountEntity.findAccount(account).getAmount(); + } + + public int decreaseBalance(String account, int amount) { + AccountEntity accountEntity = AccountEntity.findAccount(account); + int updatedAmount = accountEntity.getAmount() - amount; + if (updatedAmount < 0) { + String msg = String.format("Account %s Not enough balance.", account); + LOG.warn(msg); + throw new BadRequestException(msg); + } + AccountEntity.updateAmount(account, updatedAmount); + return updatedAmount; + } + + public List getAllAccounts() { + return AccountEntity.getAllAccountsRecords(); + } + + public AccountEntity getAccount(String accountNumber) { + return AccountEntity.findAccount(accountNumber); + } +} diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/JournalEntity.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/JournalEntity.java new file mode 100644 index 000000000..1fdd295dd --- /dev/null +++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/JournalEntity.java @@ -0,0 +1,96 @@ +package io.quarkus.ts.transactions; + +import java.sql.Timestamp; + +import javax.persistence.Column; +import javax.persistence.Entity; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.panache.common.Parameters; +import io.quarkus.panache.common.Sort; + +@Entity(name = "journal") +public class JournalEntity extends PanacheEntity { + + @Column(nullable = false) + private String annotation; + @Column(nullable = false) + private String accountTo; + @Column(nullable = false) + private String accountFrom; + @Column(nullable = false) + private int amount; + @Column(nullable = false) + private Timestamp createdAt; + + public JournalEntity() { + } + + public JournalEntity(String accountFrom, String accountTo, String annotation, int amount) { + this.accountFrom = accountFrom; + this.accountTo = accountTo; + this.annotation = annotation; + this.amount = amount; + this.createdAt = new Timestamp(System.currentTimeMillis()); + } + + public JournalEntity addLog() { + persistAndFlush(); + return this; + } + + public static JournalEntity getLatestJournalRecord(String accountNumber) { + return find("accountFrom = :accountFrom", + Sort.by("createdAt").descending(), + Parameters.with("accountFrom", accountNumber)) + .firstResult(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getAnnotation() { + return annotation; + } + + public void setAnnotation(String annotation) { + this.annotation = annotation; + } + + public String getAccountTo() { + return accountTo; + } + + public void setAccountTo(String accountTo) { + this.accountTo = accountTo; + } + + public String getAccountFrom() { + return accountFrom; + } + + public void setAccountFrom(String accountFrom) { + this.accountFrom = accountFrom; + } + + public int getAmount() { + return amount; + } + + public void setAmount(int amount) { + this.amount = amount; + } + + public Timestamp getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Timestamp createdAt) { + this.createdAt = createdAt; + } +} diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/JournalService.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/JournalService.java new file mode 100644 index 000000000..85435720e --- /dev/null +++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/JournalService.java @@ -0,0 +1,18 @@ +package io.quarkus.ts.transactions; + +import static io.quarkus.ts.transactions.JournalEntity.getLatestJournalRecord; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class JournalService { + + public JournalEntity addToJournal(String accountFrom, String accountTo, String annotation, int amount) { + JournalEntity journal = new JournalEntity(accountFrom, accountTo, annotation, amount); + return journal.addLog(); + } + + public JournalEntity getLatestJournalRecordByAccountNumber(String accountNumber) { + return getLatestJournalRecord(accountNumber); + } +} diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferDTO.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferDTO.java new file mode 100644 index 000000000..23f721906 --- /dev/null +++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferDTO.java @@ -0,0 +1,33 @@ +package io.quarkus.ts.transactions; + +import java.io.Serializable; + +public class TransferDTO implements Serializable { + private String accountTo; + private String accountFrom; + private int amount; + + public String getAccountTo() { + return accountTo; + } + + public void setAccountTo(String accountTo) { + this.accountTo = accountTo; + } + + public String getAccountFrom() { + return accountFrom; + } + + public void setAccountFrom(String accountFrom) { + this.accountFrom = accountFrom; + } + + public int getAmount() { + return amount; + } + + public void setAmount(int amount) { + this.amount = amount; + } +} diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferProcessor.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferProcessor.java new file mode 100644 index 000000000..ccb98445c --- /dev/null +++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferProcessor.java @@ -0,0 +1,20 @@ +package io.quarkus.ts.transactions; + +import javax.inject.Inject; + +public abstract class TransferProcessor { + + @Inject + AccountService accountService; + + @Inject + JournalService journalService; + + protected void verifyAccounts(String... accounts) { + for (String account : accounts) { + accountService.isPresent(account); + } + } + + public abstract JournalEntity makeTransaction(String from, String to, int amount); +} diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferResource.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferResource.java new file mode 100644 index 000000000..589585a8e --- /dev/null +++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferResource.java @@ -0,0 +1,98 @@ +package io.quarkus.ts.transactions; + +import static javax.ws.rs.core.Response.Status.CREATED; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@Path("/transfer") +public class TransferResource { + + @Inject + @Named("TransferTransactionService") + TransferProcessor regularTransaction; + + @Inject + @Named("TransferTopUpService") + TransferProcessor topUp; + + @Inject + @Named("TransferWithdrawalService") + TransferProcessor withdrawal; + + @Inject + JournalService journalService; + + @Inject + AccountService accountService; + + /** + * Transaction represent a transfer funds from one account to another account + * On the journal will look like a transaction with different from / to accounts and the annotation transaction. + */ + @Path("/transaction") + @POST + public Response makeRegularTransaction(TransferDTO transferDTO) { + Long ID = regularTransaction.makeTransaction(transferDTO.getAccountFrom(), transferDTO.getAccountTo(), + transferDTO.getAmount()).getId(); + + return Response.ok(ID).status(CREATED.getStatusCode()).build(); + } + + /** + * TopUp represent a transfer funds transaction to your account, but the money doesn't come from another account. + * On the journal will look like a transaction with the same from / to account and the annotation top-up. + */ + @Path("/top-up") + @POST + public Response topup(TransferDTO transferDTO) { + Long ID = topUp.makeTransaction(transferDTO.getAccountFrom(), transferDTO.getAccountTo(), + transferDTO.getAmount()).getId(); + + return Response.ok(ID).status(CREATED.getStatusCode()).build(); + } + + /** + * Withdrawal represent a take off funds from your account, but you don't transfer this money to another account + * On the journal will look like a transaction with the same from / to accounts and the annotation withdrawal. + */ + @Path("/withdrawal") + @POST + public Response makeMoneyTransaction(TransferDTO transferDTO) { + Long ID = withdrawal.makeTransaction(transferDTO.getAccountFrom(), transferDTO.getAccountTo(), + transferDTO.getAmount()).getId(); + + return Response.ok(ID).status(CREATED.getStatusCode()).build(); + } + + @Path("/accounts/") + @GET + public List getAccounts() { + return accountService.getAllAccounts(); + } + + @Path("/accounts/{account_id}") + @GET + public AccountEntity getAccountById(@PathParam("account_id") String accountNumber) { + return accountService.getAccount(accountNumber); + } + + @Path("/journal/latest/{account_id}") + @GET + public JournalEntity getLatestJournalRecord(@PathParam("account_id") String accountNumber) { + return journalService.getLatestJournalRecordByAccountNumber(accountNumber); + } + +} diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferTopUpService.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferTopUpService.java new file mode 100644 index 000000000..0a2f6be5e --- /dev/null +++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferTopUpService.java @@ -0,0 +1,48 @@ +package io.quarkus.ts.transactions; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Named; + +import org.jboss.logging.Logger; + +import io.micrometer.core.instrument.MeterRegistry; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.narayana.jta.RunOptions; + +@ApplicationScoped +@Named("TransferTopUpService") +public class TransferTopUpService extends TransferProcessor { + private static final Logger LOG = Logger.getLogger(TransferTopUpService.class); + private final static String ANNOTATION_TOP_UP = "user top up"; + private final static int TRANSACTION_TIMEOUT_SEC = 10; + private final MeterRegistry registry; + private long transactionsAmount; + + public TransferTopUpService(MeterRegistry registry) { + this.registry = registry; + registry.gauge("transaction.topup.amount", this, TransferTopUpService::getTransactionsAmount); + } + + public JournalEntity makeTransaction(String from, String to, int amount) { + LOG.infof("TopUp account %s amount %s", from, amount); + verifyAccounts(to); + JournalEntity journal = QuarkusTransaction.call(QuarkusTransaction.runOptions() + .timeout(TRANSACTION_TIMEOUT_SEC) + .exceptionHandler(t -> { + transactionsAmount--; + return RunOptions.ExceptionResult.ROLLBACK; + }) + .semantic(RunOptions.Semantic.REQUIRE_NEW), () -> { + transactionsAmount++; + JournalEntity journalentity = journalService.addToJournal(from, to, ANNOTATION_TOP_UP, amount); + accountService.increaseBalance(from, amount); + return journalentity; + }); + LOG.infof("TopUp completed account %s", from); + return journal; + } + + public long getTransactionsAmount() { + return transactionsAmount; + } +} diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferTransactionService.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferTransactionService.java new file mode 100644 index 000000000..25bcc0110 --- /dev/null +++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferTransactionService.java @@ -0,0 +1,52 @@ +package io.quarkus.ts.transactions; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Named; + +import org.jboss.logging.Logger; + +import io.micrometer.core.instrument.MeterRegistry; +import io.quarkus.narayana.jta.QuarkusTransaction; + +@ApplicationScoped +@Named("TransferTransactionService") +public class TransferTransactionService extends TransferProcessor { + + private static final Logger LOG = Logger.getLogger(TransferTransactionService.class); + private final static String ANNOTATION_TRANSACTION = "user transaction to other user"; + private final static int TRANSACTION_TIMEOUT_SEC = 10; + private final MeterRegistry registry; + private long transactionsAmount; + + public TransferTransactionService(MeterRegistry registry) { + this.registry = registry; + registry.gauge("transaction.regular.amount", this, TransferTransactionService::getTransactionsAmount); + } + + public JournalEntity makeTransaction(String from, String to, int amount) { + JournalEntity journal = null; + LOG.infof("Regular transaction, from %s to %s amount %s", from, to, amount); + try { + // please don't move this gauge after commit statement, because we want to test the gauges after a rollback + transactionsAmount++; + verifyAccounts(from, to); + QuarkusTransaction.begin(QuarkusTransaction.beginOptions().timeout(TRANSACTION_TIMEOUT_SEC)); + journal = journalService.addToJournal(from, to, ANNOTATION_TRANSACTION, amount); + accountService.decreaseBalance(from, amount); + accountService.increaseBalance(to, amount); + QuarkusTransaction.commit(); + LOG.infof("Regular transaction completed, from %s to %s", from, to); + } catch (Exception e) { + LOG.errorf("Error on regular transaction %s ", e.getMessage()); + QuarkusTransaction.rollback(); + transactionsAmount--; + throw e; + } + + return journal; + } + + public long getTransactionsAmount() { + return transactionsAmount; + } +} diff --git a/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferWithdrawalService.java b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferWithdrawalService.java new file mode 100644 index 000000000..f3332a312 --- /dev/null +++ b/sql-db/narayana-transactions/src/main/java/io.quarkus.ts.transactions/TransferWithdrawalService.java @@ -0,0 +1,51 @@ +package io.quarkus.ts.transactions; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Named; + +import org.jboss.logging.Logger; + +import io.micrometer.core.instrument.MeterRegistry; +import io.quarkus.narayana.jta.QuarkusTransaction; + +@ApplicationScoped +@Named("TransferWithdrawalService") +public class TransferWithdrawalService extends TransferProcessor { + + private static final Logger LOG = Logger.getLogger(TransferWithdrawalService.class); + private final static int TRANSACTION_TIMEOUT_SEC = 10; + private final static String ANNOTATION_WITHDRAWAL = "user withdrawal"; + private final MeterRegistry registry; + private long transactionsAmount; + + public TransferWithdrawalService(MeterRegistry registry) { + this.registry = registry; + registry.gauge("transaction.withdrawal.amount", this, TransferWithdrawalService::getTransactionsAmount); + } + + public JournalEntity makeTransaction(String from, String to, int amount) { + JournalEntity journal = null; + try { + LOG.infof("Withdrawal account %s amount %s", from, amount); + // please don't move this gauge after commit statement, because we want to test the gauges after a rollback + transactionsAmount++; + verifyAccounts(from); + QuarkusTransaction.begin(QuarkusTransaction.beginOptions().timeout(TRANSACTION_TIMEOUT_SEC)); + journal = journalService.addToJournal(from, to, ANNOTATION_WITHDRAWAL, amount); + accountService.decreaseBalance(from, amount); + QuarkusTransaction.commit(); + LOG.infof("Withdrawal completed account %s", from); + } catch (Exception e) { + LOG.errorf("Error on withdrawal transaction %s ", e.getMessage()); + QuarkusTransaction.rollback(); + transactionsAmount--; + throw e; + } + + return journal; + } + + public long getTransactionsAmount() { + return transactionsAmount; + } +} diff --git a/sql-db/narayana-transactions/src/main/resources/application.properties b/sql-db/narayana-transactions/src/main/resources/application.properties new file mode 100644 index 000000000..32005d62e --- /dev/null +++ b/sql-db/narayana-transactions/src/main/resources/application.properties @@ -0,0 +1,6 @@ +quarkus.datasource.db-kind=postgresql +quarkus.hibernate-orm.database.charset=utf-8 +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.hibernate-orm.sql-load-script=import.sql +quarkus.opentelemetry.enabled=false +quarkus.application.name=narayanaTransactions diff --git a/sql-db/narayana-transactions/src/main/resources/import.sql b/sql-db/narayana-transactions/src/main/resources/import.sql new file mode 100644 index 000000000..b66c0920b --- /dev/null +++ b/sql-db/narayana-transactions/src/main/resources/import.sql @@ -0,0 +1,6 @@ +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (nextval('hibernate_sequence'), 'Garcilaso', 'de la Vega', 'CZ9250512252717368964232', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (nextval('hibernate_sequence'), 'Miguel', 'de Cervantes', 'SK0389852379529966291984', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (nextval('hibernate_sequence'), 'Luis', 'de Góngora', 'ES8521006742088984966816', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (nextval('hibernate_sequence'), 'Lope', 'de Vega', 'FR9317569000409377431694J37', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (nextval('hibernate_sequence'), 'Francisco', 'Quevedo', 'ES8521006742088984966817', 100, null, CURRENT_TIMESTAMP); + diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MariaDbTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MariaDbTransactionGeneralUsageIT.java new file mode 100644 index 000000000..e2632ace3 --- /dev/null +++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MariaDbTransactionGeneralUsageIT.java @@ -0,0 +1,28 @@ +package io.quarkus.ts.transactions; + +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.test.bootstrap.MariaDbService; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.Container; +import io.quarkus.test.services.QuarkusApplication; + +@QuarkusScenario +@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Windows does not support Linux Containers / Testcontainers (Jaeger)") +public class MariaDbTransactionGeneralUsageIT extends TransactionCommons { + + static final int MARIADB_PORT = 3306; + + @Container(image = "${mariadb.10.image}", port = MARIADB_PORT, expectedLog = "socket: '/run/mysqld/mysqld.sock' port: " + + MARIADB_PORT) + static MariaDbService database = new MariaDbService(); + + @QuarkusApplication + static RestService app = new RestService().withProperties("mariadb_app.properties") + .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl) + .withProperty("quarkus.datasource.username", database.getUser()) + .withProperty("quarkus.datasource.password", database.getPassword()) + .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl); +} diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MssqlTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MssqlTransactionGeneralUsageIT.java new file mode 100644 index 000000000..cfbf0a4b7 --- /dev/null +++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MssqlTransactionGeneralUsageIT.java @@ -0,0 +1,27 @@ +package io.quarkus.ts.transactions; + +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.bootstrap.SqlServerService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.Container; +import io.quarkus.test.services.QuarkusApplication; + +@QuarkusScenario +@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Windows does not support Linux Containers / Testcontainers (Jaeger)") +public class MssqlTransactionGeneralUsageIT extends TransactionCommons { + + private static final int MSSQL_PORT = 1433; + + @Container(image = "${mssql.image}", port = MSSQL_PORT, expectedLog = "Service Broker manager has started") + static SqlServerService database = new SqlServerService(); + + @QuarkusApplication + public static final RestService app = new RestService().withProperties("mssql.properties") + .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl) + .withProperty("quarkus.datasource.username", database.getUser()) + .withProperty("quarkus.datasource.password", database.getPassword()) + .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl); +} diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MysqlTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MysqlTransactionGeneralUsageIT.java new file mode 100644 index 000000000..14e0f4174 --- /dev/null +++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/MysqlTransactionGeneralUsageIT.java @@ -0,0 +1,30 @@ +package io.quarkus.ts.transactions; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.test.bootstrap.MySqlService; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.Container; +import io.quarkus.test.services.QuarkusApplication; + +// TODO https://github.com/quarkus-qe/quarkus-test-suite/issues/756 +@Tag("fips-incompatible") // native-mode +@QuarkusScenario +@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Windows does not support Linux Containers / Testcontainers (Jaeger)") +public class MysqlTransactionGeneralUsageIT extends TransactionCommons { + + static final int MYSQL_PORT = 3306; + + @Container(image = "${mysql.57.image}", port = MYSQL_PORT, expectedLog = "port: " + MYSQL_PORT) + static final MySqlService database = new MySqlService(); + + @QuarkusApplication + static RestService app = new RestService().withProperties("mysql.properties") + .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl) + .withProperty("quarkus.datasource.username", database.getUser()) + .withProperty("quarkus.datasource.password", database.getPassword()) + .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl); +} diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMariaDbTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMariaDbTransactionGeneralUsageIT.java new file mode 100644 index 000000000..fb30e84dc --- /dev/null +++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMariaDbTransactionGeneralUsageIT.java @@ -0,0 +1,26 @@ +package io.quarkus.ts.transactions; + +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +import io.quarkus.test.bootstrap.MariaDbService; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.OpenShiftScenario; +import io.quarkus.test.services.Container; +import io.quarkus.test.services.QuarkusApplication; + +@OpenShiftScenario +@EnabledIfSystemProperty(named = "ts.redhat.registry.enabled", matches = "true") +public class OpenShiftMariaDbTransactionGeneralUsageIT extends TransactionCommons { + + static final int MARIADB_PORT = 3306; + + @Container(image = "${mariadb.105.image}", port = MARIADB_PORT, expectedLog = "Only MySQL server logs after this point") + static MariaDbService database = new MariaDbService(); + + @QuarkusApplication + static RestService app = new RestService().withProperties("mariadb_app.properties") + .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl) + .withProperty("quarkus.datasource.username", database.getUser()) + .withProperty("quarkus.datasource.password", database.getPassword()) + .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl); +} diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMsqlTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMsqlTransactionGeneralUsageIT.java new file mode 100644 index 000000000..f2590784b --- /dev/null +++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMsqlTransactionGeneralUsageIT.java @@ -0,0 +1,25 @@ +package io.quarkus.ts.transactions; + +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +import io.quarkus.test.bootstrap.MySqlService; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.OpenShiftScenario; +import io.quarkus.test.services.Container; +import io.quarkus.test.services.QuarkusApplication; + +@OpenShiftScenario +@EnabledIfSystemProperty(named = "ts.redhat.registry.enabled", matches = "true") +public class OpenShiftMsqlTransactionGeneralUsageIT extends TransactionCommons { + static final int MYSQL_PORT = 3306; + + @Container(image = "${mysql.80.image}", port = MYSQL_PORT, expectedLog = "Only MySQL server logs after this point") + static MySqlService database = new MySqlService(); + + @QuarkusApplication + static RestService app = new RestService().withProperties("mysql.properties") + .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl) + .withProperty("quarkus.datasource.username", database.getUser()) + .withProperty("quarkus.datasource.password", database.getPassword()) + .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl); +} diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMssqlTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMssqlTransactionGeneralUsageIT.java new file mode 100644 index 000000000..def63bff2 --- /dev/null +++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftMssqlTransactionGeneralUsageIT.java @@ -0,0 +1,10 @@ +package io.quarkus.ts.transactions; + +import org.junit.jupiter.api.Disabled; + +import io.quarkus.test.scenarios.OpenShiftScenario; + +@OpenShiftScenario +@Disabled("https://github.com/microsoft/mssql-docker/issues/769") +public class OpenShiftMssqlTransactionGeneralUsageIT extends MssqlTransactionGeneralUsageIT { +} diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftOracleTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftOracleTransactionGeneralUsageIT.java new file mode 100644 index 000000000..2c4b89183 --- /dev/null +++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftOracleTransactionGeneralUsageIT.java @@ -0,0 +1,10 @@ +package io.quarkus.ts.transactions; + +import org.junit.jupiter.api.Disabled; + +import io.quarkus.test.scenarios.OpenShiftScenario; + +@OpenShiftScenario +@Disabled("https://github.com/quarkus-qe/quarkus-test-suite/issues/246") +public class OpenShiftOracleTransactionGeneralUsageIT extends OracleTransactionGeneralUsageIT { +} diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftPostgresqlTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftPostgresqlTransactionGeneralUsageIT.java new file mode 100644 index 000000000..5461f9200 --- /dev/null +++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OpenShiftPostgresqlTransactionGeneralUsageIT.java @@ -0,0 +1,7 @@ +package io.quarkus.ts.transactions; + +import io.quarkus.test.scenarios.OpenShiftScenario; + +@OpenShiftScenario +public class OpenShiftPostgresqlTransactionGeneralUsageIT extends PostgresqlTransactionGeneralUsageIT { +} diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OracleTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OracleTransactionGeneralUsageIT.java new file mode 100644 index 000000000..ede185809 --- /dev/null +++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/OracleTransactionGeneralUsageIT.java @@ -0,0 +1,27 @@ +package io.quarkus.ts.transactions; + +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.test.bootstrap.OracleService; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.Container; +import io.quarkus.test.services.QuarkusApplication; + +@QuarkusScenario +@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Windows does not support Linux Containers / Testcontainers (Jaeger)") +public class OracleTransactionGeneralUsageIT extends TransactionCommons { + + static final int ORACLE_PORT = 1521; + + @Container(image = "${oracle.image}", port = ORACLE_PORT, expectedLog = "DATABASE IS READY TO USE!") + static OracleService database = new OracleService(); + + @QuarkusApplication + static RestService app = new RestService().withProperties("oracle.properties") + .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl) + .withProperty("quarkus.datasource.username", database.getUser()) + .withProperty("quarkus.datasource.password", database.getPassword()) + .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl); +} diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/PostgresqlTransactionGeneralUsageIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/PostgresqlTransactionGeneralUsageIT.java new file mode 100644 index 000000000..7a41f86fc --- /dev/null +++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/PostgresqlTransactionGeneralUsageIT.java @@ -0,0 +1,28 @@ +package io.quarkus.ts.transactions; + +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.test.bootstrap.PostgresqlService; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.Container; +import io.quarkus.test.services.QuarkusApplication; + +@QuarkusScenario +@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Windows does not support Linux Containers / Testcontainers (Jaeger)") +public class PostgresqlTransactionGeneralUsageIT extends TransactionCommons { + + static final int POSTGRESQL_PORT = 5432; + + @Container(image = "${postgresql.latest.image}", port = POSTGRESQL_PORT, expectedLog = "listening on IPv4 address") + static final PostgresqlService database = new PostgresqlService().withProperty("PGDATA", "/tmp/psql"); + + @QuarkusApplication + public static final RestService app = new RestService() + .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl) + .withProperty("quarkus.opentelemetry.enabled", "true") + .withProperty("quarkus.datasource.username", database.getUser()) + .withProperty("quarkus.datasource.password", database.getPassword()) + .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl); +} diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/SwaggerUiIT.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/SwaggerUiIT.java new file mode 100644 index 000000000..bacb53d14 --- /dev/null +++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/SwaggerUiIT.java @@ -0,0 +1,28 @@ +package io.quarkus.ts.transactions; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.containsString; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.scenarios.annotations.DisabledOnNative; +import io.quarkus.test.services.DevModeQuarkusApplication; + +@QuarkusScenario +@DisabledOnNative +public class SwaggerUiIT { + + @DevModeQuarkusApplication + static RestService app = new RestService(); + + @Test + public void smokeTestSwaggerUi() { + given() + .when().get("/q/swagger-ui") + .then() + .statusCode(200) + .body(containsString("/openapi")); + } +} diff --git a/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/TransactionCommons.java b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/TransactionCommons.java new file mode 100644 index 000000000..4cc66a33b --- /dev/null +++ b/sql-db/narayana-transactions/src/test/java/io/quarkus/ts/transactions/TransactionCommons.java @@ -0,0 +1,208 @@ +package io.quarkus.ts.transactions; + +import static io.restassured.RestAssured.given; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.containsInAnyOrder; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.JaegerService; +import io.quarkus.test.services.JaegerContainer; +import io.restassured.http.ContentType; +import io.restassured.response.Response; + +public abstract class TransactionCommons { + + static final String ACCOUNT_NUMBER_MIGUEL = "SK0389852379529966291984"; + static final String ACCOUNT_NUMBER_GARCILASO = "FR9317569000409377431694J37"; + static final String ACCOUNT_NUMBER_LUIS = "ES8521006742088984966816"; + static final String ACCOUNT_NUMBER_LOPE = "CZ9250512252717368964232"; + static final String ACCOUNT_NUMBER_FRANCISCO = "ES8521006742088984966817"; + static final int ASSERT_SERVICE_TIMEOUT_MINUTES = 1; + private Response jaegerResponse; + + @JaegerContainer(useOtlpCollector = true, expectedLog = "\"Health Check state change\",\"status\":\"ready\"") + static final JaegerService jaeger = new JaegerService(); + + @Tag("QUARKUS-2492") + @Test + public void verifyNarayanaProgrammaticApproachTransaction() { + TransferDTO transferDTO = new TransferDTO(); + transferDTO.setAccountFrom(ACCOUNT_NUMBER_MIGUEL); + transferDTO.setAccountTo(ACCOUNT_NUMBER_LOPE); + transferDTO.setAmount(100); + + given() + .contentType(ContentType.JSON) + .body(transferDTO).post("/transfer/transaction") + .then().statusCode(HttpStatus.SC_CREATED); + + AccountEntity miguelAccount = getAccount(ACCOUNT_NUMBER_MIGUEL); + Assertions.assertEquals(0, miguelAccount.getAmount(), "Unexpected amount on source account."); + + AccountEntity lopeAccount = getAccount(ACCOUNT_NUMBER_LOPE); + Assertions.assertEquals(200, lopeAccount.getAmount(), "Unexpected amount on source account."); + + JournalEntity miguelJournal = getLatestJournalRecord(ACCOUNT_NUMBER_MIGUEL); + Assertions.assertEquals(100, miguelJournal.getAmount(), "Unexpected journal amount."); + } + + @Tag("QUARKUS-2492") + @Test + public void verifyNarayanaLambdaApproachTransaction() { + TransferDTO transferDTO = new TransferDTO(); + transferDTO.setAccountFrom(ACCOUNT_NUMBER_GARCILASO); + transferDTO.setAccountTo(ACCOUNT_NUMBER_GARCILASO); + transferDTO.setAmount(100); + + given() + .contentType(ContentType.JSON) + .body(transferDTO).post("/transfer/top-up") + .then().statusCode(HttpStatus.SC_CREATED); + + AccountEntity garcilasoAccount = getAccount(ACCOUNT_NUMBER_GARCILASO); + Assertions.assertEquals(200, garcilasoAccount.getAmount(), + "Unexpected account amount. Expected 200 found " + garcilasoAccount.getAmount()); + + JournalEntity garcilasoJournal = getLatestJournalRecord(ACCOUNT_NUMBER_GARCILASO); + Assertions.assertEquals(100, garcilasoJournal.getAmount(), "Unexpected journal amount."); + } + + @Tag("QUARKUS-2492") + @Test + public void verifyRollbackForNarayanaProgrammaticApproach() { + TransferDTO transferDTO = new TransferDTO(); + transferDTO.setAccountFrom(ACCOUNT_NUMBER_LUIS); + transferDTO.setAccountTo(ACCOUNT_NUMBER_LUIS); + transferDTO.setAmount(200); + + given() + .contentType(ContentType.JSON) + .body(transferDTO).post("/transfer/withdrawal") + .then().statusCode(HttpStatus.SC_BAD_REQUEST); + + AccountEntity luisAccount = getAccount(ACCOUNT_NUMBER_LUIS); + Assertions.assertEquals(100, luisAccount.getAmount(), "Unexpected account amount."); + + given().get("/transfer/journal/latest/" + ACCOUNT_NUMBER_LUIS) + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + } + + @Tag("QUARKUS-2492") + @Test + public void smokeTestNarayanaProgrammaticTransactionTrace() { + String operationName = "/transfer/accounts/{account_id}"; + given().get("/transfer/accounts/" + ACCOUNT_NUMBER_LUIS).then().statusCode(HttpStatus.SC_OK); + verifyRestRequestTraces(operationName); + } + + @Tag("QUARKUS-2492") + @Test + public void smokeTestMetricsNarayanaProgrammaticTransaction() { + String metricName = "transaction_withdrawal_amount"; + TransferDTO transferDTO = new TransferDTO(); + transferDTO.setAccountFrom(ACCOUNT_NUMBER_FRANCISCO); + transferDTO.setAccountTo(ACCOUNT_NUMBER_FRANCISCO); + transferDTO.setAmount(20); + + given() + .contentType(ContentType.JSON) + .body(transferDTO).post("/transfer/withdrawal") + .then().statusCode(HttpStatus.SC_CREATED); + + verifyMetrics(metricName, greater(0)); + + // check rollback gauge + transferDTO.setAmount(3000); + double beforeRollback = getMetricsValue(metricName); + given() + .contentType(ContentType.JSON) + .body(transferDTO).post("/transfer/withdrawal") + .then().statusCode(HttpStatus.SC_BAD_REQUEST); + double afterRollback = getMetricsValue(metricName); + Assertions.assertEquals(beforeRollback, afterRollback, "Gauge should not be increased on a rollback transaction"); + } + + private AccountEntity getAccount(String accountNumber) { + return given().get("/transfer/accounts/" + accountNumber) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body().as(AccountEntity.class); + } + + private void verifyRestRequestTraces(String operationName) { + String[] operations = new String[] { operationName }; + await().atMost(1, TimeUnit.MINUTES).pollInterval(Duration.ofSeconds(5)).untilAsserted(() -> { + retrieveTraces(20, "1h", "narayanaTransactions", operationName); + jaegerResponse.then().body("data[0].spans.operationName", containsInAnyOrder(operations)); + }); + } + + private JournalEntity getLatestJournalRecord(String accountNumber) { + return given().get("/transfer/journal/latest/" + accountNumber) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body().as(JournalEntity.class); + } + + private void retrieveTraces(int pageLimit, String lookBack, String serviceName, String operationName) { + jaegerResponse = given().when() + .log().uri() + .queryParam("operation", operationName) + .queryParam("lookback", lookBack) + .queryParam("limit", pageLimit) + .queryParam("service", serviceName) + .get(jaeger.getTraceUrl()); + } + + private void verifyMetrics(String name, Predicate valueMatcher) { + await().ignoreExceptions().atMost(ASSERT_SERVICE_TIMEOUT_MINUTES, TimeUnit.MINUTES).untilAsserted(() -> { + String response = given().get("/q/metrics").then() + .statusCode(HttpStatus.SC_OK) + .extract().asString(); + + boolean matches = false; + for (String line : response.split("[\r\n]+")) { + if (line.startsWith(name)) { + Double value = extractValueFromMetric(line); + Assertions.assertTrue(valueMatcher.test(value), "Metric " + name + " has unexpected value " + value); + matches = true; + break; + } + } + + Assertions.assertTrue(matches, "Metric " + name + " not found in " + response); + }); + } + + private Double getMetricsValue(String name) { + String response = given().get("/q/metrics").then().statusCode(HttpStatus.SC_OK).extract().asString(); + for (String line : response.split("[\r\n]+")) { + if (line.startsWith(name)) { + return extractValueFromMetric(line); + } + } + + Assertions.fail("Metrics property " + name + " not found."); + return 0d; + } + + private Double extractValueFromMetric(String line) { + return Double.parseDouble(line.substring(line.lastIndexOf(" "))); + } + + private Predicate greater(double expected) { + return actual -> actual > expected; + } + +} diff --git a/sql-db/narayana-transactions/src/test/resources/container-license-acceptance.txt b/sql-db/narayana-transactions/src/test/resources/container-license-acceptance.txt new file mode 100644 index 000000000..1457cfad7 --- /dev/null +++ b/sql-db/narayana-transactions/src/test/resources/container-license-acceptance.txt @@ -0,0 +1,5 @@ +mcr.microsoft.com/mssql/server:2017-CU12 +mcr.microsoft.com/mssql/server:2019-CU10-ubuntu-20.04 +mcr.microsoft.com/mssql/server:2019-CU15-ubuntu-20.04 +mcr.microsoft.com/mssql/server:2019-latest +mcr.microsoft.com/mssql/rhel/server:2022-latest diff --git a/sql-db/narayana-transactions/src/test/resources/mariadb_app.properties b/sql-db/narayana-transactions/src/test/resources/mariadb_app.properties new file mode 100644 index 000000000..83ef7569d --- /dev/null +++ b/sql-db/narayana-transactions/src/test/resources/mariadb_app.properties @@ -0,0 +1,6 @@ +quarkus.datasource.db-kind=mariadb +quarkus.hibernate-orm.dialect=org.hibernate.dialect.MariaDB102Dialect +quarkus.hibernate-orm.sql-load-script=mariadb_import.sql +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.opentelemetry.enabled=true +quarkus.application.name=narayanaTransactions \ No newline at end of file diff --git a/sql-db/narayana-transactions/src/test/resources/mariadb_import.sql b/sql-db/narayana-transactions/src/test/resources/mariadb_import.sql new file mode 100644 index 000000000..a4bf1ead2 --- /dev/null +++ b/sql-db/narayana-transactions/src/test/resources/mariadb_import.sql @@ -0,0 +1,7 @@ +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (0, 'Garcilaso', 'de la Vega', 'CZ9250512252717368964232', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (1, 'Miguel', 'de Cervantes', 'SK0389852379529966291984', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (2, 'Luis', 'de Góngora', 'ES8521006742088984966816', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (3, 'Lope', 'de Vega', 'FR9317569000409377431694J37', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (5, 'Francisco', 'Quevedo', 'ES8521006742088984966817', 100, null, CURRENT_TIMESTAMP); + +UPDATE hibernate_sequence SET next_val = 5; diff --git a/sql-db/narayana-transactions/src/test/resources/mssql.properties b/sql-db/narayana-transactions/src/test/resources/mssql.properties new file mode 100644 index 000000000..895f040a0 --- /dev/null +++ b/sql-db/narayana-transactions/src/test/resources/mssql.properties @@ -0,0 +1,7 @@ +quarkus.datasource.db-kind=mssql +quarkus.hibernate-orm.dialect=org.hibernate.dialect.SQLServer2012Dialect +quarkus.hibernate-orm.sql-load-script=mssql_import.sql +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.datasource.jdbc.additional-jdbc-properties.trustservercertificate=true +quarkus.opentelemetry.enabled=true +quarkus.application.name=narayanaTransactions \ No newline at end of file diff --git a/sql-db/narayana-transactions/src/test/resources/mssql_import.sql b/sql-db/narayana-transactions/src/test/resources/mssql_import.sql new file mode 100644 index 000000000..bc9fd902c --- /dev/null +++ b/sql-db/narayana-transactions/src/test/resources/mssql_import.sql @@ -0,0 +1,6 @@ +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (NEXT VALUE FOR hibernate_sequence, 'Garcilaso', 'de la Vega', 'CZ9250512252717368964232', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (NEXT VALUE FOR hibernate_sequence, 'Miguel', 'de Cervantes', 'SK0389852379529966291984', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (NEXT VALUE FOR hibernate_sequence, 'Luis', 'de Góngora', 'ES8521006742088984966816', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (NEXT VALUE FOR hibernate_sequence, 'Lope', 'de Vega', 'FR9317569000409377431694J37', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (NEXT VALUE FOR hibernate_sequence, 'Francisco', 'Quevedo', 'ES8521006742088984966817', 100, null, CURRENT_TIMESTAMP); + diff --git a/sql-db/narayana-transactions/src/test/resources/mysql.properties b/sql-db/narayana-transactions/src/test/resources/mysql.properties new file mode 100644 index 000000000..e55179f53 --- /dev/null +++ b/sql-db/narayana-transactions/src/test/resources/mysql.properties @@ -0,0 +1,6 @@ +quarkus.datasource.db-kind=mysql +quarkus.hibernate-orm.dialect=org.hibernate.dialect.MariaDB102Dialect +quarkus.hibernate-orm.sql-load-script=mariadb_import.sql +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.opentelemetry.enabled=true +quarkus.application.name=narayanaTransactions diff --git a/sql-db/narayana-transactions/src/test/resources/oracle.properties b/sql-db/narayana-transactions/src/test/resources/oracle.properties new file mode 100644 index 000000000..e6699024d --- /dev/null +++ b/sql-db/narayana-transactions/src/test/resources/oracle.properties @@ -0,0 +1,5 @@ +quarkus.datasource.db-kind=oracle +quarkus.hibernate-orm.sql-load-script=oracle_import.sql +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.opentelemetry.enabled=true +quarkus.application.name=narayanaTransactions \ No newline at end of file diff --git a/sql-db/narayana-transactions/src/test/resources/oracle_import.sql b/sql-db/narayana-transactions/src/test/resources/oracle_import.sql new file mode 100644 index 000000000..8262c6fa8 --- /dev/null +++ b/sql-db/narayana-transactions/src/test/resources/oracle_import.sql @@ -0,0 +1,5 @@ +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (hibernate_sequence.NEXTVAL, 'Garcilaso', 'de la Vega', 'CZ9250512252717368964232', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (hibernate_sequence.NEXTVAL, 'Miguel', 'de Cervantes', 'SK0389852379529966291984', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (hibernate_sequence.NEXTVAL, 'Luis', 'de Góngora', 'ES8521006742088984966816', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (hibernate_sequence.NEXTVAL, 'Lope', 'de Vega', 'FR9317569000409377431694J37', 100, null, CURRENT_TIMESTAMP); +INSERT INTO account (id, name, lastName, accountNumber, amount, updatedAt, createdAt) VALUES (hibernate_sequence.NEXTVAL, 'Francisco', 'Quevedo', 'ES8521006742088984966817', 100, null, CURRENT_TIMESTAMP); diff --git a/sql-db/narayana-transactions/src/test/resources/test.properties b/sql-db/narayana-transactions/src/test/resources/test.properties new file mode 100644 index 000000000..217165f4b --- /dev/null +++ b/sql-db/narayana-transactions/src/test/resources/test.properties @@ -0,0 +1,4 @@ +ts.app.log.enable=true +ts.postgresql.log.enable=true +ts.database.openshift.use-internal-service-as-url=true +ts.database.container.delete.image.on.stop=false \ No newline at end of file