From ed7f19a99a9d1eab2d2582015f5759199e9bae34 Mon Sep 17 00:00:00 2001 From: DanieleRanaldo <124155243+DanieleRanaldo@users.noreply.github.com> Date: Wed, 16 Oct 2024 10:48:51 +0200 Subject: [PATCH 01/18] feat: add emd citizen implementation (#1) Co-authored-by: Vitolo-Andrea --- .devops/code-review-pipelines.yml | 79 ++++++ .devops/deploy-pipelines.yml | 192 +++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 28 ++ .github/workflows/code-review.yml | 61 +++++ .github/workflows/pr-title.yml | 56 ++++ .github/workflows/release.yml | 25 ++ .github/workflows/snapshot-docker.yml | 23 ++ .github/workflows/trivy.yml | 52 ++++ .gitignore | 33 +++ .mvn/wrapper/maven-wrapper.properties | 19 ++ mvnw | 259 ++++++++++++++++++ mvnw.cmd | 149 ++++++++++ pom.xml | 125 +++++++++ .../CustomReactiveMongoHealthIndicator.java | 28 ++ .../common/configuration/MongoConfig.java | 104 +++++++ .../configuration/MongoHealthConfig.java | 14 + .../pagopa/common/utils/CommonConstants.java | 16 ++ .../it/gov/pagopa/common/utils/Utils.java | 38 +++ .../gov/pagopa/common/web/dto/ErrorDTO.java | 22 ++ .../common/web/exception/ClientException.java | 25 ++ .../web/exception/ClientExceptionNoBody.java | 20 ++ .../exception/ClientExceptionWithBody.java | 23 ++ .../web/exception/EmdEncryptionException.java | 14 + .../common/web/exception/ErrorManager.java | 77 ++++++ .../web/exception/ServiceException.java | 25 ++ .../exception/ServiceExceptionHandler.java | 56 ++++ .../exception/ServiceExceptionPayload.java | 7 + .../exception/ValidationExceptionHandler.java | 75 +++++ .../pagopa/onboarding/citizen/EmdCitizen.java | 13 + .../citizen/configuration/ExceptionMap.java | 37 +++ .../constants/OnboardingCitizenConstants.java | 27 ++ .../citizen/controller/CitizenController.java | 56 ++++ .../controller/CitizenControllerImpl.java | 53 ++++ .../citizen/dto/CitizenConsentDTO.java | 23 ++ .../CitizenConsentObjectToDTOMapper.java | 19 ++ .../citizen/model/CitizenConsent.java | 24 ++ .../CitizenConsentDTOToObjectMapper.java | 19 ++ .../citizen/repository/CitizenRepository.java | 16 ++ .../citizen/service/CitizenService.java | 15 + .../citizen/service/CitizenServiceImpl.java | 103 +++++++ src/main/resources/application.yml | 42 +++ .../CustomMongoHealthIndicatorTest.java | 50 ++++ .../pagopa/common/utils/MemoryAppender.java | 27 ++ .../it/gov/pagopa/common/utils/UtilsTest.java | 41 +++ .../web/exception/ErrorManagerTest.java | 195 +++++++++++++ .../ServiceExceptionHandlerTest.java | 104 +++++++ .../ValidationExceptionHandlerTest.java | 90 ++++++ .../controller/CitizenControllerTest.java | 136 +++++++++ .../citizen/faker/CitizenConsentDTOFaker.java | 22 ++ .../citizen/faker/CitizenConsentFaker.java | 21 ++ .../citizen/service/CitizenServiceTest.java | 159 +++++++++++ 51 files changed, 2937 insertions(+) create mode 100644 .devops/code-review-pipelines.yml create mode 100644 .devops/deploy-pipelines.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/code-review.yml create mode 100644 .github/workflows/pr-title.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/snapshot-docker.yml create mode 100644 .github/workflows/trivy.yml create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 src/main/java/it/gov/pagopa/common/configuration/CustomReactiveMongoHealthIndicator.java create mode 100644 src/main/java/it/gov/pagopa/common/configuration/MongoConfig.java create mode 100644 src/main/java/it/gov/pagopa/common/configuration/MongoHealthConfig.java create mode 100644 src/main/java/it/gov/pagopa/common/utils/CommonConstants.java create mode 100644 src/main/java/it/gov/pagopa/common/utils/Utils.java create mode 100644 src/main/java/it/gov/pagopa/common/web/dto/ErrorDTO.java create mode 100644 src/main/java/it/gov/pagopa/common/web/exception/ClientException.java create mode 100644 src/main/java/it/gov/pagopa/common/web/exception/ClientExceptionNoBody.java create mode 100644 src/main/java/it/gov/pagopa/common/web/exception/ClientExceptionWithBody.java create mode 100644 src/main/java/it/gov/pagopa/common/web/exception/EmdEncryptionException.java create mode 100644 src/main/java/it/gov/pagopa/common/web/exception/ErrorManager.java create mode 100644 src/main/java/it/gov/pagopa/common/web/exception/ServiceException.java create mode 100644 src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandler.java create mode 100644 src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionPayload.java create mode 100644 src/main/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandler.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/EmdCitizen.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/configuration/ExceptionMap.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/constants/OnboardingCitizenConstants.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenController.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerImpl.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentDTO.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/model/CitizenConsent.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/model/mapper/CitizenConsentDTOToObjectMapper.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenRepository.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenService.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java create mode 100644 src/main/resources/application.yml create mode 100644 src/test/java/it/gov/pagopa/common/configuration/CustomMongoHealthIndicatorTest.java create mode 100644 src/test/java/it/gov/pagopa/common/utils/MemoryAppender.java create mode 100644 src/test/java/it/gov/pagopa/common/utils/UtilsTest.java create mode 100644 src/test/java/it/gov/pagopa/common/web/exception/ErrorManagerTest.java create mode 100644 src/test/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandlerTest.java create mode 100644 src/test/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandlerTest.java create mode 100644 src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java create mode 100644 src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentDTOFaker.java create mode 100644 src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentFaker.java create mode 100644 src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java diff --git a/.devops/code-review-pipelines.yml b/.devops/code-review-pipelines.yml new file mode 100644 index 0000000..bb04ac4 --- /dev/null +++ b/.devops/code-review-pipelines.yml @@ -0,0 +1,79 @@ +# Build your Java project and run tests with Apache Maven. +# Add steps that analyze code, save build artifacts, deploy, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/java + +trigger: + - main + +pool: + #vmImage: 'ubuntu-latest' + vmImage: ubuntu-22.04 + +#variables: +# MAVEN_CACHE_FOLDER: $(Pipeline.Workspace)/.m2/repository +# MAVEN_OPTS: '-Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)' + +steps: + # - task: Cache@2 + # inputs: + # key: 'maven | "$(Agent.OS)" | pom.xml' + # restoreKeys: | + # maven | "$(Agent.OS)" + # maven + # path: $(MAVEN_CACHE_FOLDER) + # displayName: Cache Maven local repo + + - task: SonarCloudPrepare@1 + displayName: 'Prepare SonarCloud analysis configuration' + inputs: + SonarCloud: '$(SONARCLOUD_SERVICE_CONN)' + organization: '$(SONARCLOUD_ORG)' + scannerMode: Other + extraProperties: | + sonar.projectKey=$(SONARCLOUD_PROJECT_KEY) + sonar.projectName=$(SONARCLOUD_PROJECT_NAME) + sonar.exclusions='**/enums/**, **/model/**, **/stub/**, **/dto/**, **/*Constant*, **/*Config.java, **/*Scheduler.java, **/*Application.java, **/src/test/**, **/Dummy*.java' + + # - task: DownloadSecureFile@1 + # displayName: 'download settings.xml for Maven' + # name: settingsxml + # inputs: + # secureFile: '$(SETTINGS_XML_RO_SECURE_FILE_NAME)' + # retryCount: '2' + + # options: '-B -s $(settingsxml.secureFilePath)' + - task: Maven@3 + inputs: + mavenPomFile: 'pom.xml' + goals: 'clean org.jacoco:jacoco-maven-plugin:0.8.8:prepare-agent verify org.jacoco:jacoco-maven-plugin:0.8.8:report org.jacoco:jacoco-maven-plugin:0.8.8:report-aggregate ' + options: '-B' + publishJUnitResults: true + testResultsFiles: '**/surefire-reports/TEST-*.xml' + javaHomeOption: 'JDKVersion' + jdkVersionOption: '1.17' + mavenVersionOption: 'Default' + mavenAuthenticateFeed: false + effectivePomSkip: false + sonarQubeRunAnalysis: false + - bash: xmlReportPaths=$(find "$(pwd)" -path '*jacoco.xml' | sed 's/.*/&/' | tr '\n' ','); echo "##vso[task.setvariable variable=xmlReportPaths]$xmlReportPaths" + displayName: finding jacoco.xml + + # options: '-B -s $(settingsxml.secureFilePath) -Dsonar.coverage.jacoco.xmlReportPaths=$(xmlReportPaths)' + + - task: Maven@3 + inputs: + mavenPomFile: 'pom.xml' + goals: 'sonar:sonar' + options: '-B -Dsonar.coverage.jacoco.xmlReportPaths=$(xmlReportPaths)' + publishJUnitResults: false + javaHomeOption: 'JDKVersion' + jdkVersionOption: '1.17' + mavenVersionOption: 'Default' + mavenAuthenticateFeed: false + effectivePomSkip: false + sonarQubeRunAnalysis: true + isJacocoCoverageReportXML: false + sqMavenPluginVersionChoice: 'latest' + - task: SonarCloudPublish@1 + inputs: + pollingTimeoutSec: '300' \ No newline at end of file diff --git a/.devops/deploy-pipelines.yml b/.devops/deploy-pipelines.yml new file mode 100644 index 0000000..d27c14c --- /dev/null +++ b/.devops/deploy-pipelines.yml @@ -0,0 +1,192 @@ +# Build and push image to Azure Container Registry; Deploy to Azure Kubernetes Service +# https://docs.microsoft.com/azure/devops/pipelines/languages/docker + +parameters: + - name: 'executeBuild' + displayName: 'Launch maven and docker build' + type: boolean + default: true + +trigger: + branches: + include: + - release-* + - main + paths: + include: + - src/* + - helm/* + - pom.xml + - Dockerfile + +pr: none + +resources: + - repo: self + +variables: + + # vmImageNameDefault: 'ubuntu-latest' + vmImageNameDefault: ubuntu-22.04 + + imageRepository: '$(K8S_IMAGE_REPOSITORY_NAME)' + deployNamespace: '$(DEPLOY_NAMESPACE)' + helmReleaseName : '$(HELM_RELEASE_NAME)' + settingsXmlROsecureFileName: '$(SETTINGS_XML_RO_SECURE_FILE_NAME)' + settingsXmlSecureFileName: '$(SETTINGS_XML_RO_SECURE_FILE_NAME)' + canDeploy: true + + # If the branch is develop or a feature branch starting with CEN, deploy in DEV environment + ${{ if startsWith(variables['Build.SourceBranch'], 'refs/heads/release-dev') }}: + environment: 'DEV' + dockerRegistryServiceConnection: '$(DEV_CONTAINER_REGISTRY_SERVICE_CONN)' + kubernetesServiceConnection: '$(DEV_KUBERNETES_SERVICE_CONN)' + containerRegistry: '$(DEV_CONTAINER_REGISTRY_NAME)' + selfHostedAgentPool: $(DEV_AGENT_POOL) + + ${{ elseif startsWith(variables['Build.SourceBranch'], 'refs/heads/release-uat') }}: + environment: 'UAT' + dockerRegistryServiceConnection: '$(UAT_CONTAINER_REGISTRY_SERVICE_CONN)' + kubernetesServiceConnection: '$(UAT_KUBERNETES_SERVICE_CONN)' + containerRegistry: '$(UAT_CONTAINER_REGISTRY_NAME)' + selfHostedAgentPool: $(UAT_AGENT_POOL) + + ${{ elseif or(eq(variables['Build.SourceBranch'], 'refs/heads/main'),eq(variables['Build.SourceBranch'], 'refs/heads/release-prod')) }}: + environment: 'PROD' + dockerRegistryServiceConnection: '$(PROD_CONTAINER_REGISTRY_SERVICE_CONN)' + kubernetesServiceConnection: '$(PROD_KUBERNETES_SERVICE_CONN)' + containerRegistry: '$(PROD_CONTAINER_REGISTRY_NAME)' + selfHostedAgentPool: $(PROD_AGENT_POOL) + + ${{ else }}: + environment: 'DEV' + dockerRegistryServiceConnection: '$(DEV_CONTAINER_REGISTRY_SERVICE_CONN)' + kubernetesServiceConnection: '$(DEV_KUBERNETES_SERVICE_CONN)' + containerRegistry: '$(DEV_CONTAINER_REGISTRY_NAME)' + selfHostedAgentPool: $(DEV_AGENT_POOL) + +stages: + - stage: 'pom_version' + displayName: Release + condition: eq(variables.canDeploy, true) + jobs: + - job: POM + displayName: POM + pool: + vmImage: $(vmImageNameDefault) + steps: + - task: Bash@3 + displayName: Get POM version + name: getpomversion + condition: and(succeeded(), eq(variables.canDeploy, true)) + inputs: + targetType: 'inline' + script: | + version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "##vso[task.setvariable variable=outputpomversion;isOutput=true]$version" + failOnStderr: true + + - stage: 'build' + displayName: 'Build_and_Publish_to_${{ variables.environment }}' + dependsOn: 'pom_version' + variables: + pomversion: $[ stageDependencies.Release.POM.outputs['getpomversion.outputpomversion'] ] + jobs: + - job: Build + displayName: Build + pool: + vmImage: $(vmImageNameDefault) + steps: + - task: Docker@2 + condition: and(succeeded(), ${{ parameters.executeBuild }}) + displayName: 'Publish_image_to_${{ variables.environment }}' + inputs: + containerRegistry: '$(dockerRegistryServiceConnection)' + repository: '$(imageRepository)' + command: 'buildAndPush' + tags: | + $(Build.BuildId) + latest + $(pomversion) + # - task: PublishPipelineArtifact@1 + # displayName: 'Publish Artifact manifests' + # condition: and(succeeded(), eq(variables.canDeploy, true)) + # inputs: + # targetPath: '$(Build.Repository.LocalPath)/manifests' + # artifact: 'manifests' + # publishLocation: 'pipeline' + + - stage: 'publish_artifact_helm' + displayName: 'Publish_artifact_Helm' + dependsOn: ['build'] + jobs: + - job: Publish_artifact_helm + displayName: Publish_artifact_helm + pool: + vmImage: $(vmImageNameDefault) + steps: + - task: PublishPipelineArtifact@1 + displayName: 'Publish Artifact manifests' + condition: succeeded() + inputs: + targetPath: '$(Build.Repository.LocalPath)/helm' + artifact: 'helm' + publishLocation: 'pipeline' + + - stage: 'deploy' + displayName: 'Deploy to ${{ variables.environment }} K8S' + dependsOn: ['publish_artifact_helm'] + condition: and(succeeded(), eq(variables.canDeploy, true)) + variables: + pomversion: $[ stageDependencies.Release.POM.outputs['getpomversion.outputpomversion'] ] + jobs: + - deployment: 'Deploy_to_${{ variables.environment }}' + displayName: 'Deploy to ${{ variables.environment }} K8S' + pool: + name: $(selfHostedAgentPool) + environment: '$(environment)' + strategy: + runOnce: + deploy: + steps: + - download: none + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'current' + artifactName: 'helm' + targetPath: '$(Pipeline.Workspace)/helm' + - task: KubectlInstaller@0 + - task: Bash@3 + name: helm_dependency_build + displayName: Helm dependency build + inputs: + workingDirectory: '$(Pipeline.Workspace)/helm' + targetType: 'inline' + script: | + helm repo add pagopa-microservice https://pagopa.github.io/aks-microservice-chart-blueprint + helm dep build + failOnStderr: true + - task: HelmDeploy@0 + displayName: Helm upgrade + inputs: + kubernetesServiceEndpoint: ${{ variables.kubernetesServiceConnection }} + namespace: '$(deployNamespace)' + command: upgrade + chartType: filepath + chartPath: $(Pipeline.Workspace)/helm + chartName: ${{ variables.helmReleaseName }} + releaseName: ${{ variables.helmReleaseName }} + valueFile: "$(Pipeline.Workspace)/helm/values-${{ lower(variables.environment) }}.yaml" + install: true + waitForExecution: true + arguments: "--timeout 5m0s --debug" + - task: KubernetesManifest@0 + displayName: Patch + inputs: + kubernetesServiceConnection: ${{ variables.kubernetesServiceConnection }} + namespace: '$(deployNamespace)' + action: patch + kind: deployment + name: '$(helmReleaseName)-microservice-chart' + mergeStrategy: strategic + patch: '{"spec":{"template":{"metadata":{"annotations":{"buildNumber":"$(Build.BuildNumber)"}}}}}' \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..70a11e8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,28 @@ + + + + + +### List of changes + + + +### Motivation and context + + + +### Type of changes + +- [ ] Add new feature +- [ ] Update existing feature +- [ ] Remove existing feature +- [ ] Other changes + +### Does this introduce a breaking change? + +- [ ] Yes +- [ ] No + +### Other information + + diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml new file mode 100644 index 0000000..1b9b9b7 --- /dev/null +++ b/.github/workflows/code-review.yml @@ -0,0 +1,61 @@ +name: SonarCloud Analysis + +on: + push: + branches: + - main + - release-* + pull_request: + types: + - opened + - edited + - synchronize + +jobs: + sonarcloud: + name: SonarCloud Analysis + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4.1.7 + with: + fetch-depth: 0 # Fetch all history for all branches and tags + + - name: Set up JDK 17 + uses: actions/setup-java@v4.2.1 + with: + distribution: 'adopt' + java-version: '17' + + - name: Cache Maven packages + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Build and test with Maven + run: mvn clean org.jacoco:jacoco-maven-plugin:0.8.8:prepare-agent verify org.jacoco:jacoco-maven-plugin:0.8.8:report org.jacoco:jacoco-maven-plugin:0.8.8:report-aggregate -B + + - name: Generate JaCoCo XML Report + run: mvn org.jacoco:jacoco-maven-plugin:0.8.8:report -Djacoco.reportFormat=xml -B + + - name: SonarCloud Scan + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mvn sonar:sonar \ + -Dsonar.projectKey=${{ vars.SONARCLOUD_PROJECT_KEY }} \ + -Dsonar.organization=${{ vars.SONARCLOUD_ORG }} \ + -Dsonar.host.url=https://sonarcloud.io \ + -Dsonar.token=${{ secrets.SONAR_TOKEN }} \ + -Dsonar.java.binaries=target/classes \ + -Dsonar.junit.reportPaths=target/surefire-reports \ + -Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml \ + -Dsonar.exclusions=**/configuration/**,**/enums/**,**/model/**,**/stub/**,**/dto/**,**/*Constant*,**/*Config.java,**/*Scheduler.java,**/*Application.java,**/src/test/**,**/Dummy*.java + + - name: Fetch all branches + run: git fetch --all diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..df61c59 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,56 @@ +name: "Validate PR title" + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-22.04 + steps: + # Please look up the latest version from + # https://github.com/amannn/action-semantic-pull-request/releases + # from https://github.com/amannn/action-semantic-pull-request/commits/main + - uses: amannn/action-semantic-pull-request@01d5fd8a8ebb9aafe902c40c53f0f4744f7381eb + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + # Configure which types are allowed. + # Default: https://github.com/commitizen/conventional-commit-types + types: | + fix + feat + docs + chore + breaking + # Configure that a scope must always be provided. + requireScope: false + # Configure additional validation for the subject based on a regex. + # This example ensures the subject starts with an uppercase character. + subjectPattern: ^.+$ + # If `subjectPattern` is configured, you can use this property to override + # the default error message that is shown when the pattern doesn't match. + # The variables `subject` and `title` can be used within the message. + subjectPatternError: | + The subject "{subject}" found in the pull request title "{title}" + didn't match the configured pattern. Please ensure that the subject + starts with an uppercase character. + # For work-in-progress PRs you can typically use draft pull requests + # from Github. However, private repositories on the free plan don't have + # this option and therefore this action allows you to opt-in to using the + # special "[WIP]" prefix to indicate this state. This will avoid the + # validation of the PR title and the pull request checks remain pending. + # Note that a second check will be reported if this is enabled. + wip: true + # When using "Squash and merge" on a PR with only one commit, GitHub + # will suggest using that commit message instead of the PR title for the + # merge commit, and it's easy to commit this by mistake. Enable this option + # to also validate the commit message for one commit PRs. + validateSingleCommit: false + # Related to `validateSingleCommit` you can opt-in to validate that the PR + # title matches a single commit to avoid confusion. + validateSingleCommitMatchesPrTitle: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..82277ad --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,25 @@ +name: Release + +on: + # Trigger the workflow on push on the main branch + push: + branches: + - main + paths-ignore: + - 'CODEOWNERS' + - '**.md' + - '.**' + +jobs: + release: + name: Release + runs-on: ubuntu-22.04 + + steps: + + - name: 🚀 Release with docker action + id: release + uses: pagopa/eng-github-actions-iac-template/global/release-with-docker@main # + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.github/workflows/snapshot-docker.yml b/.github/workflows/snapshot-docker.yml new file mode 100644 index 0000000..3496adf --- /dev/null +++ b/.github/workflows/snapshot-docker.yml @@ -0,0 +1,23 @@ +name: Snapshot docker build and push + +on: + push: + # Sequence of patterns matched against refs/heads + branches-ignore: + - 'main' + paths-ignore: + - 'CODEOWNERS' + - '**.md' + - '.**' + +jobs: + release: + name: Snapshot Docker + runs-on: ubuntu-22.04 + + steps: + - name: 📦 Docker build and push + id: release + uses: pagopa/eng-github-actions-iac-template/global/docker-build-push@main # + with: + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 0000000..e039c2b --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,52 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Docker security scan + +on: + push: + branches: [ "main", "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main", "master" ] + schedule: + - cron: '00 07 * * *' + +permissions: + contents: read + +jobs: + build: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + name: Build + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + # from https://github.com/actions/checkout/commits/main + uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656657ea2c6707 + + - name: Build an image from Dockerfile + run: | + docker build -t docker.io/my-organization/my-app:${{ github.sha }} . + + - name: Run Trivy vulnerability scanner + # from https://github.com/aquasecurity/trivy-action/commits/master + uses: aquasecurity/trivy-action@d63413b0a4a4482237085319f7f4a1ce99a8f2ac + with: + image-ref: 'docker.io/my-organization/my-app:${{ github.sha }}' + format: 'template' + template: '@/contrib/sarif.tpl' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH' + timeout: '10m0s' + + - name: Upload Trivy scan results to GitHub Security tab + # from https://github.com/github/codeql-action/commits/main + uses: github/codeql-action/upload-sarif@f0a12816612c7306b485a22cb164feb43c6df818 + with: + sarif_file: 'trivy-results.sarif' \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..45deb9c --- /dev/null +++ b/pom.xml @@ -0,0 +1,125 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + it.gov.pagopa.citizen + emd-citizen + emd-citizen + Citizen Microservice + 0.0.1-SNAPSHOT + + + 17 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-data-mongodb-reactive + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.azure.spring + spring-cloud-azure-starter-data-cosmos + + + org.springframework.cloud + spring-cloud-stream-test-support + test + + + + + + + org.projectlombok + lombok + 1.18.30 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.vaadin.external.google + android-json + + + + + io.projectreactor + reactor-test + test + + + + + + + com.azure.spring + spring-cloud-azure-dependencies + 5.16.0 + pom + import + + + org.springframework.cloud + spring-cloud-dependencies + 2023.0.2 + pom + import + + + org.xmlunit + xmlunit-core + 2.10.0 + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/src/main/java/it/gov/pagopa/common/configuration/CustomReactiveMongoHealthIndicator.java b/src/main/java/it/gov/pagopa/common/configuration/CustomReactiveMongoHealthIndicator.java new file mode 100644 index 0000000..d713e4e --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/configuration/CustomReactiveMongoHealthIndicator.java @@ -0,0 +1,28 @@ +package it.gov.pagopa.common.configuration; + +import org.bson.Document; +import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.util.Assert; +import reactor.core.publisher.Mono; + +public class CustomReactiveMongoHealthIndicator extends AbstractReactiveHealthIndicator { + + private final ReactiveMongoTemplate reactiveMongoTemplate; + + public CustomReactiveMongoHealthIndicator(ReactiveMongoTemplate reactiveMongoTemplate) { + super("Mongo health check failed"); + Assert.notNull(reactiveMongoTemplate, "ReactiveMongoTemplate must not be null"); + this.reactiveMongoTemplate = reactiveMongoTemplate; + } + + @Override + protected Mono doHealthCheck(Health.Builder builder) { + Mono buildInfo = this.reactiveMongoTemplate.executeCommand("{ isMaster: 1 }"); + return buildInfo.map(document -> builderUp(builder, document)); + } + private Health builderUp(Health.Builder builder, Document document) { + return builder.up().withDetail("maxWireVersion", document.getInteger("maxWireVersion")).build(); + } +} diff --git a/src/main/java/it/gov/pagopa/common/configuration/MongoConfig.java b/src/main/java/it/gov/pagopa/common/configuration/MongoConfig.java new file mode 100644 index 0000000..aa906e0 --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/configuration/MongoConfig.java @@ -0,0 +1,104 @@ +package it.gov.pagopa.common.configuration; + +import com.mongodb.lang.NonNull; +import it.gov.pagopa.common.utils.CommonConstants; +import lombok.Setter; +import org.bson.types.Decimal128; +import org.springframework.boot.autoconfigure.mongo.MongoClientSettingsBuilderCustomizer; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +@Configuration +public class MongoConfig { + + @Configuration + @ConfigurationProperties(prefix = "spring.data.mongodb.config") + public static class MongoDbCustomProperties { + @Setter + ConnectionPoolSettings connectionPool; + + @Setter + static class ConnectionPoolSettings { + int maxSize; + int minSize; + long maxWaitTimeMS; + long maxConnectionLifeTimeMS; + long maxConnectionIdleTimeMS; + int maxConnecting; + } + + } + + @Bean + public MongoClientSettingsBuilderCustomizer customizer(MongoDbCustomProperties mongoDbCustomProperties) { + return builder -> builder.applyToConnectionPoolSettings( + connectionPool -> { + connectionPool.maxSize(mongoDbCustomProperties.connectionPool.maxSize); + connectionPool.minSize(mongoDbCustomProperties.connectionPool.minSize); + connectionPool.maxWaitTime(mongoDbCustomProperties.connectionPool.maxWaitTimeMS, TimeUnit.MILLISECONDS); + connectionPool.maxConnectionLifeTime(mongoDbCustomProperties.connectionPool.maxConnectionLifeTimeMS, TimeUnit.MILLISECONDS); + connectionPool.maxConnectionIdleTime(mongoDbCustomProperties.connectionPool.maxConnectionIdleTimeMS, TimeUnit.MILLISECONDS); + connectionPool.maxConnecting(mongoDbCustomProperties.connectionPool.maxConnecting); + }); + } + + @Bean + public MongoCustomConversions mongoCustomConversions() { + return new MongoCustomConversions(Arrays.asList( + // BigDecimal support + new BigDecimalDecimal128Converter(), + new Decimal128BigDecimalConverter(), + + // OffsetDateTime support + new OffsetDateTimeWriteConverter(), + new OffsetDateTimeReadConverter() + )); + } + + @WritingConverter + private static class BigDecimalDecimal128Converter implements Converter { + + @Override + public Decimal128 convert(@NonNull BigDecimal source) { + return new Decimal128(source); + } + } + + @ReadingConverter + private static class Decimal128BigDecimalConverter implements Converter { + + @Override + public BigDecimal convert(@NonNull Decimal128 source) { + return source.bigDecimalValue(); + } + + } + + @WritingConverter + public static class OffsetDateTimeWriteConverter implements Converter { + @Override + public Date convert(OffsetDateTime offsetDateTime) { + return Date.from(offsetDateTime.toInstant()); + } + } + + @ReadingConverter + public static class OffsetDateTimeReadConverter implements Converter { + @Override + public OffsetDateTime convert(Date date) { + return date.toInstant().atZone(CommonConstants.ZONEID).toOffsetDateTime(); + } + } +} + diff --git a/src/main/java/it/gov/pagopa/common/configuration/MongoHealthConfig.java b/src/main/java/it/gov/pagopa/common/configuration/MongoHealthConfig.java new file mode 100644 index 0000000..d046ec2 --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/configuration/MongoHealthConfig.java @@ -0,0 +1,14 @@ +package it.gov.pagopa.common.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; + +@Configuration +public class MongoHealthConfig { + @Bean + public CustomReactiveMongoHealthIndicator customMongoHealthIndicator(ReactiveMongoTemplate reactiveMongoTemplate) { + return new CustomReactiveMongoHealthIndicator(reactiveMongoTemplate); + } +} + diff --git a/src/main/java/it/gov/pagopa/common/utils/CommonConstants.java b/src/main/java/it/gov/pagopa/common/utils/CommonConstants.java new file mode 100644 index 0000000..79440b2 --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/utils/CommonConstants.java @@ -0,0 +1,16 @@ +package it.gov.pagopa.common.utils; + +import java.time.ZoneId; + +public class CommonConstants { + + + public static final class ExceptionCode { + public static final String GENERIC_ERROR = "GENERIC_ERROR"; + private ExceptionCode() {} + } + + public static final ZoneId ZONEID = ZoneId.of("Europe/Rome"); + + private CommonConstants(){} +} diff --git a/src/main/java/it/gov/pagopa/common/utils/Utils.java b/src/main/java/it/gov/pagopa/common/utils/Utils.java new file mode 100644 index 0000000..28bfd53 --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/utils/Utils.java @@ -0,0 +1,38 @@ +package it.gov.pagopa.common.utils; +import it.gov.pagopa.common.web.exception.EmdEncryptionException; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +@Slf4j +public class Utils { + + + private Utils(){} + public static String createSHA256(String fiscalCode) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] encodedhash = md.digest(fiscalCode.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(2 * encodedhash.length); + for (byte b : encodedhash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + log.error("Something went wrong creating SHA256"); + throw new EmdEncryptionException("Something went wrong creating SHA256",true,e); + } + } + + public static String inputSanify(String message){ + if (message != null) + return message.replaceAll("[\\r\\n]", ""); + return "[EMD][WARNING] Null log"; + } +} diff --git a/src/main/java/it/gov/pagopa/common/web/dto/ErrorDTO.java b/src/main/java/it/gov/pagopa/common/web/dto/ErrorDTO.java new file mode 100644 index 0000000..3e4622d --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/web/dto/ErrorDTO.java @@ -0,0 +1,22 @@ +package it.gov.pagopa.common.web.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import it.gov.pagopa.common.web.exception.ServiceExceptionPayload; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@AllArgsConstructor +@NoArgsConstructor +@Data +@EqualsAndHashCode +public class ErrorDTO implements ServiceExceptionPayload { + + @NotBlank + private String code; + @NotBlank + private String message; +} diff --git a/src/main/java/it/gov/pagopa/common/web/exception/ClientException.java b/src/main/java/it/gov/pagopa/common/web/exception/ClientException.java new file mode 100644 index 0000000..4bf7708 --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/web/exception/ClientException.java @@ -0,0 +1,25 @@ +package it.gov.pagopa.common.web.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class ClientException extends RuntimeException { + private final HttpStatus httpStatus; + private final boolean printStackTrace; + + public ClientException(HttpStatus httpStatus, String message) { + this(httpStatus, message, null); + } + + public ClientException(HttpStatus httpStatus, String message, Throwable ex) { + this(httpStatus, message, false, ex); + } + + public ClientException( + HttpStatus httpStatus, String message, boolean printStackTrace, Throwable ex) { + super(message, ex); + this.httpStatus = httpStatus; + this.printStackTrace = printStackTrace; + } +} diff --git a/src/main/java/it/gov/pagopa/common/web/exception/ClientExceptionNoBody.java b/src/main/java/it/gov/pagopa/common/web/exception/ClientExceptionNoBody.java new file mode 100644 index 0000000..f330ec2 --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/web/exception/ClientExceptionNoBody.java @@ -0,0 +1,20 @@ +package it.gov.pagopa.common.web.exception; + +import org.springframework.http.HttpStatus; + +public class ClientExceptionNoBody extends ClientException { + + public ClientExceptionNoBody(HttpStatus httpStatus, String message) { + super(httpStatus, message); + } + + public ClientExceptionNoBody(HttpStatus httpStatus, String message, Throwable ex) { + super(httpStatus, message, ex); + } + + public ClientExceptionNoBody(HttpStatus httpStatus, String message, boolean printStackTrace, + Throwable ex) { + super(httpStatus, message, printStackTrace, ex); + } +} + diff --git a/src/main/java/it/gov/pagopa/common/web/exception/ClientExceptionWithBody.java b/src/main/java/it/gov/pagopa/common/web/exception/ClientExceptionWithBody.java new file mode 100644 index 0000000..7faac1c --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/web/exception/ClientExceptionWithBody.java @@ -0,0 +1,23 @@ +package it.gov.pagopa.common.web.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class ClientExceptionWithBody extends ClientException { + private final String code; + + public ClientExceptionWithBody(HttpStatus httpStatus, String code, String message){ + this(httpStatus, code, message, null); + } + + public ClientExceptionWithBody(HttpStatus httpStatus, String code, String message, + Throwable ex){ + this(httpStatus, code, message, false, ex); + } + + public ClientExceptionWithBody(HttpStatus httpStatus, String code, String message, boolean printStackTrace, Throwable ex){ + super(httpStatus, message, printStackTrace, ex); + this.code = code; + } +} diff --git a/src/main/java/it/gov/pagopa/common/web/exception/EmdEncryptionException.java b/src/main/java/it/gov/pagopa/common/web/exception/EmdEncryptionException.java new file mode 100644 index 0000000..c846862 --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/web/exception/EmdEncryptionException.java @@ -0,0 +1,14 @@ +package it.gov.pagopa.common.web.exception; + + +import it.gov.pagopa.common.utils.CommonConstants.ExceptionCode; + +public class EmdEncryptionException extends ServiceException { + + public EmdEncryptionException(String message, boolean printStackTrace, Throwable ex) { + this(ExceptionCode.GENERIC_ERROR, message, printStackTrace, ex); + } + public EmdEncryptionException(String code, String message, boolean printStackTrace, Throwable ex) { + super(code, message,null, printStackTrace, ex); + } +} diff --git a/src/main/java/it/gov/pagopa/common/web/exception/ErrorManager.java b/src/main/java/it/gov/pagopa/common/web/exception/ErrorManager.java new file mode 100644 index 0000000..7b37f7a --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/web/exception/ErrorManager.java @@ -0,0 +1,77 @@ +package it.gov.pagopa.common.web.exception; + +import it.gov.pagopa.common.web.dto.ErrorDTO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.lang.Nullable; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.Optional; + +@RestControllerAdvice +@Slf4j +public class ErrorManager { + private final ErrorDTO defaultErrorDTO; + + public ErrorManager(@Nullable ErrorDTO defaultErrorDTO) { + this.defaultErrorDTO = Optional.ofNullable(defaultErrorDTO) + .orElse(new ErrorDTO("Error", "Something gone wrong")); + } + + @ExceptionHandler(RuntimeException.class) + protected ResponseEntity handleException(RuntimeException error, ServerHttpRequest request) { + + logClientException(error, request); + + if(error instanceof ClientExceptionNoBody clientExceptionNoBody){ + return ResponseEntity.status(clientExceptionNoBody.getHttpStatus()).build(); + } + else { + ErrorDTO errorDTO; + HttpStatus httpStatus; + if (error instanceof ClientExceptionWithBody clientExceptionWithBody){ + httpStatus=clientExceptionWithBody.getHttpStatus(); + errorDTO = new ErrorDTO(clientExceptionWithBody.getCode(), error.getMessage()); + } + else { + httpStatus=HttpStatus.INTERNAL_SERVER_ERROR; + errorDTO = defaultErrorDTO; + } + return ResponseEntity.status(httpStatus) + .contentType(MediaType.APPLICATION_JSON) + .body(errorDTO); + } + } + public static void logClientException(RuntimeException error, ServerHttpRequest request) { + Throwable unwrappedException = error.getCause() instanceof ServiceException + ? error.getCause() + : error; + + String clientExceptionMessage = ""; + if(error instanceof ClientException clientException) { + clientExceptionMessage = ": HttpStatus %s - %s%s".formatted( + clientException.getHttpStatus(), + (clientException instanceof ClientExceptionWithBody clientExceptionWithBody) ? clientExceptionWithBody.getCode() + ": " : "", + clientException.getMessage() + ); + } + + if(!(error instanceof ClientException clientException) || clientException.isPrintStackTrace() || unwrappedException.getCause() != null){ + log.error("Something went wrong handling request {}{}", getRequestDetails(request), clientExceptionMessage, unwrappedException); + } else { + log.info("A {} occurred handling request {}{} at {}", + unwrappedException.getClass().getSimpleName() , + getRequestDetails(request), + clientExceptionMessage, + unwrappedException.getStackTrace().length > 0 ? unwrappedException.getStackTrace()[0] : "UNKNOWN"); + } + } + + public static String getRequestDetails(ServerHttpRequest request) { + return "%s %s".formatted(request.getMethod(), request.getURI()); + } +} diff --git a/src/main/java/it/gov/pagopa/common/web/exception/ServiceException.java b/src/main/java/it/gov/pagopa/common/web/exception/ServiceException.java new file mode 100644 index 0000000..dfd8317 --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/web/exception/ServiceException.java @@ -0,0 +1,25 @@ +package it.gov.pagopa.common.web.exception; + +import lombok.Getter; + +@Getter +public class ServiceException extends RuntimeException { + private final String code; + private final boolean printStackTrace; + private final ServiceExceptionPayload payload; + + public ServiceException(String code, String message) { + this(code, message, null); + } + public ServiceException(String code, String message, ServiceExceptionPayload payload) { + this(code, message, payload, false, null); + } + + public ServiceException(String code, String message, ServiceExceptionPayload payload, boolean printStackTrace, Throwable ex) { + super(message, ex); + this.code = code; + this.printStackTrace = printStackTrace; + this.payload = payload; + } + +} diff --git a/src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandler.java b/src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandler.java new file mode 100644 index 0000000..4763292 --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandler.java @@ -0,0 +1,56 @@ +package it.gov.pagopa.common.web.exception; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.Map; + +@RestControllerAdvice +@Slf4j +@Order(Ordered.HIGHEST_PRECEDENCE) +public class ServiceExceptionHandler { + private final ErrorManager errorManager; + private final Map, HttpStatus> transcodeMap; + + public ServiceExceptionHandler(ErrorManager errorManager, Map, HttpStatus> transcodeMap) { + this.errorManager = errorManager; + this.transcodeMap = transcodeMap; + } + + @SuppressWarnings("squid:S1452") + @ExceptionHandler(ServiceException.class) + protected ResponseEntity handleException(ServiceException error, ServerHttpRequest request) { + if (null != error.getPayload()) { + return handleBodyProvidedException(error, request); + } + return errorManager.handleException(transcodeException(error), request); + } + + private ClientException transcodeException(ServiceException error) { + HttpStatus httpStatus = transcodeMap.get(error.getClass()); + + if (httpStatus == null) { + log.warn("Unhandled exception: {}", error.getClass().getName()); + httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; + } + + return new ClientExceptionWithBody(httpStatus, error.getCode(), error.getMessage(), error.isPrintStackTrace(), error); + } + + private ResponseEntity handleBodyProvidedException(ServiceException error, ServerHttpRequest request) { + ClientException clientException = transcodeException(error); + ErrorManager.logClientException(clientException, request); + + return ResponseEntity.status(clientException.getHttpStatus()) + .contentType(MediaType.APPLICATION_JSON) + .body(error.getPayload()); + } +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionPayload.java b/src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionPayload.java new file mode 100644 index 0000000..32e805f --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionPayload.java @@ -0,0 +1,7 @@ +package it.gov.pagopa.common.web.exception; + +import java.io.Serializable; + +public interface ServiceExceptionPayload extends Serializable{ + +} diff --git a/src/main/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandler.java b/src/main/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandler.java new file mode 100644 index 0000000..b7873ff --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandler.java @@ -0,0 +1,75 @@ +package it.gov.pagopa.common.web.exception; + +import it.gov.pagopa.common.web.dto.ErrorDTO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.lang.Nullable; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.bind.support.WebExchangeBindException; +import org.springframework.web.reactive.resource.NoResourceFoundException; +import org.springframework.web.server.MissingRequestValueException; + +import java.util.Optional; +import java.util.stream.Collectors; + +@RestControllerAdvice +@Slf4j +@Order(Ordered.HIGHEST_PRECEDENCE) +public class ValidationExceptionHandler { + + private final ErrorDTO templateValidationErrorDTO; + + public ValidationExceptionHandler(@Nullable ErrorDTO templateValidationErrorDTO) { + this.templateValidationErrorDTO = Optional.ofNullable(templateValidationErrorDTO) + .orElse(new ErrorDTO("INVALID_REQUEST", "Invalid request")); + } + + @ExceptionHandler(WebExchangeBindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorDTO handleWebExchangeBindException( + WebExchangeBindException ex, ServerHttpRequest request) { + + String message = ex.getBindingResult().getAllErrors().stream() + .map(error -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + return String.format("[%s]: %s", fieldName, errorMessage); + }).collect(Collectors.joining("; ")); + + log.info("A WebExchangeBindException occurred handling request {}: HttpStatus 400 - {}", + ErrorManager.getRequestDetails(request), message); + log.debug("Something went wrong while validating http request", ex); + + return new ErrorDTO(templateValidationErrorDTO.getCode(), message); + } + + @ExceptionHandler(MissingRequestValueException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorDTO handleMissingRequestValueException(MissingRequestValueException e, ServerHttpRequest request) { + + log.info("A MissingRequestValueException occurred handling request {}: HttpStatus 400 - {}", + ErrorManager.getRequestDetails(request), e.getMessage()); + log.debug("Something went wrong due to a missing request value", e); + + return new ErrorDTO(templateValidationErrorDTO.getCode(), templateValidationErrorDTO.getMessage()); + } + + @ExceptionHandler(NoResourceFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorDTO handleNoResourceFoundException(NoResourceFoundException e, ServerHttpRequest request) { + + log.info("A NoResourceFoundException occurred handling request {}: HttpStatus 400 - {}", + ErrorManager.getRequestDetails(request), e.getMessage()); + log.debug("Something went wrong due to a missing request value", e); + + return new ErrorDTO(templateValidationErrorDTO.getCode(), templateValidationErrorDTO.getMessage()); + } + + +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/EmdCitizen.java b/src/main/java/it/gov/pagopa/onboarding/citizen/EmdCitizen.java new file mode 100644 index 0000000..94b6308 --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/EmdCitizen.java @@ -0,0 +1,13 @@ +package it.gov.pagopa.onboarding.citizen; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = "it.gov.pagopa") +public class EmdCitizen { + + public static void main(String[] args) { + SpringApplication.run(EmdCitizen.class, args); + } + +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/configuration/ExceptionMap.java b/src/main/java/it/gov/pagopa/onboarding/citizen/configuration/ExceptionMap.java new file mode 100644 index 0000000..141f65f --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/configuration/ExceptionMap.java @@ -0,0 +1,37 @@ +package it.gov.pagopa.onboarding.citizen.configuration; + + +import it.gov.pagopa.common.web.exception.ClientExceptionWithBody; +import it.gov.pagopa.onboarding.citizen.constants.OnboardingCitizenConstants; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +@Configuration +public class ExceptionMap { + + private final Map> exMap = new HashMap<>(); + + public ExceptionMap() { + exMap.put(OnboardingCitizenConstants.ExceptionName.CITIZEN_NOT_ONBOARDED, () -> + new ClientExceptionWithBody( + HttpStatus.NOT_FOUND, + OnboardingCitizenConstants.ExceptionCode.CITIZEN_NOT_ONBOARDED, + OnboardingCitizenConstants.ExceptionMessage.CITIZEN_NOT_ONBOARDED + ) + ); + + } + + public RuntimeException getException(String exceptionKey) { + if (exMap.containsKey(exceptionKey)) { + return exMap.get(exceptionKey).get(); + } else { + throw new IllegalArgumentException(String.format("Exception Name Not Found: %s", exceptionKey)); + } + } + +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/constants/OnboardingCitizenConstants.java b/src/main/java/it/gov/pagopa/onboarding/citizen/constants/OnboardingCitizenConstants.java new file mode 100644 index 0000000..4ec7f3f --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/constants/OnboardingCitizenConstants.java @@ -0,0 +1,27 @@ +package it.gov.pagopa.onboarding.citizen.constants; + +public class OnboardingCitizenConstants { + public static final class ExceptionCode { + + public static final String CITIZEN_NOT_ONBOARDED = "CITIZEN_NOT_ONBOARDED"; + public static final String GENERIC_ERROR = "GENERIC_ERROR"; + private ExceptionCode() {} + } + + public static final class ExceptionMessage { + + public static final String CITIZEN_NOT_ONBOARDED = "CITIZEN_NOT_ONBOARDED"; + public static final String GENERIC_ERROR = "GENERIC_ERROR"; + private ExceptionMessage() {} + } + + public static final class ExceptionName { + + public static final String CITIZEN_NOT_ONBOARDED = "CITIZEN_NOT_ONBOARDED"; + public static final String GENERIC_ERROR = "GENERIC_ERROR"; + private ExceptionName() {} + } + + + private OnboardingCitizenConstants() {} +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenController.java b/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenController.java new file mode 100644 index 0000000..505f2a5 --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenController.java @@ -0,0 +1,56 @@ +package it.gov.pagopa.onboarding.citizen.controller; + +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import jakarta.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Mono; + +import java.util.List; + + +@RequestMapping("/emd/citizen") +public interface CitizenController { + + @PostMapping("") + Mono> saveCitizenConsent(@Valid @RequestBody CitizenConsentDTO citizenConsentDTO); + + /** + * Update the state of a citizen's consent. + * + * @param citizenConsentDTO contains the hashedFiscalCode, channelId, and channelState to update + * @return the updated citizen consents + */ + @PutMapping("/stateUpdate") + Mono> stateUpdate(@Valid @RequestBody CitizenConsentDTO citizenConsentDTO); + + /** + * Get the consent status for a specific citizen and channel. + * + * @param fiscalCode the fiscal code of the citizen + * @param tppId the ID of the tpp + * @return the citizen consent status + */ + @GetMapping("/{fiscalCode}/{tppId}") + Mono> getConsentStatus(@PathVariable String fiscalCode, @PathVariable String tppId); + + /** + * List all channels with enabled consents for a specific citizen. + * + * @param fiscalCode the fiscal code of the citizen + * @return a list of channels with enabled consents + */ + @GetMapping("/list/{fiscalCode}/enabled") + Mono>> getCitizenConsentsEnabled(@PathVariable String fiscalCode); + + /** + * List all channels and their consent status for a specific citizen. + * + * @param fiscalCode the fiscal code of the citizen + * @return a list of all channels with their consent statuses + */ + @GetMapping("/list/{fiscalCode}") + Mono>> getCitizenConsents(@PathVariable String fiscalCode); + +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerImpl.java new file mode 100644 index 0000000..668325d --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerImpl.java @@ -0,0 +1,53 @@ +package it.gov.pagopa.onboarding.citizen.controller; + +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import it.gov.pagopa.onboarding.citizen.service.CitizenServiceImpl; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +import java.util.List; + +@RestController +public class CitizenControllerImpl implements CitizenController { + + private final CitizenServiceImpl citizenService; + + public CitizenControllerImpl(CitizenServiceImpl citizenService) { + this.citizenService = citizenService; + } + + @Override + public Mono> saveCitizenConsent(@Valid CitizenConsentDTO citizenConsentDTO) { + return citizenService.createCitizenConsent(citizenConsentDTO) + .map(ResponseEntity::ok); + } + + @Override + public Mono> stateUpdate(@Valid CitizenConsentDTO citizenConsentDTO) { + return citizenService.updateChannelState( + citizenConsentDTO.getHashedFiscalCode(), //at this stage the fiscalCode has not yet been hashed + citizenConsentDTO.getTppId(), + citizenConsentDTO.getTppState()) + .map(ResponseEntity::ok); + } + + @Override + public Mono> getConsentStatus(String fiscalCode, String tppId) { + return citizenService.getConsentStatus(fiscalCode, tppId) + .map(ResponseEntity::ok); + } + + @Override + public Mono>> getCitizenConsentsEnabled(String fiscalCode) { + return citizenService.getListEnabledConsents(fiscalCode) + .map(ResponseEntity::ok); + } + + @Override + public Mono>> getCitizenConsents(String fiscalCode) { + return citizenService.getListAllConsents(fiscalCode) + .map(ResponseEntity::ok); + } +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentDTO.java b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentDTO.java new file mode 100644 index 0000000..9151702 --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentDTO.java @@ -0,0 +1,23 @@ +package it.gov.pagopa.onboarding.citizen.dto; + +import com.fasterxml.jackson.annotation.JsonAlias; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDateTime; + +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class CitizenConsentDTO { + @JsonAlias("fiscalCode") + private String hashedFiscalCode; + private String tppId; + private Boolean tppState; + private String userId; + private LocalDateTime creationDate; + private LocalDateTime lastUpdateDate; +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java new file mode 100644 index 0000000..885bf6f --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java @@ -0,0 +1,19 @@ +package it.gov.pagopa.onboarding.citizen.dto.mapper; + + +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import org.springframework.stereotype.Service; + +@Service +public class CitizenConsentObjectToDTOMapper { + + public CitizenConsentDTO map(CitizenConsent citizenConsent){ + return CitizenConsentDTO.builder() + .tppState(citizenConsent.getTppState()) + .tppId(citizenConsent.getTppId()) + .hashedFiscalCode(citizenConsent.getHashedFiscalCode()) + .userId(citizenConsent.getUserId()) + .build(); + } +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/model/CitizenConsent.java b/src/main/java/it/gov/pagopa/onboarding/citizen/model/CitizenConsent.java new file mode 100644 index 0000000..4c8657e --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/model/CitizenConsent.java @@ -0,0 +1,24 @@ +package it.gov.pagopa.onboarding.citizen.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Document(collection = "citizen_consents") +@Data +@SuperBuilder +@NoArgsConstructor +public class CitizenConsent { + + private String id; + private String hashedFiscalCode; + private String tppId; + private Boolean tppState; + private String userId; + private LocalDateTime creationDate; + private LocalDateTime lastUpdateDate; + +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/model/mapper/CitizenConsentDTOToObjectMapper.java b/src/main/java/it/gov/pagopa/onboarding/citizen/model/mapper/CitizenConsentDTOToObjectMapper.java new file mode 100644 index 0000000..195aebe --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/model/mapper/CitizenConsentDTOToObjectMapper.java @@ -0,0 +1,19 @@ +package it.gov.pagopa.onboarding.citizen.model.mapper; + + +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import org.springframework.stereotype.Service; + +@Service +public class CitizenConsentDTOToObjectMapper { + + public CitizenConsent map(CitizenConsentDTO citizenConsentDTO){ + return CitizenConsent.builder() + .tppState(true) + .tppId(citizenConsentDTO.getTppId()) + .hashedFiscalCode(citizenConsentDTO.getHashedFiscalCode()) + .userId(citizenConsentDTO.getUserId()) + .build(); + } +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenRepository.java b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenRepository.java new file mode 100644 index 0000000..103dc2f --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenRepository.java @@ -0,0 +1,16 @@ +package it.gov.pagopa.onboarding.citizen.repository; + +import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import org.springframework.data.mongodb.repository.ReactiveMongoRepository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface CitizenRepository extends ReactiveMongoRepository { + + Flux findByHashedFiscalCode(String hashedFiscalCode); + + Flux findByHashedFiscalCodeAndTppStateTrue(String hashedFiscalCode); + + Mono findByHashedFiscalCodeAndTppId(String hashedFiscalCode, String tppId); + +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenService.java b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenService.java new file mode 100644 index 0000000..ae98fcd --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenService.java @@ -0,0 +1,15 @@ +package it.gov.pagopa.onboarding.citizen.service; + +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import reactor.core.publisher.Mono; + +import java.util.List; + +public interface CitizenService { + + Mono createCitizenConsent(CitizenConsentDTO citizenConsent); + Mono updateChannelState(String fiscalCode, String tppId, boolean tppState); + Mono getConsentStatus(String fiscalCode, String tppId); + Mono> getListEnabledConsents(String fiscalCode); + Mono> getListAllConsents(String fiscalCode); +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java new file mode 100644 index 0000000..14e9875 --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java @@ -0,0 +1,103 @@ +package it.gov.pagopa.onboarding.citizen.service; + +import it.gov.pagopa.common.utils.Utils; +import it.gov.pagopa.onboarding.citizen.constants.OnboardingCitizenConstants.ExceptionName; +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import it.gov.pagopa.onboarding.citizen.dto.mapper.CitizenConsentObjectToDTOMapper; +import it.gov.pagopa.onboarding.citizen.configuration.ExceptionMap; +import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import it.gov.pagopa.onboarding.citizen.model.mapper.CitizenConsentDTOToObjectMapper; +import it.gov.pagopa.onboarding.citizen.repository.CitizenRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; +import java.util.List; + +import static it.gov.pagopa.common.utils.Utils.inputSanify; + +@Service +@Slf4j +public class CitizenServiceImpl implements CitizenService { + + private final CitizenRepository citizenRepository; + private final CitizenConsentObjectToDTOMapper mapperToDTO; + private final CitizenConsentDTOToObjectMapper mapperToObject; + private final ExceptionMap exceptionMap; + + public CitizenServiceImpl(CitizenRepository citizenRepository, CitizenConsentObjectToDTOMapper mapperToDTO, CitizenConsentDTOToObjectMapper mapperToObject, ExceptionMap exceptionMap) { + this.citizenRepository = citizenRepository; + this.mapperToDTO = mapperToDTO; + this.mapperToObject = mapperToObject; + this.exceptionMap = exceptionMap; + } + + @Override + public Mono createCitizenConsent(CitizenConsentDTO citizenConsentDTO) { + log.info("[EMD][CITIZEN][CREATE] Received message: {}",inputSanify(citizenConsentDTO.toString())); + CitizenConsent citizenConsent = mapperToObject.map(citizenConsentDTO); + String hashedFiscalCode = Utils.createSHA256(citizenConsent.getHashedFiscalCode()); + citizenConsent.setHashedFiscalCode(hashedFiscalCode); + citizenConsent.setCreationDate(LocalDateTime.now()); + citizenConsent.setLastUpdateDate(LocalDateTime.now()); + return citizenRepository.save(citizenConsent) + .map(mapperToDTO::map) + .doOnSuccess(savedConsent -> log.info("[EMD][CREATE-CITIZEN-CONSENT] Created")); + + } + + @Override + public Mono updateChannelState(String fiscalCode, String tppId, boolean tppState) { + String hashedFiscalCode = Utils.createSHA256(fiscalCode); + log.info("[EMD][[CITIZEN][UPDATE-CHANNEL-STATE] Received hashedFiscalCode: {} and tppId: {} with state: {}" + ,hashedFiscalCode, inputSanify(tppId), tppState); + return citizenRepository.findByHashedFiscalCodeAndTppId(hashedFiscalCode, tppId) + .switchIfEmpty(Mono.error(exceptionMap.getException(ExceptionName.CITIZEN_NOT_ONBOARDED))) + .flatMap(citizenConsent -> { + citizenConsent.setTppState(tppState); + citizenConsent.setLastUpdateDate(LocalDateTime.now()); + return citizenRepository.save(citizenConsent); + }) + .map(mapperToDTO::map) + .doOnSuccess(savedConsent -> log.info("[EMD][[CITIZEN][UPDATE-CHANNEL-STATE] Updated state")); + } + + @Override + public Mono getConsentStatus(String fiscalCode, String tppId) { + String hashedFiscalCode = Utils.createSHA256(fiscalCode); + log.info("[EMD][CITIZEN][GET-CONSENT-STATUS] Received hashedFiscalCode: {} and tppId: {}",hashedFiscalCode,inputSanify(tppId)); + return citizenRepository.findByHashedFiscalCodeAndTppId(hashedFiscalCode, tppId) + .switchIfEmpty(Mono.error(exceptionMap.getException(ExceptionName.CITIZEN_NOT_ONBOARDED))) + .map(mapperToDTO::map) + .doOnSuccess(consent -> log.info("[EMD][CITIZEN][GET-CONSENT-STATUS] Consent found:: {}",consent)); + + } + + @Override + public Mono> getListEnabledConsents(String fiscalCode) { + String hashedFiscalCode = Utils.createSHA256(fiscalCode); + log.info("[EMD][CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] Received hashedFiscalCode: {}",hashedFiscalCode); + return citizenRepository.findByHashedFiscalCodeAndTppStateTrue(hashedFiscalCode) + .collectList() + .map(consentList -> consentList.stream() + .map(mapperToDTO::map) + .toList() + ) + .doOnSuccess(consentList -> log.info("EMD][CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] Consents founded: {}",(consentList.size()))); + + } + + @Override + public Mono> getListAllConsents(String fiscalCode) { + String hashedFiscalCode = Utils.createSHA256(fiscalCode); + log.info("[EMD][CITIZEN][FIND-ALL-CITIZEN-CONSENTS] Received hashedFiscalCode: {}",(hashedFiscalCode)); + return citizenRepository.findByHashedFiscalCode(hashedFiscalCode) + .collectList() + .map(consentList -> consentList.stream() + .map(mapperToDTO::map) + .toList() + ) + .doOnSuccess(consentList -> log.info("[EMD][CITIZEN][FIND-ALL-CITIZEN-CONSENTS] Consents found:: {}",consentList)); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..9dc143c --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,42 @@ +spring: + jackson: + time-zone: + Europe/Rome + application: + name: "@project.artifactId@" + version: "@project.version@" + jmx.enabled: true + config: + activate: + on-profile: default + data: + mongodb: + database: ${MONGODB_DBNAME:mil} + uri: ${MONGODB_URI:mongodb://localhost:27017} + config: + connectionPool: + maxSize: ${MONGODB_CONNECTIONPOOL_MAX_SIZE:100} + minSize: ${MONGODB_CONNECTIONPOOL_MIN_SIZE:5} + maxWaitTimeMS: ${MONGODB_CONNECTIONPOOL_MAX_WAIT_MS:120000} + maxConnectionLifeTimeMS: ${MONGODB_CONNECTIONPOOL_MAX_CONNECTION_LIFE_MS:0} + maxConnectionIdleTimeMS: ${MONGODB_CONNECTIONPOOL_MAX_CONNECTION_IDLE_MS:120000} + maxConnecting: ${MONGODB_CONNECTIONPOOL_MAX_CONNECTING:2} + +server: + port: ${CITIZEN_PORT:8084} + +management: + health: + mongo.enabled: ${HEALTH_MONGO_ENABLED:false} + endpoint: + health: + show-details: always + probes.enabled: true + group: + readiness.include: "*" + liveness.include: livenessState,diskSpace,ping + endpoints: + jmx: + exposure.include: "*" + web: + exposure.include: info, health diff --git a/src/test/java/it/gov/pagopa/common/configuration/CustomMongoHealthIndicatorTest.java b/src/test/java/it/gov/pagopa/common/configuration/CustomMongoHealthIndicatorTest.java new file mode 100644 index 0000000..8b0b8c5 --- /dev/null +++ b/src/test/java/it/gov/pagopa/common/configuration/CustomMongoHealthIndicatorTest.java @@ -0,0 +1,50 @@ +package it.gov.pagopa.common.configuration; + +import com.mongodb.MongoException; +import org.bson.Document; +import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + + +class CustomMongoHealthIndicatorTest { + @Test + void testMongoIsUp() { + Document buildInfo = mock(Document.class); + given(buildInfo.getInteger("maxWireVersion")).willReturn(10); + ReactiveMongoTemplate reactiveMongoTemplate = mock(ReactiveMongoTemplate.class); + given(reactiveMongoTemplate.executeCommand("{ isMaster: 1 }")).willReturn(Mono.just(buildInfo)); + CustomReactiveMongoHealthIndicator customReactiveMongoHealthIndicator = new CustomReactiveMongoHealthIndicator( + reactiveMongoTemplate); + Mono health = customReactiveMongoHealthIndicator.health(); + StepVerifier.create(health).consumeNextWith(h -> { + assertThat(h.getStatus()).isEqualTo(Status.UP); + assertThat(h.getDetails()).containsOnlyKeys("maxWireVersion"); + assertThat(h.getDetails()).containsEntry("maxWireVersion", 10); + }).expectComplete().verify(Duration.ofSeconds(30)); + } + + @Test + void testMongoIsDown() { + ReactiveMongoTemplate reactiveMongoTemplate = mock(ReactiveMongoTemplate.class); + given(reactiveMongoTemplate.executeCommand("{ isMaster: 1 }")).willThrow(new MongoException("Connection failed")); + CustomReactiveMongoHealthIndicator customReactiveMongoHealthIndicator = new CustomReactiveMongoHealthIndicator( + reactiveMongoTemplate); + Mono health = customReactiveMongoHealthIndicator.health(); + StepVerifier.create(health).consumeNextWith(h -> { + assertThat(h.getStatus()).isEqualTo(Status.DOWN); + assertThat(h.getDetails()).containsOnlyKeys("error"); + assertThat(h.getDetails()).containsEntry("error", MongoException.class.getName() + ": Connection failed"); + }).expectComplete().verify(Duration.ofSeconds(30)); + } + +} diff --git a/src/test/java/it/gov/pagopa/common/utils/MemoryAppender.java b/src/test/java/it/gov/pagopa/common/utils/MemoryAppender.java new file mode 100644 index 0000000..fb46b58 --- /dev/null +++ b/src/test/java/it/gov/pagopa/common/utils/MemoryAppender.java @@ -0,0 +1,27 @@ +package it.gov.pagopa.common.utils; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; + +import java.util.Collections; +import java.util.List; + +public class MemoryAppender extends ListAppender { + public void reset() { + this.list.clear(); + } + + public boolean contains(ch.qos.logback.classic.Level level, String string) { + return this.list.stream() + .anyMatch(event -> event.toString().contains(string) + && event.getLevel().equals(level)); + } + + public int getSize() { + return this.list.size(); + } + + public List getLoggedEvents() { + return Collections.unmodifiableList(this.list); + } +} diff --git a/src/test/java/it/gov/pagopa/common/utils/UtilsTest.java b/src/test/java/it/gov/pagopa/common/utils/UtilsTest.java new file mode 100644 index 0000000..3fa89a3 --- /dev/null +++ b/src/test/java/it/gov/pagopa/common/utils/UtilsTest.java @@ -0,0 +1,41 @@ +package it.gov.pagopa.common.utils; + + +import it.gov.pagopa.common.web.exception.EmdEncryptionException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; + +@ExtendWith(MockitoExtension.class) +class UtilsTest { + + + @Test + void createSHA256_Ko_NoSuchAlgorithm() { + try (MockedStatic mockedStatic = Mockito.mockStatic(MessageDigest.class)) { + mockedStatic.when(() -> MessageDigest.getInstance(any())) + .thenThrow(new NoSuchAlgorithmException("SHA-256 not available")); + + EmdEncryptionException exception = assertThrows(EmdEncryptionException.class, () -> Utils.createSHA256("")); + + assertEquals("SHA-256 not available", exception.getCause().getMessage()); + } + } + @Test + void createSHA256_Ok(){ + String toHash = "RSSMRA98B18L049O"; + String hashedExpected = "0b393cbe68a39f26b90c80a8dc95abc0fe4c21821195b4671a374c1443f9a1bb"; + String actualHash = Utils.createSHA256(toHash); + assertEquals(actualHash,hashedExpected); + } + +} diff --git a/src/test/java/it/gov/pagopa/common/web/exception/ErrorManagerTest.java b/src/test/java/it/gov/pagopa/common/web/exception/ErrorManagerTest.java new file mode 100644 index 0000000..078708d --- /dev/null +++ b/src/test/java/it/gov/pagopa/common/web/exception/ErrorManagerTest.java @@ -0,0 +1,195 @@ +package it.gov.pagopa.common.web.exception; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.StackTraceElementProxy; +import it.gov.pagopa.common.utils.MemoryAppender; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.regex.Pattern; + +@WebFluxTest(value = { + ErrorManagerTest.TestController.class}, excludeAutoConfiguration = SecurityAutoConfiguration.class) +@ContextConfiguration(classes = {ErrorManagerTest.TestController.class, ErrorManager.class}) +@Slf4j +class ErrorManagerTest { + private static final String EXPECTED_GENERIC_ERROR = "{\"code\":\"Error\",\"message\":\"Something gone wrong\"}"; + + @Autowired + private WebTestClient webTestClient; + @SpyBean + private TestController testControllerSpy; + + private static MemoryAppender memoryAppender; + + @RestController + static class TestController { + @GetMapping("/test") + String testEndpoint() { + return "OK"; + } + } + + @BeforeAll + static void configureMemoryAppender() { + memoryAppender = new MemoryAppender(); + memoryAppender.setContext((LoggerContext) LoggerFactory.getILoggerFactory()); + memoryAppender.start(); + } + + @BeforeEach + void clearMemoryAppender() { + memoryAppender.reset(); + ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ErrorManager.class.getName()); + logger.setLevel(ch.qos.logback.classic.Level.INFO); + logger.addAppender(memoryAppender); + } + + @Test + void handleExceptionClientExceptionNoBody() { + Mockito.doThrow( + new ClientExceptionNoBody(HttpStatus.BAD_REQUEST, "NOTFOUND ClientExceptionNoBody")) + .when(testControllerSpy).testEndpoint(); + + webTestClient.get() + .uri("/test") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isBadRequest(); + + checkStackTraceSuppressedLog(memoryAppender, + "A ClientExceptionNoBody occurred handling request GET /test: HttpStatus 400 BAD_REQUEST - NOTFOUND ClientExceptionNoBody at it.gov.pagopa.common.web.exception.ErrorManagerTest\\$TestController.testEndpoint\\(ErrorManagerTest.java:[0-9]+\\)"); + } + + @Test + void handleExceptionClientExceptionWithBody() { + Mockito.doThrow( + new ClientExceptionWithBody(HttpStatus.BAD_REQUEST, "Error", "Error ClientExceptionWithBody")) + .when(testControllerSpy).testEndpoint(); + + webTestClient.get() + .uri("/test") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isBadRequest() + .expectBody() + .json("{\"code\":\"Error\",\"message\":\"Error ClientExceptionWithBody\"}"); + + Mockito.doThrow( + new ClientExceptionWithBody(HttpStatus.BAD_REQUEST, "Error", "Error ClientExceptionWithBody", + new Exception())) + .when(testControllerSpy).testEndpoint(); + + webTestClient.get() + .uri("/test") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isBadRequest() + .expectBody() + .json("{\"code\":\"Error\",\"message\":\"Error ClientExceptionWithBody\"}"); + } + + @Test + void handleExceptionClientExceptionTest() { + Mockito.doThrow(ClientException.class) + .when(testControllerSpy).testEndpoint(); + + webTestClient.get() + .uri("/test") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().is5xxServerError() + .expectBody() + .json(EXPECTED_GENERIC_ERROR); + + checkStackTraceSuppressedLog(memoryAppender, "A ClientException occurred handling request GET /test: HttpStatus null - null at UNKNOWN"); + memoryAppender.reset(); + + Mockito.doThrow( + new ClientException(HttpStatus.BAD_REQUEST, "ClientException with httpStatus and message")) + .when(testControllerSpy).testEndpoint(); + + webTestClient.get() + .uri("/test") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().is5xxServerError() + .expectBody() + .json(EXPECTED_GENERIC_ERROR); + + checkStackTraceSuppressedLog(memoryAppender, "A ClientException occurred handling request GET /test: HttpStatus 400 BAD_REQUEST - ClientException with httpStatus and message at it.gov.pagopa.common.web.exception.ErrorManagerTest\\$TestController.testEndpoint\\(ErrorManagerTest.java:[0-9]+\\)"); + memoryAppender.reset(); + + Mockito.doThrow(new ClientException(HttpStatus.BAD_REQUEST, + "ClientException with httpStatus, message and throwable", new Throwable())) + .when(testControllerSpy).testEndpoint(); + + webTestClient.get() + .uri("/test") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().is5xxServerError() + .expectBody() + .json(EXPECTED_GENERIC_ERROR); + + checkLog(memoryAppender, + "Something went wrong handling request GET /test: HttpStatus 400 BAD_REQUEST - ClientException with httpStatus, message and throwable", + "it.gov.pagopa.common.web.exception.ClientException: ClientException with httpStatus, message and throwable", + "it.gov.pagopa.common.web.exception.ErrorManagerTest$TestController.testEndpoint" + ); + } + + @Test + void handleExceptionRuntimeException() { + Mockito.doThrow(RuntimeException.class) + .when(testControllerSpy).testEndpoint(); + + webTestClient.get() + .uri("/test") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().is5xxServerError() + .expectBody() + .json(EXPECTED_GENERIC_ERROR); + } + + public static void checkStackTraceSuppressedLog(MemoryAppender memoryAppender, String expectedLoggedMessage) { + String loggedMessage = memoryAppender.getLoggedEvents().get(0).getFormattedMessage(); + Assertions.assertTrue(Pattern.matches(expectedLoggedMessage, loggedMessage), + "Unexpected logged message: " + loggedMessage); + } + + public static void checkLog(MemoryAppender memoryAppender, String expectedLoggedMessageRegexp, String expectedLoggedExceptionMessage, String expectedLoggedExceptionOccurrencePosition) { + ILoggingEvent loggedEvent = memoryAppender.getLoggedEvents().get(0); + IThrowableProxy loggedException = loggedEvent.getThrowableProxy(); + StackTraceElementProxy loggedExceptionOccurrenceStackTrace = loggedException.getStackTraceElementProxyArray()[0]; + + String loggedMessage = loggedEvent.getFormattedMessage(); + Assertions.assertTrue(Pattern.matches(expectedLoggedMessageRegexp, + loggedEvent.getFormattedMessage()), + "Unexpected logged message: " + loggedMessage); + + Assertions.assertEquals(expectedLoggedExceptionMessage, + loggedException.getClassName() + ": " + loggedException.getMessage()); + + Assertions.assertEquals(expectedLoggedExceptionOccurrencePosition, + loggedExceptionOccurrenceStackTrace.getStackTraceElement().getClassName() + "." + loggedExceptionOccurrenceStackTrace.getStackTraceElement().getMethodName()); + } +} diff --git a/src/test/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandlerTest.java b/src/test/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandlerTest.java new file mode 100644 index 0000000..0896c70 --- /dev/null +++ b/src/test/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandlerTest.java @@ -0,0 +1,104 @@ +package it.gov.pagopa.common.web.exception; + +import ch.qos.logback.classic.LoggerContext; +import it.gov.pagopa.common.utils.MemoryAppender; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@WebFluxTest(value = { + ServiceExceptionHandlerTest.TestController.class}, excludeAutoConfiguration = SecurityAutoConfiguration.class) +@ContextConfiguration(classes = {ServiceExceptionHandler.class, + ServiceExceptionHandlerTest.TestController.class, ErrorManager.class}) +class ServiceExceptionHandlerTest { + @Autowired + private WebTestClient webTestClient; + + private static MemoryAppender memoryAppender; + + @BeforeAll + static void configureMemoryAppender(){ + memoryAppender = new MemoryAppender(); + memoryAppender.setContext((LoggerContext) LoggerFactory.getILoggerFactory()); + memoryAppender.start(); + } + + @BeforeEach + void clearMemoryAppender(){ + memoryAppender.reset(); + + ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ErrorManager.class.getName()); + logger.setLevel(ch.qos.logback.classic.Level.INFO); + logger.addAppender(memoryAppender); + } + + @RestController + @Slf4j + static class TestController { + + @GetMapping("/test") + String test() { + throw new ServiceException("DUMMY_CODE", "DUMMY_MESSAGE"); + } + + @GetMapping("/test/customBody") + String testCustomBody() { + throw new ServiceException("DUMMY_CODE", "DUMMY_MESSAGE", new ErrorPayloadTest("RESPONSE",0), true, null); + } + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class ErrorPayloadTest implements ServiceExceptionPayload { + private String stringCode; + private long longCode; + } + + @Test + void testSimpleException(){ + webTestClient.method(HttpMethod.GET) + .uri("/test") + .contentType(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().is5xxServerError() + .expectBody() + .json("{\"code\":\"DUMMY_CODE\",\"message\":\"DUMMY_MESSAGE\"}", false); + + ErrorManagerTest.checkStackTraceSuppressedLog(memoryAppender, "A ServiceException occurred handling request GET /test: HttpStatus 500 INTERNAL_SERVER_ERROR - DUMMY_CODE: DUMMY_MESSAGE at it.gov.pagopa.common.web.exception.ServiceExceptionHandlerTest\\$TestController.test\\(ServiceExceptionHandlerTest.java:[0-9]+\\)"); + + } + + @Test + void testCustomBodyException(){ + + webTestClient.method(HttpMethod.GET) + .uri("/test/customBody") + .contentType(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().is5xxServerError() + .expectBody() + .json("{\"stringCode\":\"RESPONSE\",\"longCode\":0}", false); + + ErrorManagerTest.checkLog(memoryAppender, + "Something went wrong handling request GET /test/customBody: HttpStatus 500 INTERNAL_SERVER_ERROR - DUMMY_CODE: DUMMY_MESSAGE", + "it.gov.pagopa.common.web.exception.ServiceException: DUMMY_MESSAGE", + "it.gov.pagopa.common.web.exception.ServiceExceptionHandlerTest$TestController.testCustomBody" + + ); + } +} diff --git a/src/test/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandlerTest.java b/src/test/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandlerTest.java new file mode 100644 index 0000000..3236568 --- /dev/null +++ b/src/test/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandlerTest.java @@ -0,0 +1,90 @@ +package it.gov.pagopa.common.web.exception; + + +import it.gov.pagopa.common.web.dto.ErrorDTO; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; + +@WebFluxTest(value = {ValidationExceptionHandlerTest.TestController.class}, excludeAutoConfiguration = SecurityAutoConfiguration.class) +@ContextConfiguration(classes = { + ValidationExceptionHandlerTest.TestController.class, + ValidationExceptionHandler.class}) +class ValidationExceptionHandlerTest { + + @Autowired + private WebTestClient webTestClient; + + + @RestController + static class TestController { + + @PutMapping("/test") + String testEndpoint(@RequestBody @Valid ValidationDTO body, @RequestHeader("data") String data) { + return "OK"; + } + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class ValidationDTO { + @NotBlank(message = "The field is mandatory!") + private String data; + } + + @Test + void testHandleValueNotValidException() { + String invalidJson = "{}"; + + webTestClient.put() + .uri("/test") + .contentType(MediaType.APPLICATION_JSON) + .header("data", "someValue") + .bodyValue(invalidJson) + .exchange() + .expectStatus().isBadRequest() + .expectBody(ErrorDTO.class) + .consumeWith(response -> { + ErrorDTO errorDTO = response.getResponseBody(); + assertThat(errorDTO).isNotNull(); + assertThat(errorDTO.getCode()).isEqualTo("INVALID_REQUEST"); + assertThat(errorDTO.getMessage()).isEqualTo("[data]: The field is mandatory!"); + }); + } + @Test + void testHandleHeaderNotValidException() { + String invalidJson = "{}"; + + webTestClient.put() + .uri("/test") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(new ValidationDTO("data")) + .exchange() + .expectStatus().isBadRequest() + .expectBody(ErrorDTO.class) + + .consumeWith(response -> { + ErrorDTO errorDTO = response.getResponseBody(); + assertThat(errorDTO).isNotNull(); + assertThat(errorDTO.getCode()).isEqualTo("INVALID_REQUEST"); + assertThat(errorDTO.getMessage()).isEqualTo("Invalid request"); + + }); + } +} diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java new file mode 100644 index 0000000..d19116e --- /dev/null +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java @@ -0,0 +1,136 @@ +package it.gov.pagopa.onboarding.citizen.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import it.gov.pagopa.onboarding.citizen.faker.CitizenConsentDTOFaker; +import it.gov.pagopa.onboarding.citizen.service.CitizenServiceImpl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; +import reactor.core.publisher.Mono; + +import java.util.List; + +@WebFluxTest(CitizenControllerImpl.class) +class CitizenControllerTest { + + @MockBean + private CitizenServiceImpl citizenService; + + @Autowired + private WebTestClient webClient; + + @Autowired + private ObjectMapper objectMapper; + + private static final String FISCAL_CODE = "fiscalCode"; + private static final String TPP_ID = "tppId"; + + + @Test + void saveCitizenConsent_Ok() { + CitizenConsentDTO citizenConsentDTO = CitizenConsentDTOFaker.mockInstance(true); + + Mockito.when(citizenService.createCitizenConsent(citizenConsentDTO)).thenReturn(Mono.just(citizenConsentDTO)); + + webClient.post() + .uri("/emd/citizen") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(citizenConsentDTO) + .exchange() + .expectStatus().isOk() + .expectBody(CitizenConsentDTO.class) + .consumeWith(response -> { + CitizenConsentDTO resultResponse = response.getResponseBody(); + Assertions.assertNotNull(resultResponse); + Assertions.assertEquals(citizenConsentDTO, resultResponse); + }); + } + + @Test + void stateUpdate_Ok() { + CitizenConsentDTO citizenConsentDTO = CitizenConsentDTOFaker.mockInstance(true); + + Mockito.when(citizenService.updateChannelState( + citizenConsentDTO.getHashedFiscalCode(), + citizenConsentDTO.getTppId(), + citizenConsentDTO.getTppState())) + .thenReturn(Mono.just(citizenConsentDTO)); + + webClient.put() + .uri("/emd/citizen/stateUpdate") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(citizenConsentDTO) + .exchange() + .expectStatus().isOk() + .expectBody(CitizenConsentDTO.class) + .consumeWith(response -> { + CitizenConsentDTO resultResponse = response.getResponseBody(); + Assertions.assertNotNull(resultResponse); + Assertions.assertEquals(citizenConsentDTO, resultResponse); + }); + } + + @Test + void getConsentStatus_Ok() { + + CitizenConsentDTO citizenConsentDTO = CitizenConsentDTOFaker.mockInstance(true); + + Mockito.when(citizenService.getConsentStatus(FISCAL_CODE, TPP_ID)) + .thenReturn(Mono.just(citizenConsentDTO)); + + webClient.get() + .uri("/emd/citizen/{fiscalCode}/{tppId}", FISCAL_CODE, TPP_ID) + .exchange() + .expectStatus().isOk() + .expectBody(CitizenConsentDTO.class) + .consumeWith(response -> { + CitizenConsentDTO resultResponse = response.getResponseBody(); + Assertions.assertNotNull(resultResponse); + Assertions.assertEquals(citizenConsentDTO, resultResponse); + }); + } + + @Test + void getCitizenConsentsEnabled_Ok() { + List citizenConsentDTOList = List.of(CitizenConsentDTOFaker.mockInstance(true)); + + Mockito.when(citizenService.getListEnabledConsents(FISCAL_CODE)) + .thenReturn(Mono.just(citizenConsentDTOList)); + + webClient.get() + .uri("/emd/citizen/list/{fiscalCode}/enabled", FISCAL_CODE) + .exchange() + .expectStatus().isOk() + .expectBodyList(CitizenConsentDTO.class) + .consumeWith(response -> { + List resultResponse = response.getResponseBody(); + Assertions.assertNotNull(resultResponse); + Assertions.assertEquals(citizenConsentDTOList.size(), resultResponse.size()); + }); + } + + @Test + void getCitizenConsents_Ok() { + List citizenConsentDTOList = List.of(CitizenConsentDTOFaker.mockInstance(true)); + + Mockito.when(citizenService.getListAllConsents(FISCAL_CODE)) + .thenReturn(Mono.just(citizenConsentDTOList)); + + webClient.get() + .uri("/emd/citizen/list/{fiscalCode}", FISCAL_CODE) + .exchange() + .expectStatus().isOk() + .expectBodyList(CitizenConsentDTO.class) + .consumeWith(response -> { + List resultResponse = response.getResponseBody(); + Assertions.assertNotNull(resultResponse); + Assertions.assertEquals(citizenConsentDTOList.size(), resultResponse.size()); + }); + } +} diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentDTOFaker.java b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentDTOFaker.java new file mode 100644 index 0000000..f913681 --- /dev/null +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentDTOFaker.java @@ -0,0 +1,22 @@ +package it.gov.pagopa.onboarding.citizen.faker; + + +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; + +import java.time.LocalDateTime; + +public class CitizenConsentDTOFaker { + + private CitizenConsentDTOFaker(){} + public static CitizenConsentDTO mockInstance(Boolean bias) { + return CitizenConsentDTO.builder() + .tppId("channelId") + .tppState(bias) + .userId("userId") + .hashedFiscalCode("hashedFiscalCode") + .creationDate(LocalDateTime.now()) + .lastUpdateDate(LocalDateTime.now()) + .build(); + + } +} diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentFaker.java b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentFaker.java new file mode 100644 index 0000000..fa233ec --- /dev/null +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentFaker.java @@ -0,0 +1,21 @@ +package it.gov.pagopa.onboarding.citizen.faker; + + +import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; + +import java.time.LocalDateTime; + +public class CitizenConsentFaker { + + private CitizenConsentFaker(){} + public static CitizenConsent mockInstance(Boolean bias) { + return CitizenConsent.builder() + .tppId("tppId") + .tppState(bias) + .userId("userId") + .hashedFiscalCode("hashedFiscalCode") + .creationDate(LocalDateTime.now()) + .lastUpdateDate(LocalDateTime.now()) + .build(); + } +} diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java new file mode 100644 index 0000000..f6ee817 --- /dev/null +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java @@ -0,0 +1,159 @@ +package it.gov.pagopa.onboarding.citizen.service; + +import it.gov.pagopa.common.utils.Utils; +import it.gov.pagopa.common.web.exception.ClientExceptionWithBody; +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import it.gov.pagopa.onboarding.citizen.dto.mapper.CitizenConsentObjectToDTOMapper; +import it.gov.pagopa.common.web.exception.EmdEncryptionException; +import it.gov.pagopa.onboarding.citizen.configuration.ExceptionMap; +import it.gov.pagopa.onboarding.citizen.faker.CitizenConsentDTOFaker; +import it.gov.pagopa.onboarding.citizen.faker.CitizenConsentFaker; +import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import it.gov.pagopa.onboarding.citizen.model.mapper.CitizenConsentDTOToObjectMapper; +import it.gov.pagopa.onboarding.citizen.repository.CitizenRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.function.Executable; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; + +@ExtendWith({SpringExtension.class, MockitoExtension.class}) +@ContextConfiguration(classes = { + CitizenServiceImpl.class, + CitizenConsentObjectToDTOMapper.class, + CitizenConsentDTOToObjectMapper.class, + ExceptionMap.class +}) +class CitizenServiceTest { + + @Autowired + CitizenServiceImpl citizenService; + + @MockBean + CitizenRepository citizenRepository; + + @Autowired + CitizenConsentDTOToObjectMapper mapperToObject; + + private static final String FISCAL_CODE = "fiscalCode"; + private static final String HASHED_FISCAL_CODE = Utils.createSHA256(FISCAL_CODE); + private static final String TPP_ID = "tppId"; + private static final boolean TPP_STATE = true; + private static final CitizenConsent CITIZEN_CONSENT = CitizenConsentFaker.mockInstance(true); + + @Test + void createCitizenConsent_Ok() { + + CitizenConsentDTO citizenConsentDTO = CitizenConsentDTOFaker.mockInstance(true); + CitizenConsent citizenConsent = mapperToObject.map(citizenConsentDTO); + + Mockito.when(citizenRepository.save(Mockito.any())) + .thenReturn(Mono.just(citizenConsent)); + + CitizenConsentDTO response = citizenService.createCitizenConsent(citizenConsentDTO).block(); + assertNotNull(response); + citizenConsentDTO.setLastUpdateDate(response.getLastUpdateDate()); + citizenConsentDTO.setCreationDate(response.getCreationDate()); + + assertEquals(citizenConsentDTO, response); + } + + @Test + void createCitizenConsent_Ko_EmdEncryptError() { + CitizenConsentDTO citizenConsentDTO = CitizenConsentDTOFaker.mockInstance(true); + + try (MockedStatic mockedStatic = Mockito.mockStatic(Utils.class)) { + mockedStatic.when(() -> Utils.createSHA256(any())) + .thenThrow(EmdEncryptionException.class); + + assertThrows(EmdEncryptionException.class, () -> citizenService.createCitizenConsent(citizenConsentDTO)); + } + } + + @Test + void updateChannelState_Ok() { + + + + Mockito.when(citizenRepository.findByHashedFiscalCodeAndTppId(HASHED_FISCAL_CODE, TPP_ID)) + .thenReturn(Mono.just(CITIZEN_CONSENT)); + Mockito.when(citizenRepository.save(Mockito.any())) + .thenReturn(Mono.just(CITIZEN_CONSENT)); + + CitizenConsentDTO response = citizenService.updateChannelState(FISCAL_CODE, TPP_ID, TPP_STATE).block(); + assertNotNull(response); + assertEquals(TPP_STATE, response.getTppState()); + } + + @Test + void updateChannelState_Ko_CitizenNotOnboarded() { + + + Mockito.when(citizenRepository.findByHashedFiscalCodeAndTppId(HASHED_FISCAL_CODE, TPP_ID)) + .thenReturn(Mono.empty()); + + Executable executable = () -> citizenService.updateChannelState(FISCAL_CODE, TPP_ID, true).block(); + ClientExceptionWithBody exception = assertThrows(ClientExceptionWithBody.class, executable); + + assertEquals("CITIZEN_NOT_ONBOARDED", exception.getMessage()); + } + + @Test + void getConsentStatus_Ok() { + + + Mockito.when(citizenRepository.findByHashedFiscalCodeAndTppId(HASHED_FISCAL_CODE, TPP_ID)) + .thenReturn(Mono.just(CITIZEN_CONSENT)); + + CitizenConsentDTO response = citizenService.getConsentStatus(FISCAL_CODE, TPP_ID).block(); + assertNotNull(response); + } + + @Test + void getConsentStatus_Ko_CitizenNotOnboarded() { + + Mockito.when(citizenRepository.findByHashedFiscalCodeAndTppId(HASHED_FISCAL_CODE, TPP_ID)) + .thenReturn(Mono.empty()); + + Executable executable = () -> citizenService.getConsentStatus(FISCAL_CODE, TPP_ID).block(); + ClientExceptionWithBody exception = assertThrows(ClientExceptionWithBody.class, executable); + + assertEquals("CITIZEN_NOT_ONBOARDED", exception.getMessage()); + } + + @Test + void getListEnabledConsents_Ok() { + + + Mockito.when(citizenRepository.findByHashedFiscalCodeAndTppStateTrue(HASHED_FISCAL_CODE)) + .thenReturn(Flux.just(CITIZEN_CONSENT)); + + List response = citizenService.getListEnabledConsents(FISCAL_CODE).block(); + assertNotNull(response); + assertEquals(1, response.size()); + } + + @Test + void getListAllConsents_Ok() { + + + Mockito.when(citizenRepository.findByHashedFiscalCode(HASHED_FISCAL_CODE)) + .thenReturn(Flux.just(CITIZEN_CONSENT)); + + List response = citizenService.getListAllConsents(FISCAL_CODE).block(); + assertNotNull(response); + assertEquals(1, response.size()); + } +} From a80e06c2985522e67fb9761dc0184bdfc8ef3f2b Mon Sep 17 00:00:00 2001 From: DanieleRanaldo <124155243+DanieleRanaldo@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:13:47 +0200 Subject: [PATCH 02/18] chore: add dockerFile (#2) --- Dockerfile | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e4b8047 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# +# Build +# +FROM maven:3.9.6-amazoncorretto-17-al2023@sha256:459be099faa25a32c06cd45ed1ef2bc9dbbf8a5414da4e72349459a1bb4d6166 AS buildtime + +WORKDIR /build +COPY . . + +RUN mvn clean package -DskipTests + +# +# Docker RUNTIME +# +FROM amazoncorretto:17-alpine3.20@sha256:1b1d0653d890ff313a1f7afadd1fd81f5ea742c9c48670d483b1bbccef98bb8b AS runtime + +RUN apk --no-cache add shadow +RUN useradd --uid 10000 runner + +VOLUME /tmp +WORKDIR /app + +COPY --from=buildtime /build/target/*.jar /app/app.jar +# The agent is enabled at runtime via JAVA_TOOL_OPTIONS. +ADD https://github.com/microsoft/ApplicationInsights-Java/releases/download/3.6.1/applicationinsights-agent-3.6.1.jar /app/applicationinsights-agent.jar + +RUN chown -R runner:runner /app + +USER 10000 + +ENTRYPOINT ["java","-jar","/app/app.jar"] From 39f4d398c3988005dd6b004f68bdae366ec8caf3 Mon Sep 17 00:00:00 2001 From: stedelia <144045955+stedelia@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:28:07 +0200 Subject: [PATCH 03/18] fix: Log refactor (#3) Co-authored-by: Vitolo-Andrea --- .../it/gov/pagopa/common/utils/Utils.java | 8 ++-- .../web/exception/EmdEncryptionException.java | 4 +- .../common/web/exception/ErrorManager.java | 20 +++------ .../exception/ServiceExceptionHandler.java | 11 +++-- .../exception/ValidationExceptionHandler.java | 40 ++++++++++-------- .../citizen/configuration/ExceptionMap.java | 26 +++++++----- ...enConstants.java => CitizenConstants.java} | 4 +- .../citizen/dto/CitizenConsentDTO.java | 1 - .../CitizenConsentObjectToDTOMapper.java | 4 +- .../citizen/model/CitizenConsent.java | 1 - .../CitizenConsentDTOToObjectMapper.java | 1 - .../citizen/service/CitizenServiceImpl.java | 22 +++++----- .../it/gov/pagopa/common/utils/UtilsTest.java | 1 + .../web/exception/ErrorManagerTest.java | 8 ++-- .../ServiceExceptionHandlerTest.java | 4 +- .../ValidationExceptionHandlerTest.java | 42 ++++++++++++++++++- .../controller/CitizenControllerTest.java | 4 +- .../citizen/faker/CitizenConsentDTOFaker.java | 3 +- .../citizen/faker/CitizenConsentFaker.java | 1 - .../citizen/service/CitizenServiceTest.java | 24 ++++------- 20 files changed, 128 insertions(+), 101 deletions(-) rename src/main/java/it/gov/pagopa/onboarding/citizen/constants/{OnboardingCitizenConstants.java => CitizenConstants.java} (90%) diff --git a/src/main/java/it/gov/pagopa/common/utils/Utils.java b/src/main/java/it/gov/pagopa/common/utils/Utils.java index 28bfd53..bc0a27c 100644 --- a/src/main/java/it/gov/pagopa/common/utils/Utils.java +++ b/src/main/java/it/gov/pagopa/common/utils/Utils.java @@ -1,4 +1,5 @@ package it.gov.pagopa.common.utils; + import it.gov.pagopa.common.web.exception.EmdEncryptionException; import lombok.extern.slf4j.Slf4j; @@ -9,7 +10,6 @@ @Slf4j public class Utils { - private Utils(){} public static String createSHA256(String fiscalCode) { try { @@ -25,14 +25,14 @@ public static String createSHA256(String fiscalCode) { } return hexString.toString(); } catch (NoSuchAlgorithmException e) { - log.error("Something went wrong creating SHA256"); + log.info("Something went wrong creating SHA256"); throw new EmdEncryptionException("Something went wrong creating SHA256",true,e); } } public static String inputSanify(String message){ if (message != null) - return message.replaceAll("[\\r\\n]", ""); - return "[EMD][WARNING] Null log"; + return message.replaceAll("[\\r\\n]", " "); + return "[EMD][WARNING] Null log"; } } diff --git a/src/main/java/it/gov/pagopa/common/web/exception/EmdEncryptionException.java b/src/main/java/it/gov/pagopa/common/web/exception/EmdEncryptionException.java index c846862..99efea0 100644 --- a/src/main/java/it/gov/pagopa/common/web/exception/EmdEncryptionException.java +++ b/src/main/java/it/gov/pagopa/common/web/exception/EmdEncryptionException.java @@ -1,12 +1,12 @@ package it.gov.pagopa.common.web.exception; -import it.gov.pagopa.common.utils.CommonConstants.ExceptionCode; +import it.gov.pagopa.common.utils.CommonConstants; public class EmdEncryptionException extends ServiceException { public EmdEncryptionException(String message, boolean printStackTrace, Throwable ex) { - this(ExceptionCode.GENERIC_ERROR, message, printStackTrace, ex); + this(CommonConstants.ExceptionCode.GENERIC_ERROR, message, printStackTrace, ex); } public EmdEncryptionException(String code, String message, boolean printStackTrace, Throwable ex) { super(code, message,null, printStackTrace, ex); diff --git a/src/main/java/it/gov/pagopa/common/web/exception/ErrorManager.java b/src/main/java/it/gov/pagopa/common/web/exception/ErrorManager.java index 7b37f7a..437672b 100644 --- a/src/main/java/it/gov/pagopa/common/web/exception/ErrorManager.java +++ b/src/main/java/it/gov/pagopa/common/web/exception/ErrorManager.java @@ -5,7 +5,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.lang.Nullable; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -23,9 +22,9 @@ public ErrorManager(@Nullable ErrorDTO defaultErrorDTO) { } @ExceptionHandler(RuntimeException.class) - protected ResponseEntity handleException(RuntimeException error, ServerHttpRequest request) { + protected ResponseEntity handleException(RuntimeException error) { - logClientException(error, request); + logClientException(error); if(error instanceof ClientExceptionNoBody clientExceptionNoBody){ return ResponseEntity.status(clientExceptionNoBody.getHttpStatus()).build(); @@ -46,14 +45,14 @@ protected ResponseEntity handleException(RuntimeException error, Serve .body(errorDTO); } } - public static void logClientException(RuntimeException error, ServerHttpRequest request) { + public static void logClientException(RuntimeException error) { Throwable unwrappedException = error.getCause() instanceof ServiceException ? error.getCause() : error; String clientExceptionMessage = ""; if(error instanceof ClientException clientException) { - clientExceptionMessage = ": HttpStatus %s - %s%s".formatted( + clientExceptionMessage = "HttpStatus %s - %s%s".formatted( clientException.getHttpStatus(), (clientException instanceof ClientExceptionWithBody clientExceptionWithBody) ? clientExceptionWithBody.getCode() + ": " : "", clientException.getMessage() @@ -61,17 +60,10 @@ public static void logClientException(RuntimeException error, ServerHttpRequest } if(!(error instanceof ClientException clientException) || clientException.isPrintStackTrace() || unwrappedException.getCause() != null){ - log.error("Something went wrong handling request {}{}", getRequestDetails(request), clientExceptionMessage, unwrappedException); + log.error("Something went wrong : {}", clientExceptionMessage, unwrappedException); } else { - log.info("A {} occurred handling request {}{} at {}", - unwrappedException.getClass().getSimpleName() , - getRequestDetails(request), - clientExceptionMessage, - unwrappedException.getStackTrace().length > 0 ? unwrappedException.getStackTrace()[0] : "UNKNOWN"); + log.info("{}",clientExceptionMessage); } } - public static String getRequestDetails(ServerHttpRequest request) { - return "%s %s".formatted(request.getMethod(), request.getURI()); - } } diff --git a/src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandler.java b/src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandler.java index 4763292..0940802 100644 --- a/src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandler.java +++ b/src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandler.java @@ -7,7 +7,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -27,11 +26,11 @@ public ServiceExceptionHandler(ErrorManager errorManager, Map handleException(ServiceException error, ServerHttpRequest request) { + protected ResponseEntity handleException(ServiceException error) { if (null != error.getPayload()) { - return handleBodyProvidedException(error, request); + return handleBodyProvidedException(error); } - return errorManager.handleException(transcodeException(error), request); + return errorManager.handleException(transcodeException(error)); } private ClientException transcodeException(ServiceException error) { @@ -45,9 +44,9 @@ private ClientException transcodeException(ServiceException error) { return new ClientExceptionWithBody(httpStatus, error.getCode(), error.getMessage(), error.isPrintStackTrace(), error); } - private ResponseEntity handleBodyProvidedException(ServiceException error, ServerHttpRequest request) { + private ResponseEntity handleBodyProvidedException(ServiceException error) { ClientException clientException = transcodeException(error); - ErrorManager.logClientException(clientException, request); + ErrorManager.logClientException(clientException); return ResponseEntity.status(clientException.getHttpStatus()) .contentType(MediaType.APPLICATION_JSON) diff --git a/src/main/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandler.java b/src/main/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandler.java index b7873ff..7450d23 100644 --- a/src/main/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandler.java +++ b/src/main/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandler.java @@ -1,11 +1,9 @@ package it.gov.pagopa.common.web.exception; - import it.gov.pagopa.common.web.dto.ErrorDTO; import lombok.extern.slf4j.Slf4j; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.lang.Nullable; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -13,6 +11,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.reactive.resource.NoResourceFoundException; +import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.MissingRequestValueException; import java.util.Optional; @@ -33,42 +32,47 @@ public ValidationExceptionHandler(@Nullable ErrorDTO templateValidationErrorDTO) @ExceptionHandler(WebExchangeBindException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorDTO handleWebExchangeBindException( - WebExchangeBindException ex, ServerHttpRequest request) { + WebExchangeBindException e) { - String message = ex.getBindingResult().getAllErrors().stream() + String message = e.getBindingResult().getAllErrors().stream() .map(error -> { String fieldName = ((FieldError) error).getField(); String errorMessage = error.getDefaultMessage(); return String.format("[%s]: %s", fieldName, errorMessage); }).collect(Collectors.joining("; ")); - log.info("A WebExchangeBindException occurred handling request {}: HttpStatus 400 - {}", - ErrorManager.getRequestDetails(request), message); - log.debug("Something went wrong while validating http request", ex); + log.info("A WebExchangeBindException occurred : HttpStatus 400 - {}", message); + log.debug("Something went wrong while validating http request", e); return new ErrorDTO(templateValidationErrorDTO.getCode(), message); } @ExceptionHandler(MissingRequestValueException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) - public ErrorDTO handleMissingRequestValueException(MissingRequestValueException e, ServerHttpRequest request) { - - log.info("A MissingRequestValueException occurred handling request {}: HttpStatus 400 - {}", - ErrorManager.getRequestDetails(request), e.getMessage()); + public ErrorDTO handleMissingRequestValueException(MissingRequestValueException e) { + log.info("A MissingRequestValueException occurred : HttpStatus 400 - Something went wrong due to a missing request value"); log.debug("Something went wrong due to a missing request value", e); - return new ErrorDTO(templateValidationErrorDTO.getCode(), templateValidationErrorDTO.getMessage()); + return new ErrorDTO(templateValidationErrorDTO.getCode(), "Something went wrong due to a missing request value"); } - @ExceptionHandler(NoResourceFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) - public ErrorDTO handleNoResourceFoundException(NoResourceFoundException e, ServerHttpRequest request) { + public ErrorDTO handleNoResourceFoundException(NoResourceFoundException e) { - log.info("A NoResourceFoundException occurred handling request {}: HttpStatus 400 - {}", - ErrorManager.getRequestDetails(request), e.getMessage()); - log.debug("Something went wrong due to a missing request value", e); + log.info("A NoResourceFoundException occurred : HttpStatus 400 - Something went wrong due to a missing static resource"); + log.debug("Something went wrong due to a missing static resource", e); + + return new ErrorDTO(templateValidationErrorDTO.getCode(), "Something went wrong due to a missing static resource"); + } + + @ExceptionHandler(MethodNotAllowedException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorDTO handleMethodNotAllowedException(MethodNotAllowedException e) { + + log.info("A MethodNotAllowedException occurred : HttpStatus 405 - Request is not supported"); + log.debug("Something went wrong due to a request not supported", e); - return new ErrorDTO(templateValidationErrorDTO.getCode(), templateValidationErrorDTO.getMessage()); + return new ErrorDTO(templateValidationErrorDTO.getCode(), "Request is not supported"); } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/configuration/ExceptionMap.java b/src/main/java/it/gov/pagopa/onboarding/citizen/configuration/ExceptionMap.java index 141f65f..61cda9e 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/configuration/ExceptionMap.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/configuration/ExceptionMap.java @@ -1,37 +1,41 @@ package it.gov.pagopa.onboarding.citizen.configuration; +import it.gov.pagopa.common.web.exception.ClientException; import it.gov.pagopa.common.web.exception.ClientExceptionWithBody; -import it.gov.pagopa.onboarding.citizen.constants.OnboardingCitizenConstants; +import it.gov.pagopa.onboarding.citizen.constants.CitizenConstants; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import java.util.HashMap; import java.util.Map; -import java.util.function.Supplier; +import java.util.function.Function; @Configuration +@Slf4j public class ExceptionMap { - private final Map> exMap = new HashMap<>(); + private final Map> exceptions = new HashMap<>(); public ExceptionMap() { - exMap.put(OnboardingCitizenConstants.ExceptionName.CITIZEN_NOT_ONBOARDED, () -> + exceptions.put(CitizenConstants.ExceptionName.CITIZEN_NOT_ONBOARDED, message -> new ClientExceptionWithBody( HttpStatus.NOT_FOUND, - OnboardingCitizenConstants.ExceptionCode.CITIZEN_NOT_ONBOARDED, - OnboardingCitizenConstants.ExceptionMessage.CITIZEN_NOT_ONBOARDED + CitizenConstants.ExceptionCode.CITIZEN_NOT_ONBOARDED, + message ) ); - } - public RuntimeException getException(String exceptionKey) { - if (exMap.containsKey(exceptionKey)) { - return exMap.get(exceptionKey).get(); + public RuntimeException throwException(String exceptionKey, String message) { + if (exceptions.containsKey(exceptionKey)) { + return exceptions.get(exceptionKey).apply(message); } else { - throw new IllegalArgumentException(String.format("Exception Name Not Found: %s", exceptionKey)); + log.error("Exception Name Not Found: {}", exceptionKey); + return new RuntimeException(); } } } + diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/constants/OnboardingCitizenConstants.java b/src/main/java/it/gov/pagopa/onboarding/citizen/constants/CitizenConstants.java similarity index 90% rename from src/main/java/it/gov/pagopa/onboarding/citizen/constants/OnboardingCitizenConstants.java rename to src/main/java/it/gov/pagopa/onboarding/citizen/constants/CitizenConstants.java index 4ec7f3f..349f395 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/constants/OnboardingCitizenConstants.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/constants/CitizenConstants.java @@ -1,6 +1,6 @@ package it.gov.pagopa.onboarding.citizen.constants; -public class OnboardingCitizenConstants { +public class CitizenConstants { public static final class ExceptionCode { public static final String CITIZEN_NOT_ONBOARDED = "CITIZEN_NOT_ONBOARDED"; @@ -23,5 +23,5 @@ private ExceptionName() {} } - private OnboardingCitizenConstants() {} + private CitizenConstants() {} } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentDTO.java b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentDTO.java index 9151702..c35fec5 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentDTO.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentDTO.java @@ -17,7 +17,6 @@ public class CitizenConsentDTO { private String hashedFiscalCode; private String tppId; private Boolean tppState; - private String userId; private LocalDateTime creationDate; private LocalDateTime lastUpdateDate; } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java index 885bf6f..fa68c7c 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java @@ -12,8 +12,8 @@ public CitizenConsentDTO map(CitizenConsent citizenConsent){ return CitizenConsentDTO.builder() .tppState(citizenConsent.getTppState()) .tppId(citizenConsent.getTppId()) - .hashedFiscalCode(citizenConsent.getHashedFiscalCode()) - .userId(citizenConsent.getUserId()) + .creationDate(citizenConsent.getCreationDate()) + .lastUpdateDate(citizenConsent.getLastUpdateDate()) .build(); } } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/model/CitizenConsent.java b/src/main/java/it/gov/pagopa/onboarding/citizen/model/CitizenConsent.java index 4c8657e..64ebd8e 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/model/CitizenConsent.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/model/CitizenConsent.java @@ -17,7 +17,6 @@ public class CitizenConsent { private String hashedFiscalCode; private String tppId; private Boolean tppState; - private String userId; private LocalDateTime creationDate; private LocalDateTime lastUpdateDate; diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/model/mapper/CitizenConsentDTOToObjectMapper.java b/src/main/java/it/gov/pagopa/onboarding/citizen/model/mapper/CitizenConsentDTOToObjectMapper.java index 195aebe..96caf6a 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/model/mapper/CitizenConsentDTOToObjectMapper.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/model/mapper/CitizenConsentDTOToObjectMapper.java @@ -13,7 +13,6 @@ public CitizenConsent map(CitizenConsentDTO citizenConsentDTO){ .tppState(true) .tppId(citizenConsentDTO.getTppId()) .hashedFiscalCode(citizenConsentDTO.getHashedFiscalCode()) - .userId(citizenConsentDTO.getUserId()) .build(); } } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java index 14e9875..51962a8 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java @@ -1,10 +1,10 @@ package it.gov.pagopa.onboarding.citizen.service; import it.gov.pagopa.common.utils.Utils; -import it.gov.pagopa.onboarding.citizen.constants.OnboardingCitizenConstants.ExceptionName; +import it.gov.pagopa.onboarding.citizen.configuration.ExceptionMap; +import it.gov.pagopa.onboarding.citizen.constants.CitizenConstants.ExceptionName; import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; import it.gov.pagopa.onboarding.citizen.dto.mapper.CitizenConsentObjectToDTOMapper; -import it.gov.pagopa.onboarding.citizen.configuration.ExceptionMap; import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; import it.gov.pagopa.onboarding.citizen.model.mapper.CitizenConsentDTOToObjectMapper; import it.gov.pagopa.onboarding.citizen.repository.CitizenRepository; @@ -35,12 +35,12 @@ public CitizenServiceImpl(CitizenRepository citizenRepository, CitizenConsentObj @Override public Mono createCitizenConsent(CitizenConsentDTO citizenConsentDTO) { - log.info("[EMD][CITIZEN][CREATE] Received message: {}",inputSanify(citizenConsentDTO.toString())); CitizenConsent citizenConsent = mapperToObject.map(citizenConsentDTO); String hashedFiscalCode = Utils.createSHA256(citizenConsent.getHashedFiscalCode()); citizenConsent.setHashedFiscalCode(hashedFiscalCode); citizenConsent.setCreationDate(LocalDateTime.now()); citizenConsent.setLastUpdateDate(LocalDateTime.now()); + log.info("[EMD-CITIZEN][CREATE] Received consent: {}",inputSanify(citizenConsent.toString())); return citizenRepository.save(citizenConsent) .map(mapperToDTO::map) .doOnSuccess(savedConsent -> log.info("[EMD][CREATE-CITIZEN-CONSENT] Created")); @@ -53,7 +53,8 @@ public Mono updateChannelState(String fiscalCode, String tppI log.info("[EMD][[CITIZEN][UPDATE-CHANNEL-STATE] Received hashedFiscalCode: {} and tppId: {} with state: {}" ,hashedFiscalCode, inputSanify(tppId), tppState); return citizenRepository.findByHashedFiscalCodeAndTppId(hashedFiscalCode, tppId) - .switchIfEmpty(Mono.error(exceptionMap.getException(ExceptionName.CITIZEN_NOT_ONBOARDED))) + .switchIfEmpty(Mono.error(exceptionMap.throwException + (ExceptionName.CITIZEN_NOT_ONBOARDED,"Citizen not founded during update state process"))) .flatMap(citizenConsent -> { citizenConsent.setTppState(tppState); citizenConsent.setLastUpdateDate(LocalDateTime.now()); @@ -66,18 +67,19 @@ public Mono updateChannelState(String fiscalCode, String tppI @Override public Mono getConsentStatus(String fiscalCode, String tppId) { String hashedFiscalCode = Utils.createSHA256(fiscalCode); - log.info("[EMD][CITIZEN][GET-CONSENT-STATUS] Received hashedFiscalCode: {} and tppId: {}",hashedFiscalCode,inputSanify(tppId)); + log.info("[EMD-CITIZEN][GET-CONSENT-STATUS] Received hashedFiscalCode: {} and tppId: {}",hashedFiscalCode,inputSanify(tppId)); return citizenRepository.findByHashedFiscalCodeAndTppId(hashedFiscalCode, tppId) - .switchIfEmpty(Mono.error(exceptionMap.getException(ExceptionName.CITIZEN_NOT_ONBOARDED))) + .switchIfEmpty(Mono.error(exceptionMap.throwException + (ExceptionName.CITIZEN_NOT_ONBOARDED,"Citizen not founded during get process "))) .map(mapperToDTO::map) - .doOnSuccess(consent -> log.info("[EMD][CITIZEN][GET-CONSENT-STATUS] Consent found:: {}",consent)); + .doOnSuccess(consent -> log.info("[EMD-CITIZEN][GET-CONSENT-STATUS] Consent found:: {}",consent)); } @Override public Mono> getListEnabledConsents(String fiscalCode) { String hashedFiscalCode = Utils.createSHA256(fiscalCode); - log.info("[EMD][CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] Received hashedFiscalCode: {}",hashedFiscalCode); + log.info("[EMD-CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] Received hashedFiscalCode: {}",hashedFiscalCode); return citizenRepository.findByHashedFiscalCodeAndTppStateTrue(hashedFiscalCode) .collectList() .map(consentList -> consentList.stream() @@ -91,13 +93,13 @@ public Mono> getListEnabledConsents(String fiscalCode) { @Override public Mono> getListAllConsents(String fiscalCode) { String hashedFiscalCode = Utils.createSHA256(fiscalCode); - log.info("[EMD][CITIZEN][FIND-ALL-CITIZEN-CONSENTS] Received hashedFiscalCode: {}",(hashedFiscalCode)); + log.info("[EMD-CITIZEN][FIND-ALL-CITIZEN-CONSENTS] Received hashedFiscalCode: {}",(hashedFiscalCode)); return citizenRepository.findByHashedFiscalCode(hashedFiscalCode) .collectList() .map(consentList -> consentList.stream() .map(mapperToDTO::map) .toList() ) - .doOnSuccess(consentList -> log.info("[EMD][CITIZEN][FIND-ALL-CITIZEN-CONSENTS] Consents found:: {}",consentList)); + .doOnSuccess(consentList -> log.info("[EMD-CITIZEN][FIND-ALL-CITIZEN-CONSENTS] Consents found:: {}",consentList)); } } diff --git a/src/test/java/it/gov/pagopa/common/utils/UtilsTest.java b/src/test/java/it/gov/pagopa/common/utils/UtilsTest.java index 3fa89a3..f2534a1 100644 --- a/src/test/java/it/gov/pagopa/common/utils/UtilsTest.java +++ b/src/test/java/it/gov/pagopa/common/utils/UtilsTest.java @@ -1,6 +1,7 @@ package it.gov.pagopa.common.utils; + import it.gov.pagopa.common.web.exception.EmdEncryptionException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/src/test/java/it/gov/pagopa/common/web/exception/ErrorManagerTest.java b/src/test/java/it/gov/pagopa/common/web/exception/ErrorManagerTest.java index 078708d..5e00799 100644 --- a/src/test/java/it/gov/pagopa/common/web/exception/ErrorManagerTest.java +++ b/src/test/java/it/gov/pagopa/common/web/exception/ErrorManagerTest.java @@ -75,7 +75,7 @@ void handleExceptionClientExceptionNoBody() { .expectStatus().isBadRequest(); checkStackTraceSuppressedLog(memoryAppender, - "A ClientExceptionNoBody occurred handling request GET /test: HttpStatus 400 BAD_REQUEST - NOTFOUND ClientExceptionNoBody at it.gov.pagopa.common.web.exception.ErrorManagerTest\\$TestController.testEndpoint\\(ErrorManagerTest.java:[0-9]+\\)"); + "HttpStatus 400 BAD_REQUEST - NOTFOUND ClientExceptionNoBody"); } @Test @@ -119,7 +119,7 @@ void handleExceptionClientExceptionTest() { .expectBody() .json(EXPECTED_GENERIC_ERROR); - checkStackTraceSuppressedLog(memoryAppender, "A ClientException occurred handling request GET /test: HttpStatus null - null at UNKNOWN"); + checkStackTraceSuppressedLog(memoryAppender, "HttpStatus null - null"); memoryAppender.reset(); Mockito.doThrow( @@ -134,7 +134,7 @@ void handleExceptionClientExceptionTest() { .expectBody() .json(EXPECTED_GENERIC_ERROR); - checkStackTraceSuppressedLog(memoryAppender, "A ClientException occurred handling request GET /test: HttpStatus 400 BAD_REQUEST - ClientException with httpStatus and message at it.gov.pagopa.common.web.exception.ErrorManagerTest\\$TestController.testEndpoint\\(ErrorManagerTest.java:[0-9]+\\)"); + checkStackTraceSuppressedLog(memoryAppender, "HttpStatus 400 BAD_REQUEST - ClientException with httpStatus and message"); memoryAppender.reset(); Mockito.doThrow(new ClientException(HttpStatus.BAD_REQUEST, @@ -150,7 +150,7 @@ void handleExceptionClientExceptionTest() { .json(EXPECTED_GENERIC_ERROR); checkLog(memoryAppender, - "Something went wrong handling request GET /test: HttpStatus 400 BAD_REQUEST - ClientException with httpStatus, message and throwable", + "Something went wrong : HttpStatus 400 BAD_REQUEST - ClientException with httpStatus, message and throwable", "it.gov.pagopa.common.web.exception.ClientException: ClientException with httpStatus, message and throwable", "it.gov.pagopa.common.web.exception.ErrorManagerTest$TestController.testEndpoint" ); diff --git a/src/test/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandlerTest.java b/src/test/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandlerTest.java index 0896c70..a37f4ae 100644 --- a/src/test/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandlerTest.java +++ b/src/test/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandlerTest.java @@ -79,7 +79,7 @@ void testSimpleException(){ .expectBody() .json("{\"code\":\"DUMMY_CODE\",\"message\":\"DUMMY_MESSAGE\"}", false); - ErrorManagerTest.checkStackTraceSuppressedLog(memoryAppender, "A ServiceException occurred handling request GET /test: HttpStatus 500 INTERNAL_SERVER_ERROR - DUMMY_CODE: DUMMY_MESSAGE at it.gov.pagopa.common.web.exception.ServiceExceptionHandlerTest\\$TestController.test\\(ServiceExceptionHandlerTest.java:[0-9]+\\)"); + ErrorManagerTest.checkStackTraceSuppressedLog(memoryAppender, "HttpStatus 500 INTERNAL_SERVER_ERROR - DUMMY_CODE: DUMMY_MESSAGE"); } @@ -95,7 +95,7 @@ void testCustomBodyException(){ .json("{\"stringCode\":\"RESPONSE\",\"longCode\":0}", false); ErrorManagerTest.checkLog(memoryAppender, - "Something went wrong handling request GET /test/customBody: HttpStatus 500 INTERNAL_SERVER_ERROR - DUMMY_CODE: DUMMY_MESSAGE", + "Something went wrong : HttpStatus 500 INTERNAL_SERVER_ERROR - DUMMY_CODE: DUMMY_MESSAGE", "it.gov.pagopa.common.web.exception.ServiceException: DUMMY_MESSAGE", "it.gov.pagopa.common.web.exception.ServiceExceptionHandlerTest$TestController.testCustomBody" diff --git a/src/test/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandlerTest.java b/src/test/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandlerTest.java index 3236568..745f8cf 100644 --- a/src/test/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandlerTest.java +++ b/src/test/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandlerTest.java @@ -68,7 +68,7 @@ void testHandleValueNotValidException() { }); } @Test - void testHandleHeaderNotValidException() { + void testHandleMissingRequestValueException() { String invalidJson = "{}"; webTestClient.put() @@ -83,7 +83,45 @@ void testHandleHeaderNotValidException() { ErrorDTO errorDTO = response.getResponseBody(); assertThat(errorDTO).isNotNull(); assertThat(errorDTO.getCode()).isEqualTo("INVALID_REQUEST"); - assertThat(errorDTO.getMessage()).isEqualTo("Invalid request"); + assertThat(errorDTO.getMessage()).isEqualTo("Something went wrong due to a missing request value"); + + }); + } + + @Test + void testHandleNoResourceFoundException() { + String invalidJson = "{}"; + + webTestClient.put() + .uri("/wrongPath") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(new ValidationDTO("data")) + .exchange() + .expectStatus().isNotFound() + .expectBody(ErrorDTO.class) + + .consumeWith(response -> { + ErrorDTO errorDTO = response.getResponseBody(); + assertThat(errorDTO).isNotNull(); + assertThat(errorDTO.getCode()).isEqualTo("INVALID_REQUEST"); + assertThat(errorDTO.getMessage()).isEqualTo("Something went wrong due to a missing static resource"); + + }); + } + + @Test + void testHandleMethodNotAllowedException() { + + webTestClient.get() + .uri("/test") + .exchange() + .expectStatus().is4xxClientError() + .expectBody(ErrorDTO.class) + .consumeWith(response -> { + ErrorDTO errorDTO = response.getResponseBody(); + assertThat(errorDTO).isNotNull(); + assertThat(errorDTO.getCode()).isEqualTo("INVALID_REQUEST"); + assertThat(errorDTO.getMessage()).isEqualTo("Request is not supported"); }); } diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java index d19116e..f49c259 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java @@ -1,6 +1,5 @@ package it.gov.pagopa.onboarding.citizen.controller; -import com.fasterxml.jackson.databind.ObjectMapper; import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; import it.gov.pagopa.onboarding.citizen.faker.CitizenConsentDTOFaker; import it.gov.pagopa.onboarding.citizen.service.CitizenServiceImpl; @@ -25,8 +24,7 @@ class CitizenControllerTest { @Autowired private WebTestClient webClient; - @Autowired - private ObjectMapper objectMapper; + private static final String FISCAL_CODE = "fiscalCode"; private static final String TPP_ID = "tppId"; diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentDTOFaker.java b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentDTOFaker.java index f913681..2c639cc 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentDTOFaker.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentDTOFaker.java @@ -10,9 +10,8 @@ public class CitizenConsentDTOFaker { private CitizenConsentDTOFaker(){} public static CitizenConsentDTO mockInstance(Boolean bias) { return CitizenConsentDTO.builder() - .tppId("channelId") + .tppId("tppId") .tppState(bias) - .userId("userId") .hashedFiscalCode("hashedFiscalCode") .creationDate(LocalDateTime.now()) .lastUpdateDate(LocalDateTime.now()) diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentFaker.java b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentFaker.java index fa233ec..195afad 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentFaker.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentFaker.java @@ -12,7 +12,6 @@ public static CitizenConsent mockInstance(Boolean bias) { return CitizenConsent.builder() .tppId("tppId") .tppState(bias) - .userId("userId") .hashedFiscalCode("hashedFiscalCode") .creationDate(LocalDateTime.now()) .lastUpdateDate(LocalDateTime.now()) diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java index f6ee817..ad99409 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java @@ -45,27 +45,24 @@ class CitizenServiceTest { CitizenRepository citizenRepository; @Autowired - CitizenConsentDTOToObjectMapper mapperToObject; + CitizenConsentObjectToDTOMapper dtoMapper; private static final String FISCAL_CODE = "fiscalCode"; private static final String HASHED_FISCAL_CODE = Utils.createSHA256(FISCAL_CODE); private static final String TPP_ID = "tppId"; private static final boolean TPP_STATE = true; private static final CitizenConsent CITIZEN_CONSENT = CitizenConsentFaker.mockInstance(true); - + private static final CitizenConsentDTO CITIZEN_CONSENT_DTO = CitizenConsentDTOFaker.mockInstance(true); @Test void createCitizenConsent_Ok() { - CitizenConsentDTO citizenConsentDTO = CitizenConsentDTOFaker.mockInstance(true); - CitizenConsent citizenConsent = mapperToObject.map(citizenConsentDTO); + CitizenConsentDTO citizenConsentDTO = dtoMapper.map(CITIZEN_CONSENT); Mockito.when(citizenRepository.save(Mockito.any())) - .thenReturn(Mono.just(citizenConsent)); + .thenReturn(Mono.just(CITIZEN_CONSENT)); - CitizenConsentDTO response = citizenService.createCitizenConsent(citizenConsentDTO).block(); + CitizenConsentDTO response = citizenService.createCitizenConsent(CITIZEN_CONSENT_DTO).block(); assertNotNull(response); - citizenConsentDTO.setLastUpdateDate(response.getLastUpdateDate()); - citizenConsentDTO.setCreationDate(response.getCreationDate()); assertEquals(citizenConsentDTO, response); } @@ -85,7 +82,6 @@ void createCitizenConsent_Ko_EmdEncryptError() { @Test void updateChannelState_Ok() { - Mockito.when(citizenRepository.findByHashedFiscalCodeAndTppId(HASHED_FISCAL_CODE, TPP_ID)) .thenReturn(Mono.just(CITIZEN_CONSENT)); @@ -107,12 +103,12 @@ void updateChannelState_Ko_CitizenNotOnboarded() { Executable executable = () -> citizenService.updateChannelState(FISCAL_CODE, TPP_ID, true).block(); ClientExceptionWithBody exception = assertThrows(ClientExceptionWithBody.class, executable); - assertEquals("CITIZEN_NOT_ONBOARDED", exception.getMessage()); + assertEquals("Citizen not founded during update state process", exception.getMessage()); } @Test void getConsentStatus_Ok() { - + Mockito.when(citizenRepository.findByHashedFiscalCodeAndTppId(HASHED_FISCAL_CODE, TPP_ID)) .thenReturn(Mono.just(CITIZEN_CONSENT)); @@ -130,12 +126,12 @@ void getConsentStatus_Ko_CitizenNotOnboarded() { Executable executable = () -> citizenService.getConsentStatus(FISCAL_CODE, TPP_ID).block(); ClientExceptionWithBody exception = assertThrows(ClientExceptionWithBody.class, executable); - assertEquals("CITIZEN_NOT_ONBOARDED", exception.getMessage()); + assertEquals("Citizen not founded during get process ", exception.getMessage()); } @Test void getListEnabledConsents_Ok() { - + Mockito.when(citizenRepository.findByHashedFiscalCodeAndTppStateTrue(HASHED_FISCAL_CODE)) .thenReturn(Flux.just(CITIZEN_CONSENT)); @@ -147,8 +143,6 @@ void getListEnabledConsents_Ok() { @Test void getListAllConsents_Ok() { - - Mockito.when(citizenRepository.findByHashedFiscalCode(HASHED_FISCAL_CODE)) .thenReturn(Flux.just(CITIZEN_CONSENT)); From 299ef7f50b5c7d7df9b539b75e2d2200140c400d Mon Sep 17 00:00:00 2001 From: stefanodel Date: Tue, 22 Oct 2024 16:19:18 +0200 Subject: [PATCH 04/18] fix: replace server port --- src/main/resources/application.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9dc143c..ad8085a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -22,8 +22,6 @@ spring: maxConnectionIdleTimeMS: ${MONGODB_CONNECTIONPOOL_MAX_CONNECTION_IDLE_MS:120000} maxConnecting: ${MONGODB_CONNECTIONPOOL_MAX_CONNECTING:2} -server: - port: ${CITIZEN_PORT:8084} management: health: From 1d80e4c44af9cdc4327cbfbcc33727aa992f62d9 Mon Sep 17 00:00:00 2001 From: Vitolo-Andrea <157486351+Vitolo-Andrea@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:11:38 +0200 Subject: [PATCH 05/18] Mapper fix (#4) --- .../CitizenConsentObjectToDTOMapper.java | 1 + .../citizen/service/CitizenServiceImpl.java | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java index fa68c7c..022d91d 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java @@ -10,6 +10,7 @@ public class CitizenConsentObjectToDTOMapper { public CitizenConsentDTO map(CitizenConsent citizenConsent){ return CitizenConsentDTO.builder() + .hashedFiscalCode(citizenConsent.getHashedFiscalCode()) .tppState(citizenConsent.getTppState()) .tppId(citizenConsent.getTppId()) .creationDate(citizenConsent.getCreationDate()) diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java index 51962a8..35bd2b1 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java @@ -32,7 +32,6 @@ public CitizenServiceImpl(CitizenRepository citizenRepository, CitizenConsentObj this.mapperToObject = mapperToObject; this.exceptionMap = exceptionMap; } - @Override public Mono createCitizenConsent(CitizenConsentDTO citizenConsentDTO) { CitizenConsent citizenConsent = mapperToObject.map(citizenConsentDTO); @@ -41,10 +40,16 @@ public Mono createCitizenConsent(CitizenConsentDTO citizenCon citizenConsent.setCreationDate(LocalDateTime.now()); citizenConsent.setLastUpdateDate(LocalDateTime.now()); log.info("[EMD-CITIZEN][CREATE] Received consent: {}",inputSanify(citizenConsent.toString())); - return citizenRepository.save(citizenConsent) - .map(mapperToDTO::map) - .doOnSuccess(savedConsent -> log.info("[EMD][CREATE-CITIZEN-CONSENT] Created")); - + return citizenRepository.findById(hashedFiscalCode) + .flatMap(existingConsent -> { + log.info("[EMD][CREATE-CITIZEN-CONSENT] Citizen consent already exists"); + return Mono.just(mapperToDTO.map(existingConsent)); + }) + .switchIfEmpty( + citizenRepository.save(citizenConsent) + .doOnSuccess(savedConsent -> log.info("[EMD][CREATE-CITIZEN-CONSENT] Created new citizen consent")) + .map(mapperToDTO::map) + ); } @Override @@ -54,7 +59,7 @@ public Mono updateChannelState(String fiscalCode, String tppI ,hashedFiscalCode, inputSanify(tppId), tppState); return citizenRepository.findByHashedFiscalCodeAndTppId(hashedFiscalCode, tppId) .switchIfEmpty(Mono.error(exceptionMap.throwException - (ExceptionName.CITIZEN_NOT_ONBOARDED,"Citizen not founded during update state process"))) + (ExceptionName.CITIZEN_NOT_ONBOARDED,"Citizen consent not founded during update state process"))) .flatMap(citizenConsent -> { citizenConsent.setTppState(tppState); citizenConsent.setLastUpdateDate(LocalDateTime.now()); @@ -70,9 +75,9 @@ public Mono getConsentStatus(String fiscalCode, String tppId) log.info("[EMD-CITIZEN][GET-CONSENT-STATUS] Received hashedFiscalCode: {} and tppId: {}",hashedFiscalCode,inputSanify(tppId)); return citizenRepository.findByHashedFiscalCodeAndTppId(hashedFiscalCode, tppId) .switchIfEmpty(Mono.error(exceptionMap.throwException - (ExceptionName.CITIZEN_NOT_ONBOARDED,"Citizen not founded during get process "))) + (ExceptionName.CITIZEN_NOT_ONBOARDED,"Citizen consent not founded during get process "))) .map(mapperToDTO::map) - .doOnSuccess(consent -> log.info("[EMD-CITIZEN][GET-CONSENT-STATUS] Consent found:: {}",consent)); + .doOnSuccess(consent -> log.info("[EMD-CITIZEN][GET-CONSENT-STATUS] Consent consent found:: {}",consent)); } From 7d693e687b649a212998549bc2d3b35b30aed7fc Mon Sep 17 00:00:00 2001 From: Vitolo-Andrea <157486351+Vitolo-Andrea@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:35:12 +0200 Subject: [PATCH 06/18] Mapper fix (#5) --- .../citizen/service/CitizenServiceImpl.java | 2 +- .../citizen/service/CitizenServiceTest.java | 25 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java index 35bd2b1..feb0baf 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java @@ -48,7 +48,7 @@ public Mono createCitizenConsent(CitizenConsentDTO citizenCon .switchIfEmpty( citizenRepository.save(citizenConsent) .doOnSuccess(savedConsent -> log.info("[EMD][CREATE-CITIZEN-CONSENT] Created new citizen consent")) - .map(mapperToDTO::map) + .flatMap(savedConsent -> Mono.just(mapperToDTO.map(savedConsent))) // Map the saved consent ); } diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java index ad99409..5f3b54e 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java @@ -28,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; @ExtendWith({SpringExtension.class, MockitoExtension.class}) @ContextConfiguration(classes = { @@ -61,6 +62,26 @@ void createCitizenConsent_Ok() { Mockito.when(citizenRepository.save(Mockito.any())) .thenReturn(Mono.just(CITIZEN_CONSENT)); + Mockito.when(citizenRepository.findById(anyString())) + .thenReturn(Mono.empty()); + + CitizenConsentDTO response = citizenService.createCitizenConsent(CITIZEN_CONSENT_DTO).block(); + assertNotNull(response); + + assertEquals(citizenConsentDTO, response); + } + + @Test + void createCitizenConsent_AlreadyExists() { + + CitizenConsentDTO citizenConsentDTO = dtoMapper.map(CITIZEN_CONSENT); + + Mockito.when(citizenRepository.save(Mockito.any())) + .thenReturn(Mono.empty()); + + Mockito.when(citizenRepository.findById(anyString())) + .thenReturn(Mono.just(CITIZEN_CONSENT)); + CitizenConsentDTO response = citizenService.createCitizenConsent(CITIZEN_CONSENT_DTO).block(); assertNotNull(response); @@ -103,7 +124,7 @@ void updateChannelState_Ko_CitizenNotOnboarded() { Executable executable = () -> citizenService.updateChannelState(FISCAL_CODE, TPP_ID, true).block(); ClientExceptionWithBody exception = assertThrows(ClientExceptionWithBody.class, executable); - assertEquals("Citizen not founded during update state process", exception.getMessage()); + assertEquals("Citizen consent not founded during update state process", exception.getMessage()); } @Test @@ -126,7 +147,7 @@ void getConsentStatus_Ko_CitizenNotOnboarded() { Executable executable = () -> citizenService.getConsentStatus(FISCAL_CODE, TPP_ID).block(); ClientExceptionWithBody exception = assertThrows(ClientExceptionWithBody.class, executable); - assertEquals("Citizen not founded during get process ", exception.getMessage()); + assertEquals("Citizen consent not founded during get process ", exception.getMessage()); } @Test From 3aed12c42ac11548544c9b8b975c1282aabaee4e Mon Sep 17 00:00:00 2001 From: Vitolo-Andrea <157486351+Vitolo-Andrea@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:43:51 +0200 Subject: [PATCH 07/18] Mapper fix (#6) --- .../pagopa/onboarding/citizen/service/CitizenServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java index feb0baf..ddcc279 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java @@ -40,7 +40,7 @@ public Mono createCitizenConsent(CitizenConsentDTO citizenCon citizenConsent.setCreationDate(LocalDateTime.now()); citizenConsent.setLastUpdateDate(LocalDateTime.now()); log.info("[EMD-CITIZEN][CREATE] Received consent: {}",inputSanify(citizenConsent.toString())); - return citizenRepository.findById(hashedFiscalCode) + return citizenRepository.findByHashedFiscalCodeAndTppId(hashedFiscalCode,citizenConsent.getTppId()) .flatMap(existingConsent -> { log.info("[EMD][CREATE-CITIZEN-CONSENT] Citizen consent already exists"); return Mono.just(mapperToDTO.map(existingConsent)); @@ -48,7 +48,7 @@ public Mono createCitizenConsent(CitizenConsentDTO citizenCon .switchIfEmpty( citizenRepository.save(citizenConsent) .doOnSuccess(savedConsent -> log.info("[EMD][CREATE-CITIZEN-CONSENT] Created new citizen consent")) - .flatMap(savedConsent -> Mono.just(mapperToDTO.map(savedConsent))) // Map the saved consent + .flatMap(savedConsent -> Mono.just(mapperToDTO.map(savedConsent))) ); } From 63157c2dcee739abda8f457e4b037cea40e217a7 Mon Sep 17 00:00:00 2001 From: stedelia <144045955+stedelia@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:10:42 +0100 Subject: [PATCH 08/18] feat: update Citizen Consents structure (#7) Co-authored-by: DanieleRanaldo Co-authored-by: Vitolo-Andrea --- .spectral.yaml | 19 + pom.xml | 10 + .../citizen/configuration/ExceptionMap.java | 8 + .../citizen/connector/tpp/TppConnector.java | 8 + .../connector/tpp/TppConnectorImpl.java | 27 + .../citizen/constants/CitizenConstants.java | 13 + .../citizen/controller/CitizenController.java | 32 +- .../controller/CitizenControllerImpl.java | 37 +- .../citizen/dto/CitizenConsentDTO.java | 24 +- .../dto/CitizenConsentStateUpdateDTO.java | 16 + .../onboarding/citizen/dto/Contact.java | 15 + .../pagopa/onboarding/citizen/dto/TppDTO.java | 26 + .../CitizenConsentObjectToDTOMapper.java | 21 +- .../citizen/enums/AuthenticationType.java | 16 + .../citizen/model/CitizenConsent.java | 10 +- .../citizen/model/ConsentDetails.java | 15 + .../CitizenConsentDTOToObjectMapper.java | 19 +- .../citizen/repository/CitizenRepository.java | 8 +- .../repository/CitizenSpecificRepository.java | 10 + .../CitizenSpecificRepositoryImpl.java | 39 + .../citizen/service/BloomFilterService.java | 6 + .../service/BloomFilterServiceImpl.java | 52 + .../citizen/service/CitizenService.java | 6 +- .../citizen/service/CitizenServiceImpl.java | 115 +- .../CitizenConsentValidationService.java | 13 + .../CitizenConsentValidationServiceImpl.java | 76 + src/main/resources/META-INF/openapi.yaml | 1306 +++++++++++++++++ src/main/resources/application.yml | 3 + .../connector/tpp/TppConnectorImplTest.java | 58 + .../controller/CitizenControllerTest.java | 88 +- .../citizen/faker/CitizenConsentDTOFaker.java | 19 +- .../citizen/faker/CitizenConsentFaker.java | 23 +- .../CitizenConsentStateUpdateDTOFaker.java | 16 + .../onboarding/citizen/faker/TppDTOFaker.java | 12 + .../CitizenSpecificRepositoryImplTest.java | 110 ++ .../service/BloomFilterServiceTest.java | 52 + .../citizen/service/CitizenServiceTest.java | 184 ++- ...tizenConsentValidationServiceImplTest.java | 201 +++ 38 files changed, 2519 insertions(+), 194 deletions(-) create mode 100644 .spectral.yaml create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/connector/tpp/TppConnector.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/connector/tpp/TppConnectorImpl.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentStateUpdateDTO.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/dto/Contact.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/dto/TppDTO.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/enums/AuthenticationType.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/model/ConsentDetails.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepository.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/service/BloomFilterService.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/service/BloomFilterServiceImpl.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationService.java create mode 100644 src/main/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImpl.java create mode 100644 src/main/resources/META-INF/openapi.yaml create mode 100644 src/test/java/it/gov/pagopa/onboarding/citizen/connector/tpp/TppConnectorImplTest.java create mode 100644 src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentStateUpdateDTOFaker.java create mode 100644 src/test/java/it/gov/pagopa/onboarding/citizen/faker/TppDTOFaker.java create mode 100644 src/test/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImplTest.java create mode 100644 src/test/java/it/gov/pagopa/onboarding/citizen/service/BloomFilterServiceTest.java create mode 100644 src/test/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImplTest.java diff --git a/.spectral.yaml b/.spectral.yaml new file mode 100644 index 0000000..b950826 --- /dev/null +++ b/.spectral.yaml @@ -0,0 +1,19 @@ +extends: + - "spectral:oas" + - "spectral:asyncapi" + - "https://unpkg.com/@stoplight/spectral-owasp-ruleset/dist/ruleset.mjs" +overrides: + - files: + - "src/main/resources/META-INF/openapi.yaml#/paths/~1token/post/security" + rules: + owasp:api2:2023-write-restricted: "off" + - files: + - "src/main/resources/META-INF/openapi.yaml#/paths/~1.well-known~1jwks.json/get/security" + - "src/main/resources/META-INF/openapi.yaml#/paths/~1.well-known~1openid-configuration/get/security" + rules: + owasp:api2:2023-read-restricted: "off" + - files: + - "src/main/resources/META-INF/openapi.yaml" + rules: + owasp:api3:2023-no-additionalProperties: "off" + owasp:api3:2023-constrained-additionalProperties: "off" \ No newline at end of file diff --git a/pom.xml b/pom.xml index 45deb9c..a306984 100644 --- a/pom.xml +++ b/pom.xml @@ -78,6 +78,16 @@ reactor-test test + + com.squareup.okhttp3 + okhttp + test + + + com.squareup.okhttp3 + mockwebserver + test + diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/configuration/ExceptionMap.java b/src/main/java/it/gov/pagopa/onboarding/citizen/configuration/ExceptionMap.java index 61cda9e..1187e1a 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/configuration/ExceptionMap.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/configuration/ExceptionMap.java @@ -26,6 +26,14 @@ public ExceptionMap() { message ) ); + + exceptions.put(CitizenConstants.ExceptionName.TPP_NOT_FOUND, message -> + new ClientExceptionWithBody( + HttpStatus.NOT_FOUND, + CitizenConstants.ExceptionCode.TPP_NOT_FOUND, + message + ) + ); } public RuntimeException throwException(String exceptionKey, String message) { diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/connector/tpp/TppConnector.java b/src/main/java/it/gov/pagopa/onboarding/citizen/connector/tpp/TppConnector.java new file mode 100644 index 0000000..a712bf1 --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/connector/tpp/TppConnector.java @@ -0,0 +1,8 @@ +package it.gov.pagopa.onboarding.citizen.connector.tpp; + +import it.gov.pagopa.onboarding.citizen.dto.TppDTO; +import reactor.core.publisher.Mono; + +public interface TppConnector { + Mono get(String tppId); +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/connector/tpp/TppConnectorImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/connector/tpp/TppConnectorImpl.java new file mode 100644 index 0000000..d3f09d7 --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/connector/tpp/TppConnectorImpl.java @@ -0,0 +1,27 @@ +package it.gov.pagopa.onboarding.citizen.connector.tpp; + +import it.gov.pagopa.onboarding.citizen.dto.TppDTO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@Service +public class TppConnectorImpl implements TppConnector { + private final WebClient webClient; + + public TppConnectorImpl(WebClient.Builder webClientBuilder, + @Value("${rest-client.tpp.baseUrl}") String baseUrl) { + this.webClient = webClientBuilder.baseUrl(baseUrl).build(); + } + + @Override + public Mono get(String tppId) { + return webClient.get() + .uri("/emd/tpp/" + tppId) + .retrieve() + .bodyToMono(new ParameterizedTypeReference<>() { + }); + } +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/constants/CitizenConstants.java b/src/main/java/it/gov/pagopa/onboarding/citizen/constants/CitizenConstants.java index 349f395..647e90f 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/constants/CitizenConstants.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/constants/CitizenConstants.java @@ -5,6 +5,8 @@ public static final class ExceptionCode { public static final String CITIZEN_NOT_ONBOARDED = "CITIZEN_NOT_ONBOARDED"; public static final String GENERIC_ERROR = "GENERIC_ERROR"; + public static final String TPP_NOT_FOUND = "TPP_NOT_FOUND"; + private ExceptionCode() {} } @@ -12,6 +14,8 @@ public static final class ExceptionMessage { public static final String CITIZEN_NOT_ONBOARDED = "CITIZEN_NOT_ONBOARDED"; public static final String GENERIC_ERROR = "GENERIC_ERROR"; + public static final String TPP_NOT_FOUND = "TPP does not exist or is not active"; + private ExceptionMessage() {} } @@ -19,9 +23,18 @@ public static final class ExceptionName { public static final String CITIZEN_NOT_ONBOARDED = "CITIZEN_NOT_ONBOARDED"; public static final String GENERIC_ERROR = "GENERIC_ERROR"; + public static final String TPP_NOT_FOUND = "TPP_NOT_FOUND"; + private ExceptionName() {} } + public static final class ValidationRegex { + + public static final String FISCAL_CODE_STRUCTURE_REGEX = "(^([A-Za-z]{6}[0-9lmnpqrstuvLMNPQRSTUV]{2}[abcdehlmprstABCDEHLMPRST][0-9lmnpqrstuvLMNPQRSTUV]{2}[A-Za-z][0-9lmnpqrstuvLMNPQRSTUV]{3}[A-Za-z])$)|(^(\\d{11})$)"; + + private ValidationRegex() {} + } + private CitizenConstants() {} } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenController.java b/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenController.java index 505f2a5..34c7d80 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenController.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenController.java @@ -1,14 +1,18 @@ package it.gov.pagopa.onboarding.citizen.controller; import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentStateUpdateDTO; import jakarta.validation.Valid; +import jakarta.validation.constraints.Pattern; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Mono; import java.util.List; +import static it.gov.pagopa.onboarding.citizen.constants.CitizenConstants.ValidationRegex.FISCAL_CODE_STRUCTURE_REGEX; + @RequestMapping("/emd/citizen") public interface CitizenController { @@ -16,15 +20,6 @@ public interface CitizenController { @PostMapping("") Mono> saveCitizenConsent(@Valid @RequestBody CitizenConsentDTO citizenConsentDTO); - /** - * Update the state of a citizen's consent. - * - * @param citizenConsentDTO contains the hashedFiscalCode, channelId, and channelState to update - * @return the updated citizen consents - */ - @PutMapping("/stateUpdate") - Mono> stateUpdate(@Valid @RequestBody CitizenConsentDTO citizenConsentDTO); - /** * Get the consent status for a specific citizen and channel. * @@ -33,7 +28,16 @@ public interface CitizenController { * @return the citizen consent status */ @GetMapping("/{fiscalCode}/{tppId}") - Mono> getConsentStatus(@PathVariable String fiscalCode, @PathVariable String tppId); + Mono> getConsentStatus(@PathVariable @Pattern(regexp = FISCAL_CODE_STRUCTURE_REGEX, message = "Invalid fiscal code format") String fiscalCode, @PathVariable String tppId); + + /** + * Update the state of a citizen's consent. + * + * @param citizenConsentStateUpdateDTO contains the hashedFiscalCode, channelId, and channelState to update + * @return the updated citizen consents + */ + @PutMapping("/stateUpdate") + Mono> stateUpdate(@Valid @RequestBody CitizenConsentStateUpdateDTO citizenConsentStateUpdateDTO); /** * List all channels with enabled consents for a specific citizen. @@ -42,7 +46,7 @@ public interface CitizenController { * @return a list of channels with enabled consents */ @GetMapping("/list/{fiscalCode}/enabled") - Mono>> getCitizenConsentsEnabled(@PathVariable String fiscalCode); + Mono>> getTppEnabledList(@PathVariable @Pattern(regexp = FISCAL_CODE_STRUCTURE_REGEX, message = "Invalid fiscal code format") String fiscalCode); /** * List all channels and their consent status for a specific citizen. @@ -51,6 +55,10 @@ public interface CitizenController { * @return a list of all channels with their consent statuses */ @GetMapping("/list/{fiscalCode}") - Mono>> getCitizenConsents(@PathVariable String fiscalCode); + Mono> get(@PathVariable @Pattern(regexp = FISCAL_CODE_STRUCTURE_REGEX, message = "Invalid fiscal code format") String fiscalCode); + + @GetMapping("/filter/{fiscalCode}") + Mono> getAllFiscalCode(@PathVariable @Pattern(regexp = FISCAL_CODE_STRUCTURE_REGEX, message = "Invalid fiscal code format") String fiscalCode); + } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerImpl.java index 668325d..18c3d33 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerImpl.java @@ -1,8 +1,11 @@ package it.gov.pagopa.onboarding.citizen.controller; import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentStateUpdateDTO; +import it.gov.pagopa.onboarding.citizen.service.BloomFilterServiceImpl; import it.gov.pagopa.onboarding.citizen.service.CitizenServiceImpl; import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; @@ -12,9 +15,12 @@ @RestController public class CitizenControllerImpl implements CitizenController { + private final BloomFilterServiceImpl bloomFilterService; + private final CitizenServiceImpl citizenService; - public CitizenControllerImpl(CitizenServiceImpl citizenService) { + public CitizenControllerImpl(BloomFilterServiceImpl bloomFilterService, CitizenServiceImpl citizenService) { + this.bloomFilterService = bloomFilterService; this.citizenService = citizenService; } @@ -25,11 +31,11 @@ public Mono> saveCitizenConsent(@Valid Citizen } @Override - public Mono> stateUpdate(@Valid CitizenConsentDTO citizenConsentDTO) { - return citizenService.updateChannelState( - citizenConsentDTO.getHashedFiscalCode(), //at this stage the fiscalCode has not yet been hashed - citizenConsentDTO.getTppId(), - citizenConsentDTO.getTppState()) + public Mono> stateUpdate(@Valid CitizenConsentStateUpdateDTO citizenConsentStateUpdateDTO) { + return citizenService.updateTppState( + citizenConsentStateUpdateDTO.getFiscalCode(), + citizenConsentStateUpdateDTO.getTppId(), + citizenConsentStateUpdateDTO.getTppState()) .map(ResponseEntity::ok); } @@ -40,14 +46,25 @@ public Mono> getConsentStatus(String fiscalCod } @Override - public Mono>> getCitizenConsentsEnabled(String fiscalCode) { - return citizenService.getListEnabledConsents(fiscalCode) + public Mono>> getTppEnabledList(String fiscalCode) { + return citizenService.getTppEnabledList(fiscalCode) .map(ResponseEntity::ok); } @Override - public Mono>> getCitizenConsents(String fiscalCode) { - return citizenService.getListAllConsents(fiscalCode) + public Mono> get(String fiscalCode) { + return citizenService.get(fiscalCode) .map(ResponseEntity::ok); } + + @Override + public Mono> getAllFiscalCode(String fiscalCode) { + return Mono.fromCallable(() -> + bloomFilterService.mightContain(fiscalCode) ? + ResponseEntity.ok("OK") : + ResponseEntity.status(HttpStatus.ACCEPTED).body("NO CHANNELS ENABLED") + ); + + } + } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentDTO.java b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentDTO.java index c35fec5..3ed1f7a 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentDTO.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentDTO.java @@ -1,12 +1,17 @@ package it.gov.pagopa.onboarding.citizen.dto; import com.fasterxml.jackson.annotation.JsonAlias; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import java.time.LocalDateTime; +import java.util.Map; + +import static it.gov.pagopa.onboarding.citizen.constants.CitizenConstants.ValidationRegex.FISCAL_CODE_STRUCTURE_REGEX; @Data @SuperBuilder @@ -14,9 +19,18 @@ @NoArgsConstructor public class CitizenConsentDTO { @JsonAlias("fiscalCode") - private String hashedFiscalCode; - private String tppId; - private Boolean tppState; - private LocalDateTime creationDate; - private LocalDateTime lastUpdateDate; + @NotBlank(message = "Fiscal Code must not be blank") + @Pattern(regexp = FISCAL_CODE_STRUCTURE_REGEX, + message = "Fiscal Code must be 11 digits or up to 16 alphanumeric characters") + private String fiscalCode; + private Map consents; + + @Data + @SuperBuilder + @NoArgsConstructor + @AllArgsConstructor + public static class ConsentDTO { + private Boolean tppState; + private LocalDateTime tcDate; + } } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentStateUpdateDTO.java b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentStateUpdateDTO.java new file mode 100644 index 0000000..edb0f7a --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/CitizenConsentStateUpdateDTO.java @@ -0,0 +1,16 @@ +package it.gov.pagopa.onboarding.citizen.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class CitizenConsentStateUpdateDTO { + private String fiscalCode; + private String tppId; + private Boolean tppState; +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/Contact.java b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/Contact.java new file mode 100644 index 0000000..fa6c88d --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/Contact.java @@ -0,0 +1,15 @@ +package it.gov.pagopa.onboarding.citizen.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Contact { + + private String name; + private String number; + private String email; +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/TppDTO.java b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/TppDTO.java new file mode 100644 index 0000000..bc22e8b --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/TppDTO.java @@ -0,0 +1,26 @@ +package it.gov.pagopa.onboarding.citizen.dto; + + +import it.gov.pagopa.onboarding.citizen.enums.AuthenticationType; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +public class TppDTO { + @NotNull + private String tppId; + private String entityId; + private String businessName; + private String messageUrl; + private String authenticationUrl; + private AuthenticationType authenticationType; + private Contact contact; + private Boolean state; + + + +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java index 022d91d..9ba6a1a 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/dto/mapper/CitizenConsentObjectToDTOMapper.java @@ -1,20 +1,27 @@ package it.gov.pagopa.onboarding.citizen.dto.mapper; - import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO.ConsentDTO; import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; import org.springframework.stereotype.Service; +import java.util.HashMap; +import java.util.Map; + @Service public class CitizenConsentObjectToDTOMapper { - public CitizenConsentDTO map(CitizenConsent citizenConsent){ + public CitizenConsentDTO map(CitizenConsent citizenConsent) { + Map consentsDTO = new HashMap<>(); + + citizenConsent.getConsents().forEach((tppId, consentDetails) -> consentsDTO.put(tppId, ConsentDTO.builder() + .tppState(consentDetails.getTppState()) + .tcDate(consentDetails.getTcDate()) + .build())); + return CitizenConsentDTO.builder() - .hashedFiscalCode(citizenConsent.getHashedFiscalCode()) - .tppState(citizenConsent.getTppState()) - .tppId(citizenConsent.getTppId()) - .creationDate(citizenConsent.getCreationDate()) - .lastUpdateDate(citizenConsent.getLastUpdateDate()) + .fiscalCode(citizenConsent.getFiscalCode()) + .consents(consentsDTO) .build(); } } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/enums/AuthenticationType.java b/src/main/java/it/gov/pagopa/onboarding/citizen/enums/AuthenticationType.java new file mode 100644 index 0000000..26c74ce --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/enums/AuthenticationType.java @@ -0,0 +1,16 @@ +package it.gov.pagopa.onboarding.citizen.enums; + +import lombok.Getter; + +@Getter +public enum AuthenticationType { + OAUTH2("OAUTH2"); + + + private final String status; + + AuthenticationType(String status) { + this.status = status; + } + +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/model/CitizenConsent.java b/src/main/java/it/gov/pagopa/onboarding/citizen/model/CitizenConsent.java index 64ebd8e..d9862d6 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/model/CitizenConsent.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/model/CitizenConsent.java @@ -5,7 +5,7 @@ import lombok.experimental.SuperBuilder; import org.springframework.data.mongodb.core.mapping.Document; -import java.time.LocalDateTime; +import java.util.Map; @Document(collection = "citizen_consents") @Data @@ -14,10 +14,8 @@ public class CitizenConsent { private String id; - private String hashedFiscalCode; - private String tppId; - private Boolean tppState; - private LocalDateTime creationDate; - private LocalDateTime lastUpdateDate; + private String fiscalCode; + private Map consents; } + diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/model/ConsentDetails.java b/src/main/java/it/gov/pagopa/onboarding/citizen/model/ConsentDetails.java new file mode 100644 index 0000000..bc91264 --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/model/ConsentDetails.java @@ -0,0 +1,15 @@ +package it.gov.pagopa.onboarding.citizen.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@SuperBuilder +public class ConsentDetails { + private Boolean tppState; + private LocalDateTime tcDate; +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/model/mapper/CitizenConsentDTOToObjectMapper.java b/src/main/java/it/gov/pagopa/onboarding/citizen/model/mapper/CitizenConsentDTOToObjectMapper.java index 96caf6a..58b9bff 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/model/mapper/CitizenConsentDTOToObjectMapper.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/model/mapper/CitizenConsentDTOToObjectMapper.java @@ -1,18 +1,27 @@ package it.gov.pagopa.onboarding.citizen.model.mapper; - import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import it.gov.pagopa.onboarding.citizen.model.ConsentDetails; import org.springframework.stereotype.Service; +import java.util.HashMap; +import java.util.Map; + @Service public class CitizenConsentDTOToObjectMapper { - public CitizenConsent map(CitizenConsentDTO citizenConsentDTO){ + public CitizenConsent map(CitizenConsentDTO citizenConsentDTO) { + Map consents = new HashMap<>(); + + citizenConsentDTO.getConsents().forEach((tppId, consentDTO) -> consents.put(tppId, ConsentDetails.builder() + .tppState(consentDTO.getTppState()) + .tcDate(consentDTO.getTcDate()) + .build())); + return CitizenConsent.builder() - .tppState(true) - .tppId(citizenConsentDTO.getTppId()) - .hashedFiscalCode(citizenConsentDTO.getHashedFiscalCode()) + .fiscalCode(citizenConsentDTO.getFiscalCode()) + .consents(consents) .build(); } } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenRepository.java b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenRepository.java index 103dc2f..92f80a4 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenRepository.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenRepository.java @@ -2,15 +2,11 @@ import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -public interface CitizenRepository extends ReactiveMongoRepository { +public interface CitizenRepository extends ReactiveMongoRepository, CitizenSpecificRepository { - Flux findByHashedFiscalCode(String hashedFiscalCode); + Mono findByFiscalCode(String fiscalCode); - Flux findByHashedFiscalCodeAndTppStateTrue(String hashedFiscalCode); - - Mono findByHashedFiscalCodeAndTppId(String hashedFiscalCode, String tppId); } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepository.java b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepository.java new file mode 100644 index 0000000..2fccd8f --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepository.java @@ -0,0 +1,10 @@ +package it.gov.pagopa.onboarding.citizen.repository; + +import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import reactor.core.publisher.Mono; + + +public interface CitizenSpecificRepository { + + Mono findByFiscalCodeAndTppId(String fiscalCode, String tppId); +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java new file mode 100644 index 0000000..8d57830 --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java @@ -0,0 +1,39 @@ +package it.gov.pagopa.onboarding.citizen.repository; + +import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import lombok.Data; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Mono; + +@Repository +public class CitizenSpecificRepositoryImpl implements CitizenSpecificRepository { + + private final ReactiveMongoTemplate mongoTemplate; + + public CitizenSpecificRepositoryImpl(ReactiveMongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + + public Mono findByFiscalCodeAndTppId(String fiscalCode, String tppId) { + if (tppId == null) { + return Mono.empty(); + } + + Aggregation aggregation = Aggregation.newAggregation( + Aggregation.match(Criteria.where("fiscalCode").is(fiscalCode)), + Aggregation.match(Criteria.where("consents." + tppId).exists(true)) + ); + + return mongoTemplate.aggregate(aggregation, CitizenConsent.class, CitizenConsent.class) + .next(); + } + @Data + public static class ConsentKeyWrapper { + private String k; + } + +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/service/BloomFilterService.java b/src/main/java/it/gov/pagopa/onboarding/citizen/service/BloomFilterService.java new file mode 100644 index 0000000..bce36f0 --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/service/BloomFilterService.java @@ -0,0 +1,6 @@ +package it.gov.pagopa.onboarding.citizen.service; + +public interface BloomFilterService { + + boolean mightContain(String hashedFiscalCode); +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/service/BloomFilterServiceImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/service/BloomFilterServiceImpl.java new file mode 100644 index 0000000..efef5f1 --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/service/BloomFilterServiceImpl.java @@ -0,0 +1,52 @@ +package it.gov.pagopa.onboarding.citizen.service; + + +import com.azure.cosmos.implementation.guava25.hash.BloomFilter; +import com.azure.cosmos.implementation.guava25.hash.Funnels; +import it.gov.pagopa.onboarding.citizen.repository.CitizenRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.nio.charset.StandardCharsets; + +@Service +@Slf4j +public class BloomFilterServiceImpl implements BloomFilterService{ + + private final CitizenRepository citizenRepository; + + private BloomFilter bloomFilter; + + public BloomFilterServiceImpl(CitizenRepository citizenRepository) { + this.citizenRepository = citizenRepository; + } + + + @PostConstruct + public void initializeBloomFilter() { + bloomFilter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), 1000000, 0.01); + + citizenRepository.findAll() + .doOnNext(citizenConsent -> + bloomFilter.put(citizenConsent.getFiscalCode())) + .doOnComplete(() -> log.info("Bloom filter initialized")) + .subscribe(); + } + @Override + + public boolean mightContain(String fiscalCode) { + return bloomFilter.mightContain(fiscalCode); + } + + + @Scheduled(fixedRate = 3600000) + public void update() { + this.initializeBloomFilter(); + } + + public void add(String fiscalCode){ + bloomFilter.put(fiscalCode); + } +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenService.java b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenService.java index ae98fcd..f33a6a6 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenService.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenService.java @@ -8,8 +8,8 @@ public interface CitizenService { Mono createCitizenConsent(CitizenConsentDTO citizenConsent); - Mono updateChannelState(String fiscalCode, String tppId, boolean tppState); + Mono updateTppState(String fiscalCode, String tppId, boolean tppState); Mono getConsentStatus(String fiscalCode, String tppId); - Mono> getListEnabledConsents(String fiscalCode); - Mono> getListAllConsents(String fiscalCode); + Mono> getTppEnabledList(String fiscalCode); + Mono get(String fiscalCode); } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java index ddcc279..c0c45a5 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java @@ -2,18 +2,23 @@ import it.gov.pagopa.common.utils.Utils; import it.gov.pagopa.onboarding.citizen.configuration.ExceptionMap; +import it.gov.pagopa.onboarding.citizen.connector.tpp.TppConnectorImpl; +import it.gov.pagopa.onboarding.citizen.constants.CitizenConstants.ExceptionMessage; import it.gov.pagopa.onboarding.citizen.constants.CitizenConstants.ExceptionName; import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; import it.gov.pagopa.onboarding.citizen.dto.mapper.CitizenConsentObjectToDTOMapper; import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import it.gov.pagopa.onboarding.citizen.model.ConsentDetails; import it.gov.pagopa.onboarding.citizen.model.mapper.CitizenConsentDTOToObjectMapper; import it.gov.pagopa.onboarding.citizen.repository.CitizenRepository; +import it.gov.pagopa.onboarding.citizen.validation.CitizenConsentValidationServiceImpl; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import static it.gov.pagopa.common.utils.Utils.inputSanify; @@ -25,86 +30,86 @@ public class CitizenServiceImpl implements CitizenService { private final CitizenConsentObjectToDTOMapper mapperToDTO; private final CitizenConsentDTOToObjectMapper mapperToObject; private final ExceptionMap exceptionMap; + private final TppConnectorImpl tppConnector; + private final CitizenConsentValidationServiceImpl validationService; - public CitizenServiceImpl(CitizenRepository citizenRepository, CitizenConsentObjectToDTOMapper mapperToDTO, CitizenConsentDTOToObjectMapper mapperToObject, ExceptionMap exceptionMap) { + public CitizenServiceImpl(CitizenRepository citizenRepository, CitizenConsentObjectToDTOMapper mapperToDTO, CitizenConsentDTOToObjectMapper mapperToObject, ExceptionMap exceptionMap, TppConnectorImpl tppConnector, CitizenConsentValidationServiceImpl validationService) { this.citizenRepository = citizenRepository; this.mapperToDTO = mapperToDTO; this.mapperToObject = mapperToObject; this.exceptionMap = exceptionMap; + this.tppConnector = tppConnector; + this.validationService = validationService; } + @Override public Mono createCitizenConsent(CitizenConsentDTO citizenConsentDTO) { + CitizenConsent citizenConsent = mapperToObject.map(citizenConsentDTO); - String hashedFiscalCode = Utils.createSHA256(citizenConsent.getHashedFiscalCode()); - citizenConsent.setHashedFiscalCode(hashedFiscalCode); - citizenConsent.setCreationDate(LocalDateTime.now()); - citizenConsent.setLastUpdateDate(LocalDateTime.now()); - log.info("[EMD-CITIZEN][CREATE] Received consent: {}",inputSanify(citizenConsent.toString())); - return citizenRepository.findByHashedFiscalCodeAndTppId(hashedFiscalCode,citizenConsent.getTppId()) - .flatMap(existingConsent -> { - log.info("[EMD][CREATE-CITIZEN-CONSENT] Citizen consent already exists"); - return Mono.just(mapperToDTO.map(existingConsent)); - }) - .switchIfEmpty( - citizenRepository.save(citizenConsent) - .doOnSuccess(savedConsent -> log.info("[EMD][CREATE-CITIZEN-CONSENT] Created new citizen consent")) - .flatMap(savedConsent -> Mono.just(mapperToDTO.map(savedConsent))) - ); + String fiscalCode = citizenConsent.getFiscalCode(); + + citizenConsent.getConsents().forEach((tppId, consentDetails) -> consentDetails.setTcDate(LocalDateTime.now())); + + log.info("[EMD-CITIZEN][CREATE] Received consent: {}", inputSanify(citizenConsent.toString())); + + String tppId = citizenConsent.getConsents().keySet().stream().findFirst().orElse(null); + if (tppId == null) { + return Mono.error(exceptionMap.throwException(ExceptionName.TPP_NOT_FOUND, ExceptionMessage.TPP_NOT_FOUND)); + } + + return citizenRepository.findByFiscalCode(fiscalCode) + .flatMap(existingConsent -> validationService.handleExistingConsent(existingConsent, tppId, citizenConsent)) + .switchIfEmpty(validationService.validateTppAndSaveConsent(fiscalCode, tppId, citizenConsent)); } @Override - public Mono updateChannelState(String fiscalCode, String tppId, boolean tppState) { - String hashedFiscalCode = Utils.createSHA256(fiscalCode); - log.info("[EMD][[CITIZEN][UPDATE-CHANNEL-STATE] Received hashedFiscalCode: {} and tppId: {} with state: {}" - ,hashedFiscalCode, inputSanify(tppId), tppState); - return citizenRepository.findByHashedFiscalCodeAndTppId(hashedFiscalCode, tppId) - .switchIfEmpty(Mono.error(exceptionMap.throwException - (ExceptionName.CITIZEN_NOT_ONBOARDED,"Citizen consent not founded during update state process"))) - .flatMap(citizenConsent -> { - citizenConsent.setTppState(tppState); - citizenConsent.setLastUpdateDate(LocalDateTime.now()); - return citizenRepository.save(citizenConsent); - }) - .map(mapperToDTO::map) - .doOnSuccess(savedConsent -> log.info("[EMD][[CITIZEN][UPDATE-CHANNEL-STATE] Updated state")); + public Mono updateTppState(String fiscalCode, String tppId, boolean tppState) { + log.info("[EMD][CITIZEN][UPDATE-CHANNEL-STATE] Received hashedFiscalCode: {} and tppId: {} with state: {}", + Utils.createSHA256(fiscalCode), inputSanify(tppId), tppState); + + return tppConnector.get(tppId) + .onErrorMap(error ->exceptionMap.throwException(ExceptionName.TPP_NOT_FOUND, ExceptionMessage.TPP_NOT_FOUND)) + .flatMap(tppResponse -> citizenRepository.findByFiscalCodeAndTppId(fiscalCode, tppId) + .switchIfEmpty(Mono.error(exceptionMap.throwException + (ExceptionName.CITIZEN_NOT_ONBOARDED, "Citizen consent not founded during update state process"))) + .flatMap(citizenConsent -> { + ConsentDetails consentDetails = citizenConsent.getConsents().get(tppId); + consentDetails.setTppState(tppState); + return citizenRepository.save(citizenConsent); + }) + .map(mapperToDTO::map) + .doOnSuccess(savedConsent -> log.info("[EMD][CITIZEN][UPDATE-CHANNEL-STATE] Updated state"))); } @Override public Mono getConsentStatus(String fiscalCode, String tppId) { - String hashedFiscalCode = Utils.createSHA256(fiscalCode); - log.info("[EMD-CITIZEN][GET-CONSENT-STATUS] Received hashedFiscalCode: {} and tppId: {}",hashedFiscalCode,inputSanify(tppId)); - return citizenRepository.findByHashedFiscalCodeAndTppId(hashedFiscalCode, tppId) + log.info("[EMD-CITIZEN][GET-CONSENT-STATUS] Received hashedFiscalCode: {} and tppId: {}", Utils.createSHA256(fiscalCode), inputSanify(tppId)); + return citizenRepository.findByFiscalCodeAndTppId(fiscalCode, tppId) .switchIfEmpty(Mono.error(exceptionMap.throwException - (ExceptionName.CITIZEN_NOT_ONBOARDED,"Citizen consent not founded during get process "))) + (ExceptionName.CITIZEN_NOT_ONBOARDED, "Citizen consent not founded during get process "))) .map(mapperToDTO::map) - .doOnSuccess(consent -> log.info("[EMD-CITIZEN][GET-CONSENT-STATUS] Consent consent found:: {}",consent)); + .doOnSuccess(consent -> log.info("[EMD-CITIZEN][GET-CONSENT-STATUS] Consent consent found:: {}", consent)); } @Override - public Mono> getListEnabledConsents(String fiscalCode) { - String hashedFiscalCode = Utils.createSHA256(fiscalCode); - log.info("[EMD-CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] Received hashedFiscalCode: {}",hashedFiscalCode); - return citizenRepository.findByHashedFiscalCodeAndTppStateTrue(hashedFiscalCode) - .collectList() - .map(consentList -> consentList.stream() - .map(mapperToDTO::map) - .toList() - ) - .doOnSuccess(consentList -> log.info("EMD][CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] Consents founded: {}",(consentList.size()))); + public Mono> getTppEnabledList(String fiscalCode) { + log.info("[EMD-CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] Received hashedFiscalCode: {}", Utils.createSHA256(fiscalCode)); + return citizenRepository.findByFiscalCode(fiscalCode) + .switchIfEmpty(Mono.empty()) + .map(citizenConsent -> citizenConsent.getConsents().entrySet().stream() + .filter(tpp -> tpp.getValue().getTppState()) + .map(Map.Entry::getKey) + .toList()) + .doOnSuccess(tppIdList -> log.info("EMD][CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] Consents found: {}", (tppIdList.size()))); } @Override - public Mono> getListAllConsents(String fiscalCode) { - String hashedFiscalCode = Utils.createSHA256(fiscalCode); - log.info("[EMD-CITIZEN][FIND-ALL-CITIZEN-CONSENTS] Received hashedFiscalCode: {}",(hashedFiscalCode)); - return citizenRepository.findByHashedFiscalCode(hashedFiscalCode) - .collectList() - .map(consentList -> consentList.stream() - .map(mapperToDTO::map) - .toList() - ) - .doOnSuccess(consentList -> log.info("[EMD-CITIZEN][FIND-ALL-CITIZEN-CONSENTS] Consents found:: {}",consentList)); + public Mono get(String fiscalCode) { + log.info("[EMD-CITIZEN][FIND-ALL-CITIZEN-CONSENTS] Received hashedFiscalCode: {}", (Utils.createSHA256(fiscalCode))); + return citizenRepository.findByFiscalCode(fiscalCode) + .map(mapperToDTO::map) + .doOnSuccess(consentList -> log.info("[EMD-CITIZEN][FIND-ALL-CITIZEN-CONSENTS] Consents found:: {}", consentList)); } } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationService.java b/src/main/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationService.java new file mode 100644 index 0000000..8cbabed --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationService.java @@ -0,0 +1,13 @@ +package it.gov.pagopa.onboarding.citizen.validation; + +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import reactor.core.publisher.Mono; + +public interface CitizenConsentValidationService { + + Mono handleExistingConsent(CitizenConsent existingConsent, String tppId, CitizenConsent citizenConsent); + + Mono validateTppAndSaveConsent(String fiscalCode, String tppId, CitizenConsent citizenConsent); + +} diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImpl.java new file mode 100644 index 0000000..d2e8fd5 --- /dev/null +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImpl.java @@ -0,0 +1,76 @@ +package it.gov.pagopa.onboarding.citizen.validation; + +import it.gov.pagopa.common.utils.Utils; +import it.gov.pagopa.onboarding.citizen.configuration.ExceptionMap; +import it.gov.pagopa.onboarding.citizen.connector.tpp.TppConnectorImpl; +import it.gov.pagopa.onboarding.citizen.constants.CitizenConstants; +import it.gov.pagopa.onboarding.citizen.constants.CitizenConstants.ExceptionName; +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import it.gov.pagopa.onboarding.citizen.dto.mapper.CitizenConsentObjectToDTOMapper; +import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import it.gov.pagopa.onboarding.citizen.repository.CitizenRepository; +import it.gov.pagopa.onboarding.citizen.service.BloomFilterServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +@Service +@Slf4j +public class CitizenConsentValidationServiceImpl implements CitizenConsentValidationService { + + private final CitizenRepository citizenRepository; + + private final BloomFilterServiceImpl bloomFilterService; + + private final TppConnectorImpl tppConnector; + private final CitizenConsentObjectToDTOMapper mapperToDTO; + private final ExceptionMap exceptionMap; + + public CitizenConsentValidationServiceImpl(CitizenRepository citizenRepository, BloomFilterServiceImpl bloomFilterService, TppConnectorImpl tppConnector, + CitizenConsentObjectToDTOMapper mapperToDTO, ExceptionMap exceptionMap) { + this.citizenRepository = citizenRepository; + this.bloomFilterService = bloomFilterService; + this.tppConnector = tppConnector; + this.mapperToDTO = mapperToDTO; + this.exceptionMap = exceptionMap; + } + + @Override + public Mono handleExistingConsent(CitizenConsent existingConsent, String tppId, CitizenConsent citizenConsent) { + if (existingConsent.getConsents().containsKey(tppId)) { + return Mono.just(mapperToDTO.map(existingConsent)); + } else { + return validateTppAndUpdateConsent(existingConsent, tppId, citizenConsent); + } + } + + @Override + public Mono validateTppAndSaveConsent(String fiscalCode, String tppId, CitizenConsent citizenConsent) { + return tppConnector.get(tppId) + .onErrorMap(error -> exceptionMap.throwException(ExceptionName.TPP_NOT_FOUND, CitizenConstants.ExceptionMessage.TPP_NOT_FOUND)) + .flatMap(tppResponse -> { + if (Boolean.TRUE.equals(citizenConsent.getConsents().get(tppId).getTppState())) { + return citizenRepository.save(citizenConsent) + .doOnSuccess(savedConsent -> { + log.info("[EMD][CREATE-CITIZEN-CONSENT] Created new citizen consent for fiscal code: {}", Utils.createSHA256(fiscalCode)); + bloomFilterService.add(fiscalCode); + }) + .map(mapperToDTO::map); + } else { + return Mono.error(exceptionMap.throwException(ExceptionName.TPP_NOT_FOUND, "TPP is not active or is invalid")); + } + }); + } + + + private Mono validateTppAndUpdateConsent(CitizenConsent existingConsent, String tppId, CitizenConsent citizenConsent) { + return tppConnector.get(tppId) + .onErrorMap(error -> exceptionMap.throwException(ExceptionName.TPP_NOT_FOUND, CitizenConstants.ExceptionMessage.TPP_NOT_FOUND)) + .flatMap(tppResponse -> { + existingConsent.getConsents().put(tppId, citizenConsent.getConsents().get(tppId)); + return citizenRepository.save(existingConsent) + .doOnSuccess(savedConsent -> log.info("[EMD][CREATE-CITIZEN-CONSENT] Updated citizen consent for TPP: {}", tppId)) + .flatMap(savedConsent -> Mono.just(mapperToDTO.map(savedConsent))); + }); + } +} diff --git a/src/main/resources/META-INF/openapi.yaml b/src/main/resources/META-INF/openapi.yaml new file mode 100644 index 0000000..06c9136 --- /dev/null +++ b/src/main/resources/META-INF/openapi.yaml @@ -0,0 +1,1306 @@ +openapi: 3.0.3 +info: + title: EMD CITIZEN CONSENT API + version: '1.0' + description: |- + EMD CITIZEN CONSENT + contact: + name: Stefano D'Elia + email: stefano11.delia@emeal.nttdata.com + +servers: + - description: Development Test + url: https://api-io.dev.cstar.pagopa.it/emd/citizen + x-internal: true + - description: User Acceptance Test + url: https://api-io.uat.cstar.pagopa.it/emd/citizen + x-internal: true + +security: + - bearerAuth: [ ] + +tags: + - name: Citizen consent + description: 'Citizen consent operation' +paths: + '/': + post: + tags: + - Citizen consent + summary: >- + ENG: Save citizen consent information - IT: Salvataggio dei consensi del cittadino + operationId: saveCitizenConsent + description: Save citizen consents + parameters: + - name: Accept-Language + in: header + description: 'ENG: Language - IT: Lingua' + schema: + type: string + pattern: "^[ -~]{2,5}$" + minLength: 2 + maxLength: 5 + example: it-IT + default: it-IT + required: true + requestBody: + description: 'ENG: Citizen consent details - IT: Dettagli consensi del cittadino' + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentRequestDTO' + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentResponseDTO' + examples: + example1: + $ref: '#/components/examples/example1' + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_BAD_REQUEST + message: Something went wrong handling the request + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '401': + description: Authentication failed + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_AUTHENTICATION_FAILED + message: Something went wrong with authentication + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '404': + description: The citizen consent was not found + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_NOT_ONBOARDED + message: Citizen consent not inserted + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '429': + description: Too many Request + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_TOO_MANY_REQUESTS + message: Too many requests + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '500': + description: Server ERROR + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_GENERIC_ERROR + message: Application error + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '/stateUpdate': + put: + tags: + - Citizen consent + summary: >- + ENG: Update citizen consent state - IT: Aggiornamento dello stato dei consensi del cittadino + operationId: updateState + description: Update citizen consents + parameters: + - name: Accept-Language + in: header + description: 'ENG: Language - IT: Lingua' + schema: + type: string + pattern: "^[ -~]{2,5}$" + minLength: 2 + maxLength: 5 + example: it-IT + default: it-IT + required: true + requestBody: + description: 'ENG: Citizen consent details - IT: Dettagli consensi del cittadino' + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentStateUpdateDTO' + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentResponseDTO' + examples: + example2: + $ref: '#/components/examples/example2' + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_BAD_REQUEST + message: Something went wrong handling the request + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '401': + description: Authentication failed + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_AUTHENTICATION_FAILED + message: Something went wrong with authentication + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '404': + description: The citizen consent was not found + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_NOT_ONBOARDED + message: Citizen consent not inserted + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '429': + description: Too many Request + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_TOO_MANY_REQUESTS + message: Too many requests + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '500': + description: Server ERROR + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_GENERIC_ERROR + message: Application error + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '/{fiscalCode}/{tppId}': + get: + tags: + - Citizen consent + summary: >- + ENG: Returns the Citizen consent detalil from fiscalCode and tppId associated - IT: Ritorna il dettaglio dei consensi del cittadino tramite fiscalCode e tppId + operationId: getConsentStatus + description: Get citizen consent from fiscalCode and tppId + parameters: + - name: Accept-Language + in: header + description: 'ENG: Language - IT: Lingua' + schema: + type: string + pattern: "^[ -~]{2,5}$" + minLength: 2 + maxLength: 5 + example: it-IT + default: it-IT + required: true + - name: fiscalCode + in: path + description: 'ENG: Fiscal code of the citizen - IT: codice fiscale del cittadino' + required: true + schema: + type: string + description: "Fiscal Code or P.IVA of the citizen" + pattern: "^[A-Za-z0-9]{11,16}$" + minLength: 11 + maxLength: 16 + example: "RSSMRO92S18L048H" + - name: tppId + in: path + description: 'ENG: Unique ID that identify TPP on PagoPA systems - IT: Identificativo univoco della TPP sui sistemi PagoPA' + required: true + schema: + type: string + description: "Unique ID that identify TPP on PagoPA systems" + pattern: "^[ -~]{1,50}$" + minLength: 1 + maxLength: 50 + example: "0e3bee29-8753-447c-b0da-1f7965558ec2_1706867960900" + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentResponseDTO' + examples: + example3: + $ref: '#/components/examples/example3' + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_BAD_REQUEST + message: Something went wrong handling the request + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '401': + description: Authentication failed + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_AUTHENTICATION_FAILED + message: Something went wrong with authentication + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '404': + description: The citizen consent was not found + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_NOT_ONBOARDED + message: Citizen consent not inserted + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '429': + description: Too many Request + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_TOO_MANY_REQUESTS + message: Too many requests + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '500': + description: Server ERROR + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_GENERIC_ERROR + message: Application error + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + + '/list/{fiscalCode}': + get: + tags: + - Citizen consent + summary: >- + ENG: Returns the list of tpp associated with a specific citizen - IT: Restituisce la lista dei canali associati ad un determinato cittadino + operationId: get + description: Get citizen consents list from fiscalCode + parameters: + - name: Accept-Language + in: header + description: 'ENG: Language - IT: Lingua' + schema: + type: string + pattern: "^[ -~]{2,5}$" + minLength: 2 + maxLength: 5 + example: it-IT + default: it-IT + required: true + - name: fiscalCode + in: path + description: 'ENG: Fiscal code of the citizen - IT: codice fiscale del cittadino' + required: true + schema: + type: string + description: "Fiscal Code or P.IVA of the citizen" + pattern: "^[A-Za-z0-9]{11,16}$" + minLength: 11 + maxLength: 16 + example: "RSSMRO92S18L048H" + responses: + '200': + description: "A list of citizen consents" + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentResponseDTO' + examples: + example4: + $ref: '#/components/examples/example4' + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_BAD_REQUEST + message: Something went wrong handling the request + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '401': + description: Authentication failed + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_AUTHENTICATION_FAILED + message: Something went wrong with authentication + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '404': + description: The citizen consent was not found + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_NOT_ONBOARDED + message: Citizen consent not inserted + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '429': + description: Too many Request + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_TOO_MANY_REQUESTS + message: Too many requests + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '500': + description: Server ERROR + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_GENERIC_ERROR + message: Application error + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + + '/list/{fiscalCode}/enabled': + get: + tags: + - Citizen consent + summary: >- + ENG: Returns the list of tpp enabled associated with a specific citizen - IT: Restituisce la lista dei canali abilitati associati ad un determinato cittadino + operationId: getTppEnabledList + description: Get citizen consents tppId enabled list from fiscalCode + parameters: + - name: Accept-Language + in: header + description: 'ENG: Language - IT: Lingua' + schema: + type: string + pattern: "^[ -~]{2,5}$" + minLength: 2 + maxLength: 5 + example: it-IT + default: it-IT + required: true + - name: fiscalCode + in: path + description: 'ENG: Fiscal code of the citizen - IT: codice fiscale del cittadino' + required: true + schema: + type: string + description: "Fiscal Code or P.IVA of the citizen" + pattern: "^[A-Za-z0-9]{11,16}$" + minLength: 11 + maxLength: 16 + example: "RSSMRO92S18L048H" + responses: + '200': + description: "Lista di tppId abilitati per il codice fiscale fornito" + content: + application/json: + schema: + type: array + items: + type: string + description: "Identificativo univoco di un TPP (tppId)" + pattern: "^[ -~]{1,50}$" + minLength: 1 + maxLength: 50 + maxItems: 100 + example: + - "0e3bee29-8753-447c-b0da-1f7965558ec2_1706867960900" + - "bcdef234-5678-90ab-cdef-abcdef012345_1706869005678" + - "cafe1234-babe-5678-cafe-123456789abc_1706872007890" + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_BAD_REQUEST + message: Something went wrong handling the request + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '401': + description: Authentication failed + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_AUTHENTICATION_FAILED + message: Something went wrong with authentication + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '404': + description: The citizen consent was not found + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_NOT_ONBOARDED + message: Citizen consent not inserted + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '429': + description: Too many Request + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_TOO_MANY_REQUESTS + message: Too many requests + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + '500': + description: Server ERROR + content: + application/json: + schema: + $ref: '#/components/schemas/CitizenConsentErrorDTO' + example: + code: CITIZEN_CONSENT_GENERIC_ERROR + message: Application error + headers: + Access-Control-Allow-Origin: + description: Indicates whether the response can be shared with requesting code from the given origin + required: false + schema: + $ref: '#/components/schemas/AccessControlAllowOrigin' + RateLimit-Limit: + description: The number of allowed requests in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitLimit' + RateLimit-Reset: + description: The number of seconds left in the current period + required: false + schema: + $ref: '#/components/schemas/RateLimitReset' + Retry-After: + description: The number of seconds to wait before allowing a follow-up request + required: false + schema: + $ref: '#/components/schemas/RetryAfter' + +components: + schemas: + + AccessControlAllowOrigin: + description: Indicates whether the response can be shared with requesting code from the given origin + type: string + pattern: "^[ -~]{1,2048}$" + minLength: 1 + maxLength: 2048 + + RateLimitLimit: + description: The number of allowed requests in the current period + type: integer + format: int32 + minimum: 1 + maximum: 240 + + RateLimitReset: + description: The number of seconds left in the current period + type: integer + format: int32 + minimum: 1 + maximum: 60 + + RetryAfter: + description: The number of seconds to wait before allowing a follow-up request + type: integer + format: int32 + minimum: 1 + maximum: 240 + + CitizenConsentRequestDTO: + type: object + description: "Schema di richiesta per aggiornare i consensi" + properties: + fiscalCode: + type: string + example: "RSSMRO92S18L048H" + description: "Codice fiscale dell'utente" + pattern: "^[A-Za-z0-9]{11,16}$" + minLength: 11 + maxLength: 16 + consents: + type: object + description: "Oggetto contenente consensi con chiavi dinamiche basate su tppId (UUID concatenati con timestamp)" + additionalProperties: + type: object + properties: + tppState: + type: boolean + description: "Stato del consenso TPP" + example: true + required: + - tppState + example: + "0e3bee29-8753-447c-b0da-1f7965558ec2_1706867960900": + tppState: true + "a12cd3e4-5678-90ab-cdef-1234567890ab_1706868990123": + tppState: true + + required: + - fiscalCode + - consents + + + + CitizenConsentResponseDTO: + type: object + description: "Schema di risposta che contiene lo stato e la data dei consensi" + properties: + fiscalCode: + type: string + example: "RSSMRO92S18L048H" + description: "Codice fiscale del cittadino" + pattern: "^[A-Za-z0-9]{11,16}$" + minLength: 11 + maxLength: 16 + consents: + type: object + description: "Oggetto contenente consensi con chiavi dinamiche basate su tppId (UUID concatenati con timestamp)" + additionalProperties: + type: object + properties: + tppState: + type: boolean + description: "Stato del consenso TPP" + example: true + tcDate: + type: string + format: date-time + description: "Data e ora dell'ultimo aggiornamento del consenso" + example: "2024-11-01T11:25:40.695Z" + maxLength: 30 + pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + example: + "0e3bee29-8753-447c-b0da-1f7965558ec2_1706867960900": + tppState: true + tcDate: "2024-11-01T11:25:40.695Z" + "a12cd3e4-5678-90ab-cdef-1234567890ab_1706868990123": + tppState: true + tcDate: "2024-11-05T11:25:40.695Z" + required: + - fiscalCode + - consents + + + CitizenConsentStateUpdateDTO: + type: object + description: "Schema che permette la modifica dei consensi del cittadino" + properties: + fiscalCode: + type: string + example: "RSSMRO92S18L048H" + description: "Codice fiscale del cittadino" + pattern: "^[A-Za-z0-9]{11,16}$" + minLength: 11 + maxLength: 16 + tppId: + type: string + example: "0e3bee29-8753-447c-b0da-1f7965558ec2_1706867960900" + description: "ENG: Unique ID that identify TPP on PagoPA systems - IT: Identificativo univoco della TPP sui sistemi PagoPA" + pattern: "^[ -~]{1,50}$" + minLength: 1 + maxLength: 50 + tppState: + type: boolean + example: false + description: "Stato del consenso TPP" + + CitizenConsentErrorDTO: + type: object + required: + - code + - message + properties: + code: + type: string + enum: + - CITIZEN_CONSENT_BAD_REQUEST + - CITIZEN_CONSENT_NOT_ONBOARDED + - CITIZEN_CONSENT_TOO_MANY_REQUESTS + - CITIZEN_CONSENT_GENERIC_ERROR + - CITIZEN_CONSENT_AUTHENTICATION_FAILED + description: |- + "ENG: Error code: CITIZEN_CONSENT_BAD_REQUEST: Something went wrong handling the request, + CITIZEN_CONSENT_NOT_ONBOARDED: Citizen consent not inserted, + CITIZEN_CONSENT_TOO_MANY_REQUESTS: Too many requests, + CITIZEN_CONSENT_GENERIC_ERROR: Application Error, + CITIZEN_CONSENT_AUTHENTICATION_FAILED: Something went wrong with authentication - + IT: Codice di errore: + CITIZEN_CONSENT_BAD_REQUEST: Qualcosa è andato storto durante + l'invio della richiesta, + CITIZEN_CONSENT_NOT_ONBOARDED: Consensi del cittadino non inseriti, + CITIZEN_CONSENT_TOO_MANY_REQUESTS: Troppe richieste, + CITIZEN_CONSENT_GENERIC_ERROR: Errore generico, + CITIZEN_CONSENT_AUTHENTICATION_FAILED: Qualcosa è andato storto con l'autenticazione" + message: + type: string + description: 'ENG: Error message - IT: Messaggio di errore' + maxLength: 250 + pattern: "^[\\w\\s.,!?'\"-]+$" + + examples: + example1: + summary: "Esempio con più consensi" + value: + fiscalCode: "RSSMRO92S18L048H" + consents: + "0e3bee29-8753-447c-b0da-1f7965558ec2_1706867960900": + tppState: true + tcDate: "2024-11-01T11:25:40.695Z" + "a12cd3e4-5678-90ab-cdef-1234567890ab_1706868990123": + tppState: true + tcDate: "2024-11-05T11:25:40.695Z" + example2: + summary: "Esempio con un solo consenso disattivo" + value: + fiscalCode: "RSSMRO92S18L048H" + consents: + "0e3bee29-8753-447c-b0da-1f7965558ec2_1706867960900": + tppState: false + tcDate: "2024-11-06T11:25:40.695Z" + example3: + summary: "Esempio con un solo consenso attivo" + value: + fiscalCode: "RSSMRO92S18L048H" + consents: + "0e3bee29-8753-447c-b0da-1f7965558ec2_1706867960900": + tppState: true + tcDate: "2024-11-06T11:25:40.695Z" + example4: + summary: "Esempio con un consenso attivo e uno non attivo" + value: + fiscalCode: "RSSMRO92S18L048H" + consents: + "0e3bee29-8753-447c-b0da-1f7965558ec2_1706867960900": + tppState: true + tcDate: "2024-11-01T11:25:40.695Z" + "a12cd3e4-5678-90ab-cdef-1234567890ab_1706868990123": + tppState: false + tcDate: "2024-11-05T11:25:40.695Z" + securitySchemes: + bearerAuth: + type: http + scheme: bearer diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ad8085a..74fb1cc 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -38,3 +38,6 @@ management: exposure.include: "*" web: exposure.include: info, health +rest-client: + tpp: + baseUrl: ${EMD_TPP:http://emd-tpp} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/connector/tpp/TppConnectorImplTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/connector/tpp/TppConnectorImplTest.java new file mode 100644 index 0000000..ec0594e --- /dev/null +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/connector/tpp/TppConnectorImplTest.java @@ -0,0 +1,58 @@ +package it.gov.pagopa.onboarding.citizen.connector.tpp; + +import it.gov.pagopa.onboarding.citizen.dto.TppDTO; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import static it.gov.pagopa.onboarding.citizen.enums.AuthenticationType.OAUTH2; +import static org.assertj.core.api.Assertions.assertThat; + +class TppConnectorImplTest { + + private MockWebServer mockWebServer; + private TppConnectorImpl tppConnector; + + @BeforeEach + void setUp() throws Exception { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + + WebClient.Builder webClientBuilder = WebClient.builder(); + + tppConnector = new TppConnectorImpl(webClientBuilder, mockWebServer.url("/").toString()); + } + + @AfterEach + void tearDown() throws Exception { + mockWebServer.shutdown(); + } + + @Test + void testGetTppInfoOk() { + + mockWebServer.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"tppId\":\"TPP_OK_1\",\"entityId\":\"ENTITY_OK_1\",\"businessName\":\"Test Business\",\"messageUrl\":\"https://example.com/message\",\"authenticationUrl\":\"https://example.com/auth\",\"authenticationType\":\"OAUTH2\",\"contact\":{\"name\":\"John Doe\",\"number\":\"+1234567890\",\"email\":\"contact@example.com\"},\"state\":true}") + .addHeader("Content-Type", "application/json")); + + Mono resultMono = tppConnector.get("TPP_OK_1"); + TppDTO tppDTO = resultMono.block(); + + assertThat(tppDTO).isNotNull(); + assertThat(tppDTO.getTppId()).isEqualTo("TPP_OK_1"); + assertThat(tppDTO.getEntityId()).isEqualTo("ENTITY_OK_1"); + assertThat(tppDTO.getBusinessName()).isEqualTo("Test Business"); + assertThat(tppDTO.getMessageUrl()).isEqualTo("https://example.com/message"); + assertThat(tppDTO.getAuthenticationUrl()).isEqualTo("https://example.com/auth"); + assertThat(tppDTO.getAuthenticationType()).isEqualTo(OAUTH2); + assertThat(tppDTO.getContact().getName()).isEqualTo("John Doe"); + assertThat(tppDTO.getContact().getNumber()).isEqualTo("+1234567890"); + assertThat(tppDTO.getContact().getEmail()).isEqualTo("contact@example.com"); + assertThat(tppDTO.getState()).isTrue(); + } +} diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java index f49c259..f42d41b 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java @@ -1,7 +1,10 @@ package it.gov.pagopa.onboarding.citizen.controller; import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentStateUpdateDTO; import it.gov.pagopa.onboarding.citizen.faker.CitizenConsentDTOFaker; +import it.gov.pagopa.onboarding.citizen.faker.CitizenConsentStateUpdateDTOFaker; +import it.gov.pagopa.onboarding.citizen.service.BloomFilterServiceImpl; import it.gov.pagopa.onboarding.citizen.service.CitizenServiceImpl; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -9,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Mono; @@ -21,12 +25,15 @@ class CitizenControllerTest { @MockBean private CitizenServiceImpl citizenService; + @MockBean + private BloomFilterServiceImpl bloomFilterService; + @Autowired private WebTestClient webClient; - private static final String FISCAL_CODE = "fiscalCode"; + private static final String FISCAL_CODE = "MLXHZZ43A70H203T"; private static final String TPP_ID = "tppId"; @@ -52,25 +59,27 @@ void saveCitizenConsent_Ok() { @Test void stateUpdate_Ok() { - CitizenConsentDTO citizenConsentDTO = CitizenConsentDTOFaker.mockInstance(true); + CitizenConsentStateUpdateDTO citizenConsentStateUpdateDTO = CitizenConsentStateUpdateDTOFaker.mockInstance(true); - Mockito.when(citizenService.updateChannelState( - citizenConsentDTO.getHashedFiscalCode(), - citizenConsentDTO.getTppId(), - citizenConsentDTO.getTppState())) - .thenReturn(Mono.just(citizenConsentDTO)); + CitizenConsentDTO expectedResponseDTO = CitizenConsentDTOFaker.mockInstance(true); + + Mockito.when(citizenService.updateTppState( + citizenConsentStateUpdateDTO.getFiscalCode(), + citizenConsentStateUpdateDTO.getTppId(), + citizenConsentStateUpdateDTO.getTppState())) + .thenReturn(Mono.just(expectedResponseDTO)); webClient.put() .uri("/emd/citizen/stateUpdate") .contentType(MediaType.APPLICATION_JSON) - .bodyValue(citizenConsentDTO) + .bodyValue(citizenConsentStateUpdateDTO) .exchange() .expectStatus().isOk() .expectBody(CitizenConsentDTO.class) .consumeWith(response -> { CitizenConsentDTO resultResponse = response.getResponseBody(); Assertions.assertNotNull(resultResponse); - Assertions.assertEquals(citizenConsentDTO, resultResponse); + Assertions.assertEquals(expectedResponseDTO, resultResponse); }); } @@ -95,40 +104,73 @@ void getConsentStatus_Ok() { } @Test - void getCitizenConsentsEnabled_Ok() { - List citizenConsentDTOList = List.of(CitizenConsentDTOFaker.mockInstance(true)); + void getTppEnabledList_Ok() { + List tppEnabledList = List.of("TPP1", "TPP2"); - Mockito.when(citizenService.getListEnabledConsents(FISCAL_CODE)) - .thenReturn(Mono.just(citizenConsentDTOList)); + Mockito.when(citizenService.getTppEnabledList(FISCAL_CODE)) + .thenReturn(Mono.just(tppEnabledList)); webClient.get() .uri("/emd/citizen/list/{fiscalCode}/enabled", FISCAL_CODE) .exchange() .expectStatus().isOk() - .expectBodyList(CitizenConsentDTO.class) + .expectBody(new ParameterizedTypeReference>() {}) .consumeWith(response -> { - List resultResponse = response.getResponseBody(); + List resultResponse = response.getResponseBody(); Assertions.assertNotNull(resultResponse); - Assertions.assertEquals(citizenConsentDTOList.size(), resultResponse.size()); + Assertions.assertEquals(tppEnabledList.size(), resultResponse.size()); }); } @Test - void getCitizenConsents_Ok() { - List citizenConsentDTOList = List.of(CitizenConsentDTOFaker.mockInstance(true)); + void get_Ok() { + CitizenConsentDTO citizenConsentDTO = CitizenConsentDTOFaker.mockInstance(true); - Mockito.when(citizenService.getListAllConsents(FISCAL_CODE)) - .thenReturn(Mono.just(citizenConsentDTOList)); + Mockito.when(citizenService.get(FISCAL_CODE)) + .thenReturn(Mono.just(citizenConsentDTO)); webClient.get() .uri("/emd/citizen/list/{fiscalCode}", FISCAL_CODE) .exchange() .expectStatus().isOk() - .expectBodyList(CitizenConsentDTO.class) + .expectBody(CitizenConsentDTO.class) + .consumeWith(response -> { + CitizenConsentDTO resultResponse = response.getResponseBody(); + Assertions.assertNotNull(resultResponse); + Assertions.assertEquals(citizenConsentDTO, resultResponse); + }); + } + + @Test + void getAllFiscalCode_Ok() { + Mockito.when(bloomFilterService.mightContain(FISCAL_CODE)) + .thenReturn(true); + + webClient.get() + .uri("/emd/citizen/filter/{fiscalCode}", FISCAL_CODE) + .exchange() + .expectStatus().isOk() + .expectBody(String.class) + .consumeWith(response -> { + String resultResponse = response.getResponseBody(); + Assertions.assertNotNull(resultResponse); + Assertions.assertEquals("OK", resultResponse); + }); + } + @Test + void getAllFiscalCode_NoChannelsEnabled() { + Mockito.when(bloomFilterService.mightContain(FISCAL_CODE)) + .thenReturn(false); + + webClient.get() + .uri("/emd/citizen/filter/{fiscalCode}", FISCAL_CODE) + .exchange() + .expectStatus().isAccepted() + .expectBody(String.class) .consumeWith(response -> { - List resultResponse = response.getResponseBody(); + String resultResponse = response.getResponseBody(); Assertions.assertNotNull(resultResponse); - Assertions.assertEquals(citizenConsentDTOList.size(), resultResponse.size()); + Assertions.assertEquals("NO CHANNELS ENABLED", resultResponse); }); } } diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentDTOFaker.java b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentDTOFaker.java index 2c639cc..d6a1442 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentDTOFaker.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentDTOFaker.java @@ -1,21 +1,22 @@ package it.gov.pagopa.onboarding.citizen.faker; - import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; - import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; public class CitizenConsentDTOFaker { - private CitizenConsentDTOFaker(){} + private CitizenConsentDTOFaker() {} + public static CitizenConsentDTO mockInstance(Boolean bias) { + Map consents = new HashMap<>(); + + consents.put("tppId", new CitizenConsentDTO.ConsentDTO(bias, LocalDateTime.now())); + return CitizenConsentDTO.builder() - .tppId("tppId") - .tppState(bias) - .hashedFiscalCode("hashedFiscalCode") - .creationDate(LocalDateTime.now()) - .lastUpdateDate(LocalDateTime.now()) + .fiscalCode("MLXHZZ43A70H203T") + .consents(consents) .build(); - } } diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentFaker.java b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentFaker.java index 195afad..03a2755 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentFaker.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentFaker.java @@ -1,20 +1,29 @@ package it.gov.pagopa.onboarding.citizen.faker; - import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import it.gov.pagopa.onboarding.citizen.model.ConsentDetails; import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; public class CitizenConsentFaker { - private CitizenConsentFaker(){} + private CitizenConsentFaker() {} + public static CitizenConsent mockInstance(Boolean bias) { - return CitizenConsent.builder() - .tppId("tppId") + Map consents = new HashMap<>(); + + ConsentDetails consentDetails = ConsentDetails.builder() .tppState(bias) - .hashedFiscalCode("hashedFiscalCode") - .creationDate(LocalDateTime.now()) - .lastUpdateDate(LocalDateTime.now()) + .tcDate(LocalDateTime.now()) + .build(); + + consents.put("tppId", consentDetails); + + return CitizenConsent.builder() + .fiscalCode("fiscalCode") + .consents(consents) .build(); } } diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentStateUpdateDTOFaker.java b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentStateUpdateDTOFaker.java new file mode 100644 index 0000000..31464f6 --- /dev/null +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/CitizenConsentStateUpdateDTOFaker.java @@ -0,0 +1,16 @@ +package it.gov.pagopa.onboarding.citizen.faker; + +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentStateUpdateDTO; + +public class CitizenConsentStateUpdateDTOFaker { + + private CitizenConsentStateUpdateDTOFaker() {} + + public static CitizenConsentStateUpdateDTO mockInstance(Boolean tppState) { + return CitizenConsentStateUpdateDTO.builder() + .fiscalCode("hashedFiscalCode") + .tppId("tppId") + .tppState(tppState) + .build(); + } +} diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/faker/TppDTOFaker.java b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/TppDTOFaker.java new file mode 100644 index 0000000..b5befd4 --- /dev/null +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/faker/TppDTOFaker.java @@ -0,0 +1,12 @@ +package it.gov.pagopa.onboarding.citizen.faker; + +import it.gov.pagopa.onboarding.citizen.dto.TppDTO; + +public class TppDTOFaker { + private TppDTOFaker(){} + public static TppDTO mockInstance() { + return TppDTO.builder() + .tppId("id") + .build(); + } +} diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImplTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImplTest.java new file mode 100644 index 0000000..0acc9cf --- /dev/null +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImplTest.java @@ -0,0 +1,110 @@ +package it.gov.pagopa.onboarding.citizen.repository; + +import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import it.gov.pagopa.onboarding.citizen.model.ConsentDetails; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CitizenSpecificRepositoryImplTest { + + private ReactiveMongoTemplate mongoTemplate; + private CitizenSpecificRepositoryImpl repository; + + @BeforeEach + public void setUp() { + mongoTemplate = Mockito.mock(ReactiveMongoTemplate.class); + repository = new CitizenSpecificRepositoryImpl(mongoTemplate); + } + + @Test + void testFindByFiscalCodeAndTppId() { + String hashedFiscalCode = "hashedCode"; + String tppId = "tpp1"; + CitizenConsent citizenConsent = new CitizenConsent(); + citizenConsent.setId("1"); + citizenConsent.setFiscalCode(hashedFiscalCode); + + Map consents = new HashMap<>(); + ConsentDetails consentDetails = new ConsentDetails(); + consentDetails.setTppState(true); + consents.put(tppId, consentDetails); + citizenConsent.setConsents(consents); + + when(mongoTemplate.aggregate(Mockito.any(), Mockito.eq(CitizenConsent.class), Mockito.eq(CitizenConsent.class))) + .thenReturn(Flux.just(citizenConsent)); + + Mono result = repository.findByFiscalCodeAndTppId(hashedFiscalCode, tppId); + + Assertions.assertEquals(hashedFiscalCode, Objects.requireNonNull(result.block()).getFiscalCode()); + Mockito.verify(mongoTemplate).aggregate(Mockito.any(), Mockito.eq(CitizenConsent.class), Mockito.eq(CitizenConsent.class)); + } + + @Test + void testFindByFiscalCodeAndTppId_TppIdNull() { + String hashedFiscalCode = "hashedCode"; + String tppId = null; + Mono result = repository.findByFiscalCodeAndTppId(hashedFiscalCode, tppId); + + Assertions.assertNotEquals(Boolean.TRUE, result.hasElement().block()); + } + + @Test + void testFindByFiscalCodeAndTppId_EmptyResult() { + String hashedFiscalCode = "hashedCode"; + String tppId = "tpp1"; + + when(mongoTemplate.aggregate(Mockito.any(), Mockito.eq(CitizenConsent.class), Mockito.eq(CitizenConsent.class))) + .thenReturn(Flux.empty()); + + Mono result = repository.findByFiscalCodeAndTppId(hashedFiscalCode, tppId); + + Assertions.assertNull(result.block()); + } + + @Test + void testConsentKeyWrapperGetterSetter() { + CitizenSpecificRepositoryImpl.ConsentKeyWrapper consentKeyWrapper = new CitizenSpecificRepositoryImpl.ConsentKeyWrapper(); + + consentKeyWrapper.setK("testKey"); + + Assertions.assertEquals("testKey", consentKeyWrapper.getK()); + } + + @Test + void testConsentKeyWrapperToString() { + + CitizenSpecificRepositoryImpl.ConsentKeyWrapper consentKeyWrapper = new CitizenSpecificRepositoryImpl.ConsentKeyWrapper(); + consentKeyWrapper.setK("testKey"); + + Assertions.assertEquals("CitizenSpecificRepositoryImpl.ConsentKeyWrapper(k=testKey)", consentKeyWrapper.toString()); + } + + @Test + void testConsentKeyWrapperEqualsAndHashCode() { + + CitizenSpecificRepositoryImpl.ConsentKeyWrapper consentKeyWrapper1 = new CitizenSpecificRepositoryImpl.ConsentKeyWrapper(); + CitizenSpecificRepositoryImpl.ConsentKeyWrapper consentKeyWrapper2 = new CitizenSpecificRepositoryImpl.ConsentKeyWrapper(); + consentKeyWrapper1.setK("testKey"); + consentKeyWrapper2.setK("testKey"); + + Assertions.assertEquals(consentKeyWrapper1, consentKeyWrapper2); + Assertions.assertEquals(consentKeyWrapper1.hashCode(), consentKeyWrapper2.hashCode()); + + consentKeyWrapper2.setK("differentKey"); + Assertions.assertNotEquals(consentKeyWrapper1, consentKeyWrapper2); + } +} diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/service/BloomFilterServiceTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/service/BloomFilterServiceTest.java new file mode 100644 index 0000000..9516cb4 --- /dev/null +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/service/BloomFilterServiceTest.java @@ -0,0 +1,52 @@ +package it.gov.pagopa.onboarding.citizen.service; + +import it.gov.pagopa.onboarding.citizen.faker.CitizenConsentFaker; +import it.gov.pagopa.onboarding.citizen.repository.CitizenRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Flux; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class BloomFilterServiceTest { + + @Mock + private CitizenRepository citizenRepository; + + @InjectMocks + private BloomFilterServiceImpl bloomFilterService; + + @BeforeEach + void setUp() { + + when(citizenRepository.findAll()).thenReturn(Flux.just(CitizenConsentFaker.mockInstance(true))); + bloomFilterService.initializeBloomFilter(); + } + + @Test + void testMightContain() { + assertTrue(bloomFilterService.mightContain("fiscalCode")); + assertFalse(bloomFilterService.mightContain("nonExistentFiscalCode")); + } + + @Test + void testUpdate() { + when(citizenRepository.findAll()).thenReturn(Flux.just(CitizenConsentFaker.mockInstance(true))); + bloomFilterService.update(); + assertTrue(bloomFilterService.mightContain("fiscalCode")); + assertFalse(bloomFilterService.mightContain("nonexistentHashedFiscalCode")); + } + + @Test + void testAdd() { + bloomFilterService.add("fiscalCode3"); + assertTrue(bloomFilterService.mightContain("fiscalCode3")); + } +} diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java index 5f3b54e..34961c8 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java @@ -1,38 +1,46 @@ package it.gov.pagopa.onboarding.citizen.service; -import it.gov.pagopa.common.utils.Utils; import it.gov.pagopa.common.web.exception.ClientExceptionWithBody; +import it.gov.pagopa.onboarding.citizen.configuration.ExceptionMap; +import it.gov.pagopa.onboarding.citizen.connector.tpp.TppConnectorImpl; import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import it.gov.pagopa.onboarding.citizen.dto.TppDTO; import it.gov.pagopa.onboarding.citizen.dto.mapper.CitizenConsentObjectToDTOMapper; -import it.gov.pagopa.common.web.exception.EmdEncryptionException; -import it.gov.pagopa.onboarding.citizen.configuration.ExceptionMap; import it.gov.pagopa.onboarding.citizen.faker.CitizenConsentDTOFaker; import it.gov.pagopa.onboarding.citizen.faker.CitizenConsentFaker; +import it.gov.pagopa.onboarding.citizen.faker.TppDTOFaker; import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import it.gov.pagopa.onboarding.citizen.model.ConsentDetails; import it.gov.pagopa.onboarding.citizen.model.mapper.CitizenConsentDTOToObjectMapper; import it.gov.pagopa.onboarding.citizen.repository.CitizenRepository; +import it.gov.pagopa.onboarding.citizen.validation.CitizenConsentValidationServiceImpl; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.function.Executable; -import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import java.time.LocalDateTime; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; @ExtendWith({SpringExtension.class, MockitoExtension.class}) @ContextConfiguration(classes = { CitizenServiceImpl.class, + CitizenConsentValidationServiceImpl.class, CitizenConsentObjectToDTOMapper.class, CitizenConsentDTOToObjectMapper.class, ExceptionMap.class @@ -42,86 +50,149 @@ class CitizenServiceTest { @Autowired CitizenServiceImpl citizenService; + @MockBean + CitizenConsentValidationServiceImpl validationService; + @MockBean CitizenRepository citizenRepository; + @MockBean + TppConnectorImpl tppConnector; + @Autowired CitizenConsentObjectToDTOMapper dtoMapper; private static final String FISCAL_CODE = "fiscalCode"; - private static final String HASHED_FISCAL_CODE = Utils.createSHA256(FISCAL_CODE); private static final String TPP_ID = "tppId"; private static final boolean TPP_STATE = true; private static final CitizenConsent CITIZEN_CONSENT = CitizenConsentFaker.mockInstance(true); private static final CitizenConsentDTO CITIZEN_CONSENT_DTO = CitizenConsentDTOFaker.mockInstance(true); + private static final TppDTO TPP_DTO = TppDTOFaker.mockInstance(); + @Test void createCitizenConsent_Ok() { - CitizenConsentDTO citizenConsentDTO = dtoMapper.map(CITIZEN_CONSENT); - - Mockito.when(citizenRepository.save(Mockito.any())) - .thenReturn(Mono.just(CITIZEN_CONSENT)); + CitizenConsentDTO expectedConsentDTO = dtoMapper.map(CITIZEN_CONSENT); + TppDTO activeTppDTO = TPP_DTO; + activeTppDTO.setState(true); - Mockito.when(citizenRepository.findById(anyString())) - .thenReturn(Mono.empty()); + when(tppConnector.get(anyString())).thenReturn(Mono.just(activeTppDTO)); + when(citizenRepository.save(Mockito.any())).thenReturn(Mono.just(CITIZEN_CONSENT)); + when(citizenRepository.findByFiscalCode(anyString())).thenReturn(Mono.empty()); + when(validationService.validateTppAndSaveConsent(anyString(), anyString(), any(CitizenConsent.class))) + .thenReturn(Mono.just(expectedConsentDTO)); CitizenConsentDTO response = citizenService.createCitizenConsent(CITIZEN_CONSENT_DTO).block(); assertNotNull(response); - - assertEquals(citizenConsentDTO, response); + assertEquals(expectedConsentDTO, response); } @Test void createCitizenConsent_AlreadyExists() { - CitizenConsentDTO citizenConsentDTO = dtoMapper.map(CITIZEN_CONSENT); + CitizenConsentDTO expectedConsentDTO = dtoMapper.map(CITIZEN_CONSENT); + TppDTO activeTppDTO = TPP_DTO; + activeTppDTO.setState(true); - Mockito.when(citizenRepository.save(Mockito.any())) - .thenReturn(Mono.empty()); + when(tppConnector.get(anyString())).thenReturn(Mono.just(activeTppDTO)); - Mockito.when(citizenRepository.findById(anyString())) - .thenReturn(Mono.just(CITIZEN_CONSENT)); + when(citizenRepository.save(Mockito.any())).thenReturn(Mono.just(CITIZEN_CONSENT)); + when(citizenRepository.findByFiscalCode(anyString())).thenReturn(Mono.just(CITIZEN_CONSENT)); + when(citizenRepository.findByFiscalCodeAndTppId(anyString(), anyString())).thenReturn(Mono.just(CITIZEN_CONSENT)); + + when(validationService.handleExistingConsent(any(), anyString(), any())) + .thenReturn(Mono.just(expectedConsentDTO)); + + when(validationService.validateTppAndSaveConsent(anyString(), anyString(), any(CitizenConsent.class))) + .thenReturn(Mono.just(expectedConsentDTO)); CitizenConsentDTO response = citizenService.createCitizenConsent(CITIZEN_CONSENT_DTO).block(); + assertNotNull(response); + assertEquals(expectedConsentDTO, response); + } + + @Test + void createCitizenConsent_Ko_TppNotProvided() { + + CitizenConsentDTO incompleteConsentDTO = CitizenConsentDTOFaker.mockInstance(true); + incompleteConsentDTO.getConsents().clear(); + + when(tppConnector.get(anyString())).thenReturn(Mono.empty()); + when(citizenRepository.findByFiscalCodeAndTppId(anyString(), anyString())).thenReturn(Mono.empty()); + + when(validationService.validateTppAndSaveConsent(anyString(), anyString(), any(CitizenConsent.class))) + .thenReturn(Mono.error(new ClientExceptionWithBody(HttpStatus.BAD_REQUEST, "TPP_NOT_FOUND", "TPP does not exist or is not active"))); - assertEquals(citizenConsentDTO, response); + StepVerifier.create(citizenService.createCitizenConsent(incompleteConsentDTO)) + .expectErrorMatches(throwable -> throwable instanceof ClientExceptionWithBody && + "TPP does not exist or is not active".equals(throwable.getMessage()) && + "TPP_NOT_FOUND".equals(((ClientExceptionWithBody) throwable).getCode())) + .verify(); } @Test - void createCitizenConsent_Ko_EmdEncryptError() { - CitizenConsentDTO citizenConsentDTO = CitizenConsentDTOFaker.mockInstance(true); + void createCitizenConsent_Ko_TppInactive() { - try (MockedStatic mockedStatic = Mockito.mockStatic(Utils.class)) { - mockedStatic.when(() -> Utils.createSHA256(any())) - .thenThrow(EmdEncryptionException.class); + TppDTO inactiveTppDTO = TPP_DTO; + inactiveTppDTO.setState(false); - assertThrows(EmdEncryptionException.class, () -> citizenService.createCitizenConsent(citizenConsentDTO)); - } + when(tppConnector.get(anyString())).thenReturn(Mono.just(inactiveTppDTO)); + when(citizenRepository.findByFiscalCodeAndTppId(anyString(), anyString())).thenReturn(Mono.empty()); + when(citizenRepository.findByFiscalCode(anyString())).thenReturn(Mono.empty()); + + when(validationService.validateTppAndSaveConsent(anyString(), anyString(), any(CitizenConsent.class))) + .thenReturn(Mono.error(new ClientExceptionWithBody(HttpStatus.BAD_REQUEST, "TPP_NOT_FOUND", "TPP does not exist or is not active"))); + + CitizenConsentDTO citizenConsentDTO = dtoMapper.map(CITIZEN_CONSENT); + + StepVerifier.create(citizenService.createCitizenConsent(citizenConsentDTO)) + .expectErrorMatches(throwable -> throwable instanceof ClientExceptionWithBody && + "TPP does not exist or is not active".equals(throwable.getMessage()) && + "TPP_NOT_FOUND".equals(((ClientExceptionWithBody) throwable).getCode())) + .verify(); + + Mockito.verify(citizenRepository, Mockito.never()).save(Mockito.any()); } @Test void updateChannelState_Ok() { + TppDTO mockTppDTO = TppDTOFaker.mockInstance(); + mockTppDTO.setState(true); + + when(tppConnector.get(anyString())) + .thenReturn(Mono.just(mockTppDTO)); - Mockito.when(citizenRepository.findByHashedFiscalCodeAndTppId(HASHED_FISCAL_CODE, TPP_ID)) + when(citizenRepository.findByFiscalCodeAndTppId(FISCAL_CODE, TPP_ID)) .thenReturn(Mono.just(CITIZEN_CONSENT)); - Mockito.when(citizenRepository.save(Mockito.any())) + + when(citizenRepository.save(any())) .thenReturn(Mono.just(CITIZEN_CONSENT)); - CitizenConsentDTO response = citizenService.updateChannelState(FISCAL_CODE, TPP_ID, TPP_STATE).block(); + + CitizenConsentDTO response = citizenService.updateTppState(FISCAL_CODE, TPP_ID, TPP_STATE).block(); + assertNotNull(response); - assertEquals(TPP_STATE, response.getTppState()); + + assertEquals(TPP_STATE, response.getConsents().get(TPP_ID).getTppState()); } @Test void updateChannelState_Ko_CitizenNotOnboarded() { + TppDTO mockTppDTO = TppDTOFaker.mockInstance(); + mockTppDTO.setState(true); + + when(tppConnector.get(anyString())) + .thenReturn(Mono.just(mockTppDTO)); - Mockito.when(citizenRepository.findByHashedFiscalCodeAndTppId(HASHED_FISCAL_CODE, TPP_ID)) + when(citizenRepository.findByFiscalCodeAndTppId(FISCAL_CODE, TPP_ID)) .thenReturn(Mono.empty()); - Executable executable = () -> citizenService.updateChannelState(FISCAL_CODE, TPP_ID, true).block(); + + + Executable executable = () -> citizenService.updateTppState(FISCAL_CODE, TPP_ID, true).block(); ClientExceptionWithBody exception = assertThrows(ClientExceptionWithBody.class, executable); assertEquals("Citizen consent not founded during update state process", exception.getMessage()); @@ -131,7 +202,7 @@ void updateChannelState_Ko_CitizenNotOnboarded() { void getConsentStatus_Ok() { - Mockito.when(citizenRepository.findByHashedFiscalCodeAndTppId(HASHED_FISCAL_CODE, TPP_ID)) + when(citizenRepository.findByFiscalCodeAndTppId(FISCAL_CODE, TPP_ID)) .thenReturn(Mono.just(CITIZEN_CONSENT)); CitizenConsentDTO response = citizenService.getConsentStatus(FISCAL_CODE, TPP_ID).block(); @@ -141,7 +212,7 @@ void getConsentStatus_Ok() { @Test void getConsentStatus_Ko_CitizenNotOnboarded() { - Mockito.when(citizenRepository.findByHashedFiscalCodeAndTppId(HASHED_FISCAL_CODE, TPP_ID)) + when(citizenRepository.findByFiscalCodeAndTppId(FISCAL_CODE, TPP_ID)) .thenReturn(Mono.empty()); Executable executable = () -> citizenService.getConsentStatus(FISCAL_CODE, TPP_ID).block(); @@ -151,24 +222,45 @@ void getConsentStatus_Ko_CitizenNotOnboarded() { } @Test - void getListEnabledConsents_Ok() { + void testGetTppEnabledList_Success() { + Map consents = new HashMap<>(); - Mockito.when(citizenRepository.findByHashedFiscalCodeAndTppStateTrue(HASHED_FISCAL_CODE)) - .thenReturn(Flux.just(CITIZEN_CONSENT)); + consents.put("Tpp1", ConsentDetails.builder() + .tppState(true) + .tcDate(LocalDateTime.now()) + .build()); - List response = citizenService.getListEnabledConsents(FISCAL_CODE).block(); - assertNotNull(response); - assertEquals(1, response.size()); + consents.put("Tpp2", ConsentDetails.builder() + .tppState(false) + .tcDate(LocalDateTime.now()) + .build()); + + CitizenConsent citizenConsent = CitizenConsent.builder() + .fiscalCode(FISCAL_CODE) + .consents(consents) + .build(); + + when(citizenRepository.findByFiscalCode(FISCAL_CODE)).thenReturn(Mono.just(citizenConsent)); + + Mono> result = citizenService.getTppEnabledList(FISCAL_CODE); + + StepVerifier.create(result) + .expectNext(List.of("Tpp1")) + .verifyComplete(); } @Test - void getListAllConsents_Ok() { - Mockito.when(citizenRepository.findByHashedFiscalCode(HASHED_FISCAL_CODE)) - .thenReturn(Flux.just(CITIZEN_CONSENT)); + void get_Ok() { + + when(citizenRepository.findByFiscalCode(FISCAL_CODE)) + .thenReturn(Mono.just(CITIZEN_CONSENT)); + + CitizenConsentDTO response = citizenService.get(FISCAL_CODE).block(); - List response = citizenService.getListAllConsents(FISCAL_CODE).block(); assertNotNull(response); - assertEquals(1, response.size()); + + assertEquals(CITIZEN_CONSENT.getFiscalCode(), response.getFiscalCode()); } + } diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImplTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImplTest.java new file mode 100644 index 0000000..48165b7 --- /dev/null +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImplTest.java @@ -0,0 +1,201 @@ +package it.gov.pagopa.onboarding.citizen.validation; + +import it.gov.pagopa.common.web.exception.ClientExceptionWithBody; +import it.gov.pagopa.onboarding.citizen.connector.tpp.TppConnectorImpl; +import it.gov.pagopa.onboarding.citizen.dto.CitizenConsentDTO; +import it.gov.pagopa.onboarding.citizen.dto.TppDTO; +import it.gov.pagopa.onboarding.citizen.dto.mapper.CitizenConsentObjectToDTOMapper; +import it.gov.pagopa.onboarding.citizen.faker.CitizenConsentDTOFaker; +import it.gov.pagopa.onboarding.citizen.faker.CitizenConsentFaker; +import it.gov.pagopa.onboarding.citizen.faker.TppDTOFaker; +import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import it.gov.pagopa.onboarding.citizen.model.ConsentDetails; +import it.gov.pagopa.onboarding.citizen.repository.CitizenRepository; +import it.gov.pagopa.onboarding.citizen.service.BloomFilterServiceImpl; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@SpringBootTest +class CitizenConsentValidationServiceImplTest { + + @MockBean + BloomFilterServiceImpl bloomFilterService; + + @MockBean + CitizenRepository citizenRepository; + + @MockBean + TppConnectorImpl tppConnector; + + @MockBean + CitizenConsentObjectToDTOMapper dtoMapper; + + @Autowired + CitizenConsentValidationServiceImpl validationService; + + private static final CitizenConsent CITIZEN_CONSENT = CitizenConsentFaker.mockInstance(true); + private static final CitizenConsentDTO CITIZEN_CONSENT_DTO = CitizenConsentDTOFaker.mockInstance(true); + private static final TppDTO TPP_DTO = TppDTOFaker.mockInstance(); + + @Test + void handleExistingConsent_ConsentAlreadyExists() { + + CitizenConsent existingConsent = CITIZEN_CONSENT; + String tppId = "existingTppId"; + existingConsent.getConsents().put(tppId, CITIZEN_CONSENT.getConsents().get(tppId)); + + when(dtoMapper.map(existingConsent)).thenReturn(CITIZEN_CONSENT_DTO); + + when(citizenRepository.findAll()).thenReturn(Flux.just(CitizenConsentFaker.mockInstance(true))); + + doNothing().when(bloomFilterService).add(anyString()); + + CitizenConsentDTO result = validationService.handleExistingConsent(existingConsent, tppId, CITIZEN_CONSENT).block(); + + assertNotNull(result); + assertEquals(CITIZEN_CONSENT_DTO, result); + } + + @Test + void handleExistingConsent_NewConsentForTpp() { + + CitizenConsent existingConsent = CITIZEN_CONSENT; + TppDTO activeTppDTO = TPP_DTO; + activeTppDTO.setState(true); + + when(tppConnector.get(anyString())).thenReturn(Mono.just(activeTppDTO)); + when(citizenRepository.save(existingConsent)).thenReturn(Mono.just(existingConsent)); + when(dtoMapper.map(existingConsent)).thenReturn(CITIZEN_CONSENT_DTO); + + when(citizenRepository.findAll()).thenReturn(Flux.just(CitizenConsentFaker.mockInstance(true))); + + doNothing().when(bloomFilterService).add(anyString()); + + CitizenConsentDTO result = validationService.handleExistingConsent(existingConsent, activeTppDTO.getTppId(), CITIZEN_CONSENT).block(); + + assertNotNull(result); + assertEquals(CITIZEN_CONSENT_DTO, result); + } + + @Test + void validateTppAndSaveConsent_TppValidAndActive() { + + CitizenConsent citizenConsentWithValidConsent = CITIZEN_CONSENT; + String fiscalCode = citizenConsentWithValidConsent.getFiscalCode(); + TppDTO activeTppDTO = TPP_DTO; + activeTppDTO.setState(true); + + ConsentDetails consentDetails = new ConsentDetails(); + consentDetails.setTppState(true); + + citizenConsentWithValidConsent.getConsents().put(activeTppDTO.getTppId(), consentDetails); + + when(tppConnector.get(anyString())).thenReturn(Mono.just(activeTppDTO)); + when(citizenRepository.save(CITIZEN_CONSENT)).thenReturn(Mono.just(CITIZEN_CONSENT)); + when(dtoMapper.map(CITIZEN_CONSENT)).thenReturn(CITIZEN_CONSENT_DTO); + + when(citizenRepository.findAll()).thenReturn(Flux.just(CitizenConsentFaker.mockInstance(true))); + + doNothing().when(bloomFilterService).add(anyString()); + + CitizenConsentDTO result = validationService.validateTppAndSaveConsent(fiscalCode, activeTppDTO.getTppId(), CITIZEN_CONSENT).block(); + + assertNotNull(result); + assertEquals(CITIZEN_CONSENT_DTO, result); + verify(bloomFilterService, times(1)).add(fiscalCode); + } + + @Test + void validateTppAndSaveConsent_TppInvalid() { + + CitizenConsent citizenConsentWithInvalidConsent = CITIZEN_CONSENT; + String fiscalCode = citizenConsentWithInvalidConsent.getFiscalCode(); + + TppDTO inactiveTppDTO = TPP_DTO; + inactiveTppDTO.setState(false); + + ConsentDetails consentDetails = new ConsentDetails(); + consentDetails.setTppState(false); + + citizenConsentWithInvalidConsent.getConsents().put(inactiveTppDTO.getTppId(), consentDetails); + + when(tppConnector.get(anyString())).thenReturn(Mono.just(inactiveTppDTO)); + + StepVerifier.create(validationService.validateTppAndSaveConsent(fiscalCode, inactiveTppDTO.getTppId(), CITIZEN_CONSENT)) + .expectErrorMatches(throwable -> throwable instanceof ClientExceptionWithBody && + "TPP is not active or is invalid".equals(throwable.getMessage()) && + "TPP_NOT_FOUND".equals(((ClientExceptionWithBody) throwable).getCode())) + .verify(); + + verify(citizenRepository, never()).save(any()); + verify(bloomFilterService, never()).add(fiscalCode); + } + + @Test + void validateTppAndSaveConsent_TppNotFound() { + + String fiscalCode = CITIZEN_CONSENT.getFiscalCode(); + String tppId = "nonExistentTppId"; + + when(tppConnector.get(tppId)).thenReturn(Mono.error(new RuntimeException("TPP not found"))); + + StepVerifier.create(validationService.validateTppAndSaveConsent(fiscalCode, tppId, CITIZEN_CONSENT)) + .expectErrorMatches(throwable -> throwable instanceof ClientExceptionWithBody && + "TPP does not exist or is not active".equals(throwable.getMessage()) && + "TPP_NOT_FOUND".equals(((ClientExceptionWithBody) throwable).getCode())) + .verify(); + + verify(citizenRepository, never()).save(any()); + verify(bloomFilterService, never()).add(fiscalCode); + } + + @Test + void validateTppAndSaveConsent_TppInactive() { + + String fiscalCode = CITIZEN_CONSENT.getFiscalCode(); + TppDTO inactiveTppDTO = TPP_DTO; + inactiveTppDTO.setState(false); + + ConsentDetails consentDetails = new ConsentDetails(); + consentDetails.setTppState(false); + + CITIZEN_CONSENT.getConsents().put(inactiveTppDTO.getTppId(), consentDetails); + + when(tppConnector.get(anyString())).thenReturn(Mono.just(inactiveTppDTO)); + + StepVerifier.create(validationService.validateTppAndSaveConsent(fiscalCode, inactiveTppDTO.getTppId(), CITIZEN_CONSENT)) + .expectErrorMatches(throwable -> throwable instanceof ClientExceptionWithBody && + "TPP is not active or is invalid".equals(throwable.getMessage()) && + "TPP_NOT_FOUND".equals(((ClientExceptionWithBody) throwable).getCode())) + .verify(); + + verify(citizenRepository, never()).save(any()); + verify(bloomFilterService, never()).add(fiscalCode); + } + + @Test + void handleExistingConsent_TppNotFound() { + + String tppId = "nonExistentTppId"; + + when(tppConnector.get(anyString())).thenReturn(Mono.error(new RuntimeException("TPP not found"))); + + StepVerifier.create(validationService.handleExistingConsent(CITIZEN_CONSENT, tppId, CITIZEN_CONSENT)) + .expectErrorMatches(throwable -> throwable instanceof ClientExceptionWithBody && + "TPP does not exist or is not active".equals(throwable.getMessage()) && + "TPP_NOT_FOUND".equals(((ClientExceptionWithBody) throwable).getCode())) + .verify(); + + verify(citizenRepository, never()).save(any()); + verify(bloomFilterService, never()).add(anyString()); + } +} From 2f6f1af199418a45bd0bbd2d4145fcb4d6d5328a Mon Sep 17 00:00:00 2001 From: DanieleRanaldo Date: Wed, 20 Nov 2024 10:48:24 +0100 Subject: [PATCH 09/18] modified query --- .../citizen/repository/CitizenSpecificRepositoryImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java index 8d57830..071f1a5 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java @@ -25,12 +25,14 @@ public Mono findByFiscalCodeAndTppId(String fiscalCode, String t Aggregation aggregation = Aggregation.newAggregation( Aggregation.match(Criteria.where("fiscalCode").is(fiscalCode)), - Aggregation.match(Criteria.where("consents." + tppId).exists(true)) + Aggregation.match(Criteria.where("consents." + tppId).exists(true)), + Aggregation.project("fiscalCode").and("consents." + tppId).as("consents") ); return mongoTemplate.aggregate(aggregation, CitizenConsent.class, CitizenConsent.class) .next(); } + @Data public static class ConsentKeyWrapper { private String k; From 24f2de5139d3b962b4b4a6a5ca51a634785e1e39 Mon Sep 17 00:00:00 2001 From: Vitolo-Andrea <157486351+Vitolo-Andrea@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:05:24 +0100 Subject: [PATCH 10/18] fix: Api update (#9) Co-authored-by: DanieleRanaldo --- pom.xml | 6 +- .../common/configuration/MongoConfig.java | 15 +- .../common/kafka/utils/KafkaConstants.java | 17 ++ .../kafka/consumer/BaseKafkaConsumer.java | 178 ++++++++++++++++++ .../kafka/exception/UncommittableError.java | 11 ++ .../reactive/utils/PerformanceLogger.java | 54 ++++++ .../pagopa/common/utils/CommonUtilities.java | 80 ++++++++ .../gov/pagopa/common/web/dto/ErrorDTO.java | 2 - .../common/web/exception/ErrorManager.java | 20 +- .../exception/ServiceExceptionHandler.java | 11 +- .../exception/ValidationExceptionHandler.java | 40 ++-- .../citizen/controller/CitizenController.java | 43 ++--- .../controller/CitizenControllerImpl.java | 22 ++- .../citizen/repository/CitizenRepository.java | 1 - .../repository/CitizenSpecificRepository.java | 3 + .../CitizenSpecificRepositoryImpl.java | 21 ++- .../citizen/service/CitizenService.java | 6 +- .../citizen/service/CitizenServiceImpl.java | 33 +++- .../common/configuration/MongoConfigTest.java | 111 +++++++++++ .../reactive/kafka/BaseKafkaConsumerTest.java | 87 +++++++++ .../reactive/kafka/TestKafkaConsumer.java | 51 +++++ .../reactive/utils/PerformanceLoggerTest.java | 71 +++++++ .../common/utils/CommonUtilitiesTest.java | 128 +++++++++++++ .../web/exception/ErrorManagerTest.java | 41 +++- .../ServiceExceptionHandlerTest.java | 4 +- .../ValidationExceptionHandlerTest.java | 39 +--- .../controller/CitizenControllerTest.java | 6 +- .../CitizenSpecificRepositoryImplTest.java | 93 +++++---- .../citizen/service/CitizenServiceTest.java | 6 +- 29 files changed, 1039 insertions(+), 161 deletions(-) create mode 100644 src/main/java/it/gov/pagopa/common/kafka/utils/KafkaConstants.java create mode 100644 src/main/java/it/gov/pagopa/common/reactive/kafka/consumer/BaseKafkaConsumer.java create mode 100644 src/main/java/it/gov/pagopa/common/reactive/kafka/exception/UncommittableError.java create mode 100644 src/main/java/it/gov/pagopa/common/reactive/utils/PerformanceLogger.java create mode 100644 src/main/java/it/gov/pagopa/common/utils/CommonUtilities.java create mode 100644 src/test/java/it/gov/pagopa/common/configuration/MongoConfigTest.java create mode 100644 src/test/java/it/gov/pagopa/common/reactive/kafka/BaseKafkaConsumerTest.java create mode 100644 src/test/java/it/gov/pagopa/common/reactive/kafka/TestKafkaConsumer.java create mode 100644 src/test/java/it/gov/pagopa/common/reactive/utils/PerformanceLoggerTest.java create mode 100644 src/test/java/it/gov/pagopa/common/utils/CommonUtilitiesTest.java diff --git a/pom.xml b/pom.xml index a306984..1913399 100644 --- a/pom.xml +++ b/pom.xml @@ -51,8 +51,10 @@ spring-cloud-stream-test-support test - - + + org.springframework.cloud + spring-cloud-starter-stream-kafka + diff --git a/src/main/java/it/gov/pagopa/common/configuration/MongoConfig.java b/src/main/java/it/gov/pagopa/common/configuration/MongoConfig.java index aa906e0..259f24e 100644 --- a/src/main/java/it/gov/pagopa/common/configuration/MongoConfig.java +++ b/src/main/java/it/gov/pagopa/common/configuration/MongoConfig.java @@ -2,10 +2,12 @@ import com.mongodb.lang.NonNull; import it.gov.pagopa.common.utils.CommonConstants; +import lombok.Getter; import lombok.Setter; import org.bson.types.Decimal128; import org.springframework.boot.autoconfigure.mongo.MongoClientSettingsBuilderCustomizer; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; @@ -20,16 +22,19 @@ import java.util.concurrent.TimeUnit; @Configuration +@EnableConfigurationProperties(MongoConfig.MongoDbCustomProperties.class) public class MongoConfig { - @Configuration @ConfigurationProperties(prefix = "spring.data.mongodb.config") + @Getter + @Setter public static class MongoDbCustomProperties { - @Setter + ConnectionPoolSettings connectionPool; + @Getter @Setter - static class ConnectionPoolSettings { + public static class ConnectionPoolSettings { int maxSize; int minSize; long maxWaitTimeMS; @@ -67,7 +72,7 @@ public MongoCustomConversions mongoCustomConversions() { } @WritingConverter - private static class BigDecimalDecimal128Converter implements Converter { + public static class BigDecimalDecimal128Converter implements Converter { @Override public Decimal128 convert(@NonNull BigDecimal source) { @@ -76,7 +81,7 @@ public Decimal128 convert(@NonNull BigDecimal source) { } @ReadingConverter - private static class Decimal128BigDecimalConverter implements Converter { + public static class Decimal128BigDecimalConverter implements Converter { @Override public BigDecimal convert(@NonNull Decimal128 source) { diff --git a/src/main/java/it/gov/pagopa/common/kafka/utils/KafkaConstants.java b/src/main/java/it/gov/pagopa/common/kafka/utils/KafkaConstants.java new file mode 100644 index 0000000..6f847ed --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/kafka/utils/KafkaConstants.java @@ -0,0 +1,17 @@ +package it.gov.pagopa.common.kafka.utils; + +public class KafkaConstants { + private KafkaConstants(){} + +//region error-topic headers + public static final String ERROR_MSG_HEADER_APPLICATION_NAME = "applicationName"; + public static final String ERROR_MSG_HEADER_GROUP = "group"; + public static final String ERROR_MSG_HEADER_SRC_TYPE = "srcType"; + public static final String ERROR_MSG_HEADER_SRC_SERVER = "srcServer"; + public static final String ERROR_MSG_HEADER_SRC_TOPIC = "srcTopic"; + public static final String ERROR_MSG_HEADER_DESCRIPTION = "description"; + public static final String ERROR_MSG_HEADER_RETRY = "retry"; + public static final String ERROR_MSG_HEADER_RETRYABLE = "retryable"; + public static final String ERROR_MSG_HEADER_STACKTRACE = "stacktrace"; +//endregion +} diff --git a/src/main/java/it/gov/pagopa/common/reactive/kafka/consumer/BaseKafkaConsumer.java b/src/main/java/it/gov/pagopa/common/reactive/kafka/consumer/BaseKafkaConsumer.java new file mode 100644 index 0000000..bfe2115 --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/reactive/kafka/consumer/BaseKafkaConsumer.java @@ -0,0 +1,178 @@ +package it.gov.pagopa.common.reactive.kafka.consumer; + +import com.fasterxml.jackson.databind.ObjectReader; +import it.gov.pagopa.common.kafka.utils.KafkaConstants; +import it.gov.pagopa.common.reactive.kafka.exception.UncommittableError; +import it.gov.pagopa.common.reactive.utils.PerformanceLogger; +import it.gov.pagopa.common.utils.CommonUtilities; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.kafka.support.Acknowledgment; +import org.springframework.kafka.support.KafkaHeaders; +import org.springframework.messaging.Message; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +/** + * Base class to extend in order to configure a timed commit behavior when using KafkaBinder. + * Other than extend this class, you should: + *
    + *
  1. Turn off the autoCommit (spring.cloud.stream.kafka.bindings.BINDINGNAME.consumer.autoCommitOffset=false)
  2. + *
  3. Set the ackMode to MANUAL_IMMEDIATE (spring.cloud.stream.kafka.bindings.BINDINGNAME.consumer.ackMode=MANUAL_IMMEDIATE)
  4. + *
+ * @param The type of the message to read and deserialize + * @param The type of the message resulted + */ +@Slf4j +public abstract class BaseKafkaConsumer { + + /** Key used inside the {@link Context} to store the startTime */ + protected static final String CONTEXT_KEY_START_TIME = "START_TIME"; + /** Key used inside the {@link Context} to store a msg identifier used for logging purpose */ + protected static final String CONTEXT_KEY_MSG_ID = "MSG_ID"; + + private final String applicationName; + + private static final Collector, ?, Map>>> kafkaAcknowledgeResultMapCollector = + Collectors.groupingBy(KafkaAcknowledgeResult::partition + , Collectors.teeing( + Collectors.minBy(Comparator.comparing(KafkaAcknowledgeResult::offset)), + Collectors.maxBy(Comparator.comparing(KafkaAcknowledgeResult::offset)), + (min, max) -> Pair.of( + min.map(KafkaAcknowledgeResult::offset).orElse(null), + max.orElse(null)) + )); + + protected BaseKafkaConsumer(String applicationName) { + this.applicationName = applicationName; + } + + record KafkaAcknowledgeResult (Acknowledgment ack, Integer partition, Long offset, T result){ + public KafkaAcknowledgeResult(Message message, T result) { + this( + (Acknowledgment) CommonUtilities.getHeaderValue(message, KafkaHeaders.ACKNOWLEDGMENT), + getMessagePartitionId(message), + getMessageOffset(message), + result + ); + } + } + + private static Integer getMessagePartitionId(Message message) { + return (Integer) CommonUtilities.getHeaderValue(message, KafkaHeaders.RECEIVED_PARTITION); + } + + private static Long getMessageOffset(Message message) { + return (Long) CommonUtilities.getHeaderValue(message, KafkaHeaders.OFFSET); + } + + /** It will ask the superclass to handle the messages, then sequentially it will acknowledge them */ + public final void execute(Flux> messagesFlux) { + Flux> processUntilCommits = + messagesFlux + .flatMapSequential(this::executeAcknowledgeAware) + + .buffer(getCommitDelay()) + .map(p -> { + Map>> partition2Offsets = p.stream() + .collect(kafkaAcknowledgeResultMapCollector); + + log.info("[KAFKA_COMMIT][{}] Committing {} messages: {}", getFlowName(), p.size(), + partition2Offsets.entrySet().stream() + .map(e->"partition %d: %d - %d".formatted(e.getKey(),e.getValue().getKey(), e.getValue().getValue().offset())) + .collect(Collectors.joining(";"))); + + partition2Offsets.forEach((partition, offsets) -> Optional.ofNullable(offsets.getValue().ack()).ifPresent(Acknowledgment::acknowledge)); + + return p.stream() + .map(KafkaAcknowledgeResult::result) + .filter(Objects::nonNull) + .toList(); + } + ); + + subscribeAfterCommits(processUntilCommits); + } + + /** The {@link Duration} to wait before to commit processed messages */ + protected abstract Duration getCommitDelay(); + + /** {@link Flux} to which subscribe in order to start its execution and eventually perform some logic on results */ + protected abstract void subscribeAfterCommits(Flux> afterCommits2subscribe); + + private Mono> executeAcknowledgeAware(Message message) { + KafkaAcknowledgeResult defaultAck = new KafkaAcknowledgeResult<>(message, null); + + byte[] retryingApplicationName = message.getHeaders().get(KafkaConstants.ERROR_MSG_HEADER_APPLICATION_NAME, byte[].class); + if(retryingApplicationName != null && !new String(retryingApplicationName, StandardCharsets.UTF_8).equals(this.applicationName)){ + log.info("[{}] Discarding message due to other application retry ({}): {}", getFlowName(), retryingApplicationName, CommonUtilities.readMessagePayload(message)); + return Mono.just(defaultAck); + } + + Map ctx=new HashMap<>(); + ctx.put(CONTEXT_KEY_START_TIME, System.currentTimeMillis()); + ctx.put(CONTEXT_KEY_MSG_ID, CommonUtilities.readMessagePayload(message)); + + return execute(message, ctx) + .map(r -> new KafkaAcknowledgeResult<>(message, r)) + .defaultIfEmpty(defaultAck) + + .onErrorResume(e -> { + if(e instanceof UncommittableError) { + return Mono.error(e); + } else { + return Mono.just(defaultAck); + } + }) + .doOnNext(r -> doFinally(message, ctx)) + + .onErrorResume(e -> { + log.info("Retrying after reactive pipeline error: ", e); + return executeAcknowledgeAware(message); + }); + } + + /** to perform some operation at the end of business logic execution, thus before to wait for commit. As default, it will perform an INFO logging with performance time */ + protected void doFinally(Message message, Map ctx) { + Long startTime = (Long)ctx.get(CONTEXT_KEY_START_TIME); + String msgId = (String)ctx.get(CONTEXT_KEY_MSG_ID); + if(startTime != null){ + PerformanceLogger.logTiming(getFlowName(), startTime, + "(partition: %s, offset: %s) %s".formatted(getMessagePartitionId(message), getMessageOffset(message), msgId)); + } + } + + /** Name used for logging purpose */ + public String getFlowName() { + return getClass().getSimpleName(); + } + + /** It will deserialize the message and then call the {@link #execute(Object, Message, Map)} method */ + protected Mono execute(Message message, Map ctx){ + return Mono.just(message) + .mapNotNull(this::deserializeMessage) + .flatMap(payload->execute(payload, message, ctx)); + } + + /** The {@link ObjectReader} to use in order to deserialize the input message */ + protected abstract ObjectReader getObjectReader(); + /** The action to take if the deserialization will throw an error */ + protected abstract Consumer onDeserializationError(Message message); + + /** The function invoked in order to process the current message */ + protected abstract Mono execute(T payload, Message message, Map ctx); + + /** It will read and deserialize {@link Message#getPayload()} using the given {@link #getObjectReader()} */ + protected T deserializeMessage(Message message) { + return CommonUtilities.deserializeMessage(message, getObjectReader(),null/*onDeserializationError(message)*/); + } + +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/common/reactive/kafka/exception/UncommittableError.java b/src/main/java/it/gov/pagopa/common/reactive/kafka/exception/UncommittableError.java new file mode 100644 index 0000000..57c5932 --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/reactive/kafka/exception/UncommittableError.java @@ -0,0 +1,11 @@ +package it.gov.pagopa.common.reactive.kafka.exception; + +public class UncommittableError extends RuntimeException { + public UncommittableError(String message){ + super(message); + } + + public UncommittableError(String message, Exception e){ + super(message, e); + } +} diff --git a/src/main/java/it/gov/pagopa/common/reactive/utils/PerformanceLogger.java b/src/main/java/it/gov/pagopa/common/reactive/utils/PerformanceLogger.java new file mode 100644 index 0000000..3aad658 --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/reactive/utils/PerformanceLogger.java @@ -0,0 +1,54 @@ +package it.gov.pagopa.common.reactive.utils; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.function.Function; + +@Slf4j +public class PerformanceLogger { + + private PerformanceLogger(){} + +//region Mono ops + public static Mono logTimingOnNext(String flowName, Mono publisher, Function data2LogPayload){ + return logTimingOnNext(flowName, System.currentTimeMillis(), publisher, data2LogPayload); + } + public static Mono logTimingOnNext(String flowName, long startTime, Mono publisher, Function data2LogPayload){ + return publisher + .doOnNext(x -> logTiming(flowName, startTime, data2LogPayload!=null? data2LogPayload.apply(x) : "")); + } + + public static Mono logTimingFinally(String flowName, Mono publisher, String logPayload){ + return logTimingFinally(flowName, System.currentTimeMillis(), publisher, logPayload); + } + public static Mono logTimingFinally(String flowName, long startTime, Mono publisher, String logPayload){ + return publisher + .doFinally(x -> logTiming(flowName, startTime, ObjectUtils.firstNonNull(logPayload, ""))); + } +//endregion + +//region Flux ops + public static Flux logTimingOnNext(String flowName, Flux publisher, Function data2LogPayload){ + return logTimingOnNext(flowName, System.currentTimeMillis(), publisher, data2LogPayload); + } + public static Flux logTimingOnNext(String flowName, long startTime, Flux publisher, Function data2LogPayload){ + return publisher + .doOnNext(x -> logTiming(flowName, startTime, data2LogPayload!=null? data2LogPayload.apply(x) : "")); + } + + public static Flux logTimingFinally(String flowName, Flux publisher, String logPayload){ + return logTimingFinally(flowName, System.currentTimeMillis(), publisher, logPayload); + } + public static Flux logTimingFinally(String flowName, long startTime, Flux publisher, String logPayload){ + return publisher + .doFinally(x -> logTiming(flowName, startTime, ObjectUtils.firstNonNull(logPayload, ""))); + } +//endregion + + public static void logTiming(String flowName, long startTime, String logPayload){ + log.info("[PERFORMANCE_LOG] [{}] Time occurred to perform business logic: {} ms {}", flowName, System.currentTimeMillis() - startTime, logPayload); + } +} diff --git a/src/main/java/it/gov/pagopa/common/utils/CommonUtilities.java b/src/main/java/it/gov/pagopa/common/utils/CommonUtilities.java new file mode 100644 index 0000000..32bec5f --- /dev/null +++ b/src/main/java/it/gov/pagopa/common/utils/CommonUtilities.java @@ -0,0 +1,80 @@ +package it.gov.pagopa.common.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectReader; +import it.gov.pagopa.common.web.exception.EmdEncryptionException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.Message; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.function.Consumer; + +@Slf4j +public class CommonUtilities { + private CommonUtilities() {} + + public static final DecimalFormatSymbols decimalFormatterSymbols = new DecimalFormatSymbols(); + public static final DecimalFormat decimalFormatter; + + static { + decimalFormatterSymbols.setDecimalSeparator(','); + decimalFormatter = new DecimalFormat("0.00", CommonUtilities.decimalFormatterSymbols); + } + + /** It will try to deserialize a message, eventually notifying the error */ + public static T deserializeMessage(Message message, ObjectReader objectReader, Consumer onError) { + try { + String payload = readMessagePayload(message); + return objectReader.readValue(payload); + } catch (JsonProcessingException e) { + onError.accept(e); + return null; + } + } + + /** It will read message payload checking if it's a byte[] or String */ + public static String readMessagePayload(Message message) { + String payload; + if(message.getPayload() instanceof byte[] bytes){ + payload=new String(bytes); + } else { + payload= message.getPayload().toString(); + } + return payload; + } + + /** To read Message header value */ + public static Object getHeaderValue(Message message, String headerName) { + return message.getHeaders().get(headerName); + } + + /** To read {@link org.apache.kafka.common.header.Header} value */ + public static String getByteArrayHeaderValue(Message message, String headerName) { + byte[] headerValue = message.getHeaders().get(headerName, byte[].class); + return headerValue!=null? new String(headerValue, StandardCharsets.UTF_8) : null; + } + + /** To convert cents into euro */ + public static String createSHA256(String fiscalCode) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] encodedhash = md.digest(fiscalCode.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(2 * encodedhash.length); + for (byte b : encodedhash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + log.info("Something went wrong creating SHA256"); + throw new EmdEncryptionException("Something went wrong creating SHA256",true,e); + } + } +} diff --git a/src/main/java/it/gov/pagopa/common/web/dto/ErrorDTO.java b/src/main/java/it/gov/pagopa/common/web/dto/ErrorDTO.java index 3e4622d..98a1b67 100644 --- a/src/main/java/it/gov/pagopa/common/web/dto/ErrorDTO.java +++ b/src/main/java/it/gov/pagopa/common/web/dto/ErrorDTO.java @@ -5,14 +5,12 @@ import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Data; -import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @JsonInclude(JsonInclude.Include.NON_NULL) @AllArgsConstructor @NoArgsConstructor @Data -@EqualsAndHashCode public class ErrorDTO implements ServiceExceptionPayload { @NotBlank diff --git a/src/main/java/it/gov/pagopa/common/web/exception/ErrorManager.java b/src/main/java/it/gov/pagopa/common/web/exception/ErrorManager.java index 437672b..7b37f7a 100644 --- a/src/main/java/it/gov/pagopa/common/web/exception/ErrorManager.java +++ b/src/main/java/it/gov/pagopa/common/web/exception/ErrorManager.java @@ -5,6 +5,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.lang.Nullable; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -22,9 +23,9 @@ public ErrorManager(@Nullable ErrorDTO defaultErrorDTO) { } @ExceptionHandler(RuntimeException.class) - protected ResponseEntity handleException(RuntimeException error) { + protected ResponseEntity handleException(RuntimeException error, ServerHttpRequest request) { - logClientException(error); + logClientException(error, request); if(error instanceof ClientExceptionNoBody clientExceptionNoBody){ return ResponseEntity.status(clientExceptionNoBody.getHttpStatus()).build(); @@ -45,14 +46,14 @@ protected ResponseEntity handleException(RuntimeException error) { .body(errorDTO); } } - public static void logClientException(RuntimeException error) { + public static void logClientException(RuntimeException error, ServerHttpRequest request) { Throwable unwrappedException = error.getCause() instanceof ServiceException ? error.getCause() : error; String clientExceptionMessage = ""; if(error instanceof ClientException clientException) { - clientExceptionMessage = "HttpStatus %s - %s%s".formatted( + clientExceptionMessage = ": HttpStatus %s - %s%s".formatted( clientException.getHttpStatus(), (clientException instanceof ClientExceptionWithBody clientExceptionWithBody) ? clientExceptionWithBody.getCode() + ": " : "", clientException.getMessage() @@ -60,10 +61,17 @@ public static void logClientException(RuntimeException error) { } if(!(error instanceof ClientException clientException) || clientException.isPrintStackTrace() || unwrappedException.getCause() != null){ - log.error("Something went wrong : {}", clientExceptionMessage, unwrappedException); + log.error("Something went wrong handling request {}{}", getRequestDetails(request), clientExceptionMessage, unwrappedException); } else { - log.info("{}",clientExceptionMessage); + log.info("A {} occurred handling request {}{} at {}", + unwrappedException.getClass().getSimpleName() , + getRequestDetails(request), + clientExceptionMessage, + unwrappedException.getStackTrace().length > 0 ? unwrappedException.getStackTrace()[0] : "UNKNOWN"); } } + public static String getRequestDetails(ServerHttpRequest request) { + return "%s %s".formatted(request.getMethod(), request.getURI()); + } } diff --git a/src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandler.java b/src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandler.java index 0940802..4763292 100644 --- a/src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandler.java +++ b/src/main/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandler.java @@ -7,6 +7,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -26,11 +27,11 @@ public ServiceExceptionHandler(ErrorManager errorManager, Map handleException(ServiceException error) { + protected ResponseEntity handleException(ServiceException error, ServerHttpRequest request) { if (null != error.getPayload()) { - return handleBodyProvidedException(error); + return handleBodyProvidedException(error, request); } - return errorManager.handleException(transcodeException(error)); + return errorManager.handleException(transcodeException(error), request); } private ClientException transcodeException(ServiceException error) { @@ -44,9 +45,9 @@ private ClientException transcodeException(ServiceException error) { return new ClientExceptionWithBody(httpStatus, error.getCode(), error.getMessage(), error.isPrintStackTrace(), error); } - private ResponseEntity handleBodyProvidedException(ServiceException error) { + private ResponseEntity handleBodyProvidedException(ServiceException error, ServerHttpRequest request) { ClientException clientException = transcodeException(error); - ErrorManager.logClientException(clientException); + ErrorManager.logClientException(clientException, request); return ResponseEntity.status(clientException.getHttpStatus()) .contentType(MediaType.APPLICATION_JSON) diff --git a/src/main/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandler.java b/src/main/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandler.java index 7450d23..cecf4fd 100644 --- a/src/main/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandler.java +++ b/src/main/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandler.java @@ -1,9 +1,11 @@ package it.gov.pagopa.common.web.exception; + import it.gov.pagopa.common.web.dto.ErrorDTO; import lombok.extern.slf4j.Slf4j; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.lang.Nullable; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -11,7 +13,6 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.reactive.resource.NoResourceFoundException; -import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.MissingRequestValueException; import java.util.Optional; @@ -32,48 +33,41 @@ public ValidationExceptionHandler(@Nullable ErrorDTO templateValidationErrorDTO) @ExceptionHandler(WebExchangeBindException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorDTO handleWebExchangeBindException( - WebExchangeBindException e) { + WebExchangeBindException ex, ServerHttpRequest request) { - String message = e.getBindingResult().getAllErrors().stream() + String message = ex.getBindingResult().getAllErrors().stream() .map(error -> { String fieldName = ((FieldError) error).getField(); String errorMessage = error.getDefaultMessage(); return String.format("[%s]: %s", fieldName, errorMessage); }).collect(Collectors.joining("; ")); - log.info("A WebExchangeBindException occurred : HttpStatus 400 - {}", message); - log.debug("Something went wrong while validating http request", e); + log.info("A WebExchangeBindException occurred handling request {}: HttpStatus 400 - {}", + ErrorManager.getRequestDetails(request), message); + log.debug("Something went wrong while validating http request", ex); return new ErrorDTO(templateValidationErrorDTO.getCode(), message); } @ExceptionHandler(MissingRequestValueException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) - public ErrorDTO handleMissingRequestValueException(MissingRequestValueException e) { - log.info("A MissingRequestValueException occurred : HttpStatus 400 - Something went wrong due to a missing request value"); + public ErrorDTO handleMissingRequestValueException(MissingRequestValueException e, ServerHttpRequest request) { + + log.info("A MissingRequestValueException occurred handling request {}: HttpStatus 400 - {}", + ErrorManager.getRequestDetails(request), e.getMessage()); log.debug("Something went wrong due to a missing request value", e); - return new ErrorDTO(templateValidationErrorDTO.getCode(), "Something went wrong due to a missing request value"); + return new ErrorDTO(templateValidationErrorDTO.getCode(), templateValidationErrorDTO.getMessage()); } @ExceptionHandler(NoResourceFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) - public ErrorDTO handleNoResourceFoundException(NoResourceFoundException e) { - - log.info("A NoResourceFoundException occurred : HttpStatus 400 - Something went wrong due to a missing static resource"); - log.debug("Something went wrong due to a missing static resource", e); - - return new ErrorDTO(templateValidationErrorDTO.getCode(), "Something went wrong due to a missing static resource"); - } + public ErrorDTO handleNoResourceFoundException(NoResourceFoundException e, ServerHttpRequest request) { - @ExceptionHandler(MethodNotAllowedException.class) - @ResponseStatus(HttpStatus.NOT_FOUND) - public ErrorDTO handleMethodNotAllowedException(MethodNotAllowedException e) { - - log.info("A MethodNotAllowedException occurred : HttpStatus 405 - Request is not supported"); - log.debug("Something went wrong due to a request not supported", e); + log.info("A NoResourceFoundException occurred handling request {}: HttpStatus 400 - {}", + ErrorManager.getRequestDetails(request), e.getMessage()); + log.debug("Something went wrong due to a missing request value", e); - return new ErrorDTO(templateValidationErrorDTO.getCode(), "Request is not supported"); + return new ErrorDTO(templateValidationErrorDTO.getCode(), templateValidationErrorDTO.getMessage()); } - } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenController.java b/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenController.java index 34c7d80..397ba0f 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenController.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenController.java @@ -20,45 +20,40 @@ public interface CitizenController { @PostMapping("") Mono> saveCitizenConsent(@Valid @RequestBody CitizenConsentDTO citizenConsentDTO); - /** - * Get the consent status for a specific citizen and channel. - * - * @param fiscalCode the fiscal code of the citizen - * @param tppId the ID of the tpp - * @return the citizen consent status - */ - @GetMapping("/{fiscalCode}/{tppId}") - Mono> getConsentStatus(@PathVariable @Pattern(regexp = FISCAL_CODE_STRUCTURE_REGEX, message = "Invalid fiscal code format") String fiscalCode, @PathVariable String tppId); - - /** - * Update the state of a citizen's consent. - * - * @param citizenConsentStateUpdateDTO contains the hashedFiscalCode, channelId, and channelState to update - * @return the updated citizen consents - */ @PutMapping("/stateUpdate") Mono> stateUpdate(@Valid @RequestBody CitizenConsentStateUpdateDTO citizenConsentStateUpdateDTO); + @GetMapping("/filter/{fiscalCode}") + Mono> bloomFilterSearch(@PathVariable @Pattern(regexp = FISCAL_CODE_STRUCTURE_REGEX, message = "Invalid fiscal code format") String fiscalCode); + + @GetMapping("/list/{fiscalCode}/enabled/tpp") + Mono>> getTppEnabledList(@PathVariable @Pattern(regexp = FISCAL_CODE_STRUCTURE_REGEX, message = "Invalid fiscal code format") String fiscalCode); + /** - * List all channels with enabled consents for a specific citizen. + * Get the consent status for a specific citizen and tpp. * * @param fiscalCode the fiscal code of the citizen - * @return a list of channels with enabled consents + * @param tppId the ID of the tpp + * @return the citizen consent status */ - @GetMapping("/list/{fiscalCode}/enabled") - Mono>> getTppEnabledList(@PathVariable @Pattern(regexp = FISCAL_CODE_STRUCTURE_REGEX, message = "Invalid fiscal code format") String fiscalCode); + @GetMapping("/{fiscalCode}/{tppId}") + Mono> getCitizenConsentStatus(@PathVariable @Pattern(regexp = FISCAL_CODE_STRUCTURE_REGEX, message = "Invalid fiscal code format") String fiscalCode, @PathVariable String tppId); + /** - * List all channels and their consent status for a specific citizen. + * Get consents for a specific citizen. * * @param fiscalCode the fiscal code of the citizen * @return a list of all channels with their consent statuses */ @GetMapping("/list/{fiscalCode}") - Mono> get(@PathVariable @Pattern(regexp = FISCAL_CODE_STRUCTURE_REGEX, message = "Invalid fiscal code format") String fiscalCode); + Mono> getCitizenConsentsList(@PathVariable @Pattern(regexp = FISCAL_CODE_STRUCTURE_REGEX, message = "Invalid fiscal code format") String fiscalCode); - @GetMapping("/filter/{fiscalCode}") - Mono> getAllFiscalCode(@PathVariable @Pattern(regexp = FISCAL_CODE_STRUCTURE_REGEX, message = "Invalid fiscal code format") String fiscalCode); + @GetMapping("/list/{fiscalCode}/enabled") + Mono> getCitizenConsentsListEnabled(@PathVariable @Pattern(regexp = FISCAL_CODE_STRUCTURE_REGEX, message = "Invalid fiscal code format") String fiscalCode); + + @GetMapping("/{tppId}") + Mono>> getCitizenEnabled(@PathVariable String tppId); } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerImpl.java index 18c3d33..ef78227 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerImpl.java @@ -40,8 +40,8 @@ public Mono> stateUpdate(@Valid CitizenConsent } @Override - public Mono> getConsentStatus(String fiscalCode, String tppId) { - return citizenService.getConsentStatus(fiscalCode, tppId) + public Mono> getCitizenConsentStatus(String fiscalCode, String tppId) { + return citizenService.getCitizenConsentStatus(fiscalCode, tppId) .map(ResponseEntity::ok); } @@ -52,13 +52,25 @@ public Mono>> getTppEnabledList(String fiscalCode) { } @Override - public Mono> get(String fiscalCode) { - return citizenService.get(fiscalCode) + public Mono> getCitizenConsentsList(String fiscalCode) { + return citizenService.getCitizenConsentsList(fiscalCode) .map(ResponseEntity::ok); } @Override - public Mono> getAllFiscalCode(String fiscalCode) { + public Mono> getCitizenConsentsListEnabled(String fiscalCode) { + return citizenService.getCitizenConsentsListEnabled(fiscalCode) + .map(ResponseEntity::ok); + } + + @Override + public Mono>> getCitizenEnabled(String tppId) { + return citizenService.getCitizenEnabled(tppId) + .map(ResponseEntity::ok); + } + + @Override + public Mono> bloomFilterSearch(String fiscalCode) { return Mono.fromCallable(() -> bloomFilterService.mightContain(fiscalCode) ? ResponseEntity.ok("OK") : diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenRepository.java b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenRepository.java index 92f80a4..9263ea7 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenRepository.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenRepository.java @@ -8,5 +8,4 @@ public interface CitizenRepository extends ReactiveMongoRepository findByFiscalCode(String fiscalCode); - } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepository.java b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepository.java index 2fccd8f..9305eb9 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepository.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepository.java @@ -1,10 +1,13 @@ package it.gov.pagopa.onboarding.citizen.repository; import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public interface CitizenSpecificRepository { Mono findByFiscalCodeAndTppId(String fiscalCode, String tppId); + + Flux findByTppIdEnabled(String tppId); } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java index 071f1a5..54e3781 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java @@ -6,6 +6,7 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @Repository @@ -23,16 +24,30 @@ public Mono findByFiscalCodeAndTppId(String fiscalCode, String t return Mono.empty(); } + String consent = "consents." + tppId; Aggregation aggregation = Aggregation.newAggregation( Aggregation.match(Criteria.where("fiscalCode").is(fiscalCode)), - Aggregation.match(Criteria.where("consents." + tppId).exists(true)), - Aggregation.project("fiscalCode").and("consents." + tppId).as("consents") + Aggregation.match(Criteria.where(consent).exists(true)), + Aggregation.project("fiscalCode").and(consent).as(consent) ); - return mongoTemplate.aggregate(aggregation, CitizenConsent.class, CitizenConsent.class) + return mongoTemplate.aggregate(aggregation, "citizen_consents", CitizenConsent.class) .next(); } + public Flux findByTppIdEnabled(String tppId) { + String consent = "consents." + tppId; + String tppStatePath = consent + ".tppState"; + + Aggregation aggregation = Aggregation.newAggregation( + Aggregation.match(Criteria.where(tppStatePath).is(true)), + Aggregation.project("fiscalCode").and(consent).as(consent) + ); + + return mongoTemplate.aggregate(aggregation, "citizen_consents", CitizenConsent.class); + } + + @Data public static class ConsentKeyWrapper { private String k; diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenService.java b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenService.java index f33a6a6..5aabd00 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenService.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenService.java @@ -9,7 +9,9 @@ public interface CitizenService { Mono createCitizenConsent(CitizenConsentDTO citizenConsent); Mono updateTppState(String fiscalCode, String tppId, boolean tppState); - Mono getConsentStatus(String fiscalCode, String tppId); + Mono getCitizenConsentStatus(String fiscalCode, String tppId); Mono> getTppEnabledList(String fiscalCode); - Mono get(String fiscalCode); + Mono getCitizenConsentsList(String fiscalCode); + Mono getCitizenConsentsListEnabled(String fiscalCode); + Mono> getCitizenEnabled(String tppId); } diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java index c0c45a5..ec6da71 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java @@ -19,6 +19,7 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import static it.gov.pagopa.common.utils.Utils.inputSanify; @@ -82,7 +83,7 @@ public Mono updateTppState(String fiscalCode, String tppId, b } @Override - public Mono getConsentStatus(String fiscalCode, String tppId) { + public Mono getCitizenConsentStatus(String fiscalCode, String tppId) { log.info("[EMD-CITIZEN][GET-CONSENT-STATUS] Received hashedFiscalCode: {} and tppId: {}", Utils.createSHA256(fiscalCode), inputSanify(tppId)); return citizenRepository.findByFiscalCodeAndTppId(fiscalCode, tppId) .switchIfEmpty(Mono.error(exceptionMap.throwException @@ -106,10 +107,36 @@ public Mono> getTppEnabledList(String fiscalCode) { } @Override - public Mono get(String fiscalCode) { + public Mono getCitizenConsentsList(String fiscalCode) { log.info("[EMD-CITIZEN][FIND-ALL-CITIZEN-CONSENTS] Received hashedFiscalCode: {}", (Utils.createSHA256(fiscalCode))); return citizenRepository.findByFiscalCode(fiscalCode) .map(mapperToDTO::map) - .doOnSuccess(consentList -> log.info("[EMD-CITIZEN][FIND-ALL-CITIZEN-CONSENTS] Consents found:: {}", consentList)); + .doOnSuccess(consentList -> log.info("[EMD-CITIZEN][FIND-ALL-CITIZEN-CONSENTS] Consents found: {}", consentList)); + } + + @Override + public Mono getCitizenConsentsListEnabled(String fiscalCode) { + log.info("[EMD-CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] Received hashedFiscalCode: {}", Utils.createSHA256(fiscalCode)); + + return citizenRepository.findByFiscalCode(fiscalCode) + .switchIfEmpty(Mono.empty()) + .map(citizenConsent -> { + Map filteredConsents = citizenConsent.getConsents().entrySet().stream() + .filter(tpp -> tpp.getValue().getTppState()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + citizenConsent.setConsents(filteredConsents); + + return mapperToDTO.map(citizenConsent); + }) + .doOnSuccess(citizenConsent -> log.info("EMD][CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] Consents found: {}", citizenConsent.getConsents().size())); + + + } + + @Override + public Mono> getCitizenEnabled(String tppId) { + return citizenRepository.findByTppIdEnabled(tppId) + .map(mapperToDTO::map) + .collectList(); } } diff --git a/src/test/java/it/gov/pagopa/common/configuration/MongoConfigTest.java b/src/test/java/it/gov/pagopa/common/configuration/MongoConfigTest.java new file mode 100644 index 0000000..242c5fb --- /dev/null +++ b/src/test/java/it/gov/pagopa/common/configuration/MongoConfigTest.java @@ -0,0 +1,111 @@ +package it.gov.pagopa.common.configuration; + +import com.mongodb.MongoClientSettings; +import it.gov.pagopa.common.utils.CommonConstants; +import org.bson.types.Decimal128; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.mongo.MongoClientSettingsBuilderCustomizer; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(classes = { + MongoConfig.class +}) +@TestPropertySource(properties = { + "spring.data.mongodb.config.connectionPool.maxSize=50", + "spring.data.mongodb.config.connectionPool.minSize=5", + "spring.data.mongodb.config.connectionPool.maxWaitTimeMS=1000", + "spring.data.mongodb.config.connectionPool.maxConnectionLifeTimeMS=60000", + "spring.data.mongodb.config.connectionPool.maxConnectionIdleTimeMS=30000", + "spring.data.mongodb.config.connectionPool.maxConnecting=2" +}) +class MongoConfigTest { + + @Autowired + private MongoConfig.MongoDbCustomProperties mongoDbCustomProperties; + + + @Test + void testConnectionPoolSettings() { + assertThat(mongoDbCustomProperties).isNotNull(); + assertThat(mongoDbCustomProperties.getConnectionPool()).isNotNull(); + + Assertions.assertEquals(1000L, mongoDbCustomProperties.getConnectionPool().getMaxWaitTimeMS()); + Assertions.assertEquals(2, mongoDbCustomProperties.getConnectionPool().getMaxConnecting()); + Assertions.assertEquals(5L, mongoDbCustomProperties.getConnectionPool().getMinSize()); + Assertions.assertEquals(50L, mongoDbCustomProperties.getConnectionPool().getMaxSize()); + Assertions.assertEquals(60000, mongoDbCustomProperties.getConnectionPool().getMaxConnectionLifeTimeMS()); + Assertions.assertEquals(30000, mongoDbCustomProperties.getConnectionPool().getMaxConnectionIdleTimeMS()); + } + + @Test + void testCustomizer() { + MongoClientSettingsBuilderCustomizer customizer = new MongoConfig().customizer(mongoDbCustomProperties); + + MongoClientSettings.Builder builder = MongoClientSettings.builder(); + customizer.customize(builder); + + MongoClientSettings settings = builder.build(); + + Assertions.assertEquals(mongoDbCustomProperties.getConnectionPool().getMaxSize(), settings.getConnectionPoolSettings().getMaxSize()); + Assertions.assertEquals(mongoDbCustomProperties.getConnectionPool().getMinSize(), settings.getConnectionPoolSettings().getMinSize()); + Assertions.assertEquals(mongoDbCustomProperties.getConnectionPool().getMaxWaitTimeMS(), settings.getConnectionPoolSettings().getMaxWaitTime(TimeUnit.MILLISECONDS)); + Assertions.assertEquals(mongoDbCustomProperties.getConnectionPool().getMaxConnectionLifeTimeMS(), settings.getConnectionPoolSettings().getMaxConnectionLifeTime(TimeUnit.MILLISECONDS)); + Assertions.assertEquals(mongoDbCustomProperties.getConnectionPool().getMaxConnectionIdleTimeMS(), settings.getConnectionPoolSettings().getMaxConnectionIdleTime(TimeUnit.MILLISECONDS)); + Assertions.assertEquals(mongoDbCustomProperties.getConnectionPool().getMaxConnecting(), settings.getConnectionPoolSettings().getMaxConnecting()); + } + + @Test + void testBigDecimalToDecimal128Conversion() { + BigDecimal bigDecimal = new BigDecimal("12345.6789"); + + MongoConfig.BigDecimalDecimal128Converter converter = new MongoConfig.BigDecimalDecimal128Converter(); + Decimal128 decimal128 = converter.convert(bigDecimal); + + Assertions.assertEquals(new Decimal128(bigDecimal), decimal128); + } + + @Test + void testDecimal128ToBigDecimalConversion() { + BigDecimal bigDecimal = new BigDecimal("12345.6789"); + Decimal128 decimal128 = new Decimal128(bigDecimal); + + MongoConfig.Decimal128BigDecimalConverter converter = new MongoConfig.Decimal128BigDecimalConverter(); + BigDecimal result = converter.convert(decimal128); + + Assertions.assertEquals(bigDecimal, result); + } + + @Test + void testOffsetDateTimeToDateConversion() { + OffsetDateTime offsetDateTime = OffsetDateTime.now(); + + MongoConfig.OffsetDateTimeWriteConverter converter = new MongoConfig.OffsetDateTimeWriteConverter(); + Date date = converter.convert(offsetDateTime); + + Assertions.assertEquals(Date.from(offsetDateTime.toInstant()), date); + } + + @Test + void testDateToOffsetDateTimeConversion() { + Date date = new Date(); + + MongoConfig.OffsetDateTimeReadConverter converter = new MongoConfig.OffsetDateTimeReadConverter(); + OffsetDateTime offsetDateTime = converter.convert(date); + + + Assertions.assertEquals(date.toInstant().atZone(CommonConstants.ZONEID).toOffsetDateTime(), offsetDateTime); + } + + +} + diff --git a/src/test/java/it/gov/pagopa/common/reactive/kafka/BaseKafkaConsumerTest.java b/src/test/java/it/gov/pagopa/common/reactive/kafka/BaseKafkaConsumerTest.java new file mode 100644 index 0000000..c1a0f70 --- /dev/null +++ b/src/test/java/it/gov/pagopa/common/reactive/kafka/BaseKafkaConsumerTest.java @@ -0,0 +1,87 @@ +package it.gov.pagopa.common.reactive.kafka; + +import it.gov.pagopa.common.kafka.utils.KafkaConstants; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.kafka.support.KafkaHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import java.nio.charset.StandardCharsets; + +class BaseKafkaConsumerTest { + + private TestKafkaConsumer testConsumer; + + @BeforeEach + void setUp() { + testConsumer = new TestKafkaConsumer("test-app"); + } + + @Test + void testExecuteSuccess() { + + Flux> messageFlux = Flux.just( + createMessage("message1"), + createMessage("message2"), + createMessage("message3") + ); + + + StepVerifier.create(Flux.defer(() -> { + testConsumer.execute(messageFlux); + return messageFlux.map(Message::getPayload); + })) + .expectNext("{\"message\": \"message1\"}", "{\"message\": \"message2\"}", "{\"message\": \"message3\"}") + .verifyComplete(); + } + + @Test + void testExecuteWithErrorMessage() { + + Flux> messageFlux = Flux.just( + createMessage("message1"), + createMessage("error"), + createMessage("message2") + ); + + + StepVerifier.create(Flux.defer(() -> { + testConsumer.execute(messageFlux); + return messageFlux.map(Message::getPayload); + })) + .expectNext("{\"message\": \"message1\"}", "{\"message\": \"error\"}", "{\"message\": \"message2\"}") + .verifyComplete(); + } + + @Test + void testDiscardForeignMessages() { + + Message foreignMessage = MessageBuilder.withPayload("foreign-message") + .setHeader(KafkaConstants.ERROR_MSG_HEADER_APPLICATION_NAME, "other-app".getBytes(StandardCharsets.UTF_8)) + .setHeader(KafkaHeaders.RECEIVED_PARTITION, 0) + .setHeader(KafkaHeaders.OFFSET, 1L) + .build(); + + Flux> messageFlux = Flux.just(foreignMessage); + + StepVerifier.create(Flux.defer(() -> { + testConsumer.execute(messageFlux); + return messageFlux.map(Message::getPayload); + })) + .expectNext("foreign-message") + .verifyComplete(); + } + + private Message createMessage(String payload) { + String jsonPayload = "{\"message\": \"" + payload + "\"}"; + return MessageBuilder.withPayload(jsonPayload) + .setHeader(KafkaHeaders.RECEIVED_PARTITION, 0) + .setHeader(KafkaHeaders.OFFSET, 1L) + .setHeader(KafkaConstants.ERROR_MSG_HEADER_APPLICATION_NAME, "test-app".getBytes(StandardCharsets.UTF_8)) + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/common/reactive/kafka/TestKafkaConsumer.java b/src/test/java/it/gov/pagopa/common/reactive/kafka/TestKafkaConsumer.java new file mode 100644 index 0000000..3842340 --- /dev/null +++ b/src/test/java/it/gov/pagopa/common/reactive/kafka/TestKafkaConsumer.java @@ -0,0 +1,51 @@ +package it.gov.pagopa.common.reactive.kafka; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import it.gov.pagopa.common.reactive.kafka.consumer.BaseKafkaConsumer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.Message; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +@Slf4j + +class TestKafkaConsumer extends BaseKafkaConsumer { + + protected TestKafkaConsumer(String applicationName) { + super(applicationName); + } + + @Override + protected Duration getCommitDelay() { + return Duration.ofMillis(500); + } + @Override + protected void subscribeAfterCommits(Flux> afterCommits2subscribe) { + afterCommits2subscribe + .buffer(getCommitDelay()) + .subscribe(r -> log.info("Processed offsets committed successfully")); + } + @Override + protected ObjectReader getObjectReader() { + return new ObjectMapper().readerFor(Map.class); + } + @Override + protected Consumer onDeserializationError(Message message) { + return e -> log.info("Unexpected JSON : {}", e.getMessage()); + } + + @Override + protected Mono execute(String payload, Message message, Map ctx) { + if ("error".equals(payload)) { + return Mono.error(new RuntimeException("Error")); + } + return Mono.just("Processed: " + payload); + } +} + diff --git a/src/test/java/it/gov/pagopa/common/reactive/utils/PerformanceLoggerTest.java b/src/test/java/it/gov/pagopa/common/reactive/utils/PerformanceLoggerTest.java new file mode 100644 index 0000000..7e64831 --- /dev/null +++ b/src/test/java/it/gov/pagopa/common/reactive/utils/PerformanceLoggerTest.java @@ -0,0 +1,71 @@ +package it.gov.pagopa.common.reactive.utils; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.function.Function; + +class PerformanceLoggerTest { + +//region mono ops + @Test + void testMonoLogTimingOnNext(){ + testMonoLogTimingOnNext(null); + testMonoLogTimingOnNext(Object::toString); + } + + private static void testMonoLogTimingOnNext(Function data2LogPayload) { + boolean[] result=new boolean[]{false}; + Mono mono = PerformanceLogger.logTimingOnNext("PROVA", Mono.defer(() -> Mono.just(result[0] = true)), data2LogPayload); + Assertions.assertFalse(result[0]); + mono.block(); + Assertions.assertTrue(result[0]); + } + + @Test + void testMonoLogTimingFinally(){ + testMonoLogTimingFinally(null); + testMonoLogTimingFinally("END"); + } + + private static void testMonoLogTimingFinally(String logPayload) { + boolean[] result=new boolean[]{false}; + Mono mono = PerformanceLogger.logTimingFinally("PROVA", Mono.defer(() -> Mono.just(result[0] = true)), logPayload); + Assertions.assertFalse(result[0]); + mono.block(); + Assertions.assertTrue(result[0]); + } +//endregion + + //region flux ops + @Test + void testFluxLogTimingOnNext(){ + testFluxLogTimingOnNext(null); + testFluxLogTimingOnNext(Object::toString); + } + + private static void testFluxLogTimingOnNext(Function data2LogPayload) { + boolean[] result=new boolean[]{false}; + Flux flux = PerformanceLogger.logTimingOnNext("PROVA", Flux.defer(() -> Flux.just(result[0] = true)), data2LogPayload); + Assertions.assertFalse(result[0]); + flux.collectList().block(); + Assertions.assertTrue(result[0]); + } + + @Test + void testFluxLogTimingFinally(){ + testFluxLogTimingFinally(null); + testFluxLogTimingFinally("END"); + } + + private static void testFluxLogTimingFinally(String logPayload) { + boolean[] result=new boolean[]{false}; + Flux flux = PerformanceLogger.logTimingFinally("PROVA", Flux.defer(() -> Flux.just(result[0] = true)), logPayload); + Assertions.assertFalse(result[0]); + flux.collectList().block(); + Assertions.assertTrue(result[0]); + } +//endregion +} diff --git a/src/test/java/it/gov/pagopa/common/utils/CommonUtilitiesTest.java b/src/test/java/it/gov/pagopa/common/utils/CommonUtilitiesTest.java new file mode 100644 index 0000000..624d754 --- /dev/null +++ b/src/test/java/it/gov/pagopa/common/utils/CommonUtilitiesTest.java @@ -0,0 +1,128 @@ +package it.gov.pagopa.common.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectReader; +import it.gov.pagopa.common.web.exception.EmdEncryptionException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CommonUtilitiesTest { + + @Mock + private Message messageMock; + @Mock + private ObjectReader objectReaderMock; + + @Test + void createSHA256_Ko_NoSuchAlgorithm() { + try (MockedStatic mockedStatic = Mockito.mockStatic(MessageDigest.class)) { + mockedStatic.when(() -> MessageDigest.getInstance(any())) + .thenThrow(new NoSuchAlgorithmException("SHA-256 not available")); + + EmdEncryptionException exception = assertThrows(EmdEncryptionException.class, () -> CommonUtilities.createSHA256("")); + + assertEquals("SHA-256 not available", exception.getCause().getMessage()); + } + } + + @Test + void createSHA256_Ok() { + String toHash = "RSSMRA98B18L049O"; + String hashedExpected = "0b393cbe68a39f26b90c80a8dc95abc0fe4c21821195b4671a374c1443f9a1bb"; + String actualHash = CommonUtilities.createSHA256(toHash); + assertEquals(hashedExpected, actualHash); + } + + @Test + void deserializeMessage_Ko_JsonProcessingException() { + Consumer errorHandler = e -> Assertions.assertTrue(e instanceof JsonProcessingException); + when(messageMock.getPayload()).thenReturn("invalid payload"); + + try { + when(objectReaderMock.readValue("invalid payload")).thenThrow(new JsonProcessingException("Invalid JSON") {}); + + // Act + Object result = CommonUtilities.deserializeMessage(messageMock, objectReaderMock, errorHandler); + + // Assert + Assertions.assertNull(result); + } catch (JsonProcessingException e) { + Assertions.fail("Exception should be handled in the method"); + } + } + + @Test + void deserializeMessage_Ok() { + // Setup + String validJson = "{\"name\":\"John\"}"; + MyObject expectedObject = new MyObject("John"); + Consumer errorHandler = e -> Assertions.fail("Should not have thrown an error"); + + when(messageMock.getPayload()).thenReturn(validJson); + try { + when(objectReaderMock.readValue(validJson)).thenReturn(expectedObject); + + MyObject result = CommonUtilities.deserializeMessage(messageMock, objectReaderMock, errorHandler); + + Assertions.assertNotNull(result); + assertEquals(expectedObject.name(), result.name()); + } catch (JsonProcessingException e) { + Assertions.fail("Exception should not be thrown"); + } + } + + @Test + void readMessagePayload_StringPayload() { + String expectedPayload = "test message"; + when(messageMock.getPayload()).thenReturn(expectedPayload); + + String actualPayload = CommonUtilities.readMessagePayload(messageMock); + + assertEquals(expectedPayload, actualPayload); + } + + @Test + void getHeaderValue_Test() { + String headerName = "testHeader"; + String headerValue = "headerValue"; + MessageHeaders headers = new MessageHeaders(Map.of(headerName, headerValue)); + + when(messageMock.getHeaders()).thenReturn(headers); + + Object result = CommonUtilities.getHeaderValue(messageMock, headerName); + + assertEquals(headerValue, result); + } + + @Test + void getByteArrayHeaderValue_Test() { + String headerName = "byteArrayHeader"; + String headerValue = "headerValue"; + MessageHeaders headers = new MessageHeaders(Map.of(headerName, headerValue.getBytes(StandardCharsets.UTF_8))); + when(messageMock.getHeaders()).thenReturn(headers); + + String result = CommonUtilities.getByteArrayHeaderValue(messageMock, headerName); + + assertEquals(headerValue, result); + } + record MyObject(String name) { } +} diff --git a/src/test/java/it/gov/pagopa/common/web/exception/ErrorManagerTest.java b/src/test/java/it/gov/pagopa/common/web/exception/ErrorManagerTest.java index 5e00799..03247b9 100644 --- a/src/test/java/it/gov/pagopa/common/web/exception/ErrorManagerTest.java +++ b/src/test/java/it/gov/pagopa/common/web/exception/ErrorManagerTest.java @@ -75,7 +75,40 @@ void handleExceptionClientExceptionNoBody() { .expectStatus().isBadRequest(); checkStackTraceSuppressedLog(memoryAppender, - "HttpStatus 400 BAD_REQUEST - NOTFOUND ClientExceptionNoBody"); + "A ClientExceptionNoBody occurred handling request GET /test: HttpStatus 400 BAD_REQUEST - NOTFOUND ClientExceptionNoBody at it.gov.pagopa.common.web.exception.ErrorManagerTest\\$TestController.testEndpoint\\(ErrorManagerTest.java:[0-9]+\\)"); + + memoryAppender.reset(); + + Throwable throwable = new Exception("Cause of the exception"); + + Mockito.doThrow( + new ClientExceptionNoBody(HttpStatus.BAD_REQUEST, "ClientExceptionNoBody with Throwable", throwable)) + .when(testControllerSpy).testEndpoint(); + + webTestClient.get() + .uri("/test") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isBadRequest(); + + checkStackTraceSuppressedLog(memoryAppender, + "Something went wrong handling request GET /test: HttpStatus 400 BAD_REQUEST - ClientExceptionNoBody with Throwable"); + + memoryAppender.reset(); + + Mockito.doThrow( + new ClientExceptionNoBody(HttpStatus.BAD_REQUEST, "ClientExceptionNoBody", true, throwable)) + .when(testControllerSpy).testEndpoint(); + + webTestClient.get() + .uri("/test") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isBadRequest(); + + checkStackTraceSuppressedLog(memoryAppender, + "Something went wrong handling request GET /test: HttpStatus 400 BAD_REQUEST - ClientExceptionNoBody"); + } @Test @@ -119,7 +152,7 @@ void handleExceptionClientExceptionTest() { .expectBody() .json(EXPECTED_GENERIC_ERROR); - checkStackTraceSuppressedLog(memoryAppender, "HttpStatus null - null"); + checkStackTraceSuppressedLog(memoryAppender, "A ClientException occurred handling request GET /test: HttpStatus null - null at UNKNOWN"); memoryAppender.reset(); Mockito.doThrow( @@ -134,7 +167,7 @@ void handleExceptionClientExceptionTest() { .expectBody() .json(EXPECTED_GENERIC_ERROR); - checkStackTraceSuppressedLog(memoryAppender, "HttpStatus 400 BAD_REQUEST - ClientException with httpStatus and message"); + checkStackTraceSuppressedLog(memoryAppender, "A ClientException occurred handling request GET /test: HttpStatus 400 BAD_REQUEST - ClientException with httpStatus and message at it.gov.pagopa.common.web.exception.ErrorManagerTest\\$TestController.testEndpoint\\(ErrorManagerTest.java:[0-9]+\\)"); memoryAppender.reset(); Mockito.doThrow(new ClientException(HttpStatus.BAD_REQUEST, @@ -150,7 +183,7 @@ void handleExceptionClientExceptionTest() { .json(EXPECTED_GENERIC_ERROR); checkLog(memoryAppender, - "Something went wrong : HttpStatus 400 BAD_REQUEST - ClientException with httpStatus, message and throwable", + "Something went wrong handling request GET /test: HttpStatus 400 BAD_REQUEST - ClientException with httpStatus, message and throwable", "it.gov.pagopa.common.web.exception.ClientException: ClientException with httpStatus, message and throwable", "it.gov.pagopa.common.web.exception.ErrorManagerTest$TestController.testEndpoint" ); diff --git a/src/test/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandlerTest.java b/src/test/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandlerTest.java index a37f4ae..0896c70 100644 --- a/src/test/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandlerTest.java +++ b/src/test/java/it/gov/pagopa/common/web/exception/ServiceExceptionHandlerTest.java @@ -79,7 +79,7 @@ void testSimpleException(){ .expectBody() .json("{\"code\":\"DUMMY_CODE\",\"message\":\"DUMMY_MESSAGE\"}", false); - ErrorManagerTest.checkStackTraceSuppressedLog(memoryAppender, "HttpStatus 500 INTERNAL_SERVER_ERROR - DUMMY_CODE: DUMMY_MESSAGE"); + ErrorManagerTest.checkStackTraceSuppressedLog(memoryAppender, "A ServiceException occurred handling request GET /test: HttpStatus 500 INTERNAL_SERVER_ERROR - DUMMY_CODE: DUMMY_MESSAGE at it.gov.pagopa.common.web.exception.ServiceExceptionHandlerTest\\$TestController.test\\(ServiceExceptionHandlerTest.java:[0-9]+\\)"); } @@ -95,7 +95,7 @@ void testCustomBodyException(){ .json("{\"stringCode\":\"RESPONSE\",\"longCode\":0}", false); ErrorManagerTest.checkLog(memoryAppender, - "Something went wrong : HttpStatus 500 INTERNAL_SERVER_ERROR - DUMMY_CODE: DUMMY_MESSAGE", + "Something went wrong handling request GET /test/customBody: HttpStatus 500 INTERNAL_SERVER_ERROR - DUMMY_CODE: DUMMY_MESSAGE", "it.gov.pagopa.common.web.exception.ServiceException: DUMMY_MESSAGE", "it.gov.pagopa.common.web.exception.ServiceExceptionHandlerTest$TestController.testCustomBody" diff --git a/src/test/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandlerTest.java b/src/test/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandlerTest.java index 745f8cf..7e95f91 100644 --- a/src/test/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandlerTest.java +++ b/src/test/java/it/gov/pagopa/common/web/exception/ValidationExceptionHandlerTest.java @@ -51,7 +51,6 @@ static class ValidationDTO { @Test void testHandleValueNotValidException() { String invalidJson = "{}"; - webTestClient.put() .uri("/test") .contentType(MediaType.APPLICATION_JSON) @@ -68,9 +67,7 @@ void testHandleValueNotValidException() { }); } @Test - void testHandleMissingRequestValueException() { - String invalidJson = "{}"; - + void testHandleHeaderNotValidException() { webTestClient.put() .uri("/test") .contentType(MediaType.APPLICATION_JSON) @@ -78,51 +75,29 @@ void testHandleMissingRequestValueException() { .exchange() .expectStatus().isBadRequest() .expectBody(ErrorDTO.class) - .consumeWith(response -> { ErrorDTO errorDTO = response.getResponseBody(); assertThat(errorDTO).isNotNull(); assertThat(errorDTO.getCode()).isEqualTo("INVALID_REQUEST"); - assertThat(errorDTO.getMessage()).isEqualTo("Something went wrong due to a missing request value"); + assertThat(errorDTO.getMessage()).isEqualTo("Invalid request"); }); } @Test void testHandleNoResourceFoundException() { - String invalidJson = "{}"; - webTestClient.put() - .uri("/wrongPath") + .uri("/test/missing") .contentType(MediaType.APPLICATION_JSON) - .bodyValue(new ValidationDTO("data")) + .bodyValue(new ValidationDTO("someData")) .exchange() - .expectStatus().isNotFound() + .expectStatus().isNotFound() // Expect 404 Not Found .expectBody(ErrorDTO.class) - .consumeWith(response -> { ErrorDTO errorDTO = response.getResponseBody(); assertThat(errorDTO).isNotNull(); - assertThat(errorDTO.getCode()).isEqualTo("INVALID_REQUEST"); - assertThat(errorDTO.getMessage()).isEqualTo("Something went wrong due to a missing static resource"); - - }); - } - - @Test - void testHandleMethodNotAllowedException() { - - webTestClient.get() - .uri("/test") - .exchange() - .expectStatus().is4xxClientError() - .expectBody(ErrorDTO.class) - .consumeWith(response -> { - ErrorDTO errorDTO = response.getResponseBody(); - assertThat(errorDTO).isNotNull(); - assertThat(errorDTO.getCode()).isEqualTo("INVALID_REQUEST"); - assertThat(errorDTO.getMessage()).isEqualTo("Request is not supported"); - + assertThat(errorDTO.getCode()).isEqualTo("INVALID_REQUEST"); // Check the code from ErrorDTO + assertThat(errorDTO.getMessage()).isEqualTo("Invalid request"); // Check the message from ErrorDTO }); } } diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java index f42d41b..bb02f0f 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java @@ -88,7 +88,7 @@ void getConsentStatus_Ok() { CitizenConsentDTO citizenConsentDTO = CitizenConsentDTOFaker.mockInstance(true); - Mockito.when(citizenService.getConsentStatus(FISCAL_CODE, TPP_ID)) + Mockito.when(citizenService.getCitizenConsentStatus(FISCAL_CODE, TPP_ID)) .thenReturn(Mono.just(citizenConsentDTO)); webClient.get() @@ -111,7 +111,7 @@ void getTppEnabledList_Ok() { .thenReturn(Mono.just(tppEnabledList)); webClient.get() - .uri("/emd/citizen/list/{fiscalCode}/enabled", FISCAL_CODE) + .uri("/emd/citizen/list/{fiscalCode}/enabled/tpp", FISCAL_CODE) .exchange() .expectStatus().isOk() .expectBody(new ParameterizedTypeReference>() {}) @@ -126,7 +126,7 @@ void getTppEnabledList_Ok() { void get_Ok() { CitizenConsentDTO citizenConsentDTO = CitizenConsentDTOFaker.mockInstance(true); - Mockito.when(citizenService.get(FISCAL_CODE)) + Mockito.when(citizenService.getCitizenConsentsList(FISCAL_CODE)) .thenReturn(Mono.just(citizenConsentDTO)); webClient.get() diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImplTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImplTest.java index 0acc9cf..4e555ec 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImplTest.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImplTest.java @@ -3,63 +3,62 @@ import it.gov.pagopa.onboarding.citizen.model.CitizenConsent; import it.gov.pagopa.onboarding.citizen.model.ConsentDetails; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class CitizenSpecificRepositoryImplTest { + @Mock private ReactiveMongoTemplate mongoTemplate; - private CitizenSpecificRepositoryImpl repository; - @BeforeEach - public void setUp() { - mongoTemplate = Mockito.mock(ReactiveMongoTemplate.class); - repository = new CitizenSpecificRepositoryImpl(mongoTemplate); - } + @InjectMocks + private CitizenSpecificRepositoryImpl repository; @Test void testFindByFiscalCodeAndTppId() { String hashedFiscalCode = "hashedCode"; String tppId = "tpp1"; - CitizenConsent citizenConsent = new CitizenConsent(); - citizenConsent.setId("1"); - citizenConsent.setFiscalCode(hashedFiscalCode); - - Map consents = new HashMap<>(); - ConsentDetails consentDetails = new ConsentDetails(); - consentDetails.setTppState(true); - consents.put(tppId, consentDetails); - citizenConsent.setConsents(consents); - - when(mongoTemplate.aggregate(Mockito.any(), Mockito.eq(CitizenConsent.class), Mockito.eq(CitizenConsent.class))) - .thenReturn(Flux.just(citizenConsent)); - - Mono result = repository.findByFiscalCodeAndTppId(hashedFiscalCode, tppId); - - Assertions.assertEquals(hashedFiscalCode, Objects.requireNonNull(result.block()).getFiscalCode()); - Mockito.verify(mongoTemplate).aggregate(Mockito.any(), Mockito.eq(CitizenConsent.class), Mockito.eq(CitizenConsent.class)); + CitizenConsent citizenConsent = createMockCitizenConsent(hashedFiscalCode, tppId); + + when(mongoTemplate.aggregate( + Mockito.any(Aggregation.class), + Mockito.eq("citizen_consents"), + Mockito.eq(CitizenConsent.class) + )).thenReturn(Flux.just(citizenConsent)); + + StepVerifier.create(repository.findByFiscalCodeAndTppId(hashedFiscalCode, tppId)) + .expectNextMatches(result -> result.getFiscalCode().equals(hashedFiscalCode)) + .verifyComplete(); + + Mockito.verify(mongoTemplate).aggregate( + Mockito.any(Aggregation.class), + Mockito.eq("citizen_consents"), + Mockito.eq(CitizenConsent.class) + ); } @Test void testFindByFiscalCodeAndTppId_TppIdNull() { String hashedFiscalCode = "hashedCode"; String tppId = null; - Mono result = repository.findByFiscalCodeAndTppId(hashedFiscalCode, tppId); - Assertions.assertNotEquals(Boolean.TRUE, result.hasElement().block()); + StepVerifier.create(repository.findByFiscalCodeAndTppId(hashedFiscalCode, tppId)) + .expectComplete() // Expect an empty Mono + .verify(); } @Test @@ -67,12 +66,21 @@ void testFindByFiscalCodeAndTppId_EmptyResult() { String hashedFiscalCode = "hashedCode"; String tppId = "tpp1"; - when(mongoTemplate.aggregate(Mockito.any(), Mockito.eq(CitizenConsent.class), Mockito.eq(CitizenConsent.class))) - .thenReturn(Flux.empty()); - - Mono result = repository.findByFiscalCodeAndTppId(hashedFiscalCode, tppId); - - Assertions.assertNull(result.block()); + when(mongoTemplate.aggregate( + Mockito.any(Aggregation.class), + Mockito.eq("citizen_consents"), + Mockito.eq(CitizenConsent.class) + )).thenReturn(Flux.empty()); + + StepVerifier.create(repository.findByFiscalCodeAndTppId(hashedFiscalCode, tppId)) + .expectComplete() // Expect an empty Mono + .verify(); + + Mockito.verify(mongoTemplate).aggregate( + Mockito.any(Aggregation.class), + Mockito.eq("citizen_consents"), + Mockito.eq(CitizenConsent.class) + ); } @Test @@ -86,7 +94,6 @@ void testConsentKeyWrapperGetterSetter() { @Test void testConsentKeyWrapperToString() { - CitizenSpecificRepositoryImpl.ConsentKeyWrapper consentKeyWrapper = new CitizenSpecificRepositoryImpl.ConsentKeyWrapper(); consentKeyWrapper.setK("testKey"); @@ -95,9 +102,9 @@ void testConsentKeyWrapperToString() { @Test void testConsentKeyWrapperEqualsAndHashCode() { - CitizenSpecificRepositoryImpl.ConsentKeyWrapper consentKeyWrapper1 = new CitizenSpecificRepositoryImpl.ConsentKeyWrapper(); CitizenSpecificRepositoryImpl.ConsentKeyWrapper consentKeyWrapper2 = new CitizenSpecificRepositoryImpl.ConsentKeyWrapper(); + consentKeyWrapper1.setK("testKey"); consentKeyWrapper2.setK("testKey"); @@ -107,4 +114,18 @@ void testConsentKeyWrapperEqualsAndHashCode() { consentKeyWrapper2.setK("differentKey"); Assertions.assertNotEquals(consentKeyWrapper1, consentKeyWrapper2); } + + private CitizenConsent createMockCitizenConsent(String hashedFiscalCode, String tppId) { + CitizenConsent citizenConsent = new CitizenConsent(); + citizenConsent.setId("1"); + citizenConsent.setFiscalCode(hashedFiscalCode); + + Map consents = new HashMap<>(); + ConsentDetails consentDetails = new ConsentDetails(); + consentDetails.setTppState(true); + consents.put(tppId, consentDetails); + citizenConsent.setConsents(consents); + + return citizenConsent; + } } diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java index 34961c8..5484657 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java @@ -205,7 +205,7 @@ void getConsentStatus_Ok() { when(citizenRepository.findByFiscalCodeAndTppId(FISCAL_CODE, TPP_ID)) .thenReturn(Mono.just(CITIZEN_CONSENT)); - CitizenConsentDTO response = citizenService.getConsentStatus(FISCAL_CODE, TPP_ID).block(); + CitizenConsentDTO response = citizenService.getCitizenConsentStatus(FISCAL_CODE, TPP_ID).block(); assertNotNull(response); } @@ -215,7 +215,7 @@ void getConsentStatus_Ko_CitizenNotOnboarded() { when(citizenRepository.findByFiscalCodeAndTppId(FISCAL_CODE, TPP_ID)) .thenReturn(Mono.empty()); - Executable executable = () -> citizenService.getConsentStatus(FISCAL_CODE, TPP_ID).block(); + Executable executable = () -> citizenService.getCitizenConsentStatus(FISCAL_CODE, TPP_ID).block(); ClientExceptionWithBody exception = assertThrows(ClientExceptionWithBody.class, executable); assertEquals("Citizen consent not founded during get process ", exception.getMessage()); @@ -256,7 +256,7 @@ void get_Ok() { when(citizenRepository.findByFiscalCode(FISCAL_CODE)) .thenReturn(Mono.just(CITIZEN_CONSENT)); - CitizenConsentDTO response = citizenService.get(FISCAL_CODE).block(); + CitizenConsentDTO response = citizenService.getCitizenConsentsList(FISCAL_CODE).block(); assertNotNull(response); From aac5f7631688e02771a93f29a0175e60bd91bc1b Mon Sep 17 00:00:00 2001 From: Vitolo-Andrea <157486351+Vitolo-Andrea@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:27:55 +0100 Subject: [PATCH 11/18] fix: Test coverage update (#10) --- .../citizen/service/CitizenServiceImpl.java | 16 ++++- .../controller/CitizenControllerTest.java | 37 +++++++++++ .../citizen/service/CitizenServiceTest.java | 66 +++++++++++++++++++ ...tizenConsentValidationServiceImplTest.java | 16 +---- 4 files changed, 119 insertions(+), 16 deletions(-) diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java index ec6da71..83ddf29 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceImpl.java @@ -103,7 +103,13 @@ public Mono> getTppEnabledList(String fiscalCode) { .filter(tpp -> tpp.getValue().getTppState()) .map(Map.Entry::getKey) .toList()) - .doOnSuccess(tppIdList -> log.info("EMD][CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] Consents found: {}", (tppIdList.size()))); + .doOnSuccess(tppIdList -> { + if (tppIdList != null) { + log.info("EMD][CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] Consents found: {}", (tppIdList.size())); + } else { + log.info("EMD][CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] No consents found."); + } + }); } @Override @@ -128,7 +134,13 @@ public Mono getCitizenConsentsListEnabled(String fiscalCode) return mapperToDTO.map(citizenConsent); }) - .doOnSuccess(citizenConsent -> log.info("EMD][CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] Consents found: {}", citizenConsent.getConsents().size())); + .doOnSuccess(citizenConsent -> { + if (citizenConsent != null && citizenConsent.getConsents() != null) { + log.info("EMD][CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] Consents found: {}", citizenConsent.getConsents().size()); + } else { + log.info("EMD][CITIZEN][FIND-CITIZEN-CONSENTS-ENABLED] No consents found."); + } + }); } diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java index bb02f0f..eb686dc 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/controller/CitizenControllerTest.java @@ -173,4 +173,41 @@ void getAllFiscalCode_NoChannelsEnabled() { Assertions.assertEquals("NO CHANNELS ENABLED", resultResponse); }); } + + @Test + void getCitizenConsentsListEnabled_ShouldReturnCitizenConsent() { + CitizenConsentDTO mockConsent = CitizenConsentDTOFaker.mockInstance(true); + Mockito.when(citizenService.getCitizenConsentsListEnabled(FISCAL_CODE)) + .thenReturn(Mono.just(mockConsent)); + + webClient.get() + .uri("/emd/citizen/list/{fiscalCode}/enabled", FISCAL_CODE) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectBody(CitizenConsentDTO.class) + .value(response -> { + assert response != null; + Assertions.assertEquals(response, mockConsent); + }); + + } + + @Test + void getCitizenEnabled_ShouldReturnListOfCitizens() { + String tppId = "TPP123"; + List mockConsents = List.of(CitizenConsentDTOFaker.mockInstance(true)); + Mockito.when(citizenService.getCitizenEnabled(tppId)) + .thenReturn(Mono.just(mockConsents)); + + webClient.get() + .uri("/emd/citizen/{tppId}", tppId) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectBodyList(CitizenConsentDTO.class) + .value(response -> Assertions.assertEquals(1, response.size())); + + } + } diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java index 5484657..b917005 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/service/CitizenServiceTest.java @@ -24,6 +24,7 @@ import org.springframework.http.HttpStatus; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -263,4 +264,69 @@ void get_Ok() { assertEquals(CITIZEN_CONSENT.getFiscalCode(), response.getFiscalCode()); } + @Test + void getCitizenConsentsListEnabled_Ok() { + Map consents = new HashMap<>(); + consents.put("Tpp1", ConsentDetails.builder().tppState(true).tcDate(LocalDateTime.now()).build()); + consents.put("Tpp2", ConsentDetails.builder().tppState(false).tcDate(LocalDateTime.now()).build()); + + CitizenConsent citizenConsent = CitizenConsent.builder() + .fiscalCode(FISCAL_CODE) + .consents(consents) + .build(); + + CitizenConsentDTO expectedDTO = dtoMapper.map(citizenConsent); + expectedDTO.getConsents().remove("Tpp2"); // Filter out disabled TPP + + when(citizenRepository.findByFiscalCode(FISCAL_CODE)).thenReturn(Mono.just(citizenConsent)); + + CitizenConsentDTO response = citizenService.getCitizenConsentsListEnabled(FISCAL_CODE).block(); + + assertNotNull(response); + assertEquals(1, response.getConsents().size()); + assertTrue(response.getConsents().containsKey("Tpp1")); + assertFalse(response.getConsents().containsKey("Tpp2")); + } + + @Test + void getCitizenConsentsListEnabled_Empty() { + when(citizenRepository.findByFiscalCode(FISCAL_CODE)).thenReturn(Mono.empty()); + + CitizenConsentDTO response = citizenService.getCitizenConsentsListEnabled(FISCAL_CODE).block(); + + assertNull(response); + } + + @Test + void getCitizenEnabled_Ok() { + CitizenConsent citizenConsent1 = CitizenConsent.builder() + .fiscalCode("FiscalCode1") + .consents(Map.of(TPP_ID, ConsentDetails.builder().tppState(true).tcDate(LocalDateTime.now()).build())) + .build(); + + CitizenConsent citizenConsent2 = CitizenConsent.builder() + .fiscalCode("FiscalCode2") + .consents(Map.of(TPP_ID, ConsentDetails.builder().tppState(true).tcDate(LocalDateTime.now()).build())) + .build(); + + when(citizenRepository.findByTppIdEnabled(TPP_ID)).thenReturn(Flux.just(citizenConsent1, citizenConsent2)); + + List response = citizenService.getCitizenEnabled(TPP_ID).block(); + + assertNotNull(response); + assertEquals(2, response.size()); + assertEquals("FiscalCode1", response.get(0).getFiscalCode()); + assertEquals("FiscalCode2", response.get(1).getFiscalCode()); + } + + @Test + void getCitizenEnabled_Empty() { + when(citizenRepository.findByTppIdEnabled(TPP_ID)).thenReturn(Flux.empty()); + + List response = citizenService.getCitizenEnabled(TPP_ID).block(); + + assertNotNull(response); + assertTrue(response.isEmpty()); + } + } diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImplTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImplTest.java index 48165b7..1b16ab4 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImplTest.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImplTest.java @@ -16,11 +16,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; @@ -55,10 +55,6 @@ void handleExistingConsent_ConsentAlreadyExists() { when(dtoMapper.map(existingConsent)).thenReturn(CITIZEN_CONSENT_DTO); - when(citizenRepository.findAll()).thenReturn(Flux.just(CitizenConsentFaker.mockInstance(true))); - - doNothing().when(bloomFilterService).add(anyString()); - CitizenConsentDTO result = validationService.handleExistingConsent(existingConsent, tppId, CITIZEN_CONSENT).block(); assertNotNull(result); @@ -76,10 +72,6 @@ void handleExistingConsent_NewConsentForTpp() { when(citizenRepository.save(existingConsent)).thenReturn(Mono.just(existingConsent)); when(dtoMapper.map(existingConsent)).thenReturn(CITIZEN_CONSENT_DTO); - when(citizenRepository.findAll()).thenReturn(Flux.just(CitizenConsentFaker.mockInstance(true))); - - doNothing().when(bloomFilterService).add(anyString()); - CitizenConsentDTO result = validationService.handleExistingConsent(existingConsent, activeTppDTO.getTppId(), CITIZEN_CONSENT).block(); assertNotNull(result); @@ -103,10 +95,6 @@ void validateTppAndSaveConsent_TppValidAndActive() { when(citizenRepository.save(CITIZEN_CONSENT)).thenReturn(Mono.just(CITIZEN_CONSENT)); when(dtoMapper.map(CITIZEN_CONSENT)).thenReturn(CITIZEN_CONSENT_DTO); - when(citizenRepository.findAll()).thenReturn(Flux.just(CitizenConsentFaker.mockInstance(true))); - - doNothing().when(bloomFilterService).add(anyString()); - CitizenConsentDTO result = validationService.validateTppAndSaveConsent(fiscalCode, activeTppDTO.getTppId(), CITIZEN_CONSENT).block(); assertNotNull(result); From c0714435305d14eb85d41dbf1c130a94b1c2f8c3 Mon Sep 17 00:00:00 2001 From: Vitolo-Andrea <157486351+Vitolo-Andrea@users.noreply.github.com> Date: Tue, 26 Nov 2024 12:00:38 +0100 Subject: [PATCH 12/18] fix: Test coverage update (#11) --- .../CitizenSpecificRepositoryImplTest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImplTest.java b/src/test/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImplTest.java index 4e555ec..9688b58 100644 --- a/src/test/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImplTest.java +++ b/src/test/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImplTest.java @@ -128,4 +128,21 @@ private CitizenConsent createMockCitizenConsent(String hashedFiscalCode, String return citizenConsent; } + + @Test + void testFindByTppIdEnabled_Success() { + String tppId = "tpp1"; + CitizenConsent citizenConsent = createMockCitizenConsent("hashedCode", tppId); + + when(mongoTemplate.aggregate( + Mockito.any(Aggregation.class), + Mockito.eq("citizen_consents"), + Mockito.eq(CitizenConsent.class) + )).thenReturn(Flux.just(citizenConsent)); + + StepVerifier.create(repository.findByTppIdEnabled(tppId)) + .expectNextMatches(result -> result.getConsents().containsKey(tppId) && result.getConsents().get(tppId).getTppState()) + .verifyComplete(); + + } } From 30e765fb043473602c7ed9543f8718cb3dbffe31 Mon Sep 17 00:00:00 2001 From: Vitolo-Andrea <157486351+Vitolo-Andrea@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:09:40 +0100 Subject: [PATCH 13/18] fix: Sonar maintainability (#12) --- .../citizen/repository/CitizenSpecificRepositoryImpl.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java index 54e3781..a68c749 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java @@ -14,6 +14,8 @@ public class CitizenSpecificRepositoryImpl implements CitizenSpecificRepository private final ReactiveMongoTemplate mongoTemplate; + private static final String FISCAL_CODE = "fiscalCode"; + public CitizenSpecificRepositoryImpl(ReactiveMongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } @@ -26,9 +28,9 @@ public Mono findByFiscalCodeAndTppId(String fiscalCode, String t String consent = "consents." + tppId; Aggregation aggregation = Aggregation.newAggregation( - Aggregation.match(Criteria.where("fiscalCode").is(fiscalCode)), + Aggregation.match(Criteria.where(FISCAL_CODE).is(fiscalCode)), Aggregation.match(Criteria.where(consent).exists(true)), - Aggregation.project("fiscalCode").and(consent).as(consent) + Aggregation.project(FISCAL_CODE).and(consent).as(consent) ); return mongoTemplate.aggregate(aggregation, "citizen_consents", CitizenConsent.class) @@ -41,7 +43,7 @@ public Flux findByTppIdEnabled(String tppId) { Aggregation aggregation = Aggregation.newAggregation( Aggregation.match(Criteria.where(tppStatePath).is(true)), - Aggregation.project("fiscalCode").and(consent).as(consent) + Aggregation.project(FISCAL_CODE).and(consent).as(consent) ); return mongoTemplate.aggregate(aggregation, "citizen_consents", CitizenConsent.class); From cba6c47cc9761486acaf78586cf2bc80190a04bf Mon Sep 17 00:00:00 2001 From: DanieleRanaldo Date: Fri, 6 Dec 2024 10:19:52 +0100 Subject: [PATCH 14/18] space --- .../citizen/repository/CitizenSpecificRepositoryImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java index a68c749..a4e422f 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java @@ -55,4 +55,5 @@ public static class ConsentKeyWrapper { private String k; } + } From 0bd2c56ce76cb39b39815f83c76d218b849ef763 Mon Sep 17 00:00:00 2001 From: DanieleRanaldo Date: Fri, 6 Dec 2024 10:22:44 +0100 Subject: [PATCH 15/18] space --- .../citizen/repository/CitizenSpecificRepositoryImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java index a4e422f..a68c749 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java @@ -55,5 +55,4 @@ public static class ConsentKeyWrapper { private String k; } - } From bb803f298301dbc330b09800197e28807947741a Mon Sep 17 00:00:00 2001 From: DanieleRanaldo <124155243+DanieleRanaldo@users.noreply.github.com> Date: Fri, 6 Dec 2024 10:29:02 +0100 Subject: [PATCH 16/18] chore: promote to UAT (#14) --- .../citizen/repository/CitizenSpecificRepositoryImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java index a68c749..a4e422f 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/repository/CitizenSpecificRepositoryImpl.java @@ -55,4 +55,5 @@ public static class ConsentKeyWrapper { private String k; } + } From 810f4f41b7456b5ac8330a7fc7ce3b56cbd8bdd9 Mon Sep 17 00:00:00 2001 From: stedelia <144045955+stedelia@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:10:05 +0100 Subject: [PATCH 17/18] fix: promote to Uat (#16) Co-authored-by: DanieleRanaldo Co-authored-by: DanieleRanaldo <124155243+DanieleRanaldo@users.noreply.github.com> Co-authored-by: Vitolo-Andrea <157486351+Vitolo-Andrea@users.noreply.github.com> --- .../validation/CitizenConsentValidationServiceImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImpl.java b/src/main/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImpl.java index d2e8fd5..85ffdfd 100644 --- a/src/main/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImpl.java +++ b/src/main/java/it/gov/pagopa/onboarding/citizen/validation/CitizenConsentValidationServiceImpl.java @@ -38,7 +38,7 @@ public CitizenConsentValidationServiceImpl(CitizenRepository citizenRepository, @Override public Mono handleExistingConsent(CitizenConsent existingConsent, String tppId, CitizenConsent citizenConsent) { if (existingConsent.getConsents().containsKey(tppId)) { - return Mono.just(mapperToDTO.map(existingConsent)); + return Mono.just(mapperToDTO.map(citizenConsent)); } else { return validateTppAndUpdateConsent(existingConsent, tppId, citizenConsent); } @@ -55,7 +55,7 @@ public Mono validateTppAndSaveConsent(String fiscalCode, Stri log.info("[EMD][CREATE-CITIZEN-CONSENT] Created new citizen consent for fiscal code: {}", Utils.createSHA256(fiscalCode)); bloomFilterService.add(fiscalCode); }) - .map(mapperToDTO::map); + .map(savedConsent -> mapperToDTO.map(citizenConsent)); } else { return Mono.error(exceptionMap.throwException(ExceptionName.TPP_NOT_FOUND, "TPP is not active or is invalid")); } @@ -70,7 +70,7 @@ private Mono validateTppAndUpdateConsent(CitizenConsent exist existingConsent.getConsents().put(tppId, citizenConsent.getConsents().get(tppId)); return citizenRepository.save(existingConsent) .doOnSuccess(savedConsent -> log.info("[EMD][CREATE-CITIZEN-CONSENT] Updated citizen consent for TPP: {}", tppId)) - .flatMap(savedConsent -> Mono.just(mapperToDTO.map(savedConsent))); + .map(savedConsent -> mapperToDTO.map(citizenConsent)); }); } } From 0363700c99923032516cc7615c1c98325896110f Mon Sep 17 00:00:00 2001 From: stedelia <144045955+stedelia@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:57:00 +0100 Subject: [PATCH 18/18] fix: promote to UAT (#17) Co-authored-by: DanieleRanaldo Co-authored-by: DanieleRanaldo <124155243+DanieleRanaldo@users.noreply.github.com> Co-authored-by: Vitolo-Andrea <157486351+Vitolo-Andrea@users.noreply.github.com>