diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e40af03 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +mvnw text eol=lf +*.xml text eol=lf +*.java text diff=java eol=lf diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml deleted file mode 100644 index fe95ecf..0000000 --- a/.gitea/workflows/build.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build, test and publish the Quarkus libraries -on: [ push, workflow_dispatch ] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout the code - uses: https://github.com/actions/checkout@v4 - - - name: Set up JDK 21 - uses: https://github.com/actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' - - - name: Configure Maven CI/CD settings - uses: https://github.com/s4u/maven-settings-action@v3.1.0 - with: - servers: | - [{ - "id": "kvant", - "configuration": { - "httpHeaders": { - "property": { - "name": "Authorization", - "value": "token ${{ secrets.PHOENIX_PACKAGE_WRITER_TOKEN }}" - } - } - } - }] - - - name: Make maven wrapper executable - run: chmod +x mvnw - - - name: Build and run tests - run: ./mvnw verify - - - name: Upload libs to Gitea Maven Registry - if: gitea.ref == 'refs/heads/main' - run: ./mvnw deploy -Dmaven.test.skip=true -Dmaven.javadoc.skip=true \ No newline at end of file diff --git a/.github/project.yaml b/.github/project.yaml new file mode 100644 index 0000000..0b40189 --- /dev/null +++ b/.github/project.yaml @@ -0,0 +1,5 @@ +name: Quarkus Commons +release: + current-version: "1.0.8" + next-version: "1.0.9-SNAPSHOT" + diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..1272b20 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,99 @@ +name: Build + +on: + push: + branches: + - "main" + tags: + - '[0-9]+.[0-9]+.[0-9]+' + paths-ignore: + - '.gitattributes' + - '.gitignore' + - '.github/renovate.json5' + - '.github/project.yaml' + - 'docs/**' + - 'README.md' + pull_request: + workflow_dispatch: + +env: + COMMON_MAVEN_OPTS: "-e -B --fae" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Set up JDK 21 + uses: https://github.com/actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Generate cache keys + id: cache-key + run: | + CURRENT_BRANCH="${{ github.head_ref || github.ref_name }}" + CURRENT_MONTH=$(/bin/date -u "+%Y-%m") + CURRENT_DAY=$(/bin/date -u "+%d") + ROOT_CACHE_KEY="m2-cache-quarkus-commons" + echo "m2-monthly-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}" >> $GITHUB_OUTPUT + echo "m2-monthly-branch-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}" >> $GITHUB_OUTPUT + echo "m2-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}-${CURRENT_DAY}" >> $GITHUB_OUTPUT + + - name: Cache Maven Repository + id: cache-maven + uses: actions/cache@v4 + with: + path: ~/.m2/repository + # The cache is per branch but in case we don't find a branch for a given branch, we will get a cache from another branch. + key: ${{ steps.cache-key.outputs.m2-cache-key }} + restore-keys: | + ${{ steps.cache-key.outputs.m2-monthly-branch-cache-key }} + ${{ steps.cache-key.outputs.m2-monthly-cache-key }} + + - name: Configure Maven CI/CD settings + uses: https://github.com/s4u/maven-settings-action@v3.1.0 + with: + servers: | + [{ + "id": "phoenix-oss", + "username": "${{ vars.ORG_PACKAGE_WRITER_USERNAME }}", + "password": "${{ secrets.ORG_PACKAGE_WRITER_TOKEN }}" + }] + + - name: Make maven wrapper executable + run: chmod +x mvnw + + - name: Validate tag + if: startsWith(github.ref, 'refs/tags/') + run: | + TAG_NAME="${GITHUB_REF#refs/tags/}" + PROJECT_VERSION=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout) + + if [[ "$PROJECT_VERSION" != "$TAG_NAME" ]]; then + echo "::error::pom.xml version '$PROJECT_VERSION' does not match tag '$TAG_NAME'" + exit 1 + fi + + - name: Download dependencies + run: ./mvnw $COMMON_MAVEN_OPTS quarkus:go-offline + + - name: Build and run tests + run: ./mvnw $COMMON_MAVEN_OPTS verify + + - name: Analyze with Sonar + if: vars.SONAR_ENABLED == 'true' + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }} + run: ./mvnw $COMMON_MAVEN_OPTS org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=quarkus-commons -Dsonar.projectName='quarkus-commons' -Dsonar.coverage.jacoco.xmlReportPaths=../**/target/jacoco-report/jacoco.xml + + - name: Publish jars + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') + run: ./mvnw $COMMON_MAVEN_OPTS deploy -Dmaven.test.skip=true -Dmaven.javadoc.skip=true \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..3043398 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,97 @@ +name: Release the current version + +on: + push: + branches: + - "main" + paths: + - '.github/project.yaml' + +env: + COMMON_MAVEN_OPTS: "-e -B --fae" + +jobs: + release: + name: Execute the release + runs-on: ubuntu-latest + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + + - name: Retrieve project metadata + uses: https://github.com/radcortez/project-metadata-action@main + id: metadata + with: + metadata-file-path: '.github/project.yaml' + local-file: true + + - name: Validate current version + if: contains(steps.metadata.outputs.current-version, 'SNAPSHOT') + run: | + echo '::error::Cannot release a SNAPSHOT version.' + exit 1 + + - name: Validate next version + if: contains(steps.metadata.outputs.next-version, 'SNAPSHOT') == 'false' + run: | + echo '::error::Next development version should be a SNAPSHOT version.' + exit 1 + + - name: Set environment variables + run: | + echo "CURRENT_VERSION=${{steps.metadata.outputs.current-version}}" >> $GITHUB_ENV + echo "NEXT_VERSION=${{steps.metadata.outputs.next-version}}" >> $GITHUB_ENV + + - name: Configure SSH and Git + env: + SSH_DIR: /root/.ssh + MAVEN_RELEASE_SSH_KEY: ${{ secrets.MAVEN_RELEASE_SSH_KEY }} + run: | + mkdir -p ${SSH_DIR}/ + touch ${SSH_DIR}/known_hosts + ssh-keyscan -t rsa -p 2222 git-ssh.kvant.cloud >> ${SSH_DIR}/known_hosts + echo "${MAVEN_RELEASE_SSH_KEY}" | base64 -d >> ${SSH_DIR}/id_rsa + chmod 600 ${SSH_DIR}/id_rsa + git config --global user.name 'maven_release_technical_account' + git config --global user.email 'maven-release-bot@phoenix-technologies.ch' + git config --global commit.gpgsign true + git config --global gpg.format ssh + git config --global user.signingkey ${SSH_DIR}/id_rsa + + - name: Set up JDK 21 + uses: https://github.com/actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Generate cache keys + id: cache-key + run: | + CURRENT_BRANCH="${{ github.ref_name }}" + CURRENT_MONTH=$(/bin/date -u "+%Y-%m") + CURRENT_DAY=$(/bin/date -u "+%d") + ROOT_CACHE_KEY="m2-cache-quarkus-commons" + echo "m2-monthly-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}" >> $GITHUB_OUTPUT + echo "m2-monthly-branch-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}" >> $GITHUB_OUTPUT + echo "m2-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}-${CURRENT_DAY}" >> $GITHUB_OUTPUT + + - name: Cache Maven Repository + id: cache-maven + uses: actions/cache@v4 + with: + path: ~/.m2/repository + # The cache is per branch but in case we don't find a branch for a given branch, we will get a cache from another branch. + key: ${{ steps.cache-key.outputs.m2-cache-key }} + restore-keys: | + ${{ steps.cache-key.outputs.m2-monthly-branch-cache-key }} + ${{ steps.cache-key.outputs.m2-monthly-cache-key }} + + - name: Make maven wrapper executable + run: chmod +x mvnw + + - name: Download dependencies + run: ./mvnw $COMMON_MAVEN_OPTS quarkus:go-offline + + - name: Prepare release + run: ./mvnw $COMMON_MAVEN_OPTS release:prepare -DreleaseVersion=${CURRENT_VERSION} -DdevelopmentVersion=${NEXT_VERSION} diff --git a/.github/workflows/validate-versions.yaml b/.github/workflows/validate-versions.yaml new file mode 100644 index 0000000..7828cbe --- /dev/null +++ b/.github/workflows/validate-versions.yaml @@ -0,0 +1,33 @@ +name: Validate release versions + +on: + pull_request: + paths: + - '.github/project.yaml' + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout the code + uses: https://github.com/actions/checkout@v4 + + - name: Retrieve project metadata + uses: https://github.com/radcortez/project-metadata-action@main + id: metadata + with: + metadata-file-path: '.github/project.yaml' + local-file: true + + - name: Validate current version + if: contains(steps.metadata.outputs.current-version, 'SNAPSHOT') + run: | + echo '::error::Cannot release a SNAPSHOT version.' + exit 1 + + - name: Validate next version + if: contains(steps.metadata.outputs.next-version, 'SNAPSHOT') == false + run: | + echo '::error::Next development version should be a SNAPSHOT version.' + exit 1 \ No newline at end of file diff --git a/README.md b/README.md index ed89049..1cfb1f7 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ Quarkus Commons =============== -[![Java version](https://img.shields.io/badge/Java%20version-21-brightgreen?style=for-the-badge)](https://openjdk.org/projects/jdk/21/) -[![Quarkus version](https://img.shields.io/badge/Quarkus%20version-3.16.2-brightgreen?style=for-the-badge)](https://quarkus.io/guides/) -[![Maven Wrapper](https://img.shields.io/badge/Maven%20version-3.9.9-brightgreen?style=for-the-badge)](https://maven.apache.org/) - - -* [Quarkus Commons](#quarkus-commons) -* [Introduction](#introduction) - +[![Java version](https://img.shields.io/badge/Java%20version-21-brightgreen)](https://openjdk.org/projects/jdk/21/) +[![Coverage](https://sonarqube.pub.basel.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=coverage&token=sqb_b56d9ea175c7f51f522ce63acd7fe7807643ac9e)](https://sonarqube.pub.basel.kvant.cloud/dashboard?id=quarkus-commons) +[![Duplicated Lines (%)](https://sonarqube.pub.basel.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=duplicated_lines_density&token=sqb_b56d9ea175c7f51f522ce63acd7fe7807643ac9e)](https://sonarqube.pub.basel.kvant.cloud/dashboard?id=quarkus-commons) +[![Quality Gate Status](https://sonarqube.pub.basel.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=alert_status&token=sqb_b56d9ea175c7f51f522ce63acd7fe7807643ac9e)](https://sonarqube.pub.basel.kvant.cloud/dashboard?id=quarkus-commons) +[![Security Rating](https://sonarqube.pub.basel.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=security_rating&token=sqb_b56d9ea175c7f51f522ce63acd7fe7807643ac9e)](https://sonarqube.pub.basel.kvant.cloud/dashboard?id=quarkus-commons) +[![Reliability Rating](https://sonarqube.pub.basel.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=reliability_rating&token=sqb_b56d9ea175c7f51f522ce63acd7fe7807643ac9e)](https://sonarqube.pub.basel.kvant.cloud/dashboard?id=quarkus-commons) +[![Maintainability Rating](https://sonarqube.pub.basel.kvant.cloud/api/project_badges/measure?project=quarkus-commons&metric=sqale_rating&token=sqb_b56d9ea175c7f51f522ce63acd7fe7807643ac9e)](https://sonarqube.pub.basel.kvant.cloud/dashboard?id=quarkus-commons) # Introduction @@ -17,9 +16,11 @@ that can be used by Quarkus applications. The modules are: +* `quarkus-audit-tools` * `quarkus-clock-service` * `quarkus-json-service` * `quarkus-message-digest-service` * `quarkus-random-number-generator` +* `quarkus-tracing-service` * `quarkus-uuid-generator` diff --git a/.gitea/CODEOWNERS b/docs/CODEOWNERS similarity index 100% rename from .gitea/CODEOWNERS rename to docs/CODEOWNERS diff --git a/pom.xml b/pom.xml index 8e34f06..3b61944 100644 --- a/pom.xml +++ b/pom.xml @@ -1,49 +1,59 @@ - + 4.0.0 - ch.phoenixtechnologies.quarkus + ch.phoenix.oss quarkus-commons - 0.1.0-SNAPSHOT + 1.0.9-SNAPSHOT pom + quarkus-audit-tools + quarkus-client-logger quarkus-clock-service quarkus-json-service quarkus-message-digest-service quarkus-random-number-generator + quarkus-tracing-service quarkus-uuid-generator - 21 - UTF-8 - UTF-8 - - 3.13.0 - 3.5.2 - 0.8.12 - + quarkus-bom io.quarkus.platform - 3.16.2 + 3.25.0 + + + 3.14.0 + 3.5.3 + 2.46.1 + 2.72.0 + 0.8.13 + 3.4.0 + 3.1.1 + 3.3.1 + + + 21 + UTF-8 + UTF-8 + true 5.1.0 - 3.26.3 + 3.27.3 - kvant - https://git.kvant.cloud/api/packages/phoenix/maven + phoenix-oss + https://git.kvant.cloud/api/packages/phoenix-oss/maven - kvant - https://git.kvant.cloud/api/packages/phoenix/maven + phoenix-oss + https://git.kvant.cloud/api/packages/phoenix-oss/maven @@ -59,11 +69,22 @@ + + scm:git:ssh://git@git-ssh.kvant.cloud:2222/phoenix-oss/quarkus-commons.git + scm:git:ssh://git@git-ssh.kvant.cloud:2222/phoenix-oss/quarkus-commons.git + https://git.kvant.cloud/phoenix-oss/quarkus-commons.git + HEAD + + io.quarkus quarkus-arc + + io.quarkus + quarkus-cyclonedx + org.assertj assertj-core @@ -117,7 +138,6 @@ -parameters - ${java.version} @@ -132,6 +152,68 @@ + + com.diffplug.spotless + spotless-maven-plugin + ${spotless-plugin.version} + + + + check + + compile + + + + + + + ${palantir-java-format.version} + + false + + + + + + io.smallrye + jandex-maven-plugin + ${jandex-plugin.version} + + + make-index + + jandex + + + + + + org.apache.maven.plugins + maven-release-plugin + ${release-plugin.version} + + @{project.version} + mvnw + chore: release @{releaseLabel} + chore: prepare for next development iteration + true + + + + org.apache.maven.plugins + maven-source-plugin + ${source-plugin.version} + + + attach-sources + verify + + jar-no-fork + + + + diff --git a/quarkus-audit-tools/pom.xml b/quarkus-audit-tools/pom.xml new file mode 100644 index 0000000..1a01d07 --- /dev/null +++ b/quarkus-audit-tools/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + + ch.phoenix.oss + quarkus-commons + 1.0.9-SNAPSHOT + + + quarkus-audit-tools + jar + + + + ch.phoenix.oss + quarkus-tracing-service + ${project.version} + + + io.quarkus + quarkus-hibernate-envers + + + io.quarkus + quarkus-hibernate-orm-panache + + + io.quarkus + quarkus-jdbc-postgresql + + + io.quarkus + quarkus-flyway + test + + + io.quarkus + quarkus-flyway-postgresql + test + + + io.quarkus + quarkus-elytron-security-properties-file + test + + + io.quarkus + quarkus-config-yaml + test + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-plugin.version} + + + jacoco-check + + check + + test + + ${project.build.directory}/jacoco-quarkus.exec + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 1 + + + + + + + + + + + + diff --git a/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditRevisionListener.java b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditRevisionListener.java new file mode 100644 index 0000000..29884f8 --- /dev/null +++ b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditRevisionListener.java @@ -0,0 +1,20 @@ +package ch.phoenix.oss.quarkus.commons.audit; + +import jakarta.enterprise.inject.spi.CDI; +import org.hibernate.envers.RevisionListener; + +public class AuditRevisionListener implements RevisionListener { + + @Override + public void newRevision(Object revisionEntity) { + var provider = CDI.current().select(RevisionContextProvider.class).get(); + + var rev = (Revision) revisionEntity; + rev.actor = provider.getActor(); + rev.traceId = provider.getTraceId(); + rev.spanId = provider.getSpanId(); + rev.requestId = provider.getRequestId(); + rev.clientIp = provider.getClientIp(); + rev.hostName = provider.getHostName(); + } +} diff --git a/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntity.java b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntity.java new file mode 100644 index 0000000..4540cbf --- /dev/null +++ b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntity.java @@ -0,0 +1,30 @@ +package ch.phoenix.oss.quarkus.commons.audit; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import java.time.Instant; +import org.hibernate.envers.NotAudited; + +/** + * The goal of this class is to have on the entity itself the exact same + * timestamps as the ones from revisions generated by envers. Because of that, + * we can't use @CreationTimestamp and @UpdateTimestamp, as those timestamp values + * are managed by different Hibernate classes, so the generated values will drift. + *

