diff --git a/.devops/performance-test-pipelines.yml b/.devops/performance-test-pipelines.yml new file mode 100644 index 0000000..b8975e1 --- /dev/null +++ b/.devops/performance-test-pipelines.yml @@ -0,0 +1,83 @@ +# azure-pipelines.yml +trigger: none + +parameters: + - name: "ENVIRONMENT" + displayName: "Environment" + type: string + values: + - "dev" + - "uat" + default: "uat" + - name: "TEST_TYPE" + displayName: "Test type" + type: string + values: + - "constant" + - "load" + - "spike" + - "stress" + default: "constant" + - name: "SCRIPT" + displayName: "Script name" + type: string + values: + - receipt_generator + - name: "DB_NAME" + displayName: "DB name" + type: string + values: + - pagopa_receipt_pdf_generatork6 + - name: "PROCESS_TIME" + displayName: "Process time" + type: number + default: 5 + +variables: + ${{ if eq(parameters['ENVIRONMENT'], 'dev') }}: + receiptCosmosSubscriptionKey: "$(DEV_RECEIPT_COSMOS_DB_SUBSCRIPTION_KEY)" + receiptQueueAccountName: "pagopadweureceiptsfnsa" + receiptQueueName: "pagopa-d-weu-receipts-queue-receipt-waiting-4-gen" + azureSubscription: "$(TF_DEV_AZURE_SERVICE_CONNECTION)" + poolImage: "pagopa-dev-loadtest-linux" + ${{ if eq(parameters['ENVIRONMENT'], 'uat') }}: + receiptCosmosSubscriptionKey: "$(UAT_RECEIPT_COSMOS_DB_SUBSCRIPTION_KEY)" + receiptQueueAccountName: "pagopauweureceiptsfnsa" + receiptQueueName: "pagopa-u-weu-receipts-queue-receipt-waiting-4-gen" + azureSubscription: "$(TF_UAT_AZURE_SERVICE_CONNECTION)" + poolImage: "pagopa-uat-loadtest-linux" + +pool: + name: $(poolImage) + +steps: + - script: | + cd ./performance-test/src + docker pull grafana/k6 + displayName: Pull k6 image + - script: | + apt-get update + apt-get install azure-cli + az version + displayName: Install Azure CLI + - task: AzureCLI@2 + displayName: Login Azure + inputs: + azureSubscription: "$(azureSubscription)" + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + end="$(date -d '+1 day' +'%Y-%m-%d')" + export SAS_TOKEN="$(az storage queue generate-sas \ + -n ${{ variables.receiptQueueName }} \ + --account-name ${{ variables.receiptQueueAccountName }} \ + --permissions apru \ + --expiry $end)" + echo "##vso[task.setvariable variable=SAS_TOKEN]$SAS_TOKEN" + - script: | + cd ./performance-test + sh ./run_performance_test.sh ${{ parameters.ENVIRONMENT }} ${{ parameters.TEST_TYPE }} ${{ parameters.SCRIPT }} ${{ parameters.DB_NAME }} $(SAS_TOKEN) $RECEIPT_COSMOS_SUBSCRIPTION_KEY $PROCESS_TIME + displayName: Run k6 ${{ parameters.SCRIPT }} on ${{ parameters.ENVIRONMENT }} + env: + RECEIPT_COSMOS_SUBSCRIPTION_KEY: ${{ variables.receiptCosmosSubscriptionKey }} + PROCESS_TIME: ${{ parameters.PROCESS_TIME }} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d5c29d6 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +FUNCTIONS_WORKER_RUNTIME=java +AzureWebJobsStorage=DefaultEndpointsProtocol=https;AccountName= \ 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..6ceafec --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,36 @@ + + + + + +#### List of Changes + + + +#### Motivation and Context + + + +#### How Has This Been Tested? + + + + + +#### Screenshots (if appropriate): + +#### Types of changes + + + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) + +#### Checklist: + + + + +- [ ] My change requires a change to the documentation. +- [ ] I have updated the documentation accordingly. \ No newline at end of file diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 0000000..2e0bbe2 --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,3 @@ +addAssignees: author + +runOnDraft: true diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..9cbf0b9 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,18 @@ +# release.yml + +changelog: + exclude: + labels: + - ignore-for-release + authors: + - pagopa-github-bot + categories: + - title: Breaking Changes 🛠 + labels: + - breaking-change + - title: Exciting New Features 🎉 + labels: + - enhancement + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml new file mode 100644 index 0000000..802f626 --- /dev/null +++ b/.github/workflows/anchore.yml @@ -0,0 +1,54 @@ +# 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. + +# This workflow checks out code, builds an image, performs a container image +# vulnerability scan with Anchore's Grype tool, and integrates the results with GitHub Advanced Security +# code scanning feature. For more information on the Anchore scan action usage +# and parameters, see https://github.com/anchore/scan-action. For more +# information on Anchore's container image scanning tool Grype, see +# https://github.com/anchore/grype +name: Anchore Container Scan + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '00 07 * * *' + +permissions: + contents: read + +env: + DOCKERFILE: Dockerfile + +jobs: + Anchore-Build-Scan: + 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 + runs-on: ubuntu-latest + steps: + - name: Checkout the code + uses: actions/checkout@v3 + + - name: Build the Docker image + run: docker build . --file ${{ env.DOCKERFILE }} --tag localbuild/testimage:latest + + - name: Run the Anchore scan action itself with GitHub Advanced Security code scanning integration enabled + uses: anchore/scan-action@v3 + with: + image: "localbuild/testimage:latest" + acs-report-enable: true + fail-build: true + severity-cutoff: "high" + - name: Upload Anchore Scan Report + uses: github/codeql-action/upload-sarif@v2 + if: always() + with: + sarif_file: results.sarif diff --git a/.github/workflows/check_pr.yml b/.github/workflows/check_pr.yml new file mode 100644 index 0000000..57a5458 --- /dev/null +++ b/.github/workflows/check_pr.yml @@ -0,0 +1,226 @@ +name: Check PR + +# Controls when the workflow will run +on: + pull_request: + branches: + - main + types: [ opened, synchronize, labeled, unlabeled, reopened, edited ] + + +permissions: + pull-requests: write + + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + auto_assign: + name: Auto Assign + + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Assign Me + # You may pin to the exact commit or the version. + uses: kentaro-m/auto-assign-action@v1.2.1 + with: + configuration-path: '.github/auto_assign.yml' + + check_labels: + name: Check Required Labels + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Verify PR Labels + if: ${{ !contains(github.event.pull_request.labels.*.name, 'major') && !contains(github.event.pull_request.labels.*.name, 'minor') && !contains(github.event.pull_request.labels.*.name, 'patch') && !contains(github.event.pull_request.labels.*.name, 'skip') }} + uses: actions/github-script@v6.3.3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + var comments = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + for (const comment of comments.data) { + if (comment.body.includes('This pull request does not contain a valid label')){ + github.rest.issues.deleteComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id + }) + } + } + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'This pull request does not contain a valid label. Please add one of the following labels: `[patch, minor, major, skip]`' + }) + core.setFailed('Missing required labels') + + + check_format: + name: Check Format + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Formatting + id: format + continue-on-error: true + uses: axel-op/googlejavaformat-action@v3 + with: + args: "--set-exit-if-changed" + + - uses: actions/github-script@v6.3.3 + if: steps.format.outcome != 'success' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + console.log(context); + var comments = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + for (const comment of comments.data) { + console.log(comment); + if (comment.body.includes('Comment this PR with')){ + github.rest.issues.deleteComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id + }) + } + } + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'Comment this PR with *update_code* to update `openapi.json` and format the code. Consider to use pre-commit to format the code.' + }) + core.setFailed('Format your code.') + + check_size: + runs-on: ubuntu-latest + name: Check Size + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Check Size + uses: actions/github-script@v6.3.3 + env: + IGNORED_FILES: openapi.json, openapi-node.json + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const additions = context.payload.pull_request.additions || 0 + const deletions = context.payload.pull_request.deletions || 0 + var changes = additions + deletions + console.log('additions: '+additions+' + deletions: '+deletions+ ' = total changes: ' + changes); + + const { IGNORED_FILES } = process.env + const ignored_files = IGNORED_FILES.trim().split(',').filter(word => word.length > 0); + if (ignored_files.length > 0){ + var ignored = 0 + const execSync = require('child_process').execSync; + for (const file of IGNORED_FILES.trim().split(',')) { + + const ignored_additions_str = execSync('git --no-pager diff --numstat origin/main..origin/${{ github.head_ref}} | grep ' + file + ' | cut -f 1', { encoding: 'utf-8' }) + const ignored_deletions_str = execSync('git --no-pager diff --numstat origin/main..origin/${{ github.head_ref}} | grep ' + file + ' | cut -f 2', { encoding: 'utf-8' }) + + const ignored_additions = ignored_additions_str.split('\n').map(elem=> parseInt(elem || 0)).reduce( + (accumulator, currentValue) => accumulator + currentValue, + 0); + const ignored_deletions = ignored_deletions_str.split('\n').map(elem=> parseInt(elem || 0)).reduce( + (accumulator, currentValue) => accumulator + currentValue, + 0); + + ignored += ignored_additions + ignored_deletions; + } + changes -= ignored + console.log('ignored lines: ' + ignored + ' , consider changes: ' + changes); + } + + if (changes < 200){ + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['size/small'] + }) + + + var labels = await github.rest.issues.listLabelsOnIssue({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + + if (labels.data.find(label => label.name == 'size/large')){ + github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: 'size/large' + }) + } + } + + if (changes > 400){ + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['size/large'] + }) + + var comments = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + for (const comment of comments.data) { + if (comment.body.includes('This PR exceeds the recommended size')){ + github.rest.issues.deleteComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id + }) + } + } + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'This PR exceeds the recommended size of 400 lines. Please make sure you are NOT addressing multiple issues with one PR. _Note this PR might be rejected due to its size._' + }) + + var labels = await github.rest.issues.listLabelsOnIssue({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + + if (labels.data.find(label => label.name == 'size/small')){ + github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: 'size/small' + }) + } + + } diff --git a/.github/workflows/code_review.yml b/.github/workflows/code_review.yml new file mode 100644 index 0000000..a84e924 --- /dev/null +++ b/.github/workflows/code_review.yml @@ -0,0 +1,124 @@ +name: Code Review + +# Controls when the workflow will run +on: + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + push: + branches: + - main + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +env: + PROJECT_KEY: pagopa_pagopa-print-payment-notice-functions + +permissions: + id-token: write + contents: read + deployments: write + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + code-review: + name: Code Review + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Code Review + uses: pagopa/github-actions-template/maven-code-review@v1.10.6 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + sonar_token: ${{ secrets.SONAR_TOKEN }} + project_key: ${{env.PROJECT_KEY}} + coverage_exclusions: "**/config/*,**/*Mock*,**/model/**,**/entity/*,**/exception/*" + cpd_exclusions: "**/models/**,**/entity/*" + java_version: 17 + +# smoke-test: +# name: Smoke Test +# runs-on: ubuntu-latest +# environment: +# name: dev +# steps: +# - name: Checkout +# id: checkout +# uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656657ea2c6707 +# +# - name: Login +# id: login +# # from https://github.com/Azure/login/commits/master +# uses: azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 +# with: +# client-id: ${{ secrets.CLIENT_ID }} +# tenant-id: ${{ secrets.TENANT_ID }} +# subscription-id: ${{ secrets.SUBSCRIPTION_ID }} +# +# - name: Run Service on Docker +# shell: bash +# id: run_service_docker +# run: | +# cd ./docker +# chmod +x ./run_docker.sh +# ./run_docker.sh local +# +# - name: Run Integration Tests +# shell: bash +# id: run_integration_test +# run: | +# export CUCUMBER_PUBLISH_TOKEN=${{ secrets.CUCUMBER_PUBLISH_TOKEN }} +# export RECEIPTS_STORAGE_CONN_STRING='${{ secrets.RECEIPTS_STORAGE_CONN_STRING }}' +# export RECEIPTS_COSMOS_CONN_STRING='${{ secrets.RECEIPTS_COSMOS_CONN_STRING }}' +# export AES_SALT='${{ secrets.AES_SALT }}' +# export AES_SECRET_KEY='${{ secrets.AES_SECRET_KEY }}' +# +# cd ./integration-test +# chmod +x ./run_integration_test.sh +# ./run_integration_test.sh local +# +# delete_github_deployments: +# runs-on: ubuntu-latest +# needs: smoke-test +# if: ${{ always() }} +# steps: +# - name: Dump GitHub context +# env: +# GITHUB_CONTEXT: ${{ toJSON(github) }} +# run: echo "$GITHUB_CONTEXT" +# +# - name: Delete Previous deployments +# uses: actions/github-script@v6 +# env: +# SHA_HEAD: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.sha}} +# with: +# script: | +# const { SHA_HEAD } = process.env +# +# const deployments = await github.rest.repos.listDeployments({ +# owner: context.repo.owner, +# repo: context.repo.repo, +# sha: SHA_HEAD +# }); +# await Promise.all( +# deployments.data.map(async (deployment) => { +# await github.rest.repos.createDeploymentStatus({ +# owner: context.repo.owner, +# repo: context.repo.repo, +# deployment_id: deployment.id, +# state: 'inactive' +# }); +# return github.rest.repos.deleteDeployment({ +# owner: context.repo.owner, +# repo: context.repo.repo, +# deployment_id: deployment.id +# }); +# }) +# ); diff --git a/.github/workflows/deploy_with_github_runner.yml b/.github/workflows/deploy_with_github_runner.yml new file mode 100644 index 0000000..a1f0c66 --- /dev/null +++ b/.github/workflows/deploy_with_github_runner.yml @@ -0,0 +1,84 @@ +name: Deploy on AKS + +on: + workflow_call: + inputs: + environment: + required: true + description: The name of the environment where to deploy + type: string + target: + required: true + description: The environment target of the job + type: string + +env: + NAMESPACE: printit + APP_NAME: pagopa_print_payment_notice_functions + +permissions: + id-token: write + contents: read + +jobs: + create_runner: + name: Create Runner + runs-on: ubuntu-22.04 + environment: + name: ${{ inputs.environment }} + if: ${{ inputs.target == inputs.environment || inputs.target == 'all' }} + outputs: + runner_name: ${{ steps.create_github_runner.outputs.runner_name }} + steps: + - name: Create GitHub Runner + id: create_github_runner + # from https://github.com/pagopa/eng-github-actions-iac-template/tree/main/azure/github-self-hosted-runner-azure-create-action + uses: pagopa/eng-github-actions-iac-template/azure/github-self-hosted-runner-azure-create-action@main + with: + client_id: ${{ secrets.CLIENT_ID }} + tenant_id: ${{ secrets.TENANT_ID }} + subscription_id: ${{ secrets.SUBSCRIPTION_ID }} + container_app_environment_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_NAME }} + resource_group_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME }} # RG of the runner + pat_token: ${{ secrets.BOT_TOKEN_GITHUB }} + # self_hosted_runner_image_tag: "v1.4.1" + + deploy: + needs: [ create_runner ] + runs-on: [ self-hosted, "${{ needs.create_runner.outputs.runner_name }}" ] + if: ${{ inputs.target == inputs.environment || inputs.target == 'all' }} + name: Deploy on AKS + environment: ${{ inputs.environment }} + steps: + - name: Deploy + uses: pagopa/github-actions-template/aks-deploy@main + with: + branch: ${{ github.ref_name }} + client_id: ${{ secrets.CLIENT_ID }} + subscription_id: ${{ secrets.SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.TENANT_ID }} + env: ${{ inputs.environment }} + namespace: ${{ env.NAMESPACE }} + cluster_name: ${{ vars.CLUSTER_NAME }} + resource_group: ${{ vars.CLUSTER_RESOURCE_GROUP }} + app_name: ${{ env.APP_NAME }} + helm_upgrade_options: "--debug" + + cleanup_runner: + name: Cleanup Runner + needs: [ create_runner, deploy ] + if: ${{ success() || failure() && inputs.target == inputs.environment || inputs.target == 'all' }} + runs-on: ubuntu-22.04 + environment: ${{ inputs.environment }} + steps: + - name: Cleanup GitHub Runner + id: cleanup_github_runner + # from https://github.com/pagopa/eng-github-actions-iac-template/tree/main/azure/github-self-hosted-runner-azure-cleanup-action + uses: pagopa/eng-github-actions-iac-template/azure/github-self-hosted-runner-azure-cleanup-action@0ee2f58fd46d10ac7f00bce4304b98db3dbdbe9a + with: + client_id: ${{ secrets.CLIENT_ID }} + tenant_id: ${{ secrets.TENANT_ID }} + subscription_id: ${{ secrets.SUBSCRIPTION_ID }} + resource_group_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME }} + runner_name: ${{ needs.create_runner.outputs.runner_name }} + pat_token: ${{ secrets.BOT_TOKEN_GITHUB }} \ No newline at end of file diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml new file mode 100644 index 0000000..406ba3c --- /dev/null +++ b/.github/workflows/integration_test.yml @@ -0,0 +1,158 @@ +name: Integration Tests + +on: + schedule: + - cron: '00 06 * * *' + + workflow_dispatch: + inputs: + environment: + required: true + type: choice + description: Select the Environment + options: + - dev + - uat + canary: + description: 'run the tests on canary version' + required: false + type: boolean + default: false + notify: + description: 'notify test results' + required: false + type: boolean + default: true + +permissions: + id-token: write + contents: read + deployments: write + + +jobs: + create_runner: + name: Create Runner + runs-on: ubuntu-22.04 + environment: + name: ${{(github.event.inputs == null && 'uat') || inputs.environment }} + outputs: + runner_name: ${{ steps.create_github_runner.outputs.runner_name }} + steps: + - name: Create GitHub Runner + id: create_github_runner + # from https://github.com/pagopa/eng-github-actions-iac-template/tree/main/azure/github-self-hosted-runner-azure-create-action + uses: pagopa/eng-github-actions-iac-template/azure/github-self-hosted-runner-azure-create-action@main + with: + client_id: ${{ secrets.CLIENT_ID }} + tenant_id: ${{ secrets.TENANT_ID }} + subscription_id: ${{ secrets.SUBSCRIPTION_ID }} + container_app_environment_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_NAME }} + resource_group_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME }} # RG of the runner + pat_token: ${{ secrets.BOT_TOKEN_GITHUB }} + # self_hosted_runner_image_tag: "v1.6.0" + + integration_test: + needs: [ create_runner ] + name: Test ${{(github.event.inputs == null && 'uat') || inputs.environment }} + runs-on: [ self-hosted, "${{ needs.create_runner.outputs.runner_name }}" ] + environment: ${{(github.event.inputs == null && 'uat') || inputs.environment }} + steps: + + - name: Checkout + id: checkout + uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656657ea2c6707 + + - name: Login + id: login + # from https://github.com/Azure/login/commits/master + uses: azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 + with: + client-id: ${{ secrets.CLIENT_ID }} + tenant-id: ${{ secrets.TENANT_ID }} + subscription-id: ${{ secrets.SUBSCRIPTION_ID }} + + - name: Run Integration Tests + shell: bash + run: | + export CUCUMBER_PUBLISH_TOKEN=${{ secrets.CUCUMBER_PUBLISH_TOKEN }} + export RECEIPTS_STORAGE_CONN_STRING='${{ secrets.RECEIPTS_STORAGE_CONN_STRING }}' + export RECEIPTS_COSMOS_CONN_STRING='${{ secrets.RECEIPTS_COSMOS_CONN_STRING }}' + export AES_SALT='${{ secrets.AES_SALT }}' + export AES_SECRET_KEY='${{ secrets.AES_SECRET_KEY }}' + + cd ./integration-test + chmod +x ./run_integration_test.sh + ./run_integration_test.sh ${{( github.event.inputs == null && 'uat') || inputs.environment }} + + notify: + needs: [ create_runner, integration_test ] + runs-on: [ self-hosted, "${{ needs.create_runner.outputs.runner_name }}" ] + name: Notify + if: ${{ (success() || failure()) && (github.event.inputs == null || inputs.notify)}} + steps: + - name: Report Status + if: always() + uses: ravsamhq/notify-slack-action@v2 + with: + status: ${{ needs.integration_test.result }} + token: ${{ secrets.GITHUB_TOKEN }} + notify_when: 'failure,skipped' + notification_title: "<{run_url}|Scheduled Integration Test> has {status_message} in ${{( github.event.inputs == null && 'uat') || inputs.environment }} env" + message_format: '{emoji} <{run_url}|{workflow}> {status_message} in <{repo_url}|{repo}>' + footer: 'Linked to <{workflow_url}| workflow file>' + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + delete_github_deployments: + runs-on: ubuntu-latest + needs: integration_test + if: ${{ always() }} + steps: + - name: Delete Previous deployments + uses: actions/github-script@v6 + env: + SHA_HEAD: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.sha}} + with: + script: | + const { SHA_HEAD } = process.env + + const deployments = await github.rest.repos.listDeployments({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: SHA_HEAD + }); + await Promise.all( + deployments.data.map(async (deployment) => { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: 'inactive' + }); + return github.rest.repos.deleteDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id + }); + }) + ); + + cleanup_runner: + name: Cleanup Runner + needs: [ create_runner, integration_test ] + if: ${{ always() }} + runs-on: ubuntu-22.04 + environment: ${{(github.event.inputs == null && 'uat') || inputs.environment }} + steps: + - name: Cleanup GitHub Runner + id: cleanup_github_runner + # from https://github.com/pagopa/eng-github-actions-iac-template/tree/main/azure/github-self-hosted-runner-azure-cleanup-action + uses: pagopa/eng-github-actions-iac-template/azure/github-self-hosted-runner-azure-cleanup-action@0ee2f58fd46d10ac7f00bce4304b98db3dbdbe9a + with: + client_id: ${{ secrets.CLIENT_ID }} + tenant_id: ${{ secrets.TENANT_ID }} + subscription_id: ${{ secrets.SUBSCRIPTION_ID }} + resource_group_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME }} + runner_name: ${{ needs.create_runner.outputs.runner_name }} + pat_token: ${{ secrets.BOT_TOKEN_GITHUB }} \ No newline at end of file diff --git a/.github/workflows/release-deploy.yml b/.github/workflows/release-deploy.yml new file mode 100644 index 0000000..8895a9a --- /dev/null +++ b/.github/workflows/release-deploy.yml @@ -0,0 +1,138 @@ +name: Release And Deploy + +# Controls when the workflow will run +on: + pull_request: + branches: + - main + types: [ closed ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + inputs: + environment: + required: true + type: choice + description: Select the Environment + options: + - dev + - uat + - prod + - all + semver: + required: true + type: choice + description: Select the new Semantic Version + options: + - major + - minor + - patch + - buildNumber + - skip + default: skip + beta: + required: false + type: boolean + description: deploy beta version on AKS + default: false + +permissions: + packages: write + contents: write + issues: write + id-token: write + actions: read + + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + setup: + name: Setup + runs-on: ubuntu-latest + outputs: + semver: ${{ steps.get_semver.outputs.semver }} + environment: ${{ steps.output.outputs.environment }} + steps: + - name: Get semver + id: get_semver + uses: pagopa/github-actions-template/semver-setup@v1.4.2 + + - if: ${{ github.event.inputs.environment == null || github.event.inputs.environment == 'dev' }} + run: echo "ENVIRNOMENT=dev" >> $GITHUB_ENV + + - if: ${{ github.event.inputs.environment == 'uat' }} + run: echo "ENVIRNOMENT=uat" >> $GITHUB_ENV + + - if: ${{ github.event.inputs.environment == 'prod' }} + run: echo "ENVIRNOMENT=prod" >> $GITHUB_ENV + + - if: ${{ github.event.inputs.environment == 'all' }} + run: echo "ENVIRNOMENT=all" >> $GITHUB_ENV + + - id: output + name: Set Output + run: | + echo "environment=${{env.ENVIRNOMENT}}" >> $GITHUB_OUTPUT + + + release: + name: Create a New Release + runs-on: ubuntu-latest + needs: [setup] + outputs: + version: ${{ steps.release.outputs.version }} + steps: + - name: Make Release + id: release + uses: pagopa/github-actions-template/maven-release@v1.5.4 + with: + semver: ${{ needs.setup.outputs.semver }} + github_token: ${{ secrets.BOT_TOKEN_GITHUB }} + beta: ${{ inputs.beta }} + skip_ci: ${{ inputs.beta }} + + image: + needs: [ setup, release ] + name: Build and Push Docker Image + runs-on: ubuntu-latest + if: ${{ inputs.semver != 'skip' }} + steps: + - name: Build and Push + id: semver + uses: pagopa/github-actions-template/ghcr-build-push@v1.5.4 + with: + branch: ${{ github.ref_name}} + github_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ needs.release.outputs.version }} + + deploy_aks: + name: Deploy on AKS + needs: [ setup, release, image ] + if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} + strategy: + matrix: + environment: [ dev, uat, prod ] + uses: ./.github/workflows/deploy_with_github_runner.yml + with: + environment: ${{ matrix.environment }} + target: ${{ needs.setup.outputs.environment }} + secrets: inherit + + notify: + needs: [ deploy_aks ] + runs-on: ubuntu-latest + name: Notify + if: always() + steps: + - name: Report Status + if: always() + uses: ravsamhq/notify-slack-action@v2 + with: + status: ${{ needs.deploy_aks.result }} + token: ${{ secrets.GITHUB_TOKEN }} + notify_when: 'failure,skipped' + notification_title: '{workflow} has {status_message}' + message_format: '{emoji} <{workflow_url}|{workflow}> {status_message} in <{repo_url}|{repo}>' + footer: 'Linked to Repo <{repo_url}|{repo}>' + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7afb77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# Build output +target/ +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# IDE +.idea/ +*.iml +.settings/ +.project +.classpath +.vscode/ + +# macOS +.DS_Store + +# Azure Functions +local.settings.json +bin/ +obj/ +**/.terraform +**/node_modules +yarn.lock +**/*.copy.js +**/*.local.prod +**/*.local.uat +.azure/ + +.env \ No newline at end of file diff --git a/.identity/.terraform.lock.hcl b/.identity/.terraform.lock.hcl new file mode 100644 index 0000000..7cfd5bc --- /dev/null +++ b/.identity/.terraform.lock.hcl @@ -0,0 +1,64 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/azuread" { + version = "2.30.0" + constraints = "2.30.0" + hashes = [ + "h1:WnSPiREAFwnBUKREokMdHQ8Cjs47MzvS9pG8VS1ktec=", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:2e62c193030e04ebb10cc0526119cf69824bf2d7e4ea5a2f45bd5d5fb7221d36", + "zh:2f3c7a35257332d68b778cefc5201a5f044e4914dd03794a4da662ddfe756483", + "zh:35d0d3a1b58fdb8b8c4462d6b7e7016042da43ea9cc734ce897f52a73407d9b0", + "zh:47ede0cd0206ec953d40bf4a80aa6e59af64e26cbbd877614ac424533dbb693b", + "zh:48c190307d4d42ea67c9b8cc544025024753f46cef6ea64db84735e7055a72da", + "zh:6fff9b2c6a962252a70a15b400147789ab369b35a781e9d21cce3804b04d29af", + "zh:7646980cf3438bff29c91ffedb74458febbb00a996638751fbd204ab1c628c9b", + "zh:77aa2fa7ca6d5446afa71d4ff83cb87b70a2f3b72110fc442c339e8e710b2928", + "zh:e20b2b2c37175b89dd0db058a096544d448032e28e3b56e2db368343533a9684", + "zh:eab175b1dfe9865ad9404dccb6d5542899f8c435095aa7c679314b811c717ce7", + "zh:efc862bd78c55d2ff089729e2a34c1831ab4b0644fc11b36ee4ebed00a4797ba", + ] +} + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "3.45.0" + constraints = "3.45.0" + hashes = [ + "h1:gQLNY1I5e9kcle1p/VYEWb0eteQ/t5kUfnqVu2/GBNY=", + "zh:04c5dbb8845366ce5eb0dc2d55e151270cc2c0ace20993867fdae9af43b953ad", + "zh:2589585da615ccae341400d45d672ee3fae413fdd88449b5befeff12a85a44b2", + "zh:603869ed98fff5d9bf841a51afd9e06b628533c59356c8433aef4b15df63f5f7", + "zh:853fecab9c987b6772c8d9aa10362675f6c626b60ebc7118aa33ce91366fcc38", + "zh:979848c45e8e058862c36ba3a661457f7c81ef26ebb6634f479600de9c203d65", + "zh:9b512c8588ecc9c1b803b746a3a8517422561a918f0dfb0faaa707ed53ef1760", + "zh:a9601ffb58043426bcff1220662d6d137f0b2857a24f2dcf180aeac2c9cea688", + "zh:d52d2652328f0ed3ba202561d88cb9f43c174edbfaab1abf69f772125dbfe15e", + "zh:d92d91ca597c47f575bf3ae129f4b723be9b7dcb71b906ec6ec740fac29b1aaa", + "zh:ded73b730e4197b70fda9e83447c119f92f75dc37be3ff2ed45730c8f0348c28", + "zh:ec37ac332d50f8ca5827f97198346b0f8ecbf470e2e3ba1e027bb389d826b902", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/integrations/github" { + version = "5.18.3" + constraints = "5.18.3" + hashes = [ + "h1:rv3mwpUeJ0n13sY+KZMI25WAVCSeipX4n8JMWKD1XcE=", + "zh:050b37d96628cb7451137755929ca8d21ea546bc46d11a715652584070e83ff2", + "zh:053051061f1b7f7673b0ceffac1f239ba28b0e5b375999206fd39976e85d9f2b", + "zh:0c300a977ca66d0347ed62bb116fd8fc9abb376a554d4c192d14f3ea71c83500", + "zh:1d5a1a5243eba78819d2f92ff2d504ebf9a9008a6670fb5f5660f44eb6a156d8", + "zh:a13ac15d251ebf4e7dc40acb0e40df066f443f4c7799186a29e2e44addc7d8e7", + "zh:a316d94b885953c036ebc9fba64a23da93974746bc3ac9d207462a6f02d44540", + "zh:a658a00373bff5979cc227052c693cbde8ca4c8f9fef1bc8094a3516f2e2a96d", + "zh:a7bfc6ad8465d5dc11b6f19d6805364de87fffe27622bb4f37da2319bb1c4956", + "zh:d7379a76861f1a6bfc36eca7a20f1f477711247563b105744d69d7bd1f365fad", + "zh:de1cd959fd4821248e8d21570601193408648474e74f49597f1d0c43185a4ab7", + "zh:e0b281240dd6f2aa405b2d6fe329bc15ab877161affe163fb150d1efca2fccdb", + "zh:e372c171358757a983d7aa878abfd05a84484fb4d22167e45c9c1267e78ed060", + "zh:f6d3116526030b3f6905f530cd6c04b23d42890d973fa2abe10ce9c89cb1db80", + "zh:f99eec731e03cc6a28996c875bd435887cd7ea75ec07cc77b9e768bb12da2227", + ] +} diff --git a/.identity/00_data.tf b/.identity/00_data.tf new file mode 100644 index 0000000..9e17ced --- /dev/null +++ b/.identity/00_data.tf @@ -0,0 +1,57 @@ +data "azurerm_storage_account" "tf_storage_account" { + name = "pagopainfraterraform${var.env}" + resource_group_name = "io-infra-rg" +} + +data "azurerm_resource_group" "dashboards" { + name = "dashboards" +} + +data "azurerm_kubernetes_cluster" "aks" { + name = local.aks_cluster.name + resource_group_name = local.aks_cluster.resource_group_name +} + +data "github_organization_teams" "all" { + root_teams_only = true + summary_only = true +} + +data "azurerm_user_assigned_identity" "identity_cd_01" { + name = "${local.prefix}-${var.env_short}-${local.domain}-01-github-cd-identity" + resource_group_name = "${local.prefix}-${var.env_short}-identity-rg" +} + +data "azurerm_key_vault" "key_vault" { + name = "pagopa-${var.env_short}-kv" + resource_group_name = "pagopa-${var.env_short}-sec-rg" +} + +data "azurerm_key_vault" "domain_key_vault" { + name = "pagopa-${var.env_short}-itn-${local.domain}-kv" + resource_group_name = "pagopa-${var.env_short}-itn-${local.domain}-sec-rg" +} + +data "azurerm_resource_group" "apim_resource_group" { + name = "${local.product}-api-rg" +} + +data "azurerm_key_vault_secret" "key_vault_sonar" { + name = "sonar-token" + key_vault_id = data.azurerm_key_vault.key_vault.id +} + +data "azurerm_key_vault_secret" "key_vault_bot_token" { + name = "bot-token-github" + key_vault_id = data.azurerm_key_vault.key_vault.id +} + +data "azurerm_key_vault_secret" "key_vault_cucumber_token" { + name = "cucumber-token" + key_vault_id = data.azurerm_key_vault.key_vault.id +} + +data "azurerm_key_vault_secret" "key_vault_integration_test_subkey" { + name = "integration-test-subkey" + key_vault_id = data.azurerm_key_vault.key_vault.id +} diff --git a/.identity/01_github_environment.tf b/.identity/01_github_environment.tf new file mode 100644 index 0000000..ec4caad --- /dev/null +++ b/.identity/01_github_environment.tf @@ -0,0 +1,79 @@ +resource "github_repository_environment" "github_repository_environment" { + environment = var.env + repository = local.github.repository + # filter teams reviewers from github_organization_teams + # if reviewers_teams is null no reviewers will be configured for environment + dynamic "reviewers" { + for_each = (var.github_repository_environment.reviewers_teams == null || var.env_short != "p" ? [] : [1]) + content { + teams = matchkeys( + data.github_organization_teams.all.teams.*.id, + data.github_organization_teams.all.teams.*.name, + var.github_repository_environment.reviewers_teams + ) + } + } + deployment_branch_policy { + protected_branches = var.github_repository_environment.protected_branches + custom_branch_policies = var.github_repository_environment.custom_branch_policies + } +} + +locals { + env_secrets = { + "CLIENT_ID" : data.azurerm_user_assigned_identity.identity_cd_01.client_id, + "TENANT_ID" : data.azurerm_client_config.current.tenant_id, + "SUBSCRIPTION_ID" : data.azurerm_subscription.current.subscription_id, + "SUBKEY" : data.azurerm_key_vault_secret.key_vault_integration_test_subkey.value, + } + env_variables = { + "CONTAINER_APP_ENVIRONMENT_NAME" : local.container_app_environment.name, + "CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME" : local.container_app_environment.resource_group, + "CLUSTER_NAME" : local.aks_cluster.name, + "CLUSTER_RESOURCE_GROUP" : local.aks_cluster.resource_group_name, + "NAMESPACE" : local.domain, + } + repo_secrets = { + "SONAR_TOKEN" : data.azurerm_key_vault_secret.key_vault_sonar.value, + "BOT_TOKEN_GITHUB" : data.azurerm_key_vault_secret.key_vault_bot_token.value, + "CUCUMBER_PUBLISH_TOKEN" : data.azurerm_key_vault_secret.key_vault_cucumber_token.value, + } +} + +############### +# ENV Secrets # +############### + +resource "github_actions_environment_secret" "github_environment_runner_secrets" { + for_each = local.env_secrets + repository = local.github.repository + environment = var.env + secret_name = each.key + plaintext_value = each.value +} + +################# +# ENV Variables # +################# + + +resource "github_actions_environment_variable" "github_environment_runner_variables" { + for_each = local.env_variables + repository = local.github.repository + environment = var.env + variable_name = each.key + value = each.value +} + +############################# +# Secrets of the Repository # +############################# + + +resource "github_actions_secret" "repo_secrets" { + for_each = local.repo_secrets + repository = local.github.repository + secret_name = each.key + plaintext_value = each.value +} + diff --git a/.identity/99_main.tf b/.identity/99_main.tf new file mode 100644 index 0000000..c5eb056 --- /dev/null +++ b/.identity/99_main.tf @@ -0,0 +1,32 @@ +terraform { + required_version = ">=1.3.0" + + required_providers { + azuread = { + source = "hashicorp/azuread" + version = "2.30.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = "3.45.0" + } + github = { + source = "integrations/github" + version = "5.18.3" + } + } + + backend "azurerm" {} +} + +provider "azurerm" { + features {} +} + +provider "github" { + owner = "pagopa" +} + +data "azurerm_subscription" "current" {} + +data "azurerm_client_config" "current" {} diff --git a/.identity/99_variables.tf b/.identity/99_variables.tf new file mode 100644 index 0000000..aa316fa --- /dev/null +++ b/.identity/99_variables.tf @@ -0,0 +1,56 @@ +locals { + github = { + org = "pagopa" + repository = "pagopa-print-payment-notice-functions" + } + + prefix = "pagopa" + domain = "printit" + location_short = "itn" + product = "${var.prefix}-${var.env_short}" + + app_name = "github-${local.github.org}-${local.github.repository}-${var.prefix}-${local.domain}-${var.env}-aks" + + aks_cluster = { + name = "${local.product}-${local.location_short}-${var.env}-aks" + resource_group_name = "${local.product}-${local.location_short}-${var.env}-aks-rg" + } + + container_app_environment = { + name = "${local.prefix}-${var.env_short}-${local.location_short}-core-tools-cae", + resource_group = "${local.prefix}-${var.env_short}-${local.location_short}-core-tools-rg", + } +} + +variable "env" { + type = string +} + +variable "env_short" { + type = string +} + +variable "prefix" { + type = string + default = "pagopa" + validation { + condition = ( + length(var.prefix) <= 6 + ) + error_message = "Max length is 6 chars." + } +} + +variable "github_repository_environment" { + type = object({ + protected_branches = bool + custom_branch_policies = bool + reviewers_teams = list(string) + }) + description = "GitHub Continuous Integration roles" + default = { + protected_branches = false + custom_branch_policies = true + reviewers_teams = ["pagopa-team-core"] + } +} diff --git a/.identity/env/dev/backend.ini b/.identity/env/dev/backend.ini new file mode 100644 index 0000000..f3ea2d5 --- /dev/null +++ b/.identity/env/dev/backend.ini @@ -0,0 +1 @@ +subscription=DEV-pagoPA \ No newline at end of file diff --git a/.identity/env/dev/backend.tfvars b/.identity/env/dev/backend.tfvars new file mode 100644 index 0000000..0440582 --- /dev/null +++ b/.identity/env/dev/backend.tfvars @@ -0,0 +1,4 @@ +resource_group_name = "io-infra-rg" +storage_account_name = "pagopainfraterraformdev" +container_name = "azurermstate" +key = "pagopa-print-payment-notice-functions.tfstate" diff --git a/.identity/env/dev/terraform.tfvars b/.identity/env/dev/terraform.tfvars new file mode 100644 index 0000000..7b9be8d --- /dev/null +++ b/.identity/env/dev/terraform.tfvars @@ -0,0 +1,11 @@ +prefix = "pagopa" +env = "dev" +env_short = "d" + +tags = { + CreatedBy = "Terraform" + Environment = "Dev" + Owner = "pagoPA" + Source = "https://github.com/pagopa/pagopa-print-payment-notice-functions" + CostCenter = "TS310 - PAGAMENTI & SERVIZI" +} diff --git a/.identity/env/prod/backend.ini b/.identity/env/prod/backend.ini new file mode 100644 index 0000000..6318425 --- /dev/null +++ b/.identity/env/prod/backend.ini @@ -0,0 +1 @@ +subscription=PROD-pagoPA diff --git a/.identity/env/prod/backend.tfvars b/.identity/env/prod/backend.tfvars new file mode 100644 index 0000000..664b55b --- /dev/null +++ b/.identity/env/prod/backend.tfvars @@ -0,0 +1,4 @@ +resource_group_name = "io-infra-rg" +storage_account_name = "pagopainfraterraformprod" +container_name = "azurermstate" +key = "pagopa-print-payment-notice-functions.tfstate" diff --git a/.identity/env/prod/terraform.tfvars b/.identity/env/prod/terraform.tfvars new file mode 100644 index 0000000..a0604d3 --- /dev/null +++ b/.identity/env/prod/terraform.tfvars @@ -0,0 +1,11 @@ +prefix = "pagopa" +env = "prod" +env_short = "p" + +tags = { + CreatedBy = "Terraform" + Environment = "Prod" + Owner = "pagoPA" + Source = "https://github.com/pagopa/pagopa-print-payment-notice-functions" + CostCenter = "TS310 - PAGAMENTI & SERVIZI" +} diff --git a/.identity/env/uat/backend.ini b/.identity/env/uat/backend.ini new file mode 100644 index 0000000..1a01415 --- /dev/null +++ b/.identity/env/uat/backend.ini @@ -0,0 +1 @@ +subscription=UAT-pagoPA diff --git a/.identity/env/uat/backend.tfvars b/.identity/env/uat/backend.tfvars new file mode 100644 index 0000000..5e880cc --- /dev/null +++ b/.identity/env/uat/backend.tfvars @@ -0,0 +1,4 @@ +resource_group_name = "io-infra-rg" +storage_account_name = "pagopainfraterraformuat" +container_name = "azurermstate" +key = "pagopa-print-payment-notice-functions.tfstate" diff --git a/.identity/env/uat/terraform.tfvars b/.identity/env/uat/terraform.tfvars new file mode 100644 index 0000000..7e6ad3e --- /dev/null +++ b/.identity/env/uat/terraform.tfvars @@ -0,0 +1,11 @@ +prefix = "pagopa" +env = "uat" +env_short = "u" + +tags = { + CreatedBy = "Terraform" + Environment = "Uat" + Owner = "pagoPA" + Source = "https://github.com/pagopa/pagopa-print-payment-notice-functions" + CostCenter = "TS310 - PAGAMENTI & SERVIZI" +} diff --git a/.java-version b/.java-version new file mode 100644 index 0000000..b4de394 --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +11 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..790419f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +# 1. `pip install pre-commit` +# 2. `pre-commit install` +# 3. set GITGUARDIAN_API_KEY in your develop environment (get an api key here: https://dashboard.gitguardian.com/workspace/230910/settings/personal/personal-access-tokens) +# more info https://docs.gitguardian.com/internal-repositories-monitoring/integrations/git_hooks/pre_commit +repos: + # - repo: https://github.com/gitguardian/ggshield + # rev: v1.11.0 + # hooks: + # - id: ggshield + # language_version: python3 + # stages: [ commit ] + - repo: https://github.com/ejba/pre-commit-maven + rev: v0.3.3 + hooks: + - id: maven + args: [ 'clean compile', "spotless:apply" ] + - id: maven-spotless-apply + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - repo: https://github.com/gitleaks/gitleaks + rev: v8.16.1 + hooks: + - id: gitleaks diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..e514c2f --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,3 @@ +# see https://help.github.com/en/articles/about-code-owners#example-of-a-codeowners-file + +* @pagopa/pagopa-tech diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..201c851 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +ARG JAVA_VERSION=17 +# This image additionally contains function core tools – useful when using custom extensions +FROM mcr.microsoft.com/azure-functions/java:4-java$JAVA_VERSION-build AS installer-env + +COPY . /src/java-function-app +RUN echo $(ls -1 /src/java-function-app) +RUN chmod 777 /src/java-function-app/agent/config.yaml +RUN cd /src/java-function-app && \ + wget https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.19.0/jmx_prometheus_javaagent-0.19.0.jar && \ + curl -o 'opentelemetry-javaagent.jar' -L 'https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.25.1/opentelemetry-javaagent.jar' && \ + mkdir -p /home/site/wwwroot && \ + mvn clean package -Dmaven.test.skip=true && \ + cd ./target/azure-functions/ && \ + cd $(ls -d */|head -n 1) && \ + cp -a . /home/site/wwwroot && \ + cp /src/java-function-app/agent/config.yaml /home/site/wwwroot/config.yaml +RUN chmod 777 /src/java-function-app/jmx_prometheus_javaagent-0.19.0.jar && \ + cp /src/java-function-app/jmx_prometheus_javaagent-0.19.0.jar /home/site/wwwroot/jmx_prometheus_javaagent-0.19.0.jar + +RUN chmod 777 /src/java-function-app/opentelemetry-javaagent.jar && \ + cp /src/java-function-app/opentelemetry-javaagent.jar /home/site/wwwroot/opentelemetry-javaagent.jar + +# This image is ssh enabled +#FROM mcr.microsoft.com/azure-functions/java:3.0-java$JAVA_VERSION-appservice +# This image isn't ssh enabled +FROM mcr.microsoft.com/azure-functions/java:4-java$JAVA_VERSION + +ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ + AzureFunctionsJobHost__Logging__Console__IsEnabled=true + +EXPOSE 80 +EXPOSE 12345 +COPY --from=installer-env ["/home/site/wwwroot", "/home/site/wwwroot"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a442c4a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 PagoPA SpA + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/agent/config.yaml b/agent/config.yaml new file mode 100644 index 0000000..ef6a288 --- /dev/null +++ b/agent/config.yaml @@ -0,0 +1,3 @@ +excludeObjectNames: ["io.opentelemetry:*"] +rules: + - pattern: ".*" \ No newline at end of file diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..688cbb5 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,11 @@ +# Docker Environment đŸŗ +`run_docker.sh` is a script to launch the image of this microservice and all the dependencies on Docker. + +## How to use đŸ’ģ +You can use `local`, `dev`, `uat` or `prod` images + +`sh ./run_docker.sh ` + +--- + +ℹī¸ _Note_: If you run the script without the parameter, `local` is used as default. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..3b29021 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + app: + container_name: 'receipt-pdf-generator' + image: ${image} + platform: linux/amd64 + build: + dockerfile: Dockerfile + context: ../ + environment: + BRAND_LOGO_MAP: '{"MASTERCARD":"assets/mastercard.png","VISA":"assets/visa.png","AMEX":"assets/amex.png","MAESTRO":"assets/maestro.png","JCB":"assets/jcb.png","OTHER":"","DINERS":"assets/diners.png","DISCOVER":"assets/discover.png","UNIONPAY":"assets/unionpay.png"}' + PSP_INFO_MAP: '{"60000000001":{"logo":"assets/nexi-logo.png","name":"Nexi","companyName":"Nexi Payments S.p.A.","address":"Corso Sempione","buildingNumber":"55","postalCode":"20149","city":"Milano","province":"MI","fee":{"amount":"2,00"}}}' + env_file: + - ./.env + ports: + - "60486:80" diff --git a/docker/run_docker.sh b/docker/run_docker.sh new file mode 100644 index 0000000..aa50bd4 --- /dev/null +++ b/docker/run_docker.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# sh ./run_docker.sh + +ENV=$1 + +if [ -z "$ENV" ] +then + ENV="local" + echo "No environment specified: local is used." +fi + +pip3 install yq + +if [ "$ENV" = "local" ]; then + image="service-local:latest" + ENV="dev" +else + repository=$(yq -r '."microservice-chart".image.repository' ../helm/values-$ENV.yaml) + image="${repository}:latest" +fi +export image=${image} + +FILE=.env +if test -f "$FILE"; then + rm .env +fi +config=$(yq -r '."microservice-chart".envConfig' ../helm/values-$ENV.yaml) +for line in $(echo $config | jq -r '. | to_entries[] | select(.key) | "\(.key)=\(.value)"'); do + echo $line >> .env +done + +keyvault=$(yq -r '."microservice-chart".keyvault.name' ../helm/values-$ENV.yaml) +secret=$(yq -r '."microservice-chart".envSecret' ../helm/values-$ENV.yaml) +for line in $(echo $secret | jq -r '. | to_entries[] | select(.key) | "\(.key)=\(.value)"'); do + IFS='=' read -r -a array <<< "$line" + response=$(az keyvault secret show --vault-name $keyvault --name "${array[1]}") + value=$(echo $response | jq -r '.value') + echo "${array[0]}=$value" >> .env +# if [ "${array[0]}" = "AFM_SA_CONNECTION_STRING" ];then +# echo "Set secret env ${array[0]}" +# echo "::add-mask::$value" +# echo AFM_SA_CONNECTION_STRING=$value >> $GITHUB_ENV +# fi +done + + +stack_name=$(cd .. && basename "$PWD") +docker compose -p "${stack_name}" up -d --remove-orphans --force-recreate --build +#docker build -t receipt-pdf-generator ../ +# docker run -d -p 60486:80 --name="${stack_name}" receipt-pdf-generator + +# waiting the containers +printf 'Waiting for the service' +attempt_counter=0 +max_attempts=50 +until [ $(curl -s -o /dev/null -w "%{http_code}" http://localhost:60486/info) -eq 200 ]; do + if [ ${attempt_counter} -eq ${max_attempts} ];then + echo "Max attempts reached" + exit 1 + fi + + printf '.' + attempt_counter=$((attempt_counter+1)) + sleep 5 +done +echo 'Service Started' diff --git a/helm/.helmignore b/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000..4bef721 --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: pagopa-print-payment-notice-generator +description: Microservice that handles services for notice print generation +type: application +version: 0.6.0 +appVersion: 0.0.5 +dependencies: + - name: microservice-chart + version: 2.8.0 + repository: "https://pagopa.github.io/aks-microservice-chart-blueprint" diff --git a/helm/values-dev.yaml b/helm/values-dev.yaml new file mode 100644 index 0000000..0b7b481 --- /dev/null +++ b/helm/values-dev.yaml @@ -0,0 +1,122 @@ +microservice-chart: + namespace: "printit" + nameOverride: "" + fullnameOverride: "print-payment-notice-functions" + image: + repository: ghcr.io/pagopa/pagopa-print-payment-notice-functions + tag: "0.0.5" + pullPolicy: Always + livenessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 150 + failureThreshold: 6 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 150 + failureThreshold: 6 + periodSeconds: 10 + deployment: + create: true + service: + create: true + type: ClusterIP + ports: + - 8080 + ingress: + create: true + host: "printit.itn.internal.dev.platform.pagopa.it" + path: /pagopa-print-payment-notice-functions/(.*) + servicePort: 8080 + serviceAccount: + create: false + annotations: {} + name: "" + podAnnotations: {} + podSecurityContext: + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + resources: + requests: + memory: "512Mi" + cpu: "0.25" + limits: + memory: "512Mi" + cpu: "0.25" + autoscaling: + enable: true + minReplica: 1 + maxReplica: 10 + pollingInterval: 10 # seconds + cooldownPeriod: 50 # seconds + triggers: + - type: cpu + metadata: + # Required + type: Utilization # Allowed types are 'Utilization' or 'AverageValue' + value: "75" + envConfig: + WEBSITE_SITE_NAME: 'print-payment-notice-functions' # required to show cloud role name in application insights + ENV: 'azure-dev' + APP_LOGGING_LEVEL: 'DEBUG' + DEFAULT_LOGGING_LEVEL: 'INFO' + OTEL_SERVICE_NAME: 'print-payment-notice-functions' + OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=dev" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.elastic-system.svc:4317" + OTEL_TRACES_EXPORTER: otlp + OTEL_METRICS_EXPORTER: otlp + OTEL_LOGS_EXPORTER: none + OTEL_TRACES_SAMPLER: "always_on" + NOTICE_REQUEST_MONGO_DB_NAME: "noticesMongoDb" + NOTICE_REQUEST_MONGO_COLLECTION_NAME: "payment_notice_generation_request" + BLOB_STORAGE_CONTAINER_NAME: "notices" + BLOB_STORAGE_ACCOUNT_ENDPOINT: "https://pagopaprintitnotices.blob.core.windows.net" + envSecret: + # required + APPLICATIONINSIGHTS_CONNECTION_STRING: 'app-insight-connection-string' + OTEL_EXPORTER_OTLP_HEADERS: 'elastic-apm-secret-token' + NOTICE_EVENTHUB_CONN_STRING: 'ehub-d-notice-complete-connection-string' + NOTICE_ERR_EVENTHUB_CONN_STRING: 'ehub-d-notice-errors-connection-string' + NOTICE_REQUEST_MONGODB_CONN_STRING: 'notices-mongo-connection-string' + NOTICE_STORAGE_CONN_STRING: 'notices-storage-account-connection-string' + AES_SECRET_KEY: 'aes-key' + AES_SALT: 'aes-salt' + keyvault: + name: "pagopa-d-itn-printit-kv" + tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" + nodeSelector: {} + tolerations: [] + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node_type + operator: In + values: + - user + canaryDelivery: + create: false + ingress: + create: true + canary: + type: header + headerName: X-Canary + headerValue: canary + weightPercent: 0 + service: + create: true + deployment: + create: true + image: + repository: ghcr.io/pagopa/pagopa-print-payment-notice-functions + tag: "0.0.0" + pullPolicy: Always + envConfig: {} + envSecret: {} diff --git a/helm/values-prod.yaml b/helm/values-prod.yaml new file mode 100644 index 0000000..3166607 --- /dev/null +++ b/helm/values-prod.yaml @@ -0,0 +1,122 @@ +microservice-chart: + namespace: "printit" + nameOverride: "" + fullnameOverride: "print-payment-notice-functions" + image: + repository: ghcr.io/pagopa/pagopa-print-payment-notice-functions + tag: "0.0.5" + pullPolicy: Always + livenessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 150 + failureThreshold: 6 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 150 + failureThreshold: 6 + periodSeconds: 10 + deployment: + create: true + service: + create: true + type: ClusterIP + ports: + - 8080 + ingress: + create: true + host: "printit.itn.internal.platform.pagopa.it" + path: /pagopa-print-payment-notice-functions/(.*) + servicePort: 8080 + serviceAccount: + create: false + annotations: {} + name: "" + podAnnotations: {} + podSecurityContext: + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + resources: + requests: + memory: "512Mi" + cpu: "0.25" + limits: + memory: "512Mi" + cpu: "0.25" + autoscaling: + enable: true + minReplica: 1 + maxReplica: 10 + pollingInterval: 10 # seconds + cooldownPeriod: 50 # seconds + triggers: + - type: cpu + metadata: + # Required + type: Utilization # Allowed types are 'Utilization' or 'AverageValue' + value: "75" + envConfig: + WEBSITE_SITE_NAME: 'print-payment-notice-functions' # required to show cloud role name in application insights + ENV: 'azure-prod' + APP_LOGGING_LEVEL: 'DEBUG' + DEFAULT_LOGGING_LEVEL: 'INFO' + OTEL_SERVICE_NAME: 'print-payment-notice-functions' + OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=prod" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.elastic-system.svc:4317" + OTEL_TRACES_EXPORTER: otlp + OTEL_METRICS_EXPORTER: otlp + OTEL_LOGS_EXPORTER: none + OTEL_TRACES_SAMPLER: "always_on" + NOTICE_REQUEST_MONGO_DB_NAME: "noticesMongoDb" + NOTICE_REQUEST_MONGO_COLLECTION_NAME: "payment_notice_generation_request" + BLOB_STORAGE_CONTAINER_NAME: "notices" + BLOB_STORAGE_ACCOUNT_ENDPOINT: "https://pagopaprintitnotices.blob.core.windows.net" + envSecret: + # required + APPLICATIONINSIGHTS_CONNECTION_STRING: 'app-insight-connection-string' + OTEL_EXPORTER_OTLP_HEADERS: 'elastic-apm-secret-token' + NOTICE_EVENTHUB_CONN_STRING: 'ehub-p-notice-complete-connection-string' + NOTICE_ERR_EVENTHUB_CONN_STRING: 'ehub-p-notice-errors-connection-string' + NOTICE_REQUEST_MONGODB_CONN_STRING: 'notices-mongo-connection-string' + NOTICE_STORAGE_CONN_STRING: 'notices-storage-account-connection-string' + AES_SECRET_KEY: 'aes-key' + AES_SALT: 'aes-salt' + keyvault: + name: "pagopa-p-itn-printit-kv" + tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" + nodeSelector: {} + tolerations: [] + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node_type + operator: In + values: + - user + canaryDelivery: + create: false + ingress: + create: true + canary: + type: header + headerName: X-Canary + headerValue: canary + weightPercent: 0 + service: + create: true + deployment: + create: true + image: + repository: ghcr.io/pagopa/pagopa-print-payment-notice-functions + tag: "0.0.0" + pullPolicy: Always + envConfig: {} + envSecret: {} diff --git a/helm/values-uat.yaml b/helm/values-uat.yaml new file mode 100644 index 0000000..9740a64 --- /dev/null +++ b/helm/values-uat.yaml @@ -0,0 +1,122 @@ +microservice-chart: + namespace: "printit" + nameOverride: "" + fullnameOverride: "print-payment-notice-generator" + image: + repository: ghcr.io/pagopa/pagopa-print-payment-notice-functions + tag: "0.0.5" + pullPolicy: Always + livenessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 150 + failureThreshold: 6 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 150 + failureThreshold: 6 + periodSeconds: 10 + deployment: + create: true + service: + create: true + type: ClusterIP + ports: + - 8080 + ingress: + create: true + host: "printit.itn.internal.uat.platform.pagopa.it" + path: /pagopa-print-payment-notice-functions/(.*) + servicePort: 8080 + serviceAccount: + create: false + annotations: {} + name: "" + podAnnotations: {} + podSecurityContext: + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + resources: + requests: + memory: "512Mi" + cpu: "0.25" + limits: + memory: "512Mi" + cpu: "0.25" + autoscaling: + enable: true + minReplica: 1 + maxReplica: 3 + pollingInterval: 10 # seconds + cooldownPeriod: 50 # seconds + triggers: + - type: cpu + metadata: + # Required + type: Utilization # Allowed types are 'Utilization' or 'AverageValue' + value: "75" + envConfig: + WEBSITE_SITE_NAME: 'print-payment-notice-functions' # required to show cloud role name in application insights + ENV: 'azure-uat' + APP_LOGGING_LEVEL: 'DEBUG' + DEFAULT_LOGGING_LEVEL: 'INFO' + OTEL_SERVICE_NAME: 'print-payment-notice-functions' + OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=uat" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.elastic-system.svc:4317" + OTEL_TRACES_EXPORTER: otlp + OTEL_METRICS_EXPORTER: otlp + OTEL_LOGS_EXPORTER: none + OTEL_TRACES_SAMPLER: "always_on" + NOTICE_REQUEST_MONGO_DB_NAME: "noticesMongoDb" + NOTICE_REQUEST_MONGO_COLLECTION_NAME: "payment_notice_generation_request" + BLOB_STORAGE_CONTAINER_NAME: "notices" + BLOB_STORAGE_ACCOUNT_ENDPOINT: "https://pagopaprintitnotices.blob.core.windows.net" + envSecret: + # required + APPLICATIONINSIGHTS_CONNECTION_STRING: 'app-insight-connection-string' + OTEL_EXPORTER_OTLP_HEADERS: 'elastic-apm-secret-token' + NOTICE_EVENTHUB_CONN_STRING: 'ehub-u-notice-complete-connection-string' + NOTICE_ERR_EVENTHUB_CONN_STRING: 'ehub-u-notice-errors-connection-string' + NOTICE_REQUEST_MONGODB_CONN_STRING: 'notices-mongo-connection-string' + NOTICE_STORAGE_CONN_STRING: 'notices-storage-account-connection-string' + AES_SECRET_KEY: 'aes-key' + AES_SALT: 'aes-salt' + keyvault: + name: "pagopa-u-itn-printit-kv" + tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" + nodeSelector: {} + tolerations: [] + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node_type + operator: In + values: + - user + canaryDelivery: + create: false + ingress: + create: true + canary: + type: header + headerName: X-Canary + headerValue: canary + weightPercent: 0 + service: + create: true + deployment: + create: true + image: + repository: ghcr.io/pagopa/pagopa-print-payment-notice-functions + tag: "0.0.0" + pullPolicy: Always + envConfig: {} + envSecret: {} diff --git a/host.json b/host.json new file mode 100644 index 0000000..82b69e1 --- /dev/null +++ b/host.json @@ -0,0 +1,36 @@ +{ + "version": "2.0", + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[2.*, 3.0.0)" + }, + "extensions": { + "http": { + "routePrefix": "" + }, + "queues": { + "maxPollingInterval": "00:00:02", + "visibilityTimeout": "00:00:30", + "batchSize": 32, + "maxDequeueCount": 5, + "messageEncoding": "none" + } + }, + "logging": { + "fileLoggingMode": "always", + "logLevel": { + "default": "Information", + "Host.Results": "Error", + "Function": "Information", + "Host.Aggregator": "Trace" + }, + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "maxTelemetryItemsPerSecond": 5, + "includedTypes": "PageView;Trace;Dependency;Request", + "excludedTypes": "Exception;Event;CustomEvent" + } + } + } +} diff --git a/integration-test/run_integration_test.sh b/integration-test/run_integration_test.sh new file mode 100644 index 0000000..750b584 --- /dev/null +++ b/integration-test/run_integration_test.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# example: sh ./run_integration_test.sh +set -e + +# run integration tests +cd ./src || exit +yarn install +yarn test:"$1" \ No newline at end of file diff --git a/integration-test/src/README.md b/integration-test/src/README.md new file mode 100644 index 0000000..03de5b3 --- /dev/null +++ b/integration-test/src/README.md @@ -0,0 +1,17 @@ +# Integration Tests +👀 Integration tests are in `integration-test/src/` folder. See there for more information. + +## How run on Docker đŸŗ + +To run the integration tests on docker, you can run from this directory the script: + + +``` shell +sh ./run_integration_test.sh +``` + +--- +đŸ’ģ If you want to test your local branch, +``` shell +sh ./run_integration_test.sh local +``` \ No newline at end of file diff --git a/integration-test/src/config/.env.dev b/integration-test/src/config/.env.dev new file mode 100644 index 0000000..0e46d2a --- /dev/null +++ b/integration-test/src/config/.env.dev @@ -0,0 +1,12 @@ +RECEIPTS_COSMOS_CONN_STRING= +RECEIPT_COSMOS_DB_NAME=db +RECEIPT_COSMOS_DB_CONTAINER_NAME=receipts +RECEIPT_MESSAGE_ERRORS_COSMOS_DB_CONTAINER_NAME=receipts-message-errors + +RECEIPTS_STORAGE_CONN_STRING= +BLOB_STORAGE_CONTAINER_NAME='pagopa-d-weu-receipts-azure-blob-receipt-st-attach' +RECEIPT_QUEUE_NAME=pagopa-d-weu-receipts-queue-receipt-waiting-4-gen +POISON_QUEUE_NAME=pagopa-d-weu-receipts-queue-receipt-waiting-4-gen-poison + +AES_SALT= +AES_SECRET_KEY= \ No newline at end of file diff --git a/integration-test/src/config/.env.local b/integration-test/src/config/.env.local new file mode 100644 index 0000000..0e46d2a --- /dev/null +++ b/integration-test/src/config/.env.local @@ -0,0 +1,12 @@ +RECEIPTS_COSMOS_CONN_STRING= +RECEIPT_COSMOS_DB_NAME=db +RECEIPT_COSMOS_DB_CONTAINER_NAME=receipts +RECEIPT_MESSAGE_ERRORS_COSMOS_DB_CONTAINER_NAME=receipts-message-errors + +RECEIPTS_STORAGE_CONN_STRING= +BLOB_STORAGE_CONTAINER_NAME='pagopa-d-weu-receipts-azure-blob-receipt-st-attach' +RECEIPT_QUEUE_NAME=pagopa-d-weu-receipts-queue-receipt-waiting-4-gen +POISON_QUEUE_NAME=pagopa-d-weu-receipts-queue-receipt-waiting-4-gen-poison + +AES_SALT= +AES_SECRET_KEY= \ No newline at end of file diff --git a/integration-test/src/config/.env.uat b/integration-test/src/config/.env.uat new file mode 100644 index 0000000..b7ba8b6 --- /dev/null +++ b/integration-test/src/config/.env.uat @@ -0,0 +1,12 @@ +RECEIPTS_COSMOS_CONN_STRING= +RECEIPT_COSMOS_DB_NAME=db +RECEIPT_COSMOS_DB_CONTAINER_NAME=receipts +RECEIPT_MESSAGE_ERRORS_COSMOS_DB_CONTAINER_NAME=receipts-message-errors + +RECEIPTS_STORAGE_CONN_STRING= +BLOB_STORAGE_CONTAINER_NAME='pagopa-u-weu-receipts-azure-blob-receipt-st-attach' +RECEIPT_QUEUE_NAME=pagopa-u-weu-receipts-queue-receipt-waiting-4-gen +POISON_QUEUE_NAME=pagopa-u-weu-receipts-queue-receipt-waiting-4-gen-poison + +AES_SALT= +AES_SECRET_KEY= \ No newline at end of file diff --git a/integration-test/src/config/properties.json b/integration-test/src/config/properties.json new file mode 100644 index 0000000..e69de29 diff --git a/integration-test/src/package.json b/integration-test/src/package.json new file mode 100644 index 0000000..e2f9742 --- /dev/null +++ b/integration-test/src/package.json @@ -0,0 +1,21 @@ +{ + "name": "pagopa-receipt-pdf-generator", + "license": "MIT", + "version": "0.0.1", + "scripts": { + "test": "dotenv -e ./config/.env.local yarn cucumber", + "test:local": "dotenv -e ./config/.env.local yarn cucumber", + "test:dev": "dotenv -e ./config/.env.dev yarn cucumber", + "test:uat": "dotenv -e ./config/.env.uat yarn cucumber", + "cucumber": "npx cucumber-js --publish -r step_definitions" + }, + "devDependencies": { + "@azure/cosmos": "^3.17.3", + "@azure/storage-blob": "^12.14.0", + "@azure/storage-queue": "^12.13.0", + "@cucumber/cucumber": "^9.1.2", + "dotenv": "^16.1.4", + "dotenv-cli": "^7.2.1", + "npx": "^10.2.2" + } +} diff --git a/local.settings.json.example b/local.settings.json.example new file mode 100644 index 0000000..0a1cd2a --- /dev/null +++ b/local.settings.json.example @@ -0,0 +1,6 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "java", + } +} diff --git a/performance-test/README.md b/performance-test/README.md new file mode 100644 index 0000000..1a7c058 --- /dev/null +++ b/performance-test/README.md @@ -0,0 +1,14 @@ +# K6 tests + +This is a set of [k6](https://k6.io) tests. + +To invoke k6 tests use `run_performance_test.sh` script. + + +## How to run 🚀 + +Use this command to launch the tests: + +``` shell +sh run_performance_test.sh +``` diff --git a/performance-test/docker-compose.yaml b/performance-test/docker-compose.yaml new file mode 100644 index 0000000..e058ce0 --- /dev/null +++ b/performance-test/docker-compose.yaml @@ -0,0 +1,27 @@ +version: '3.3' +services: + k6: + image: grafana/k6 + container_name: k6 + volumes: + - '${PWD}/src:/scripts' + environment: + - PROCESS_TIME=${process_time} + - VARS=${env}.environment.json + - TEST_TYPE=/scripts/test-types/${type}.json + - K6_OUT=influxdb=http://nginx:8086/${db_name} + command: run /scripts/${script}.js + depends_on: + - nginx + + nginx: + image: nginx + container_name: nginx + volumes: + - '${PWD}/nginx/nginx.conf:/etc/nginx/nginx.conf' + environment: + - ENVIRONMENT=${env} + ports: + - "8086:8086" + - "80:80" + diff --git a/performance-test/nginx/nginx.conf b/performance-test/nginx/nginx.conf new file mode 100644 index 0000000..a9cc127 --- /dev/null +++ b/performance-test/nginx/nginx.conf @@ -0,0 +1,16 @@ +events { + worker_connections 1024; +} + +http { + server { + listen 8086; + location / { + proxy_pass https://api.uat.platform.pagopa.it/shared/influxdb/v1/; + proxy_http_version 1.1; + proxy_set_header Host api.uat.platform.pagopa.it; + proxy_pass_request_headers on; + } + } +} + diff --git a/performance-test/run_performance_test.sh b/performance-test/run_performance_test.sh new file mode 100644 index 0000000..dc96940 --- /dev/null +++ b/performance-test/run_performance_test.sh @@ -0,0 +1,39 @@ +# sh run_performance_test.sh + +ENVIRONMENT=$1 +TYPE=$2 +SCRIPT=$3 +DB_NAME=$4 +RECEIPT_QUEUE_SUBSCRIPTION_KEY=$5 +RECEIPT_COSMOS_DB_SUBSCRIPTION_KEY=$6 +PROCESS_TIME=$7 + +if [ -z "$ENVIRONMENT" ] +then + echo "No env specified: sh run_performance_test.sh " + exit 1 +fi + +if [ -z "$TYPE" ] +then + echo "No test type specified: sh run_performance_test.sh " + exit 1 +fi +if [ -z "$SCRIPT" ] +then + echo "No script name specified: sh run_performance_test.sh " + exit 1 +fi + +export env=${ENVIRONMENT} +export type=${TYPE} +export script=${SCRIPT} +export db_name=${DB_NAME} +export receipts_queue_key=${RECEIPT_QUEUE_SUBSCRIPTION_KEY} +export receipts_cosmos_key=${RECEIPT_COSMOS_DB_SUBSCRIPTION_KEY} +export process_time=${PROCESS_TIME} + +stack_name=$(cd .. && basename "$PWD") +docker compose -p "${stack_name}-k6" up -d --remove-orphans --force-recreate --build +docker logs -f k6 +docker stop nginx diff --git a/performance-test/src/README.md b/performance-test/src/README.md new file mode 100644 index 0000000..68b2fa7 --- /dev/null +++ b/performance-test/src/README.md @@ -0,0 +1,49 @@ +# K6 tests for _ReceiptsToDatastore_ project + +[k6](https://k6.io/) is a load testing tool. 👀 See [here](https://k6.io/docs/get-started/installation/) to install it. + +- [01. Receipt datastore function](#01-receipt-datastore-function) + +This is a set of [k6](https://k6.io) tests related to the _Biz Events to Datastore_ initiative. + +To invoke k6 test passing parameter use -e (or --env) flag: + +``` +-e MY_VARIABLE=MY_VALUE +``` + +## 01. Receipt datastore function + +Test the receipt datastore function: + +``` +k6 run --env VARS=local.environment.json --env TEST_TYPE=./test-types/load.json --env RECEIPT_QUEUE_SUBSCRIPTION_KEY= --env RECEIPT_COSMOS_DB_SUBSCRIPTION_KEY= receipt_generator.js +``` + +where the mean of the environment variables is: + +```json + "environment": [ + { + "env": "local", + "receiptCosmosDBURI": "", + "receiptDatabaseID":"", + "receiptContainerID":"", + "receiptQueueAccountName": "", + "receiptQueueName": "", + "processTime":"" + } + ] +``` + +`receiptCosmosDBURI`: CosmosDB url to access Receipts CosmosDB REST API + +`receiptDatabaseID`: database name to access Receipts Cosmos DB REST API + +`receiptContainerID`: collection name to access Receipts Cosmos DB REST API + +`receiptQueueAccountName`: storage account name to access Receipts Queue + +`receiptQueueName`: queue name for the Receipts, + +`processTime`: boundary time taken by azure function to fetch the payment event and save it in the datastore \ No newline at end of file diff --git a/performance-test/src/dev.environment.json b/performance-test/src/dev.environment.json new file mode 100644 index 0000000..d120e91 --- /dev/null +++ b/performance-test/src/dev.environment.json @@ -0,0 +1,7 @@ +{ + "environment": [ + { + "env": "dev" + } + ] +} diff --git a/performance-test/src/local.environment.json b/performance-test/src/local.environment.json new file mode 100644 index 0000000..75b9187 --- /dev/null +++ b/performance-test/src/local.environment.json @@ -0,0 +1,7 @@ +{ + "environment": [ + { + "env": "local" + } + ] +} diff --git a/performance-test/src/test-types/constant.json b/performance-test/src/test-types/constant.json new file mode 100644 index 0000000..156206b --- /dev/null +++ b/performance-test/src/test-types/constant.json @@ -0,0 +1,50 @@ +{ + "discardResponseBodies": true, + "summaryTrendStats": [ + "avg", + "min", + "med", + "max", + "p(95)", + "p(99)", + "p(99.99)", + "count" + ], + "scenarios": { + "contacts": { + "executor": "constant-arrival-rate", + "duration": "3m", + "rate": 50, + "timeUnit": "1s", + "preAllocatedVUs": 20, + "maxVUs": 50 + } + }, + "thresholds": { + "http_req_failed": [ + "rate<0.1" + ], + "http_req_duration": [ + "p(99)<2000" + ], + "http_req_duration{group:::setup}": [ + "max>=0" + ], + "http_req_duration{scenario:contacts}": [ + "max>=0" + ], + "iteration_duration{scenario:contacts}": [ + "max>=0" + ], + "iteration_duration{group:::setup}": [ + "max>=0" + ], + "iterations{group:::setup}": [ + "rate>=0" + ], + "iterations{scenario:contacts}": [ + "rate>=0" + ] + }, + "setupTimeout": "60m" + } \ No newline at end of file diff --git a/performance-test/src/test-types/load.json b/performance-test/src/test-types/load.json new file mode 100644 index 0000000..1525a1a --- /dev/null +++ b/performance-test/src/test-types/load.json @@ -0,0 +1,34 @@ +{ + "summaryTrendStats": [ + "avg", + "min", + "med", + "max", + "p(95)", + "p(99)", + "p(99.99)", + "count" + ], + "stages": [ + { + "duration": "1m", + "target": 20 + }, + { + "duration": "2m", + "target": 20 + }, + { + "duration": "2m", + "target": 0 + } + ], + "thresholds": { + "http_req_failed": [ + "rate<0.001" + ], + "http_req_duration": [ + "p(99)<1500" + ] + } +} diff --git a/performance-test/src/test-types/spike.json b/performance-test/src/test-types/spike.json new file mode 100644 index 0000000..efbf1d4 --- /dev/null +++ b/performance-test/src/test-types/spike.json @@ -0,0 +1,38 @@ +{ + "summaryTrendStats": [ + "avg", + "min", + "med", + "max", + "p(95)", + "p(99)", + "p(99.99)", + "count" + ], + "stages": [ + { + "duration": "10s", + "target": 1 + }, + { + "duration": "20s", + "target": 5 + }, + { + "duration": "10s", + "target": 1 + }, + { + "duration": "10s", + "target": 0 + } + ], + "thresholds": { + "http_req_failed": [ + "rate<0.001" + ], + "http_req_duration": [ + "p(99)<100" + ] + } +} diff --git a/performance-test/src/test-types/stress.json b/performance-test/src/test-types/stress.json new file mode 100644 index 0000000..f9cf5d5 --- /dev/null +++ b/performance-test/src/test-types/stress.json @@ -0,0 +1,61 @@ +{ + "summaryTrendStats": [ + "avg", + "min", + "med", + "max", + "p(95)", + "p(99)", + "p(99.99)", + "count" + ], + "stages": [ + { + "duration": "2m", + "target": 100 + }, + { + "duration": "5m", + "target": 100 + }, + { + "duration": "2m", + "target": 200 + }, + { + "duration": "5m", + "target": 200 + }, + { + "duration": "2m", + "target": 300 + }, + { + "duration": "5m", + "target": 300 + }, + { + "duration": "2m", + "target": 400 + }, + { + "duration": "5m", + "target": 400 + }, + { + "duration": "10m", + "target": 0 + } + ], + "thresholds": { + "http_req_failed": [ + "rate<0.001" + ], + "http_req_duration": [ + "p(99)<5000" + ], + "http_req_duration{bizEventMethod:GetOrganizationReceipt}": [ + "p(95)<1500" + ] + } +} diff --git a/performance-test/src/uat.environment.json b/performance-test/src/uat.environment.json new file mode 100644 index 0000000..de8ae85 --- /dev/null +++ b/performance-test/src/uat.environment.json @@ -0,0 +1,7 @@ +{ + "environment": [ + { + "env": "uat" + } + ] +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..df32e6c --- /dev/null +++ b/pom.xml @@ -0,0 +1,364 @@ + + + 4.0.0 + + it.gov.pagopa + print-payment-notice-functions + 0.0.1 + jar + + pagopa-print-payment-notice-functions + + + UTF-8 + 17 + 1.15.0 + 1.4.2 + com.microsoft.azure-20220215182005862 + 3.15.3.Final + + + + + + + org.modelmapper + modelmapper + 3.0.0 + + + com.microsoft.azure.functions + azure-functions-java-library + ${azure.functions.java.library.version} + + + + com.azure + azure-storage-blob + 12.22.2 + + + + org.mongodb + mongodb-driver-sync + 4.11.2 + + + + + + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + org.apache.httpcomponents + httpmime + 4.5.14 + + + + + + + org.junit.jupiter + junit-jupiter + 5.8.2 + test + + + + uk.org.webcompere + system-stubs-jupiter + 2.1.3 + test + + + + org.mockito + mockito-core + 5.4.0 + test + + + org.mockito + mockito-junit-jupiter + 5.4.0 + test + + + + + com.sun.xml.ws + jaxws-ri + 2.3.3 + pom + + + + javax.xml.ws + jaxws-api + 2.3.1 + + + + com.sun.xml.bind + jaxb-core + 2.3.0.1 + + + + com.sun.xml.bind + jaxb-impl + 2.3.1 + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + + + org.jboss.resteasy + resteasy-client + ${resteasy.version} + + + + org.jboss.resteasy + resteasy-jackson2-provider + ${resteasy.version} + + + + org.jboss.resteasy + resteasy-jaxb-provider + 6.0.0.Final + + + + + + + + com.fasterxml.jackson.core + jackson-core + 2.13.1 + + + + com.fasterxml.jackson.core + jackson-databind + 2.13.4.2 + + + + com.fasterxml.jackson.core + jackson-annotations + 2.13.1 + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + 2.13.1 + + + + + + + org.projectlombok + lombok + 1.18.20 + provided + + + + com.azure + azure-storage-queue + 12.17.1 + + + + com.google.api-client + google-api-client + 1.32.1 + + + + ch.qos.logback + logback-classic + 1.4.12 + + + + org.slf4j + slf4j-api + 2.0.9 + + + + co.elastic.logging + logback-ecs-encoder + 1.5.0 + + + + + + org.codehaus.janino + janino + 2.6.1 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + com.microsoft.azure + azure-functions-maven-plugin + ${azure.functions.maven.plugin.version} + + + ${functionAppName} + + + FUNCTIONS_EXTENSION_VERSION + ~3 + + + java-functions-group + java-functions-app-service-plan + westus + + windows + 11 + + + + + package-functions + + package + + + + + + + maven-clean-plugin + 3.1.0 + + + + obj + + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.7 + + + **/config/* + + + + + + prepare-agent + + + + report + prepare-package + + report + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.19.1 + + + + + org.junit.platform + junit-platform-surefire-provider + 1.0.1 + + + org.junit.jupiter + junit-jupiter-engine + 5.0.3 + + + + + ${argLine} + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.2.0 + + + add-source + generate-sources + + add-source + + + + target/generated + + + + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.9.0 + + + + + + + src/main/resources-filtered + true + + + src/main/resources + + + + diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/Health.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/Health.java new file mode 100644 index 0000000..bd5f13a --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/Health.java @@ -0,0 +1,32 @@ +package it.gov.pagopa.print.payment.notice.functions; + +import com.microsoft.azure.functions.*; +import com.microsoft.azure.functions.annotation.AuthorizationLevel; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.annotation.HttpTrigger; + +import java.util.Optional; + + +/** + * Azure Functions with Azure Http trigger. + */ +public class Health { + + /** + * This function will be invoked when a Http Trigger occurs + * + * @return response with HttpStatus.OK + */ + @FunctionName("Health") + public HttpResponseMessage run ( + @HttpTrigger(name = "HealthTrigger", + methods = {HttpMethod.GET}, + route = "health", + authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, + final ExecutionContext context) { + + return request.createResponseBuilder(HttpStatus.OK) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/Info.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/Info.java new file mode 100644 index 0000000..b33df37 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/Info.java @@ -0,0 +1,59 @@ +package it.gov.pagopa.print.payment.notice.functions; + +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.HttpMethod; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.HttpResponseMessage; +import com.microsoft.azure.functions.HttpStatus; +import com.microsoft.azure.functions.annotation.AuthorizationLevel; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.annotation.HttpTrigger; +import it.gov.pagopa.print.payment.notice.functions.model.AppInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.util.Optional; +import java.util.Properties; + + +/** + * Azure Functions with Azure Http trigger. + */ +public class Info { + + private final Logger logger = LoggerFactory.getLogger(Info.class); + + /** + * This function will be invoked when a Http Trigger occurs + * + * @return response with HttpStatus.OK + */ + @FunctionName("Info") + public HttpResponseMessage run ( + @HttpTrigger(name = "InfoTrigger", + methods = {HttpMethod.GET}, + route = "info", + authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, + final ExecutionContext context) { + + return request.createResponseBuilder(HttpStatus.OK) + .body(getInfo()) + .build(); + } + public AppInfo getInfo() { + String version = null; + String name = null; + try (InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("application.properties")) { + Properties properties = new Properties(); + if (inputStream != null) { + properties.load(inputStream); + version = properties.getProperty("version", null); + name = properties.getProperty("name", null); + } + } catch (Exception e) { + logger.error("Impossible to retrieve information from pom.properties file.", e); + } + return AppInfo.builder().version(version).environment("azure-fn").name(name).build(); + } +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/ManagePaymentNoticeFolderUpdates.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/ManagePaymentNoticeFolderUpdates.java new file mode 100644 index 0000000..ca1eefc --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/ManagePaymentNoticeFolderUpdates.java @@ -0,0 +1,87 @@ +package it.gov.pagopa.print.payment.notice.functions; + +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.annotation.*; +import it.gov.pagopa.print.payment.notice.functions.entity.PaymentGenerationRequestStatus; +import it.gov.pagopa.print.payment.notice.functions.entity.PaymentNoticeGenerationRequest; +import it.gov.pagopa.print.payment.notice.functions.entity.PaymentNoticeGenerationRequestError; +import it.gov.pagopa.print.payment.notice.functions.service.NoticeFolderService; +import it.gov.pagopa.print.payment.notice.functions.service.impl.NoticeFolderServiceImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Azure Functions with Azure Queue trigger. + */ +public class ManagePaymentNoticeFolderUpdates { + + private final Logger logger = LoggerFactory.getLogger(ManagePaymentNoticeFolderUpdates.class); + + private final NoticeFolderService noticeFolderService; + + public ManagePaymentNoticeFolderUpdates() { + this.noticeFolderService = new NoticeFolderServiceImpl(); + } + + ManagePaymentNoticeFolderUpdates(NoticeFolderService noticeFolderService) { + this.noticeFolderService = noticeFolderService; + } + + /** + * This function will be invoked when a Queue trigger occurs + * + * The function handles requests coming through the provided EH channel, + * whenever a request is sent in status 'COMPLETING' it will check if the + * number of elements are considered to be processed + * + * The function will attempt to retrieve the folder notices, compressing and + * saving on the folder within the blob storage + * + * If the folder is successfully compressed the status will be saved + * as PROCESSED, or PROCESSED_WITH_FAILURES if the folder is a partial + * + * In case of errors a new element will be sent on the error channel + */ + @FunctionName("ManagePaymentNoticeFolderUpdatesProcess") + public void processGenerateReceipt( + @EventHubTrigger( + name = "PaymentNoticeRequest", + eventHubName = "", // blank because the value is included in the connection string + connection = "NOTICE_EVENTHUB_CONN_STRING", + cardinality = Cardinality.MANY) + List requestMsg, + @BindingName(value = "PropertiesArray") Map[] properties, + @EventHubOutput( + name = "PaymentNoticeErrors", + eventHubName = "", // blank because the value is included in the connection string + connection = "NOTICE_ERR_EVENTHUB_CONN_STRING") + List errors, + final ExecutionContext context) { + + requestMsg.stream().filter(item -> ( + Objects.equals( + item.getNumberOfElementsProcessed() + item.getNumberOfElementsFailed(), + item.getNumberOfElementsTotal()) && + PaymentGenerationRequestStatus.COMPLETING.equals(item.getStatus())) + ) + .forEach(item -> { + try { + noticeFolderService.manageFolder(item); + } catch (Exception e) { + logger.error("[{}] error managing notice rewuest with id {}", + context.getFunctionName(), item.getId(), e); + errors.add(PaymentNoticeGenerationRequestError.builder() + .folderId(item.getId()) + .numberOfAttempts(0) + .compressionError(true) + .build()); + } + }); + + } + +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/client/PaymentNoticeBlobClient.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/client/PaymentNoticeBlobClient.java new file mode 100644 index 0000000..b61df89 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/client/PaymentNoticeBlobClient.java @@ -0,0 +1,11 @@ +package it.gov.pagopa.print.payment.notice.functions.client; + +import it.gov.pagopa.print.payment.notice.functions.model.response.BlobStorageResponse; + +import java.io.InputStream; + +public interface PaymentNoticeBlobClient { + + BlobStorageResponse compressFolder(String folderId); + +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/client/PaymentNoticeGenerationRequestClient.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/client/PaymentNoticeGenerationRequestClient.java new file mode 100644 index 0000000..2ebed55 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/client/PaymentNoticeGenerationRequestClient.java @@ -0,0 +1,7 @@ +package it.gov.pagopa.print.payment.notice.functions.client; + +import it.gov.pagopa.print.payment.notice.functions.entity.PaymentNoticeGenerationRequest; + +public interface PaymentNoticeGenerationRequestClient { + void updatePaymentGenerationRequest(PaymentNoticeGenerationRequest paymentNoticeGenerationRequest); +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/client/impl/PaymentNoticeBlobClientImpl.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/client/impl/PaymentNoticeBlobClientImpl.java new file mode 100644 index 0000000..bd59911 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/client/impl/PaymentNoticeBlobClientImpl.java @@ -0,0 +1,117 @@ +package it.gov.pagopa.print.payment.notice.functions.client.impl; + +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.blob.models.ListBlobsOptions; +import com.microsoft.azure.functions.HttpStatus; +import it.gov.pagopa.print.payment.notice.functions.client.PaymentNoticeBlobClient; +import it.gov.pagopa.print.payment.notice.functions.model.response.BlobStorageResponse; +import lombok.SneakyThrows; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * Client for the Blob Storage regarding notices + */ +public class PaymentNoticeBlobClientImpl implements PaymentNoticeBlobClient { + + private static PaymentNoticeBlobClientImpl instance; + + private final String containerName = System.getenv("BLOB_STORAGE_CONTAINER_NAME"); + + private final BlobServiceClient blobServiceClient; + + private PaymentNoticeBlobClientImpl() { + String connectionString = System.getenv("BLOB_STORAGE_CONN_STRING"); + String storageAccount = System.getenv("BLOB_STORAGE_ACCOUNT_ENDPOINT"); + + this.blobServiceClient = new BlobServiceClientBuilder() + .endpoint(storageAccount) + .connectionString(connectionString) + .buildClient(); + } + + PaymentNoticeBlobClientImpl(BlobServiceClient serviceClient) { + this.blobServiceClient = serviceClient; + } + + public static PaymentNoticeBlobClientImpl getInstance() { + if (instance == null) { + instance = new PaymentNoticeBlobClientImpl(); + } + + return instance; + } + + /** + * Using the provided id it will attempt to recover the folder data, + * compressing + * @param folderId + * @return + */ + @SneakyThrows + public BlobStorageResponse compressFolder(String folderId) { + + BlobContainerClient blobContainerClient = blobServiceClient.getBlobContainerClient(containerName); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + try (ZipOutputStream zipStream = new ZipOutputStream(outputStream)) { + List> futures = new ArrayList<>(); + + String delimiter = "/"; + ListBlobsOptions options = new ListBlobsOptions() + .setPrefix(folderId); + + + blobContainerClient.listBlobsByHierarchy(delimiter, options, null) + .stream().forEach(blobItem -> { + final BlobClient blobClient = blobContainerClient.getBlobClient(blobItem.getName()); + + final String[] splitName = blobItem.getName().split(delimiter,2); + final String finalSingleFileName = splitName.length > 1 ? splitName[1] : splitName[0]; + final String finalSingleFilepath = blobItem.getName(); + + CompletableFuture future = CompletableFuture.runAsync(() -> { + try (ByteArrayOutputStream fileOutputStream = new ByteArrayOutputStream()) { + if (blobClient.exists()) { + blobClient.downloadStream(fileOutputStream); + zipStream.putNextEntry(new ZipEntry(finalSingleFileName)); + zipStream.write(fileOutputStream.toByteArray()); + zipStream.closeEntry(); + } else { + throw new RuntimeException("File not found: " + finalSingleFilepath); + } + } catch (IOException e) { + throw new RuntimeException("Error processing file: " + finalSingleFileName, e); + } + }); + + futures.add(future); + }); + + CompletableFuture allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + allOf.join(); + } catch (IOException e) { + throw new RuntimeException("Error creating zip file", e); + } + + BlobClient zipFileClient = blobContainerClient.getBlobClient( + folderId + "/" + folderId.concat(".zip")); + zipFileClient.upload(new ByteArrayInputStream( + outputStream.toByteArray()), outputStream.size(), true); + + BlobStorageResponse blobStorageResponse = new BlobStorageResponse(); + blobStorageResponse.setStatusCode(HttpStatus.OK.value()); + return blobStorageResponse; + } +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/client/impl/PaymentNoticeGenerationRequestClientImpl.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/client/impl/PaymentNoticeGenerationRequestClientImpl.java new file mode 100644 index 0000000..8d00a85 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/client/impl/PaymentNoticeGenerationRequestClientImpl.java @@ -0,0 +1,59 @@ +package it.gov.pagopa.print.payment.notice.functions.client.impl; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.Updates; +import it.gov.pagopa.print.payment.notice.functions.client.PaymentNoticeGenerationRequestClient; +import it.gov.pagopa.print.payment.notice.functions.entity.PaymentNoticeGenerationRequest; +import org.bson.codecs.configuration.CodecProvider; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.codecs.pojo.PojoCodecProvider; + +import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry; +import static org.bson.codecs.configuration.CodecRegistries.fromProviders; +import static org.bson.codecs.configuration.CodecRegistries.fromRegistries; + +public class PaymentNoticeGenerationRequestClientImpl implements PaymentNoticeGenerationRequestClient { + + private static PaymentNoticeGenerationRequestClientImpl instance; + + private final MongoCollection mongoCollection; + + private PaymentNoticeGenerationRequestClientImpl() { + String connectionString = System.getenv("NOTICE_REQUEST_MONGODB_CONN_STRING"); + String databaseName = System.getenv("NOTICE_REQUEST_MONGO_DB_NAME"); + String collectionName = System.getenv("NOTICE_REQUEST_MONGO_COLLECTION_NAME"); + + + CodecProvider pojoCodecProvider = PojoCodecProvider.builder().automatic(true).build(); + CodecRegistry pojoCodecRegistry = fromRegistries(getDefaultCodecRegistry(), + fromProviders(pojoCodecProvider)); + MongoClient mongoClient = MongoClients.create(connectionString); + MongoDatabase database = mongoClient.getDatabase(databaseName) + .withCodecRegistry(pojoCodecRegistry); + mongoCollection = database.getCollection(collectionName, PaymentNoticeGenerationRequest.class); + + } + + PaymentNoticeGenerationRequestClientImpl(MongoCollection mongoCollection) { + this.mongoCollection = mongoCollection; + } + + public static PaymentNoticeGenerationRequestClientImpl getInstance() { + if (instance == null) { + instance = new PaymentNoticeGenerationRequestClientImpl(); + } + + return instance; + } + + @Override + public void updatePaymentGenerationRequest(PaymentNoticeGenerationRequest paymentNoticeGenerationRequest) { + mongoCollection.updateOne(Filters.eq("_id", paymentNoticeGenerationRequest.getId()), + Updates.set("status", paymentNoticeGenerationRequest.getStatus().name())); + } + +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/entity/PaymentGenerationRequestStatus.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/entity/PaymentGenerationRequestStatus.java new file mode 100644 index 0000000..9ca44b8 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/entity/PaymentGenerationRequestStatus.java @@ -0,0 +1,15 @@ +package it.gov.pagopa.print.payment.notice.functions.entity; + +/** + * Enum containing generation request status + */ +public enum PaymentGenerationRequestStatus { + + INSERTED, + PROCESSING, + COMPLETING, + FAILED, + PROCESSED, + PROCESSED_WITH_FAILURES + +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/entity/PaymentNoticeGenerationRequest.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/entity/PaymentNoticeGenerationRequest.java new file mode 100644 index 0000000..b365970 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/entity/PaymentNoticeGenerationRequest.java @@ -0,0 +1,34 @@ +package it.gov.pagopa.print.payment.notice.functions.entity; + +import lombok.*; + +import java.time.Instant; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(of = "id") +@ToString +public class PaymentNoticeGenerationRequest { + + private String id; + + private String userId; + + private Instant createdAt; + + private Instant requestDate; + + private PaymentGenerationRequestStatus status; + + private List items; + + private Integer numberOfElementsProcessed; + + private Integer numberOfElementsFailed; + + private Integer numberOfElementsTotal; + +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/entity/PaymentNoticeGenerationRequestError.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/entity/PaymentNoticeGenerationRequestError.java new file mode 100644 index 0000000..df514af --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/entity/PaymentNoticeGenerationRequestError.java @@ -0,0 +1,31 @@ +package it.gov.pagopa.print.payment.notice.functions.entity; + +import lombok.*; + +import java.time.Instant; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(of = "folderId") +@ToString +public class PaymentNoticeGenerationRequestError { + + private String id; + + private String folderId; + + private Instant createdAt; + + private String errorCode; + + private String errorDescription; + + private String data; + + private Integer numberOfAttempts; + + private boolean compressionError; + +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/exception/Aes256Exception.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/exception/Aes256Exception.java new file mode 100644 index 0000000..d1cdc17 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/exception/Aes256Exception.java @@ -0,0 +1,35 @@ +package it.gov.pagopa.print.payment.notice.functions.exception; + +import lombok.Getter; + +/** + * Thrown in case an error occur when encrypting or decrypting a BizEvent + */ +@Getter +public class Aes256Exception extends Exception{ + private final int statusCode; + + /** + * Constructs new exception with provided message + * + * @param message Detail message + * @param statusCode status code + */ + public Aes256Exception(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + /** + * Constructs new exception with provided message + * + * @param message Detail message + * @param statusCode status code + * @param cause Exception causing the constructed one + */ + public Aes256Exception(String message, int statusCode, Throwable cause) { + super(message, cause); + this.statusCode = statusCode; + } + +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/exception/PaymentNoticeManagementException.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/exception/PaymentNoticeManagementException.java new file mode 100644 index 0000000..55883d4 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/exception/PaymentNoticeManagementException.java @@ -0,0 +1,34 @@ +package it.gov.pagopa.print.payment.notice.functions.exception; + +/** Thrown in case an error occurred when manating notice data */ +public class PaymentNoticeManagementException extends Exception { + + private final int statusCode; + + /** + * Constructs new exception with provided message, status code and cause + * + * @param message Detail message + * @param statusCode Error code + * @param cause Exception thrown + */ + public PaymentNoticeManagementException(String message, int statusCode, Throwable cause) { + super(message, cause); + this.statusCode = statusCode; + } + + /** + * Constructs new exception with provided message, status code + * + * @param message Detail message + * @param statusCode Error code + */ + public PaymentNoticeManagementException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/exception/SaveNoticeToBlobException.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/exception/SaveNoticeToBlobException.java new file mode 100644 index 0000000..a2b5856 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/exception/SaveNoticeToBlobException.java @@ -0,0 +1,26 @@ +package it.gov.pagopa.print.payment.notice.functions.exception; + +/** Thrown in case an error occurred when saving a PDF Receipt to the Blob storage */ +public class SaveNoticeToBlobException extends PaymentNoticeManagementException { + + /** + * Constructs new exception with provided message, status code and cause + * + * @param message Detail message + * @param statusCode Error code + * @param cause Exception thrown + */ + public SaveNoticeToBlobException(String message, int statusCode, Throwable cause) { + super(message, statusCode, cause); + } + + /** + * Constructs new exception with provided message, status code + * + * @param message Detail message + * @param statusCode Error code + */ + public SaveNoticeToBlobException(String message, int statusCode) { + super(message, statusCode); + } +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/exception/UpdatePaymentNoticeEntityException.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/exception/UpdatePaymentNoticeEntityException.java new file mode 100644 index 0000000..0841f87 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/exception/UpdatePaymentNoticeEntityException.java @@ -0,0 +1,26 @@ +package it.gov.pagopa.print.payment.notice.functions.exception; + +public class UpdatePaymentNoticeEntityException extends PaymentNoticeManagementException { + + /** + * Constructs new exception with provided message, status code and cause + * + * @param message Detail message + * @param statusCode Error code + * @param cause Exception thrown + */ + public UpdatePaymentNoticeEntityException(String message, int statusCode, Throwable cause) { + super(message, statusCode, cause); + } + + /** + * Constructs new exception with provided message, status code + * + * @param message Detail message + * @param statusCode Error code + */ + public UpdatePaymentNoticeEntityException(String message, int statusCode) { + super(message, statusCode); + } + +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/model/AppInfo.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/model/AppInfo.java new file mode 100644 index 0000000..4267cd2 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/model/AppInfo.java @@ -0,0 +1,18 @@ +package it.gov.pagopa.print.payment.notice.functions.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.*; + +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class AppInfo { + + private String name; + private String version; + private String environment; +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/model/response/BlobStorageResponse.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/model/response/BlobStorageResponse.java new file mode 100644 index 0000000..ff57d01 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/model/response/BlobStorageResponse.java @@ -0,0 +1,18 @@ +package it.gov.pagopa.print.payment.notice.functions.model.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * Model class for Blob Storage client's response + */ +@Getter +@Setter +@NoArgsConstructor +public class BlobStorageResponse { + + String documentUrl; + String documentName; + int statusCode; +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/service/NoticeFolderService.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/service/NoticeFolderService.java new file mode 100644 index 0000000..73d3141 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/service/NoticeFolderService.java @@ -0,0 +1,23 @@ +package it.gov.pagopa.print.payment.notice.functions.service; + + +import it.gov.pagopa.print.payment.notice.functions.entity.PaymentNoticeGenerationRequest; +import it.gov.pagopa.print.payment.notice.functions.exception.SaveNoticeToBlobException; + +import java.nio.file.Path; + +/** + * Provides services to manage the incoming requests + */ +public interface NoticeFolderService { + + /** + * Method that contains the logic for the compression function, whenever + * a valid request is provided it will attempt to save the compressed folder + * and update the status accordingly + * @param paymentNoticeGenerationRequest data to use as input for folder management + * @throws SaveNoticeToBlobException + */ + void manageFolder(PaymentNoticeGenerationRequest paymentNoticeGenerationRequest) throws SaveNoticeToBlobException; + +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/service/impl/NoticeFolderServiceImpl.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/service/impl/NoticeFolderServiceImpl.java new file mode 100644 index 0000000..26f6e0d --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/service/impl/NoticeFolderServiceImpl.java @@ -0,0 +1,63 @@ +package it.gov.pagopa.print.payment.notice.functions.service.impl; + +import com.azure.storage.blob.models.BlobStorageException; +import it.gov.pagopa.print.payment.notice.functions.client.PaymentNoticeBlobClient; +import it.gov.pagopa.print.payment.notice.functions.client.PaymentNoticeGenerationRequestClient; +import it.gov.pagopa.print.payment.notice.functions.client.impl.PaymentNoticeBlobClientImpl; +import it.gov.pagopa.print.payment.notice.functions.client.impl.PaymentNoticeGenerationRequestClientImpl; +import it.gov.pagopa.print.payment.notice.functions.entity.PaymentGenerationRequestStatus; +import it.gov.pagopa.print.payment.notice.functions.entity.PaymentNoticeGenerationRequest; +import it.gov.pagopa.print.payment.notice.functions.exception.SaveNoticeToBlobException; +import it.gov.pagopa.print.payment.notice.functions.model.response.BlobStorageResponse; +import it.gov.pagopa.print.payment.notice.functions.service.NoticeFolderService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class NoticeFolderServiceImpl implements NoticeFolderService { + + private final Logger logger = LoggerFactory.getLogger(NoticeFolderServiceImpl.class); + + private final PaymentNoticeBlobClient paymentNoticeBlobClient; + + private final PaymentNoticeGenerationRequestClient paymentNoticeGenerationRequestClient; + + public NoticeFolderServiceImpl() { + paymentNoticeBlobClient = PaymentNoticeBlobClientImpl.getInstance(); + paymentNoticeGenerationRequestClient = PaymentNoticeGenerationRequestClientImpl.getInstance(); + } + + public NoticeFolderServiceImpl(PaymentNoticeBlobClient paymentNoticeBlobClient, + PaymentNoticeGenerationRequestClient paymentNoticeGenerationRequestClient) { + this.paymentNoticeBlobClient = paymentNoticeBlobClient; + this.paymentNoticeGenerationRequestClient = paymentNoticeGenerationRequestClient; + } + + @Override + public void manageFolder(PaymentNoticeGenerationRequest paymentNoticeGenerationRequest) + throws SaveNoticeToBlobException { + + try { + BlobStorageResponse response = paymentNoticeBlobClient + .compressFolder(paymentNoticeGenerationRequest.getId()); + if (response.getStatusCode() != 200) { + throw new SaveNoticeToBlobException("Couldn't create the compressed file", + response.getStatusCode()); + } + } catch (SaveNoticeToBlobException e) { + logger.error(e.getMessage(), e); + throw e; + } + + try { + paymentNoticeGenerationRequest.setStatus(PaymentGenerationRequestStatus.PROCESSED); + paymentNoticeGenerationRequestClient + .updatePaymentGenerationRequest(paymentNoticeGenerationRequest); + } catch (Exception e) { + logger.error(e.getMessage(), e); + throw e; + } + + } + +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/utils/Aes256Utils.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/utils/Aes256Utils.java new file mode 100644 index 0000000..112e1c2 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/utils/Aes256Utils.java @@ -0,0 +1,89 @@ +package it.gov.pagopa.print.payment.notice.functions.utils; + +import it.gov.pagopa.print.payment.notice.functions.exception.Aes256Exception; + +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.security.spec.KeySpec; +import java.util.Base64; + +public class Aes256Utils { + + private static final String AES_SECRET_KEY = System.getenv().getOrDefault("AES_SECRET_KEY", ""); + private static final String AES_SALT = System.getenv().getOrDefault("AES_SALT", ""); + private static final int KEY_LENGTH = 256; + private static final int ITERATION_COUNT = 65536; + public static final String PBKDF_2_WITH_HMAC_SHA_256 = "PBKDF2WithHmacSHA256"; + public static final String AES_CBC_PKCS_5_PADDING = "AES/CBC/PKCS5Padding"; + + public static final String ALGORITHM = "AES"; + + private static final int AES_UNEXPECTED_ERROR = 701; + + + /** + * Hide from public usage. + */ + private Aes256Utils() { + } + + public static String encrypt(String strToEncrypt) throws Aes256Exception { + + try { + + SecureRandom secureRandom = new SecureRandom(); + byte[] iv = new byte[16]; + secureRandom.nextBytes(iv); + IvParameterSpec ivspec = new IvParameterSpec(iv); + + SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF_2_WITH_HMAC_SHA_256); + KeySpec spec = new PBEKeySpec(AES_SECRET_KEY.toCharArray(), AES_SALT.getBytes(), ITERATION_COUNT, KEY_LENGTH); + SecretKey tmp = factory.generateSecret(spec); + SecretKeySpec secretKeySpec = new SecretKeySpec(tmp.getEncoded(), ALGORITHM); + + //Padding vulnerability rule java:S5542 ignored because encryption is used inside application workflow + Cipher cipher = Cipher.getInstance(AES_CBC_PKCS_5_PADDING); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivspec); + + byte[] cipherText = cipher.doFinal(strToEncrypt.getBytes(StandardCharsets.UTF_8)); + byte[] encryptedData = new byte[iv.length + cipherText.length]; + System.arraycopy(iv, 0, encryptedData, 0, iv.length); + System.arraycopy(cipherText, 0, encryptedData, iv.length, cipherText.length); + + return Base64.getEncoder().encodeToString(encryptedData); + } catch (Exception e) { + throw new Aes256Exception("Unexpected error when encrypting the given string", AES_UNEXPECTED_ERROR, e); + } + } + + public static String decrypt(String strToDecrypt) throws Aes256Exception { + try{ + byte[] encryptedData = Base64.getDecoder().decode(strToDecrypt); + byte[] iv = new byte[16]; + System.arraycopy(encryptedData, 0, iv, 0, iv.length); + IvParameterSpec ivspec = new IvParameterSpec(iv); + + SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF_2_WITH_HMAC_SHA_256); + KeySpec spec = new PBEKeySpec(AES_SECRET_KEY.toCharArray(), AES_SALT.getBytes(), ITERATION_COUNT, KEY_LENGTH); + SecretKey tmp = factory.generateSecret(spec); + SecretKeySpec secretKeySpec = new SecretKeySpec(tmp.getEncoded(), ALGORITHM); + + //Padding vulnerability rule java:S5542 ignored because decryption is used inside application workflow + Cipher cipher = Cipher.getInstance(AES_CBC_PKCS_5_PADDING); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivspec); + + byte[] cipherText = new byte[encryptedData.length - 16]; + System.arraycopy(encryptedData, 16, cipherText, 0, cipherText.length); + + byte[] decryptedText = cipher.doFinal(cipherText); + return new String(decryptedText, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new Aes256Exception("Unexpected error when decrypting the given string", AES_UNEXPECTED_ERROR, e); + } + } +} + diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/utils/ObjectMapperUtils.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/utils/ObjectMapperUtils.java new file mode 100644 index 0000000..8611e40 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/utils/ObjectMapperUtils.java @@ -0,0 +1,39 @@ +package it.gov.pagopa.print.payment.notice.functions.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class ObjectMapperUtils { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Hide from public usage. + */ + private ObjectMapperUtils() { + } + + /** + * Encodes an object to a string + * + * @param value Object to be encoded + * @return encoded string + */ + public static String writeValueAsString(Object value) throws JsonProcessingException { + return objectMapper.writeValueAsString(value); + } + + /** + * Maps string to object of defined Class + * + * @param string String to map + * @param outClass Class to be mapped to + * @param Defined Class + * @return object of the defined Class + */ + public static T mapString(final String string, Class outClass) throws JsonProcessingException { + return objectMapper.readValue(string, outClass); + } + + +} diff --git a/src/main/java/it/gov/pagopa/print/payment/notice/functions/utils/WorkingDirectoryUtils.java b/src/main/java/it/gov/pagopa/print/payment/notice/functions/utils/WorkingDirectoryUtils.java new file mode 100644 index 0000000..b51b956 --- /dev/null +++ b/src/main/java/it/gov/pagopa/print/payment/notice/functions/utils/WorkingDirectoryUtils.java @@ -0,0 +1,35 @@ +package it.gov.pagopa.print.payment.notice.functions.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +/** + * Utils methods for working directory + */ +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class WorkingDirectoryUtils { + + public static File createWorkingDirectory() throws IOException { + File workingDirectory = new File("temp"); + if(!workingDirectory.exists()) { + Files.createDirectory(workingDirectory.toPath()); + } + return workingDirectory; + } + + public static void clearTempDirectory(java.nio.file.Path workingDirPath) { + try { + FileUtils.deleteDirectory(workingDirPath.toFile()); + } catch (IOException e) { + log.warn("Unable to clear working directory", e); + } + } + +} diff --git a/src/main/resources-filtered/application.properties b/src/main/resources-filtered/application.properties new file mode 100644 index 0000000..8a22575 --- /dev/null +++ b/src/main/resources-filtered/application.properties @@ -0,0 +1,2 @@ +version=${project.version} +name=${project.name} \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..fb4f272 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,74 @@ + + + + + + + + ${CONSOLE_LOG_THRESHOLD} + + + ${CONSOLE_LOG_PATTERN} + ${CONSOLE_LOG_CHARSET} + + + + + + + ${name} + ${version} + ${ENV} + + + + + + true + 20000 + 0 + + + + + + + + + + + ${FILE_LOG_THRESHOLD} + + + ${FILE_LOG_PATTERN} + ${FILE_LOG_CHARSET} + + ${LOG_FILE} + + ${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz} + ${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false} + ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB} + ${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0} + ${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-7} + + + + + true + 20000 + 0 + + + + + + + + + + + + diff --git a/src/test/java/it/gov/pagopa/print/payment/notice/functions/HealthTest.java b/src/test/java/it/gov/pagopa/print/payment/notice/functions/HealthTest.java new file mode 100644 index 0000000..441117e --- /dev/null +++ b/src/test/java/it/gov/pagopa/print/payment/notice/functions/HealthTest.java @@ -0,0 +1,49 @@ +package it.gov.pagopa.print.payment.notice.functions; + +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.HttpResponseMessage; +import com.microsoft.azure.functions.HttpStatus; +import it.gov.pagopa.print.payment.notice.functions.util.HttpResponseMessageMock; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class HealthTest { + + @Mock + ExecutionContext executionContextMock; + + @Spy + Health sut; + + @Test + void runOK() { + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatus()); + } +} diff --git a/src/test/java/it/gov/pagopa/print/payment/notice/functions/InfoTest.java b/src/test/java/it/gov/pagopa/print/payment/notice/functions/InfoTest.java new file mode 100644 index 0000000..b93fbba --- /dev/null +++ b/src/test/java/it/gov/pagopa/print/payment/notice/functions/InfoTest.java @@ -0,0 +1,57 @@ +package it.gov.pagopa.print.payment.notice.functions; + +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.HttpResponseMessage; +import com.microsoft.azure.functions.HttpStatus; +import it.gov.pagopa.print.payment.notice.functions.model.AppInfo; +import it.gov.pagopa.print.payment.notice.functions.util.HttpResponseMessageMock; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class InfoTest { + + @Mock + ExecutionContext executionContextMock; + + @Spy + Info sut; + + @Test + void runOK() { + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatus()); + assertNotNull(response.getBody()); + AppInfo responseBody = (AppInfo) response.getBody(); + assertNotNull(responseBody.getName()); + assertNotNull(responseBody.getVersion()); + assertNotNull(responseBody.getEnvironment()); + assertEquals("pagopa-print-payment-notice-functions", responseBody.getName()); + assertEquals("azure-fn", responseBody.getEnvironment()); + } +} diff --git a/src/test/java/it/gov/pagopa/print/payment/notice/functions/ManagePaymentNoticeFolderUpdatesTest.java b/src/test/java/it/gov/pagopa/print/payment/notice/functions/ManagePaymentNoticeFolderUpdatesTest.java new file mode 100644 index 0000000..ebca97b --- /dev/null +++ b/src/test/java/it/gov/pagopa/print/payment/notice/functions/ManagePaymentNoticeFolderUpdatesTest.java @@ -0,0 +1,89 @@ +package it.gov.pagopa.print.payment.notice.functions; + +import com.microsoft.azure.functions.ExecutionContext; +import it.gov.pagopa.print.payment.notice.functions.entity.PaymentGenerationRequestStatus; +import it.gov.pagopa.print.payment.notice.functions.entity.PaymentNoticeGenerationRequest; +import it.gov.pagopa.print.payment.notice.functions.entity.PaymentNoticeGenerationRequestError; +import it.gov.pagopa.print.payment.notice.functions.exception.SaveNoticeToBlobException; +import it.gov.pagopa.print.payment.notice.functions.service.NoticeFolderService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ManagePaymentNoticeFolderUpdatesTest { + + @Mock + ExecutionContext executionContextMock; + + @Mock + NoticeFolderService noticeFolderService; + + ManagePaymentNoticeFolderUpdates sut; + + @BeforeEach + public void init() { + Mockito.reset(noticeFolderService); + sut = new ManagePaymentNoticeFolderUpdates(noticeFolderService); + } + + @Test + void runOK() throws SaveNoticeToBlobException { + List paymentNoticeGenerationRequestList = + Collections.singletonList(PaymentNoticeGenerationRequest.builder() + .status(PaymentGenerationRequestStatus.COMPLETING).numberOfElementsProcessed(1) + .numberOfElementsTotal(2).numberOfElementsFailed(1).build()); + List paymentNoticeGenerationRequestErrors = new ArrayList<>(); + assertDoesNotThrow(() -> sut.processGenerateReceipt(paymentNoticeGenerationRequestList, + new HashMap[]{new HashMap<>()}, + paymentNoticeGenerationRequestErrors,executionContextMock)); + verify(noticeFolderService).manageFolder(any()); + assertEquals(0, paymentNoticeGenerationRequestErrors.size()); + } + + @Test + void runShouldIgnoreUnfinishedElements() throws SaveNoticeToBlobException { + List paymentNoticeGenerationRequestList = + Collections.singletonList(PaymentNoticeGenerationRequest.builder() + .status(PaymentGenerationRequestStatus.COMPLETING).numberOfElementsTotal(2) + .numberOfElementsProcessed(1).numberOfElementsFailed(0).build()); + List paymentNoticeGenerationRequestErrors = new ArrayList<>(); + assertDoesNotThrow(() -> sut.processGenerateReceipt(paymentNoticeGenerationRequestList, + new HashMap[]{new HashMap<>()}, + paymentNoticeGenerationRequestErrors,executionContextMock)); + verifyNoInteractions(noticeFolderService); + assertEquals(0, paymentNoticeGenerationRequestErrors.size()); + } + + @Test + void runShouldSendErrorOnException() throws SaveNoticeToBlobException { + List paymentNoticeGenerationRequestList = + Collections.singletonList(PaymentNoticeGenerationRequest.builder() + .status(PaymentGenerationRequestStatus.COMPLETING).numberOfElementsTotal(2) + .numberOfElementsProcessed(1).numberOfElementsFailed(1).build()); + List paymentNoticeGenerationRequestErrors = new ArrayList<>(); + doAnswer(item -> { + throw new SaveNoticeToBlobException("Error", 500); + }).when(noticeFolderService).manageFolder(any()); + assertDoesNotThrow(() -> sut.processGenerateReceipt(paymentNoticeGenerationRequestList, + new HashMap[]{new HashMap<>()}, + paymentNoticeGenerationRequestErrors,executionContextMock)); + verify(noticeFolderService).manageFolder(any()); + assertEquals(1, paymentNoticeGenerationRequestErrors.size()); + } + + +} diff --git a/src/test/java/it/gov/pagopa/print/payment/notice/functions/client/impl/PaymentNoticeBlobClientImplTest.java b/src/test/java/it/gov/pagopa/print/payment/notice/functions/client/impl/PaymentNoticeBlobClientImplTest.java new file mode 100644 index 0000000..2acf7eb --- /dev/null +++ b/src/test/java/it/gov/pagopa/print/payment/notice/functions/client/impl/PaymentNoticeBlobClientImplTest.java @@ -0,0 +1,77 @@ +package it.gov.pagopa.print.payment.notice.functions.client.impl; + +import com.azure.core.http.rest.PagedIterable; +import com.azure.core.http.rest.PagedResponse; +import com.azure.core.http.rest.Response; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.models.BlobItem; +import com.microsoft.azure.functions.HttpStatus; +import it.gov.pagopa.print.payment.notice.functions.model.response.BlobStorageResponse; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariables; + +class PaymentNoticeBlobClientImplTest { + + @Test + void testSingleton() throws Exception { + @SuppressWarnings("secrets:S6338") + String mockKey = "mockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeyMK=="; + withEnvironmentVariables( + "BLOB_STORAGE_CONTAINER_NAME", "notice", + "BLOB_STORAGE_CONN_STRING", "DefaultEndpointsProtocol=https;AccountName=samplestorage;AccountKey="+mockKey+";EndpointSuffix=core.windows.net", + "BLOB_STORAGE_ACCOUNT_ENDPOINT", "https://samplestorage.blob.core.windows.net" + ).execute(() -> Assertions.assertDoesNotThrow(PaymentNoticeBlobClientImpl::getInstance)); + } + + @Test + void runOk() throws IOException { + BlobServiceClient mockServiceClient = mock(BlobServiceClient.class); + BlobContainerClient mockContainer = mock(BlobContainerClient.class); + BlobClient mockClient = mock(BlobClient.class); + + Response mockBlockItem = mock(Response.class); + + when(mockBlockItem.getStatusCode()).thenReturn(HttpStatus.CREATED.value()); + + when(mockClient.uploadWithResponse(any(), eq(null), eq(null))).thenReturn( + mockBlockItem + ); + String VALID_BLOB_NAME = "a valid blob name"; + String VALID_BLOB_URL = "a valid blob url"; + when(mockClient.getBlobName()).thenReturn(VALID_BLOB_NAME); + when(mockClient.getBlobUrl()).thenReturn(VALID_BLOB_URL); + + when(mockContainer.getBlobClient(any())).thenReturn(mockClient); + + when(mockServiceClient.getBlobContainerClient(any())).thenReturn(mockContainer); + + PaymentNoticeBlobClientImpl receiptBlobClient = new PaymentNoticeBlobClientImpl(mockServiceClient); + + + + BlobItem blobItem = new BlobItem(); + blobItem.setName("testName"); + when(mockClient.exists()).thenReturn(true); + PagedIterable pagedIterable = Mockito.mock(PagedIterable.class); + doReturn(Collections.singletonList(blobItem).stream()).when(pagedIterable).stream(); + when(mockContainer.listBlobsByHierarchy(any(), any(), any())).thenReturn(pagedIterable); + + BlobStorageResponse response = receiptBlobClient.compressFolder("filename"); + + assertEquals(HttpStatus.OK.value(), response.getStatusCode()); + + } + +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/print/payment/notice/functions/client/impl/PaymentNoticeGenerationRequestClientImplTest.java b/src/test/java/it/gov/pagopa/print/payment/notice/functions/client/impl/PaymentNoticeGenerationRequestClientImplTest.java new file mode 100644 index 0000000..f5c401b --- /dev/null +++ b/src/test/java/it/gov/pagopa/print/payment/notice/functions/client/impl/PaymentNoticeGenerationRequestClientImplTest.java @@ -0,0 +1,48 @@ +package it.gov.pagopa.print.payment.notice.functions.client.impl; + +import com.azure.core.http.rest.PagedIterable; +import com.azure.core.http.rest.Response; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.models.BlobItem; +import com.microsoft.azure.functions.HttpStatus; +import com.mongodb.client.MongoCollection; +import it.gov.pagopa.print.payment.notice.functions.entity.PaymentGenerationRequestStatus; +import it.gov.pagopa.print.payment.notice.functions.entity.PaymentNoticeGenerationRequest; +import it.gov.pagopa.print.payment.notice.functions.model.response.BlobStorageResponse; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariables; + +class PaymentNoticeGenerationRequestClientImplTest { + + @Test + void testSingleton() throws Exception { + withEnvironmentVariables( + "NOTICE_REQUEST_MONGO_DB_NAME", "personDB", + "NOTICE_REQUEST_MONGODB_CONN_STRING", "mongodb://localhost:27017/personDB", + "NOTICE_REQUEST_MONGO_COLLECTION_NAME", "notice" + ).execute(() -> Assertions.assertDoesNotThrow(PaymentNoticeGenerationRequestClientImpl::getInstance)); + } + + @Test + void runOk() throws IOException { + assertDoesNotThrow(() -> + new PaymentNoticeGenerationRequestClientImpl(Mockito.mock(MongoCollection.class)) + .updatePaymentGenerationRequest( + PaymentNoticeGenerationRequest.builder().status(PaymentGenerationRequestStatus.PROCESSED) + .build())); + } + +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/print/payment/notice/functions/service/impl/NoticeFolderServiceImplTest.java b/src/test/java/it/gov/pagopa/print/payment/notice/functions/service/impl/NoticeFolderServiceImplTest.java new file mode 100644 index 0000000..4b5bc58 --- /dev/null +++ b/src/test/java/it/gov/pagopa/print/payment/notice/functions/service/impl/NoticeFolderServiceImplTest.java @@ -0,0 +1,68 @@ +package it.gov.pagopa.print.payment.notice.functions.service.impl; + +import it.gov.pagopa.print.payment.notice.functions.client.PaymentNoticeBlobClient; +import it.gov.pagopa.print.payment.notice.functions.client.PaymentNoticeGenerationRequestClient; +import it.gov.pagopa.print.payment.notice.functions.entity.PaymentNoticeGenerationRequest; +import it.gov.pagopa.print.payment.notice.functions.exception.SaveNoticeToBlobException; +import it.gov.pagopa.print.payment.notice.functions.model.response.BlobStorageResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class NoticeFolderServiceImplTest { + + @Mock + PaymentNoticeBlobClient paymentNoticeBlobClient; + + @Mock + PaymentNoticeGenerationRequestClient paymentNoticeGenerationRequestClient; + + NoticeFolderServiceImpl noticeFolderService; + + @BeforeEach + void init() { + Mockito.reset(paymentNoticeBlobClient, paymentNoticeGenerationRequestClient); + noticeFolderService = new NoticeFolderServiceImpl(paymentNoticeBlobClient, paymentNoticeGenerationRequestClient); + } + + @Test + void manageShouldCompleteWithSuccess() { + BlobStorageResponse blobStorageResponse = new BlobStorageResponse(); + blobStorageResponse.setStatusCode(200); + when(paymentNoticeBlobClient.compressFolder(any())).thenReturn(blobStorageResponse); + assertDoesNotThrow(() -> noticeFolderService.manageFolder(PaymentNoticeGenerationRequest.builder().build())); + verify(paymentNoticeBlobClient).compressFolder(any()); + verify(paymentNoticeGenerationRequestClient).updatePaymentGenerationRequest(any()); + } + + @Test + void manageShouldThrowExceptionOnBlobNotSuccessful() { + BlobStorageResponse blobStorageResponse = new BlobStorageResponse(); + blobStorageResponse.setStatusCode(500); + when(paymentNoticeBlobClient.compressFolder(any())).thenReturn(blobStorageResponse); + assertThrows(SaveNoticeToBlobException.class, () -> noticeFolderService.manageFolder(PaymentNoticeGenerationRequest.builder().build())); + verify(paymentNoticeBlobClient).compressFolder(any()); + verifyNoInteractions(paymentNoticeGenerationRequestClient); + } + + @Test + void manageShouldThrowException() { + BlobStorageResponse blobStorageResponse = new BlobStorageResponse(); + blobStorageResponse.setStatusCode(500); + when(paymentNoticeBlobClient.compressFolder(any())).thenAnswer(item -> { + throw new SaveNoticeToBlobException("Error", 500); + }); + assertThrows(SaveNoticeToBlobException.class, () -> noticeFolderService.manageFolder(PaymentNoticeGenerationRequest.builder().build())); + verify(paymentNoticeBlobClient).compressFolder(any()); + verifyNoInteractions(paymentNoticeGenerationRequestClient); + } + +} diff --git a/src/test/java/it/gov/pagopa/print/payment/notice/functions/util/HttpResponseMessageMock.java b/src/test/java/it/gov/pagopa/print/payment/notice/functions/util/HttpResponseMessageMock.java new file mode 100644 index 0000000..7030cc1 --- /dev/null +++ b/src/test/java/it/gov/pagopa/print/payment/notice/functions/util/HttpResponseMessageMock.java @@ -0,0 +1,84 @@ +package it.gov.pagopa.print.payment.notice.functions.util; + + +import com.microsoft.azure.functions.HttpResponseMessage; +import com.microsoft.azure.functions.HttpStatus; +import com.microsoft.azure.functions.HttpStatusType; + +import java.util.HashMap; +import java.util.Map; + +/** + * The mock for HttpResponseMessage, can be used in unit tests to verify if the + * returned response by HTTP trigger function is correct or not. + */ +public class HttpResponseMessageMock implements HttpResponseMessage { + private int httpStatusCode; + private HttpStatusType httpStatus; + private Object body; + private Map headers; + + public HttpResponseMessageMock(HttpStatusType status, Map headers, Object body) { + this.httpStatus = status; + this.httpStatusCode = status.value(); + this.headers = headers; + this.body = body; + } + + @Override + public HttpStatusType getStatus() { + return this.httpStatus; + } + + @Override + public int getStatusCode() { + return httpStatusCode; + } + + @Override + public String getHeader(String key) { + return this.headers.get(key); + } + + @Override + public Object getBody() { + return this.body; + } + + public static class HttpResponseMessageBuilderMock implements Builder { + private Object body; + private int httpStatusCode; + private Map headers = new HashMap<>(); + private HttpStatusType httpStatus; + + public Builder status(HttpStatus status) { + this.httpStatusCode = status.value(); + this.httpStatus = status; + return this; + } + + @Override + public Builder status(HttpStatusType httpStatusType) { + this.httpStatusCode = httpStatusType.value(); + this.httpStatus = httpStatusType; + return this; + } + + @Override + public Builder header(String key, String value) { + this.headers.put(key, value); + return this; + } + + @Override + public Builder body(Object body) { + this.body = body; + return this; + } + + @Override + public HttpResponseMessage build() { + return new HttpResponseMessageMock(this.httpStatus, this.headers, this.body); + } + } +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/print/payment/notice/functions/utils/ObjectMapperUtilsTest.java b/src/test/java/it/gov/pagopa/print/payment/notice/functions/utils/ObjectMapperUtilsTest.java new file mode 100644 index 0000000..8acefc6 --- /dev/null +++ b/src/test/java/it/gov/pagopa/print/payment/notice/functions/utils/ObjectMapperUtilsTest.java @@ -0,0 +1,18 @@ +package it.gov.pagopa.print.payment.notice.functions.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; + +class ObjectMapperUtilsTest { + + @Test + void returnNullAfterException() { + Assertions.assertThrows(JsonProcessingException.class, () -> + ObjectMapperUtils.writeValueAsString(InputStream.nullInputStream())); + Assertions.assertThrows(JsonProcessingException.class, () -> + ObjectMapperUtils.mapString("", InputStream.class)); + } +} diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..7587362 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,10 @@ + + + + %msg%n + + + + + + \ No newline at end of file