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 0000000..2714c0a Binary files /dev/null and b/src/test/resources/data/avatar_test.jpg differ 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