From 5d7bd12baec904d02d6b728888610336c2df3999 Mon Sep 17 00:00:00 2001 From: milanhorvath <35398860+milanhorvath@users.noreply.github.com> Date: Mon, 22 Jan 2024 08:47:39 +0100 Subject: [PATCH] feat: initial commit (#1) * feat: initial commit * feat: user only internal API from svc --------- Co-authored-by: milan.horvath --- .github/changelog.yaml | 13 + .github/dependabot.yml | 8 + .github/workflows/build-branch.yml | 13 + .github/workflows/build-pr.yml | 9 + .github/workflows/build-release.yml | 9 + .github/workflows/build.yml | 23 + .github/workflows/create-fix-branch.yml | 7 + .github/workflows/create-release.yml | 7 + .github/workflows/documentation.yml | 10 + .github/workflows/sonar-pr.yml | 12 + .gitignore | 44 ++ pom.xml | 172 ++++ src/main/docker/Dockerfile.jvm | 13 + src/main/docker/Dockerfile.native | 11 + src/main/helm/Chart.yaml | 17 + src/main/helm/values.yaml | 4 + .../controllers/UserAvatarRestController.java | 93 +++ .../UserProfileAdminRestController.java | 96 +++ .../UserProfileRestController.java | 152 ++++ .../bff/rs/mappers/ExceptionMapper.java | 60 ++ .../bff/rs/mappers/ProblemDetailMapper.java | 17 + .../bff/rs/mappers/UserProfileMapper.java | 38 + src/main/openapi/openapi-bff.yaml | 678 ++++++++++++++++ src/main/resources/application.properties | 40 + .../user/profile/bff/rs/AbstractTest.java | 58 ++ .../bff/rs/UserAvatarRestControllerIT.java | 7 + .../bff/rs/UserAvatarRestControllerTest.java | 301 +++++++ .../rs/UserProfileAdminRestControllerIT.java | 7 + .../UserProfileAdminRestControllerTest.java | 339 ++++++++ .../bff/rs/UserProfileRestControllerIT.java | 7 + .../bff/rs/UserProfileRestControllerTest.java | 735 ++++++++++++++++++ src/test/resources/data/avatar_test.jpg | Bin 0 -> 17636 bytes src/test/resources/mockserver.properties | 15 + src/test/resources/mockserver/internal.json | 21 + 34 files changed, 3036 insertions(+) create mode 100644 .github/changelog.yaml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build-branch.yml create mode 100644 .github/workflows/build-pr.yml create mode 100644 .github/workflows/build-release.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/create-fix-branch.yml create mode 100644 .github/workflows/create-release.yml create mode 100644 .github/workflows/documentation.yml create mode 100644 .github/workflows/sonar-pr.yml create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/docker/Dockerfile.jvm create mode 100644 src/main/docker/Dockerfile.native create mode 100644 src/main/helm/Chart.yaml create mode 100644 src/main/helm/values.yaml create mode 100644 src/main/java/io/github/onecx/user/profile/bff/rs/controllers/UserAvatarRestController.java create mode 100644 src/main/java/io/github/onecx/user/profile/bff/rs/controllers/UserProfileAdminRestController.java create mode 100644 src/main/java/io/github/onecx/user/profile/bff/rs/controllers/UserProfileRestController.java create mode 100644 src/main/java/io/github/onecx/user/profile/bff/rs/mappers/ExceptionMapper.java create mode 100644 src/main/java/io/github/onecx/user/profile/bff/rs/mappers/ProblemDetailMapper.java create mode 100644 src/main/java/io/github/onecx/user/profile/bff/rs/mappers/UserProfileMapper.java create mode 100644 src/main/openapi/openapi-bff.yaml create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/io/github/onecx/user/profile/bff/rs/AbstractTest.java create mode 100644 src/test/java/io/github/onecx/user/profile/bff/rs/UserAvatarRestControllerIT.java create mode 100644 src/test/java/io/github/onecx/user/profile/bff/rs/UserAvatarRestControllerTest.java create mode 100644 src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileAdminRestControllerIT.java create mode 100644 src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileAdminRestControllerTest.java create mode 100644 src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileRestControllerIT.java create mode 100644 src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileRestControllerTest.java create mode 100644 src/test/resources/data/avatar_test.jpg create mode 100644 src/test/resources/mockserver.properties create mode 100644 src/test/resources/mockserver/internal.json diff --git a/.github/changelog.yaml b/.github/changelog.yaml new file mode 100644 index 0000000..0a5526e --- /dev/null +++ b/.github/changelog.yaml @@ -0,0 +1,13 @@ +sections: + - title: Major changes + labels: + - "release/super-feature" + - title: Complete changelog + labels: + - "bug" + - "enhancement" + - "dependencies" +template: | + {{ range $section := .Sections }}{{ if $section.Items }}### {{ $section.GetTitle }}{{ range $item := $section.Items }} + * [#{{ $item.GetID }}]({{ $item.GetURL }}) - {{ $item.GetTitle }}{{ end }}{{ end }} + {{ end }} \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..64412da --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: maven + directory: "/" + schedule: + interval: daily + labels: + - dependencies \ No newline at end of file diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml new file mode 100644 index 0000000..7270213 --- /dev/null +++ b/.github/workflows/build-branch.yml @@ -0,0 +1,13 @@ +name: Build Feature Branch + +on: + push: + branches: + - '**' + - '!main' + - '!fix/[0-9]+.[0-9]+.x' + +jobs: + branch: + uses: onecx/ci-quarkus/.github/workflows/build-branch.yml@v1 + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml new file mode 100644 index 0000000..acff155 --- /dev/null +++ b/.github/workflows/build-pr.yml @@ -0,0 +1,9 @@ +name: Build Pull Request + +on: + pull_request: + +jobs: + pr: + uses: onecx/ci-quarkus/.github/workflows/build-pr.yml@v1 + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000..b04a59a --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,9 @@ +name: Build Release +on: + push: + tags: + - '**' +jobs: + release: + uses: onecx/ci-quarkus/.github/workflows/build-release.yml@v1 + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d4cd036 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,23 @@ +name: Build + +on: + workflow_dispatch: + push: + branches: + - 'main' + - 'fix/[0-9]+.[0-9]+.x' + paths-ignore: + - '.gitignore' + - 'CODEOWNERS' + - 'LICENSE' + - '*.md' + - '*.adoc' + - '*.txt' + - '.all-contributorsrc' + +jobs: + build: + uses: onecx/ci-quarkus/.github/workflows/build.yml@v1 + secrets: inherit + with: + helmEventTargetRepository: onecx/onecx-theme \ No newline at end of file diff --git a/.github/workflows/create-fix-branch.yml b/.github/workflows/create-fix-branch.yml new file mode 100644 index 0000000..92af624 --- /dev/null +++ b/.github/workflows/create-fix-branch.yml @@ -0,0 +1,7 @@ +name: Create Fix Branch +on: + workflow_dispatch: +jobs: + fix: + uses: onecx/ci-common/.github/workflows/create-fix-branch.yml@v1 + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 0000000..c97eb42 --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,7 @@ +name: Create Release Version +on: + workflow_dispatch: +jobs: + release: + uses: onecx/ci-common/.github/workflows/create-release.yml@v1 + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..3d17922 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,10 @@ +name: Update documentation +on: + push: + branches: [ main ] + paths: + - 'docs/**' +jobs: + release: + uses: onecx/ci-common/.github/workflows/documentation.yml@v1 + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/sonar-pr.yml b/.github/workflows/sonar-pr.yml new file mode 100644 index 0000000..02c3e1e --- /dev/null +++ b/.github/workflows/sonar-pr.yml @@ -0,0 +1,12 @@ +name: Sonar Pull Request + +on: + workflow_run: + workflows: ["Build Pull Request"] + types: + - completed + +jobs: + pr: + uses: onecx/ci-quarkus/.github/workflows/quarkus-pr-sonar.yml@v1 + secrets: inherit \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5122ec0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env +*.sh + +# Plugin directory +/.quarkus/cli/plugins/ \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..bcc8e80 --- /dev/null +++ b/pom.xml @@ -0,0 +1,172 @@ + + + 4.0.0 + + io.github.onecx + onecx-quarkus3-parent + 0.25.0 + + + onecx-user-profile-bff + 999-SNAPSHOT + + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-smallrye-openapi + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkus + quarkus-smallrye-health + + + io.quarkiverse.openapi.generator + quarkus-openapi-generator + + + io.quarkus + quarkus-rest-client-reactive-jackson + + + + org.tkit.quarkus.lib + tkit-quarkus-log-cdi + + + org.tkit.quarkus.lib + tkit-quarkus-log-rs + + + org.tkit.quarkus.lib + tkit-quarkus-log-json + + + org.tkit.quarkus.lib + tkit-quarkus-rest + + + org.tkit.quarkus.lib + tkit-quarkus-jpa + + + org.mapstruct + mapstruct + + + io.quarkus + quarkus-hibernate-validator + + + org.tkit.quarkus.lib + tkit-quarkus-rest-context + + + + io.quarkiverse.mockserver + quarkus-mockserver + provided + + + + + org.eclipse.microprofile.jwt + microprofile-jwt-auth-api + test + + + io.quarkiverse.mockserver + quarkus-mockserver-test + + + io.swagger.parser.v3 + swagger-parser + + + test + + + io.swagger.parser.v3 + swagger-parser + test + + + + + + + org.openapitools + openapi-generator-maven-plugin + + + internal + + generate + + + src/main/openapi/openapi-bff.yaml + gen.io.github.onecx.user.profile.bff.rs.internal + gen.io.github.onecx.user.profile.bff.rs.internal.model + + + + + jaxrs-spec + ApiService + DTO + false + false + false + false + false + true + quarkus + + / + false + true + true + true + true + true + java8 + true + true + false + true + + + + + com.googlecode.maven-download-plugin + download-maven-plugin + + + user-profile-svc-internal + generate-resources + + wget + + + + https://raw.githubusercontent.com/onecx/onecx-user-profile-svc/main/src/main/openapi/onecx-userprofile-internal-openapi.yaml + + target/tmp/openapi + onecx-userprofile-internal-openapi.yaml + true + + + + + + + \ No newline at end of file diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..a554421 --- /dev/null +++ b/src/main/docker/Dockerfile.jvm @@ -0,0 +1,13 @@ +FROM registry.access.redhat.com/ubi9/openjdk-17:1.15 + +ENV LANGUAGE='en_US:en' + +COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 target/quarkus-app/*.jar /deployments/ +COPY --chown=185 target/quarkus-app/app/ /deployments/app/ +COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" \ No newline at end of file diff --git a/src/main/docker/Dockerfile.native b/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..c97f217 --- /dev/null +++ b/src/main/docker/Dockerfile.native @@ -0,0 +1,11 @@ +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.2 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] \ No newline at end of file diff --git a/src/main/helm/Chart.yaml b/src/main/helm/Chart.yaml new file mode 100644 index 0000000..0560b2a --- /dev/null +++ b/src/main/helm/Chart.yaml @@ -0,0 +1,17 @@ +apiVersion: v2 +name: onecx-user-profile-bff +version: 0.0.0 +appVersion: 0.0.0 +description: Onecx user profile bff +keywords: + - user profile +sources: + - https://github.com/onecx/onecx-user-profile-bff +maintainers: + - name: Tkit Developer + email: tkit_dev@1000kit.org +dependencies: + - name: helm-quarkus-app + alias: app + version: ^0 + repository: oci://ghcr.io/onecx/charts \ No newline at end of file diff --git a/src/main/helm/values.yaml b/src/main/helm/values.yaml new file mode 100644 index 0000000..c65d440 --- /dev/null +++ b/src/main/helm/values.yaml @@ -0,0 +1,4 @@ +app: + name: bff + image: + repository: "onecx/onecx-user-profile-bff" diff --git a/src/main/java/io/github/onecx/user/profile/bff/rs/controllers/UserAvatarRestController.java b/src/main/java/io/github/onecx/user/profile/bff/rs/controllers/UserAvatarRestController.java new file mode 100644 index 0000000..a983042 --- /dev/null +++ b/src/main/java/io/github/onecx/user/profile/bff/rs/controllers/UserAvatarRestController.java @@ -0,0 +1,93 @@ +package io.github.onecx.user.profile.bff.rs.controllers; + +import java.io.File; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.user.profile.bff.clients.api.AvatarApi; +import gen.io.github.onecx.user.profile.bff.clients.model.ImageInfo; +import gen.io.github.onecx.user.profile.bff.clients.model.ProblemDetailResponse; +import gen.io.github.onecx.user.profile.bff.rs.internal.UserAvatarApiService; +import io.github.onecx.user.profile.bff.rs.mappers.ExceptionMapper; +import io.github.onecx.user.profile.bff.rs.mappers.ProblemDetailMapper; +import io.github.onecx.user.profile.bff.rs.mappers.UserProfileMapper; + +@ApplicationScoped +@Transactional(value = Transactional.TxType.NOT_SUPPORTED) +@LogService +public class UserAvatarRestController implements UserAvatarApiService { + + @Inject + @RestClient + AvatarApi client; + + @Inject + UserProfileMapper mapper; + + @Inject + ExceptionMapper exceptionMapper; + + @Inject + ProblemDetailMapper problemDetailMapper; + + @Override + public Response deleteUserAvatar() { + try (Response response = client.deleteUserAvatar()) { + return Response.status(response.getStatus()).build(); + } + } + + @Override + public Response getUserAvatar(String id) { + try (Response response = client.getUserAvatar(id)) { + var byteResponse = response.readEntity(byte[].class); + return Response.status(response.getStatus()) + .entity(byteResponse).build(); + } + } + + @Override + public Response getUserAvatarInfo() { + try (Response response = client.getUserAvatarInfo()) { + var imageInfo = response.readEntity(ImageInfo.class); + return Response.status(response.getStatus()) + .entity(mapper.map(imageInfo)).build(); + } + } + + @Override + public Response uploadAvatar(File body) { + try (Response response = client.uploadAvatar(body)) { + var imageInfo = response.readEntity(ImageInfo.class); + return Response.status(response.getStatus()) + .entity(mapper.map(imageInfo)).build(); + } + } + + @ServerExceptionMapper + public Response restException(WebApplicationException ex) { + // if client response is bad request remap bad request to DTO + if (ex.getResponse().getStatus() == RestResponse.StatusCode.BAD_REQUEST) { + try { + var clientError = ex.getResponse().readEntity(ProblemDetailResponse.class); + return Response.status(ex.getResponse().getStatus()).type(MediaType.APPLICATION_JSON_TYPE) + .entity(problemDetailMapper.map(clientError)).build(); + } catch (Exception e) { + // ignore error the bad request has not problem detail response object + return Response.status(ex.getResponse().getStatus()).build(); + } + } else { + return Response.status(ex.getResponse().getStatus()).build(); + } + } +} diff --git a/src/main/java/io/github/onecx/user/profile/bff/rs/controllers/UserProfileAdminRestController.java b/src/main/java/io/github/onecx/user/profile/bff/rs/controllers/UserProfileAdminRestController.java new file mode 100644 index 0000000..d49903c --- /dev/null +++ b/src/main/java/io/github/onecx/user/profile/bff/rs/controllers/UserProfileAdminRestController.java @@ -0,0 +1,96 @@ +package io.github.onecx.user.profile.bff.rs.controllers; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.ConstraintViolationException; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.user.profile.bff.clients.api.UserProfileAdminApi; +import gen.io.github.onecx.user.profile.bff.clients.model.ProblemDetailResponse; +import gen.io.github.onecx.user.profile.bff.clients.model.UserProfile; +import gen.io.github.onecx.user.profile.bff.clients.model.UserProfilePageResult; +import gen.io.github.onecx.user.profile.bff.rs.internal.UserProfileAdminApiService; +import gen.io.github.onecx.user.profile.bff.rs.internal.model.ProblemDetailResponseDTO; +import gen.io.github.onecx.user.profile.bff.rs.internal.model.UpdateUserPersonRequestDTO; +import gen.io.github.onecx.user.profile.bff.rs.internal.model.UserPersonCriteriaDTO; +import io.github.onecx.user.profile.bff.rs.mappers.ExceptionMapper; +import io.github.onecx.user.profile.bff.rs.mappers.ProblemDetailMapper; +import io.github.onecx.user.profile.bff.rs.mappers.UserProfileMapper; + +@ApplicationScoped +@Transactional(value = Transactional.TxType.NOT_SUPPORTED) +@LogService +public class UserProfileAdminRestController implements UserProfileAdminApiService { + + @Inject + @RestClient + UserProfileAdminApi client; + + @Inject + UserProfileMapper mapper; + + @Inject + ExceptionMapper exceptionMapper; + + @Inject + ProblemDetailMapper problemDetailMapper; + + @Override + public Response deleteUserProfile(String id) { + try (Response response = client.deleteUserProfile(id)) { + return Response.status(response.getStatus()).build(); + } + } + + @Override + public Response getUserProfile(String id) { + try (Response response = client.getUserProfile(id)) { + var userProfile = response.readEntity(UserProfile.class); + return Response.status(response.getStatus()).entity(mapper.map(userProfile)).build(); + } + } + + @Override + public Response searchUserProfile(UserPersonCriteriaDTO userPersonCriteriaDTO) { + try (Response response = client.searchUserProfile(mapper.map(userPersonCriteriaDTO))) { + var userProfilePageResult = response.readEntity(UserProfilePageResult.class); + return Response.status(response.getStatus()).entity(mapper.map(userProfilePageResult)).build(); + } + } + + @Override + public Response updateUserProfile(String id, UpdateUserPersonRequestDTO updateUserPersonRequestDTO) { + try (Response response = client.updateUserProfile(id, mapper.map(updateUserPersonRequestDTO))) { + return Response.status(response.getStatus()).build(); + } + } + + @ServerExceptionMapper + public RestResponse constraint(ConstraintViolationException ex) { + return exceptionMapper.constraint(ex); + } + + @ServerExceptionMapper + public Response restException(WebApplicationException ex) { + // if client response is bad request remap bad request to DTO + if (ex.getResponse().getStatus() == RestResponse.StatusCode.BAD_REQUEST) { + try { + var clientError = ex.getResponse().readEntity(ProblemDetailResponse.class); + return Response.status(ex.getResponse().getStatus()) + .entity(problemDetailMapper.map(clientError)).build(); + } catch (Exception e) { + // ignore error the bad request has not problem detail response object + return Response.status(ex.getResponse().getStatus()).build(); + } + } else { + return Response.status(ex.getResponse().getStatus()).build(); + } + } +} diff --git a/src/main/java/io/github/onecx/user/profile/bff/rs/controllers/UserProfileRestController.java b/src/main/java/io/github/onecx/user/profile/bff/rs/controllers/UserProfileRestController.java new file mode 100644 index 0000000..e064bce --- /dev/null +++ b/src/main/java/io/github/onecx/user/profile/bff/rs/controllers/UserProfileRestController.java @@ -0,0 +1,152 @@ +package io.github.onecx.user.profile.bff.rs.controllers; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.ConstraintViolationException; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.user.profile.bff.clients.api.UserProfileApi; +import gen.io.github.onecx.user.profile.bff.clients.model.*; +import gen.io.github.onecx.user.profile.bff.rs.internal.UserProfileApiService; +import gen.io.github.onecx.user.profile.bff.rs.internal.model.CreateUserPreferenceDTO; +import gen.io.github.onecx.user.profile.bff.rs.internal.model.ProblemDetailResponseDTO; +import gen.io.github.onecx.user.profile.bff.rs.internal.model.UpdateUserPersonDTO; +import gen.io.github.onecx.user.profile.bff.rs.internal.model.UpdateUserSettingsDTO; +import io.github.onecx.user.profile.bff.rs.mappers.ExceptionMapper; +import io.github.onecx.user.profile.bff.rs.mappers.ProblemDetailMapper; +import io.github.onecx.user.profile.bff.rs.mappers.UserProfileMapper; + +@ApplicationScoped +@Transactional(value = Transactional.TxType.NOT_SUPPORTED) +@LogService +public class UserProfileRestController implements UserProfileApiService { + + @Inject + @RestClient + UserProfileApi client; + + @Inject + UserProfileMapper mapper; + + @Inject + ExceptionMapper exceptionMapper; + + @Inject + ProblemDetailMapper problemDetailMapper; + + @Override + public Response createUserPreference(CreateUserPreferenceDTO createUserPreferenceDTO) { + try (Response response = client.createUserPreference(mapper.map(createUserPreferenceDTO))) { + var preferenceResponse = response.readEntity(UserPreference.class); + return Response.status(response.getStatus()) + .location(response.getLocation()) + .entity(mapper.map(preferenceResponse)).build(); + } + } + + @Override + public Response deleteMyUserProfile() { + try (Response response = client.deleteMyUserProfile()) { + return Response.status(response.getStatus()).build(); + } + } + + @Override + public Response deleteUserPreference(String id) { + try (Response response = client.deleteUserPreference(id)) { + return Response.status(response.getStatus()).build(); + } + } + + @Override + public Response getMyUserProfile() { + try (Response response = client.getMyUserProfile()) { + var profileResponse = response.readEntity(UserProfile.class); + return Response.status(response.getStatus()) + .entity(mapper.map(profileResponse)).build(); + } + } + + @Override + public Response getUserPerson() { + try (Response response = client.getUserPerson()) { + var userPerson = response.readEntity(UserPerson.class); + return Response.status(response.getStatus()) + .entity(mapper.map(userPerson)).build(); + } + } + + @Override + public Response getUserPreference() { + try (Response response = client.getUserPreference()) { + var userPreferences = response.readEntity(UserPreferences.class); + return Response.status(response.getStatus()) + .entity(mapper.map(userPreferences)).build(); + } + } + + @Override + public Response getUserSettings() { + try (Response response = client.getUserSettings()) { + var userProfileAccountSettings = response.readEntity(UserProfileAccountSettings.class); + return Response.status(response.getStatus()) + .entity(mapper.map(userProfileAccountSettings)).build(); + } + } + + @Override + public Response updateUserPerson(UpdateUserPersonDTO updateUserPersonDTO) { + try (Response response = client.updateUserPerson(mapper.map(updateUserPersonDTO))) { + var userPerson = response.readEntity(UserPerson.class); + return Response.status(response.getStatus()) + .entity(mapper.map(userPerson)).build(); + } + } + + @Override + public Response updateUserPreference(String id, String body) { + try (Response response = client.updateUserPreference(id, body)) { + var userPreference = response.readEntity(UserPreference.class); + return Response.status(response.getStatus()) + .entity(mapper.map(userPreference)).build(); + } + } + + @Override + public Response updateUserSettings(UpdateUserSettingsDTO updateUserSettingsDTO) { + try (Response response = client.updateUserSettings(mapper.map(updateUserSettingsDTO))) { + var userProfileAccountSettings = response.readEntity(UserProfileAccountSettings.class); + return Response.status(response.getStatus()) + .entity(mapper.map(userProfileAccountSettings)).build(); + } + } + + @ServerExceptionMapper + public RestResponse constraint(ConstraintViolationException ex) { + return exceptionMapper.constraint(ex); + } + + @ServerExceptionMapper + public Response restException(WebApplicationException ex) { + // if client response is bad request remap bad request to DTO + if (ex.getResponse().getStatus() == RestResponse.StatusCode.BAD_REQUEST) { + try { + var clientError = ex.getResponse().readEntity(ProblemDetailResponse.class); + return Response.status(ex.getResponse().getStatus()) + .entity(problemDetailMapper.map(clientError)).build(); + } catch (Exception e) { + // ignore error the bad request has not problem detail response object + return Response.status(ex.getResponse().getStatus()).build(); + } + } else { + return Response.status(ex.getResponse().getStatus()).build(); + } + } +} diff --git a/src/main/java/io/github/onecx/user/profile/bff/rs/mappers/ExceptionMapper.java b/src/main/java/io/github/onecx/user/profile/bff/rs/mappers/ExceptionMapper.java new file mode 100644 index 0000000..12be4d7 --- /dev/null +++ b/src/main/java/io/github/onecx/user/profile/bff/rs/mappers/ExceptionMapper.java @@ -0,0 +1,60 @@ +package io.github.onecx.user.profile.bff.rs.mappers; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Path; +import jakarta.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.RestResponse; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; + +import gen.io.github.onecx.user.profile.bff.rs.internal.model.ProblemDetailInvalidParamDTO; +import gen.io.github.onecx.user.profile.bff.rs.internal.model.ProblemDetailParamDTO; +import gen.io.github.onecx.user.profile.bff.rs.internal.model.ProblemDetailResponseDTO; + +@Mapper(uses = { OffsetDateTimeMapper.class }) +public interface ExceptionMapper { + + default RestResponse constraint(ConstraintViolationException ex) { + var dto = exception("CONSTRAINT_VIOLATIONS", ex.getMessage()); + dto.setInvalidParams(createErrorValidationResponse(ex.getConstraintViolations())); + return RestResponse.status(Response.Status.BAD_REQUEST, dto); + } + + @Mapping(target = "removeParamsItem", ignore = true) + @Mapping(target = "params", ignore = true) + @Mapping(target = "invalidParams", ignore = true) + @Mapping(target = "removeInvalidParamsItem", ignore = true) + ProblemDetailResponseDTO exception(String errorCode, String detail); + + default List map(Map params) { + if (params == null) { + return List.of(); + } + return params.entrySet().stream().map(e -> { + var item = new ProblemDetailParamDTO(); + item.setKey(e.getKey()); + if (e.getValue() != null) { + item.setValue(e.getValue().toString()); + } + return item; + }).toList(); + } + + List createErrorValidationResponse( + Set> constraintViolation); + + @Mapping(target = "name", source = "propertyPath") + @Mapping(target = "message", source = "message") + ProblemDetailInvalidParamDTO createError(ConstraintViolation constraintViolation); + + default String mapPath(Path path) { + return path.toString(); + } +} diff --git a/src/main/java/io/github/onecx/user/profile/bff/rs/mappers/ProblemDetailMapper.java b/src/main/java/io/github/onecx/user/profile/bff/rs/mappers/ProblemDetailMapper.java new file mode 100644 index 0000000..5e298b5 --- /dev/null +++ b/src/main/java/io/github/onecx/user/profile/bff/rs/mappers/ProblemDetailMapper.java @@ -0,0 +1,17 @@ +package io.github.onecx.user.profile.bff.rs.mappers; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; + +import gen.io.github.onecx.user.profile.bff.clients.model.ProblemDetailResponse; +import gen.io.github.onecx.user.profile.bff.rs.internal.model.ProblemDetailResponseDTO; + +@Mapper(uses = { OffsetDateTimeMapper.class }) +public interface ProblemDetailMapper { + + @Mapping(target = "removeParamsItem", ignore = true) + @Mapping(target = "removeInvalidParamsItem", ignore = true) + ProblemDetailResponseDTO map(ProblemDetailResponse problemDetailResponse); + +} diff --git a/src/main/java/io/github/onecx/user/profile/bff/rs/mappers/UserProfileMapper.java b/src/main/java/io/github/onecx/user/profile/bff/rs/mappers/UserProfileMapper.java new file mode 100644 index 0000000..8acde3f --- /dev/null +++ b/src/main/java/io/github/onecx/user/profile/bff/rs/mappers/UserProfileMapper.java @@ -0,0 +1,38 @@ +package io.github.onecx.user.profile.bff.rs.mappers; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; + +import gen.io.github.onecx.user.profile.bff.clients.model.*; +import gen.io.github.onecx.user.profile.bff.rs.internal.model.*; + +@Mapper(uses = { OffsetDateTimeMapper.class }) +public interface UserProfileMapper { + + UserProfileDTO map(UserProfile userProfile); + + UserPersonCriteria map(UserPersonCriteriaDTO userPersonCriteriaDTO); + + @Mapping(target = "removeStreamItem", ignore = true) + UserProfilePageResultDTO map(UserProfilePageResult userProfilePageResult); + + UpdateUserPersonRequest map(UpdateUserPersonRequestDTO updateUserPersonRequestDTO); + + CreateUserPreference map(CreateUserPreferenceDTO createUserPreferenceDTO); + + UserPreferenceDTO map(UserPreference preferenceResponse); + + UserPersonDTO map(UserPerson userPerson); + + @Mapping(target = "removePreferencesItem", ignore = true) + UserPreferencesDTO map(UserPreferences userPreferences); + + UserProfileAccountSettingsDTO map(UserProfileAccountSettings userProfileAccountSettings); + + UpdateUserPersonRequest map(UpdateUserPersonDTO updateUserPersonDTO); + + UpdateUserSettings map(UpdateUserSettingsDTO updateUserSettingsDTO); + + ImageInfoDTO map(ImageInfo imageInfo); +} diff --git a/src/main/openapi/openapi-bff.yaml b/src/main/openapi/openapi-bff.yaml new file mode 100644 index 0000000..d77bcc9 --- /dev/null +++ b/src/main/openapi/openapi-bff.yaml @@ -0,0 +1,678 @@ +--- +openapi: 3.0.3 +info: + title: onecx-user-profile-bff + description: OneCx user profile Bff + version: "1.0" +servers: + - url: http://onecx-user-profile-bff:8080/ +tags: + - name: userProfile + - name: userProfileAdmin + - name: userAvatar +paths: + /userProfile/me: + get: + tags: + - userProfile + description: Load user profile for current user + operationId: getMyUserProfile + responses: + "200": + description: User profile of current user + content: + application/json: + schema: + $ref: '#/components/schemas/UserProfile' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + delete: + tags: + - userProfile + description: Delete user profile for current user + operationId: deleteMyUserProfile + responses: + "204": + description: OK + "400": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /userProfile/me/preferences: + get: + tags: + - userProfile + description: Load user preferences for current user + operationId: getUserPreference + responses: + "200": + description: User profile of current user + content: + application/json: + schema: + $ref: '#/components/schemas/UserPreferences' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + post: + tags: + - userProfile + description: Create user preference + operationId: createUserPreference + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserPreference' + responses: + "201": + description: OK + headers: + Location: + required: true + schema: + type: string + format: url + content: + application/json: + schema: + $ref: '#/components/schemas/UserPreference' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /userProfile/me/preferences/{id}: + patch: + tags: + - userProfile + description: Update preference value + operationId: updateUserPreference + parameters: + - $ref: '#/components/parameters/id' + requestBody: + required: true + content: + application/json: + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UserPreference' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + "404": + description: User preference not found + delete: + tags: + - userProfile + description: Delete preference by id for current user + operationId: deleteUserPreference + parameters: + - $ref: '#/components/parameters/id' + responses: + "204": + description: OK + "400": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /userProfile/me/person: + get: + tags: + - userProfile + description: Load user person for current user + operationId: getUserPerson + responses: + "200": + description: User person of current user + content: + application/json: + schema: + $ref: '#/components/schemas/UserPerson' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + put: + tags: + - userProfile + description: Update person information + operationId: updateUserPerson + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserPerson' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UserPerson' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /userProfile/me/settings: + get: + tags: + - userProfile + description: Load user profile account settings for current user + operationId: getUserSettings + responses: + "200": + description: User profile account settings of current user + content: + application/json: + schema: + $ref: '#/components/schemas/UserProfileAccountSettings' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + put: + tags: + - userProfile + description: Update user account settings + operationId: updateUserSettings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserSettings' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UserProfileAccountSettings' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /userProfile/me/avatar: + get: + tags: + - userAvatar + description: Get user avatar info + operationId: getUserAvatarInfo + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ImageInfo' + "404": + description: User avatar not found + put: + tags: + - userAvatar + description: Upload user avatar + operationId: uploadAvatar + requestBody: + required: true + content: + 'image/*': + schema: + type: string + format: binary + maxLength: 10 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ImageInfo' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + delete: + tags: + - userAvatar + description: Delete user's avatar + operationId: deleteUserAvatar + responses: + '204': + description: No Content + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /userProfile/me/avatar/{id}: + get: + tags: + - userAvatar + description: Get the image of user's avatar + operationId: getUserAvatar + parameters: + - $ref: '#/components/parameters/id' + responses: + "200": + description: OK + content: + application/octet-stream: + schema: + type: string + format: binary + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + "404": + description: Not found + /userProfiles/search: + post: + tags: + - userProfileAdmin + description: Search for user profiles by search criteria + operationId: searchUserProfile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserPersonCriteria' + responses: + "200": + description: Corresponding user profiles + content: + application/json: + schema: + $ref: '#/components/schemas/UserProfilePageResult' + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /userProfiles/{id}: + get: + tags: + - userProfileAdmin + description: Return user profile by id + operationId: getUserProfile + parameters: + - $ref: '#/components/parameters/id' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UserProfile' + "404": + description: Not found + put: + tags: + - userProfileAdmin + description: Update workspace by ID + operationId: updateUserProfile + parameters: + - $ref: '#/components/parameters/id' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserPersonRequest' + responses: + "204": + description: User profile updated + "400": + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + "404": + description: User profile not found + delete: + tags: + - userProfileAdmin + description: Delete user profile by ID + operationId: deleteUserProfile + parameters: + - $ref: '#/components/parameters/id' + responses: + "204": + description: No Content + "400": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' +components: + schemas: + UserPersonCriteria: + type: object + properties: + userId: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + pageNumber: + format: int32 + description: The number of page. + default: 0 + type: integer + pageSize: + format: int32 + description: The size of page + default: 10 + type: integer + UpdateUserPersonRequest: + type: object + properties: + modificationCount: + format: int32 + type: integer + firstName: + type: string + lastName: + type: string + displayName: + type: string + email: + type: string + address: + $ref: '#/components/schemas/UserPersonAddress' + phone: + $ref: '#/components/schemas/UserPersonPhone' + UserProfilePageResult: + type: object + properties: + totalElements: + format: int64 + description: The total elements in the resource. + type: integer + number: + format: int32 + type: integer + size: + format: int32 + type: integer + totalPages: + format: int64 + type: integer + stream: + type: array + items: + $ref: '#/components/schemas/UserProfile' + CreateUserPreference: + type: object + properties: + applicationId: + type: string + name: + type: string + description: + type: string + value: + type: string + UserPreferences: + type: object + properties: + preferences: + items: + $ref: '#/components/schemas/UserPreference' + UserPreference: + type: object + properties: + id: + type: string + applicationId: + type: string + name: + type: string + description: + type: string + value: + type: string + UserProfile: + type: object + properties: + id: + type: string + modificationCount: + format: int32 + type: integer + creationDate: + $ref: '#/components/schemas/OffsetDateTime' + creationUser: + type: string + modificationDate: + $ref: '#/components/schemas/OffsetDateTime' + modificationUser: + type: string + userId: + type: string + identityProvider: + type: string + identityProviderId: + type: string + organization: + type: string + person: + $ref: '#/components/schemas/UserPerson' + accountSettings: + $ref: '#/components/schemas/UserProfileAccountSettings' + UserPerson: + type: object + properties: + modificationCount: + format: int32 + type: integer + firstName: + type: string + lastName: + type: string + displayName: + type: string + email: + type: string + address: + $ref: '#/components/schemas/UserPersonAddress' + phone: + $ref: '#/components/schemas/UserPersonPhone' + UserPersonAddress: + type: object + properties: + street: + type: string + streetNo: + type: string + city: + type: string + country: + type: string + postalCode: + type: string + UserPersonPhone: + type: object + properties: + number: + type: string + type: + $ref: '#/components/schemas/PhoneType' + PhoneType: + enum: + - MOBILE + - LANDLINE + type: string + UpdateUserPerson: + type: object + properties: + modificationCount: + format: int32 + type: integer + firstName: + type: string + lastName: + type: string + displayName: + type: string + email: + type: string + address: + $ref: '#/components/schemas/UserPersonAddress' + phone: + $ref: '#/components/schemas/UserPersonPhone' + UpdateUserSettings: + type: object + properties: + modificationCount: + format: int32 + type: integer + hideMyProfile: + type: boolean + locale: + type: string + timezone: + type: string + menuMode: + $ref: '#/components/schemas/MenuMode' + colorScheme: + $ref: '#/components/schemas/ColorScheme' + UserProfileAccountSettings: + type: object + properties: + modificationCount: + format: int32 + type: integer + hideMyProfile: + type: boolean + locale: + type: string + timezone: + type: string + menuMode: + $ref: '#/components/schemas/MenuMode' + colorScheme: + $ref: '#/components/schemas/ColorScheme' + MenuMode: + enum: + - STATIC + - HORIZONTAL + - OVERLAY + - SLIM + - SLIMPLUS + type: string + ColorScheme: + enum: + - AUTO + - LIGHT + - DARK + type: string + ImageInfo: + type: object + properties: + modificationCount: + format: int32 + type: integer + creationDate: + $ref: '#/components/schemas/OffsetDateTime' + creationUser: + type: string + modificationDate: + $ref: '#/components/schemas/OffsetDateTime' + modificationUser: + type: string + id: + type: string + userUploaded: + description: flag whether Image was uploaded by user + type: boolean + imageUrl: + type: string + example: http://tkit-portal/data/afcc5d0d-6509-497a-8125-614f82b106ae + smallImageUrl: + type: string + example: http://tkit-portal/data/afcc5d0d-6509-497a-8125-614f82asb106ae + ProblemDetailResponse: + type: object + properties: + errorCode: + type: string + detail: + type: string + params: + type: array + items: + $ref: '#/components/schemas/ProblemDetailParam' + invalidParams: + type: array + items: + $ref: '#/components/schemas/ProblemDetailInvalidParam' + ProblemDetailParam: + type: object + properties: + key: + type: string + value: + type: string + ProblemDetailInvalidParam: + type: object + properties: + name: + type: string + message: + type: string + OffsetDateTime: + format: date-time + type: string + example: 2022-03-10T12:15:50-04:00 + parameters: + id: + in: path + name: id + required: true + schema: + type: string \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..dde30ad --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,40 @@ +# DEFAULT +tkit.log.json.enabled=false +tkit.log.json.pretty-print=false +tkit.log.json.print-details=false + +# propagate the apm-principal-token from requests we receive +org.eclipse.microprofile.rest.client.propagateHeaders=apm-principal-token + +# PROD +%prod.quarkus.rest-client.onecx_user_profile_svc.url=http://onecx-user-profile-svc:8080 + +# DEV + +# BUILD +quarkus.openapi-generator.codegen.input-base-dir=target/tmp/openapi +quarkus.openapi-generator.codegen.spec.onecx_userprofile_internal_openapi_yaml.config-key=onecx_user_profile_svc +quarkus.openapi-generator.codegen.spec.onecx_userprofile_internal_openapi_yaml.base-package=gen.io.github.onecx.user.profile.bff.clients +quarkus.openapi-generator.codegen.spec.onecx_userprofile_internal_openapi_yaml.return-response=true +quarkus.openapi-generator.codegen.spec.onecx_userprofile_internal_openapi_yaml.additional-api-type-annotations=@org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +quarkus.openapi-generator.codegen.spec.onecx_userprofile_v1_openapi_yaml.config-key=onecx_user_profile_svc +quarkus.openapi-generator.codegen.spec.onecx_userprofile_v1_openapi_yaml.base-package=gen.io.github.onecx.user.profile.bff.clients +quarkus.openapi-generator.codegen.spec.onecx_userprofile_v1_openapi_yaml.return-response=true +quarkus.openapi-generator.codegen.spec.onecx_userprofile_v1_openapi_yaml.additional-api-type-annotations=@org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; + +# INTEGRATION TEST +quarkus.test.integration-test-profile=test + +# TEST +%test.quarkus.http.test-port=0 +%test.tkit.rs.context.tenant-id.mock.claim-org-id=orgId +%test.tkit.rs.context.tenant-id.mock.token-header-param=apm-principal-token +%test.tkit.log.json.enabled=false +%test.quarkus.mockserver.devservices.config-class-path=true +%test.quarkus.mockserver.devservices.config-file=/mockserver.properties +%test.quarkus.mockserver.devservices.config-dir=/mockserver +%test.quarkus.mockserver.devservices.log=false +%test.quarkus.mockserver.devservices.reuse=true +%test.quarkus.rest-client.onecx_user_profile_svc.url=${quarkus.mockserver.endpoint} + +# PIPE CONFIG diff --git a/src/test/java/io/github/onecx/user/profile/bff/rs/AbstractTest.java b/src/test/java/io/github/onecx/user/profile/bff/rs/AbstractTest.java new file mode 100644 index 0000000..b9784c5 --- /dev/null +++ b/src/test/java/io/github/onecx/user/profile/bff/rs/AbstractTest.java @@ -0,0 +1,58 @@ +package io.github.onecx.user.profile.bff.rs; + +import java.security.PrivateKey; + +import jakarta.json.Json; +import jakarta.json.JsonObjectBuilder; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.jwt.Claims; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import io.quarkiverse.mockserver.test.MockServerTestResource; +import io.quarkus.test.common.QuarkusTestResource; +import io.restassured.RestAssured; +import io.restassured.config.ObjectMapperConfig; +import io.restassured.config.RestAssuredConfig; +import io.smallrye.jwt.build.Jwt; +import io.smallrye.jwt.util.KeyUtils; + +@QuarkusTestResource(MockServerTestResource.class) +public abstract class AbstractTest { + + protected static final String APM_HEADER_PARAM = ConfigProvider.getConfig() + .getValue("%test.tkit.rs.context.tenant-id.mock.token-header-param", String.class); + + protected static final String CLAIMS_ORG_ID = ConfigProvider.getConfig() + .getValue("%test.tkit.rs.context.tenant-id.mock.claim-org-id", String.class);; + + static { + RestAssured.config = RestAssuredConfig.config().objectMapperConfig( + ObjectMapperConfig.objectMapperConfig().jackson2ObjectMapperFactory( + (cls, charset) -> { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + return objectMapper; + })); + } + + protected static String createToken(String userId, String orgId) { + try { + String userName = userId != null ? userId : "test-user"; + String organizationId = orgId != null ? orgId : "org1"; + JsonObjectBuilder claims = Json.createObjectBuilder(); + claims.add(Claims.preferred_username.name(), userName); + claims.add(Claims.sub.name(), userName); + claims.add(CLAIMS_ORG_ID, organizationId); + PrivateKey privateKey = KeyUtils.generateKeyPair(2048).getPrivate(); + return Jwt.claims(claims.build()).sign(privateKey); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + +} diff --git a/src/test/java/io/github/onecx/user/profile/bff/rs/UserAvatarRestControllerIT.java b/src/test/java/io/github/onecx/user/profile/bff/rs/UserAvatarRestControllerIT.java new file mode 100644 index 0000000..a279af0 --- /dev/null +++ b/src/test/java/io/github/onecx/user/profile/bff/rs/UserAvatarRestControllerIT.java @@ -0,0 +1,7 @@ +package io.github.onecx.user.profile.bff.rs; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class UserAvatarRestControllerIT extends UserAvatarRestControllerTest { +} diff --git a/src/test/java/io/github/onecx/user/profile/bff/rs/UserAvatarRestControllerTest.java b/src/test/java/io/github/onecx/user/profile/bff/rs/UserAvatarRestControllerTest.java new file mode 100644 index 0000000..3aad2d7 --- /dev/null +++ b/src/test/java/io/github/onecx/user/profile/bff/rs/UserAvatarRestControllerTest.java @@ -0,0 +1,301 @@ +package io.github.onecx.user.profile.bff.rs; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.Response.Status.OK; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; +import static org.mockserver.model.NottableString.not; +import static org.mockserver.model.NottableString.string; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.core.Response; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockserver.client.MockServerClient; +import org.mockserver.model.JsonBody; +import org.mockserver.model.MediaType; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.user.profile.bff.clients.model.*; +import gen.io.github.onecx.user.profile.bff.rs.internal.model.*; +import io.github.onecx.user.profile.bff.rs.controllers.UserAvatarRestController; +import io.quarkiverse.mockserver.test.InjectMockServerClient; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@LogService +@TestHTTPEndpoint(UserAvatarRestController.class) +class UserAvatarRestControllerTest extends AbstractTest { + + @InjectMockServerClient + MockServerClient mockServerClient; + + @BeforeEach + void resetMockServer() { + mockServerClient.reset(); + // throw 500 if the apm-principal token is not there + mockServerClient.when(request().withHeader(not(APM_HEADER_PARAM), string(".*"))) + .withPriority(999) + .respond(httpRequest -> response().withStatusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())); + + } + + @Test + void deleteUserAvatarTest() { + given() + .when() + .contentType(APPLICATION_JSON) + .delete() + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + mockServerClient.when(request().withPath("/internal/userProfile/me/avatar") + .withMethod(HttpMethod.DELETE)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.NO_CONTENT.getStatusCode())); + + given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .delete() + .then() + .statusCode(Response.Status.NO_CONTENT.getStatusCode()); + + ProblemDetailResponse problem = new ProblemDetailResponse(); + problem.setErrorCode("MANUAL_ERROR"); + problem.setDetail("Manual detail of error"); + mockServerClient.when(request().withPath("/internal/userProfile/me/avatar") + .withMethod(HttpMethod.DELETE)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .delete() + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + assertThat(error.getDetail()).isEqualTo(problem.getDetail()); + } + + @Test + void getUserAvatarTest() throws IOException { + File avatar = new File("src/test/resources/data/avatar_test.jpg"); + byte[] bytes = Files.readAllBytes(avatar.toPath()); + given() + .when() + .contentType(APPLICATION_JSON) + .get("/avatarId1") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + mockServerClient.when(request().withPath("/internal/userProfile/me/avatar/avatarId1") + .withMethod(HttpMethod.GET)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_OCTET_STREAM) + .withBody(bytes)); + + var avatarByteArray = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .get("/avatarId1") + .then() + .contentType("application/octet-stream") + .statusCode(OK.getStatusCode()) + .extract().asByteArray(); + + assertThat(avatarByteArray).isNotNull().isEqualTo(bytes); + + ProblemDetailResponse problem = new ProblemDetailResponse(); + problem.setErrorCode("MANUAL_ERROR"); + problem.setDetail("Manual detail of error"); + mockServerClient.when(request().withPath("/internal/userProfile/me/avatar/avatarId1") + .withMethod(HttpMethod.GET)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .get("/avatarId1") + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .log().all() + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + assertThat(error.getDetail()).isEqualTo(problem.getDetail()); + + mockServerClient.when(request().withPath("/internal/userProfile/me/avatar/avatarId2") + .withMethod(HttpMethod.GET)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.NOT_FOUND.getStatusCode())); + + given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .get("/avatarId2") + .then() + .statusCode(Response.Status.NOT_FOUND.getStatusCode()); + } + + @Test + void getUserAvatarInfoTest() { + given() + .when() + .contentType(APPLICATION_JSON) + .get() + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + ImageInfo info = new ImageInfo(); + info.setId("avatarId1"); + info.setUserUploaded(Boolean.FALSE); + info.setImageUrl("/image/url/big/avatar"); + info.setSmallImageUrl("/image/url/small/avatar"); + mockServerClient.when(request().withPath("/internal/userProfile/me/avatar") + .withMethod(HttpMethod.GET)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(info))); + + var response = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .get() + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(ImageInfoDTO.class); + + assertThat(response).isNotNull(); + assertThat(response.getImageUrl()).isEqualTo(info.getImageUrl()); + assertThat(response.getSmallImageUrl()).isEqualTo(info.getSmallImageUrl()); + + ProblemDetailResponse problem = new ProblemDetailResponse(); + problem.setErrorCode("MANUAL_ERROR"); + problem.setDetail("Manual detail of error"); + mockServerClient.when(request().withPath("/internal/userProfile/me/avatar") + .withMethod(HttpMethod.GET)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .get() + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + assertThat(error.getDetail()).isEqualTo(problem.getDetail()); + } + + @Test + void uploadAvatarTest() { + File avatar = new File("src/test/resources/data/avatar_test.jpg"); + given() + .when() + .contentType("image/jpg") + .body(avatar) + .put() + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + ImageInfo info = new ImageInfo(); + info.setId("avatarId1"); + info.setUserUploaded(Boolean.FALSE); + info.setImageUrl("/image/url/big/avatar"); + info.setSmallImageUrl("/image/url/small/avatar"); + mockServerClient.when(request().withPath("/internal/userProfile/me/avatar") + .withMethod(HttpMethod.PUT)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(info))); + + var response = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType("image/jpg") + .body(avatar) + .put() + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(ImageInfoDTO.class); + + assertThat(response).isNotNull(); + assertThat(response.getImageUrl()).isEqualTo(info.getImageUrl()); + assertThat(response.getSmallImageUrl()).isEqualTo(info.getSmallImageUrl()); + + ProblemDetailResponse problem = new ProblemDetailResponse(); + problem.setErrorCode("MANUAL_ERROR"); + problem.setDetail("Manual detail of error"); + mockServerClient.when(request().withPath("/internal/userProfile/me/avatar") + .withMethod(HttpMethod.PUT)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType("image/jpg") + .body(avatar) + .put() + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + assertThat(error.getDetail()).isEqualTo(problem.getDetail()); + } + + @Test + void testBadRequestWithoutProblemResponse() { + mockServerClient.when(request().withPath("/internal/userProfile/me/avatar") + .withMethod(HttpMethod.GET)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode())); + + var response = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .get(); + + response.then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()); + + assertThat(response.getBody().prettyPrint()).isEmpty(); + } + +} diff --git a/src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileAdminRestControllerIT.java b/src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileAdminRestControllerIT.java new file mode 100644 index 0000000..6ab52c4 --- /dev/null +++ b/src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileAdminRestControllerIT.java @@ -0,0 +1,7 @@ +package io.github.onecx.user.profile.bff.rs; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class UserProfileAdminRestControllerIT extends UserProfileAdminRestControllerTest { +} diff --git a/src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileAdminRestControllerTest.java b/src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileAdminRestControllerTest.java new file mode 100644 index 0000000..4de2b70 --- /dev/null +++ b/src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileAdminRestControllerTest.java @@ -0,0 +1,339 @@ +package io.github.onecx.user.profile.bff.rs; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; +import static org.mockserver.model.NottableString.not; +import static org.mockserver.model.NottableString.string; + +import java.util.Arrays; +import java.util.List; + +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.core.Response; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockserver.client.MockServerClient; +import org.mockserver.model.JsonBody; +import org.mockserver.model.MediaType; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.user.profile.bff.clients.model.*; +import gen.io.github.onecx.user.profile.bff.rs.internal.model.*; +import io.github.onecx.user.profile.bff.rs.controllers.UserProfileAdminRestController; +import io.quarkiverse.mockserver.test.InjectMockServerClient; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@LogService +@TestHTTPEndpoint(UserProfileAdminRestController.class) +class UserProfileAdminRestControllerTest extends AbstractTest { + + @InjectMockServerClient + MockServerClient mockServerClient; + + @BeforeEach + void resetMockServer() { + mockServerClient.reset(); + // throw 500 if the apm-principal token is not there + mockServerClient.when(request().withHeader(not(APM_HEADER_PARAM), string(".*"))) + .withPriority(999) + .respond(httpRequest -> response().withStatusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())); + + } + + @Test + void deleteUserProfileTest() { + mockServerClient.when(request().withPath("/internal/userProfiles/user1") + .withMethod(HttpMethod.DELETE)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.NO_CONTENT.getStatusCode())); + + // Test with header apm-principal-token + given() + .when() + .pathParam("id", "user1") + .header(APM_HEADER_PARAM, createToken("user1", null)) + .delete("/{id}") + .then() + .statusCode(Response.Status.NO_CONTENT.getStatusCode()); + + // Test without header apm-principal-token + given() + .when() + .pathParam("id", "user1") + .delete("/{id}") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + + @Test + void getUserProfileTest() { + // user2 not found + mockServerClient.when(request().withPath("/internal/userProfiles/user2") + .withMethod(HttpMethod.GET)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.NOT_FOUND.getStatusCode())); + + // user1 exists + UserProfile userProfile = new UserProfile(); + userProfile.setUserId("user1"); + userProfile.setOrganization("capgemini"); + userProfile.setIdentityProvider("database"); + userProfile.setIdentityProviderId("db"); + var person = new UserPerson(); + userProfile.setPerson(person); + person.setDisplayName("Capgemini super user"); + person.setEmail("cap@capgemini.com"); + person.setFirstName("Superuser"); + person.setLastName("Capgeminius"); + mockServerClient.when(request().withPath("/internal/userProfiles/user1") + .withMethod(HttpMethod.GET)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(userProfile))); + + // Test without header apm-principal-token + given() + .when() + .pathParam("id", "user1") + .get("/{id}") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + // found for user1 + var response = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .pathParam("id", "user1") + .get("/{id}") + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(UserProfileDTO.class); + + assertThat(response).isNotNull(); + assertThat(response.getUserId()).isEqualTo(userProfile.getUserId()); + assertThat(response.getPerson().getEmail()).isEqualTo(userProfile.getPerson().getEmail()); + + // not found for user 2 + given() + .when() + .header(APM_HEADER_PARAM, createToken("user2", null)) + .pathParam("id", "user2") + .get("/{id}") + .then() + .statusCode(Response.Status.NOT_FOUND.getStatusCode()); + } + + @Test + void searchUserProfileErrorTest() { + UserPersonCriteria criteria = new UserPersonCriteria(); + ProblemDetailResponse problemDetailResponse = new ProblemDetailResponse(); + problemDetailResponse.setErrorCode("CONSTRAINT_VIOLATIONS"); + problemDetailResponse.setDetail("searchUserProfile.userPersonCriteriaDTO: must not be null"); + mockServerClient.when(request().withPath("/internal/userProfiles/search").withBody(JsonBody.json(criteria))) + .withPriority(999) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problemDetailResponse))); + + // Test without apm + given() + .when() + .contentType(APPLICATION_JSON) + .body(new UserPersonCriteriaDTO()) + .post("/search") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + // without criteria + given() + .when() + .contentType(APPLICATION_JSON) + .header(APM_HEADER_PARAM, createToken("user1", null)) + .post("/search") + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .contentType(APPLICATION_JSON); + + // with empty criteria + var error = given() + .when() + .contentType(APPLICATION_JSON) + .header(APM_HEADER_PARAM, createToken("user1", null)) + .body(new UserPersonCriteriaDTO()) + .post("/search") + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error).isNotNull(); + assertThat(error.getErrorCode()).isEqualTo(problemDetailResponse.getErrorCode()); + + } + + @Test + void searchUserProfileTest() { + UserPersonCriteria criteria = new UserPersonCriteria(); + criteria.setUserId("user1"); + criteria.setEmail("*cap.de"); + UserProfilePageResult result = new UserProfilePageResult(); + result.setSize(2); + UserProfile up1 = new UserProfile(); + up1.setUserId("user1"); + up1.setId("u1"); + UserProfileAccountSettings as1 = new UserProfileAccountSettings(); + as1.setColorScheme(ColorScheme.AUTO); + as1.setMenuMode(MenuMode.SLIMPLUS); + up1.setAccountSettings(as1); + UserPerson person1 = new UserPerson(); + person1.setDisplayName("User 1"); + person1.setEmail("user1@cap.de"); + up1.setPerson(person1); + + UserProfile up2 = new UserProfile(); + up2.setUserId("user1"); + up2.setId("u1"); + UserProfileAccountSettings as2 = new UserProfileAccountSettings(); + as2.setColorScheme(ColorScheme.AUTO); + as2.setMenuMode(MenuMode.SLIMPLUS); + up2.setAccountSettings(as2); + UserPerson person2 = new UserPerson(); + person2.setDisplayName("User 1"); + person2.setEmail("user1@cap.de"); + up2.setPerson(person2); + + List profileList = Arrays.asList(up1, up2); + result.setStream(profileList); + mockServerClient.when(request().withPath("/internal/userProfiles/search").withBody(JsonBody.json(criteria))) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(result))); + UserPersonCriteriaDTO criteriaDTO = new UserPersonCriteriaDTO(); + criteriaDTO.setUserId("user1"); + criteriaDTO.setEmail("*cap.de"); + var response = given() + .when() + .contentType(APPLICATION_JSON) + .header(APM_HEADER_PARAM, createToken("user1", null)) + .body(criteriaDTO) + .post("/search") + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract().as(UserProfilePageResultDTO.class); + + assertThat(response).isNotNull(); + assertThat(response.getSize()).isEqualTo(2); + assertThat(response.getStream().get(1).getPerson().getEmail()).isEqualTo(person2.getEmail()); + } + + @Test + void updateUserProfileTest() { + mockServerClient.when(request().withPath("/internal/userProfiles/user2")) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.NOT_FOUND.getStatusCode())); + mockServerClient.when(request().withPath("/internal/userProfiles/user3")) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode())); + ProblemDetailResponse problemDetailResponse = new ProblemDetailResponse(); + problemDetailResponse.setErrorCode("CONSTRAINT_VIOLATIONS"); + problemDetailResponse.setDetail("searchUserProfile.userPersonCriteriaDTO: must not be null"); + mockServerClient.when(request().withPath("/internal/userProfiles/user4")) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problemDetailResponse))); + mockServerClient.when(request().withPath("/internal/userProfiles/user1")) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.NO_CONTENT.getStatusCode())); + + UpdateUserPersonRequestDTO update = new UpdateUserPersonRequestDTO(); + UserPersonAddressDTO address = new UserPersonAddressDTO(); + address.setStreetNo("10"); + address.setCity("Muenich"); + update.setAddress(address); + update.setDisplayName("User 1"); + update.setEmail("user1@cap.de"); + update.setModificationCount(2); + + // Test without apm + given() + .when() + .contentType(APPLICATION_JSON) + .body(new UserPersonCriteriaDTO()) + .put("/user1") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + given() + .when() + .header(APM_HEADER_PARAM, createToken("user2", null)) + .contentType(APPLICATION_JSON) + .body(update) + .put("/user2") + .then() + .statusCode(Response.Status.NOT_FOUND.getStatusCode()); + + given() + .when() + .header(APM_HEADER_PARAM, createToken("user3", null)) + .contentType(APPLICATION_JSON) + .body(update) + .body(new UserPersonCriteriaDTO()) + .put("/user3") + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user4", null)) + .contentType(APPLICATION_JSON) + .body(update) + .body(new UserPersonCriteriaDTO()) + .put("/user4") + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + assertThat(error.getErrorCode()).isEqualTo(problemDetailResponse.getErrorCode()); + + given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .body(update) + .body(new UserPersonCriteriaDTO()) + .put("/user1") + .then() + .statusCode(Response.Status.NO_CONTENT.getStatusCode()); + + // opt lock test + problemDetailResponse.setErrorCode("OPTIMISTIC_LOCK"); + mockServerClient.when(request().withPath("/internal/userProfiles/user5")) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problemDetailResponse))); + + error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user5", null)) + .contentType(APPLICATION_JSON) + .body(update) + .body(new UserPersonCriteriaDTO()) + .put("/user5") + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + assertThat(error.getErrorCode()).isEqualTo(problemDetailResponse.getErrorCode()); + } +} diff --git a/src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileRestControllerIT.java b/src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileRestControllerIT.java new file mode 100644 index 0000000..5ecaaac --- /dev/null +++ b/src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileRestControllerIT.java @@ -0,0 +1,7 @@ +package io.github.onecx.user.profile.bff.rs; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class UserProfileRestControllerIT extends UserProfileRestControllerTest { +} diff --git a/src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileRestControllerTest.java b/src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileRestControllerTest.java new file mode 100644 index 0000000..8620978 --- /dev/null +++ b/src/test/java/io/github/onecx/user/profile/bff/rs/UserProfileRestControllerTest.java @@ -0,0 +1,735 @@ +package io.github.onecx.user.profile.bff.rs; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; +import static org.mockserver.model.NottableString.not; +import static org.mockserver.model.NottableString.string; + +import java.util.ArrayList; + +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.core.Response; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockserver.client.MockServerClient; +import org.mockserver.model.JsonBody; +import org.mockserver.model.MediaType; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.user.profile.bff.clients.model.*; +import gen.io.github.onecx.user.profile.bff.rs.internal.model.*; +import io.github.onecx.user.profile.bff.rs.controllers.UserProfileRestController; +import io.quarkiverse.mockserver.test.InjectMockServerClient; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@LogService +@TestHTTPEndpoint(UserProfileRestController.class) +class UserProfileRestControllerTest extends AbstractTest { + + @InjectMockServerClient + MockServerClient mockServerClient; + + @BeforeEach + void resetMockServer() { + mockServerClient.reset(); + // throw 500 if the apm-principal token is not there + mockServerClient.when(request().withHeader(not(APM_HEADER_PARAM), string(".*"))) + .withPriority(999) + .respond(httpRequest -> response().withStatusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())); + + } + + @Test + void createUserPreferenceTest() { + CreateUserPreferenceDTO cupDTO = new CreateUserPreferenceDTO(); + cupDTO.setApplicationId("app1"); + cupDTO.setName("name1"); + cupDTO.setValue("value1"); + cupDTO.setDescription("desc1"); + given() + .when() + .contentType(APPLICATION_JSON) + .body(cupDTO) + .post("/preferences") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + UserPreference userPreference = new UserPreference(); + userPreference.setApplicationId(cupDTO.getApplicationId()); + userPreference.setName(cupDTO.getName()); + userPreference.setDescription(cupDTO.getDescription()); + userPreference.setValue(cupDTO.getValue()); + userPreference.setId("id1"); + mockServerClient.when(request().withPath("/internal/userProfile/me/preferences") + .withMethod(HttpMethod.POST)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.CREATED.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(userPreference))); + + var response = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .body(cupDTO) + .post("/preferences") + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.CREATED.getStatusCode()) + .extract().as(UserPreferenceDTO.class); + assertThat(response.getId()).isEqualTo(userPreference.getId()); + + ProblemDetailResponse problem = new ProblemDetailResponse(); + problem.setErrorCode("MANUAL_ERROR"); + problem.setDetail("Manual detail of error"); + mockServerClient.when(request().withPath("/internal/userProfile/me/preferences") + .withMethod(HttpMethod.POST)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .body(cupDTO) + .post("/preferences") + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + assertThat(error.getDetail()).isEqualTo(problem.getDetail()); + } + + @Test + void deleteMyUserProfile() { + given() + .when() + .contentType(APPLICATION_JSON) + .delete() + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + mockServerClient.when(request().withPath("/internal/userProfile/me") + .withMethod(HttpMethod.DELETE)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.NO_CONTENT.getStatusCode())); + + given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .delete() + .then() + .statusCode(Response.Status.NO_CONTENT.getStatusCode()); + + ProblemDetailResponse problem = new ProblemDetailResponse(); + problem.setErrorCode("MANUAL_ERROR"); + problem.setDetail("Manual detail of error"); + mockServerClient.when(request().withPath("/internal/userProfile/me") + .withMethod(HttpMethod.DELETE)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .delete() + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + assertThat(error.getDetail()).isEqualTo(problem.getDetail()); + } + + @Test + void deleteUserPreference() { + given() + .when() + .contentType(APPLICATION_JSON) + .delete("/preferences/pref1") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + mockServerClient.when(request().withPath("/internal/userProfile/me/preferences/pref1") + .withMethod(HttpMethod.DELETE)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.NO_CONTENT.getStatusCode())); + + given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .delete("/preferences/pref1") + .then() + .statusCode(Response.Status.NO_CONTENT.getStatusCode()); + + ProblemDetailResponse problem = new ProblemDetailResponse(); + problem.setErrorCode("MANUAL_ERROR"); + problem.setDetail("Manual detail of error"); + mockServerClient.when(request().withPath("/internal/userProfile/me/preferences/pref1") + .withMethod(HttpMethod.DELETE)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .delete("/preferences/pref1") + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + assertThat(error.getDetail()).isEqualTo(problem.getDetail()); + } + + @Test + void getMyUserProfile() { + given() + .when() + .contentType(APPLICATION_JSON) + .get() + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + UserProfile userProfile = new UserProfile(); + userProfile.setUserId("user1"); + userProfile.setOrganization("capgemini"); + userProfile.setIdentityProvider("database"); + userProfile.setIdentityProviderId("db"); + var person = new UserPerson(); + userProfile.setPerson(person); + person.setDisplayName("Capgemini super user"); + person.setEmail("cap@capgemini.com"); + person.setFirstName("Superuser"); + person.setLastName("Capgeminius"); + mockServerClient.when(request().withPath("/internal/userProfile/me") + .withMethod(HttpMethod.GET)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(userProfile))); + + var response = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .get() + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(UserProfileDTO.class); + + assertThat(response).isNotNull(); + assertThat(response.getUserId()).isEqualTo(userProfile.getUserId()); + assertThat(response.getPerson().getEmail()).isEqualTo(userProfile.getPerson().getEmail()); + + ProblemDetailResponse problem = new ProblemDetailResponse(); + problem.setErrorCode("MANUAL_ERROR"); + problem.setDetail("Manual detail of error"); + mockServerClient.when(request().withPath("/internal/userProfile/me") + .withMethod(HttpMethod.GET)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .get() + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + assertThat(error.getDetail()).isEqualTo(problem.getDetail()); + } + + @Test + void getUserPerson() { + given() + .when() + .contentType(APPLICATION_JSON) + .get("/person") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + var person = new UserPerson(); + person.setDisplayName("Capgemini super user"); + person.setEmail("cap@capgemini.com"); + person.setFirstName("Superuser"); + person.setLastName("Capgeminius"); + UserPersonAddress addresss = new UserPersonAddress(); + addresss.setStreet("Obergasse"); + person.setAddress(addresss); + mockServerClient.when(request().withPath("/internal/userProfile/me/person") + .withMethod(HttpMethod.GET)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(person))); + + var response = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .get("/person") + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(UserPersonDTO.class); + + assertThat(response).isNotNull(); + assertThat(response.getEmail()).isEqualTo(person.getEmail()); + assertThat(response.getAddress().getStreet()).isEqualTo(person.getAddress().getStreet()); + + ProblemDetailResponse problem = new ProblemDetailResponse(); + problem.setErrorCode("MANUAL_ERROR"); + problem.setDetail("Manual detail of error"); + mockServerClient.when(request().withPath("/internal/userProfile/me/person") + .withMethod(HttpMethod.GET)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .get("/person") + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + assertThat(error.getDetail()).isEqualTo(problem.getDetail()); + } + + @Test + void getUserPreference() { + given() + .when() + .contentType(APPLICATION_JSON) + .get("/preferences") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + var preferences = new UserPreferences(); + preferences.setPreferences(new ArrayList<>()); + UserPreference preference1 = new UserPreference(); + preference1.setId("1"); + preference1.setName("name1"); + preference1.setDescription("desc1"); + preference1.setApplicationId("app1"); + preference1.setValue("value1"); + preferences.getPreferences().add(preference1); + UserPreference preference2 = new UserPreference(); + preference2.setId("2"); + preference2.setName("name2"); + preference2.setDescription("desc2"); + preference2.setApplicationId("app2"); + preference2.setValue("value2"); + preferences.getPreferences().add(preference2); + mockServerClient.when(request().withPath("/internal/userProfile/me/preferences") + .withMethod(HttpMethod.GET)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(preferences))); + + var response = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .get("/preferences") + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(UserPreferencesDTO.class); + + assertThat(response.getPreferences()).hasSize(2); + + ProblemDetailResponse problem = new ProblemDetailResponse(); + problem.setErrorCode("MANUAL_ERROR"); + problem.setDetail("Manual detail of error"); + mockServerClient.when(request().withPath("/internal/userProfile/me/preferences") + .withMethod(HttpMethod.GET)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .get("/preferences") + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + assertThat(error.getDetail()).isEqualTo(problem.getDetail()); + } + + @Test + void getUserSettings() { + given() + .when() + .contentType(APPLICATION_JSON) + .get("/settings") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + var settings = new UserProfileAccountSettings(); + settings.setMenuMode(MenuMode.STATIC); + settings.setHideMyProfile(false); + settings.setColorScheme(ColorScheme.DARK); + settings.setTimezone("get/muenich"); + + mockServerClient.when(request().withPath("/internal/userProfile/me/settings") + .withMethod(HttpMethod.GET)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(settings))); + + var response = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .get("/settings") + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(UserProfileAccountSettingsDTO.class); + + assertThat(response).isNotNull(); + assertThat(response.getHideMyProfile()).isEqualTo(settings.getHideMyProfile()); + assertThat(response.getMenuMode()).hasToString(settings.getMenuMode().toString()); + + ProblemDetailResponse problem = new ProblemDetailResponse(); + problem.setErrorCode("MANUAL_ERROR"); + problem.setDetail("Manual detail of error"); + mockServerClient.when(request().withPath("/internal/userProfile/me/settings") + .withMethod(HttpMethod.GET)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .get("/settings") + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + assertThat(error.getDetail()).isEqualTo(problem.getDetail()); + } + + @Test + void updateUserPerson() { + var update = new UpdateUserPersonRequestDTO(); + update.setEmail("test@email.de"); + + given() + .when() + .contentType(APPLICATION_JSON) + .body(update) + .put("/person") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + var person = new UserPerson(); + person.setDisplayName("Capgemini super user"); + person.setEmail("test@email.de"); + person.setFirstName("Superuser"); + person.setLastName("Capgeminius"); + UserPersonAddress addresss = new UserPersonAddress(); + addresss.setStreet("Obergasse"); + person.setAddress(addresss); + mockServerClient.when(request().withPath("/internal/userProfile/me/person") + .withMethod(HttpMethod.PUT)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(person))); + + var response = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .body(update) + .contentType(APPLICATION_JSON) + .put("/person") + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(UserPersonDTO.class); + + assertThat(response).isNotNull(); + assertThat(response.getEmail()).isEqualTo(person.getEmail()); + + ProblemDetailResponse problem = new ProblemDetailResponse(); + problem.setErrorCode("MANUAL_ERROR"); + problem.setDetail("Manual detail of error"); + mockServerClient.when(request().withPath("/internal/userProfile/me/person") + .withMethod(HttpMethod.PUT)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .body(update) + .put("/person") + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + assertThat(error.getDetail()).isEqualTo(problem.getDetail()); + + problem.setErrorCode("OPTIMISTIC_LOCK"); + mockServerClient.when(request().withPath("/internal/userProfile/me/person") + .withMethod(HttpMethod.PUT)) + .withPriority(300) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .body(update) + .put("/person") + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + } + + @Test + void updateUserPreference() { + var update = "newValue1"; + given() + .when() + .contentType(APPLICATION_JSON) + .body(update) + .patch("/preferences/pref1") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + UserPreference preference1 = new UserPreference(); + preference1.setId("1"); + preference1.setName("name1"); + preference1.setDescription("desc1"); + preference1.setApplicationId("app1"); + preference1.setValue(update); + mockServerClient.when(request().withPath("/internal/userProfile/me/preferences/pref1") + .withMethod(HttpMethod.PATCH)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(preference1))); + + var response = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .body(update) + .patch("/preferences/pref1") + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(UserPreferenceDTO.class); + + assertThat(response.getValue()).isEqualTo(update); + + mockServerClient.when(request().withPath("/internal/userProfile/me/preferences/pref2") + .withMethod(HttpMethod.PATCH)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.NOT_FOUND.getStatusCode())); + given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .body(update) + .patch("/preferences/pref2") + .then() + .statusCode(Response.Status.NOT_FOUND.getStatusCode()); + + ProblemDetailResponse problem = new ProblemDetailResponse(); + problem.setErrorCode("MANUAL_ERROR"); + problem.setDetail("Manual detail of error"); + mockServerClient.when(request().withPath("/internal/userProfile/me/preferences/pref1") + .withMethod(HttpMethod.PATCH)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .body(update) + .patch("/preferences/pref1") + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + assertThat(error.getDetail()).isEqualTo(problem.getDetail()); + } + + @Test + void updateUserSettings() { + var update = new UpdateUserSettingsDTO(); + update.setColorScheme(ColorSchemeDTO.LIGHT); + given() + .when() + .contentType(APPLICATION_JSON) + .body(update) + .get("/settings") + .then() + .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + + var settings = new UserProfileAccountSettings(); + settings.setMenuMode(MenuMode.STATIC); + settings.setHideMyProfile(false); + settings.setColorScheme(ColorScheme.LIGHT); + settings.setTimezone("get/muenich"); + + mockServerClient.when(request().withPath("/internal/userProfile/me/settings") + .withMethod(HttpMethod.PUT)) + .withPriority(100) + .respond(httpRequest -> response().withStatusCode(Response.Status.OK.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(settings))); + + var response = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .body(update) + .put("/settings") + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(UserProfileAccountSettingsDTO.class); + + assertThat(response).isNotNull(); + assertThat(response.getHideMyProfile()).isEqualTo(settings.getHideMyProfile()); + assertThat(response.getMenuMode()).hasToString(settings.getMenuMode().toString()); + + ProblemDetailResponse problem = new ProblemDetailResponse(); + problem.setErrorCode("MANUAL_ERROR"); + problem.setDetail("Manual detail of error"); + mockServerClient.when(request().withPath("/internal/userProfile/me/settings") + .withMethod(HttpMethod.PUT)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .body(update) + .put("/settings") + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + assertThat(error.getDetail()).isEqualTo(problem.getDetail()); + + problem.setErrorCode("OPTIMISTIC_LOCK"); + mockServerClient.when(request().withPath("/internal/userProfile/me/settings") + .withMethod(HttpMethod.PUT)) + .withPriority(300) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withContentType(MediaType.APPLICATION_JSON) + .withBody(JsonBody.json(problem))); + + error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .body(update) + .put("/settings") + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error.getErrorCode()).isEqualTo(problem.getErrorCode()); + + } + + @Test + void testConstraintViolationException() { + var error = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .put("/settings") + .then() + .contentType(APPLICATION_JSON) + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(error).isNotNull(); + assertThat(error.getErrorCode()).isEqualTo("CONSTRAINT_VIOLATIONS"); + } + + @Test + void testBadRequestWithoutProblemResponse() { + var update = new UpdateUserSettingsDTO(); + mockServerClient.when(request().withPath("/internal/userProfile/me/settings") + .withMethod(HttpMethod.PUT)) + .withPriority(200) + .respond(httpRequest -> response().withStatusCode(Response.Status.BAD_REQUEST.getStatusCode())); + + var response = given() + .when() + .header(APM_HEADER_PARAM, createToken("user1", null)) + .contentType(APPLICATION_JSON) + .body(update) + .put("/settings"); + + response.then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()); + + assertThat(response.getBody().prettyPrint()).isEmpty(); + + } +} diff --git a/src/test/resources/data/avatar_test.jpg b/src/test/resources/data/avatar_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2714c0adb1648c0f36db4667ffe94ac413e4c1b9 GIT binary patch literal 17636 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 II?~wx0Slwbr~m)} literal 0 HcmV?d00001 diff --git a/src/test/resources/mockserver.properties b/src/test/resources/mockserver.properties new file mode 100644 index 0000000..7f9d719 --- /dev/null +++ b/src/test/resources/mockserver.properties @@ -0,0 +1,15 @@ +mockserver.initializationJsonPath=/mockserver/*.json +# watch changes in the file +mockserver.watchInitializationJson=true + +# Certificate Generation +# dynamically generated CA key pair (if they don't already exist in specified directory) +mockserver.dynamicallyCreateCertificateAuthorityCertificate=true +# save dynamically generated CA key pair in working directory +mockserver.directoryToSaveDynamicSSLCertificate=. +# certificate domain name (default "localhost") +mockserver.sslCertificateDomainName=localhost +# comma separated list of ip addresses for Subject Alternative Name domain names (default empty list) +mockserver.sslSubjectAlternativeNameDomains=www.example.com,www.another.com +# comma separated list of ip addresses for Subject Alternative Name ips (default empty list) +mockserver.sslSubjectAlternativeNameIps=127.0.0.1 \ No newline at end of file diff --git a/src/test/resources/mockserver/internal.json b/src/test/resources/mockserver/internal.json new file mode 100644 index 0000000..8500701 --- /dev/null +++ b/src/test/resources/mockserver/internal.json @@ -0,0 +1,21 @@ +[ + { + "id": "1", + "httpRequest": { + "path": "/internal/themes" + }, + "httpResponse": { + "body": { + "type": "JSON", + "json": { + "totalElements": 1, + "number": 1, + "size": 1, + "totalPages": 1, + "stream": ["app1","app2"] + }, + "contentType": "application/json" + } + } + } +] \ No newline at end of file