+ * Manually setting these values to match envers revisions would be error-prone, + * verbose and tedious. So, the recommendation is to implement triggers on the + * audit tables which will update the main entity whenever a revision is created. + * An example of how to do that can be found in this module's integration tests. + */ +@MappedSuperclass +public abstract class AuditedPanacheEntity extends PanacheEntity { + + @NotAudited + @Column(updatable = false) + public Instant createdAt; + + @NotAudited + @Column(updatable = false) + public Instant lastUpdatedAt; +} diff --git a/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityBase.java b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityBase.java new file mode 100644 index 0000000..a6114b3 --- /dev/null +++ b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityBase.java @@ -0,0 +1,30 @@ +package ch.phoenix.oss.quarkus.commons.audit; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import java.time.Instant; +import org.hibernate.envers.NotAudited; + +/** + * The goal of this class is to have on the entity itself the exact same + * timestamps as the ones from revisions generated by envers. Because of that, + * we can't use @CreationTimestamp and @UpdateTimestamp, as those timestamp values + * are managed by different Hibernate classes, so the generated values will drift. + *

+ * Manually setting these values to match envers revisions would be error-prone, + * verbose and tedious. So, the recommendation is to implement triggers on the + * audit tables which will update the main entity whenever a revision is created. + * An example of how to do that can be found in this module's integration tests. + */ +@MappedSuperclass +public abstract class AuditedPanacheEntityBase extends PanacheEntityBase { + + @NotAudited + @Column(updatable = false) + public Instant createdAt; + + @NotAudited + @Column(updatable = false) + public Instant lastUpdatedAt; +} diff --git a/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/DefaultRevisionContextProvider.java b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/DefaultRevisionContextProvider.java new file mode 100644 index 0000000..18ebba6 --- /dev/null +++ b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/DefaultRevisionContextProvider.java @@ -0,0 +1,67 @@ +package ch.phoenix.oss.quarkus.commons.audit; + +import ch.phoenix.oss.quarkus.commons.tracing.TracingService; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.quarkus.arc.DefaultBean; +import io.quarkus.arc.Unremovable; +import io.quarkus.logging.Log; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.net.InetAddress; +import java.net.UnknownHostException; + +@Unremovable +@DefaultBean +@ApplicationScoped +class DefaultRevisionContextProvider implements RevisionContextProvider { + + private static final String UNKNOWN = "unknown"; + + private final TracingService tracingService; + + @Inject + DefaultRevisionContextProvider(TracingService tracingService) { + this.tracingService = tracingService; + } + + @Override + @WithSpan + public String getActor() { + return tracingService.getActor(); + } + + @Override + @WithSpan + public String getTraceId() { + return tracingService.getTraceId(); + } + + @Override + @WithSpan + public String getSpanId() { + return tracingService.getSpanId(); + } + + @Override + @WithSpan + public String getRequestId() { + return tracingService.getRequestId(); + } + + @Override + @WithSpan + public String getClientIp() { + return tracingService.getClientIp(); + } + + @Override + @WithSpan + public String getHostName() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + Log.error("Unable to determine host name", e); + return UNKNOWN; + } + } +} diff --git a/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/Revision.java b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/Revision.java new file mode 100644 index 0000000..fa3085a --- /dev/null +++ b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/Revision.java @@ -0,0 +1,58 @@ +package ch.phoenix.oss.quarkus.commons.audit; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import jakarta.persistence.*; +import java.time.Instant; +import java.util.Objects; +import org.hibernate.envers.RevisionEntity; +import org.hibernate.envers.RevisionNumber; +import org.hibernate.envers.RevisionTimestamp; + +@Entity +@Table(name = "revinfo") +@RevisionEntity(AuditRevisionListener.class) +public class Revision extends PanacheEntityBase { + + @Id + @GeneratedValue + @RevisionNumber + public long rev; + + @RevisionTimestamp + @Column(nullable = false, updatable = false) + public Instant timestamp; + + @Column(updatable = false) + public String actor; + + @Column(updatable = false) + public String traceId; + + @Column(updatable = false) + public String spanId; + + @Column(updatable = false) + public String requestId; + + @Column(updatable = false) + public String clientIp; + + @Column(updatable = false) + public String hostName; + + @Override + public boolean equals(Object o) { + if (!(o instanceof Revision that)) return false; + return rev == that.rev; + } + + @Override + public int hashCode() { + return Objects.hashCode(rev); + } + + @Override + public String toString() { + return "Revision{rev=" + rev + '}'; + } +} diff --git a/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/RevisionContextProvider.java b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/RevisionContextProvider.java new file mode 100644 index 0000000..477c44b --- /dev/null +++ b/quarkus-audit-tools/src/main/java/ch/phoenix/oss/quarkus/commons/audit/RevisionContextProvider.java @@ -0,0 +1,16 @@ +package ch.phoenix.oss.quarkus.commons.audit; + +public interface RevisionContextProvider { + + String getActor(); + + String getTraceId(); + + String getSpanId(); + + String getRequestId(); + + String getClientIp(); + + String getHostName(); +} diff --git a/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityBaseTest.java b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityBaseTest.java new file mode 100644 index 0000000..f4ba0de --- /dev/null +++ b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityBaseTest.java @@ -0,0 +1,136 @@ +package ch.phoenix.oss.quarkus.commons.audit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.when; + +import ch.phoenix.oss.quarkus.commons.tracing.TracingService; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectSpy; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import java.time.Instant; +import org.hibernate.envers.AuditReaderFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +class AuditedPanacheEntityBaseTest { + + @Inject + EntityManager entityManager; + + @InjectSpy + TracingService tracingService; + + @SuppressWarnings("Convert2MethodRef") + @BeforeEach + void setup() { + QuarkusTransaction.requiringNew().run(() -> TestEntity2.deleteAll()); + } + + @Test + void persistAndUpdate() { + when(tracingService.getRequestId()).thenReturn("00000000-0000-0000-0000-000000000000"); + when(tracingService.getSpanId()).thenReturn("0000000000000000"); + when(tracingService.getTraceId()).thenReturn("00000000000000000000000000000000"); + when(tracingService.getActor()).thenReturn("unknown"); + when(tracingService.getClientIp()).thenReturn("unknown"); + + var now = Instant.now(); + + QuarkusTransaction.requiringNew().run(() -> { + var entity = new TestEntity2("something"); + entity.persist(); + }); + + QuarkusTransaction.requiringNew().run(() -> { + var entity = TestEntity2.findBySomething("something"); + assertAll( + () -> assertThat(entity.createdAt) + .as("createdAt should be after or equal to expected value") + .isAfterOrEqualTo(now), + () -> assertThat(entity.lastUpdatedAt) + .as("lastUpdatedAt should be equal to createdAt") + .isEqualTo(entity.createdAt)); + + var auditReader = AuditReaderFactory.get(entityManager); + + var revisions = auditReader.getRevisions(TestEntity2.class, entity.id); + assertThat(revisions).hasSize(1); + + var revInfo = entityManager.find(Revision.class, revisions.getFirst()); + assertThat(revInfo).isNotNull(); + + assertAll( + () -> assertThat(revInfo.timestamp) + .as("revision timestamp should be equal to entity's createdAt timestamp") + .isEqualTo(entity.createdAt), + () -> assertThat(revInfo.actor) + .as("actor should match expected value") + .isEqualTo("unknown"), + () -> assertThat(revInfo.traceId) + .as("traceId should match expected value") + .isEqualTo("00000000000000000000000000000000"), + () -> assertThat(revInfo.spanId) + .as("spanId should match expected value") + .isEqualTo("0000000000000000"), + () -> assertThat(revInfo.requestId) + .as("requestId should match expected value") + .isEqualTo("00000000-0000-0000-0000-000000000000"), + () -> assertThat(revInfo.clientIp) + .as("clientIp should match expected value") + .isEqualTo("unknown"), + () -> assertThat(revInfo.hostName) + .as("hostName should not be blank") + .isNotBlank()); + }); + + QuarkusTransaction.requiringNew().run(() -> { + var entity = TestEntity2.findBySomething("something"); + entity.something = "else"; + }); + + QuarkusTransaction.requiringNew().run(() -> { + var entity = TestEntity2.findBySomething("else"); + assertAll(() -> assertThat(entity.createdAt) + .as("createdAt should be before lastUpdatedAt") + .isBefore(entity.lastUpdatedAt)); + + var auditReader = AuditReaderFactory.get(entityManager); + + var revisions = auditReader.getRevisions(TestEntity2.class, entity.id); + assertThat(revisions).hasSize(2); + + Revision revInfo = Revision.findById(revisions.getLast()); + assertThat(revInfo).isNotNull(); + + assertAll( + () -> assertThat(revInfo.timestamp) + .as("revision timestamp should not be equal to entity's createdAt") + .isNotEqualTo(entity.createdAt), + () -> assertThat(revInfo.timestamp) + .as("revision timestamp should be equal to entity's lastUpdatedAt") + .isEqualTo(entity.lastUpdatedAt), + () -> assertThat(revInfo.actor) + .as("actor should match expected value") + .isEqualTo("unknown"), + () -> assertThat(revInfo.traceId) + .as("traceId should match expected value") + .isEqualTo("00000000000000000000000000000000"), + () -> assertThat(revInfo.spanId) + .as("spanId should match expected value") + .isEqualTo("0000000000000000"), + () -> assertThat(revInfo.requestId) + .as("requestId should match expected value") + .isEqualTo("00000000-0000-0000-0000-000000000000"), + () -> assertThat(revInfo.clientIp) + .as("clientIp should match expected value") + .isEqualTo("unknown"), + () -> assertThat(revInfo.hostName) + .as("hostName should not be blank") + .isNotBlank()); + }); + } +} diff --git a/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityTest.java b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityTest.java new file mode 100644 index 0000000..0f182ee --- /dev/null +++ b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/AuditedPanacheEntityTest.java @@ -0,0 +1,136 @@ +package ch.phoenix.oss.quarkus.commons.audit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.when; + +import ch.phoenix.oss.quarkus.commons.tracing.TracingService; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectSpy; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import java.time.Instant; +import org.hibernate.envers.AuditReaderFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +class AuditedPanacheEntityTest { + + @Inject + EntityManager entityManager; + + @InjectSpy + TracingService tracingService; + + @SuppressWarnings("Convert2MethodRef") + @BeforeEach + void setup() { + QuarkusTransaction.requiringNew().run(() -> TestEntity.deleteAll()); + } + + @Test + void persistAndUpdate() { + when(tracingService.getRequestId()).thenReturn("00000000-0000-0000-0000-000000000000"); + when(tracingService.getSpanId()).thenReturn("0000000000000000"); + when(tracingService.getTraceId()).thenReturn("00000000000000000000000000000000"); + when(tracingService.getActor()).thenReturn("unknown"); + when(tracingService.getClientIp()).thenReturn("unknown"); + + var now = Instant.now(); + + QuarkusTransaction.requiringNew().run(() -> { + var entity = new TestEntity("something"); + entity.persist(); + }); + + QuarkusTransaction.requiringNew().run(() -> { + var entity = TestEntity.findBySomething("something"); + assertAll( + () -> assertThat(entity.createdAt) + .as("createdAt should be after or equal to expected value") + .isAfterOrEqualTo(now), + () -> assertThat(entity.lastUpdatedAt) + .as("lastUpdatedAt should be equal to createdAt") + .isEqualTo(entity.createdAt)); + + var auditReader = AuditReaderFactory.get(entityManager); + + var revisions = auditReader.getRevisions(TestEntity.class, entity.id); + assertThat(revisions).hasSize(1); + + var revInfo = entityManager.find(Revision.class, revisions.getFirst()); + assertThat(revInfo).isNotNull(); + + assertAll( + () -> assertThat(revInfo.timestamp) + .as("revision timestamp should be equal to entity's createdAt timestamp") + .isEqualTo(entity.createdAt), + () -> assertThat(revInfo.actor) + .as("actor should match expected value") + .isEqualTo("unknown"), + () -> assertThat(revInfo.traceId) + .as("traceId should match expected value") + .isEqualTo("00000000000000000000000000000000"), + () -> assertThat(revInfo.spanId) + .as("spanId should match expected value") + .isEqualTo("0000000000000000"), + () -> assertThat(revInfo.requestId) + .as("requestId should match expected value") + .isEqualTo("00000000-0000-0000-0000-000000000000"), + () -> assertThat(revInfo.clientIp) + .as("clientIp should match expected value") + .isEqualTo("unknown"), + () -> assertThat(revInfo.hostName) + .as("hostName should not be blank") + .isNotBlank()); + }); + + QuarkusTransaction.requiringNew().run(() -> { + var entity = TestEntity.findBySomething("something"); + entity.something = "else"; + }); + + QuarkusTransaction.requiringNew().run(() -> { + var entity = TestEntity.findBySomething("else"); + assertAll(() -> assertThat(entity.createdAt) + .as("createdAt should be before lastUpdatedAt") + .isBefore(entity.lastUpdatedAt)); + + var auditReader = AuditReaderFactory.get(entityManager); + + var revisions = auditReader.getRevisions(TestEntity.class, entity.id); + assertThat(revisions).hasSize(2); + + Revision revInfo = Revision.findById(revisions.getLast()); + assertThat(revInfo).isNotNull(); + + assertAll( + () -> assertThat(revInfo.timestamp) + .as("revision timestamp should not be equal to entity's createdAt") + .isNotEqualTo(entity.createdAt), + () -> assertThat(revInfo.timestamp) + .as("revision timestamp should be equal to entity's lastUpdatedAt") + .isEqualTo(entity.lastUpdatedAt), + () -> assertThat(revInfo.actor) + .as("actor should match expected value") + .isEqualTo("unknown"), + () -> assertThat(revInfo.traceId) + .as("traceId should match expected value") + .isEqualTo("00000000000000000000000000000000"), + () -> assertThat(revInfo.spanId) + .as("spanId should match expected value") + .isEqualTo("0000000000000000"), + () -> assertThat(revInfo.requestId) + .as("requestId should match expected value") + .isEqualTo("00000000-0000-0000-0000-000000000000"), + () -> assertThat(revInfo.clientIp) + .as("clientIp should match expected value") + .isEqualTo("unknown"), + () -> assertThat(revInfo.hostName) + .as("hostName should not be blank") + .isNotBlank()); + }); + } +} diff --git a/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/DefaultRevisionContextProviderTest.java b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/DefaultRevisionContextProviderTest.java new file mode 100644 index 0000000..8ef3a73 --- /dev/null +++ b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/DefaultRevisionContextProviderTest.java @@ -0,0 +1,31 @@ +package ch.phoenix.oss.quarkus.commons.audit; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.Mockito.mockStatic; + +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import java.net.InetAddress; +import java.net.UnknownHostException; +import org.junit.jupiter.api.Test; + +@QuarkusTest +class DefaultRevisionContextProviderTest { + + @Inject + DefaultRevisionContextProvider underTest; + + @Test + void getHostName() { + assertThat(underTest.getHostName()).isNotBlank(); + } + + @Test + void getHostNameWhenUnknown() { + try (var inetMock = mockStatic(InetAddress.class)) { + inetMock.when(InetAddress::getLocalHost).thenThrow(new UnknownHostException("simulated failure")); + + assertThat(underTest.getHostName()).isEqualTo("unknown"); + } + } +} diff --git a/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/RevisionTest.java b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/RevisionTest.java new file mode 100644 index 0000000..d0f5d1f --- /dev/null +++ b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/RevisionTest.java @@ -0,0 +1,51 @@ +package ch.phoenix.oss.quarkus.commons.audit; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +@QuarkusTest +class RevisionTest { + + @Test + void testEquals() { + var r1 = new Revision(); + r1.rev = 1; + + var r2 = new Revision(); + r2.rev = 1; + + var r3 = new Revision(); + r3.rev = 2; + + assertThat(r1) + .as("Revisions equality should should match expected value") + .isEqualTo(r1) + .isEqualTo(r2) + .isNotEqualTo(r3) + .isNotEqualTo(new Object()); + } + + @Test + void testHashCode() { + var r1 = new Revision(); + r1.rev = 123; + + var r2 = new Revision(); + r2.rev = 123; + + var r3 = new Revision(); + r3.rev = 2; + + assertThat(r1.hashCode()).isEqualTo(123).isEqualTo(r2.hashCode()).isNotEqualTo(r3.hashCode()); + } + + @Test + void testToString() { + var rev = new Revision(); + rev.rev = 1; + + assertThat(rev).as("Revision's toString should match expected value").hasToString("Revision{rev=1}"); + } +} diff --git a/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/TestEntity.java b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/TestEntity.java new file mode 100644 index 0000000..2dea9ea --- /dev/null +++ b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/TestEntity.java @@ -0,0 +1,23 @@ +package ch.phoenix.oss.quarkus.commons.audit; + +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import org.hibernate.envers.Audited; + +@Entity +@Audited +@Table(name = "test_entity") +public class TestEntity extends AuditedPanacheEntity { + + public String something; + + public TestEntity() {} + + public TestEntity(String something) { + this.something = something; + } + + public static TestEntity findBySomething(String something) { + return find("something", something).singleResult(); + } +} diff --git a/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/TestEntity2.java b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/TestEntity2.java new file mode 100644 index 0000000..922b96a --- /dev/null +++ b/quarkus-audit-tools/src/test/java/ch/phoenix/oss/quarkus/commons/audit/TestEntity2.java @@ -0,0 +1,29 @@ +package ch.phoenix.oss.quarkus.commons.audit; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.envers.Audited; + +@Entity +@Audited +@Table(name = "test_entity") +public class TestEntity2 extends AuditedPanacheEntityBase { + + @Id + @GeneratedValue + public Long id; + + public String something; + + public TestEntity2() {} + + public TestEntity2(String something) { + this.something = something; + } + + public static TestEntity2 findBySomething(String something) { + return find("something", something).singleResult(); + } +} diff --git a/quarkus-audit-tools/src/test/resources/application.yaml b/quarkus-audit-tools/src/test/resources/application.yaml new file mode 100644 index 0000000..d44d31a --- /dev/null +++ b/quarkus-audit-tools/src/test/resources/application.yaml @@ -0,0 +1,22 @@ +quarkus: + flyway: + migrate-at-start: true + datasource: + db-kind: postgresql + hibernate-orm: + sql-load-script: no-file + schema-management: + strategy: none + log: + sql: true + bind-parameters: true + hibernate-envers: + audit-strategy: org.hibernate.envers.strategy.internal.ValidityAuditStrategy + revision-listener: ch.phoenix.oss.quarkus.commons.audit.AuditRevisionListener + security: + users: + embedded: + enabled: true + plain-text: true + users: + jon: doe diff --git a/quarkus-audit-tools/src/test/resources/db/migration/V1__create_schema.sql b/quarkus-audit-tools/src/test/resources/db/migration/V1__create_schema.sql new file mode 100644 index 0000000..daabe85 --- /dev/null +++ b/quarkus-audit-tools/src/test/resources/db/migration/V1__create_schema.sql @@ -0,0 +1,46 @@ +create sequence revinfo_seq start with 1 increment by 50; +create sequence test_entity_seq start with 1 increment by 50; + +create table revinfo +( + rev bigint not null, + timestamp timestamp(6) with time zone not null, + actor varchar(255), + spanId varchar(255), + traceId varchar(255), + requestId varchar(255), + clientIp varchar(255), + hostName varchar(255), + primary key (rev) +); + +create table test_entity +( + + id bigint primary key not null, + something varchar(255), + createdAt timestamp(6) with time zone, + lastUpdatedAt timestamp(6) with time zone +); + +create table test_entity_aud +( + revtype smallint, + rev bigint not null, + revend bigint, + id bigint not null, + something varchar(255), + primary key (rev, id) +); + +alter table if exists test_entity_aud + add constraint fk_rev__revinfo_rev + foreign key (rev) + references revinfo; + + +alter table if exists test_entity_aud + add constraint fk_revend__revinfo_rev + foreign key (revend) + references revinfo; + diff --git a/quarkus-audit-tools/src/test/resources/db/migration/V2__create_audit_trigger.sql b/quarkus-audit-tools/src/test/resources/db/migration/V2__create_audit_trigger.sql new file mode 100644 index 0000000..f76a2cb --- /dev/null +++ b/quarkus-audit-tools/src/test/resources/db/migration/V2__create_audit_trigger.sql @@ -0,0 +1,35 @@ +CREATE OR REPLACE FUNCTION trg_test_entity_aud_apply_rev() + RETURNS TRIGGER + LANGUAGE plpgsql +AS $func$ +DECLARE + ts TIMESTAMP; +BEGIN + -- fetch the exact revision timestamp from revinfo + SELECT r.timestamp + INTO ts + FROM revinfo r + WHERE r.rev = NEW.rev; + + -- only set created_at once, when still NULL + UPDATE test_entity + SET createdAt = ts + WHERE id = NEW.id + AND createdAt IS NULL; + + -- always bump last_updated_at + UPDATE test_entity + SET lastUpdatedAt = ts + WHERE id = NEW.id; + + RETURN NULL; +END; +$func$; + +DROP TRIGGER IF EXISTS trg_test_entity_aud_after_insert + ON test_entity_aud; + +CREATE TRIGGER trg_test_entity_aud_after_insert + AFTER INSERT ON test_entity_aud + FOR EACH ROW +EXECUTE FUNCTION trg_test_entity_aud_apply_rev(); diff --git a/quarkus-audit-tools/src/test/resources/import.sql b/quarkus-audit-tools/src/test/resources/import.sql new file mode 100644 index 0000000..c4a0b85 --- /dev/null +++ b/quarkus-audit-tools/src/test/resources/import.sql @@ -0,0 +1,37 @@ +-- 1) Create or replace the trigger function +CREATE OR REPLACE FUNCTION trg_test_entity_aud_apply_rev() + RETURNS TRIGGER + LANGUAGE plpgsql +AS $func$ +DECLARE + ts TIMESTAMP; +BEGIN + -- fetch the exact revision timestamp from revinfo + SELECT r.timestamp + INTO ts + FROM revinfo r + WHERE r.rev = NEW.rev; + + -- only set created_at once, when still NULL + UPDATE test_entity + SET created_at = ts + WHERE id = NEW.id + AND created_at IS NULL; + + -- always bump last_updated_at + UPDATE test_entity + SET last_updated_at = ts + WHERE id = NEW.id; + + RETURN NULL; -- AFTER trigger ignores return value +END; +$func$; + +-- 2) Drop any existing trigger, then attach the new one +DROP TRIGGER IF EXISTS trg_test_entity_aud_after_insert + ON test_entity_aud; + +CREATE TRIGGER trg_test_entity_aud_after_insert + AFTER INSERT ON test_entity_aud + FOR EACH ROW +EXECUTE FUNCTION trg_test_entity_aud_apply_rev(); diff --git a/quarkus-client-logger/pom.xml b/quarkus-client-logger/pom.xml new file mode 100644 index 0000000..3095369 --- /dev/null +++ b/quarkus-client-logger/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + + ch.phoenix.oss + quarkus-commons + 1.0.9-SNAPSHOT + + + quarkus-client-logger + jar + + + + io.quarkus + quarkus-rest-client + + + io.quarkus + quarkus-config-yaml + test + + + io.quarkus + quarkus-rest-jackson + test + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-plugin.version} + + + jacoco-check + + check + + test + + ${project.build.directory}/jacoco-quarkus.exec + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.92 + + + + + + + + + + + + diff --git a/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/LowerCaseStringConverter.java b/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/LowerCaseStringConverter.java new file mode 100644 index 0000000..498e318 --- /dev/null +++ b/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/LowerCaseStringConverter.java @@ -0,0 +1,10 @@ +package ch.phoenix.oss.quarkus.commons.client.logger; + +import org.eclipse.microprofile.config.spi.Converter; + +public class LowerCaseStringConverter implements Converter { + @Override + public String convert(String value) throws IllegalArgumentException, NullPointerException { + return value.toLowerCase(); + } +} diff --git a/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLogger.java b/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLogger.java new file mode 100644 index 0000000..cb2b63b --- /dev/null +++ b/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLogger.java @@ -0,0 +1,119 @@ +package ch.phoenix.oss.quarkus.commons.client.logger; + +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Inject; +import java.util.Map; +import java.util.Set; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.client.api.ClientLogger; + +/** + * This is based on org.jboss.resteasy.reactive.client.logging.DefaultClientLogger, + * with the only change being that headers are redacted based on the Set provided + * by the configuration. + */ +@Dependent +public class RedactingClientLogger implements ClientLogger { + + private static final Logger log = Logger.getLogger(RedactingClientLogger.class); + + private static final String REDACTED_VALUE = "*****"; + + private final Set redactedHeaders; + + private int bodySize; + + @Inject + public RedactingClientLogger(RedactingClientLoggerConfiguration configuration) { + this.redactedHeaders = configuration + .headers() + .redact() + .orElse(RedactingClientLoggerConfiguration.Headers.DEFAULT_REDACTED_HEADERS); + } + + @Override + public void setBodySize(int bodySize) { + this.bodySize = bodySize; + } + + @Override + public void logResponse(HttpClientResponse response, boolean redirect) { + if (!log.isDebugEnabled()) { + return; + } + + //noinspection Convert2Lambda + response.bodyHandler(new Handler<>() { + @Override + public void handle(Buffer body) { + log.debugf( + "%s: %s %s, Status[%d %s], Headers[%s], Body:\n%s", + redirect ? "Redirect" : "Response", + response.request().getMethod(), + response.request().absoluteURI(), + response.statusCode(), + response.statusMessage(), + asString(response.headers()), + bodyToString(body)); + } + }); + } + + @Override + public void logRequest(HttpClientRequest request, Buffer body, boolean omitBody) { + if (!log.isDebugEnabled()) { + return; + } + if (omitBody) { + log.debugf( + "Request: %s %s Headers[%s], Body omitted", + request.getMethod(), request.absoluteURI(), asString(request.headers())); + } else if (body == null || body.length() == 0) { + log.debugf( + "Request: %s %s Headers[%s], Empty body", + request.getMethod(), request.absoluteURI(), asString(request.headers())); + } else { + log.debugf( + "Request: %s %s Headers[%s], Body:\n%s", + request.getMethod(), request.absoluteURI(), asString(request.headers()), bodyToString(body)); + } + } + + private String bodyToString(Buffer body) { + if (body == null) { + return ""; + } else if (bodySize <= 0) { + return body.toString(); + } else { + String bodyAsString = body.toString(); + return bodyAsString.substring(0, Math.min(bodySize, bodyAsString.length())); + } + } + + private String asString(MultiMap headers) { + if (headers.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder((headers.size() * (6 + 1 + 6)) + + (headers.size() - 1)); // this is a very rough estimate of a result like 'key1=value1 key2=value2' + boolean isFirst = true; + for (Map.Entry entry : headers) { + if (isFirst) { + isFirst = false; + } else { + sb.append(' '); + } + + var key = entry.getKey(); + var value = redactedHeaders.contains(key.toLowerCase()) ? REDACTED_VALUE : entry.getValue(); + + sb.append(key).append('=').append(value); + } + return sb.toString(); + } +} diff --git a/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLoggerConfiguration.java b/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLoggerConfiguration.java new file mode 100644 index 0000000..24fa05f --- /dev/null +++ b/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLoggerConfiguration.java @@ -0,0 +1,20 @@ +package ch.phoenix.oss.quarkus.commons.client.logger; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithConverter; +import jakarta.ws.rs.core.HttpHeaders; +import java.util.Optional; +import java.util.Set; + +@ConfigMapping(prefix = "phoenix.client-logger") +public interface RedactingClientLoggerConfiguration { + + Headers headers(); + + interface Headers { + + Set DEFAULT_REDACTED_HEADERS = Set.of(HttpHeaders.AUTHORIZATION.toLowerCase()); + + Optional> redact(); + } +} diff --git a/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/InfoLevelProfile.java b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/InfoLevelProfile.java new file mode 100644 index 0000000..35acb14 --- /dev/null +++ b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/InfoLevelProfile.java @@ -0,0 +1,12 @@ +package ch.phoenix.oss.quarkus.commons.client.logger; + +import io.quarkus.test.junit.QuarkusTestProfile; +import java.util.Map; + +public class InfoLevelProfile implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + return Map.of("quarkus.log.category.\"ch.phoenix.oss.quarkus.commons.client.logger\".level", "INFO"); + } +} diff --git a/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/InfoLevelTest.java b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/InfoLevelTest.java new file mode 100644 index 0000000..c6b27ef --- /dev/null +++ b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/InfoLevelTest.java @@ -0,0 +1,44 @@ +package ch.phoenix.oss.quarkus.commons.client.logger; + +import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import jakarta.inject.Inject; +import java.net.URI; +import java.util.Optional; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestProfile(InfoLevelProfile.class) +class InfoLevelTest { + + @Inject + @RestClient + TestClient injectedClient; + + TestClient builtClient = QuarkusRestClientBuilder.newBuilder() + .clientLogger(new RedactingClientLogger(() -> Optional::empty)) + .baseUri(URI.create("http://localhost:8087")) + .build(TestClient.class); + + @Test + void getWithInjectedClient() { + injectedClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted"); + } + + @Test + void getWithBuiltClientAndEmptyConfig() { + builtClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted"); + } + + @Test + void postWithInjectedClient() { + injectedClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted", "body"); + } + + @Test + void postWithBuiltClientAndEmptyConfig() { + builtClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted", ""); + } +} diff --git a/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLoggerTest.java b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLoggerTest.java new file mode 100644 index 0000000..01e9389 --- /dev/null +++ b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLoggerTest.java @@ -0,0 +1,47 @@ +package ch.phoenix.oss.quarkus.commons.client.logger; + +import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import java.net.URI; +import java.util.Optional; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Test; + +@QuarkusTest +class RedactingClientLoggerTest { + + @Inject + @RestClient + TestClient injectedClient; + + TestClient builtClient = QuarkusRestClientBuilder.newBuilder() + .clientLogger(new RedactingClientLogger(() -> Optional::empty)) + .baseUri(URI.create("http://localhost:8087")) + .build(TestClient.class); + + @Test + void getWithInjectedClient() { + injectedClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted"); + } + + @Test + void getWithBuiltClientAndEmptyConfig() { + builtClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted"); + } + + @Test + void postWithInjectedClient() { + injectedClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted", "body"); + } + + @Test + void postWithInjectedClientAndNullBody() { + injectedClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted", null); + } + + @Test + void postWithBuiltClientAndEmptyConfig() { + builtClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted", ""); + } +} diff --git a/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/ScopeNoneProfile.java b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/ScopeNoneProfile.java new file mode 100644 index 0000000..42daa28 --- /dev/null +++ b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/ScopeNoneProfile.java @@ -0,0 +1,12 @@ +package ch.phoenix.oss.quarkus.commons.client.logger; + +import io.quarkus.test.junit.QuarkusTestProfile; +import java.util.Map; + +public class ScopeNoneProfile implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + return Map.of("quarkus.rest-client.logging.scope", "none"); + } +} diff --git a/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/ScopeNoneTest.java b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/ScopeNoneTest.java new file mode 100644 index 0000000..14538b2 --- /dev/null +++ b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/ScopeNoneTest.java @@ -0,0 +1,44 @@ +package ch.phoenix.oss.quarkus.commons.client.logger; + +import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import jakarta.inject.Inject; +import java.net.URI; +import java.util.Optional; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestProfile(ScopeNoneProfile.class) +class ScopeNoneTest { + + @Inject + @RestClient + TestClient injectedClient; + + TestClient builtClient = QuarkusRestClientBuilder.newBuilder() + .clientLogger(new RedactingClientLogger(() -> Optional::empty)) + .baseUri(URI.create("http://localhost:8087")) + .build(TestClient.class); + + @Test + void getWithInjectedClient() { + injectedClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted"); + } + + @Test + void getWithBuiltClientAndEmptyConfig() { + builtClient.get("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted"); + } + + @Test + void postWithInjectedClient() { + injectedClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "also redacted", "body"); + } + + @Test + void postWithBuiltClientAndEmptyConfig() { + builtClient.post("this will be redacted", "5c0d8e45-e402-4b71-8f84-24cc0cfd7eec", "not redacted", ""); + } +} diff --git a/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/TestClient.java b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/TestClient.java new file mode 100644 index 0000000..79769ac --- /dev/null +++ b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/TestClient.java @@ -0,0 +1,28 @@ +package ch.phoenix.oss.quarkus.commons.client.logger; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@SuppressWarnings("UastIncorrectHttpHeaderInspection") +@RegisterRestClient(configKey = "test") +public interface TestClient { + + @GET + @Path("/") + @Produces(MediaType.TEXT_PLAIN) + String get( + @HeaderParam("Authorization") String authorization, + @HeaderParam("X-Request-ID") String requestId, + @HeaderParam("X-Something-Else") String somethingElse); + + @POST + @Path("/") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + String post( + @HeaderParam("Authorization") String authorization, + @HeaderParam("X-Request-ID") String requestId, + @HeaderParam("X-Something-Else") String somethingElse, + String body); +} diff --git a/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/TestResource.java b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/TestResource.java new file mode 100644 index 0000000..c83bdd0 --- /dev/null +++ b/quarkus-client-logger/src/test/java/ch/phoenix/oss/quarkus/commons/client/logger/TestResource.java @@ -0,0 +1,21 @@ +package ch.phoenix.oss.quarkus.commons.client.logger; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +@Path("/") +public class TestResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String get() { + return "get"; + } + + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String post(String body) { + return body; + } +} diff --git a/quarkus-client-logger/src/test/resources/application.yaml b/quarkus-client-logger/src/test/resources/application.yaml new file mode 100644 index 0000000..a02f41d --- /dev/null +++ b/quarkus-client-logger/src/test/resources/application.yaml @@ -0,0 +1,20 @@ +quarkus: + http: + test-port: 8087 + rest-client: + logging: + scope: request-response + body-limit: 10000 + test: + url: http://localhost:${quarkus.http.test-port} + log: + category: + "ch.phoenix.oss.quarkus.commons.client.logger": + level: DEBUG + +phoenix: + client-logger: + headers: + redact: + - AUTHORIZATION + - X-SOMETHING-ELSE diff --git a/quarkus-clock-service/pom.xml b/quarkus-clock-service/pom.xml index adbd668..aaab0c1 100644 --- a/quarkus-clock-service/pom.xml +++ b/quarkus-clock-service/pom.xml @@ -1,13 +1,11 @@ - + 4.0.0 - ch.phoenixtechnologies.quarkus + ch.phoenix.oss quarkus-commons - 0.1.0-SNAPSHOT + 1.0.9-SNAPSHOT quarkus-clock-service diff --git a/quarkus-clock-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockProducer.java b/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockProducer.java similarity index 84% rename from quarkus-clock-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockProducer.java rename to quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockProducer.java index 97d95fc..104b0c7 100644 --- a/quarkus-clock-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockProducer.java +++ b/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockProducer.java @@ -1,9 +1,8 @@ -package ch.phoenixtechnologies.quarkus.commons.clock; +package ch.phoenix.oss.quarkus.commons.clock; import io.quarkus.arc.DefaultBean; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - import java.time.Clock; class ClockProducer { @@ -14,5 +13,4 @@ class ClockProducer { Clock produceClock() { return Clock.systemDefaultZone(); } - } diff --git a/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockService.java b/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockService.java new file mode 100644 index 0000000..f6c3c26 --- /dev/null +++ b/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockService.java @@ -0,0 +1,19 @@ +package ch.phoenix.oss.quarkus.commons.clock; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; + +public interface ClockService { + + Instant instant(); + + long currentTimeMillis(); + + LocalDate localDate(); + + LocalDateTime localDateTime(); + + ZonedDateTime zonedDateTime(); +} diff --git a/quarkus-clock-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockServiceImpl.java b/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockServiceImpl.java similarity index 55% rename from quarkus-clock-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockServiceImpl.java rename to quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockServiceImpl.java index bfe34be..e47c757 100644 --- a/quarkus-clock-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockServiceImpl.java +++ b/quarkus-clock-service/src/main/java/ch/phoenix/oss/quarkus/commons/clock/ClockServiceImpl.java @@ -1,10 +1,8 @@ -package ch.phoenixtechnologies.quarkus.commons.clock; +package ch.phoenix.oss.quarkus.commons.clock; import io.quarkus.arc.DefaultBean; import jakarta.enterprise.context.ApplicationScoped; - -import java.time.Clock; -import java.time.Instant; +import java.time.*; @DefaultBean @ApplicationScoped @@ -25,4 +23,19 @@ class ClockServiceImpl implements ClockService { public long currentTimeMillis() { return clock.millis(); } + + @Override + public LocalDate localDate() { + return LocalDate.now(clock); + } + + @Override + public LocalDateTime localDateTime() { + return LocalDateTime.now(clock); + } + + @Override + public ZonedDateTime zonedDateTime() { + return ZonedDateTime.now(clock); + } } diff --git a/quarkus-clock-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockService.java b/quarkus-clock-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockService.java deleted file mode 100644 index 5954727..0000000 --- a/quarkus-clock-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockService.java +++ /dev/null @@ -1,10 +0,0 @@ -package ch.phoenixtechnologies.quarkus.commons.clock; - -import java.time.Instant; - -public interface ClockService { - - Instant instant(); - - long currentTimeMillis(); -} diff --git a/quarkus-clock-service/src/test/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockProducerTest.java b/quarkus-clock-service/src/test/java/ch/phoenix/oss/quarkus/commons/clock/ClockProducerTest.java similarity index 81% rename from quarkus-clock-service/src/test/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockProducerTest.java rename to quarkus-clock-service/src/test/java/ch/phoenix/oss/quarkus/commons/clock/ClockProducerTest.java index 3735ff0..c05ad74 100644 --- a/quarkus-clock-service/src/test/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockProducerTest.java +++ b/quarkus-clock-service/src/test/java/ch/phoenix/oss/quarkus/commons/clock/ClockProducerTest.java @@ -1,13 +1,11 @@ -package ch.phoenixtechnologies.quarkus.commons.clock; +package ch.phoenix.oss.quarkus.commons.clock; + +import static org.assertj.core.api.Assertions.assertThat; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; -import org.junit.jupiter.api.Test; - import java.time.Clock; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; @QuarkusTest class ClockProducerTest { @@ -21,4 +19,4 @@ class ClockProducerTest { .as("Produced clock's zone should match expected value") .isEqualTo(Clock.systemDefaultZone().getZone()); } -} \ No newline at end of file +} diff --git a/quarkus-clock-service/src/test/java/ch/phoenix/oss/quarkus/commons/clock/ClockServiceImplTest.java b/quarkus-clock-service/src/test/java/ch/phoenix/oss/quarkus/commons/clock/ClockServiceImplTest.java new file mode 100644 index 0000000..dfffea8 --- /dev/null +++ b/quarkus-clock-service/src/test/java/ch/phoenix/oss/quarkus/commons/clock/ClockServiceImplTest.java @@ -0,0 +1,70 @@ +package ch.phoenix.oss.quarkus.commons.clock; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import java.time.*; +import org.junit.jupiter.api.Test; + +@QuarkusTest +class ClockServiceImplTest { + + @Inject + ClockService clockService; + + @InjectMock + Clock clock; + + @Test + void instant() { + var expected = Instant.ofEpochMilli(1729280640915L); + when(clock.instant()).thenReturn(expected); + + assertThat(clockService.instant()) + .as("Instant should match expected value") + .isEqualTo(expected); + } + + @Test + void currentTimeMillis() { + var expected = 1729280640915L; + when(clock.millis()).thenReturn(expected); + + assertThat(clockService.currentTimeMillis()) + .as("Instant should match expected value") + .isEqualTo(expected); + } + + @Test + void localDate() { + var instant = Instant.ofEpochMilli(1729280640915L); + when(clock.instant()).thenReturn(instant); + when(clock.getZone()).thenReturn(ZoneId.of("UTC")); + assertThat(clockService.localDate()) + .as("LocalDate should match expected value") + .isEqualTo(LocalDate.parse("2024-10-18")); + } + + @Test + void localDateTime() { + var instant = Instant.ofEpochMilli(1729280640915L); + when(clock.instant()).thenReturn(instant); + when(clock.getZone()).thenReturn(ZoneId.of("UTC")); + assertThat(clockService.localDateTime()) + .as("LocalDateTime should match expected value") + .isEqualTo(LocalDateTime.parse("2024-10-18T19:44:00.915")); + } + + @Test + void zonedDateTime() { + var instant = Instant.ofEpochMilli(1729280640915L); + when(clock.instant()).thenReturn(instant); + when(clock.getZone()).thenReturn(ZoneId.of("UTC")); + assertThat(clockService.zonedDateTime()) + .as("ZonedDateTime should match expected value") + .isEqualTo(ZonedDateTime.parse("2024-10-18T19:44:00.915Z[UTC]")); + } +} diff --git a/quarkus-clock-service/src/test/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockServiceImplTest.java b/quarkus-clock-service/src/test/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockServiceImplTest.java deleted file mode 100644 index b7c0ff6..0000000 --- a/quarkus-clock-service/src/test/java/ch/phoenixtechnologies/quarkus/commons/clock/ClockServiceImplTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package ch.phoenixtechnologies.quarkus.commons.clock; - -import io.quarkus.test.InjectMock; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.mockito.InjectSpy; -import jakarta.inject.Inject; -import org.junit.jupiter.api.Test; - -import java.time.Clock; -import java.time.Instant; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -@QuarkusTest -class ClockServiceImplTest { - - @Inject - ClockService clockService; - - @InjectMock - Clock clock; - - @Test - void instant() { - var expected = Instant.ofEpochMilli(1729280640915L); - when(clock.instant()).thenReturn(expected); - - assertThat(clockService.instant()) - .as("Instant should match expected value") - .isEqualTo(expected); - } - - @Test - void currentTimeMillis() { - var expected = 1729280640915L; - when(clock.millis()).thenReturn(expected); - - assertThat(clockService.currentTimeMillis()) - .as("Instant should match expected value") - .isEqualTo(expected); - } -} \ No newline at end of file diff --git a/quarkus-json-service/pom.xml b/quarkus-json-service/pom.xml index 647b150..717c39f 100644 --- a/quarkus-json-service/pom.xml +++ b/quarkus-json-service/pom.xml @@ -1,13 +1,11 @@ - + 4.0.0 - ch.phoenixtechnologies.quarkus + ch.phoenix.oss quarkus-commons - 0.1.0-SNAPSHOT + 1.0.9-SNAPSHOT quarkus-json-service @@ -42,7 +40,7 @@ INSTRUCTION COVEREDRATIO - 0.83 + 1 diff --git a/quarkus-json-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/json/JsonService.java b/quarkus-json-service/src/main/java/ch/phoenix/oss/quarkus/commons/json/JsonService.java similarity index 81% rename from quarkus-json-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/json/JsonService.java rename to quarkus-json-service/src/main/java/ch/phoenix/oss/quarkus/commons/json/JsonService.java index 9570ae7..9d229b7 100644 --- a/quarkus-json-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/json/JsonService.java +++ b/quarkus-json-service/src/main/java/ch/phoenix/oss/quarkus/commons/json/JsonService.java @@ -1,4 +1,4 @@ -package ch.phoenixtechnologies.quarkus.commons.json; +package ch.phoenix.oss.quarkus.commons.json; import com.fasterxml.jackson.core.type.TypeReference; @@ -9,5 +9,4 @@ public interface JsonService { T fromJson(String json, Class clazz); T fromJson(String json, TypeReference typeReference); - } diff --git a/quarkus-json-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/json/JsonServiceImpl.java b/quarkus-json-service/src/main/java/ch/phoenix/oss/quarkus/commons/json/JsonServiceImpl.java similarity index 73% rename from quarkus-json-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/json/JsonServiceImpl.java rename to quarkus-json-service/src/main/java/ch/phoenix/oss/quarkus/commons/json/JsonServiceImpl.java index 32f2b41..5c310e6 100644 --- a/quarkus-json-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/json/JsonServiceImpl.java +++ b/quarkus-json-service/src/main/java/ch/phoenix/oss/quarkus/commons/json/JsonServiceImpl.java @@ -1,4 +1,4 @@ -package ch.phoenixtechnologies.quarkus.commons.json; +package ch.phoenix.oss.quarkus.commons.json; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -30,10 +30,9 @@ class JsonServiceImpl implements JsonService { try { return objectMapper.readValue(json, clazz); } catch (JsonProcessingException e) { - throw new IllegalArgumentException("Unable to read object of class [" + clazz.getName() - + "] from json String: " + json, e); + throw new IllegalArgumentException( + "Unable to read object of class [" + clazz.getName() + "] from json String: " + json, e); } - } @Override @@ -41,8 +40,10 @@ class JsonServiceImpl implements JsonService { try { return objectMapper.readValue(json, typeReference); } catch (JsonProcessingException e) { - throw new IllegalArgumentException("Unable to read object of type [" + typeReference.getType().getTypeName() - + "] from json String: " + json, e); + throw new IllegalArgumentException( + "Unable to read object of type [" + typeReference.getType().getTypeName() + "] from json String: " + + json, + e); } } } diff --git a/quarkus-json-service/src/test/java/ch/phoenixtechnologies/quarkus/commons/json/JsonServiceImplTest.java b/quarkus-json-service/src/test/java/ch/phoenix/oss/quarkus/commons/json/JsonServiceImplTest.java similarity index 59% rename from quarkus-json-service/src/test/java/ch/phoenixtechnologies/quarkus/commons/json/JsonServiceImplTest.java rename to quarkus-json-service/src/test/java/ch/phoenix/oss/quarkus/commons/json/JsonServiceImplTest.java index a9a388e..527a330 100644 --- a/quarkus-json-service/src/test/java/ch/phoenixtechnologies/quarkus/commons/json/JsonServiceImplTest.java +++ b/quarkus-json-service/src/test/java/ch/phoenix/oss/quarkus/commons/json/JsonServiceImplTest.java @@ -1,23 +1,29 @@ -package ch.phoenixtechnologies.quarkus.commons.json; - -import com.fasterxml.jackson.core.type.TypeReference; -import io.quarkus.test.junit.QuarkusTest; -import jakarta.inject.Inject; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.util.List; +package ch.phoenix.oss.quarkus.commons.json; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.fasterxml.jackson.core.type.TypeReference; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + @QuarkusTest class JsonServiceImplTest { - public static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { - }; + public static final TypeReference> TYPE_REFERENCE = new TypeReference<>() {}; - record TestRecord(String name, int age) { + record TestRecord(String name, int age) {} + + static class CircularReference { + CircularReference reference; + + @Override + public String toString() { + return "CircularReference"; + } } @Inject @@ -29,9 +35,18 @@ class JsonServiceImplTest { var actual = jsonService.toJson(new TestRecord("John Doe", 30)); - assertThat(actual) - .as("Json should match expected value") - .isEqualTo(expected); + assertThat(actual).as("Json should match expected value").isEqualTo(expected); + } + + @Test + void toJsonWithCircularReference() { + var circularReference = new CircularReference(); + circularReference.reference = circularReference; + + assertThatThrownBy(() -> jsonService.toJson(circularReference)) + .as("Should throw IllegalArgumentException when input is invalid") + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unable to write object as json String: CircularReference"); } @Test @@ -41,9 +56,7 @@ class JsonServiceImplTest { var actual = jsonService.fromJson(json, TestRecord.class); - assertThat(actual) - .as("Deserialized object should match expected value") - .isEqualTo(expected); + assertThat(actual).as("Deserialized object should match expected value").isEqualTo(expected); } @Test @@ -53,7 +66,8 @@ class JsonServiceImplTest { assertThatThrownBy(() -> jsonService.fromJson(json, TestRecord.class)) .as("Should throw IllegalArgumentException when input is invalid") .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unable to read object of class [ch.phoenixtechnologies.quarkus.commons.json.JsonServiceImplTest$TestRecord] from json String: {\"name\":\"John Doe\",\"age\":\"30\""); + .hasMessage( + "Unable to read object of class [ch.phoenix.oss.quarkus.commons.json.JsonServiceImplTest$TestRecord] from json String: {\"name\":\"John Doe\",\"age\":\"30\""); } @Test @@ -75,6 +89,7 @@ class JsonServiceImplTest { assertThatThrownBy(() -> jsonService.fromJson(json, TYPE_REFERENCE)) .as("Should throw IllegalArgumentException when input is invalid") .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unable to read object of type [java.util.List] from json String: {\"name\":\"John Doe\",\"age\":30},{\"name\":\"Jane Doe\",\"age\":\"25\"}"); + .hasMessage( + "Unable to read object of type [java.util.List] from json String: {\"name\":\"John Doe\",\"age\":30},{\"name\":\"Jane Doe\",\"age\":\"25\"}"); } -} \ No newline at end of file +} diff --git a/quarkus-message-digest-service/pom.xml b/quarkus-message-digest-service/pom.xml index dfee178..c5d2c2d 100644 --- a/quarkus-message-digest-service/pom.xml +++ b/quarkus-message-digest-service/pom.xml @@ -1,13 +1,11 @@ - + 4.0.0 - ch.phoenixtechnologies.quarkus + ch.phoenix.oss quarkus-commons - 0.1.0-SNAPSHOT + 1.0.9-SNAPSHOT quarkus-message-digest-service diff --git a/quarkus-message-digest-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/digest/MessageDigestConfiguration.java b/quarkus-message-digest-service/src/main/java/ch/phoenix/oss/quarkus/commons/digest/MessageDigestConfiguration.java similarity index 81% rename from quarkus-message-digest-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/digest/MessageDigestConfiguration.java rename to quarkus-message-digest-service/src/main/java/ch/phoenix/oss/quarkus/commons/digest/MessageDigestConfiguration.java index 6720b90..e30fdf6 100644 --- a/quarkus-message-digest-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/digest/MessageDigestConfiguration.java +++ b/quarkus-message-digest-service/src/main/java/ch/phoenix/oss/quarkus/commons/digest/MessageDigestConfiguration.java @@ -1,4 +1,4 @@ -package ch.phoenixtechnologies.quarkus.commons.digest; +package ch.phoenix.oss.quarkus.commons.digest; import io.smallrye.config.ConfigMapping; import io.smallrye.config.WithDefault; @@ -8,5 +8,4 @@ public interface MessageDigestConfiguration { @WithDefault("SHA-256") String defaultAlgorithm(); - } diff --git a/quarkus-message-digest-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/digest/MessageDigestService.java b/quarkus-message-digest-service/src/main/java/ch/phoenix/oss/quarkus/commons/digest/MessageDigestService.java similarity index 97% rename from quarkus-message-digest-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/digest/MessageDigestService.java rename to quarkus-message-digest-service/src/main/java/ch/phoenix/oss/quarkus/commons/digest/MessageDigestService.java index de1f2a5..f2cd285 100644 --- a/quarkus-message-digest-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/digest/MessageDigestService.java +++ b/quarkus-message-digest-service/src/main/java/ch/phoenix/oss/quarkus/commons/digest/MessageDigestService.java @@ -1,4 +1,4 @@ -package ch.phoenixtechnologies.quarkus.commons.digest; +package ch.phoenix.oss.quarkus.commons.digest; import java.nio.charset.Charset; import java.security.MessageDigest; diff --git a/quarkus-message-digest-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/digest/MessageDigestServiceImpl.java b/quarkus-message-digest-service/src/main/java/ch/phoenix/oss/quarkus/commons/digest/MessageDigestServiceImpl.java similarity index 96% rename from quarkus-message-digest-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/digest/MessageDigestServiceImpl.java rename to quarkus-message-digest-service/src/main/java/ch/phoenix/oss/quarkus/commons/digest/MessageDigestServiceImpl.java index eb3e749..b64c0ab 100644 --- a/quarkus-message-digest-service/src/main/java/ch/phoenixtechnologies/quarkus/commons/digest/MessageDigestServiceImpl.java +++ b/quarkus-message-digest-service/src/main/java/ch/phoenix/oss/quarkus/commons/digest/MessageDigestServiceImpl.java @@ -1,8 +1,7 @@ -package ch.phoenixtechnologies.quarkus.commons.digest; +package ch.phoenix.oss.quarkus.commons.digest; import io.quarkus.arc.DefaultBean; import jakarta.enterprise.context.ApplicationScoped; - import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; diff --git a/quarkus-message-digest-service/src/test/java/ch/phoenixtechnologies/quarkus/commons/digest/MessageDigestServiceImplTest.java b/quarkus-message-digest-service/src/test/java/ch/phoenix/oss/quarkus/commons/digest/MessageDigestServiceImplTest.java similarity index 83% rename from quarkus-message-digest-service/src/test/java/ch/phoenixtechnologies/quarkus/commons/digest/MessageDigestServiceImplTest.java rename to quarkus-message-digest-service/src/test/java/ch/phoenix/oss/quarkus/commons/digest/MessageDigestServiceImplTest.java index 42647b5..64e0374 100644 --- a/quarkus-message-digest-service/src/test/java/ch/phoenixtechnologies/quarkus/commons/digest/MessageDigestServiceImplTest.java +++ b/quarkus-message-digest-service/src/test/java/ch/phoenix/oss/quarkus/commons/digest/MessageDigestServiceImplTest.java @@ -1,37 +1,37 @@ -package ch.phoenixtechnologies.quarkus.commons.digest; - -import io.quarkus.test.junit.QuarkusTest; -import jakarta.inject.Inject; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; +package ch.phoenix.oss.quarkus.commons.digest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + @QuarkusTest class MessageDigestServiceImplTest { public static final String INPUT = "test"; - public static final byte[] EXPECTED_SHA256_DIGEST = {-97, -122, -48, -127, -120, 76, 125, 101, -102, 47, -22, -96, -59, 90, -48, 21, -93, -65, 79, 27, 43, 11, -126, 44, -47, 93, 108, 21, -80, -16, 10, 8}; + public static final byte[] EXPECTED_SHA256_DIGEST = { + -97, -122, -48, -127, -120, 76, 125, 101, -102, 47, -22, -96, -59, 90, -48, 21, -93, -65, 79, 27, 43, 11, -126, + 44, -47, 93, 108, 21, -80, -16, 10, 8 + }; @Inject MessageDigestServiceImpl service; @Test void getInstance() { - assertThat(service.getInstance()) - .isInstanceOf(MessageDigest.class); + assertThat(service.getInstance()).isInstanceOf(MessageDigest.class); } @ParameterizedTest @ValueSource(strings = {"SHA-256", "SHA-512", "MD5"}) void getInstanceWithAlgorithm(String algorithm) { - assertThat(service.getInstance(algorithm)) - .isInstanceOf(MessageDigest.class); + assertThat(service.getInstance(algorithm)).isInstanceOf(MessageDigest.class); } @ParameterizedTest @@ -61,7 +61,6 @@ class MessageDigestServiceImplTest { assertThat(service.digest(INPUT, StandardCharsets.UTF_8)) .as("Digest should match expected value") .isEqualTo(EXPECTED_SHA256_DIGEST); - } @Test @@ -78,4 +77,4 @@ class MessageDigestServiceImplTest { .as("Digest should match expected value") .isEqualTo(EXPECTED_SHA256_DIGEST); } -} \ No newline at end of file +} diff --git a/quarkus-random-number-generator/pom.xml b/quarkus-random-number-generator/pom.xml index ee11f07..940f965 100644 --- a/quarkus-random-number-generator/pom.xml +++ b/quarkus-random-number-generator/pom.xml @@ -1,13 +1,11 @@ - + 4.0.0 - ch.phoenixtechnologies.quarkus + ch.phoenix.oss quarkus-commons - 0.1.0-SNAPSHOT + 1.0.9-SNAPSHOT quarkus-random-number-generator diff --git a/quarkus-random-number-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/random/RandomNumberGenerator.java b/quarkus-random-number-generator/src/main/java/ch/phoenix/oss/quarkus/commons/random/RandomNumberGenerator.java similarity index 98% rename from quarkus-random-number-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/random/RandomNumberGenerator.java rename to quarkus-random-number-generator/src/main/java/ch/phoenix/oss/quarkus/commons/random/RandomNumberGenerator.java index 922eb78..32365f1 100644 --- a/quarkus-random-number-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/random/RandomNumberGenerator.java +++ b/quarkus-random-number-generator/src/main/java/ch/phoenix/oss/quarkus/commons/random/RandomNumberGenerator.java @@ -1,4 +1,4 @@ -package ch.phoenixtechnologies.quarkus.commons.random; +package ch.phoenix.oss.quarkus.commons.random; import java.math.BigInteger; import java.util.Random; diff --git a/quarkus-random-number-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/random/RandomNumberGeneratorImpl.java b/quarkus-random-number-generator/src/main/java/ch/phoenix/oss/quarkus/commons/random/RandomNumberGeneratorImpl.java similarity index 97% rename from quarkus-random-number-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/random/RandomNumberGeneratorImpl.java rename to quarkus-random-number-generator/src/main/java/ch/phoenix/oss/quarkus/commons/random/RandomNumberGeneratorImpl.java index 3b8ab2d..b88353b 100644 --- a/quarkus-random-number-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/random/RandomNumberGeneratorImpl.java +++ b/quarkus-random-number-generator/src/main/java/ch/phoenix/oss/quarkus/commons/random/RandomNumberGeneratorImpl.java @@ -1,8 +1,7 @@ -package ch.phoenixtechnologies.quarkus.commons.random; +package ch.phoenix.oss.quarkus.commons.random; import io.quarkus.arc.DefaultBean; import jakarta.enterprise.context.ApplicationScoped; - import java.math.BigInteger; import java.util.Random; diff --git a/quarkus-random-number-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/random/RandomProvider.java b/quarkus-random-number-generator/src/main/java/ch/phoenix/oss/quarkus/commons/random/RandomProvider.java similarity index 87% rename from quarkus-random-number-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/random/RandomProvider.java rename to quarkus-random-number-generator/src/main/java/ch/phoenix/oss/quarkus/commons/random/RandomProvider.java index e58466d..95ed63d 100644 --- a/quarkus-random-number-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/random/RandomProvider.java +++ b/quarkus-random-number-generator/src/main/java/ch/phoenix/oss/quarkus/commons/random/RandomProvider.java @@ -1,4 +1,4 @@ -package ch.phoenixtechnologies.quarkus.commons.random; +package ch.phoenix.oss.quarkus.commons.random; import java.util.Random; diff --git a/quarkus-random-number-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/random/RandomProviderImpl.java b/quarkus-random-number-generator/src/main/java/ch/phoenix/oss/quarkus/commons/random/RandomProviderImpl.java similarity index 90% rename from quarkus-random-number-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/random/RandomProviderImpl.java rename to quarkus-random-number-generator/src/main/java/ch/phoenix/oss/quarkus/commons/random/RandomProviderImpl.java index 0cb083e..f32e8d4 100644 --- a/quarkus-random-number-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/random/RandomProviderImpl.java +++ b/quarkus-random-number-generator/src/main/java/ch/phoenix/oss/quarkus/commons/random/RandomProviderImpl.java @@ -1,8 +1,7 @@ -package ch.phoenixtechnologies.quarkus.commons.random; +package ch.phoenix.oss.quarkus.commons.random; import io.quarkus.arc.DefaultBean; import jakarta.enterprise.context.ApplicationScoped; - import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Random; @@ -19,6 +18,4 @@ class RandomProviderImpl implements RandomProvider { throw new IllegalStateException("Unable to obtain strong SecureRandom instance", e); } } - } - diff --git a/quarkus-random-number-generator/src/test/java/ch/phoenixtechnologies/quarkus/commons/random/RandomNumberGeneratorImplTest.java b/quarkus-random-number-generator/src/test/java/ch/phoenix/oss/quarkus/commons/random/RandomNumberGeneratorImplTest.java similarity index 64% rename from quarkus-random-number-generator/src/test/java/ch/phoenixtechnologies/quarkus/commons/random/RandomNumberGeneratorImplTest.java rename to quarkus-random-number-generator/src/test/java/ch/phoenix/oss/quarkus/commons/random/RandomNumberGeneratorImplTest.java index f6680bf..973e19b 100644 --- a/quarkus-random-number-generator/src/test/java/ch/phoenixtechnologies/quarkus/commons/random/RandomNumberGeneratorImplTest.java +++ b/quarkus-random-number-generator/src/test/java/ch/phoenix/oss/quarkus/commons/random/RandomNumberGeneratorImplTest.java @@ -1,15 +1,14 @@ -package ch.phoenixtechnologies.quarkus.commons.random; +package ch.phoenix.oss.quarkus.commons.random; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.mockito.InjectSpy; import jakarta.inject.Inject; -import org.junit.jupiter.api.Test; - import java.math.BigInteger; import java.util.Random; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; +import org.junit.jupiter.api.Test; @QuarkusTest class RandomNumberGeneratorImplTest { @@ -26,9 +25,7 @@ class RandomNumberGeneratorImplTest { var actual = generator.generateInt(); - assertThat(actual) - .as("Generated int should match expected value") - .isEqualTo(-1305521323); + assertThat(actual).as("Generated int should match expected value").isEqualTo(-1305521323); } @Test @@ -37,9 +34,7 @@ class RandomNumberGeneratorImplTest { var actual = generator.generateInt(40); - assertThat(actual) - .as("Generated int should match expected value") - .isEqualTo(26); + assertThat(actual).as("Generated int should match expected value").isEqualTo(26); } @Test @@ -48,21 +43,16 @@ class RandomNumberGeneratorImplTest { var actual = generator.generateInt(1, 40); - assertThat(actual) - .as("Generated int should match expected value") - .isEqualTo(17); + assertThat(actual).as("Generated int should match expected value").isEqualTo(17); } - @Test void generateLong() { when(provider.get()).thenReturn(getRandom()); var actual = generator.generateLong(); - assertThat(actual) - .as("Generated long should match expected value") - .isEqualTo(-5607171386684657918L); + assertThat(actual).as("Generated long should match expected value").isEqualTo(-5607171386684657918L); } @Test @@ -71,9 +61,7 @@ class RandomNumberGeneratorImplTest { var actual = generator.generateLong(40L); - assertThat(actual) - .as("Generated long should match expected value") - .isEqualTo(9L); + assertThat(actual).as("Generated long should match expected value").isEqualTo(9L); } @Test @@ -82,9 +70,7 @@ class RandomNumberGeneratorImplTest { var actual = generator.generateLong(1L, 40L); - assertThat(actual) - .as("Generated long should match expected value") - .isEqualTo(23L); + assertThat(actual).as("Generated long should match expected value").isEqualTo(23L); } @Test @@ -93,9 +79,7 @@ class RandomNumberGeneratorImplTest { var actual = generator.generateFloat(); - assertThat(actual) - .as("Generated float should match expected value") - .isEqualTo(0.6960346f); + assertThat(actual).as("Generated float should match expected value").isEqualTo(0.6960346f); } @Test @@ -104,9 +88,7 @@ class RandomNumberGeneratorImplTest { var actual = generator.generateFloat(0.99f); - assertThat(actual) - .as("Generated float should match expected value") - .isEqualTo(0.6890743f); + assertThat(actual).as("Generated float should match expected value").isEqualTo(0.6890743f); } @Test @@ -115,9 +97,7 @@ class RandomNumberGeneratorImplTest { var actual = generator.generateFloat(0.01f, 0.99f); - assertThat(actual) - .as("Generated float should match expected value") - .isEqualTo(0.69211394f); + assertThat(actual).as("Generated float should match expected value").isEqualTo(0.69211394f); } @Test @@ -126,9 +106,7 @@ class RandomNumberGeneratorImplTest { var actual = generator.generateDouble(); - assertThat(actual) - .as("Generated double should match expected value") - .isEqualTo(0.6960346394874213d); + assertThat(actual).as("Generated double should match expected value").isEqualTo(0.6960346394874213d); } @Test @@ -137,9 +115,7 @@ class RandomNumberGeneratorImplTest { var actual = generator.generateDouble(0.99d); - assertThat(actual) - .as("Generated double should match expected value") - .isEqualTo(0.6890742930925471d); + assertThat(actual).as("Generated double should match expected value").isEqualTo(0.6890742930925471d); } @Test @@ -148,9 +124,7 @@ class RandomNumberGeneratorImplTest { var actual = generator.generateDouble(0.01d, 0.99d); - assertThat(actual) - .as("Generated double should match expected value") - .isEqualTo(0.6921139466976729d); + assertThat(actual).as("Generated double should match expected value").isEqualTo(0.6921139466976729d); } @Test @@ -176,5 +150,4 @@ class RandomNumberGeneratorImplTest { private static Random getRandom() { return new Random(24353L); } - -} \ No newline at end of file +} diff --git a/quarkus-random-number-generator/src/test/java/ch/phoenixtechnologies/quarkus/commons/random/RandomProviderImplTest.java b/quarkus-random-number-generator/src/test/java/ch/phoenix/oss/quarkus/commons/random/RandomProviderImplTest.java similarity index 80% rename from quarkus-random-number-generator/src/test/java/ch/phoenixtechnologies/quarkus/commons/random/RandomProviderImplTest.java rename to quarkus-random-number-generator/src/test/java/ch/phoenix/oss/quarkus/commons/random/RandomProviderImplTest.java index 4f66f4e..bd01e75 100644 --- a/quarkus-random-number-generator/src/test/java/ch/phoenixtechnologies/quarkus/commons/random/RandomProviderImplTest.java +++ b/quarkus-random-number-generator/src/test/java/ch/phoenix/oss/quarkus/commons/random/RandomProviderImplTest.java @@ -1,16 +1,15 @@ -package ch.phoenixtechnologies.quarkus.commons.random; +package ch.phoenix.oss.quarkus.commons.random; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; -import org.junit.jupiter.api.Test; - import java.security.AccessController; import java.security.PrivilegedAction; import java.security.SecureRandom; import java.security.Security; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.junit.jupiter.api.Test; @QuarkusTest @SuppressWarnings("removal") @@ -47,16 +46,13 @@ class RandomProviderImplTest { private static String getSecurityProperty() { return AccessController.doPrivileged( - (PrivilegedAction) () -> Security.getProperty( - SECURE_RANDOM_STRONG_ALGORITHMS)); + (PrivilegedAction) () -> Security.getProperty(SECURE_RANDOM_STRONG_ALGORITHMS)); } private static void setSecurityProperty(String datum) { - AccessController.doPrivileged( - (PrivilegedAction) () -> { - Security.setProperty( - SECURE_RANDOM_STRONG_ALGORITHMS, datum); - return (Void) null; - }); + AccessController.doPrivileged((PrivilegedAction) () -> { + Security.setProperty(SECURE_RANDOM_STRONG_ALGORITHMS, datum); + return (Void) null; + }); } -} \ No newline at end of file +} diff --git a/quarkus-tracing-service/pom.xml b/quarkus-tracing-service/pom.xml new file mode 100644 index 0000000..214e936 --- /dev/null +++ b/quarkus-tracing-service/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + + ch.phoenix.oss + quarkus-commons + 1.0.9-SNAPSHOT + + + quarkus-tracing-service + jar + + + + io.quarkus + quarkus-rest-jackson + + + io.quarkus + quarkus-opentelemetry + + + io.quarkus + quarkus-security + + + io.quarkus + quarkus-elytron-security-properties-file + test + + + io.quarkus + quarkus-config-yaml + test + + + io.rest-assured + rest-assured + test + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-plugin.version} + + + jacoco-check + + check + + test + + ${project.build.directory}/jacoco-quarkus.exec + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.95 + + + + + + + + + + + + diff --git a/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/LowerCaseStringConverter.java b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/LowerCaseStringConverter.java new file mode 100644 index 0000000..ede1518 --- /dev/null +++ b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/LowerCaseStringConverter.java @@ -0,0 +1,10 @@ +package ch.phoenix.oss.quarkus.commons.tracing; + +import org.eclipse.microprofile.config.spi.Converter; + +public class LowerCaseStringConverter implements Converter { + @Override + public String convert(String value) throws IllegalArgumentException, NullPointerException { + return value.toLowerCase(); + } +} diff --git a/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingConfiguration.java b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingConfiguration.java new file mode 100644 index 0000000..1093348 --- /dev/null +++ b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingConfiguration.java @@ -0,0 +1,58 @@ +package ch.phoenix.oss.quarkus.commons.tracing; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithConverter; +import io.smallrye.config.WithDefault; +import jakarta.ws.rs.core.HttpHeaders; +import java.util.Optional; +import java.util.Set; + +@ConfigMapping(prefix = "phoenix.commons.tracing") +public interface TracingConfiguration { + + RequestFilterConfiguration requestFilter(); + + interface RequestFilterConfiguration { + + Headers headers(); + + interface Headers { + + Set DEFAULT_REDACTED = Set.of(HttpHeaders.AUTHORIZATION.toLowerCase()); + + /** + * Optional set of headers to redact when tracing. By default, redacts + * the 'Authorization' header. + * + * @return the set of headers to be redacted + */ + Optional> redact(); + } + + Path path(); + + interface Path { + + @WithDefault("false") + boolean includeRaw(); + } + + Query query(); + + interface Query { + + Set DEFAULT_REDACTED = Set.of("access_token", "refresh_token", "apikey"); + + @WithDefault("false") + boolean includeRaw(); + + /** + * Optional set of query params to redact when tracing. By default, redacts + * the following params: 'access_token', 'refresh_token' and 'apikey'. + * + * @return the set of query params to be redacted + */ + Optional> redact(); + } + } +} diff --git a/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingConstants.java b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingConstants.java new file mode 100644 index 0000000..7022a22 --- /dev/null +++ b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingConstants.java @@ -0,0 +1,18 @@ +package ch.phoenix.oss.quarkus.commons.tracing; + +public class TracingConstants { + + public static final String ACTOR = "actor"; + public static final String ANONYMOUS = "anonymous"; + public static final String REQUEST_ROUTE = "request.route"; + public static final String REQUEST_METHOD = "request.method"; + public static final String REQUEST_PATH_RAW = "request.path.raw"; + public static final String REQUEST_PATH_PARAMS = "request.path.params"; + public static final String REQUEST_QUERY_RAW = "request.query.raw"; + public static final String REQUEST_QUERY_PARAMS = "request.query.params"; + public static final String REQUEST_HEADERS = "request.headers"; + public static final String REQUEST_CLIENT_IP = "request.client.ip"; + public static final String SCHEDULER_JOB_NAME = "scheduler.job.name"; + + private TracingConstants() {} +} diff --git a/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingRequestFilter.java b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingRequestFilter.java new file mode 100644 index 0000000..7bb2427 --- /dev/null +++ b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingRequestFilter.java @@ -0,0 +1,151 @@ +package ch.phoenix.oss.quarkus.commons.tracing; + +import io.quarkus.arc.Unremovable; +import io.quarkus.logging.Log; +import io.quarkus.security.identity.SecurityIdentity; +import io.vertx.ext.web.RoutingContext; +import jakarta.inject.Inject; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ResourceInfo; +import java.util.List; +import org.jboss.resteasy.reactive.server.ServerRequestFilter; + +@Unremovable +public class TracingRequestFilter { + + private static final String REDACTED_VALUE = "********"; + + private final RoutingContext routingContext; + private final TracingService tracingService; + private final SecurityIdentity securityIdentity; + private final TracingConfiguration configuration; + + @Inject + public TracingRequestFilter( + RoutingContext routingContext, + TracingService tracingService, + SecurityIdentity securityIdentity, + TracingConfiguration configuration) { + this.routingContext = routingContext; + this.tracingService = tracingService; + this.securityIdentity = securityIdentity; + this.configuration = configuration; + } + + @ServerRequestFilter + public void filter(ContainerRequestContext requestContext, ResourceInfo resourceInfo) { + if (securityIdentity.isAnonymous()) { + tracingService.trace(TracingConstants.ACTOR, TracingConstants.ANONYMOUS); + } else { + tracingService.trace( + TracingConstants.ACTOR, securityIdentity.getPrincipal().getName()); + } + + var method = requestContext.getMethod(); + tracingService.trace(TracingConstants.REQUEST_METHOD, method); + + var routePattern = getRoutePattern(resourceInfo); + tracingService.trace(TracingConstants.REQUEST_ROUTE, routePattern); + + var uriInfo = requestContext.getUriInfo(); + uriInfo.getPathParameters() + .forEach((key, value) -> + tracingService.trace(TracingConstants.REQUEST_PATH_PARAMS + '.' + key, joinStrings(value))); + + if (configuration.requestFilter().path().includeRaw()) { + tracingService.trace( + TracingConstants.REQUEST_PATH_RAW, uriInfo.getAbsolutePath().getRawPath()); + } + + var redactedHeaders = configuration + .requestFilter() + .headers() + .redact() + .orElse(TracingConfiguration.RequestFilterConfiguration.Headers.DEFAULT_REDACTED); + + requestContext.getHeaders().forEach((key, value) -> { + var lowerCaseKey = key.toLowerCase(); + var property = TracingConstants.REQUEST_HEADERS + '.' + lowerCaseKey; + if (redactedHeaders.contains(lowerCaseKey)) { + tracingService.trace(property, REDACTED_VALUE); + } else { + tracingService.trace(property, joinStrings(value)); + } + }); + + var redactedQueryParams = configuration + .requestFilter() + .query() + .redact() + .orElse(TracingConfiguration.RequestFilterConfiguration.Query.DEFAULT_REDACTED); + + uriInfo.getQueryParameters().forEach((key, value) -> { + var lowerCaseKey = key.toLowerCase(); + var property = TracingConstants.REQUEST_QUERY_PARAMS + '.' + lowerCaseKey; + + if (redactedQueryParams.contains(lowerCaseKey)) { + tracingService.trace(property, REDACTED_VALUE); + } else { + tracingService.trace(property, joinStrings(value)); + } + }); + + if (configuration.requestFilter().query().includeRaw()) { + var rawQuery = uriInfo.getRequestUri().getRawQuery(); + if (rawQuery != null && !rawQuery.isBlank()) { + tracingService.trace(TracingConstants.REQUEST_QUERY_RAW, rawQuery); + } + } + + tracingService.trace( + TracingConstants.REQUEST_CLIENT_IP, + routingContext.request().connection().remoteAddress(true).hostAddress()); + + Log.debugf("Incoming request: %s %s", method, uriInfo.getAbsolutePath().getRawPath()); + } + + private static String joinStrings(List value) { + return String.join(", ", value); + } + + private String getRoutePattern(ResourceInfo resourceInfo) { + String classPath = getPathValue(resourceInfo.getResourceClass().getAnnotation(Path.class)); + String methodPath = getPathValue(resourceInfo.getResourceMethod().getAnnotation(Path.class)); + + if (!classPath.isEmpty()) { + if (methodPath.isEmpty()) { + return "/" + classPath; + } else { + return "/" + classPath + "/" + methodPath; + } + } else { + if (methodPath.isEmpty()) { + return "/"; + } else { + return "/" + methodPath; + } + } + } + + private static String getPathValue(Path path) { + if (path == null) { + return ""; + } + return trimSlashes(path.value()); + } + + private static String trimSlashes(String segment) { + if (segment.isEmpty() || "/".equals(segment)) { + return ""; + } + + // Assuming that it's not possible for a segment to contain //, + // thus it's possible to avoid extra ifs and whiles, as well + // as the use of regexes + int start = (segment.charAt(0) == '/') ? 1 : 0; + int end = (segment.charAt(segment.length() - 1) == '/') ? segment.length() - 1 : segment.length(); + + return segment.substring(start, end); + } +} diff --git a/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingService.java b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingService.java new file mode 100644 index 0000000..812867d --- /dev/null +++ b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingService.java @@ -0,0 +1,24 @@ +package ch.phoenix.oss.quarkus.commons.tracing; + +public interface TracingService { + + void clearAll(); + + void trace(String key, Object value); + + String getActor(); + + String getRequestPathRaw(); + + String getRequestMethod(); + + String getRequestId(); + + String getTraceId(); + + String getSpanId(); + + String getClientIp(); + + String getSchedulerJob(); +} diff --git a/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingServiceImpl.java b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingServiceImpl.java new file mode 100644 index 0000000..d46f684 --- /dev/null +++ b/quarkus-tracing-service/src/main/java/ch/phoenix/oss/quarkus/commons/tracing/TracingServiceImpl.java @@ -0,0 +1,69 @@ +package ch.phoenix.oss.quarkus.commons.tracing; + +import io.opentelemetry.api.trace.Span; +import io.quarkus.arc.DefaultBean; +import io.quarkus.logging.Log; +import jakarta.enterprise.context.ApplicationScoped; +import org.jboss.logging.MDC; + +@DefaultBean +@ApplicationScoped +class TracingServiceImpl implements TracingService { + + private final Span span; + + TracingServiceImpl(Span span) { + this.span = span; + } + + @Override + public void clearAll() { + MDC.clear(); + } + + @Override + public void trace(final String key, final Object value) { + Log.tracef("tracing key=%s value=%s", key, value); + MDC.put(key, value); + } + + @Override + public String getActor() { + return (String) MDC.get(TracingConstants.ACTOR); + } + + @Override + public String getRequestPathRaw() { + return (String) MDC.get(TracingConstants.REQUEST_PATH_RAW); + } + + @Override + public String getRequestMethod() { + return (String) MDC.get(TracingConstants.REQUEST_METHOD); + } + + @Override + public String getRequestId() { + return (String) MDC.get(TracingConstants.REQUEST_HEADERS + ".x-request-id"); + } + + @Override + public String getTraceId() { + return span.getSpanContext().getTraceId(); + } + + @Override + public String getSpanId() { + return span.getSpanContext().getSpanId(); + } + + @Override + public String getClientIp() { + return (String) MDC.get(TracingConstants.REQUEST_CLIENT_IP); + } + + @Override + public String getSchedulerJob() { + return (String) MDC.get(TracingConstants.SCHEDULER_JOB_NAME); + } +} diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/ActorTest.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/ActorTest.java new file mode 100644 index 0000000..8ab52eb --- /dev/null +++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/ActorTest.java @@ -0,0 +1,44 @@ +package ch.phoenix.oss.quarkus.commons.tracing; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectSpy; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; + +@QuarkusTest +class ActorTest { + + @InjectSpy + TracingService tracingService; + + @Test + void getAuthenticated() { + var route = "/authenticated"; + RestAssured.given() + .auth() + .basic("jon", "doe") + .accept(ContentType.TEXT) + .when() + .get(route) + .then() + .statusCode(200); + + verify(tracingService).trace("actor", "jon"); + verify(tracingService).trace("request.method", "GET"); + verify(tracingService).trace("request.route", route); + verify(tracingService).trace("request.headers.accept", "text/plain"); + verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate"); + verify(tracingService).trace("request.headers.authorization", "********"); + verify(tracingService).trace("request.headers.connection", "Keep-Alive"); + verify(tracingService).trace(eq("request.headers.host"), startsWith("localhost:")); + verify(tracingService).trace(eq("request.headers.user-agent"), startsWith("Apache-HttpClient")); + verify(tracingService).trace("request.client.ip", "127.0.0.1"); + verifyNoMoreInteractions(tracingService); + } +} diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/QueryParamTest.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/QueryParamTest.java new file mode 100644 index 0000000..10b3894 --- /dev/null +++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/QueryParamTest.java @@ -0,0 +1,54 @@ +package ch.phoenix.oss.quarkus.commons.tracing; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectSpy; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; + +@QuarkusTest +class QueryParamTest { + + @InjectSpy + TracingService tracingService; + + @Test + void traceQueryParams() { + var route = "/authenticated"; + RestAssured.given() + .auth() + .basic("jon", "doe") + .accept(ContentType.TEXT) + .header("X-SOMETHING-ELSE", "whatever") + .queryParam("access_token", "api123") + .queryParam("refresh_token", "refresh123") + .queryParam("apikey", "apikey123") + .queryParam("grant_type", "authorization_code") + .when() + .get(route) + .then() + .statusCode(200); + + verify(tracingService).trace("actor", "jon"); + verify(tracingService).trace("request.method", "GET"); + verify(tracingService).trace("request.route", route); + verify(tracingService).trace("request.headers.accept", "text/plain"); + verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate"); + verify(tracingService).trace("request.headers.authorization", "********"); + verify(tracingService).trace("request.headers.connection", "Keep-Alive"); + verify(tracingService).trace(eq("request.headers.host"), startsWith("localhost:")); + verify(tracingService).trace(eq("request.headers.user-agent"), startsWith("Apache-HttpClient")); + verify(tracingService).trace("request.headers.x-something-else", "whatever"); + verify(tracingService).trace("request.query.params.access_token", "********"); + verify(tracingService).trace("request.query.params.refresh_token", "********"); + verify(tracingService).trace("request.query.params.apikey", "********"); + verify(tracingService).trace("request.query.params.grant_type", "authorization_code"); + verify(tracingService).trace("request.client.ip", "127.0.0.1"); + verifyNoMoreInteractions(tracingService); + } +} diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RawPathTest.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RawPathTest.java new file mode 100644 index 0000000..e3579ba --- /dev/null +++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RawPathTest.java @@ -0,0 +1,46 @@ +package ch.phoenix.oss.quarkus.commons.tracing; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.quarkus.test.junit.mockito.InjectSpy; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestProfile(Test2Profile.class) +class RawPathTest { + + @InjectSpy + TracingService tracingService; + + @Test + void getWithRawPath() { + var route = "/no-leading-and-trailing/{param}/{param2}"; + RestAssured.given() + .accept(ContentType.TEXT) + .when() + .get(route, 1, 2) + .then() + .statusCode(200); + + verify(tracingService).trace("actor", "anonymous"); + verify(tracingService).trace("request.method", "GET"); + verify(tracingService).trace("request.route", route); + verify(tracingService).trace("request.path.params.param", "1"); + verify(tracingService).trace("request.path.params.param2", "2"); + verify(tracingService).trace("request.path.raw", "/no-leading-and-trailing/1/2"); + verify(tracingService).trace("request.headers.accept", "text/plain"); + verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate"); + verify(tracingService).trace("request.headers.connection", "Keep-Alive"); + verify(tracingService).trace(eq("request.headers.host"), startsWith("localhost:")); + verify(tracingService).trace(eq("request.headers.user-agent"), startsWith("Apache-HttpClient")); + verify(tracingService).trace("request.client.ip", "127.0.0.1"); + verifyNoMoreInteractions(tracingService); + } +} diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RedactedTest.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RedactedTest.java new file mode 100644 index 0000000..c928917 --- /dev/null +++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RedactedTest.java @@ -0,0 +1,61 @@ +package ch.phoenix.oss.quarkus.commons.tracing; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.quarkus.test.junit.mockito.InjectSpy; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestProfile(Test2Profile.class) +class RedactedTest { + + @InjectSpy + TracingService tracingService; + + @Test + void traceRedactedValues() { + var route = "/authenticated"; + RestAssured.given() + .auth() + .basic("jon", "doe") + .accept(ContentType.TEXT) + .header("X-SOMETHING-ELSE", "whatever") + .queryParam("access_token", "api123") + .queryParam("refresh_token", "refresh123") + .queryParam("apikey", "apikey123") + .queryParam("grant_type", "authorization_code") + .when() + .get(route) + .then() + .statusCode(200); + + verify(tracingService).trace("actor", "jon"); + verify(tracingService).trace("request.method", "GET"); + verify(tracingService).trace("request.route", route); + verify(tracingService).trace("request.path.raw", route); + verify(tracingService).trace("request.headers.accept", "text/plain"); + verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate"); + verify(tracingService).trace("request.headers.authorization", "********"); + verify(tracingService).trace("request.headers.connection", "Keep-Alive"); + verify(tracingService).trace(eq("request.headers.host"), startsWith("localhost:")); + verify(tracingService).trace(eq("request.headers.user-agent"), startsWith("Apache-HttpClient")); + verify(tracingService).trace("request.headers.x-something-else", "********"); + verify(tracingService).trace("request.query.params.access_token", "********"); + verify(tracingService).trace("request.query.params.refresh_token", "refresh123"); + verify(tracingService).trace("request.query.params.apikey", "apikey123"); + verify(tracingService).trace("request.query.params.grant_type", "authorization_code"); + verify(tracingService) + .trace( + "request.query.raw", + "access_token=api123&refresh_token=refresh123&apikey=apikey123&grant_type=authorization_code"); + verify(tracingService).trace("request.client.ip", "127.0.0.1"); + verifyNoMoreInteractions(tracingService); + } +} diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RoutePatternTest.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RoutePatternTest.java new file mode 100644 index 0000000..156c0a4 --- /dev/null +++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/RoutePatternTest.java @@ -0,0 +1,87 @@ +package ch.phoenix.oss.quarkus.commons.tracing; + +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectSpy; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@QuarkusTest +class RoutePatternTest { + + @InjectSpy + TracingService tracingService; + + static Stream get() { + return Stream.of( + arguments("/", Map.of()), + arguments("/leading-and-no-trailing", Map.of()), + arguments("/leading/{param}/{param2}", Map.of("param", "1", "param2", "2")), + arguments("/{param}/{param2}/trailing", Map.of("param", "1", "param2", "2")), + arguments("/leading-and-no-trailing/{param}", Map.of("param", "1")), + arguments("/leading-and-no-trailing/{param}/{param2}", Map.of("param", "1", "param2", "2")), + arguments("/leading-and-trailing", Map.of()), + arguments("/leading-and-trailing/{param}", Map.of("param", "1")), + arguments("/leading-and-trailing/{param}/{param2}", Map.of("param", "1", "param2", "2")), + arguments("/no-leading-and-no-trailing", Map.of()), + arguments("/no-leading-and-no-trailing/{param}", Map.of("param", "1")), + arguments("/no-leading-and-no-trailing/{param}/{param2}", Map.of("param", "1", "param2", "2")), + arguments("/no-leading-and-trailing", Map.of()), + arguments("/no-leading-and-trailing/{param}", Map.of("param", "1")), + arguments("/no-leading-and-trailing/{param}/{param2}", Map.of("param", "1", "param2", "2"))); + } + + @MethodSource + @ParameterizedTest + void get(String route, Map pathParams) { + RestAssured.given() + .accept(ContentType.TEXT) + .when() + .get(route, pathParams) + .then() + .statusCode(200); + + verify(tracingService).trace("actor", "anonymous"); + verify(tracingService).trace("request.method", "GET"); + verify(tracingService).trace("request.route", route); + pathParams.forEach((key, value) -> verify(tracingService).trace("request.path.params." + key, value)); + verify(tracingService).trace("request.headers.accept", "text/plain"); + verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate"); + verify(tracingService).trace("request.headers.connection", "Keep-Alive"); + verify(tracingService).trace(eq("request.headers.host"), startsWith("localhost:")); + verify(tracingService).trace(eq("request.headers.user-agent"), startsWith("Apache-HttpClient")); + verify(tracingService).trace("request.client.ip", "127.0.0.1"); + verifyNoMoreInteractions(tracingService); + } + + @Test + void postBlankResource() { + var route = "/"; + RestAssured.given().accept(ContentType.TEXT).when().post(route).then().statusCode(200); + + verify(tracingService).trace("actor", "anonymous"); + verify(tracingService).trace("request.method", "POST"); + verify(tracingService).trace("request.route", route); + verify(tracingService).trace("request.headers.accept", "text/plain"); + verify(tracingService).trace("request.headers.accept-encoding", "gzip,deflate"); + verify(tracingService).trace("request.headers.connection", "Keep-Alive"); + verify(tracingService).trace("request.headers.content-length", "0"); + verify(tracingService) + .trace("request.headers.content-type", "application/x-www-form-urlencoded; charset=ISO-8859-1"); + verify(tracingService).trace(eq("request.headers.host"), startsWith("localhost:")); + verify(tracingService).trace(eq("request.headers.user-agent"), startsWith("Apache-HttpClient")); + verify(tracingService).trace("request.client.ip", "127.0.0.1"); + verifyNoMoreInteractions(tracingService); + } +} diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/Test2Profile.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/Test2Profile.java new file mode 100644 index 0000000..f98dcff --- /dev/null +++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/Test2Profile.java @@ -0,0 +1,11 @@ +package ch.phoenix.oss.quarkus.commons.tracing; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class Test2Profile implements QuarkusTestProfile { + + @Override + public String getConfigProfile() { + return "test2"; + } +} diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/TracingServiceImplTest.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/TracingServiceImplTest.java new file mode 100644 index 0000000..f728e14 --- /dev/null +++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/TracingServiceImplTest.java @@ -0,0 +1,98 @@ +package ch.phoenix.oss.quarkus.commons.tracing; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.jboss.logging.MDC; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +class TracingServiceImplTest { + + @Inject + TracingService tracingService; + + @Inject + Span span; + + @BeforeEach + void setUp() { + MDC.clear(); + } + + @Test + void getActor() { + tracingService.trace("actor", "abc"); + assertThat(tracingService.getActor()) + .as("Actor should match expected value") + .isEqualTo("abc"); + } + + @Test + void getRequestPathRaw() { + tracingService.trace("request.path.raw", "/foo/bar"); + assertThat(tracingService.getRequestPathRaw()) + .as("Request Path Raw should match expected value") + .isEqualTo("/foo/bar"); + } + + @Test + void getRequestMethod() { + tracingService.trace("request.method", "GET"); + assertThat(tracingService.getRequestMethod()) + .as("Request Method should match expected value") + .isEqualTo("GET"); + } + + @Test + void getRequestId() { + tracingService.trace("request.headers.x-request-id", "ba458367-bfeb-46ba-87da-50b9343be8f9"); + assertThat(tracingService.getRequestId()) + .as("Request Id should match expected value") + .isEqualTo("ba458367-bfeb-46ba-87da-50b9343be8f9"); + } + + @Test + @WithSpan + void getTraceId() { + assertThat(tracingService.getTraceId()) + .as("Request Trace Id should match expected value") + .isEqualTo(span.getSpanContext().getTraceId()); + } + + @Test + @WithSpan + void getSpanId() { + assertThat(tracingService.getSpanId()) + .as("Request Span Id should match expected value") + .isEqualTo(span.getSpanContext().getSpanId()); + } + + @Test + void getClientIp() { + tracingService.trace("request.client.ip", "127.0.0.1"); + assertThat(tracingService.getClientIp()) + .as("Request Client Iü should match expected value") + .isEqualTo("127.0.0.1"); + } + + @Test + void getSchedulerJob() { + tracingService.trace("scheduler.job.name", "scheduler/abc"); + assertThat(tracingService.getSchedulerJob()) + .as("Scheduler Job Name should match expected value") + .isEqualTo("scheduler/abc"); + } + + @Test + void clearAll() { + tracingService.trace("aaa", "bbb"); + assertThat(MDC.get("aaa")).isEqualTo("bbb"); + tracingService.clearAll(); + assertThat(MDC.get("aaa")).isNull(); + } +} diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/AuthenticatedResource.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/AuthenticatedResource.java new file mode 100644 index 0000000..d18b967 --- /dev/null +++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/AuthenticatedResource.java @@ -0,0 +1,15 @@ +package ch.phoenix.oss.quarkus.commons.tracing.resource; + +import io.quarkus.security.Authenticated; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Authenticated +@Path("authenticated") +public class AuthenticatedResource { + + @GET + public String get() { + return "Hello user"; + } +} diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/BlankResource.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/BlankResource.java new file mode 100644 index 0000000..04c5df9 --- /dev/null +++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/BlankResource.java @@ -0,0 +1,25 @@ +package ch.phoenix.oss.quarkus.commons.tracing.resource; + +import jakarta.annotation.security.PermitAll; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("") +@Produces(MediaType.TEXT_PLAIN) +public class BlankResource { + + @GET + @PermitAll + public String get() { + return "get"; + } + + @POST + @Path("") + public String post() { + return "post"; + } +} diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/LeadingAndNoTrailingResource.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/LeadingAndNoTrailingResource.java new file mode 100644 index 0000000..b03a7ae --- /dev/null +++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/LeadingAndNoTrailingResource.java @@ -0,0 +1,29 @@ +package ch.phoenix.oss.quarkus.commons.tracing.resource; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/leading-and-no-trailing") +@Produces(MediaType.TEXT_PLAIN) +public class LeadingAndNoTrailingResource { + + @GET + @Path("") + public String root() { + return "root"; + } + + @GET + @Path("/{param}") + public String singleParam(String param) { + return param; + } + + @GET + @Path("/{param}/{param2}") + public String multiParam(String param, String param2) { + return param + "/" + param2; + } +} diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/LeadingAndTrailingResource.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/LeadingAndTrailingResource.java new file mode 100644 index 0000000..1dbc175 --- /dev/null +++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/LeadingAndTrailingResource.java @@ -0,0 +1,29 @@ +package ch.phoenix.oss.quarkus.commons.tracing.resource; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/leading-and-trailing/") +@Produces(MediaType.TEXT_PLAIN) +public class LeadingAndTrailingResource { + + @GET + @Path("/") + public String root() { + return "root"; + } + + @GET + @Path("/{param}/") + public String singleParam(String param) { + return param; + } + + @GET + @Path("/{param}/{param2}/") + public String multiParam(String param, String param2) { + return param + "/" + param2; + } +} diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/NoLeadingAndNoTrailingResource.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/NoLeadingAndNoTrailingResource.java new file mode 100644 index 0000000..d75dade --- /dev/null +++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/NoLeadingAndNoTrailingResource.java @@ -0,0 +1,29 @@ +package ch.phoenix.oss.quarkus.commons.tracing.resource; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("no-leading-and-no-trailing") +@Produces(MediaType.TEXT_PLAIN) +public class NoLeadingAndNoTrailingResource { + + @GET + @Path("") + public String root() { + return "root"; + } + + @GET + @Path("{param}") + public String singleParam(String param) { + return param; + } + + @GET + @Path("{param}/{param2}") + public String multiParam(String param, String param2) { + return param + "/" + param2; + } +} diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/NoLeadingAndTrailingResource.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/NoLeadingAndTrailingResource.java new file mode 100644 index 0000000..9c9805a --- /dev/null +++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/NoLeadingAndTrailingResource.java @@ -0,0 +1,29 @@ +package ch.phoenix.oss.quarkus.commons.tracing.resource; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("no-leading-and-trailing/") +@Produces(MediaType.TEXT_PLAIN) +public class NoLeadingAndTrailingResource { + + @GET + @Path("/") + public String root() { + return "root"; + } + + @GET + @Path("{param}/") + public String singleParam(String param) { + return param; + } + + @GET + @Path("{param}/{param2}/") + public String multiParam(String param, String param2) { + return param + "/" + param2; + } +} diff --git a/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/SlashResource.java b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/SlashResource.java new file mode 100644 index 0000000..8c8bdc7 --- /dev/null +++ b/quarkus-tracing-service/src/test/java/ch/phoenix/oss/quarkus/commons/tracing/resource/SlashResource.java @@ -0,0 +1,23 @@ +package ch.phoenix.oss.quarkus.commons.tracing.resource; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class SlashResource { + + @GET + @Path("/leading/{param}/{param2}") + public String doubleLeading(int param, int param2) { + return "leading/" + param + "/" + param2; + } + + @GET + @Path("{param}/{param2}/trailing/") + public String doubleTrailing(int param, int param2) { + return param + "/" + param2 + "/trailing"; + } +} diff --git a/quarkus-tracing-service/src/test/resources/application.yaml b/quarkus-tracing-service/src/test/resources/application.yaml new file mode 100644 index 0000000..3c6bb6f --- /dev/null +++ b/quarkus-tracing-service/src/test/resources/application.yaml @@ -0,0 +1,39 @@ +quarkus: + http: + test-port: 0 + log: + category: + "org.apache.http.wire": + level: DEBUG # set to DEBUG when RestAssured logs are necessary to understand test failures + otel: + traces: + exporter: none + security: + users: + embedded: + enabled: true + plain-text: true + users: + jon: doe + +"%test2": + quarkus: + log: + min-level: TRACE + category: + "ch.phoenix.oss.quarkus.commons.tracing": + level: TRACE + phoenix: + commons: + tracing: + request-filter: + path: + include-raw: true + headers: + redact: + - AUTHORIZATION + - X-SOMETHING-ELSE + query: + include-raw: true + redact: + - ACCESS_TOKEN \ No newline at end of file diff --git a/quarkus-uuid-generator/pom.xml b/quarkus-uuid-generator/pom.xml index 4fd38d8..a6c395c 100644 --- a/quarkus-uuid-generator/pom.xml +++ b/quarkus-uuid-generator/pom.xml @@ -1,13 +1,11 @@ - + 4.0.0 - ch.phoenixtechnologies.quarkus + ch.phoenix.oss quarkus-commons - 0.1.0-SNAPSHOT + 1.0.9-SNAPSHOT quarkus-uuid-generator diff --git a/quarkus-uuid-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/uuid/UUIDGenerator.java b/quarkus-uuid-generator/src/main/java/ch/phoenix/oss/quarkus/commons/uuid/UUIDGenerator.java similarity index 94% rename from quarkus-uuid-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/uuid/UUIDGenerator.java rename to quarkus-uuid-generator/src/main/java/ch/phoenix/oss/quarkus/commons/uuid/UUIDGenerator.java index 3848724..d4df687 100644 --- a/quarkus-uuid-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/uuid/UUIDGenerator.java +++ b/quarkus-uuid-generator/src/main/java/ch/phoenix/oss/quarkus/commons/uuid/UUIDGenerator.java @@ -1,4 +1,4 @@ -package ch.phoenixtechnologies.quarkus.commons.uuid; +package ch.phoenix.oss.quarkus.commons.uuid; import java.util.Random; import java.util.UUID; @@ -33,5 +33,4 @@ public interface UUIDGenerator { default UUID generateV5WithX500Namespace(String name) { return generateV5(NAMESPACE_X500, name); } - } diff --git a/quarkus-uuid-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/uuid/UUIDGeneratorImpl.java b/quarkus-uuid-generator/src/main/java/ch/phoenix/oss/quarkus/commons/uuid/UUIDGeneratorImpl.java similarity index 93% rename from quarkus-uuid-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/uuid/UUIDGeneratorImpl.java rename to quarkus-uuid-generator/src/main/java/ch/phoenix/oss/quarkus/commons/uuid/UUIDGeneratorImpl.java index 9f846be..9f4a501 100644 --- a/quarkus-uuid-generator/src/main/java/ch/phoenixtechnologies/quarkus/commons/uuid/UUIDGeneratorImpl.java +++ b/quarkus-uuid-generator/src/main/java/ch/phoenix/oss/quarkus/commons/uuid/UUIDGeneratorImpl.java @@ -1,9 +1,8 @@ -package ch.phoenixtechnologies.quarkus.commons.uuid; +package ch.phoenix.oss.quarkus.commons.uuid; import com.fasterxml.uuid.Generators; import io.quarkus.arc.DefaultBean; import jakarta.enterprise.context.ApplicationScoped; - import java.util.Random; import java.util.UUID; @@ -30,5 +29,4 @@ class UUIDGeneratorImpl implements UUIDGenerator { public UUID generateV5(UUID namespace, String name) { return Generators.nameBasedGenerator(namespace).generate(name); } - } diff --git a/quarkus-uuid-generator/src/test/java/ch/phoenixtechnologies/quarkus/commons/uuid/UUIDGeneratorImplTest.java b/quarkus-uuid-generator/src/test/java/ch/phoenix/oss/quarkus/commons/uuid/UUIDGeneratorImplTest.java similarity index 80% rename from quarkus-uuid-generator/src/test/java/ch/phoenixtechnologies/quarkus/commons/uuid/UUIDGeneratorImplTest.java rename to quarkus-uuid-generator/src/test/java/ch/phoenix/oss/quarkus/commons/uuid/UUIDGeneratorImplTest.java index 7b1cc6f..25f6b02 100644 --- a/quarkus-uuid-generator/src/test/java/ch/phoenixtechnologies/quarkus/commons/uuid/UUIDGeneratorImplTest.java +++ b/quarkus-uuid-generator/src/test/java/ch/phoenix/oss/quarkus/commons/uuid/UUIDGeneratorImplTest.java @@ -1,19 +1,17 @@ -package ch.phoenixtechnologies.quarkus.commons.uuid; +package ch.phoenix.oss.quarkus.commons.uuid; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import ch.phoenixtechnologies.quarkus.commons.uuid.UUIDGeneratorImpl; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; +import java.util.Random; +import java.util.UUID; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.util.Random; -import java.util.UUID; -import java.util.stream.Stream; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - @QuarkusTest class UUIDGeneratorImplTest { @@ -44,9 +42,7 @@ class UUIDGeneratorImplTest { var actual = generator.generateV4(getRandom()); - assertThat(actual) - .as("Generated UUID should match expected value") - .isEqualTo(expected); + assertThat(actual).as("Generated UUID should match expected value").isEqualTo(expected); } @Test @@ -55,9 +51,7 @@ class UUIDGeneratorImplTest { var actual = generator.generateV5("test"); - assertThat(actual) - .as("Generated UUID should match expected value") - .isEqualTo(expected); + assertThat(actual).as("Generated UUID should match expected value").isEqualTo(expected); } public static Stream generateV5WithNamespace() { @@ -65,8 +59,7 @@ class UUIDGeneratorImplTest { Arguments.of(NAMESPACE_DNS, V5_NAME, EXPECTED_V5_DNS), Arguments.of(NAMESPACE_URL, V5_NAME, EXPECTED_V5_URL), Arguments.of(NAMESPACE_OID, V5_NAME, EXPECTED_V5_OID), - Arguments.of(NAMESPACE_X500, V5_NAME, EXPECTED_V5_X500) - ); + Arguments.of(NAMESPACE_X500, V5_NAME, EXPECTED_V5_X500)); } @MethodSource @@ -74,27 +67,21 @@ class UUIDGeneratorImplTest { void generateV5WithNamespace(UUID namespace, String name, UUID expected) { var actual = generator.generateV5(namespace, name); - assertThat(actual) - .as("Generated UUID should match expected value") - .isEqualTo(expected); + assertThat(actual).as("Generated UUID should match expected value").isEqualTo(expected); } @Test void generateV5WithDNSNamespace() { var actual = generator.generateV5WithDNSNamespace(V5_NAME); - assertThat(actual) - .as("Generated UUID should match expected value") - .isEqualTo(EXPECTED_V5_DNS); + assertThat(actual).as("Generated UUID should match expected value").isEqualTo(EXPECTED_V5_DNS); } @Test void generateV5WithURLNamespace() { var actual = generator.generateV5WithURLNamespace(V5_NAME); - assertThat(actual) - .as("Generated UUID should match expected value") - .isEqualTo(EXPECTED_V5_URL); + assertThat(actual).as("Generated UUID should match expected value").isEqualTo(EXPECTED_V5_URL); } @Test @@ -114,5 +101,4 @@ class UUIDGeneratorImplTest { private static Random getRandom() { return new Random(24353L); } - -} \ No newline at end of file +}