From fa5a39147d23f22dff2c4848e55e78d35c9d34f7 Mon Sep 17 00:00:00 2001 From: Pablo Gonzalez Granados Date: Wed, 1 Feb 2023 15:18:46 +0100 Subject: [PATCH] [2.13] Backport several scenarios related to HTTP, SQL and Monitoring modules (#1021) * Test Extended Architecture (XA) connection Provides coverage for https://issues.redhat.com/browse/QUARKUS-2742 (cherry picked from commit ce15b260389bfc215f5c9ef02d538a9861d8e2ab) * Improve reactive rest client "process paths before sub resources" scenario (#995) There is an issue in upstream (https://github.com/quarkusio/quarkus/pull/29821) that only happens under some RestClient definition hierarchy. This commit reproduces the problem in Quarkus 2.12 and earlier versions (cherry picked from commit 2350b464b2a1b0644a853cc8212e29e1add564ad) * New Scenario: RequestScope custom context was removed after `javax.enterprise` event propagation When firing an async CDI Event, the requestScope context from the emitter briefly exists for the observer and is then terminated. This commit is a reproducer of this scenario that happens on Quarkus 2.13.0.Final (cherry picked from commit ef3eed6020bc24668685808a81db223f12a11c6d) * gRPC and SSE coverage for OpenTelemetry (cherry picked from commit f327c602ba563ec01cc8297fc7554bd7c9b07ed7) * Add coverage to eventbus '@ConsumeEvent' annotation (cherry picked from commit 7da0d4ba3b0bac204987397e17c969c3b54d6535) * Drop duplicated definition of quarkus-opentelemetry (cherry picked from commit b0cba7f3cdfd731eece4cee7b6dfad3da61320b1) * OutboundSseEvent is not correctly serialized (cherry picked from commit 99b5eb14b7b51ead332fceaec789d9188686e4e7) * Check, that dev-mode is omitted on projects with pom packaging Required for QUARKUS-2757 (cherry picked from commit 4fd38c7a7ae48f129bffca0688faff3ddc780bf3) * Refactoring of QUARKUS-2748: adding http test and improving error messages (cherry picked from commit 656343178d0c2309303ed9d75c41fd0b7b781237) * Add test for security annotations in rest-data-panache (#994) Quarkus extensions based on `rest-data-panache` support propagation of security annotations into generated JAX-RS resources. These tests provide coverage of this feature for extensions: - `quarkus-hibernate-orm-rest-data-panache` - `quarkus-spring-data-rest` See also related test plan: - https://github.com/quarkus-qe/quarkus-test-plans/blob/main/QUARKUS-2788.md (cherry picked from commit 3f7a4c8d6ac3d073d18f3dbc4d172a01ac19cbcb) * Add transaction-API classic scenario (cherry picked from commit 509d4911b98023e6760aec7dbaa136de74e842a7) * Cover Vert.X-specific metrics. (#1019) Add new module, which uses Vert.X-based HTTP server Verify, that it works, and that it creates all required metrics Required for https://issues.redhat.com/browse/QUARKUS-2829 (cherry picked from commit 818837f486b43675f1d155852fd249442e320b49) * Add missing opentelemetry exporter to Narayana scenario --------- Co-authored-by: Fedor Dudinskiy Co-authored-by: Rostislav Svoboda Co-authored-by: Josef Smrcka --- README.md | 14 ++ .../src/main/resources/application.properties | 2 +- .../advanced/reactive/AbstractDevModeIT.java | 113 ++++++++++ .../http/advanced/reactive/DevModeHttpIT.java | 28 +++ .../advanced/reactive/DevModeHttpsIT.java | 90 +------- http/vertx/pom.xml | 38 ++++ .../main/java/io/quarkus/ts/vertx/Hello.java | 14 ++ .../io/quarkus/ts/vertx/HelloResource.java | 33 +++ .../src/main/resources/application.properties | 6 + .../vertx/src/main/resources/message.template | 1 + .../io/quarkus/ts/vertx/AbstractVertxIT.java | 121 ++++++++++ .../io/quarkus/ts/vertx/LocalVertxIT.java | 18 ++ ...ngExtensionDockerBuildStrategyVertxIT.java | 21 ++ .../vertx/OpenShiftUsingExtensionVertxIT.java | 21 ++ .../io/quarkus/ts/vertx/OpenShiftVertxIT.java | 17 ++ ...onDockerBuildStrategyOpenShiftVertxIT.java | 22 ++ .../ServerlessExtensionOpenShiftVertxIT.java | 22 ++ monitoring/opentelemetry-reactive/pom.xml | 48 ++++ .../reactive/grpc/GrpcPingResource.java | 28 +++ .../reactive/grpc/GrpcPongResource.java | 34 +++ .../reactive/grpc/GrpcPongService.java | 34 +++ .../sse/ServerSentEventsPingResource.java | 46 ++++ .../sse/ServerSentEventsPongClient.java | 19 ++ .../sse/ServerSentEventsPongResource.java | 20 ++ .../reactive/traceable/TraceableResource.java | 28 +++ .../src/main/proto/pong.proto | 26 +++ .../src/main/resources/application.properties | 7 + .../reactive/OpenTelemetryGrpcIT.java | 65 ++++++ .../reactive/OpenTelemetrySseIT.java | 79 +++++++ monitoring/opentelemetry/pom.xml | 56 +++++ .../opentelemetry/grpc/GrpcPingResource.java | 28 +++ .../opentelemetry/grpc/GrpcPongResource.java | 34 +++ .../opentelemetry/grpc/GrpcPongService.java | 34 +++ .../sse/ServerSentEventsPingResource.java | 27 +++ .../sse/ServerSentEventsPongClient.java | 19 ++ .../sse/ServerSentEventsPongResource.java | 20 ++ .../traceable/TraceableResource.java | 28 +++ .../opentelemetry/src/main/proto/pong.proto | 26 +++ .../src/main/resources/application.properties | 7 + .../ts/opentelemetry/OpenTelemetryGrpcIT.java | 65 ++++++ .../ts/opentelemetry/OpenTelemetrySseIT.java | 70 ++++++ pom.xml | 3 + .../cli/QuarkusCliCreateJvmApplicationIT.java | 27 ++- .../src/test/resources/test.properties | 3 + .../ts/security/vertx/Application.java | 22 +- .../ts/security/vertx/CommonApplication.java | 8 + .../ts/security/vertx/model/HelloEvent.java | 14 ++ .../security/vertx/BladeRunnerHandlerIT.java | 17 ++ spring/spring-data/pom.xml | 4 + .../data/rest/secured/DenyAllRepository.java | 27 +++ .../rest/secured/PermitAllRepository.java | 21 ++ .../data/rest/secured/PublicRepository.java | 19 ++ .../rest/secured/RolesAllowedRepository.java | 19 ++ .../src/main/resources/application.properties | 9 + ...ShiftSecuredRepositoryRestResourcesIT.java | 7 + .../SecuredRepositoryRestResourcesIT.java | 160 ++++++++++++++ sql-db/narayana-transactions/pom.xml | 112 ++++++++++ .../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 + sql-db/panache-flyway/pom.xml | 4 + .../secured/EntityDenyAllResource.java | 30 +++ .../secured/EntityPermitAllResource.java | 16 ++ .../secured/EntityPublicResource.java | 14 ++ ...esourcePropertiesRolesAllowedResource.java | 9 + .../secured/EntityRolesAllowedResource.java | 12 + .../secured/RepositoryDenyAllResource.java | 31 +++ .../secured/RepositoryPermitAllResource.java | 17 ++ .../secured/RepositoryPublicResource.java | 15 ++ ...esourcePropertiesRolesAllowedResource.java | 11 + .../RepositoryRolesAllowedResource.java | 13 ++ .../src/main/resources/application.properties | 9 + .../AbstractSecuredPanacheResourceIT.java | 176 +++++++++++++++ ...enShiftSecuredPanacheEntityResourceIT.java | 7 + ...iftSecuredPanacheRepositoryResourceIT.java | 7 + .../SecuredPanacheEntityResourceIT.java | 16 ++ .../SecuredPanacheRepositoryResourceIT.java | 16 ++ 107 files changed, 3312 insertions(+), 84 deletions(-) create mode 100644 http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/AbstractDevModeIT.java create mode 100644 http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeHttpIT.java create mode 100644 http/vertx/pom.xml create mode 100644 http/vertx/src/main/java/io/quarkus/ts/vertx/Hello.java create mode 100644 http/vertx/src/main/java/io/quarkus/ts/vertx/HelloResource.java create mode 100644 http/vertx/src/main/resources/application.properties create mode 100644 http/vertx/src/main/resources/message.template create mode 100644 http/vertx/src/test/java/io/quarkus/ts/vertx/AbstractVertxIT.java create mode 100644 http/vertx/src/test/java/io/quarkus/ts/vertx/LocalVertxIT.java create mode 100644 http/vertx/src/test/java/io/quarkus/ts/vertx/OpenShiftUsingExtensionDockerBuildStrategyVertxIT.java create mode 100644 http/vertx/src/test/java/io/quarkus/ts/vertx/OpenShiftUsingExtensionVertxIT.java create mode 100644 http/vertx/src/test/java/io/quarkus/ts/vertx/OpenShiftVertxIT.java create mode 100644 http/vertx/src/test/java/io/quarkus/ts/vertx/ServerlessExtensionDockerBuildStrategyOpenShiftVertxIT.java create mode 100644 http/vertx/src/test/java/io/quarkus/ts/vertx/ServerlessExtensionOpenShiftVertxIT.java create mode 100644 monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/grpc/GrpcPingResource.java create mode 100644 monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/grpc/GrpcPongResource.java create mode 100644 monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/grpc/GrpcPongService.java create mode 100644 monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/sse/ServerSentEventsPingResource.java create mode 100644 monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/sse/ServerSentEventsPongClient.java create mode 100644 monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/sse/ServerSentEventsPongResource.java create mode 100644 monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/traceable/TraceableResource.java create mode 100644 monitoring/opentelemetry-reactive/src/main/proto/pong.proto create mode 100644 monitoring/opentelemetry-reactive/src/test/java/io/quarkus/ts/opentelemetry/reactive/OpenTelemetryGrpcIT.java create mode 100644 monitoring/opentelemetry-reactive/src/test/java/io/quarkus/ts/opentelemetry/reactive/OpenTelemetrySseIT.java create mode 100644 monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/grpc/GrpcPingResource.java create mode 100644 monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/grpc/GrpcPongResource.java create mode 100644 monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/grpc/GrpcPongService.java create mode 100644 monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/sse/ServerSentEventsPingResource.java create mode 100644 monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/sse/ServerSentEventsPongClient.java create mode 100644 monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/sse/ServerSentEventsPongResource.java create mode 100644 monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/traceable/TraceableResource.java create mode 100644 monitoring/opentelemetry/src/main/proto/pong.proto create mode 100644 monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryGrpcIT.java create mode 100644 monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetrySseIT.java create mode 100644 security/vertx-jwt/src/main/java/io/quarkus/ts/security/vertx/CommonApplication.java create mode 100644 security/vertx-jwt/src/main/java/io/quarkus/ts/security/vertx/model/HelloEvent.java create mode 100644 spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/DenyAllRepository.java create mode 100644 spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/PermitAllRepository.java create mode 100644 spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/PublicRepository.java create mode 100644 spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/RolesAllowedRepository.java create mode 100644 spring/spring-data/src/test/java/io/quarkus/ts/spring/data/rest/secured/OpenShiftSecuredRepositoryRestResourcesIT.java create mode 100644 spring/spring-data/src/test/java/io/quarkus/ts/spring/data/rest/secured/SecuredRepositoryRestResourcesIT.java 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 create mode 100644 sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityDenyAllResource.java create mode 100644 sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityPermitAllResource.java create mode 100644 sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityPublicResource.java create mode 100644 sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityResourcePropertiesRolesAllowedResource.java create mode 100644 sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityRolesAllowedResource.java create mode 100644 sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryDenyAllResource.java create mode 100644 sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryPermitAllResource.java create mode 100644 sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryPublicResource.java create mode 100644 sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryResourcePropertiesRolesAllowedResource.java create mode 100644 sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryRolesAllowedResource.java create mode 100644 sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/AbstractSecuredPanacheResourceIT.java create mode 100644 sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/OpenShiftSecuredPanacheEntityResourceIT.java create mode 100644 sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/OpenShiftSecuredPanacheRepositoryResourceIT.java create mode 100644 sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/SecuredPanacheEntityResourceIT.java create mode 100644 sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/SecuredPanacheRepositoryResourceIT.java diff --git a/README.md b/README.md index d64fab6bd..fb3f68a08 100644 --- a/README.md +++ b/README.md @@ -349,6 +349,13 @@ Exclusions: XML test. Reason: https://quarkus.io/blog/resteasy-reactive/#what-ja Verifies HTTP endpoints validation using `quarkus-hibernate-validator` works correctly in Resteasy Classic and Resteasy Reactive. This module will setup a simple endpoint and will validate the right message format is set when there are validation errors. +### `http/vertx` +Verifies that you can deploy a simple Vert.X-based HTTP endpoint to OpenShift, access it and retrieve metrics for it. +It also verifies multiple deployment strategies like: +- Serverless +- Using OpenShift quarkus extension +- Using OpenShift quarkus extension and Docker Build strategy + #### Additions * *@Deprecated* annotation has been added for test regression purposes to ensure `java.lang` annotations are allowed for resources * Resource with multipart body support, provided parts are text, image and binary data, charset checked with `us-ascii` and `utf-8` @@ -538,6 +545,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/http/http-advanced-reactive/src/main/resources/application.properties b/http/http-advanced-reactive/src/main/resources/application.properties index 59ebbc627..1ec580cee 100644 --- a/http/http-advanced-reactive/src/main/resources/application.properties +++ b/http/http-advanced-reactive/src/main/resources/application.properties @@ -68,4 +68,4 @@ pl-container-request-filter.enabled=false quarkus.index-dependency.resteasy-multipart.group-id=org.jboss.resteasy quarkus.index-dependency.resteasy-multipart.artifact-id=resteasy-multipart-provider -quarkus.qe.test.value=42 +qe.test.value=42 diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/AbstractDevModeIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/AbstractDevModeIT.java new file mode 100644 index 000000000..c97ba46b0 --- /dev/null +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/AbstractDevModeIT.java @@ -0,0 +1,113 @@ +package io.quarkus.ts.http.advanced.reactive; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jboss.logging.Logger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.DomElement; +import com.gargoylesoftware.htmlunit.html.HtmlPage; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.services.URILike; +import io.quarkus.test.utils.AwaitilityUtils; + +public abstract class AbstractDevModeIT { + private static final Logger LOG = Logger.getLogger(AbstractDevModeIT.class); + protected static final String PROPERTY = "qe.test.value"; + protected WebClient webClient; + + @BeforeEach + void setUp() { + webClient = new WebClient(); + + webClient.getOptions().setRedirectEnabled(true); //required for the test case + //The execution breaks without the option below + webClient.getOptions().setWebSocketEnabled(false); + + //make sure, that the cache doesn't affect us + webClient.getCookieManager().clearCookies(); + webClient.getCookieManager().setCookiesEnabled(false); + webClient.getCache().clear(); + webClient.getCache().setMaxSize(0); + + //disable everything, that we don't need + webClient.getOptions().setDownloadImages(false); + webClient.getOptions().setGeolocationEnabled(false); + webClient.getOptions().setAppletEnabled(false); + webClient.getOptions().setCssEnabled(false); + } + + @Test + @Disabled("https://github.com/quarkusio/quarkus/issues/30511") + public void uiChange() throws IOException { + RestService app = getApp(); + URILike uri = getUri(); + + HtmlPage before = webClient.getPage(uri.withPath("/q/dev/io.quarkus.quarkus-vertx-http/config").toString()); + QuarkusUIField field = new QuarkusUIField(before.getElementById(PROPERTY)); + assertEquals("42", field.getValue(), "Wrong initial value shown in UI!"); + assertEquals("42", app.getProperty(PROPERTY, ""), "Properties contain wrong initial value!"); + + field.setValue("23"); + HtmlPage saved = field.getSaveButton().click(); + QuarkusUIField updated = new QuarkusUIField(saved.getElementById(PROPERTY)); + assertEquals("23", updated.getValue(), "The value was not updated in UI"); + + AwaitilityUtils.untilIsTrue(() -> app.getLogs().stream().anyMatch(log -> log.contains("File change detected"))); + try (Stream lines = Files + .lines(app.getServiceFolder().resolve("src/main/resources/application.properties"))) { + List properties = lines + .filter(line -> line.contains(PROPERTY)) + .collect(Collectors.toList()); + if (properties.size() != 1) { + LOG.warn("There should be only one property with name " + PROPERTY + ", but found these " + properties); + } + } + assertEquals("23", app.getProperty(PROPERTY, ""), "Wrong value was read from application properties"); + } + + protected abstract URILike getUri(); + + protected abstract RestService getApp(); + + @AfterEach + void tearDown() { + webClient.close(); + } + + protected class QuarkusUIField { + private final DomElement element; + + public QuarkusUIField(DomElement element) { + this.element = element; + } + + public String getValue() { + return element.getAttribute("value"); + } + + public void setValue(String newValue) { + element.setAttribute("value", newValue); + } + + public DomElement getSaveButton() { + for (DomElement sibling : element.getParentNode().getDomElementDescendants()) { + if (sibling.getAttribute("class").equals("input-group-text formInputButton")) { + return sibling; + } + } + throw new IllegalStateException("Save button was not found!"); + } + } +} diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeHttpIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeHttpIT.java new file mode 100644 index 000000000..16c3c37a5 --- /dev/null +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeHttpIT.java @@ -0,0 +1,28 @@ +package io.quarkus.ts.http.advanced.reactive; + +import io.quarkus.test.bootstrap.DevModeQuarkusService; +import io.quarkus.test.bootstrap.Protocol; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.DevModeQuarkusApplication; +import io.quarkus.test.services.URILike; + +@QuarkusScenario +public class DevModeHttpIT extends AbstractDevModeIT { + + @DevModeQuarkusApplication(ssl = false) + static RestService app = new DevModeQuarkusService() + .withProperty("quarkus.oidc.enabled", "false") + .withProperty("quarkus.keycloak.policy-enforcer.enable", "false") + .withProperty("quarkus.keycloak.devservices.enabled", "false"); + + @Override + protected URILike getUri() { + return app.getURI(Protocol.HTTP); + } + + @Override + protected RestService getApp() { + return app; + } +} diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeHttpsIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeHttpsIT.java index 698ce601a..842322aa8 100644 --- a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeHttpsIT.java +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeHttpsIT.java @@ -1,32 +1,16 @@ package io.quarkus.ts.http.advanced.reactive; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.IOException; - -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; - -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.DomElement; -import com.gargoylesoftware.htmlunit.html.HtmlPage; import io.quarkus.test.bootstrap.DevModeQuarkusService; import io.quarkus.test.bootstrap.Protocol; import io.quarkus.test.bootstrap.RestService; import io.quarkus.test.scenarios.QuarkusScenario; -import io.quarkus.test.scenarios.annotations.DisabledOnQuarkusVersion; import io.quarkus.test.services.DevModeQuarkusApplication; import io.quarkus.test.services.URILike; -import io.quarkus.test.utils.AwaitilityUtils; @QuarkusScenario -//TODO https://github.com/quarkusio/quarkus/issues/30503 -@DisabledOnQuarkusVersion(version = "2\\.13\\.[0-7].*", reason = "Missing patch from upstream") -public class DevModeHttpsIT { - private static final String PROPERTY = "quarkus.qe.test.value"; +public class DevModeHttpsIT extends AbstractDevModeIT { @DevModeQuarkusApplication(ssl = true) static RestService app = new DevModeQuarkusService() @@ -34,76 +18,20 @@ public class DevModeHttpsIT { .withProperty("quarkus.keycloak.policy-enforcer.enable", "false") .withProperty("quarkus.keycloak.devservices.enabled", "false"); - private WebClient webClient; - @BeforeEach void setUp() { - webClient = new WebClient(); - - webClient.getOptions().setRedirectEnabled(true); //required for the test case - //The execution breaks without the two options below + super.setUp(); + //The execution breaks without the option below webClient.getOptions().setUseInsecureSSL(true); - webClient.getOptions().setWebSocketEnabled(false); - - //make sure, that the cache doesn't affect us - webClient.getCookieManager().clearCookies(); - webClient.getCookieManager().setCookiesEnabled(false); - webClient.getCache().clear(); - webClient.getCache().setMaxSize(0); - - //disable everything, that we don't need - webClient.getOptions().setDownloadImages(false); - webClient.getOptions().setGeolocationEnabled(false); - webClient.getOptions().setAppletEnabled(false); - webClient.getOptions().setCssEnabled(false); - } - - @AfterEach - void tearDown() { - webClient.close(); - } - - @Tag("QUARKUS-2748") - @Test - public void uiChange() throws IOException { - URILike uri = app.getURI(Protocol.HTTPS); - - HtmlPage before = webClient.getPage(uri.withPath("/q/dev/io.quarkus.quarkus-vertx-http/config").toString()); - QuarkusUIField field = new QuarkusUIField(before.getElementById(PROPERTY)); - assertEquals("42", field.getValue()); - assertEquals("42", app.getProperty(PROPERTY, "")); - - field.setValue("23"); - HtmlPage saved = field.getSaveButton().click(); - QuarkusUIField updated = new QuarkusUIField(saved.getElementById(PROPERTY)); - assertEquals("23", updated.getValue()); - - AwaitilityUtils.untilIsTrue(() -> app.getLogs().stream().anyMatch(log -> log.contains("File change detected"))); - assertEquals("23", app.getProperty(PROPERTY, "")); - } -} - -class QuarkusUIField { - private final DomElement element; - - QuarkusUIField(DomElement element) { - this.element = element; - } - - public String getValue() { - return element.getAttribute("value"); } - public void setValue(String newValue) { - element.setAttribute("value", newValue); + @Override + protected URILike getUri() { + return app.getURI(Protocol.HTTPS); } - public DomElement getSaveButton() { - for (DomElement sibling : element.getParentNode().getDomElementDescendants()) { - if (sibling.getAttribute("class").equals("input-group-text formInputButton")) { - return sibling; - } - } - throw new IllegalStateException("Save button was not found!"); + @Override + protected RestService getApp() { + return app; } } diff --git a/http/vertx/pom.xml b/http/vertx/pom.xml new file mode 100644 index 000000000..1d2addb2f --- /dev/null +++ b/http/vertx/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + io.quarkus.ts.qe + parent + 1.0.0-SNAPSHOT + ../.. + + vertx + jar + Quarkus QE TS: HTTP: Vert.X + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkus + quarkus-vertx + + + io.quarkus + quarkus-micrometer-registry-prometheus + + + + + deploy-to-openshift-using-extension + + + io.quarkus + quarkus-openshift + + + + + diff --git a/http/vertx/src/main/java/io/quarkus/ts/vertx/Hello.java b/http/vertx/src/main/java/io/quarkus/ts/vertx/Hello.java new file mode 100644 index 000000000..8560f7554 --- /dev/null +++ b/http/vertx/src/main/java/io/quarkus/ts/vertx/Hello.java @@ -0,0 +1,14 @@ +package io.quarkus.ts.vertx; + +public class Hello { + + private final String content; + + public Hello(String content) { + this.content = content; + } + + public String getContent() { + return content; + } +} diff --git a/http/vertx/src/main/java/io/quarkus/ts/vertx/HelloResource.java b/http/vertx/src/main/java/io/quarkus/ts/vertx/HelloResource.java new file mode 100644 index 000000000..f65db94cc --- /dev/null +++ b/http/vertx/src/main/java/io/quarkus/ts/vertx/HelloResource.java @@ -0,0 +1,33 @@ +package io.quarkus.ts.vertx; + +import javax.inject.Inject; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import io.smallrye.mutiny.Uni; +import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.core.buffer.Buffer; + +@Path("/hello") +public class HelloResource { + private final Vertx vertx; + + @Inject + public HelloResource(Vertx vertx) { + this.vertx = vertx; + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Uni getMessage(@QueryParam("name") @DefaultValue("World") String name) { + return vertx.fileSystem() + .readFile("message.template") + .map(Buffer::toString) + .map(string -> String.format(string, name).trim()) + .map(Hello::new); + } +} diff --git a/http/vertx/src/main/resources/application.properties b/http/vertx/src/main/resources/application.properties new file mode 100644 index 000000000..d8b77f2fe --- /dev/null +++ b/http/vertx/src/main/resources/application.properties @@ -0,0 +1,6 @@ +quarkus.application.name=test-http +quarkus.native.resources.includes=message.template +%ServerlessExtensionOpenShiftHttpMinimumIT.quarkus.kubernetes.deployment-target=knative +%ServerlessExtensionOpenShiftHttpMinimumIT.quarkus.container-image.registry=image-registry.openshift-image-registry.svc:5000 +%ServerlessExtensionDockerBuildStrategyOpenShiftHttpMinimumIT.quarkus.kubernetes.deployment-target=knative +%ServerlessExtensionDockerBuildStrategyOpenShiftHttpMinimumIT.quarkus.container-image.registry=image-registry.openshift-image-registry.svc:5000 diff --git a/http/vertx/src/main/resources/message.template b/http/vertx/src/main/resources/message.template new file mode 100644 index 000000000..6807eef81 --- /dev/null +++ b/http/vertx/src/main/resources/message.template @@ -0,0 +1 @@ +Hello, %s! diff --git a/http/vertx/src/test/java/io/quarkus/ts/vertx/AbstractVertxIT.java b/http/vertx/src/test/java/io/quarkus/ts/vertx/AbstractVertxIT.java new file mode 100644 index 000000000..196b1e15e --- /dev/null +++ b/http/vertx/src/test/java/io/quarkus/ts/vertx/AbstractVertxIT.java @@ -0,0 +1,121 @@ +package io.quarkus.ts.vertx; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.testcontainers.shaded.org.hamcrest.MatcherAssert.assertThat; +import static org.testcontainers.shaded.org.hamcrest.Matchers.containsString; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; + +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; + +public abstract class AbstractVertxIT { + + @Test + public void httpServerAndMetrics() { + requests().get("/hello").then() + .statusCode(HttpStatus.SC_OK) + .body("content", is("Hello, World!")); + + Response response = requests().get("/q/metrics"); + assertEquals(HttpStatus.SC_OK, response.statusCode()); + assertThat(response.getContentType(), containsString("text/plain")); + String body = response.body().asString(); + Map metrics = parseMetrics(body); + assertTrue(metrics.containsKey("worker_pool_active")); + assertTrue(metrics.containsKey("worker_pool_completed_total")); + assertTrue(metrics.containsKey("worker_pool_queue_size")); + } + + @Test + public void httpServerParsing() { + requests().get("/hello?name=you").then().statusCode(HttpStatus.SC_OK).body("content", is("Hello, you!")); + } + + public abstract RequestSpecification requests(); + + private Map parseMetrics(String body) { + Map metrics = new HashMap<>(128); + Arrays.stream(body.split("\n")) + .filter(line -> !line.startsWith("#")) + .map(Metric::new) + .forEach(metric -> metrics.put(metric.name, metric)); + return metrics; + } + + private class Metric { + private final String value; + private final String name; + + /** + * + * @param source metric from the file, eg: + * worker_pool_queue_size{pool_name="vert.x-internal-blocking",pool_type="worker"} 0.0 + * content in curly brackets is ignored (for now) + * since we do not care about values, we store them as strings, and ignore duplicated keys. + */ + public Metric(String source) { + final int DEFAULT = -1; + int space = DEFAULT; + int closing = DEFAULT; + int opening = DEFAULT; + byte[] bytes = source.getBytes(StandardCharsets.UTF_8); + for (int i = bytes.length - 1; i >= 0; i--) { + byte current = bytes[i]; + if (current == ' ' && space == DEFAULT) { + space = i; + } + if (current == '}' && closing == DEFAULT) { + closing = i; + } + if (current == '{' && opening == DEFAULT) { + opening = i; + } + } + String key; + if (space > 0) { + value = source.substring(space); + key = source.substring(0, space); + } else { + throw new IllegalArgumentException("Metric " + source + " doesn't contain a value"); + } + if (closing < space && opening < closing && opening > 0) { + name = source.substring(0, opening); + } else { + name = key; + } + } + + public String getValue() { + return value; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Metric metric = (Metric) o; + return value.equals(metric.value) && name.equals(metric.name); + } + + @Override + public int hashCode() { + return Objects.hash(value, name); + } + } +} diff --git a/http/vertx/src/test/java/io/quarkus/ts/vertx/LocalVertxIT.java b/http/vertx/src/test/java/io/quarkus/ts/vertx/LocalVertxIT.java new file mode 100644 index 000000000..f69279815 --- /dev/null +++ b/http/vertx/src/test/java/io/quarkus/ts/vertx/LocalVertxIT.java @@ -0,0 +1,18 @@ +package io.quarkus.ts.vertx; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.QuarkusApplication; +import io.restassured.specification.RequestSpecification; + +@QuarkusScenario +public class LocalVertxIT extends AbstractVertxIT { + + @QuarkusApplication + static RestService app = new RestService(); + + @Override + public RequestSpecification requests() { + return app.given(); + } +} diff --git a/http/vertx/src/test/java/io/quarkus/ts/vertx/OpenShiftUsingExtensionDockerBuildStrategyVertxIT.java b/http/vertx/src/test/java/io/quarkus/ts/vertx/OpenShiftUsingExtensionDockerBuildStrategyVertxIT.java new file mode 100644 index 000000000..798fa7291 --- /dev/null +++ b/http/vertx/src/test/java/io/quarkus/ts/vertx/OpenShiftUsingExtensionDockerBuildStrategyVertxIT.java @@ -0,0 +1,21 @@ +package io.quarkus.ts.vertx; + +import org.junit.jupiter.api.Tag; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.OpenShiftDeploymentStrategy; +import io.quarkus.test.scenarios.OpenShiftScenario; +import io.quarkus.test.services.QuarkusApplication; +import io.restassured.specification.RequestSpecification; + +@Tag("use-quarkus-openshift-extension") +@OpenShiftScenario(deployment = OpenShiftDeploymentStrategy.UsingOpenShiftExtensionAndDockerBuildStrategy) +public class OpenShiftUsingExtensionDockerBuildStrategyVertxIT extends AbstractVertxIT { + @QuarkusApplication + static RestService app = new RestService(); + + @Override + public RequestSpecification requests() { + return app.given(); + } +} diff --git a/http/vertx/src/test/java/io/quarkus/ts/vertx/OpenShiftUsingExtensionVertxIT.java b/http/vertx/src/test/java/io/quarkus/ts/vertx/OpenShiftUsingExtensionVertxIT.java new file mode 100644 index 000000000..627a99211 --- /dev/null +++ b/http/vertx/src/test/java/io/quarkus/ts/vertx/OpenShiftUsingExtensionVertxIT.java @@ -0,0 +1,21 @@ +package io.quarkus.ts.vertx; + +import org.junit.jupiter.api.Tag; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.OpenShiftDeploymentStrategy; +import io.quarkus.test.scenarios.OpenShiftScenario; +import io.quarkus.test.services.QuarkusApplication; +import io.restassured.specification.RequestSpecification; + +@Tag("use-quarkus-openshift-extension") +@OpenShiftScenario(deployment = OpenShiftDeploymentStrategy.UsingOpenShiftExtension) +public class OpenShiftUsingExtensionVertxIT extends AbstractVertxIT { + @QuarkusApplication + static RestService app = new RestService(); + + @Override + public RequestSpecification requests() { + return app.given(); + } +} diff --git a/http/vertx/src/test/java/io/quarkus/ts/vertx/OpenShiftVertxIT.java b/http/vertx/src/test/java/io/quarkus/ts/vertx/OpenShiftVertxIT.java new file mode 100644 index 000000000..5206d96a2 --- /dev/null +++ b/http/vertx/src/test/java/io/quarkus/ts/vertx/OpenShiftVertxIT.java @@ -0,0 +1,17 @@ +package io.quarkus.ts.vertx; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.OpenShiftScenario; +import io.quarkus.test.services.QuarkusApplication; +import io.restassured.specification.RequestSpecification; + +@OpenShiftScenario +public class OpenShiftVertxIT extends AbstractVertxIT { + @QuarkusApplication + static RestService app = new RestService(); + + @Override + public RequestSpecification requests() { + return app.given(); + } +} diff --git a/http/vertx/src/test/java/io/quarkus/ts/vertx/ServerlessExtensionDockerBuildStrategyOpenShiftVertxIT.java b/http/vertx/src/test/java/io/quarkus/ts/vertx/ServerlessExtensionDockerBuildStrategyOpenShiftVertxIT.java new file mode 100644 index 000000000..6da0b51c2 --- /dev/null +++ b/http/vertx/src/test/java/io/quarkus/ts/vertx/ServerlessExtensionDockerBuildStrategyOpenShiftVertxIT.java @@ -0,0 +1,22 @@ +package io.quarkus.ts.vertx; + +import org.junit.jupiter.api.Tag; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.OpenShiftDeploymentStrategy; +import io.quarkus.test.scenarios.OpenShiftScenario; +import io.quarkus.test.services.QuarkusApplication; +import io.restassured.specification.RequestSpecification; + +@Tag("use-quarkus-openshift-extension") +@Tag("serverless") +@OpenShiftScenario(deployment = OpenShiftDeploymentStrategy.UsingOpenShiftExtensionAndDockerBuildStrategy) +public class ServerlessExtensionDockerBuildStrategyOpenShiftVertxIT extends AbstractVertxIT { + @QuarkusApplication + static RestService app = new RestService(); + + @Override + public RequestSpecification requests() { + return app.given().relaxedHTTPSValidation(); + } +} diff --git a/http/vertx/src/test/java/io/quarkus/ts/vertx/ServerlessExtensionOpenShiftVertxIT.java b/http/vertx/src/test/java/io/quarkus/ts/vertx/ServerlessExtensionOpenShiftVertxIT.java new file mode 100644 index 000000000..fdb7acd7b --- /dev/null +++ b/http/vertx/src/test/java/io/quarkus/ts/vertx/ServerlessExtensionOpenShiftVertxIT.java @@ -0,0 +1,22 @@ +package io.quarkus.ts.vertx; + +import org.junit.jupiter.api.Tag; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.OpenShiftDeploymentStrategy; +import io.quarkus.test.scenarios.OpenShiftScenario; +import io.quarkus.test.services.QuarkusApplication; +import io.restassured.specification.RequestSpecification; + +@Tag("use-quarkus-openshift-extension") +@Tag("serverless") +@OpenShiftScenario(deployment = OpenShiftDeploymentStrategy.UsingOpenShiftExtension) +public class ServerlessExtensionOpenShiftVertxIT extends AbstractVertxIT { + @QuarkusApplication + static RestService app = new RestService(); + + @Override + public RequestSpecification requests() { + return app.given().relaxedHTTPSValidation(); + } +} diff --git a/monitoring/opentelemetry-reactive/pom.xml b/monitoring/opentelemetry-reactive/pom.xml index 9b9ead084..5c4e7b492 100644 --- a/monitoring/opentelemetry-reactive/pom.xml +++ b/monitoring/opentelemetry-reactive/pom.xml @@ -27,10 +27,58 @@ io.quarkus quarkus-opentelemetry-exporter-otlp + + io.quarkus + quarkus-grpc + io.quarkus.qe quarkus-test-service-jaeger test + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + + + + generate-code + generate-code-tests + build + + + + + + + + + + skip-tests-on-windows + + + windows + + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + + + diff --git a/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/grpc/GrpcPingResource.java b/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/grpc/GrpcPingResource.java new file mode 100644 index 000000000..d936dcaf9 --- /dev/null +++ b/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/grpc/GrpcPingResource.java @@ -0,0 +1,28 @@ +package io.quarkus.ts.opentelemetry.reactive.grpc; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.quarkus.example.PongRequest; +import io.quarkus.example.PongServiceGrpc; +import io.quarkus.grpc.GrpcClient; +import io.quarkus.ts.opentelemetry.reactive.traceable.TraceableResource; + +@Path("/grpc-ping") +public class GrpcPingResource extends TraceableResource { + + @Inject + @GrpcClient("pong") + PongServiceGrpc.PongServiceBlockingStub pongClient; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getPing() { + recordTraceId(); + + return "ping " + pongClient.sayPong(PongRequest.newBuilder().build()).getMessage(); + } +} diff --git a/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/grpc/GrpcPongResource.java b/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/grpc/GrpcPongResource.java new file mode 100644 index 000000000..bdce6714e --- /dev/null +++ b/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/grpc/GrpcPongResource.java @@ -0,0 +1,34 @@ +package io.quarkus.ts.opentelemetry.reactive.grpc; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.logging.Logger; + +import io.quarkus.example.LastTraceIdRequest; +import io.quarkus.example.PongServiceGrpc; +import io.quarkus.grpc.GrpcClient; +import io.quarkus.ts.opentelemetry.reactive.traceable.TraceableResource; + +@Path("/grpc-pong") +public class GrpcPongResource { + + @Inject + @GrpcClient("pong") + PongServiceGrpc.PongServiceBlockingStub pongClient; + + private static final Logger LOG = Logger.getLogger(TraceableResource.class); + + @GET + @Path("/lastTraceId") + @Produces(MediaType.TEXT_PLAIN) + public String getLastTraceId() { + String lastTraceId = pongClient.returnLastTraceId(LastTraceIdRequest.newBuilder().build()).getMessage(); + LOG.info("Recorded trace ID: " + lastTraceId); + return lastTraceId; + } + +} diff --git a/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/grpc/GrpcPongService.java b/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/grpc/GrpcPongService.java new file mode 100644 index 000000000..9c112f1cb --- /dev/null +++ b/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/grpc/GrpcPongService.java @@ -0,0 +1,34 @@ +package io.quarkus.ts.opentelemetry.reactive.grpc; + +import org.jboss.logmanager.MDC; + +import io.grpc.stub.StreamObserver; +import io.quarkus.example.LastTraceIdReply; +import io.quarkus.example.LastTraceIdRequest; +import io.quarkus.example.PongReply; +import io.quarkus.example.PongRequest; +import io.quarkus.example.PongServiceGrpc; +import io.quarkus.grpc.GrpcService; + +@GrpcService +public class GrpcPongService extends PongServiceGrpc.PongServiceImplBase { + + private String lastTraceId; + + @Override + public void sayPong(PongRequest request, StreamObserver responseObserver) { + lastTraceId = MDC.get("traceId"); + responseObserver.onNext(PongReply.newBuilder().setMessage("pong").build()); + responseObserver.onCompleted(); + } + + @Override + public void returnLastTraceId(LastTraceIdRequest request, StreamObserver responseObserver) { + responseObserver.onNext(LastTraceIdReply.newBuilder().setMessage(getLastTraceId()).build()); + responseObserver.onCompleted(); + } + + public String getLastTraceId() { + return lastTraceId; + } +} diff --git a/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/sse/ServerSentEventsPingResource.java b/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/sse/ServerSentEventsPingResource.java new file mode 100644 index 000000000..5f3a13ef7 --- /dev/null +++ b/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/sse/ServerSentEventsPingResource.java @@ -0,0 +1,46 @@ +package io.quarkus.ts.opentelemetry.reactive.sse; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.sse.OutboundSseEvent; +import javax.ws.rs.sse.Sse; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import io.quarkus.ts.opentelemetry.reactive.traceable.TraceableResource; +import io.smallrye.mutiny.Multi; + +@Path("/server-sent-events-ping") +public class ServerSentEventsPingResource extends TraceableResource { + + @Inject + @RestClient + ServerSentEventsPongClient pongClient; + + @GET + @Produces(MediaType.SERVER_SENT_EVENTS) + public Multi getPing() { + recordTraceId(); + return pongClient.getPong().map(response -> "ping " + response); + } + + @Path("/raw") + @GET + @Produces(MediaType.SERVER_SENT_EVENTS) + public Multi sseRaw(@Context Sse sse, @QueryParam("amount") int amount) { + List events = new ArrayList<>(amount); + for (int i = 0; i < amount; i++) { + events.add(sse.newEventBuilder().id("id_" + i).data("data_" + i).name("name_" + i).build()); + } + + return Multi.createFrom().items(events.toArray(OutboundSseEvent[]::new)); + } +} diff --git a/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/sse/ServerSentEventsPongClient.java b/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/sse/ServerSentEventsPongClient.java new file mode 100644 index 000000000..3ae017944 --- /dev/null +++ b/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/sse/ServerSentEventsPongClient.java @@ -0,0 +1,19 @@ +package io.quarkus.ts.opentelemetry.reactive.sse; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.smallrye.mutiny.Multi; + +@RegisterRestClient +public interface ServerSentEventsPongClient { + @GET + @Path("/server-sent-events-pong") + @Produces(MediaType.SERVER_SENT_EVENTS) + Multi getPong(); + +} diff --git a/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/sse/ServerSentEventsPongResource.java b/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/sse/ServerSentEventsPongResource.java new file mode 100644 index 000000000..84a52c208 --- /dev/null +++ b/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/sse/ServerSentEventsPongResource.java @@ -0,0 +1,20 @@ +package io.quarkus.ts.opentelemetry.reactive.sse; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.quarkus.ts.opentelemetry.reactive.traceable.TraceableResource; +import io.smallrye.mutiny.Multi; + +@Path("/server-sent-events-pong") +public class ServerSentEventsPongResource extends TraceableResource { + + @GET + @Produces(MediaType.SERVER_SENT_EVENTS) + public Multi getPong() { + recordTraceId(); + return Multi.createFrom().item("pong"); + } +} diff --git a/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/traceable/TraceableResource.java b/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/traceable/TraceableResource.java new file mode 100644 index 000000000..7cb825458 --- /dev/null +++ b/monitoring/opentelemetry-reactive/src/main/java/io/quarkus/ts/opentelemetry/reactive/traceable/TraceableResource.java @@ -0,0 +1,28 @@ +package io.quarkus.ts.opentelemetry.reactive.traceable; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.logging.Logger; +import org.jboss.logmanager.MDC; + +public abstract class TraceableResource { + + private static final Logger LOG = Logger.getLogger(TraceableResource.class); + + private String lastTraceId; + + @GET + @Path("/lastTraceId") + @Produces(MediaType.TEXT_PLAIN) + public String getLastTraceId() { + return lastTraceId; + } + + protected void recordTraceId() { + lastTraceId = MDC.get("traceId"); + LOG.info("Recorded trace ID: " + lastTraceId); + } +} diff --git a/monitoring/opentelemetry-reactive/src/main/proto/pong.proto b/monitoring/opentelemetry-reactive/src/main/proto/pong.proto new file mode 100644 index 000000000..a62e5faec --- /dev/null +++ b/monitoring/opentelemetry-reactive/src/main/proto/pong.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.quarkus.example"; +option java_outer_classname = "PongProto"; + +package io.quarkus.example; + +service PongService { + rpc SayPong (PongRequest) returns (PongReply) {} + rpc ReturnLastTraceId (LastTraceIdRequest) returns (LastTraceIdReply) {} +} + +message PongRequest { +} + +message PongReply { + string message = 1; +} + +message LastTraceIdRequest { +} + +message LastTraceIdReply { + string message = 1; +} \ No newline at end of file diff --git a/monitoring/opentelemetry-reactive/src/main/resources/application.properties b/monitoring/opentelemetry-reactive/src/main/resources/application.properties index 3cbbc00ae..31d273872 100644 --- a/monitoring/opentelemetry-reactive/src/main/resources/application.properties +++ b/monitoring/opentelemetry-reactive/src/main/resources/application.properties @@ -1,3 +1,10 @@ io.quarkus.ts.opentelemetry.reactive.PingPongService/mp-rest/url=${pongservice_url}:${pongservice_port} io.quarkus.ts.opentelemetry.reactive.PingPongService/mp-rest/scope=javax.inject.Singleton +io.quarkus.ts.opentelemetry.reactive.sse.ServerSentEventsPongClient/mp-rest/url=http://localhost:${quarkus.http.port} +io.quarkus.ts.opentelemetry.reactive.sse.ServerSentEventsPongClient/mp-rest/scope=javax.inject.Singleton + +# gRPC +quarkus.grpc.clients.pong.host=localhost + +quarkus.application.name=pingpong \ No newline at end of file diff --git a/monitoring/opentelemetry-reactive/src/test/java/io/quarkus/ts/opentelemetry/reactive/OpenTelemetryGrpcIT.java b/monitoring/opentelemetry-reactive/src/test/java/io/quarkus/ts/opentelemetry/reactive/OpenTelemetryGrpcIT.java new file mode 100644 index 000000000..57d49c2f1 --- /dev/null +++ b/monitoring/opentelemetry-reactive/src/test/java/io/quarkus/ts/opentelemetry/reactive/OpenTelemetryGrpcIT.java @@ -0,0 +1,65 @@ +package io.quarkus.ts.opentelemetry.reactive; + +import static io.restassured.RestAssured.given; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.TimeUnit; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.JaegerService; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.JaegerContainer; +import io.quarkus.test.services.QuarkusApplication; + +@QuarkusScenario +public class OpenTelemetryGrpcIT { + + @JaegerContainer(useOtlpCollector = true) + static final JaegerService jaeger = new JaegerService(); + + @QuarkusApplication() + static RestService app = new RestService() + .withProperty("quarkus.application.name", "pingpong") + .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl); + + private static final String PING_ENDPOINT = "/grpc-ping"; + private static final String PONG_ENDPOINT = "/grpc-pong"; + private static final String SAY_PONG_PROTO = "SayPong"; + + @Test + public void testServerClientTrace() throws InterruptedException { + // When calling ping, the rest will invoke also the pong rest endpoint. + given() + .when().get(PING_ENDPOINT) + .then().statusCode(HttpStatus.SC_OK) + .body(containsString("ping pong")); + + // Then both ping and pong rest endpoints should have the same trace Id. + String pingTraceId = given() + .when().get(PING_ENDPOINT + "/lastTraceId") + .then().statusCode(HttpStatus.SC_OK).and().extract().asString(); + + assertTraceIdWithPongService(pingTraceId); + + // Then Jaeger is invoked + await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> given() + .when().get(jaeger.getTraceUrl() + "?traceID=" + pingTraceId) + .then().statusCode(HttpStatus.SC_OK) + .and().body(allOf(containsString(PING_ENDPOINT), containsString(SAY_PONG_PROTO)))); + } + + protected void assertTraceIdWithPongService(String expected) { + String pongTraceId = given() + .when().get(PONG_ENDPOINT + "/lastTraceId") + .then().statusCode(HttpStatus.SC_OK).and().extract().asString(); + + assertEquals(expected, pongTraceId); + } + +} diff --git a/monitoring/opentelemetry-reactive/src/test/java/io/quarkus/ts/opentelemetry/reactive/OpenTelemetrySseIT.java b/monitoring/opentelemetry-reactive/src/test/java/io/quarkus/ts/opentelemetry/reactive/OpenTelemetrySseIT.java new file mode 100644 index 000000000..33cb230e5 --- /dev/null +++ b/monitoring/opentelemetry-reactive/src/test/java/io/quarkus/ts/opentelemetry/reactive/OpenTelemetrySseIT.java @@ -0,0 +1,79 @@ +package io.quarkus.ts.opentelemetry.reactive; + +import static io.restassured.RestAssured.given; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.TimeUnit; + +import javax.ws.rs.core.MediaType; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.JaegerService; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.JaegerContainer; +import io.quarkus.test.services.QuarkusApplication; + +@QuarkusScenario +public class OpenTelemetrySseIT { + + @JaegerContainer(useOtlpCollector = true) + static final JaegerService jaeger = new JaegerService(); + + @QuarkusApplication() + static RestService app = new RestService() + .withProperty("quarkus.application.name", "pingpong") + .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl); + + private static final String PING_ENDPOINT = "/server-sent-events-ping"; + private static final String PONG_ENDPOINT = "/server-sent-events-pong"; + + @Test + public void testServerClientTrace() throws InterruptedException { + // When calling ping, the rest will invoke also the pong rest endpoint. + given() + .when().get(PING_ENDPOINT) + .then().statusCode(HttpStatus.SC_OK) + .body(containsString("ping pong")); + + // Then both ping and pong rest endpoints should have the same trace Id. + String pingTraceId = given() + .when().get(PING_ENDPOINT + "/lastTraceId") + .then().statusCode(HttpStatus.SC_OK).and().extract().asString(); + + assertTraceIdWithPongService(pingTraceId); + + // Then Jaeger is invoked + await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> given() + .when().get(jaeger.getTraceUrl() + "?traceID=" + pingTraceId) + .then().statusCode(HttpStatus.SC_OK) + .and().body(allOf(containsString(PING_ENDPOINT), containsString(PONG_ENDPOINT)))); + } + + @Tag("QUARKUS-2745") + @Test + public void verifySeeRawSerialization() { + final int amount = 3; + given() + .when().get(PING_ENDPOINT + "/raw?amount=" + amount) + .then().statusCode(HttpStatus.SC_OK) + .contentType(MediaType.SERVER_SENT_EVENTS) + .body(containsString("data:data_0")) + .body(containsString("id:id_1")) + .body(containsString("event:name_2")); + } + + protected void assertTraceIdWithPongService(String expected) { + String pongTraceId = given() + .when().get(PONG_ENDPOINT + "/lastTraceId") + .then().statusCode(HttpStatus.SC_OK).and().extract().asString(); + + assertEquals(expected, pongTraceId); + } +} diff --git a/monitoring/opentelemetry/pom.xml b/monitoring/opentelemetry/pom.xml index 837a82d63..7524105ef 100644 --- a/monitoring/opentelemetry/pom.xml +++ b/monitoring/opentelemetry/pom.xml @@ -23,14 +23,70 @@ io.quarkus quarkus-resteasy + + io.quarkus + quarkus-resteasy-mutiny + io.quarkus quarkus-opentelemetry-exporter-otlp + + io.quarkus + quarkus-opentelemetry + + + io.quarkus + quarkus-grpc + io.quarkus.qe quarkus-test-service-jaeger test + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + + + + generate-code + generate-code-tests + build + + + + + + + + + + skip-tests-on-windows + + + windows + + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + + + diff --git a/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/grpc/GrpcPingResource.java b/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/grpc/GrpcPingResource.java new file mode 100644 index 000000000..14507e813 --- /dev/null +++ b/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/grpc/GrpcPingResource.java @@ -0,0 +1,28 @@ +package io.quarkus.ts.opentelemetry.grpc; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.quarkus.example.PongRequest; +import io.quarkus.example.PongServiceGrpc; +import io.quarkus.grpc.GrpcClient; +import io.quarkus.ts.opentelemetry.traceable.TraceableResource; + +@Path("/grpc-ping") +public class GrpcPingResource extends TraceableResource { + + @Inject + @GrpcClient("pong") + PongServiceGrpc.PongServiceBlockingStub pongClient; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getPing() { + recordTraceId(); + + return "ping " + pongClient.sayPong(PongRequest.newBuilder().build()).getMessage(); + } +} diff --git a/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/grpc/GrpcPongResource.java b/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/grpc/GrpcPongResource.java new file mode 100644 index 000000000..d35879fe8 --- /dev/null +++ b/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/grpc/GrpcPongResource.java @@ -0,0 +1,34 @@ +package io.quarkus.ts.opentelemetry.grpc; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.logging.Logger; + +import io.quarkus.example.LastTraceIdRequest; +import io.quarkus.example.PongServiceGrpc; +import io.quarkus.grpc.GrpcClient; +import io.quarkus.ts.opentelemetry.traceable.TraceableResource; + +@Path("/grpc-pong") +public class GrpcPongResource { + + @Inject + @GrpcClient("pong") + PongServiceGrpc.PongServiceBlockingStub pongClient; + + private static final Logger LOG = Logger.getLogger(TraceableResource.class); + + @GET + @Path("/lastTraceId") + @Produces(MediaType.TEXT_PLAIN) + public String getLastTraceId() { + String lastTraceId = pongClient.returnLastTraceId(LastTraceIdRequest.newBuilder().build()).getMessage(); + LOG.info("Recorded trace ID: " + lastTraceId); + return lastTraceId; + } + +} diff --git a/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/grpc/GrpcPongService.java b/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/grpc/GrpcPongService.java new file mode 100644 index 000000000..39025a050 --- /dev/null +++ b/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/grpc/GrpcPongService.java @@ -0,0 +1,34 @@ +package io.quarkus.ts.opentelemetry.grpc; + +import org.jboss.logmanager.MDC; + +import io.grpc.stub.StreamObserver; +import io.quarkus.example.LastTraceIdReply; +import io.quarkus.example.LastTraceIdRequest; +import io.quarkus.example.PongReply; +import io.quarkus.example.PongRequest; +import io.quarkus.example.PongServiceGrpc; +import io.quarkus.grpc.GrpcService; + +@GrpcService +public class GrpcPongService extends PongServiceGrpc.PongServiceImplBase { + + private String lastTraceId; + + @Override + public void sayPong(PongRequest request, StreamObserver responseObserver) { + lastTraceId = MDC.get("traceId"); + responseObserver.onNext(PongReply.newBuilder().setMessage("pong").build()); + responseObserver.onCompleted(); + } + + @Override + public void returnLastTraceId(LastTraceIdRequest request, StreamObserver responseObserver) { + responseObserver.onNext(LastTraceIdReply.newBuilder().setMessage(getLastTraceId()).build()); + responseObserver.onCompleted(); + } + + public String getLastTraceId() { + return lastTraceId; + } +} diff --git a/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/sse/ServerSentEventsPingResource.java b/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/sse/ServerSentEventsPingResource.java new file mode 100644 index 000000000..f698d0266 --- /dev/null +++ b/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/sse/ServerSentEventsPingResource.java @@ -0,0 +1,27 @@ +package io.quarkus.ts.opentelemetry.sse; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import io.quarkus.ts.opentelemetry.traceable.TraceableResource; +import io.smallrye.mutiny.Multi; + +@Path("/server-sent-events-ping") +public class ServerSentEventsPingResource extends TraceableResource { + + @Inject + @RestClient + ServerSentEventsPongClient pongClient; + + @GET + @Produces(MediaType.SERVER_SENT_EVENTS) + public Multi getPing() { + recordTraceId(); + return pongClient.getPong().map(response -> "ping " + response); + } +} diff --git a/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/sse/ServerSentEventsPongClient.java b/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/sse/ServerSentEventsPongClient.java new file mode 100644 index 000000000..9da4cad17 --- /dev/null +++ b/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/sse/ServerSentEventsPongClient.java @@ -0,0 +1,19 @@ +package io.quarkus.ts.opentelemetry.sse; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.smallrye.mutiny.Multi; + +@RegisterRestClient +public interface ServerSentEventsPongClient { + @GET + @Path("/server-sent-events-pong") + @Produces(MediaType.SERVER_SENT_EVENTS) + Multi getPong(); + +} diff --git a/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/sse/ServerSentEventsPongResource.java b/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/sse/ServerSentEventsPongResource.java new file mode 100644 index 000000000..41b8e6485 --- /dev/null +++ b/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/sse/ServerSentEventsPongResource.java @@ -0,0 +1,20 @@ +package io.quarkus.ts.opentelemetry.sse; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.quarkus.ts.opentelemetry.traceable.TraceableResource; +import io.smallrye.mutiny.Multi; + +@Path("/server-sent-events-pong") +public class ServerSentEventsPongResource extends TraceableResource { + + @GET + @Produces(MediaType.SERVER_SENT_EVENTS) + public Multi getPong() { + recordTraceId(); + return Multi.createFrom().item("pong"); + } +} diff --git a/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/traceable/TraceableResource.java b/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/traceable/TraceableResource.java new file mode 100644 index 000000000..cd852b8aa --- /dev/null +++ b/monitoring/opentelemetry/src/main/java/io/quarkus/ts/opentelemetry/traceable/TraceableResource.java @@ -0,0 +1,28 @@ +package io.quarkus.ts.opentelemetry.traceable; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.logging.Logger; +import org.jboss.logmanager.MDC; + +public abstract class TraceableResource { + + private static final Logger LOG = Logger.getLogger(TraceableResource.class); + + private String lastTraceId; + + @GET + @Path("/lastTraceId") + @Produces(MediaType.TEXT_PLAIN) + public String getLastTraceId() { + return lastTraceId; + } + + protected void recordTraceId() { + lastTraceId = MDC.get("traceId"); + LOG.info("Recorded trace ID: " + lastTraceId); + } +} diff --git a/monitoring/opentelemetry/src/main/proto/pong.proto b/monitoring/opentelemetry/src/main/proto/pong.proto new file mode 100644 index 000000000..a62e5faec --- /dev/null +++ b/monitoring/opentelemetry/src/main/proto/pong.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.quarkus.example"; +option java_outer_classname = "PongProto"; + +package io.quarkus.example; + +service PongService { + rpc SayPong (PongRequest) returns (PongReply) {} + rpc ReturnLastTraceId (LastTraceIdRequest) returns (LastTraceIdReply) {} +} + +message PongRequest { +} + +message PongReply { + string message = 1; +} + +message LastTraceIdRequest { +} + +message LastTraceIdReply { + string message = 1; +} \ No newline at end of file diff --git a/monitoring/opentelemetry/src/main/resources/application.properties b/monitoring/opentelemetry/src/main/resources/application.properties index 4067be4b8..9fc09e8ce 100644 --- a/monitoring/opentelemetry/src/main/resources/application.properties +++ b/monitoring/opentelemetry/src/main/resources/application.properties @@ -1,3 +1,10 @@ io.quarkus.ts.opentelemetry.PingPongService/mp-rest/url=${pongservice_url}:${pongservice_port} io.quarkus.ts.opentelemetry.PingPongService/mp-rest/scope=javax.inject.Singleton +io.quarkus.ts.opentelemetry.sse.ServerSentEventsPongClient/mp-rest/url=http://localhost:${quarkus.http.port} +io.quarkus.ts.opentelemetry.sse.ServerSentEventsPongClient/mp-rest/scope=javax.inject.Singleton + +# gRPC +quarkus.grpc.clients.pong.host=localhost + +quarkus.application.name=pingpong \ No newline at end of file diff --git a/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryGrpcIT.java b/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryGrpcIT.java new file mode 100644 index 000000000..ce401b4cf --- /dev/null +++ b/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryGrpcIT.java @@ -0,0 +1,65 @@ +package io.quarkus.ts.opentelemetry; + +import static io.restassured.RestAssured.given; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.TimeUnit; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.JaegerService; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.JaegerContainer; +import io.quarkus.test.services.QuarkusApplication; + +@QuarkusScenario +public class OpenTelemetryGrpcIT { + + @JaegerContainer(useOtlpCollector = true) + static final JaegerService jaeger = new JaegerService(); + + @QuarkusApplication() + static RestService app = new RestService() + .withProperty("quarkus.application.name", "pingpong") + .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl); + + private static final String PING_ENDPOINT = "/grpc-ping"; + private static final String PONG_ENDPOINT = "/grpc-pong"; + private static final String SAY_PONG_PROTO = "SayPong"; + + @Test + public void testServerClientTrace() throws InterruptedException { + // When calling ping, the rest will invoke also the pong rest endpoint. + given() + .when().get(PING_ENDPOINT) + .then().statusCode(HttpStatus.SC_OK) + .body(containsString("ping pong")); + + // Then both ping and pong rest endpoints should have the same trace Id. + String pingTraceId = given() + .when().get(PING_ENDPOINT + "/lastTraceId") + .then().statusCode(HttpStatus.SC_OK).and().extract().asString(); + + assertTraceIdWithPongService(pingTraceId); + + // Then Jaeger is invoked + await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> given() + .when().get(jaeger.getTraceUrl() + "?traceID=" + pingTraceId) + .then().statusCode(HttpStatus.SC_OK) + .and().body(allOf(containsString(PING_ENDPOINT), containsString(SAY_PONG_PROTO)))); + } + + protected void assertTraceIdWithPongService(String expected) { + String pongTraceId = given() + .when().get(PONG_ENDPOINT + "/lastTraceId") + .then().statusCode(HttpStatus.SC_OK).and().extract().asString(); + + assertEquals(expected, pongTraceId); + } + +} diff --git a/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetrySseIT.java b/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetrySseIT.java new file mode 100644 index 000000000..2b0b63b35 --- /dev/null +++ b/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetrySseIT.java @@ -0,0 +1,70 @@ +package io.quarkus.ts.opentelemetry; + +import static io.restassured.RestAssured.given; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.TimeUnit; + +import javax.ws.rs.core.MediaType; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.JaegerService; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.JaegerContainer; +import io.quarkus.test.services.QuarkusApplication; + +@QuarkusScenario +@Disabled("Input from Clement: RESTEasy classic and SSE is barely working, the fact that RESTEasy classic requires a worker " + + "thread can lead to very annoying issue, we recommend to switch to RESTEasy reactive") +public class OpenTelemetrySseIT { + + @JaegerContainer(useOtlpCollector = true) + static final JaegerService jaeger = new JaegerService(); + + @QuarkusApplication() + static RestService app = new RestService() + .withProperty("quarkus.application.name", "pingpong") + .withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl); + + private static final String PING_ENDPOINT = "/server-sent-events-ping"; + private static final String PONG_ENDPOINT = "/server-sent-events-pong"; + + @Test + public void testServerClientTrace() throws InterruptedException { + // When calling ping, the rest will invoke also the pong rest endpoint. + given() + .when().get(PING_ENDPOINT) + .then().statusCode(HttpStatus.SC_OK) + .contentType(MediaType.SERVER_SENT_EVENTS) + .body(containsString("ping pong")); + + // Then both ping and pong rest endpoints should have the same trace Id. + String pingTraceId = given() + .when().get(PING_ENDPOINT + "/lastTraceId") + .then().statusCode(HttpStatus.SC_OK).and().extract().asString(); + + assertTraceIdWithPongService(pingTraceId); + + // Then Jaeger is invoked + await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> given() + .when().get(jaeger.getTraceUrl() + "?traceID=" + pingTraceId) + .then().statusCode(HttpStatus.SC_OK) + .and().body(allOf(containsString(PING_ENDPOINT), containsString(PONG_ENDPOINT)))); + } + + protected void assertTraceIdWithPongService(String expected) { + String pongTraceId = given() + .when().get(PONG_ENDPOINT + "/lastTraceId") + .then().statusCode(HttpStatus.SC_OK).and().extract().asString(); + + assertEquals(expected, pongTraceId); + } + +} diff --git a/pom.xml b/pom.xml index 35de0beb8..5c0884802 100644 --- a/pom.xml +++ b/pom.xml @@ -216,6 +216,7 @@ ${ts.global.s2i.quarkus.native.builder.image} docker.io/library/postgres:13.8 + ${postgresql.latest.image} docker.io/library/postgres:15.1 @@ -442,6 +443,7 @@ http/hibernate-validator http/graphql http/graphql-telemetry + http/vertx @@ -543,6 +545,7 @@ sql-db/hibernate-reactive sql-db/reactive-vanilla sql-db/hibernate-fulltext-search + sql-db/narayana-transactions diff --git a/quarkus-cli/src/test/java/io/quarkus/ts/quarkus/cli/QuarkusCliCreateJvmApplicationIT.java b/quarkus-cli/src/test/java/io/quarkus/ts/quarkus/cli/QuarkusCliCreateJvmApplicationIT.java index 5a951b533..38089722e 100644 --- a/quarkus-cli/src/test/java/io/quarkus/ts/quarkus/cli/QuarkusCliCreateJvmApplicationIT.java +++ b/quarkus-cli/src/test/java/io/quarkus/ts/quarkus/cli/QuarkusCliCreateJvmApplicationIT.java @@ -6,7 +6,9 @@ import static io.quarkus.test.utils.AwaitilityUtils.untilAsserted; import static io.quarkus.ts.quarkus.cli.QuarkusCliUtils.defaultWithFixedStream; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -14,7 +16,9 @@ import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -27,6 +31,7 @@ import org.apache.http.HttpStatus; import org.apache.maven.model.Model; import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.awaitility.core.ConditionTimeoutException; import org.codehaus.plexus.util.xml.pull.XmlPullParserException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Tag; @@ -264,6 +269,27 @@ public void verifyRestEasyReactiveAndClassicResteasyCollisionUserMsg() { assertBuildError(buildResult, "io.quarkus:quarkus-resteasy"); } + @Test + public void devModeIgnoresPomPackaging() throws IOException { + QuarkusCliRestService app = cliClient.createApplication("pomApp", defaultWithFixedStream()); + {//set packaging to POM + Path pom = getFileFromApplication(app, ROOT_FOLDER, "pom.xml").toPath(); + List content = Files.readAllLines(pom); + for (int i = 0; i < content.size(); i++) { + String line = content.get(i); + if (line.endsWith("pomApp")) { + content.set(i, line + "pom"); + break; + } + } + Files.write(pom, content); + } + // Start using DEV mode + assertEquals(Duration.ofSeconds(2), app.getConfiguration().getAsDuration("startup.timeout", null)); + assertThrows(ConditionTimeoutException.class, app::start, "That application shouldn't start!"); + app.logs().assertContains("Type of the artifact is POM, skipping dev goal"); + } + private void assertBuildError(Result result, String expectedError) { assertTrue(result.getOutput().contains(expectedError), "Unexpected build error message"); } @@ -300,7 +326,6 @@ private void assertDockerJavaVersion(File dockerFile, String expectedVersion) { Assertions.assertTrue(line.contains("openjdk-" + expectedVersion), DOCKERFILE_JVM + " doesn't contains expected version " + expectedVersion); - } catch (FileNotFoundException e) { fail(e.getMessage()); } diff --git a/quarkus-cli/src/test/resources/test.properties b/quarkus-cli/src/test/resources/test.properties index d21d539da..b3e5317df 100644 --- a/quarkus-cli/src/test/resources/test.properties +++ b/quarkus-cli/src/test/resources/test.properties @@ -1 +1,4 @@ ts.global.generated-service.enabled=false +#this app is not expected to start, so let's fail right after +ts.pomApp.startup.timeout=PT2s +ts.pomApp.startup.check-poll-interval=PT1s diff --git a/security/vertx-jwt/src/main/java/io/quarkus/ts/security/vertx/Application.java b/security/vertx-jwt/src/main/java/io/quarkus/ts/security/vertx/Application.java index 2fa065ab8..23aed9ffe 100644 --- a/security/vertx-jwt/src/main/java/io/quarkus/ts/security/vertx/Application.java +++ b/security/vertx-jwt/src/main/java/io/quarkus/ts/security/vertx/Application.java @@ -3,6 +3,9 @@ import static io.quarkus.ts.security.vertx.Application.AUTH.NO_SECURE; import static io.quarkus.ts.security.vertx.Application.AUTH.SECURE; +import java.time.Duration; +import java.util.UUID; + import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Observes; import javax.inject.Inject; @@ -17,7 +20,11 @@ import io.quarkus.ts.security.vertx.handlers.BladeRunnerHandler; import io.quarkus.ts.security.vertx.handlers.JWTHandler; import io.quarkus.ts.security.vertx.handlers.ReplicantHandler; +import io.quarkus.ts.security.vertx.model.HelloEvent; +import io.quarkus.vertx.ConsumeEvent; +import io.smallrye.mutiny.Multi; import io.vertx.core.Handler; +import io.vertx.core.eventbus.EventBus; import io.vertx.core.http.HttpMethod; import io.vertx.ext.auth.jwt.JWTAuth; import io.vertx.ext.web.Route; @@ -29,10 +36,13 @@ import io.vertx.ext.web.handler.LoggerHandler; @ApplicationScoped -public class Application { +public class Application extends CommonApplication { private static final Logger LOG = Logger.getLogger(Application.class); + @Inject + EventBus eventBus; + @ConfigProperty(name = "app.name") public String serviceName; @@ -66,8 +76,12 @@ void init(@Observes Router router) { } void onStart(@Observes StartupEvent ev) { + LOG.info(String.format("Application %s starting...", serviceName)); + Multi.createFrom().ticks().every(Duration.ofMillis(2000)) + .subscribe().with((Long tick) -> eventBus.publish(ADDRESS, new HelloEvent(UUID.randomUUID().toString()))); + addRoute(HttpMethod.POST, "/bladeRunner", SECURE, rc -> bladeRunner.upsertBladeRunner(rc)); addRoute(HttpMethod.GET, "/bladeRunner/:id", SECURE, rc -> bladeRunner.getBladeRunnerById(rc)); addRoute(HttpMethod.GET, "/bladeRunner", SECURE, rc -> bladeRunner.getAllBladeRunner(rc)); @@ -98,4 +112,10 @@ private void addRoute(HttpMethod method, String path, AUTH authEnabled, Handler< route.handler(handler).failureHandler(rc -> failureHandler.handler(rc)); } + + @Override + @ConsumeEvent(ADDRESS) + public void consumeEventBusEvent(HelloEvent event) { + LOG.infof("Consuming generated HelloEvent at starting point. Msg value: %s", event.getMessage()); + } } diff --git a/security/vertx-jwt/src/main/java/io/quarkus/ts/security/vertx/CommonApplication.java b/security/vertx-jwt/src/main/java/io/quarkus/ts/security/vertx/CommonApplication.java new file mode 100644 index 000000000..24078dea1 --- /dev/null +++ b/security/vertx-jwt/src/main/java/io/quarkus/ts/security/vertx/CommonApplication.java @@ -0,0 +1,8 @@ +package io.quarkus.ts.security.vertx; + +public abstract class CommonApplication { + + public static final String ADDRESS = "greeting"; + + public abstract void consumeEventBusEvent(T event); +} diff --git a/security/vertx-jwt/src/main/java/io/quarkus/ts/security/vertx/model/HelloEvent.java b/security/vertx-jwt/src/main/java/io/quarkus/ts/security/vertx/model/HelloEvent.java new file mode 100644 index 000000000..4f037c30d --- /dev/null +++ b/security/vertx-jwt/src/main/java/io/quarkus/ts/security/vertx/model/HelloEvent.java @@ -0,0 +1,14 @@ +package io.quarkus.ts.security.vertx.model; + +public class HelloEvent { + + final String message; + + public HelloEvent(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/security/vertx-jwt/src/test/java/io/quarkus/ts/security/vertx/BladeRunnerHandlerIT.java b/security/vertx-jwt/src/test/java/io/quarkus/ts/security/vertx/BladeRunnerHandlerIT.java index 458531d4d..0c7d35600 100644 --- a/security/vertx-jwt/src/test/java/io/quarkus/ts/security/vertx/BladeRunnerHandlerIT.java +++ b/security/vertx-jwt/src/test/java/io/quarkus/ts/security/vertx/BladeRunnerHandlerIT.java @@ -2,6 +2,11 @@ import static org.hamcrest.Matchers.is; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -47,4 +52,16 @@ public void deleteBladeRunner() { .then() .statusCode(404); } + + @Tag("QUARKUS-2746 ") + @Test + public void verifyConsumeEventAnnotation() { + List actualLogs = app.getLogs(); + List helloEvents = actualLogs.stream() + .filter(l -> l.contains("Consuming generated HelloEvent at starting point")) + .collect(Collectors.toList()); + + Assertions.assertTrue(new HashSet<>(helloEvents).size() == helloEvents.size(), + "@ConsumeEvent annotation should be invoked once per event"); + } } diff --git a/spring/spring-data/pom.xml b/spring/spring-data/pom.xml index 897c14c3d..43bc8b8c9 100644 --- a/spring/spring-data/pom.xml +++ b/spring/spring-data/pom.xml @@ -35,6 +35,10 @@ io.quarkus quarkus-hibernate-validator + + io.quarkus + quarkus-elytron-security-properties-file + io.quarkus.qe quarkus-test-service-database diff --git a/spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/DenyAllRepository.java b/spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/DenyAllRepository.java new file mode 100644 index 000000000..d8a17af99 --- /dev/null +++ b/spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/DenyAllRepository.java @@ -0,0 +1,27 @@ +package io.quarkus.ts.spring.data.rest.secured; + +import java.util.Optional; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.data.rest.core.annotation.RestResource; + +import io.quarkus.ts.spring.data.rest.Library; + +@RepositoryRestResource(path = "/secured/deny-all") +@DenyAll +public interface DenyAllRepository extends CrudRepository { + @Override + @RestResource + @PermitAll + Iterable findAll(); + + @Override + @RestResource + @RolesAllowed("admin") + Optional findById(Long aLong); +} diff --git a/spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/PermitAllRepository.java b/spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/PermitAllRepository.java new file mode 100644 index 000000000..c5cfdac3a --- /dev/null +++ b/spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/PermitAllRepository.java @@ -0,0 +1,21 @@ +package io.quarkus.ts.spring.data.rest.secured; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.data.rest.core.annotation.RestResource; + +import io.quarkus.ts.spring.data.rest.Library; + +@RepositoryRestResource(path = "/secured/permit-all") +@PermitAll +public interface PermitAllRepository extends JpaRepository { + @Override + @RestResource + @DenyAll + Page findAll(Pageable pageable); +} diff --git a/spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/PublicRepository.java b/spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/PublicRepository.java new file mode 100644 index 000000000..d6235a84b --- /dev/null +++ b/spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/PublicRepository.java @@ -0,0 +1,19 @@ +package io.quarkus.ts.spring.data.rest.secured; + +import javax.annotation.security.DenyAll; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.data.rest.core.annotation.RestResource; + +import io.quarkus.ts.spring.data.rest.Library; + +@RepositoryRestResource(path = "/secured/public") +public interface PublicRepository extends PagingAndSortingRepository { + @Override + @RestResource + @DenyAll + Page findAll(Pageable pageable); +} diff --git a/spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/RolesAllowedRepository.java b/spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/RolesAllowedRepository.java new file mode 100644 index 000000000..487ca3d05 --- /dev/null +++ b/spring/spring-data/src/main/java/io/quarkus/ts/spring/data/rest/secured/RolesAllowedRepository.java @@ -0,0 +1,19 @@ +package io.quarkus.ts.spring.data.rest.secured; + +import java.util.Optional; + +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +import io.quarkus.ts.spring.data.rest.Library; + +@RepositoryRestResource(path = "/secured/roles-allowed") +@RolesAllowed("admin") +public interface RolesAllowedRepository extends CrudRepository { + @Override + @PermitAll + Optional findById(Long aLong); +} diff --git a/spring/spring-data/src/main/resources/application.properties b/spring/spring-data/src/main/resources/application.properties index 1a9b6af43..5ca59acbd 100644 --- a/spring/spring-data/src/main/resources/application.properties +++ b/spring/spring-data/src/main/resources/application.properties @@ -1,3 +1,12 @@ quarkus.datasource.db-kind=postgresql quarkus.hibernate-orm.database.generation=drop-and-create quarkus.hibernate-orm.sql-load-script=import.sql + +# Basic security setup +quarkus.http.auth.basic=true +quarkus.security.users.embedded.enabled=true +quarkus.security.users.embedded.plain-text=true +quarkus.security.users.embedded.users.admin=admin +quarkus.security.users.embedded.users.user=user +quarkus.security.users.embedded.roles.admin=admin +quarkus.security.users.embedded.roles.user=user diff --git a/spring/spring-data/src/test/java/io/quarkus/ts/spring/data/rest/secured/OpenShiftSecuredRepositoryRestResourcesIT.java b/spring/spring-data/src/test/java/io/quarkus/ts/spring/data/rest/secured/OpenShiftSecuredRepositoryRestResourcesIT.java new file mode 100644 index 000000000..e36f53ca0 --- /dev/null +++ b/spring/spring-data/src/test/java/io/quarkus/ts/spring/data/rest/secured/OpenShiftSecuredRepositoryRestResourcesIT.java @@ -0,0 +1,7 @@ +package io.quarkus.ts.spring.data.rest.secured; + +import io.quarkus.test.scenarios.OpenShiftScenario; + +@OpenShiftScenario +public class OpenShiftSecuredRepositoryRestResourcesIT extends SecuredRepositoryRestResourcesIT { +} diff --git a/spring/spring-data/src/test/java/io/quarkus/ts/spring/data/rest/secured/SecuredRepositoryRestResourcesIT.java b/spring/spring-data/src/test/java/io/quarkus/ts/spring/data/rest/secured/SecuredRepositoryRestResourcesIT.java new file mode 100644 index 000000000..b1f2dd242 --- /dev/null +++ b/spring/spring-data/src/test/java/io/quarkus/ts/spring/data/rest/secured/SecuredRepositoryRestResourcesIT.java @@ -0,0 +1,160 @@ +package io.quarkus.ts.spring.data.rest.secured; + +import static io.restassured.RestAssured.given; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.scenarios.annotations.DisabledOnQuarkusVersion; +import io.quarkus.ts.spring.data.AbstractDbIT; +import io.restassured.http.ContentType; + +@Tag("QUARKUS-2788") +@QuarkusScenario +public class SecuredRepositoryRestResourcesIT extends AbstractDbIT { + private static final long NONEXISTENT_ENTITY_ID = 999; + private static final String BASE_URL = "/secured"; + + private String getUrl(String path) { + return BASE_URL + path; + } + + @Test + void publicResourceNoAuth() { + app.given() + .accept(ContentType.JSON) + .get(getUrl("/public/" + NONEXISTENT_ENTITY_ID)) + .then().statusCode(HttpStatus.SC_NOT_FOUND); + } + + @Test + void publicResourceDenyAllMethodNoAuth() { + app.given() + .accept(ContentType.JSON) + .get(getUrl("/public")) + .then().statusCode(HttpStatus.SC_UNAUTHORIZED); + } + + @Test + void publicResourceDenyAllMethodAuth() { + app.given() + .accept(ContentType.JSON) + .auth().basic("admin", "admin") + .get(getUrl("/public")) + .then().statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + void denyAllResourceNoAuth() { + app.given() + .accept(ContentType.JSON) + .delete(getUrl("/deny-all/" + NONEXISTENT_ENTITY_ID)) + .then().statusCode(HttpStatus.SC_UNAUTHORIZED); + } + + @Test + void denyAllResourceAuth() { + app.given() + .accept(ContentType.JSON) + .auth().basic("admin", "admin") + .delete(getUrl("/deny-all/" + NONEXISTENT_ENTITY_ID)) + .then().statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + void denyAllResourcePermitAllMethodNoAuth() { + given() + .accept(ContentType.JSON) + .get(getUrl("/deny-all")) + .then().statusCode(HttpStatus.SC_OK); + } + + @Test + void denyAllResourceRolesAllowedMethodNoAuth() { + app.given() + .accept(ContentType.JSON) + .get(getUrl("/deny-all/" + NONEXISTENT_ENTITY_ID)) + .then().statusCode(HttpStatus.SC_UNAUTHORIZED); + } + + @Test + void denyAllResourceRolesAllowedMethodAuthForbidden() { + app.given() + .accept(ContentType.JSON) + .auth().basic("user", "user") + .get(getUrl("/deny-all/" + NONEXISTENT_ENTITY_ID)) + .then().statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + void denyAllResourceRolesAllowedMethodAuthPermitted() { + app.given() + .accept(ContentType.JSON) + .auth().basic("admin", "admin") + .get(getUrl("/deny-all/" + NONEXISTENT_ENTITY_ID)) + .then().statusCode(HttpStatus.SC_NOT_FOUND); + } + + @Test + void permitAllResourceNoAuth() { + app.given() + .accept(ContentType.JSON) + .get(getUrl("/permit-all/" + NONEXISTENT_ENTITY_ID)) + .then().statusCode(HttpStatus.SC_NOT_FOUND); + } + + @Test + void permitAllResourceDenyAllMethodNoAuth() { + app.given() + .accept(ContentType.JSON) + .get(getUrl("/permit-all")) + .then().statusCode(HttpStatus.SC_UNAUTHORIZED); + } + + @Test + void permitAllResourceDenyAllMethodAuth() { + app.given() + .accept(ContentType.JSON) + .auth().basic("admin", "admin") + .get(getUrl("/permit-all")) + .then().statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + void rolesAllowedResourceNoAuth() { + app.given() + .accept(ContentType.JSON) + .get(getUrl("/roles-allowed")) + .then().statusCode(HttpStatus.SC_UNAUTHORIZED); + } + + @Test + void rolesAllowedResourceAuthForbidden() { + app.given() + .accept(ContentType.JSON) + .auth().basic("user", "user") + .get(getUrl("/roles-allowed")) + .then().statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + void rolesAllowedResourceAuthPermitted() { + app.given() + .accept(ContentType.JSON) + .auth().basic("admin", "admin") + .get(getUrl("/roles-allowed")) + .then().statusCode(HttpStatus.SC_OK); + } + + // Fix for https://github.com/quarkusio/quarkus/issues/30358 has been backported to Quarkus 2.13 and 2.16, but not to 2.14 and 2.15. + @DisabledOnQuarkusVersion(version = "(2\\.14\\..*)|(2\\.15\\..*)", reason = "https://github.com/quarkusio/quarkus/issues/30358") + @Test + void rolesAllowedResourcePermitAllMethodWithoutRestResourceAnnotation() { + app.given() + .accept(ContentType.JSON) + .get(getUrl("/roles-allowed/" + NONEXISTENT_ENTITY_ID)) + .then().statusCode(HttpStatus.SC_NOT_FOUND); + } +} diff --git a/sql-db/narayana-transactions/pom.xml b/sql-db/narayana-transactions/pom.xml new file mode 100644 index 000000000..1fb529a35 --- /dev/null +++ b/sql-db/narayana-transactions/pom.xml @@ -0,0 +1,112 @@ + + + 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-opentelemetry-exporter-otlp + + + + 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 diff --git a/sql-db/panache-flyway/pom.xml b/sql-db/panache-flyway/pom.xml index 757a4c5fa..069dec6cd 100644 --- a/sql-db/panache-flyway/pom.xml +++ b/sql-db/panache-flyway/pom.xml @@ -35,6 +35,10 @@ io.quarkus quarkus-hibernate-validator + + io.quarkus + quarkus-elytron-security-properties-file + org.testcontainers mysql diff --git a/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityDenyAllResource.java b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityDenyAllResource.java new file mode 100644 index 000000000..2e9becb26 --- /dev/null +++ b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityDenyAllResource.java @@ -0,0 +1,30 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import java.util.List; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; + +import io.quarkus.hibernate.orm.rest.data.panache.PanacheEntityResource; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import io.quarkus.rest.data.panache.MethodProperties; +import io.quarkus.rest.data.panache.ResourceProperties; +import io.quarkus.ts.sqldb.panacheflyway.ApplicationEntity; + +@ResourceProperties(path = "/secured/entity/deny-all") +@DenyAll +public interface EntityDenyAllResource extends PanacheEntityResource { + @Override + @PermitAll + long count(); + + @Override + @RolesAllowed("admin") + List list(Page page, Sort sort); + + @Override + @MethodProperties(rolesAllowed = "admin") + ApplicationEntity get(Long aLong); +} diff --git a/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityPermitAllResource.java b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityPermitAllResource.java new file mode 100644 index 000000000..3806acd61 --- /dev/null +++ b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityPermitAllResource.java @@ -0,0 +1,16 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; + +import io.quarkus.hibernate.orm.rest.data.panache.PanacheEntityResource; +import io.quarkus.rest.data.panache.ResourceProperties; +import io.quarkus.ts.sqldb.panacheflyway.ApplicationEntity; + +@ResourceProperties(path = "/secured/entity/permit-all") +@PermitAll +public interface EntityPermitAllResource extends PanacheEntityResource { + @Override + @DenyAll + long count(); +} diff --git a/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityPublicResource.java b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityPublicResource.java new file mode 100644 index 000000000..9dbf39641 --- /dev/null +++ b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityPublicResource.java @@ -0,0 +1,14 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import javax.annotation.security.DenyAll; + +import io.quarkus.hibernate.orm.rest.data.panache.PanacheEntityResource; +import io.quarkus.rest.data.panache.ResourceProperties; +import io.quarkus.ts.sqldb.panacheflyway.ApplicationEntity; + +@ResourceProperties(path = "/secured/entity/public") +public interface EntityPublicResource extends PanacheEntityResource { + @Override + @DenyAll + long count(); +} diff --git a/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityResourcePropertiesRolesAllowedResource.java b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityResourcePropertiesRolesAllowedResource.java new file mode 100644 index 000000000..5f6ec25b8 --- /dev/null +++ b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityResourcePropertiesRolesAllowedResource.java @@ -0,0 +1,9 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import io.quarkus.hibernate.orm.rest.data.panache.PanacheEntityResource; +import io.quarkus.rest.data.panache.ResourceProperties; +import io.quarkus.ts.sqldb.panacheflyway.ApplicationEntity; + +@ResourceProperties(path = "/secured/entity/resource-properties-roles-allowed", rolesAllowed = "admin") +public interface EntityResourcePropertiesRolesAllowedResource extends PanacheEntityResource { +} diff --git a/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityRolesAllowedResource.java b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityRolesAllowedResource.java new file mode 100644 index 000000000..d0141681f --- /dev/null +++ b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/EntityRolesAllowedResource.java @@ -0,0 +1,12 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import javax.annotation.security.RolesAllowed; + +import io.quarkus.hibernate.orm.rest.data.panache.PanacheEntityResource; +import io.quarkus.rest.data.panache.ResourceProperties; +import io.quarkus.ts.sqldb.panacheflyway.ApplicationEntity; + +@ResourceProperties(path = "/secured/entity/roles-allowed") +@RolesAllowed("admin") +public interface EntityRolesAllowedResource extends PanacheEntityResource { +} diff --git a/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryDenyAllResource.java b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryDenyAllResource.java new file mode 100644 index 000000000..ee2256d19 --- /dev/null +++ b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryDenyAllResource.java @@ -0,0 +1,31 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import java.util.List; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; + +import io.quarkus.hibernate.orm.rest.data.panache.PanacheRepositoryResource; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import io.quarkus.rest.data.panache.MethodProperties; +import io.quarkus.rest.data.panache.ResourceProperties; +import io.quarkus.ts.sqldb.panacheflyway.UserEntity; +import io.quarkus.ts.sqldb.panacheflyway.UserRepository; + +@ResourceProperties(path = "/secured/repository/deny-all") +@DenyAll +public interface RepositoryDenyAllResource extends PanacheRepositoryResource { + @Override + @PermitAll + long count(); + + @Override + @RolesAllowed("admin") + List list(Page page, Sort sort); + + @Override + @MethodProperties(rolesAllowed = "admin") + UserEntity get(Long aLong); +} diff --git a/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryPermitAllResource.java b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryPermitAllResource.java new file mode 100644 index 000000000..6cd3dbd93 --- /dev/null +++ b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryPermitAllResource.java @@ -0,0 +1,17 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; + +import io.quarkus.hibernate.orm.rest.data.panache.PanacheRepositoryResource; +import io.quarkus.rest.data.panache.ResourceProperties; +import io.quarkus.ts.sqldb.panacheflyway.UserEntity; +import io.quarkus.ts.sqldb.panacheflyway.UserRepository; + +@ResourceProperties(path = "/secured/repository/permit-all") +@PermitAll +public interface RepositoryPermitAllResource extends PanacheRepositoryResource { + @Override + @DenyAll + long count(); +} diff --git a/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryPublicResource.java b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryPublicResource.java new file mode 100644 index 000000000..c88bd054d --- /dev/null +++ b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryPublicResource.java @@ -0,0 +1,15 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import javax.annotation.security.DenyAll; + +import io.quarkus.hibernate.orm.rest.data.panache.PanacheRepositoryResource; +import io.quarkus.rest.data.panache.ResourceProperties; +import io.quarkus.ts.sqldb.panacheflyway.UserEntity; +import io.quarkus.ts.sqldb.panacheflyway.UserRepository; + +@ResourceProperties(path = "/secured/repository/public") +public interface RepositoryPublicResource extends PanacheRepositoryResource { + @Override + @DenyAll + long count(); +} diff --git a/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryResourcePropertiesRolesAllowedResource.java b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryResourcePropertiesRolesAllowedResource.java new file mode 100644 index 000000000..a0a8b5a5b --- /dev/null +++ b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryResourcePropertiesRolesAllowedResource.java @@ -0,0 +1,11 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import io.quarkus.hibernate.orm.rest.data.panache.PanacheRepositoryResource; +import io.quarkus.rest.data.panache.ResourceProperties; +import io.quarkus.ts.sqldb.panacheflyway.UserEntity; +import io.quarkus.ts.sqldb.panacheflyway.UserRepository; + +@ResourceProperties(path = "/secured/repository/resource-properties-roles-allowed", rolesAllowed = "admin") +public interface RepositoryResourcePropertiesRolesAllowedResource + extends PanacheRepositoryResource { +} diff --git a/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryRolesAllowedResource.java b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryRolesAllowedResource.java new file mode 100644 index 000000000..a2c0da618 --- /dev/null +++ b/sql-db/panache-flyway/src/main/java/io/quarkus/ts/sqldb/panacheflyway/secured/RepositoryRolesAllowedResource.java @@ -0,0 +1,13 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import javax.annotation.security.RolesAllowed; + +import io.quarkus.hibernate.orm.rest.data.panache.PanacheRepositoryResource; +import io.quarkus.rest.data.panache.ResourceProperties; +import io.quarkus.ts.sqldb.panacheflyway.UserEntity; +import io.quarkus.ts.sqldb.panacheflyway.UserRepository; + +@ResourceProperties(path = "/secured/repository/roles-allowed") +@RolesAllowed("admin") +public interface RepositoryRolesAllowedResource extends PanacheRepositoryResource { +} diff --git a/sql-db/panache-flyway/src/main/resources/application.properties b/sql-db/panache-flyway/src/main/resources/application.properties index 3164cab32..3409020b5 100644 --- a/sql-db/panache-flyway/src/main/resources/application.properties +++ b/sql-db/panache-flyway/src/main/resources/application.properties @@ -30,3 +30,12 @@ quarkus.flyway.schemas=test %agroal_pool_test.quarkus.datasource.jdbc.acquisition-timeout=60S %agroal_pool_test.quarkus.datasource.jdbc.validation-query-sql=SELECT CURRENT_TIMESTAMP %agroal_pool_test.quarkus.datasource.jdbc.new-connection-sql=SELECT CURRENT_TIMESTAMP + +# Basic security setup +quarkus.http.auth.basic=true +quarkus.security.users.embedded.enabled=true +quarkus.security.users.embedded.plain-text=true +quarkus.security.users.embedded.users.admin=admin +quarkus.security.users.embedded.users.user=user +quarkus.security.users.embedded.roles.admin=admin +quarkus.security.users.embedded.roles.user=user diff --git a/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/AbstractSecuredPanacheResourceIT.java b/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/AbstractSecuredPanacheResourceIT.java new file mode 100644 index 000000000..660617010 --- /dev/null +++ b/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/AbstractSecuredPanacheResourceIT.java @@ -0,0 +1,176 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import static io.restassured.RestAssured.given; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; + +import io.quarkus.ts.sqldb.panacheflyway.PanacheWithFlywayBaseIT; + +public abstract class AbstractSecuredPanacheResourceIT extends PanacheWithFlywayBaseIT { + private static final long NONEXISTENT_ENTITY_ID = 999; + + protected abstract String getBaseUrl(); + + private String getUrl(String path) { + return getBaseUrl() + path; + } + + @Test + void publicResourceNoAuth() { + given() + .get(getUrl("/public")) + .then().statusCode(HttpStatus.SC_OK); + } + + @Test + void publicResourceDenyAllMethodNoAuth() { + given() + .get(getUrl("/public/count")) + .then().statusCode(HttpStatus.SC_UNAUTHORIZED); + } + + @Test + void publicResourceDenyAllMethodAuth() { + given() + .auth().basic("admin", "admin") + .get(getUrl("/public/count")) + .then().statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + void denyAllResourceNoAuth() { + given() + .delete(getUrl("/deny-all/" + NONEXISTENT_ENTITY_ID)) + .then().statusCode(HttpStatus.SC_UNAUTHORIZED); + } + + @Test + void denyAllResourceAuth() { + given() + .auth().basic("admin", "admin") + .delete(getUrl("/deny-all/" + NONEXISTENT_ENTITY_ID)) + .then().statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + void denyAllResourcePermitAllMethodNoAuth() { + given() + .get(getUrl("/deny-all/count")) + .then().statusCode(HttpStatus.SC_OK); + } + + @Test + void denyAllResourceRolesAllowedMethodNoAuth() { + given() + .get(getUrl("/deny-all")) + .then().statusCode(HttpStatus.SC_UNAUTHORIZED); + } + + @Test + void denyAllResourceRolesAllowedMethodAuthForbidden() { + given() + .auth().basic("user", "user") + .get(getUrl("/deny-all")) + .then().statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + void denyAllResourceRolesAllowedMethodAuthPermitted() { + given() + .auth().basic("admin", "admin") + .get(getUrl("/deny-all")) + .then().statusCode(HttpStatus.SC_OK); + } + + @Test + void denyAllResourcePropertiesRolesAllowedMethodNoAuth() { + given() + .get(getUrl("/deny-all/" + NONEXISTENT_ENTITY_ID)) + .then().statusCode(HttpStatus.SC_UNAUTHORIZED); + } + + @Test + void denyAllResourcePropertiesRolesAllowedMethodAuthForbidden() { + given() + .auth().basic("user", "user") + .get(getUrl("/deny-all/" + NONEXISTENT_ENTITY_ID)) + .then().statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + void denyAllResourcePropertiesRolesAllowedMethodAuthPermitted() { + given() + .auth().basic("admin", "admin") + .get(getUrl("/deny-all/" + NONEXISTENT_ENTITY_ID)) + .then().statusCode(HttpStatus.SC_NOT_FOUND); + } + + @Test + void permitAllResourceNoAuth() { + given() + .get(getUrl("/permit-all")) + .then().statusCode(HttpStatus.SC_OK); + } + + @Test + void permitAllResourceDenyAllMethodNoAuth() { + given() + .get(getUrl("/permit-all/count")) + .then().statusCode(HttpStatus.SC_UNAUTHORIZED); + } + + @Test + void permitAllResourceDenyAllMethodAuth() { + given() + .auth().basic("admin", "admin") + .get(getUrl("/permit-all/count")) + .then().statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + void propertiesRolesAllowedResourceNoAuth() { + given() + .get(getUrl("/resource-properties-roles-allowed")) + .then().statusCode(HttpStatus.SC_UNAUTHORIZED); + } + + @Test + void propertiesRolesAllowedResourceAuthForbidden() { + given() + .auth().basic("user", "user") + .get(getUrl("/resource-properties-roles-allowed")) + .then().statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + void propertiesRolesAllowedResourceAuthPermitted() { + given() + .auth().basic("admin", "admin") + .get(getUrl("/resource-properties-roles-allowed")) + .then().statusCode(HttpStatus.SC_OK); + } + + @Test + void rolesAllowedResourceNoAuth() { + given() + .get(getUrl("/roles-allowed")) + .then().statusCode(HttpStatus.SC_UNAUTHORIZED); + } + + @Test + void rolesAllowedResourceAuthForbidden() { + given() + .auth().basic("user", "user") + .get(getUrl("/roles-allowed")) + .then().statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + void rolesAllowedResourceAuthPermitted() { + given() + .auth().basic("admin", "admin") + .get(getUrl("/roles-allowed")) + .then().statusCode(HttpStatus.SC_OK); + } +} diff --git a/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/OpenShiftSecuredPanacheEntityResourceIT.java b/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/OpenShiftSecuredPanacheEntityResourceIT.java new file mode 100644 index 000000000..fc555a7a9 --- /dev/null +++ b/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/OpenShiftSecuredPanacheEntityResourceIT.java @@ -0,0 +1,7 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import io.quarkus.test.scenarios.OpenShiftScenario; + +@OpenShiftScenario +public class OpenShiftSecuredPanacheEntityResourceIT extends SecuredPanacheEntityResourceIT { +} diff --git a/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/OpenShiftSecuredPanacheRepositoryResourceIT.java b/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/OpenShiftSecuredPanacheRepositoryResourceIT.java new file mode 100644 index 000000000..1ce44a269 --- /dev/null +++ b/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/OpenShiftSecuredPanacheRepositoryResourceIT.java @@ -0,0 +1,7 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import io.quarkus.test.scenarios.OpenShiftScenario; + +@OpenShiftScenario +public class OpenShiftSecuredPanacheRepositoryResourceIT extends SecuredPanacheRepositoryResourceIT { +} diff --git a/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/SecuredPanacheEntityResourceIT.java b/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/SecuredPanacheEntityResourceIT.java new file mode 100644 index 000000000..d588da07c --- /dev/null +++ b/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/SecuredPanacheEntityResourceIT.java @@ -0,0 +1,16 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import org.junit.jupiter.api.Tag; + +import io.quarkus.test.scenarios.QuarkusScenario; + +@Tag("QUARKUS-2788") +@QuarkusScenario +public class SecuredPanacheEntityResourceIT extends AbstractSecuredPanacheResourceIT { + private static final String BASE_URL = "/secured/entity"; + + @Override + protected String getBaseUrl() { + return BASE_URL; + } +} diff --git a/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/SecuredPanacheRepositoryResourceIT.java b/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/SecuredPanacheRepositoryResourceIT.java new file mode 100644 index 000000000..16f2bb543 --- /dev/null +++ b/sql-db/panache-flyway/src/test/java/io/quarkus/ts/sqldb/panacheflyway/secured/SecuredPanacheRepositoryResourceIT.java @@ -0,0 +1,16 @@ +package io.quarkus.ts.sqldb.panacheflyway.secured; + +import org.junit.jupiter.api.Tag; + +import io.quarkus.test.scenarios.QuarkusScenario; + +@Tag("QUARKUS-2788") +@QuarkusScenario +public class SecuredPanacheRepositoryResourceIT extends AbstractSecuredPanacheResourceIT { + private static final String BASE_URL = "/secured/repository"; + + @Override + protected String getBaseUrl() { + return BASE_URL; + } +}