diff --git a/.azuredevops/pipelines/build.yml b/.azuredevops/pipelines/build.yml new file mode 100644 index 0000000..6cb532f --- /dev/null +++ b/.azuredevops/pipelines/build.yml @@ -0,0 +1,83 @@ +trigger: + - develop + - main + +# the build will run on a Microsoft hosted agent, using the lastest Windows VM Image +pool: + vmImage: 'windows-latest' + +# these variables are available throughout the build file +# just the build configuration is defined, in this case we are building Release packages +variables: + buildConfiguration: 'Release' + ${{ if or(eq(variables['Build.SourceBranchName'], 'develop'), eq(variables['Build.SourceBranchName'], 'main'), startsWith(variables['Build.SourceBranchName'], 'release')) }}: + MINVERBUILDMETADATA: '' + ${{ else }}: + MINVERBUILDMETADATA: $(Build.SourceVersion) + +# The build has 3 seperate tasks run under 1 step +steps: +- checkout: self + fetchDepth: 0 + +# The first task is the dotnet command build, pointing to our csproj file +- task: DotNetCoreCLI@2 + displayName: 'dotnet build' + inputs: + command: 'build' + arguments: '--configuration $(buildConfiguration)' + projects: 'Source/**/*.csproj' + +- task: DotNetCoreCLI@2 + displayName: Unit Tests + inputs: + command: 'test' + projects: '$(System.DefaultWorkingDirectory)/Source/**/*UnitTests.csproj' + arguments: '--configuration $(buildConfiguration) --collect "XPlat Code coverage"' + +- task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@5 + displayName: 'Generate Cobertura Code Coverage Report' + inputs: + ${{ if eq(variables['Agent.OS'], 'Windows_NT') }}: + reports: '$(Agent.TempDirectory)\**\coverage.cobertura.xml' + ${{ if ne(variables['Agent.OS'], 'Windows_NT') }}: + reports: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' + targetdir: '$(Build.SourcesDirectory)\Source\TestResults\Coverage\Reports\' + historydir: '$(Build.SourcesDirectory)\Source\TestResults\Coverage\History\' + assemblyfilters: '+ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation' + verbosity: Verbose + tag: $(Build.BuildNumber) + reportTypes: HtmlInline_AzurePipelines;Cobertura + +- task: PublishCodeCoverageResults@1 + displayName: 'Publish Cobertura Code Coverage' + inputs: + codeCoverageTool: Cobertura + summaryFileLocation: '$(Build.SourcesDirectory)/Source/TestResults/Coverage/Reports/Cobertura.xml' + reportDirectory: '$(Build.SourcesDirectory)/Source/TestResults/Coverage/Reports' + +# The second task is dotnet pack command again pointing to the csproj file +# The nobuild means the project will not be compiled before running pack, because its already built in above step +- task: DotNetCoreCLI@2 + displayName: "dotnet pack" + inputs: + command: 'pack' + arguments: '--configuration $(buildConfiguration)' + packagesToPack: 'Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.csproj' + nobuild: true + versioningScheme: 'off' + +- task: PublishSymbols@2 + displayName: Publish symbols path + continueOnError: True + inputs: + SearchPattern: '**\bin\**\*.pdb' + PublishSymbols: false + SymbolServerType: TeamServices + +- task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: drop' + condition: succeededOrFailed() + inputs: + PathtoPublish: $(Build.ArtifactStagingDirectory) + TargetPath: '\\my\share\$(Build.DefinitionName)\$(Build.BuildNumber)' diff --git a/.azuredevops/pipelines/publish.yml b/.azuredevops/pipelines/publish.yml new file mode 100644 index 0000000..e71f7a6 --- /dev/null +++ b/.azuredevops/pipelines/publish.yml @@ -0,0 +1,111 @@ +pool: + vmImage: 'windows-latest' + +parameters: +- name: pushEnvironment + displayName: 'Push to which environment?' + type: string + values: + - Test + - Production + default: Test + +# The build configuration is defined, in this case we are building Release packages +# Based on 'pushEnvironment' parameter, we set other variables and variable groups to use test vs prod settings +variables: + - name: buildConfiguration + value: 'Release' + - name : apiKey + ${{ if eq(parameters.pushEnvironment, 'Production') }}: + value: $(nuget-mock-solution-test-automation-api-key) + ${{ if ne(parameters.pushEnvironment, 'Production') }}: + value: $(int-nugettest-mock-solution-test-automation-api-key) + - name : nugetOrgSource + ${{ if eq(parameters.pushEnvironment, 'Production') }}: + value: 'https://api.nuget.org/v3/index.json' + ${{ if ne(parameters.pushEnvironment, 'Production') }}: + value: 'https://apiint.nugettest.org/v3/index.json' + - ${{ if eq(parameters.pushEnvironment, 'Production') }}: + - group: nuget_package_deployment + - ${{ if ne(parameters.pushEnvironment, 'Production') }}: + - group: int_nugettest_package_deployment + +# The github-ref-prefix should be either 'tags/' (for a tagged release) or 'heads/' (for a branch). Release number is either the tag name or branch name +# The standard release process would use a tagged release where the tag name is the version number (e.g 1.0.0) so prefix is default 'tags/' and release-number would be '1.0.0' +resources: + repositories: + - repository: GitHubRepo + type: github + name: ConsumerDataRight/mock-solution-test-automation + endpoint: github.com_CDR-LukeH + ref: refs/$(github-ref-prefix)$(release-number) + +# The build has 3 seperate tasks run under 1 step +steps: +- checkout: GitHubRepo + fetchDepth: 0 + +# Build the project by running the dotnet command build, pointing to our csproj file +- task: DotNetCoreCLI@2 + displayName: 'dotnet build' + inputs: + command: 'build' + versioningScheme: byBuildNumber + arguments: '--configuration $(buildConfiguration) /p:UsingGitHubSource=true' + projects: '$(System.DefaultWorkingDirectory)\Source\**\*.csproj' + +# Create the package by running the dotnet pack command again pointing to the csproj file +# The nobuild means the project will not be compiled before running pack, because its already built in above step +- task: DotNetCoreCLI@2 + displayName: "dotnet pack" + inputs: + command: 'pack' + configuration: $(BuildConfiguration) + packagesToPack: '$(System.DefaultWorkingDirectory)\Source\**\*.csproj' + nobuild: true + versioningScheme: 'off' + +- task: PublishSymbols@2 + displayName: Publish symbols path + continueOnError: True + inputs: + SearchPattern: '**\bin\**\*.pdb' + PublishSymbols: false + SymbolServerType: TeamServices + +- task: DotNetCoreCLI@2 + displayName: Install NuGetKeyVaultSignTool + inputs: + command: 'custom' + custom: 'tool' + arguments: 'install --tool-path . NuGetKeyVaultSignTool' + +# WARNING: This will not throw an error if it can't find the file and will close silently (false positive) +- task: PowerShell@2 + displayName: Signing with NuGetKeyVaultSignTool + inputs: + targetType: 'inline' + script: | + .\NuGetKeyVaultSignTool sign $(Build.ArtifactStagingDirectory)\*.nupkg ` + --file-digest "sha256" ` + --timestamp-rfc3161 "http://timestamp.digicert.com" ` + --timestamp-digest "sha256" ` + --azure-key-vault-url $(code-signing-kv-url) ` + --azure-key-vault-tenant-id $(code-signing-kv-tenant-id) ` + --azure-key-vault-client-id $(sp-code-signing-prod-client-id) ` + --azure-key-vault-client-secret $(sp-code-signing-prod-client-secret) ` + --azure-key-vault-certificate $(code-signing-cert-name) + +# NOTE: Avoiding verifying with NuGetKeyVaultSignTool as it is rather faulty. Will give false positive for a file that doesn't exist. +# Use dotnet nuget verify instead +- task: PowerShell@2 + displayName: Verifying NuGetKeyVaultSign + inputs: + targetType: 'inline' + script: 'dotnet nuget verify $(Build.ArtifactStagingDirectory)\*.nupkg' + +- task: PowerShell@2 + displayName: Publishing signed package + inputs: + targetType: 'inline' + script: 'dotnet nuget push $(Build.ArtifactStagingDirectory)\*.nupkg --api-key $(apiKey) -n --source $(nugetOrgSource)' \ No newline at end of file diff --git a/.azuredevops/pull_request_template.md b/.azuredevops/pull_request_template.md new file mode 100644 index 0000000..0c8d12c --- /dev/null +++ b/.azuredevops/pull_request_template.md @@ -0,0 +1,27 @@ +**Checklist:** (Put an `x` in all the boxes that apply) +- [ ] My code follows the code style of this project. +- [ ] I have set this Pull Request to Auto Complete with the delete source branch option selected. +- [ ] Commented out code has been removed or will be removed. +- [ ] I have updated the documentation accordingly. +- [ ] I have added tests to cover my changes. +- [ ] All new and existing tests passed. +- [ ] I have updated the `CHANGELOG.md` file as appropriate. + + +**What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) + + + +**What is the current behavior?** (You can also link to an open issue here) + + + +**What is the new behavior?** (if this is a feature change) + + + +**Does this PR introduce a breaking change?** (What changes might users need to make in their application due to this PR?) + + + +**Other information**: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..75e99b0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug report +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. +2. +3. +4. See error + +**Expected behaviour** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..4a9678b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: CDR Website + url: http://cdr.gov.au/ + about: For information relating to the Consumer Data Right + - name: CDR Register Design Reference + url: https://cdr-register.github.io/register/#introduction + about: The place for information relating to the CDR Register Design + - name: Consumer Data Standards + url: https://consumerdatastandardsaustralia.github.io/standards/#introduction + about: The place for information relating to the Consumer Data Standards \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..dafdd41 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,27 @@ +**Checklist:** (Put an `x` in all the boxes that apply) +- [ ] I have read the **CONTRIBUTING** document. +- [ ] My code follows the code style of this project. +- [ ] I have updated the documentation accordingly. +- [ ] I have added tests to cover my changes. +- [ ] All new and existing tests passed. +- [ ] I have checked that there aren't any other open **Pull Requests** from the same change. +- [ ] The `develop` branch has been set as the `base` branch to merge changes of the pull request. +- [ ] I have updated the `CHANGELOG.md` file as appropriate. + +**What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) + + + +**What is the current behavior?** (You can also link to an open issue here) + + + +**What is the new behavior?** (if this is a feature change) + + + +**Does this PR introduce a breaking change?** (What changes might users need to make in their application due to this PR?) + + + +**Other information**: diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..4524e42 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,19 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 10 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 5 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security + - investigating + - bug report + - backlog +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had recent activity. + It will be closed if no further activity occurs. Thank you for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..de070ec --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,81 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main, develop ] + paths-ignore: + # Any update here needs to be done for + # - `pull_request` see below + - '*.md' + - '.github/ISSUE_TEMPLATE/**' + - '.github/pull_request_template.md' + - '.github/stale.yml' + - 'LICENSE' + pull_request: + # The branches below must be a subset of the branches above + branches: [ main, develop ] + paths-ignore: + # Any update here needs to be done for + # - `push`see before + - '*.md' + - '.github/ISSUE_TEMPLATE/**' + - '.github/pull_request_template.md' + - '.github/stale.yml' + - 'LICENSE' + schedule: + - cron: '24 17 * * 4' + +env: + buildConfiguration: 'Release' + buildRuntime: 'win-x64' + runEnvironment: 'Release' + ASPNETCORE_ENVIRONMENT: 'Release' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + - name: Build solution + run: | + dotnet build ./Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.sln --configuration $buildConfiguration -p:UsingGitHubSource=true + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..4ead0dd --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,70 @@ +name: Build and Test + +on: + push: + branches: [ main, develop ] + paths-ignore: + # Any update here needs to be done for + # - `pull_request` see below + - '*.md' + - '.github/ISSUE_TEMPLATE/**' + - '.github/pull_request_template.md' + - '.github/stale.yml' + - 'LICENSE' + pull_request: + branches: [ main, develop ] + types: [opened, synchronize, reopened] + paths-ignore: + # Any update here needs to be done for + # - `push`see before + - '*.md' + - '.github/ISSUE_TEMPLATE/**' + - '.github/pull_request_template.md' + - '.github/stale.yml' + - 'LICENSE' + +env: + buildConfiguration: 'Release' + buildRuntime: 'win-x64' + runEnvironment: 'Release' + ASPNETCORE_ENVIRONMENT: 'Release' + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout Mock Solution Test Automation + uses: actions/checkout@v4 + with: + path: ./mock-solution-test-automation + + - name: List contents + if: always() + run: | + ls + cd mock-solution-test-automation + ls + cd .. + + # build project + - name: Build project + run: | + dotnet build ./mock-solution-test-automation/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.csproj --configuration $buildConfiguration -p:UsingGitHubSource=true + + # run Unit Tests + - name: Run Unit Tests + run: | + dotnet test ./mock-solution-test-automation/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests.csproj --configuration $buildConfiguration -p:UsingGitHubSource=true --verbosity normal --logger "trx;LogFileName=unittests.trx" + + # publish test results + - name: Publish Unit Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + trx_files: "./mock-solution-test-automation/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/TestResults/unittests.trx" + + # build nuget pacakge + - name: Build the mock-solution-test-automation nuget package + run: | + dotnet pack ./mock-solution-test-automation/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.csproj --configuration $buildConfiguration --no-build \ No newline at end of file diff --git a/.github/workflows/sonarcloud-analysis.yml b/.github/workflows/sonarcloud-analysis.yml new file mode 100644 index 0000000..7c47ce3 --- /dev/null +++ b/.github/workflows/sonarcloud-analysis.yml @@ -0,0 +1,78 @@ +name: SonarCloud +on: + push: + branches: [main, develop] + paths-ignore: + # Any update here needs to be done for + # - `pull_request` see below + - '*.md' + - '.github/ISSUE_TEMPLATE/**' + - '.github/pull_request_template.md' + - '.github/stale.yml' + - 'LICENSE' + pull_request: + branches: [main, develop] + types: [opened, synchronize, reopened] + paths-ignore: + # Any update here needs to be done for + # - `push`see before + - '*.md' + - '.github/ISSUE_TEMPLATE/**' + - '.github/pull_request_template.md' + - '.github/stale.yml' + - 'LICENSE' + +env: + sonarSecret: ${{ secrets.SONAR_TOKEN }} + buildConfiguration: 'Release' + buildRuntime: 'win-x64' + runEnvironment: 'Release' + ASPNETCORE_ENVIRONMENT: 'Release' + +jobs: + analyze: + name: SonarCloud + runs-on: windows-latest + + steps: + - name: Set up JDK 17 + uses: actions/setup-java@v3 + if: ${{ env.sonarSecret != 0 }} # Only run scan if secret use is allowed - requires secrets to run successfully + with: + java-version: 17 + distribution: 'zulu' # Alternative distribution options are available. + - uses: actions/checkout@v3 + if: ${{ env.sonarSecret != 0 }} # Only run scan if secret use is allowed - requires secrets to run successfully + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Cache SonarCloud packages + uses: actions/cache@v3 + if: ${{ env.sonarSecret != 0 }} # Only run scan if secret use is allowed - requires secrets to run successfully + with: + path: ~\sonar\cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache SonarCloud scanner + id: cache-sonar-scanner + uses: actions/cache@v3 + if: ${{ env.sonarSecret != 0 }} # Only run scan if secret use is allowed - requires secrets to run successfully + with: + path: .\.sonar\scanner + key: ${{ runner.os }}-sonar-scanner + restore-keys: ${{ runner.os }}-sonar-scanner + - name: Install SonarCloud scanner + if: ${{ steps.cache-sonar-scanner.outputs.cache-hit != 'true' && env.sonarSecret != 0 }} + shell: powershell + run: | + New-Item -Path .\.sonar\scanner -ItemType Directory + dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner + - name: Build and analyze + if: ${{ env.sonarSecret != 0 }} # Only run scan if secret use is allowed - requires secrets to run successfully + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + shell: powershell + run: | + .\.sonar\scanner\dotnet-sonarscanner begin /k:"ConsumerDataRight_mock-solution-test-automation" /o:"consumerdataright" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" + dotnet build .\Source\ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.sln --configuration $buildConfiguration -p:UsingGitHubSource=true + .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72cbda4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,347 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# Cobertura coverage results +*.cobertura.xml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# SonarQube +.sonarqube/ +/.vscode/settings.json diff --git a/Assets/cdr-logo.png b/Assets/cdr-logo.png new file mode 100644 index 0000000..c21d558 Binary files /dev/null and b/Assets/cdr-logo.png differ diff --git a/Assets/cdr-package-logo.png b/Assets/cdr-package-logo.png new file mode 100644 index 0000000..7bba32b Binary files /dev/null and b/Assets/cdr-package-logo.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..622bfa6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.0.0] - 2023-11-23 + +### Added +- First release of the Mock Solution Test Automation project diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..7501fdc --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[opensource@cdr.gov.au](mailto:opensource@cdr.gov.au). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3786e15 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing +We encourage contributions from the CDR community. If you would like to get involved, please follow the guidelines below. + +## Questions +Our **issues** tracker is for issues concerning the Mock Solution Test Automation repository. It is not for general questions about the Consumer Data Right or about the Consumer Data Standards. General questions can be raised through the usual support channels, such as [CDR Support Portal](https://cdr-support.zendesk.com/hc/en-us). + +## Pull Request Process +If you see an opportunity for improvement or identify an issue that can be fixed and you can make the change yourself, please go ahead. We follow a typical `git` workflow process for changes to the Mock Solution Test Automation source code. + +1. Fork the Mock Solution Test Automation repository. See [Fork a repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) on GitHub. +2. Clone the forked repository. +3. Create a new feature or hotfix branch. +4. Make your changes and push your code. +5. Open a Pull Request against the `mock-solution-test-automation` repository. +6. A member of the **Mock Solution Test Automation** team will review your change and approve or comment on it in due course. +7. Approved changes are merged to the `develop` branch. +8. A new NuGet package will be built and pushed to [nuget.org](http://nuget.org). +9. At certain points the `develop` branch will be merged to `main` and a new NuGet package will be pushed to nuget.org. + +### Notes: +- We use a typical git workflow. Feature branches should start `feature/` and `hotfix/`. +- Please make sure that you **ALWAYS** create pull requests for the develop branch. + +## Representations +This software and associated documentation ("Work") is presented by the ACCC for the purpose of fostering innovation, free of charge, for the benefit of the public. The ACCC monitors the quality of the Work available, and responds to and updates the Work through methods such as Pull Requests. + +The ACCC does not guarantee, and accepts no legal liability whatsoever arising from or connected to, the use of or reliance on any views or recommendations expressed by or on behalf of the ACCC through any responses or updates to the Work, including any representations made during a Pull Request process. Any such statements do not necessarily reflect the views of the ACCC or indicate its commitment to a particular course of action. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..897e2a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Commonwealth of Australia + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..75779bc --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +![Consumer Data Right Logo](https://github.com/ConsumerDataRight/mock-solution-test-automation/blob/main/Assets/cdr-logo.png?raw=true) + +[![made-with-dotnet](https://img.shields.io/badge/Made%20with-.NET-1f425Ff.svg)](https://dotnet.microsoft.com/) +[![made-with-csharp](https://img.shields.io/badge/Made%20with-C%23-1f425Ff.svg)](https://docs.microsoft.com/en-us/dotnet/csharp/) +[![MIT License](https://img.shields.io/github/license/ConsumerDataRight/mock-solution-test-automation)](https://github.com/ConsumerDataRight/mock-solution-test-automation/blob/main/LICENSE) +[![Pull Requests Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/ConsumerDataRight/mock-solution-test-automation/blob/main/CONTRIBUTING.md) + +# Consumer Data Right - Mock Solution Test Automation + +**Note:** This repository is only relevant from version 1.1.0 of the [Authorisation Server](https://github.com/ConsumerDataRight/authorisation-server) and version 2.0.0 of the [Mock Data Holder](https://github.com/ConsumerDataRight/mock-data-holder) solutions. + +Check [here](https://github.com/ConsumerDataRight/authorisation-server/releases) for the latest Authorisation Server version and [here](https://github.com/ConsumerDataRight/mock-data-holder/releases) for the latest Mock Data Holder version. + + +This project includes source code and documentation for the Consumer Data Right (CDR) Mock Solution Test Automation NuGet package. + +It contains common source code that is used by the [Mock Data Holder](https://github.com/ConsumerDataRight/mock-data-holder) and [Authorisation Server](https://github.com/ConsumerDataRight/authorisation-server) test automation projects. + +The project is built and packaged as a NuGet package and available on [NuGet](https://www.nuget.org/packages/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation). + + +**Note:** This project is designed specifically for use in [CDR Mock Solutions](https://github.com/ConsumerDataRight) test projects. It is not intended to be used as a stand alone testing solution as it is tightly coupled with CDR Mock Solutions. + + +## Getting Started + +To get started, clone the source code. +``` +git clone https://github.com/ConsumerDataRight/mock-solution-test-automation.git +``` + +Documentation on how this project is used can be found in the test automation execution guides by following the links below: + +- [Authorisation Server Test Automation Execution Guide](https://github.com/ConsumerDataRight/authorisation-server/blob/main/Help/testing/HELP.md) +- [Mock Data Holder Test Automation Execution Guide](https://github.com/ConsumerDataRight/mock-data-holder/blob/main/Help/testing/HELP.md) + +## Technology Stack + +The following technologies have been used to build the Mock Solution Test Automation project: +- The source code has been written in `C#` using the `.Net 6` framework. +- `xUnit` is the framework used for writing and running tests. +- `Microsoft Playwright` is the framework used for Web Testing. + +# Contribute +We encourage contributions from the community. See our [contributing guidelines](https://github.com/ConsumerDataRight/mock-solution-test-automation/blob/main/CONTRIBUTING.md). + +# Code of Conduct +This project has adopted the **Contributor Covenant**. For more information see the [code of conduct](https://github.com/ConsumerDataRight/mock-solution-test-automation/blob/main/CODE_OF_CONDUCT.md). + + +# Security Policy +See our [security policy](https://github.com/ConsumerDataRight/mock-solution-test-automation/blob/main/SECURITY.md) for information on security controls, reporting a vulnerability and supported versions. +# License +[MIT License](https://github.com/ConsumerDataRight/mock-solution-test-automation/blob/main/LICENSE) + +# Notes +The Mock Solution Test Automation solution is provided as a development and testing tool that is used by the other mock solutions. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..2739b9b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,37 @@ +# Security Policy +If you have discovered a potential security vulnerability within the [Consumer Data Right GitHub Organisation](https://github.com/ConsumerDataRight) or [Consumer Data Right Sandbox](https://cdrsandbox.gov.au/) +operated by the ACCC, we encourage you to disclose it to us as quickly as possible and in a responsible manner in accordance with our [Responsible disclosure of security vulnerabilities policy](https://www.cdr.gov.au/resources/responsible-disclosure-security-vulnerabilities-policy). + +Visit our [Responsible disclosure of security vulnerabilities policy](https://www.cdr.gov.au/resources/responsible-disclosure-security-vulnerabilities-policy) for: + - A full view of our Responsible disclosure of security vulnerabilities policy + - Your responsibilities if you find a vulnerability + - Steps required for reporting a vulnerability + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 1.0.x | :white_check_mark: | + + + +## Reporting a Vulnerability +Visit our [Responsible disclosure of security vulnerabilities policy](https://www.cdr.gov.au/resources/responsible-disclosure-security-vulnerabilities-policy) for steps required for reporting a vulnerability. + + +## What controls are in place +### SonarCloud +Code repositories in [Consumer Data Right GitHub Organisation](https://github.com/ConsumerDataRight) utilise [SonarCloud](https://docs.sonarcloud.io/). Whenever a code change is made to this repository, GitHub actions are used to scan the code using SonarCloud. +The SonarCloud results are then assessed. High impact issues, that are not false positives, will be remediated. + - [mock-register results](https://sonarcloud.io/project/overview?id=ConsumerDataRight_mock-register) + - [mock-data-holder results](https://sonarcloud.io/project/overview?id=ConsumerDataRight_mock-data-holder) + - [mock-data-holder-energy results](https://sonarcloud.io/project/overview?id=ConsumerDataRight_mock-data-holder-energy) + - [mock-data-recipient results](https://sonarcloud.io/project/overview?id=ConsumerDataRight_mock-data-recipient) + - [authorisation-server results](https://sonarcloud.io/project/overview?id=ConsumerDataRight_authorisation-server) + - [mock-solution-test-automation results](https://sonarcloud.io/project/overview?id=ConsumerDataRight_mock-solution-test-automation) + +### GitHub Security Features +Code repositories in [Consumer Data Right GitHub Organisation](https://github.com/ConsumerDataRight) utilise [GitHub security features](https://docs.github.com/en/code-security/getting-started/github-security-features). + +### Keeping up to date +Code repositories in [Consumer Data Right GitHub Organisation](https://github.com/ConsumerDataRight) are routinely updated with new features and dependency updates. \ No newline at end of file diff --git a/Source/.editorconfig b/Source/.editorconfig new file mode 100644 index 0000000..c44310a --- /dev/null +++ b/Source/.editorconfig @@ -0,0 +1,255 @@ +[*.cs] + +# CA1707: Identifiers should not contain underscores +dotnet_diagnostic.CA1707.severity = suggestion + +# CA1303: Do not pass literals as localized parameters +dotnet_diagnostic.CA1303.severity = silent + +# SA1633: File should have header +dotnet_diagnostic.SA1633.severity = none + +# SA1200: Using directives should be placed correctly +dotnet_diagnostic.SA1200.severity = silent + +# IDE1006: Naming Styles +dotnet_diagnostic.IDE1006.severity = error + +# SA1309: Field names should not begin with underscore +dotnet_diagnostic.SA1309.severity = silent + +# SA1101: Prefix local calls with this +dotnet_diagnostic.SA1101.severity = silent + +# SA1623: Property summary documentation should match accessors +dotnet_diagnostic.SA1623.severity = silent + +# SA1101: Prefix local calls with this +dotnet_diagnostic.SA1101.severity = none + +# CA5350: Do Not Use Weak Cryptographic Algorithms +dotnet_diagnostic.CA5350.severity = none + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:suggestion + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false:silent +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.private_or_internal_field_should_be_begins_with_underscore.severity = warning +dotnet_naming_rule.private_or_internal_field_should_be_begins_with_underscore.symbols = private_or_internal_field +dotnet_naming_rule.private_or_internal_field_should_be_begins_with_underscore.style = begins_with_underscore + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field +dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected +dotnet_naming_symbols.private_or_internal_field.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.begins_with_underscore.required_prefix = _ +dotnet_naming_style.begins_with_underscore.required_suffix = +dotnet_naming_style.begins_with_underscore.word_separator = +dotnet_naming_style.begins_with_underscore.capitalization = camel_case +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent + +[tests/**/*.cs] +# CA1707: Identifiers should not contain underscores +dotnet_diagnostic.CA1707.severity = silent +[*.{cs,vb}] +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests.csproj b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests.csproj new file mode 100644 index 0000000..6c0c643 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + enable + enable + false + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/Tests/TestClass1.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/Tests/TestClass1.cs new file mode 100644 index 0000000..21fef27 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/Tests/TestClass1.cs @@ -0,0 +1,22 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests.Tests +{ + public class TestClass1 + { + public class Startup + { + //A default startup is required due to the test project inheriting Xunit.DependencyInjection from the Nuget project. + public void ConfigureServices(IServiceCollection services) { } + } + + [Fact] + public void Test1() + { + var num = 1; + num.Should().Be(1); + } + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/appsettings.Development.json b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/appsettings.Development.json new file mode 100644 index 0000000..3998118 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/appsettings.Development.json @@ -0,0 +1,36 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.XUnit", "Serilog.Sinks.Async" ], + "MinimumLevel": { + "Default": "Debug" + }, + "WriteTo": [ + { + "Name": "Async", + "Args": { + "configure": [ + { + "Name": "Console", + "Args": { + } + } + ] + } + }, + { + "Name": "File", + "Args": { + "path": "AutomationUnitTestLog.txt", + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {Message:lj}{NewLine}{Exception}", + "rollingInterval": "Day" + } + }, + { + "Name": "XUnit", + "Args": { + } + } + ], + "Enrich": [ "FromLogContext", "WithExceptionDetails" ] + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/appsettings.Release.json b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/appsettings.Release.json new file mode 100644 index 0000000..2c57ede --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/appsettings.Release.json @@ -0,0 +1,36 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.XUnit", "Serilog.Sinks.Async" ], + "MinimumLevel": { + "Default": "Information" + }, + "WriteTo": [ + { + "Name": "Async", + "Args": { + "configure": [ + { + "Name": "Console", + "Args": { + } + } + ] + } + }, + { + "Name": "File", + "Args": { + "path": "AutomationUnitTestLog.txt", + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {Message:lj}{NewLine}{Exception}", + "rollingInterval": "Day" + } + }, + { + "Name": "XUnit", + "Args": { + } + } + ], + "Enrich": [ "FromLogContext", "WithExceptionDetails" ] + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/appsettings.json b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/appsettings.json new file mode 100644 index 0000000..45fe774 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.sln b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.sln new file mode 100644 index 0000000..a82f242 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5EB0C4E7-3595-403D-A2EB-481BC66E3B0E}" + ProjectSection(SolutionItems) = preProject + ..\README.md = ..\README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation", "ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation\ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.csproj", "{4072F776-E850-416F-AC70-D51AAD50DA45}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests", "ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests\ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UnitTests.csproj", "{CC637FA5-4FA3-4D9C-B965-605CAE896BD3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + Shared|Any CPU = Shared|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4072F776-E850-416F-AC70-D51AAD50DA45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4072F776-E850-416F-AC70-D51AAD50DA45}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4072F776-E850-416F-AC70-D51AAD50DA45}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4072F776-E850-416F-AC70-D51AAD50DA45}.Release|Any CPU.Build.0 = Release|Any CPU + {4072F776-E850-416F-AC70-D51AAD50DA45}.Shared|Any CPU.ActiveCfg = Shared|Any CPU + {4072F776-E850-416F-AC70-D51AAD50DA45}.Shared|Any CPU.Build.0 = Shared|Any CPU + {CC637FA5-4FA3-4D9C-B965-605CAE896BD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC637FA5-4FA3-4D9C-B965-605CAE896BD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC637FA5-4FA3-4D9C-B965-605CAE896BD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC637FA5-4FA3-4D9C-B965-605CAE896BD3}.Release|Any CPU.Build.0 = Release|Any CPU + {CC637FA5-4FA3-4D9C-B965-605CAE896BD3}.Shared|Any CPU.ActiveCfg = Debug|Any CPU + {CC637FA5-4FA3-4D9C-B965-605CAE896BD3}.Shared|Any CPU.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9D3EF6CE-93E6-4304-B88D-C1F104CCBC4A} + EndGlobalSection +EndGlobal diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Assertions.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Assertions.cs new file mode 100644 index 0000000..be6db07 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Assertions.cs @@ -0,0 +1,220 @@ +using System.Net.Http.Headers; +using System.Security.Claims; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models; +using FluentAssertions; +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation +{ + public static class Assertions + { + + /// + /// Assert response content and expectedJson are equivalent + /// + /// The expected json + /// The response content + public static async Task AssertHasContentJson(string? expectedJson, HttpContent? content) + { + content.Should().NotBeNull(expectedJson ?? ""); + if (content == null) + { + return; + } + + var actualJson = await content.ReadAsStringAsync(); + AssertJson(expectedJson, actualJson); + } + + public static async Task AssertHasContentJson(string? expectedJson, HttpContent? content) + { + content.Should().NotBeNull(expectedJson ?? ""); + if (content == null) + { + return; + } + + var actualJson = await content.ReadAsStringAsync(); + AssertJson(expectedJson, actualJson); + } + + /// + /// Assert response content is empty + /// + /// The response content + /// Reason the assertion is needed + public static async Task AssertHasNoContent(HttpContent? content, string? because = null) + { + content.Should().NotBeNull(); + if (content == null) + { + return; + } + + var actualJson = await content.ReadAsStringAsync(); + actualJson.Should().BeNullOrEmpty(because); + } + + /// + /// Assert_HasNoContent because "No detail about response content in AC, check that API does not actually return any response content" + /// + /// + /// + public static async Task AssertHasNoContent2(HttpContent? content) + { + // Assert - No detail about response content in AC, check that API does not actually return any response content + await AssertHasNoContent(content, "(Assert_HasNoContent2) AC does not specify response content and yet content is returned by API. Either AC needs to specify expected response content or API needs to return no content."); + } + + /// + /// Assert actual json is equivalent to expected json + /// + /// The expected json + /// The actual json + public static void AssertJson(string? expectedJson, string actualJson) + { + AssertJson(expectedJson, actualJson); + + //if (AssertIsJsonNullOrEmpty(expectedJson, actualJson)) + //{ + // return; + //} + + //object? expectedObject = JsonConvert.DeserializeObject(expectedJson); + //expectedObject.Should().NotBeNull($"Error deserializing expected json - '{expectedJson}'"); + + //object? actualObject = JsonConvert.DeserializeObject(actualJson); + //actualObject.Should().NotBeNull($"Error deserializing actual json - '{actualJson}'"); + + //var expectedJsonNormalised = JsonConvert.SerializeObject(expectedObject); + //var actualJsonNormalised = JsonConvert.SerializeObject(actualObject); + + //actualJson?.JsonCompare(expectedJson).Should().BeTrue( + // $"\r\nExpected json:\r\n{expectedJsonNormalised}\r\nActual Json:\r\n{actualJsonNormalised}\r\n" + //); + } + + public static void AssertJson(string? expectedJson, string actualJson) + { + if (AssertIsJsonNullOrEmpty(expectedJson, actualJson)) + { + return; + } + + var expectedObject = JsonConvert.DeserializeObject(expectedJson); + expectedObject.Should().NotBeNull($"Error deserializing expected json - '{expectedJson}'"); + + var actualObject = JsonConvert.DeserializeObject(actualJson); + actualObject.Should().NotBeNull($"Error deserializing actual json - '{actualJson}'"); + + var expectedJsonNormalised = JsonConvert.SerializeObject(expectedObject); + var actualJsonNormalised = JsonConvert.SerializeObject(actualObject); + + actualJson?.JsonCompare(expectedJson).Should().BeTrue( + $"\r\nExpected json:\r\n{expectedJsonNormalised}\r\nActual Json:\r\n{actualJsonNormalised}\r\n" + ); + } + + private static bool AssertIsJsonNullOrEmpty(string? expectedJson, string actualJson) + { + expectedJson.Should().NotBeNullOrEmpty(); + actualJson.Should().NotBeNullOrEmpty(expectedJson == null ? "" : $"expected {expectedJson}"); + + if (string.IsNullOrEmpty(expectedJson) || string.IsNullOrEmpty(actualJson)) + { + return true; + } + + return false; + } + + //private static void SerializeAndCompareJson(string? expectedObject, string actualObject) + //{ + // var expectedJsonNormalised = JsonConvert.SerializeObject(expectedObject); + // var actualJsonNormalised = JsonConvert.SerializeObject(actualObject); + + // actualJson?.JsonCompare(expectedJson).Should().BeTrue( + // $"\r\nExpected json:\r\n{expectedJsonNormalised}\r\nActual Json:\r\n{actualJsonNormalised}\r\n" + // ); + //} + + /// + /// Assert headers has a single header with the expected value. + /// If expectedValue then just check for the existence of the header (and not it's value) + /// + /// The expected header value + /// The headers to check + /// Name of header to check + /// Whether the header just needs to start with the expected value (opposed to matching exactly) + public static void AssertHasHeader(string? expectedValue, HttpHeaders headers, string name, bool startsWith = false) + { + headers.Should().NotBeNull(); + if (headers != null) + { + headers.Contains(name).Should().BeTrue($"name={name}"); + if (headers.Contains(name)) + { + var headerValues = headers.GetValues(name); + headerValues.Should().ContainSingle(name, $"name={name}"); + + if (expectedValue != null) + { + string headerValue = headerValues.First(); + + if (startsWith) + { + headerValue.Should().StartWith(expectedValue, $"name={name}"); + } + else + { + headerValue.Should().Be(expectedValue, $"name={name}"); + } + } + } + } + } + + /// + /// Assert header has content type of ApplicationJson + /// + /// + public static void AssertHasContentTypeApplicationJson(HttpContent content) + { + content.Should().NotBeNull(); + content?.Headers.Should().NotBeNull(); + content?.Headers?.ContentType.Should().NotBeNull(); + content?.Headers?.ContentType?.ToString().Should().StartWith("application/json"); + } + + /// + /// Assert claim exists + /// + public static void AssertClaim(IEnumerable claims, string claimType, string claimValue) + { + claims.Should().NotBeNull(); + if (claims != null) + { + claims.FirstOrDefault(claim => claim.Type == claimType && claim.Value == claimValue).Should().NotBeNull($"Expected {claimType}={claimValue}"); + } + } + + public static async Task AssertErrorAsync(HttpResponseMessage responseMessage, AuthoriseException expectedError) + { + responseMessage.StatusCode.Should().Be(expectedError.StatusCode); + + AssertHasContentTypeApplicationJson(responseMessage.Content); + + var responseContent = await responseMessage.Content.ReadAsStringAsync(); + var receivedError = JsonConvert.DeserializeObject(responseContent); + + receivedError.Should().NotBeNull(); + if (receivedError != null) + { + receivedError.Description.Should().Be(expectedError.ErrorDescription); + receivedError.Code.Should().Be(expectedError.Error); + } + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Attributes/DisplayTestMethodNameAttribute.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Attributes/DisplayTestMethodNameAttribute.cs new file mode 100644 index 0000000..e0a9c2d --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Attributes/DisplayTestMethodNameAttribute.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using Serilog; +using Xunit.Sdk; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Attributes +{ + public class DisplayTestMethodNameAttribute : BeforeAfterTestAttribute + { + private int _count = 0; + + public override void Before(MethodInfo methodUnderTest) + { + Log.Logger.Information("-----------------------------------------------------"); + Log.Logger.Information("--Test #{count} - {TestClassName}.{TestName}", ++_count, methodUnderTest.DeclaringType?.Name, methodUnderTest.Name); + } + + public override void After(MethodInfo methodUnderTest) + { + Log.Logger.Information("--Test complete - {TestClassName}.{TestName}", methodUnderTest.DeclaringType?.Name, methodUnderTest.Name); + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/AuthoriseURLBuilder.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/AuthoriseURLBuilder.cs new file mode 100644 index 0000000..6b76957 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/AuthoriseURLBuilder.cs @@ -0,0 +1,181 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services; +using Microsoft.AspNetCore.WebUtilities; +using Serilog; +using static ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services.DataHolderAuthoriseService; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.APIs +{ + /// + /// Url for Authorise endpoint + /// + public class AuthoriseUrl + { + public string ClientId { get; private set; } + public string RedirectURI { get; private set; } + public string JwtCertificateFilename { get; private set; } = Constants.Certificates.JwtCertificateFilename; + public string JwtCertificatePassword { get; private set; } = Constants.Certificates.JwtCertificatePassword; + public string Scope { get; private set; } + public string ResponseType { get; private set; } + public string? Request { get; private set; } = null; // use this as the request, rather than build request + public string? RequestUri { get; private set; } = null; + + public string Url { get; private set; } + + /// + /// Lifetime (in seconds) of the access token. It has to be less than 60 mins + /// + public int TokenLifetime { get; private set; } = Constants.AuthServer.DefaultTokenLifetime; + + /// + /// Lifetime (in seconds) of the CDR arrangement. + /// 7776000 = 90 days + /// + public int? SharingDuration { get; private set; } = Constants.AuthServer.SharingDuration; + + private TestAutomationOptions TestAutomationOptions { get; set; } + + public class AuthoriseUrlBuilder : IBuilder + { + private readonly AuthoriseUrl _authoriseUrl = new AuthoriseUrl(); + + public AuthoriseUrlBuilder(TestAutomationOptions options) + { + _authoriseUrl.TestAutomationOptions = options ?? throw new ArgumentNullException(nameof(options)).Log(); + } + + public AuthoriseUrlBuilder WithClientId(string value) + { + _authoriseUrl.ClientId = value; + return this; + } + public AuthoriseUrlBuilder WithRedirectURI(string value) + { + _authoriseUrl.RedirectURI = value; + return this; + } + public AuthoriseUrlBuilder WithJWTCertificateFilename(string value) + { + _authoriseUrl.JwtCertificateFilename = value; + return this; + } + public AuthoriseUrlBuilder WithJWTCertificatePassword(string value) + { + _authoriseUrl.JwtCertificatePassword = value; + return this; + } + public AuthoriseUrlBuilder WithScope(string value) + { + _authoriseUrl.Scope = value; + return this; + } + public AuthoriseUrlBuilder WithResponseType(string value) + { + _authoriseUrl.ResponseType = value; + return this; + } + public AuthoriseUrlBuilder WithRequest(string value) + { + _authoriseUrl.Request = value; + return this; + } + public AuthoriseUrlBuilder WithRequestUri(string? value) + { + _authoriseUrl.RequestUri = value; + return this; + } + + public AuthoriseUrl Build() + { + Log.Information("Building a {BuiltClass} using {BuilderClass}.", nameof(AuthoriseUrl), nameof(AuthoriseUrlBuilder)); + + FillMissingDefaults(); + _authoriseUrl.Url = GenerateUrl(); + return _authoriseUrl; + } + + private string GenerateUrl() + { + var queryString = new Dictionary + { + { "client_id", _authoriseUrl.ClientId } + }; + + if (_authoriseUrl.ResponseType != null) + { + queryString.Add("response_type", _authoriseUrl.ResponseType); + } + + if (_authoriseUrl.RequestUri != null) + { + queryString.Add("request_uri", _authoriseUrl.RequestUri); + } + else + { + queryString.Add("request", _authoriseUrl.Request ?? CreateRequest()); + } + + var url = QueryHelpers.AddQueryString($"{_authoriseUrl.TestAutomationOptions.DH_TLS_AUTHSERVER_BASE_URL}/connect/authorize", queryString); + + return url; + } + + private void FillMissingDefaults() + { + Log.Information("Filling missing defaults for public properties"); + + var _options = _authoriseUrl.TestAutomationOptions; //purely to shorten reference + + //Anything that doesn't have a value which should...will get it's default value set + if (_authoriseUrl.Scope.IsNullOrWhiteSpace()) + { + _authoriseUrl.Scope = _options.SCOPE; + } + + if (_authoriseUrl.RedirectURI.IsNullOrWhiteSpace()) + { + _authoriseUrl.RedirectURI = _options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS; + } + + if (_authoriseUrl.ResponseType.IsNullOrWhiteSpace()) + { + _authoriseUrl.ResponseType = Enums.ResponseType.CodeIdToken.ToEnumMemberAttrValue(); + } + } + + private string CreateRequest() + { + var iat = new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds(); + + var subject = new Dictionary + { + { "iss", _authoriseUrl.ClientId }, + { "iat", iat }, + { "nbf", iat }, + { "exp", iat + _authoriseUrl.TokenLifetime }, + { "jti", Guid.NewGuid().ToString().Replace("-", string.Empty) }, + { "aud", _authoriseUrl.TestAutomationOptions.DH_TLS_AUTHSERVER_BASE_URL }, + { "response_type", _authoriseUrl.ResponseType }, + { "client_id", _authoriseUrl.ClientId }, + { "redirect_uri", _authoriseUrl.RedirectURI }, + { "scope", _authoriseUrl.Scope }, + { "state", "foo" }, + { "nonce", "foo" }, + { "claims", new { + sharing_duration = _authoriseUrl.SharingDuration.ToString(), + id_token = new { + acr = new { + essential = true, + values = new string[] { "urn:cds.au:cdr:2" } + } + } + }} + }; + + return Helpers.Jwt.CreateJWT(_authoriseUrl.JwtCertificateFilename, _authoriseUrl.JwtCertificatePassword, subject); + } + } + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Constants.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Constants.cs new file mode 100644 index 0000000..0e9c78b --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Constants.cs @@ -0,0 +1,357 @@ +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation +{ + public static class Constants + { + public const string Null = "***NULL***"; + public const string Omit = "***OMIT**"; + public const string GuidFoo = "f00f00f0-f00f-f00f-f00f-f00f00f00f00"; + + public const string AuthoriseOTP = "000789"; + + public const string ClientAssertionType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; + + public static class IdTypes + { + public const string IdTypeAccount = "ACCOUNT_ID"; + public const string IdTypeTransaction = "TRANSACTION_ID"; + } + + public static class AuthServer + { + public const int Days90 = 7776000; // 90 days + public const int SharingDuration = Days90; + + public const int DefaultTokenLifetime = 3600; + + public const string FapiPhase2CodeVerifier = "foo-bar"; + public const string FapiPhase2CodeChallengeMethod = "S256"; + + public const string ScopeTokenAccounts = "openid bank:accounts.basic:read"; + } + + /// + /// User/Customer IDs used for testing. Industry specific users are in an industry sub-class whereas the rest are at the first level + /// + public static class Users + { + /// + /// Banking-specific Users + /// + public static class Banking + { + public const string UserIdJaneWilson = "jwilson"; //Banking customer + public const string CustomerIdJaneWilson = "bfb689fb-7745-45b9-bbaa-b21e00072447";//Banking customer + } + + /// + /// Energy-specific Users + /// + public static class Energy + { + public const string UserIdMaryMoss = "mmoss"; // Energy customer + public const string UserIdHeddaHare = "hhare"; // Energy customer + public const string CustomerIdMaryMoss = "db1ddad1-a033-4088-8d0f-c800ed462717"; // Energy customer + } + + public const string UserIdSteveKennedy = "sken"; + public const string UserIdDewayneSteve = "dsteve"; + public const string UserIdBusiness1 = "bis1"; + public const string UserIdBusiness2 = "bis2"; + public const string UserIdBeverage = "bev"; + public const string UserIdKamillaSmith = "ksmith"; + + public const string CustomerIdBusiness1 = "a97ba8d9-c89d-4126-a3b1-5aaa50f8dc5f"; + } + + public static class Accounts + { + /// + /// Banking-specific Accounts + /// + public static class Banking + { + public const string AccountIdJaneWilson = "98765988"; + public const string AccountIdsAllJaneWilson = "98765988,98765987"; //Banking customer + } + + /// + /// Energy-specific Accounts + /// + public static class Energy + { + public const string AccountIdMaryMoss = "0011223301"; // Energy customer + public const string AccountIdsAllMaryMoss = "0011223301,0011223302,0011223303,0011223304,0011223305,0011223306,0011223307,0011223308,0011223309,0011223310,0011223311,0011223312,0011223313,0011223314,0011223315,0011223316,0011223317,0011223318,0011223319,0011223320,0011223321"; // Energy customer + public const string AccountIdsSubsetMaryMoss = "0011223301,0011223302,0011223303"; // Energy customer + public const string AccountIdsAllHeddaHare = "4ee1a8db-13af-44d7-b54b-e94dff3df548"; // Energy customer + } + + + public const string AccountIdJohnSmith = "1122334455"; + public const string AccountIdKamillaSmith = "0000001"; + + public const string AccountIdsAllBusiness1 = "54676423"; + public const string AccountIdsAllBusiness2 = ""; + public const string AccountIdsAllBeverage = "835672345"; + public const string AccountIdsAllKamillaSmith = "0000001,0000002,0000003,0000004,0000005,0000006,0000007,0000008,0000009,0000010,1000001,1000002,1000003,1000004,1000005,1000006,1000007,1000008,1000009,1000010,2000001,2000002,2000003,2000004,2000005,2000006,2000007,2000008,2000009,2000010"; + public const string AccountIdsAllDewayneSmith = "96565987,1100002,96534987"; + public const string AccountIdsAllSteveKennedy = ""; + } + + public static class AccessTokens + { + // VSCode slows on excessively long lines, splitting string constant into smaller lines. + public const string DataHolderAccessTokenExpired = + @"eyJhbGciOiJQUzI1NiIsImtpZCI6IjdDNTcxNjU1M0U5QjEzMkVGMzI1QzQ5Q0EyMDc5NzM3MTk2QzAzREIiLCJ4NXQiOiJmRmNXVlQ2YkV5N3pKY1Njb2dlWE54bHNBOXMiLCJ0eXAiOi" + + @"JhdCtqd3QifQ.eyJuYmYiOjE2NTI2Njk5NDMsImV4cCI6MTY1MjY3MDI0MywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6ODEwMSIsImF1ZCI6ImNkcy1hdSIsImNsaWVudF9pZCI6ImM2M" + + @"zI3Zjg3LTY4N2EtNDM2OS05OWE0LWVhYWNkM2JiODIxMCIsImNsaWVudF9zb2Z0d2FyZV9pZCI6ImM2MzI3Zjg3LTY4N2EtNDM2OS05OWE0LWVhYWNkM2JiODIxMCIsImNsaWVudF9zb2Z" + + @"0d2FyZV9zdGF0ZW1lbnQiOiJleUpoYkdjaU9pSlFVekkxTmlJc0ltdHBaQ0k2SWpVME1rRTVRamt4TmpBd05EZzRNRGc0UTBRMFJEZ3hOamt4TmtFNVJqUTBPRGhFUkRJMk5URWlMQ0owZ" + + @"VhBaU9pSktWMVFpZlEuZXcwS0lDQWliR1ZuWVd4ZlpXNTBhWFI1WDJsa0lqb2dJakU0WWpjMVlUYzJMVFU0TWpFdE5HTTVaUzFpTkRZMUxUUTNNRGt5T1RGalpqQm1OQ0lzRFFvZ0lDSnN" + + @"aV2RoYkY5bGJuUnBkSGxmYm1GdFpTSTZJQ0pOYjJOcklGTnZablIzWVhKbElFTnZiWEJoYm5raUxBMEtJQ0FpYVhOeklqb2dJbU5rY2kxeVpXZHBjM1JsY2lJc0RRb2dJQ0pwWVhRaU9pQ" + + @"XhOalV5TmpZNU9USTRMQTBLSUNBaVpYaHdJam9nTVRZMU1qWTNNRFV5T0N3TkNpQWdJbXAwYVNJNklDSmpOREkzTXpjd1pEa3pOR0UwWTJObFlXUTBObU0xWWpGbE9EQmlaalZoTWlJc0R" + + @"Rb2dJQ0p2Y21kZmFXUWlPaUFpWm1aaU1XTTRZbUV0TWpjNVpTMDBOR1E0TFRrMlpqQXRNV0pqTXpSaE5tSTBNelptSWl3TkNpQWdJbTl5WjE5dVlXMWxJam9nSWsxdlkyc2dSbWx1WVc1a" + + @"lpTQlViMjlzY3lJc0RRb2dJQ0pqYkdsbGJuUmZibUZ0WlNJNklDSk5lVUoxWkdkbGRFaGxiSEJsY2lJc0RRb2dJQ0pqYkdsbGJuUmZaR1Z6WTNKcGNIUnBiMjRpT2lBaVFTQndjbTlrZFd" + + @"OMElIUnZJR2hsYkhBZ2VXOTFJRzFoYm1GblpTQjViM1Z5SUdKMVpHZGxkQ0lzRFFvZ0lDSmpiR2xsYm5SZmRYSnBJam9nSW1oMGRIQnpPaTh2Ylc5amEzTnZablIzWVhKbEwyMTVZblZrW" + + @"jJWMFlYQndJaXdOQ2lBZ0luSmxaR2x5WldOMFgzVnlhWE1pT2lCYkRRb2dJQ0FnSW1oMGRIQnpPaTh2Ykc5allXeG9iM04wT2prNU9Ua3ZZMjl1YzJWdWRDOWpZV3hzWW1GamF5SU5DaUF" + + @"nWFN3TkNpQWdJbXh2WjI5ZmRYSnBJam9nSW1oMGRIQnpPaTh2Ylc5amEzTnZablIzWVhKbEwyMTVZblZrWjJWMFlYQndMMmx0Wnk5c2IyZHZMbkJ1WnlJc0RRb2dJQ0owYjNOZmRYSnBJa" + + @"m9nSW1oMGRIQnpPaTh2Ylc5amEzTnZablIzWVhKbEwyMTVZblZrWjJWMFlYQndMM1JsY20xeklpd05DaUFnSW5CdmJHbGplVjkxY21raU9pQWlhSFIwY0hNNkx5OXRiMk5yYzI5bWRIZGh" + + @"jbVV2YlhsaWRXUm5aWFJoY0hBdmNHOXNhV041SWl3TkNpQWdJbXAzYTNOZmRYSnBJam9nSW1oMGRIQnpPaTh2Ykc5allXeG9iM04wT2prNU9UZ3ZhbmRyY3lJc0RRb2dJQ0p5WlhadlkyR" + + @"jBhVzl1WDNWeWFTSTZJQ0pvZEhSd2N6b3ZMMnh2WTJGc2FHOXpkRG81TURBeEwzSmxkbTlqWVhScGIyNGlMQTBLSUNBaWNtVmphWEJwWlc1MFgySmhjMlZmZFhKcElqb2dJbWgwZEhCek9" + + @"pOHZiRzlqWVd4b2IzTjBPamt3TURFaUxBMEtJQ0FpYzI5bWRIZGhjbVZmYVdRaU9pQWlZell6TWpkbU9EY3ROamczWVMwME16WTVMVGs1WVRRdFpXRmhZMlF6WW1JNE1qRXdJaXdOQ2lBZ" + + @"0luTnZablIzWVhKbFgzSnZiR1Z6SWpvZ0ltUmhkR0V0Y21WamFYQnBaVzUwTFhOdlpuUjNZWEpsTFhCeWIyUjFZM1FpTEEwS0lDQWljMk52Y0dVaU9pQWliM0JsYm1sa0lIQnliMlpwYkd" + + @"VZ1ltRnVhenBoWTJOdmRXNTBjeTVpWVhOcFl6cHlaV0ZrSUdKaGJtczZZV05qYjNWdWRITXVaR1YwWVdsc09uSmxZV1FnWW1GdWF6cDBjbUZ1YzJGamRHbHZibk02Y21WaFpDQmlZVzVyT" + + @"25CaGVXVmxjenB5WldGa0lHSmhibXM2Y21WbmRXeGhjbDl3WVhsdFpXNTBjenB5WldGa0lHTnZiVzF2YmpwamRYTjBiMjFsY2k1aVlYTnBZenB5WldGa0lHTnZiVzF2YmpwamRYTjBiMjF" + + @"sY2k1a1pYUmhhV3c2Y21WaFpDQmpaSEk2Y21WbmFYTjBjbUYwYVc5dUlHVnVaWEpuZVRwaFkyTnZkVzUwY3k1aVlYTnBZenB5WldGa0lHVnVaWEpuZVRwaFkyTnZkVzUwY3k1amIyNWpaW" + + @"E56YVc5dWN6cHlaV0ZrSWcwS2ZRLnUzU0RhSW1fR21SbDg2Yi1hU3B3OHBYNlhCaVp2VVJRWUFJU2QyVXY0MUNVZlZmYXRSN3RZWWlEaVZhVWt5c1FUSW5sazNnVE85Yk5pT0lqM041Z0l" + + @"kYmRQUXFSTmRVNTB5emxMN1RlVGhmc2JadTZsc0hVdVR6RWVUVlNBZzVMNGlPZk9OZndEYXRMSjBrTlR3b0ZPVWRZSmt4dnFLd0RJazl0Ymw4ZVpkbG1Rc21SQmNmN3oyMnBGUlQzaDJuT" + + @"FNMWndIaEU2ck9OTEhFbk5YSjhLbUpzaXBnWmtzcFJPRzZXanZMUVl6MEtNVVdxN01EV3Y1TklIdFhXTFBsWlFpNjBLSHJYYkhxcXJkN3ZmRTRMWDFrMFRkUi1UeDFZeHpoT2EteGdvUWx" + + @"qS1M2ck9qal9uWXl0el9WSDBTUWd5czVCX3ZOSjRtQk9qNi1JQlgzRmdSZyIsImNsaWVudF9sb2dvX3VyaSI6Imh0dHBzOi8vbW9ja3NvZnR3YXJlL215YnVkZ2V0YXBwL2ltZy9sb2dvL" + + @"nBuZyIsImNsaWVudF9qd2tzX3VyaSI6Imh0dHBzOi8vbG9jYWxob3N0Ojk5OTgvandrcyIsImNsaWVudF90b2tlbl9lbmRwb2ludF9hdXRoX21ldGhvZCI6InByaXZhdGVfa2V5X2p3dCI" + + @"sImNsaWVudF90b2tlbl9lbmRwb2ludF9hdXRoX3NpZ25pbmdfYWxnIjoiUFMyNTYiLCJjbGllbnRfaWRfdG9rZW5fZW5jcnlwdGVkX3Jlc3BvbnNlX2FsZyI6IlJTQS1PQUVQIiwiY2xpZ" + + @"W50X2lkX3Rva2VuX2VuY3J5cHRlZF9yZXNwb25zZV9lbmMiOiJBMjU2R0NNIiwiY2xpZW50X2lkX3Rva2VuX3NpZ25lZF9yZXNwb25zZV9hbGciOiJQUzI1NiIsImNsaWVudF9vcmdfaWQ" + + @"iOiJmZmIxYzhiYS0yNzllLTQ0ZDgtOTZmMC0xYmMzNGE2YjQzNmYiLCJjbGllbnRfb3JnX25hbWUiOiJNb2NrIEZpbmFuY2UgVG9vbHMiLCJjbGllbnRfcmV2b2NhdGlvbl91cmkiOiJod" + + @"HRwczovL2xvY2FsaG9zdDo5MDAxL3Jldm9jYXRpb24iLCJjbGllbnRfY2xpZW50X2lkX2lzc3VlZF9hdCI6IjE2NTI2Njk5MzgiLCJjbGllbnRfYXBwbGljYXRpb25fdHlwZSI6IndlYiI" + + @"sImNsaWVudF9wb2xpY3lfdXJpIjoiaHR0cHM6Ly9tb2Nrc29mdHdhcmUvbXlidWRnZXRhcHAvcG9saWN5IiwiY2xpZW50X3Rvc191cmkiOiJodHRwczovL21vY2tzb2Z0d2FyZS9teWJ1Z" + + @"GdldGFwcC90ZXJtcyIsImNsaWVudF9sZWdhbF9lbnRpdHlfaWQiOiIxOGI3NWE3Ni01ODIxLTRjOWUtYjQ2NS00NzA5MjkxY2YwZjQiLCJjbGllbnRfbGVnYWxfZW50aXR5X25hbWUiOiJ" + + @"Nb2NrIFNvZnR3YXJlIENvbXBhbnkiLCJjbGllbnRfcmVjaXBpZW50X2Jhc2VfdXJpIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6OTAwMSIsImNsaWVudF9zZWN0b3JfaWRlbnRpZmllcl91cmkiO" + + @"iJsb2NhbGhvc3QiLCJqdGkiOiI5V0U4cWJ0Ukhpa29neURTc1gySXJnIiwic29mdHdhcmVfaWQiOiJjNjMyN2Y4Ny02ODdhLTQzNjktOTlhNC1lYWFjZDNiYjgyMTAiLCJzZWN0b3JfaWR" + + @"lbnRpZmllcl91cmkiOiJsb2NhbGhvc3QiLCJjbmYiOnsieDV0I1MyNTYiOiI3MTVDREQwNEZGNzMzMkNDREE3NENERjlGQkVEMTZCRUJBNURENzQ0In0sInNjb3BlIjpbImNkcjpyZWdpc" + + @"3RyYXRpb24iXX0.gg7pSRRLYpM69cDBg83LijyKOvnHf79ySsUe007s4Yy0eFe0ALnA_sbdxyaeoARVc0Rftg0Mpck6PLE0u3zJAsuNm6tV4r3jVf5m38EbvYB5N8cl18Z04PjoKvhVKhZ" + + @"5yM3wHYM6_eelSvv0aWkptRYDDcVCa4_H93RmiPJt5RoUX2SR7lf8gdHM9fb-n1_OcIVdEDz9W6RUw1o3TFp5kh3xIlS_sIawJ5dGTztnj3VtI36d7qL59uPojPmUQ-OT22IZE-u_KZxAe" + + @"tUQhX0-IqUzdVsdTf2t9DNva2VRkK9Cdf2kCtqs17NGDlWceQ7IKR-U6qn9izNOYeM47Qhqa6MiROtrfe5Ja3p8vjnN72eEQ_XPd2bMVxkbyh0IrG9-5JCOolbjjnbZaxh4dIggfdY52JS" + + @"2-DLYhQnMnJtrVkKe1J212x8SVf7FKcNGY0OM4MLG3Gcl5S8EzQQuh464Nr-rPnec7SbrqjQ2xyn56s4Nhv5PfQ-VOqPQXOkyBPzH"; + + + // VSCode slows on excessively long lines, splitting string constant into smaller lines. + public const string ConsumerAccessTokenBankingExpired = + @"eyJhbGciOiJQUzI1NiIsImtpZCI6IjdDNTcxNjU1M0U5QjEzMkVGMzI1QzQ5Q0EyMDc5NzM3MTk2QzAzREIiLCJ4NXQiOiJmRmNXVlQ2YkV5N3pKY1Njb2dlWE54bHNBOXMiLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE2NTI2ODM2NDksImV4cCI6MTY1MjY4NzI0OSwiaXNzIjoiaHR0cHM6Ly9tb2NrLWRh" + + @"dGEtaG9sZGVyOjgwMDEiLCJhdWQiOiJjZHMtYXUiLCJjbGllbnRfaWQiOiJjNjMyN2Y4Ny02ODdhLTQzNjktOTlhNC1lYWFjZDNiYjgyMTAiLCJhdXRoX3RpbWUiOjE2NTI2ODM2NDksImlkcCI6ImxvY2FsIiwic2hhcmluZ19leHBpcmVzX2F0IjoxNjYwNDU5NjQ5LCJjZHJfYXJyYW5nZW1lbnRfaWQiOiI" + + @"xZjc4YTY3ZS0xZDhkLTRiNzctODI3OC04NTNiMmQ5NTM5YjMiLCJqdGkiOiItNWZ0ZTJ1azMwTGZsa1g2N2JFVUx3Iiwic29mdHdhcmVfaWQiOiJjNjMyN2Y4Ny02ODdhLTQzNjktOTlhNC1lYWFjZDNiYjgyMTAiLCJzZWN0b3JfaWRlbnRpZmllcl91cmkiOiJtb2NrLWRhdGEtaG9sZGVyLWludGVncmF0aW" + + @"9uLXRlc3RzIiwiYWNjb3VudF9pZCI6WyJiMjNtTnRTWGdwU0s2WVFsMGdOcE9NVUxlQUhFbnpRMW54YXJiTzlTUDl1NWpEbjNhOWw0RDV6azd3YndFcVFiIiwiVG1vNW96aStNeHU5cVNVQVJCeWpKMjh2VjJrM2srRVBGMHBicHJsODVFSDhJJTJGTnVaTERRb09TbW9NZTRGeVpkIl0sInN1YiI6IkgvK0VrU" + + @"0xWeE1nZjhwNXlJS3BScFNzTlRSZVk5VTZlYTNGWVZtUWFSL1ZwQ2pHVGpSenA4YTdDOXVjWEk3ak0iLCJjbmYiOnsieDV0I1MyNTYiOiI3MTVDREQwNEZGNzMzMkNDREE3NENERjlGQkVEMTZCRUJBNURENzQ0In0sInNjb3BlIjpbIm9wZW5pZCIsInByb2ZpbGUiLCJiYW5rOmFjY291bnRzLmJhc2ljOnJl" + + @"YWQiLCJiYW5rOnRyYW5zYWN0aW9uczpyZWFkIiwiY29tbW9uOmN1c3RvbWVyLmJhc2ljOnJlYWQiXSwiYW1yIjpbInB3ZCJdfQ.g_x-MZa6Lq_2m3D0DKKk9SEhHs2TUIF_3oyWf2E18873KI3q6YCom7zFYpIrxrtgmcW8jN1gSMZFx_b9FhvWXRCL48GFLgVmqUml6yPNLH9Oa95UziIS58ROSxkOkidaIEbM" + + @"CIfE-6jTl_VzYxE7G19STYYbC_zU3e8hkgDShdh-KpKHW_TgWd_gvHAwHYNJF8TeFnXiAZSOSd4bfO2v9hjDWRN1SA0O-dkZssfthNZxGCsBc0yJfGYi5887KsuWhH1EMTWcAXRAfImeRa6rSgvTZu9imFFzomzdHR5KVD_L5Dq0Q4JtAlu4TmT5RIMWmQEaz7G3JvTfMAxfXkBEqe2UNP4Bm7Npgat9eCH6SS1" + + @"daaIJExpJF9C61C1PuV7t1fDHH3pz30H2rBJWZ_gXZJc2xUvw9oAFiJC10iB4dfd1nLgRbFfMx-i84aOfPm6bH-IoTEk1iJ4Odm-FkI9S_MO1rlpYVcuEBCuUKri6PCYKzHU4_istqN8x7bQ_kQQf"; + + public const string ConsumerAccessTokenEnergyExpired = + @"eyJhbGciOiJQUzI1NiIsImtpZCI6IjdDNTcxNjU1M0U5QjEzMkVGMzI1QzQ5Q0EyMDc5NzM3MTk2QzAzREIiLCJ4NXQiOiJmRmNXVlQ2YkV5N3pKY1Njb2dlWE54bHNBOXMiLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE2NTI0Mjc2MTYsImV4cCI6MTY1MjQzMTIxNiwiaXNzIjoiaHR0cHM6Ly9tb2NrLWRh" + + @"dGEtaG9sZGVyLWVuZXJneTo4MTAxIiwiYXVkIjoiY2RzLWF1IiwiY2xpZW50X2lkIjoiYzYzMjdmODctNjg3YS00MzY5LTk5YTQtZWFhY2QzYmI4MjEwIiwiYXV0aF90aW1lIjoxNjUyNDI3NjE2LCJpZHAiOiJsb2NhbCIsImNkcl9hcnJhbmdlbWVudF9pZCI6ImNiMjRhNWYxLTFjMzYtNGFmMS04MzYyLTc4" + + @"ZmRlNWJhMjIyMCIsImp0aSI6IlJjY18teHhnUEtpN1hHWlh2S0lvVlEiLCJzb2Z0d2FyZV9pZCI6ImM2MzI3Zjg3LTY4N2EtNDM2OS05OWE0LWVhYWNkM2JiODIxMCIsInNlY3Rvcl9pZGVudGlmaWVyX3VyaSI6Im1vY2stZGF0YS1ob2xkZXItZW5lcmd5LWludGVncmF0aW9uLXRlc3RzIiwiYWNjb3VudF9p" + + @"ZCI6WyJqdWhJNWZiM1dGVVAlMkZkQmZpeDJYRkRWMWs4amxkRHpwTlphVWRoVzI2UG82TXZFWHFGZlhrNXNudUI3RGkwSHEiLCJqdWhJNWZiM1dGVVAlMkZkQmZpeDJYRkRWMWs4amxkRHpwTlphVWRoVzI2UHBseTZSRVZTc2N3WDJOJTJGK1BjeDk2ZCIsImlpcGx5bzJENGhyM25mN1AzY1hkdVJzNlE3USUy" + + @"RlhXUVJ0QXpkcG1MM3AwUG14MkxYdXM0bnl4WUdRN3JBeHNRNCIsImp1aEk1ZmIzV0ZVUCUyRmRCZml4MlhGRFYxazhqbGREenBOWmFVZGhXMjZQb0wlMkZURnd2bERWelFLdG1yV0FsSm83IiwiZCUyRnJzQ1dRbmV5TFNOUm1URm0lMkYrZGM0RUk4aEUxOXUzS0s2M2VYVVNEZDBXVlFKTnRqcEpYZWZtdUVpc" + + @"DRiOWMiLCJqdWhJNWZiM1dGVVAlMkZkQmZpeDJYRkRWMWs4amxkRHpwTlphVWRoVzI2UG9DcGdMelV2cCtUSitrTzdjQkJRK20iLCJqdWhJNWZiM1dGVVAlMkZkQmZpeDJYRkRWMWs4amxkRHpwTlphVWRoVzI2UG9DQkVWVTJHZkc4NWpNU2doR3FPWXUiLCJhMEdYTzhzZStNZHZTRDBtbXZUVnJCTkZ4ZXJzRH" + + @"NKV3dHTlFRS3pvUVBoMU03N2dXQVNNU0c3Z3RZUUZZbDdkIiwiMG9uT3FQYVQrZGtYR0RrZzc1a1ZFREhtT3l3S1dVbW9sbmdhMFNaT0RTTEhoRXYlMkZWV3BjNVVHNjhiaExUdExjIiwianVoSTVmYjNXRlVQJTJGZEJmaXgyWEZEVjFrOGpsZER6cE5aYVVkaFcyNlBvV3RLdjROR2FWRlVJNmFHbFN2UVBVIiwi" + + @"anVoSTVmYjNXRlVQJTJGZEJmaXgyWEZEVjFrOGpsZER6cE5aYVVkaFcyNlBvR0xBNWt5cU1xVTNzZEkwZm1ZVWlxIiwianVoSTVmYjNXRlVQJTJGZEJmaXgyWEZEVjFrOGpsZER6cE5aYVVkaFcyNlBwTU9BbkpGY2tkRm5hU2MxNUVjUUJNIiwiaWlwbHlvMkQ0aHIzbmY3UDNjWGR1UnM2UTdRJTJGWFdRUnRBemRw" + + @"bUwzcDBQMTBPZjVlR0ZxNlFWM3paMFlBcmt4IiwianVoSTVmYjNXRlVQJTJGZEJmaXgyWEZEVjFrOGpsZER6cE5aYVVkaFcyNlBwaElpTzhRczAyV0RzWVhPRWg1SyUyRk0iLCJkJTJGcnNDV1FuZXlMU05SbVRGbSUyRitkYzRFSThoRTE5dTNLSzYzZVhVU0RkMHpFV3JjNDVqeHN1RmU3aGxJQjFqViIsImp1aEk1Z" + + @"mIzV0ZVUCUyRmRCZml4MlhGRFYxazhqbGREenBOWmFVZGhXMjZQb1FiOUthVlFXbnJqNVNkbHJucmc2OSIsImp1aEk1ZmIzV0ZVUCUyRmRCZml4MlhGRFYxazhqbGREenBOWmFVZGhXMjZQcHBtUG5BWk5pVEtDa3dyRWtKQ0x6YSIsImEwR1hPOHNlK01kdlNEMG1tdlRWckJORnhlcnNEc0pXd0dOUVFLem9RUGlXO" + + @"E1OK2l1cm1RNVE2MmVpdm93TUgiLCIwb25PcVBhVCtka1hHRGtnNzVrVkVESG1PeXdLV1Vtb2xuZ2EwU1pPRFNKNm13QktlTUVkWkFQaWRsOHIzY2dlIiwianVoSTVmYjNXRlVQJTJGZEJmaXgyWEZEVjFrOGpsZER6cE5aYVVkaFcyNlBxNFFaK3FSMXRwdEpERjYlMkZiSTJkWkgiLCJqdWhJNWZiM1dGVVAlMkZkQm" + + @"ZpeDJYRkRWMWs4amxkRHpwTlphVWRoVzI2UHJkWjlxY080MG1HdHpySHFCZElOT3giXSwic3ViIjoicEF2YklpNC8vcGY5ZXIwMkxReG14eWx0UTRlcGduY0FPSFM2UmlmYXo5MVVXakVJOS9QQlJwdlk5SlNGaGhMZSIsImNuZiI6eyJ4NXQjUzI1NiI6IjcxNUNERDA0RkY3MzMyQ0NEQTc0Q0RGOUZCRUQxNkJFQkE" + + @"1REQ3NDQifSwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsImVuZXJneTphY2NvdW50cy5iYXNpYzpyZWFkIiwiZW5lcmd5OmFjY291bnRzLmNvbmNlc3Npb25zOnJlYWQiLCJjb21tb246Y3VzdG9tZXIuYmFzaWM6cmVhZCJdLCJhbXIiOlsicHdkIl19.Mk9S5KSRfY04to3GrfP941afq__ac1owJv-6G-w9s6H" + + @"p02xUhOdZ8uTIRJMPKLJ_rcaL9gD3ONqNg_OpScbdnObaSwFanbnL9qiMo2MpyyWwjvlQaSUB8rr1iqOoooJ4HIg5P47XVdbNgy3_TlELd0f97pFUksLdjKfNO9WYZePd1HUg4oOmDpch4WPyvsG2bB3tVL29P8ffRWbj76Cg9PMJaPf97NKLXKrdwMW6WWOFa4vbPEOXTIFp9d6bGjhDXV3mgl3Kt49PQOJx8xAlh0ssU" + + @"YzdIPNmwplq6J2SwOGcwz8dU-iSYc-cMlLnu3lftFNqnONVahMKXlKZoZPeUEdrWFhBoEro7_-EbiZhRfzAvFLTM__GtaVMnAUfsQ5GMtwVvTY17LWPUihnOCds6TJg3avMS31wi0OkMDb7HHkYHAjXvWrlvv7-8TyTbsEesNlyaam-DYDMkXE7vHD_5sMPA_8R0WRxiNV3X0rMHM9kBogbkwwG4mtsdd8fafWv"; + } + + public static class Certificates + { + // Certificate used by DataRecipient for the MTLS connection with Dataholder + public const string DataHolderMtlsCertificateFilename = "Certificates/server.pfx"; + public const string DataholderMtlsCertificatePassword = "#M0ckDataHolder#"; + + // Certificate used by DataRecipient to sign client assertions + public const string CertificateFilename = "Certificates/client.pfx"; + public const string CertificatePassword = "#M0ckDataRecipient#"; + + public const string JwtCertificateFilename = "Certificates/MDR/jwks.pfx"; + public const string JwtCertificatePassword = "#M0ckDataRecipient#"; + + // An additional certificate used by DataRecipient to sign client assertions + public const string AdditionalCertificateFilename = "Certificates/client-additional.pfx"; + public const string AdditionalCertificatePassword = CertificatePassword; + + public const string InvalidCertificateFilename = "Certificates/client-invalid.pfx"; + public const string InvalidCertificatePassword = "#M0ckDataRecipient#"; + + public const string DataHolderCertificateFilename = "Certificates/mock-data-holder.pfx"; + public const string DataHolderCertificatePassword = "#M0ckDataHolder#"; + + public const string AdditionalJwksCertificateFilename = AdditionalCertificateFilename; + public const string AdditionalJwksCertificatePassword = AdditionalCertificatePassword; + } + + public static class LegalEntities + { + public const string LegalEntityId = "18B75A76-5821-4C9E-B465-4709291CF0F4"; + } + + public static class Brands + { + public const string BrandId = "FFB1C8BA-279E-44D8-96F0-1BC34A6B436F"; + public const string AdditionalBrandId = "20C0864B-CEEF-4DE0-8944-EB0962F825EB"; + } + + public static class SoftwareProducts + { + public const string SoftwareProductId = "c6327f87-687a-4369-99a4-eaacd3bb8210"; + public const string InvalidSoftwareProductId = "f00f00f0-f00f-f00f-f00f-f00f00f00f00"; + public const string AdditionalSoftwareProductId = "9381DAD2-6B68-4879-B496-C1319D7DFBC9"; + + public const string SoftwareProductSectorIdentifierUri = "api.mocksoftware"; + } + + public static class Scopes + { + // Scope + public const string ScopeBanking = "openid profile common:customer.basic:read bank:accounts.basic:read bank:transactions:read"; //Also used by Auth Server + public const string ScopeBankingWithoutOpenId = "profile common:customer.basic:read bank:accounts.basic:read bank:transactions:read"; //Also used by Auth Server + public const string ScopeEnergy = "openid profile common:customer.basic:read energy:accounts.basic:read energy:accounts.concessions:read"; // scope of 'energy:accounts.concessions:read' is not working? + public const string ScopeEnergyWithoutOpenId = "profile common:customer.basic:read energy:accounts.basic:read energy:accounts.concessions:read"; // scope of 'energy:accounts.concessions:read' is not working?; + public const string ScopeRegistration = "cdr:registration"; + } + + public static class IdPermanence + { + public const string IdPermanencePrivateKey = "90733A75F19347118B3BE0030AB590A8"; + } + + public static class ErrorMessages + { + public static class Par + { + public const string ParRequestUriFormParameterNotSupported = "ERR-PAR-001: request_uri form parameter is not supported"; + public const string ParRequestIsNotWellFormedJwt = "ERR-PAR-002: request is not a well-formed JWT"; + public const string UnsupportedChallengeMethod = "ERR-PAR-003: Unsupported code_challenge_method"; + public const string CodeChallengeInvalidLength = "ERR-PAR-004: Invalid code_challenge - invalid length"; + public const string CodeChallengeMissing = "ERR-PAR-005: code_challenge is missing"; + public const string RequestObjectJwtRequestUriNotSupported = "ERR-PAR-006: request_uri is not supported in request object"; + public const string RequestObjectJwtRedirectUriMissing = "ERR-PAR-007: redirect_uri missing from request object JWT"; + public const string RequesObjectJwtClientIdMismatch = "ERR-PAR-008: client_id does not match client_id in request object JWT"; + public const string MissingResponseMode = "ERR-PAR-009: response_mode is missing or not set to 'jwt' for response_type of 'code'"; + public const string ResponseTypeNotRegistered = "ERR-PAR-010: response_type is not registered for the client. Check client registration metadata."; + } + + public static class Mtls + { + public const string MtlsMultipleThumbprints = "ERR-MTLS-001: Multiple certificate thumbprints found on request"; + public const string MtlsNoCertificate = "ERR-MTLS-002: No client certificate was found on request"; + } + + public static class Authorization + { + public const string AuthorizationHolderOfKeyCheckFailed = "ERR-AUTH-001: Holder of Key check failed"; + public const string AuthorizationAccessTokenExpired = "ERR-AUTH-002: Access Token check failed - it has been revoked"; + public const string AuthorizationInsufficientScope = "ERR-AUTH-003: Insufficent scope"; //This doesn't match the one in AuthServer because it had no additional text + public const string RequestUriClientIdMismatch = "ERR-AUTH-0004: client_id does not match request_uri client_id"; + public const string RequestUriAlreadyUsed = "ERR-AUTH-005: request_uri has already been used"; + public const string ExpiredRequestUri = "ERR-AUTH-006: request_uri has expired"; + public const string InvalidRequestUri = "ERR-AUTH-007: Invalid request_uri"; + public const string MissingRequestUri = "ERR-AUTH-008: request_uri is missing"; + public const string AccessDenied = "ERR-AUTH-009: User cancelled the authorisation flow"; + } + + public static class ClientAssertion + { + public const string ClientAssertionTypeNotProvided = "ERR-CLIENT_ASSERTION-002: client_assertion_type not provided"; + public const string InvalidClientAssertionType = "ERR-CLIENT_ASSERTION-003: client_assertion_type must be urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; + public const string ClientAssertionClientIdMismatch = "ERR-CLIENT_ASSERTION-004: client_id does not match client_assertion"; + public const string ClientAssertionInvalidFormat = "ERR-CLIENT_ASSERTION-005: Cannot read client_assertion. Invalid format."; + public const string ClientAssertionNotReadable = "ERR-CLIENT_ASSERTION-006: Cannot read client_assertion"; + public const string ClientAssertionSubjectIssNotSetToClientId = "ERR-CLIENT_ASSERTION-007: Invalid client_assertion - 'sub' and 'iss' must be set to the client_id"; + public const string ClientAssertionSubjectIssNotSameValue = "ERR-CLIENT_ASSERTION-008: Invalid client_assertion - 'sub' and 'iss' must have the same value"; + public const string ClientAssertionMissingIssClaim = "ERR-CLIENT_ASSERTION-009: Invalid client_assertion - Missing 'iss' claim"; + } + + public static class CdrArrangement + { + public const string InvalidConsentCdrArrangement = "ERR-ARR-001: Invalid Consent Arrangement"; + } + public static class Jwt + { + public const string JwtInvalidAudience = "ERR-JWT-001: {0} - Invalid audience"; + public const string JwtExpired = "ERR-JWT-002: {0} has expired"; + public const string JwksError = "ERR-JWT-003: {0} - jwks error"; + public const string JwtValidationErro = "ERR-JWT-004: {0} - token validation error"; + } + + public static class Dcr + { + public const string DuplicateRegistration = "ERR-DCR-001: Duplicate registrations for a given software_id are not valid."; + public const string EmptyRegistrationRequest = "ERR-DCR-002: Registration request is empty"; + public const string RegistrationRequestInvalidRedirectUri = "ERR-DCR-003: The redirect_uri '{0}' is not valid as it is not included in the software_statement"; + public const string RegistrationRequestValidationFailed = "ERR-DCR-004: Client Registration Request validation failed."; + public const string SsaValidationFailed = "ERR-DCR-005: SSA validation failed."; + public const string SoftwareStatementEmptyOrInvalid = "ERR-DCR-006: The software_statement is empty or invalid"; + public const string InvalidSectorIdentifierUri = "ERR-DCR-007: Invalid sector_identifier_uri"; + } + public static class Token + { + public const string ExpiredRefreshToken = "ERR-TKN-001: refresh_token has expired"; + public const string InvalidRefreshToken = "ERR-TKN-002: refresh_token is invalid"; + public const string MissingRefreshToken = "ERR-TKN-003: refresh_token is missing"; + public const string InvalidCodeVerifier = "ERR-TKN-004: Invalid code_verifier"; + public const string AuthorizationCodeExpired = "ERR-TKN-005: authorization code has expired"; + public const string MissingCodeVerifier = "ERR-TKN-006: code_verifier is missing"; + public const string InvalidAuthorizationCode = "ERR-TKN-007: authorization code is invalid"; + } + public static class General + { + public const string SoftwareProductNotFound = "ERR-GEN-001: Software product not found"; + public const string SoftwareProductStatusInactive = "ERR-GEN-002: Software product status is {0}"; + public const string SoftwareProductRemoved = "ERR-GEN-003: Software product status is removed - consents cannot be revoked"; + public const string ClientNotFound = "ERR-GEN-004: Client not found"; + public const string MissingClientId = "ERR-GEN-005: client_id is missing"; + public const string InvalidClientId = "ERR-GEN-006: Invalid client_id"; + public const string InvalidRedirectUri = "ERR-GEN-007: Invalid redirect_uri for client"; + public const string MissingResponseType = "ERR-GEN-008: response_type is missing"; + public const string UnsupportedResponseType = "ERR-GEN-009: response_type is not supported"; + public const string ResponseTypeMismatchRequestUriResponseType = "ERR-GEN-010: response_type does not match request_uri response_type"; + public const string MissingScope = "ERR-GEN-011: scope is missing"; + public const string MissingOpenIdScope = "ERR-GEN-012: openid scope is missing"; + public const string UnsupportedResponseMode = "ERR-GEN-013: response_mode is not supported"; //This was INVALID_RESPONSE_MODE in auth server + public const string GrantTypeNotProvided = "ERR-GEN-014: grant_type not provided"; + public const string UnsupportedGrantType = "ERR-GEN-015: unsupported grant_type"; + public const string MissingIssuerClaim = "ERR-GEN-016: Missing iss claim"; + public const string JtiRequired = "ERR-GEN-017: Invalid client_assertion - 'jti' is required"; + public const string JtiNotUnique = "ERR-GEN-018: Invalid client_assertion - 'jti' must be unique"; + public const string ClientAssertionNotProvided = "ERR-GEN-019: client_assertion not provided"; + public const string InvalidJwksUri = "ERR-GEN-020: Invalid jwks_uri in SSA"; + public const string UnableToLoadJwksDataRecipient = "ERR-GEN-021: Could not load JWKS from Data Recipient endpoint: {0}"; + public const string UnableToLoadJwksFromRegister = "ERR-GEN-022: Could not load SSA JWKS from Register endpoint: {0}"; + public const string MissingExp = "ERR-GEN-023: Invalid request - exp is missing"; + public const string MissingNbf = "ERR-GEN-024: Invalid request - nbf is missing"; + public const string ExpiryGreaterThan60AfterNbf = "ERR-GEN-025: Invalid request - exp claim cannot be longer than 60 minutes after the nbf claim"; + public const string InvalidResponseModeForResponseType = "ERR-GEN-026: Invalid response_mode for response_type"; + public const string ScopeTooLong = "ERR-GEN-027: Invalid scope - scope is too long"; + public const string InvalidClaims = "ERR-GEN-028: Invalid claims in request object"; + public const string InvalidCdrArrangementId = "ERR-GEN-029: Invalid cdr_arrangement_id"; + public const string InvalidNonce = "ERR-GEN-030: Invalid nonce"; + public const string InvalidTokenRequest = "ERR-GEN-031: invalid token request"; + public const string MissingGrantType = "ERR-GEN-032: grant_type is missing"; + public const string ClientIdMismatch = "ERR-GEN-033: client_id does not match"; + public const string UnableToRetrieveClientMetadata = "ERR-GEN-034: Could not retrieve client metadata"; + public const string MissingCode = "ERR-GEN-035: code is missing"; + public const string MissingRedirectUri = "ERR-GEN-036: redirect_uri is missing"; + public const string RedirectUriAuthorizationRequestMismatch = "ERR-GEN-037: redirect_uri does not match authorization request"; + public const string InvalidClient = "ERR-GEN-038: invalid_client"; + } + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.csproj b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.csproj new file mode 100644 index 0000000..a3bc614 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.csproj @@ -0,0 +1,93 @@ + + + net6.0 + ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation + + Shared test automation code for Consumer Data Right mock solutions. + Copyright (c) 2023 Commonwealth of Australia + LICENSE + https://github.com/ConsumerDataRight/mock-solution-test-automation + cdr-package-logo.png + README.md + cdr;consumer-data-right;open-banking;open-energy + https://github.com/ConsumerDataRight/mock-solution-test-automation/releases + https://github.com/ConsumerDataRight/mock-solution-test-automation + git + true + true + True + Debug;Release;Shared + enable + enable + Library + True + True + snupkg + Embedded + True + true + true + + + + + preview + detailed + false + + 1591,8618,8602,8603,8604,8767 + + + true + + + + True + \ + false + + + + + True + \ + false + + + + + True + \ + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/DataRecipientConsentCallback.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/DataRecipientConsentCallback.cs new file mode 100644 index 0000000..208c35b --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/DataRecipientConsentCallback.cs @@ -0,0 +1,130 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation +{ + public class DataRecipientConsentCallback + { + public DataRecipientConsentCallback(string redirectUrl) + { + RedirectUrl = redirectUrl; + + Request = new CallbackRequest + { + PathAndQuery = new Uri(redirectUrl).PathAndQuery + }; + } + + public string RedirectUrl { get; init; } + private string RedirectUrlLeftPart => new Uri(RedirectUrl).GetLeftPart(UriPartial.Authority); + + private IWebHost? _host; + + public class CallbackRequest + { + public string? PathAndQuery { get; init; } + public bool received = false; + public HttpMethod? method; + public string? body; + } + private CallbackRequest Request { get; init; } + + /// + /// Start web host + /// + public void Start() + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(Start), nameof(DataRecipientConsentCallback)); + + _host = new WebHostBuilder() + .ConfigureServices(s => { s.AddSingleton(typeof(CallbackRequest), Request); }) + .UseKestrel() + .UseStartup() + .UseUrls(RedirectUrlLeftPart) + .Build(); + + _host.RunAsync(); + } + + /// + /// Stop web host + /// + public async Task Stop() + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(Stop), nameof(DataRecipientConsentCallback)); + + if (_host != null) + { + await _host.StopAsync(); + } + } + + /// + /// Wait until we get a callback or otherwise timeout + /// + public async Task WaitForCallback(int timeoutSeconds = 30) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(WaitForCallback), nameof(DataRecipientConsentCallback)); + + var stopAt = DateTime.Now.AddSeconds(timeoutSeconds); + + // Keep checking until we timeout + while (DateTime.Now < stopAt) + { + // Have we received the callback? + if (Request.received) + { + // Yes, so return the content + return Request; + } + + // Otherwise wait another second + await Task.Delay(1000); + } + + return null; // Timed out + } + + class DataRecipientConsentCallbackStartup + { + readonly CallbackRequest _callbackRequest; + + public DataRecipientConsentCallbackStartup(CallbackRequest callbackRequest) + { + _callbackRequest = callbackRequest; + } + + public void Configure(IApplicationBuilder app) + { + app.UseHttpsRedirection(); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGet(_callbackRequest.PathAndQuery!, async context => + { + var body = await new StreamReader(context.Request.Body).ReadToEndAsync(); + _callbackRequest.method = HttpMethod.Get; + _callbackRequest.body = body; + _callbackRequest.received = true; + }); + + endpoints.MapPost(_callbackRequest.PathAndQuery!, async context => + { + var body = await new StreamReader(context.Request.Body).ReadToEndAsync(); + _callbackRequest.method = HttpMethod.Post; + _callbackRequest.body = body; + _callbackRequest.received = true; + }); + }); + } + + public static void ConfigureServices(IServiceCollection services) + { + services.AddRouting(); + } + } + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/CdsErrors.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/CdsErrors.cs new file mode 100644 index 0000000..2f98e70 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/CdsErrors.cs @@ -0,0 +1,85 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums +{ + public enum CdsError + { + [CdrError("Expected Error Encountered", "urn:au-cds:error:cds-all:GeneralError/Expected")] + ExpectedError, + + [CdrError("Unexpected Error Encountered", "urn:au-cds:error:cds-all:GeneralError/Unexpected")] + UnexpectedError, + + [CdrError("Service Unavailable", "urn:au-cds:error:cds-all:Service/Unavailable")] + ServiceUnavailable, + + [CdrError("Missing Required Field", "urn:au-cds:error:cds-all:Field/Missing")] + MissingRequiredField, + + [CdrError("Missing Required Header", "urn:au-cds:error:cds-all:Header/Missing")] + MissingRequiredHeader, + + [CdrError("Invalid Field", "urn:au-cds:error:cds-all:Field/Invalid")] + InvalidField, + + [CdrError("Invalid Header", "urn:au-cds:error:cds-all:Header/Invalid")] + InvalidHeader, + + [CdrError("Invalid Date", "urn:au-cds:error:cds-all:Field/InvalidDateTime")] + InvalidDate, + + [CdrError("Invalid Page Size", "urn:au-cds:error:cds-all:Field/InvalidPageSize")] + InvalidPageSize, + + [CdrError("Invalid Version", "urn:au-cds:error:cds-all:Header/InvalidVersion")] + InvalidVersion, + + [CdrError("ADR Status Is Not Active", "urn:au-cds:error:cds-all:Authorisation/AdrStatusNotActive")] + ADRStatusIsNotActive, + + [CdrError("Consent Is Revoked", "urn:au-cds:error:cds-all:Authorisation/RevokedConsent")] + ConsentIsRevoked, + + [CdrError("Consent Is Invalid", "urn:au-cds:error:cds-all:Authorisation/InvalidConsent")] + ConsentIsInvalid, + + [CdrError("Resource Not Implemented", "urn:au-cds:error:cds-all:Resource/NotImplemented")] + ResourceNotImplemented, + + [CdrError("Resource Not Found", "urn:au-cds:error:cds-all:Resource/NotFound")] + ResourceNotFound, + + [CdrError("Unsupported Version", "urn:au-cds:error:cds-all:Header/UnsupportedVersion")] + UnsupportedVersion, + + [CdrError("Invalid Consent Arrangement", "urn:au-cds:error:cds-all:Authorisation/InvalidArrangement")] + InvalidConsentArrangement, + + [CdrError("Invalid Page", "urn:au-cds:error:cds-all:Field/InvalidPage")] + InvalidPage, + + [CdrError("Invalid Resource", "urn:au-cds:error:cds-all:Resource/Invalid")] + InvalidResource, + + [CdrError("Unavailable Resource", "urn:au-cds:error:cds-all:Resource/Unavailable")] + UnavailableResource, + + [CdrError("Invalid Brand", "urn:au-cds:error:cds-register:Field/InvalidBrand")] + InvalidBrand, + + [CdrError("Invalid Industry", "urn:au-cds:error:cds-register:Field/InvalidIndustry")] + InvalidIndustry, + + [CdrError("Invalid Software Product", "urn:au-cds:error:cds-register:Field/InvalidSoftwareProduct")] + InvalidSoftwareProduct, + + [CdrError("Invalid Energy Account","urn:au-cds:error:cds-energy:Authorisation/InvalidEnergyAccount")] + InvalidEnergyAccount, + + [CdrError("Invalid Banking Account", "urn:au-cds:error:cds-banking:Authorisation/InvalidBankingAccount")] + InvalidBankingAccount, + + [CdrError("Unavailable Banking Account", "urn:au-cds:error:cds-banking:Authorisation/UnavailableBankingAccount")] + UnavailableBankingAccount, + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/EntityType.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/EntityType.cs new file mode 100644 index 0000000..75b7bd8 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/EntityType.cs @@ -0,0 +1,4 @@ +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums +{ + public enum EntityType { SOFTWAREPRODUCT, LEGALENTITY, BRAND } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/Industry.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/Industry.cs new file mode 100644 index 0000000..dca173b --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/Industry.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums +{ + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum Industry + { + [EnumMember(Value = "Banking")] + BANKING, + + [EnumMember(Value = "Energy")] + ENERGY, + + [EnumMember(Value = "All")] + ALL, + } +} + + diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/LegalEntityStatus.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/LegalEntityStatus.cs new file mode 100644 index 0000000..437bb6b --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/LegalEntityStatus.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums +{ + public enum LegalEntityStatus + { + [EnumMember(Value = "ACTIVE")] + ACTIVE, + [EnumMember(Value = "INACTIVE")] + INACTIVE, + [EnumMember(Value = "REMOVED")] + REMOVED + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/ResponseMode.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/ResponseMode.cs new file mode 100644 index 0000000..7c211de --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/ResponseMode.cs @@ -0,0 +1,31 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums +{ + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum ResponseMode + { + [EnumMember(Value = "form_post")] + FormPost, + + [EnumMember(Value = "fragment")] + Fragment, + + [EnumMember(Value = "query")] + Query, + [EnumMember(Value = "jwt")] + Jwt, + [EnumMember(Value = "form_post.jwt")] + FormPostJwt, + + [EnumMember(Value = "fragment.jwt")] + FragmentJwt, + + [EnumMember(Value = "query.jwt")] + QueryJwt, + + [EnumMember(Value = "foo")] //Nonsense value used for testing + TestOnlyFoo + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/ResponseType.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/ResponseType.cs new file mode 100644 index 0000000..0d3405f --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/ResponseType.cs @@ -0,0 +1,30 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums +{ + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum ResponseType + { + [EnumMember(Value = "code id_token")] + CodeIdToken, + [EnumMember(Value = "code")] + Code, + + //Nonsense values used for testing + [EnumMember(Value = "Foo")] + TestOnlyFoo, + [EnumMember(Value = "token")] + TestOnlyToken, + [EnumMember(Value = "code token")] + TestOnlyCodeToken, + [EnumMember(Value = "id_token token")] + TestOnlyIdTokenToken, + [EnumMember(Value = "code id_token token")] + TestOnlyCodeIdTokenToken, + [EnumMember(Value = "code Foo")] + TestOnlyCodeFoo, + [EnumMember(Value = "code id_token Foo")] + TestOnlyCodeIdTokenFooo, + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/SoftwareProductStatus.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/SoftwareProductStatus.cs new file mode 100644 index 0000000..0ea40a6 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/SoftwareProductStatus.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums +{ + public enum SoftwareProductStatus + { + [EnumMember(Value = "ACTIVE")] + ACTIVE, + [EnumMember(Value = "INACTIVE")] + INACTIVE, + [EnumMember(Value = "REMOVED")] + REMOVED + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/Status.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/Status.cs new file mode 100644 index 0000000..b85906f --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/Status.cs @@ -0,0 +1,4 @@ +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums +{ + public enum Status { STATUS, LEGALENTITYSTATUS, BRANDSTATUS } //STATUS - SoftwareProduct Status, rest of the status as name implies +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/TokenType.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/TokenType.cs new file mode 100644 index 0000000..544f5a6 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Enums/TokenType.cs @@ -0,0 +1,20 @@ +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums +{ + public enum TokenType + { + /// + /// Used for energy + /// + MaryMoss, + /// + /// Used for energy + /// + HeddaHare,//used for energy + /// + /// Used for banking + /// + JaneWilson, + + SteveKennedy, DewayneSteve, Business1, Beverage, InvalidFoo, InvalidEmpty, InvalidOmit, KamillaSmith, Business2 + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseException.cs new file mode 100644 index 0000000..164f56c --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseException.cs @@ -0,0 +1,44 @@ +using System; +using System.Net; +using System.Runtime.Serialization; +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions +{ + [Serializable] + public class AuthoriseException : Exception + { + public HttpStatusCode StatusCode { get; } + + [JsonProperty("error")] + public string Error { get; } + + [JsonProperty("error_description")] + public string ErrorDescription { get; } + + public AuthoriseException() { } + + public AuthoriseException(string message) + : base(message) { } + + public AuthoriseException(string message, Exception inner) + : base(message, inner) { } + + public AuthoriseException(string message, HttpStatusCode statusCode, string error, string errorDescription) + : this(message) + { + StatusCode = statusCode; + Error = error; + ErrorDescription = errorDescription; + } + + protected AuthoriseException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/ExpiredRequestUriException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/ExpiredRequestUriException.cs new file mode 100644 index 0000000..5e666ac --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/ExpiredRequestUriException.cs @@ -0,0 +1,21 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions +{ + [Serializable] + public class ExpiredRequestUriException : AuthoriseException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + public ExpiredRequestUriException() + : base(string.Empty, System.Net.HttpStatusCode.BadRequest, "invalid_request_uri", Constants.ErrorMessages.Authorization.ExpiredRequestUri) + { + } + + protected ExpiredRequestUriException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidClientException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidClientException.cs new file mode 100644 index 0000000..c0f95f9 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidClientException.cs @@ -0,0 +1,119 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions +{ + [Serializable] + public class InvalidClientException : AuthoriseException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + public InvalidClientException(string detail) + : base(string.Empty, System.Net.HttpStatusCode.BadRequest, "invalid_client", detail) + { } + + protected InvalidClientException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class ClientNotFoundException : InvalidClientException + { + public ClientNotFoundException() : base(Constants.ErrorMessages.General.ClientNotFound) + { } + + protected ClientNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class MissingClientAssertionException : InvalidClientException + { + public MissingClientAssertionException() : base(Constants.ErrorMessages.General.ClientAssertionNotProvided) + { } + + protected MissingClientAssertionException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class MissingClientAssertionTypeException : InvalidClientException + { + public MissingClientAssertionTypeException() : base(Constants.ErrorMessages.ClientAssertion.ClientAssertionTypeNotProvided) + { } + + protected MissingClientAssertionTypeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class InvalidClientAssertionTypeException : InvalidClientException + { + public InvalidClientAssertionTypeException() : base(Constants.ErrorMessages.ClientAssertion.InvalidClientAssertionType) + { } + + protected InvalidClientAssertionTypeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class InvalidClientAssertionFormatException : InvalidClientException + { + public InvalidClientAssertionFormatException() : base(Constants.ErrorMessages.ClientAssertion.ClientAssertionInvalidFormat) + { } + + protected InvalidClientAssertionFormatException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class MissingIssClaimException : InvalidClientException + { + public MissingIssClaimException() : base(Constants.ErrorMessages.ClientAssertion.ClientAssertionMissingIssClaim) + { } + + protected MissingIssClaimException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class MissingJtiException : InvalidClientException + { + public MissingJtiException() : base(Constants.ErrorMessages.General.JtiRequired) + { } + + protected MissingJtiException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class TokenValidationClientAssertionException : InvalidClientException + { + public TokenValidationClientAssertionException() : base(Constants.ErrorMessages.Jwt.JwtValidationErro.Replace("{0}","client_assertion")) + { } + + protected TokenValidationClientAssertionException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class InvalidAudienceException : InvalidClientException + { + public InvalidAudienceException() : base(Constants.ErrorMessages.Jwt.JwtInvalidAudience.Replace("{0}", "client_assertion")) + { } + + protected InvalidAudienceException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class ExpiredClientAssertionException : InvalidClientException + { + public ExpiredClientAssertionException() : base(Constants.ErrorMessages.Jwt.JwtExpired.Replace("{0}", "client_assertion")) + { } + + protected ExpiredClientAssertionException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidClientMetadataException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidClientMetadataException.cs new file mode 100644 index 0000000..e42f6a6 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidClientMetadataException.cs @@ -0,0 +1,69 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions +{ + [Serializable] + public class InvalidClientMetadataException : AuthoriseException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + public InvalidClientMetadataException(string detail) + : base(string.Empty, System.Net.HttpStatusCode.BadRequest, "invalid_client_metadata", detail) + { } + + protected InvalidClientMetadataException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class AuthorizationSignedResponseAlgClaimInvalidException : InvalidClientMetadataException + { + public AuthorizationSignedResponseAlgClaimInvalidException() : base("The 'authorization_signed_response_alg' claim value must be one of 'PS256,ES256'.")//TODO: Should a error gen code and add to constants. Bug 64146 + { } + + protected AuthorizationSignedResponseAlgClaimInvalidException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class AuthorizationSignedResponseAlgClaimMissingException : InvalidClientMetadataException + { + public AuthorizationSignedResponseAlgClaimMissingException() : base("The 'authorization_signed_response_alg' claim is missing.")//TODO: Should a error gen code and add to constants. Bug 64146 + { } + + protected AuthorizationSignedResponseAlgClaimMissingException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class TokenEndpointAuthSigningAlgClaimInvalidException : InvalidClientMetadataException + { + public TokenEndpointAuthSigningAlgClaimInvalidException() : base("The 'token_endpoint_auth_signing_alg' claim value must be one of 'PS256,ES256'.")//TODO: Should a error gen code and add to constants. Bug 64146 + { } + + protected TokenEndpointAuthSigningAlgClaimInvalidException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class DuplicateRegistrationForSoftwareIdException : InvalidClientMetadataException + { + public DuplicateRegistrationForSoftwareIdException() : base(Constants.ErrorMessages.Dcr.DuplicateRegistration) + { } + + protected DuplicateRegistrationForSoftwareIdException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class GrantTypesMissingAuthorizationCodeException : InvalidClientMetadataException + { + public GrantTypesMissingAuthorizationCodeException() : base("The 'grant_types' claim value must contain the 'authorization_code' value.")//TODO: Should a error gen code and add to constants. Bug 64146 + { } + + protected GrantTypesMissingAuthorizationCodeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidGrantException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidGrantException.cs new file mode 100644 index 0000000..ab7a767 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidGrantException.cs @@ -0,0 +1,59 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions +{ + [Serializable] + public class InvalidGrantException : AuthoriseException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + public InvalidGrantException(string detail) + : base(string.Empty, System.Net.HttpStatusCode.BadRequest, "invalid_grant", detail) + { } + + protected InvalidGrantException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class ExpiredAuthorizationCodeException : InvalidGrantException + { + public ExpiredAuthorizationCodeException() : base(Constants.ErrorMessages.Token.AuthorizationCodeExpired) + { } + + protected ExpiredAuthorizationCodeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class InvalidAuthorizationCodeException : InvalidGrantException + { + public InvalidAuthorizationCodeException() : base(Constants.ErrorMessages.Token.InvalidAuthorizationCode) + { } + + protected InvalidAuthorizationCodeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class MissingRefreshTokenException : InvalidGrantException + { + public MissingRefreshTokenException() : base(Constants.ErrorMessages.Token.MissingRefreshToken) + { } + + protected MissingRefreshTokenException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class InvalidRefreshTokenException : InvalidGrantException + { + public InvalidRefreshTokenException() : base(Constants.ErrorMessages.Token.InvalidRefreshToken) + { } + + protected InvalidRefreshTokenException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidKeyHolderException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidKeyHolderException.cs new file mode 100644 index 0000000..10339e0 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidKeyHolderException.cs @@ -0,0 +1,19 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions +{ + [Serializable] + public class InvalidKeyHolderException : AuthoriseException + { + /// + /// Initializes a new instance of the class. + /// Status code: 401 (Unauthorized). + /// + public InvalidKeyHolderException() + : base(string.Empty, System.Net.HttpStatusCode.Unauthorized, "invalid_token", Constants.ErrorMessages.Authorization.AuthorizationHolderOfKeyCheckFailed) + { } + + protected InvalidKeyHolderException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidRedirectUriException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidRedirectUriException.cs new file mode 100644 index 0000000..5cc4b40 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidRedirectUriException.cs @@ -0,0 +1,19 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions +{ + [Serializable] + public class InvalidRedirectUriException : AuthoriseException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + public InvalidRedirectUriException(string redirectUri) + : base(string.Empty, System.Net.HttpStatusCode.BadRequest, "invalid_redirect_uri", Constants.ErrorMessages.Dcr.RegistrationRequestInvalidRedirectUri.Replace("{0}",redirectUri)) + { } + + protected InvalidRedirectUriException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidRequestException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidRequestException.cs new file mode 100644 index 0000000..43d677e --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidRequestException.cs @@ -0,0 +1,121 @@ +using System.Net; +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions +{ + [Serializable] + public class InvalidRequestException : AuthoriseException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + public InvalidRequestException(string detail) + : base(string.Empty, System.Net.HttpStatusCode.BadRequest, "invalid_request", detail) + { } + + /// + /// Initializes a new instance of the class. + /// Status code is variable to handle for any non-400 cases like: 302 (Redirect). + /// + public InvalidRequestException(string detail, HttpStatusCode statusCode) + : base(string.Empty, statusCode, "invalid_request", detail) + { } + + protected InvalidRequestException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class UnsupportedRequestUriFormParameterException : InvalidRequestException + { + public UnsupportedRequestUriFormParameterException() : base(Constants.ErrorMessages.Par.ParRequestUriFormParameterNotSupported) + { } + + protected UnsupportedRequestUriFormParameterException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class UnsupportedResponseModeException : InvalidRequestException + { + public UnsupportedResponseModeException() : base(Constants.ErrorMessages.General.UnsupportedResponseMode) + { } + + protected UnsupportedResponseModeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class UnsupportedResponseTypeException : InvalidRequestException + { + public UnsupportedResponseTypeException() : base(Constants.ErrorMessages.General.UnsupportedResponseType) + { } + + protected UnsupportedResponseTypeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class MissingResponseTypeException : InvalidRequestException + { + public MissingResponseTypeException() : base(Constants.ErrorMessages.General.MissingResponseType) + { } + + protected MissingResponseTypeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class MissingOpenIdScopeException : InvalidRequestException + { + public MissingOpenIdScopeException() : base("OpenID Connect requests MUST contain the openid scope value.") //TODO: Should a error gen code and add to constants. Bug 64146 + { } + + protected MissingOpenIdScopeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class MissingAuthorizationCodeException : InvalidRequestException + { + public MissingAuthorizationCodeException() : base(Constants.ErrorMessages.General.MissingCode) + { } + + protected MissingAuthorizationCodeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class InvalidRedirectUriForClientException : InvalidRequestException + { + public InvalidRedirectUriForClientException() : base(Constants.ErrorMessages.General.InvalidRedirectUri) + { } + + protected InvalidRedirectUriForClientException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + #region 302Redirects + + [Serializable] + public class UnsupportedResponseTypeRedirectException : InvalidRequestException + { + public UnsupportedResponseTypeRedirectException() : base("Unsupported response_type", HttpStatusCode.Redirect)//TODO: Should a error gen code and add to constants. Bug 64146 + { } + + protected UnsupportedResponseTypeRedirectException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class InvalidClientIdRedirectException : InvalidRequestException + { + public InvalidClientIdRedirectException() : base("Invalid client ID.", HttpStatusCode.Redirect)//TODO: Should a error gen code and add to constants. Bug 64146 + { } + + protected InvalidClientIdRedirectException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + #endregion +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidRequestObjectException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidRequestObjectException.cs new file mode 100644 index 0000000..ad6b281 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidRequestObjectException.cs @@ -0,0 +1,89 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions +{ + [Serializable] + public class InvalidRequestObjectException : AuthoriseException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + public InvalidRequestObjectException(string detail) + : base(string.Empty, System.Net.HttpStatusCode.BadRequest, "invalid_request_object", detail) + { } + + protected InvalidRequestObjectException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class TokenValidationRequestException : InvalidRequestObjectException + { + public TokenValidationRequestException() : base(Constants.ErrorMessages.Jwt.JwtValidationErro.Replace("{0}","request")) + { } + + protected TokenValidationRequestException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class ExpiredRequestException : InvalidRequestObjectException + { + public ExpiredRequestException() : base(Constants.ErrorMessages.Jwt.JwtExpired.Replace("{0}", "request")) + { } + + protected ExpiredRequestException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class InvalidExpClaimException : InvalidRequestObjectException + { + public InvalidExpClaimException() : base(Constants.ErrorMessages.General.ExpiryGreaterThan60AfterNbf) + { } + + protected InvalidExpClaimException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class InvalidResponseModeForResponseTypeException : InvalidRequestObjectException + { + public InvalidResponseModeForResponseTypeException() : base(Constants.ErrorMessages.General.InvalidResponseModeForResponseType) + { } + + protected InvalidResponseModeForResponseTypeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class InvalidJwtException : InvalidRequestObjectException + { + public InvalidJwtException() : base("Invalid JWT request")//TODO: Should a error gen code and add to constants. Bug 64146 + { } + + protected InvalidJwtException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class InvalidArrangementIdException : InvalidRequestObjectException + { + public InvalidArrangementIdException() : base(Constants.ErrorMessages.General.InvalidCdrArrangementId) + { } + + protected InvalidArrangementIdException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class MissingNbfClaimException : InvalidRequestObjectException + { + public MissingNbfClaimException() : base(Constants.ErrorMessages.General.MissingNbf) + { } + + protected MissingNbfClaimException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidScopeException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidScopeException.cs new file mode 100644 index 0000000..ef5254e --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidScopeException.cs @@ -0,0 +1,19 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions +{ + [Serializable] + public class InvalidScopeException : AuthoriseException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + public InvalidScopeException() + : base(string.Empty, System.Net.HttpStatusCode.BadRequest, "invalid_scope", "Additional scopes were requested in the refresh_token request") //TODO: Should a error gen code and add to constants. Bug 64146 + { } + + protected InvalidScopeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidSoftwareStatementException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidSoftwareStatementException.cs new file mode 100644 index 0000000..9c39fb7 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/InvalidSoftwareStatementException.cs @@ -0,0 +1,19 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions +{ + [Serializable] + public class InvalidSoftwareStatementException : AuthoriseException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + public InvalidSoftwareStatementException() + : base(string.Empty, System.Net.HttpStatusCode.BadRequest, "invalid_software_statement", Constants.ErrorMessages.Dcr.SsaValidationFailed) + { } + + protected InvalidSoftwareStatementException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/UnsupportedGrantTypeException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/UnsupportedGrantTypeException.cs new file mode 100644 index 0000000..127e944 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/AuthoriseExceptions/UnsupportedGrantTypeException.cs @@ -0,0 +1,34 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions +{ + [Serializable] + public class UnsupportedGrantTypeException : AuthoriseException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + public UnsupportedGrantTypeException(string detail) + : base(string.Empty, System.Net.HttpStatusCode.BadRequest, "unsupported_grant_type", detail) + { } + + public UnsupportedGrantTypeException() + : base(string.Empty, System.Net.HttpStatusCode.BadRequest, "unsupported_grant_type", Constants.ErrorMessages.General.UnsupportedGrantType) + { } + + protected UnsupportedGrantTypeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + + [Serializable] + public class MissingGrantTypeException : UnsupportedGrantTypeException + { + public MissingGrantTypeException() : base(Constants.ErrorMessages.General.GrantTypeNotProvided) + { } + + protected MissingGrantTypeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } + +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdrErrorExtensions.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdrErrorExtensions.cs new file mode 100644 index 0000000..857564b --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdrErrorExtensions.cs @@ -0,0 +1,38 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions +{ + public static class CdrErrorExtensions + { + private static readonly Dictionary _errors = InitErrors(); + + public static CdsErrorInfo GetErrorInfo(this CdsError error) + { + if (_errors.TryGetValue(error, out var res)) + { + return res; + } + + throw new ArgumentOutOfRangeException($"Error {error} doesn't have any Register error attribute"); + } + + private static Dictionary InitErrors() + { + var result = new Dictionary(); + foreach (var i in Enum.GetValues(typeof(CdsError))) + { + var error = Enum.Parse(i.ToString()); + var attr = error.GetAttribute(); + + if (attr != null) + { + result.Add(error, new CdsErrorInfo { Title = attr.Title, ErrorCode = attr.ErrorCode }); + } + } + + return result; + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdrException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdrException.cs new file mode 100644 index 0000000..c0c0a49 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdrException.cs @@ -0,0 +1,52 @@ +using System; +using System.Net; +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions +{ + [Serializable] + public class CdrException : Exception + { + private readonly string _code; + private readonly string _title; + private readonly string _detail; + private readonly HttpStatusCode _statusCode; + + public CdrException(string code, string title, string detail, HttpStatusCode statusCode, string? message) + : base(message) + { + _code = code; + _title = title; + _detail = detail; + _statusCode = statusCode; + } + + public CdrException(CdsError cdsError, string detail, HttpStatusCode statusCode, string? message) + : base(message) + { + var errorInfo = cdsError.GetErrorInfo(); + _code = errorInfo.ErrorCode; + _title = errorInfo.Title; + _detail = detail; + _statusCode = statusCode; + } + + protected CdrException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + } + + public string Code { get => _code; } + + public string Title { get => _title; } + + public string Detail { get => _detail; } + + public HttpStatusCode StatusCode { get => _statusCode; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdrValidationException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdrValidationException.cs new file mode 100644 index 0000000..c2ed358 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdrValidationException.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; + +namespace Register.Common.Exceptions +{ + [Serializable] + public class CdrValidationException : Exception, ICdrValidationException + { + public CdrValidationException() + { + } + + public CdrValidationException(string message) + : base(message) + { + } + + public CdrValidationException(string message, List items, List validationErrors) + : base(message) + { + Items = items; + ValidationErrors = validationErrors; + } + + protected CdrValidationException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + } + + public List Items { get; set; } + + public List ValidationErrors { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/AdrStatusNotActiveException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/AdrStatusNotActiveException.cs new file mode 100644 index 0000000..8d5badf --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/AdrStatusNotActiveException.cs @@ -0,0 +1,35 @@ +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions +{ + [Serializable] + public class AdrStatusNotActiveException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 403 (Forbidden). + /// + /// + /// + public AdrStatusNotActiveException(string detail, string message) + : base(CdsError.ADRStatusIsNotActive, detail, System.Net.HttpStatusCode.Forbidden, message) + { } + + public AdrStatusNotActiveException(string detail) + : base(CdsError.ADRStatusIsNotActive, detail, System.Net.HttpStatusCode.Forbidden, null) + { } + + public AdrStatusNotActiveException(SoftwareProductStatus status) + : base(CdsError.ADRStatusIsNotActive, Constants.ErrorMessages.General.SoftwareProductStatusInactive.Replace("{0}", status.ToEnumMemberAttrValue()), System.Net.HttpStatusCode.Forbidden, null) + { } + + public AdrStatusNotActiveException(LegalEntityStatus status) + : base(CdsError.ADRStatusIsNotActive, Constants.ErrorMessages.General.SoftwareProductStatusInactive.Replace("{0}", status.ToEnumMemberAttrValue()), System.Net.HttpStatusCode.Forbidden, null) //TODO: Should the message be Legal Entity Status instead of Software Product? Noted in Bug 63710 + { } + + protected AdrStatusNotActiveException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidArrangementException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidArrangementException.cs new file mode 100644 index 0000000..ba33d02 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidArrangementException.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions +{ + [Serializable] + public class InvalidArrangementException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 422 (Unprocessable Entity). + /// + /// + /// + public InvalidArrangementException(string detail, string message) + : base(CdsError.InvalidConsentArrangement, detail, System.Net.HttpStatusCode.UnprocessableEntity, message) + { } + + public InvalidArrangementException(string detail) + : base(CdsError.InvalidConsentArrangement, detail, System.Net.HttpStatusCode.UnprocessableEntity, null) + { } + + protected InvalidArrangementException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidConsentException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidConsentException.cs new file mode 100644 index 0000000..29f6fa4 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidConsentException.cs @@ -0,0 +1,31 @@ +using System.Net; +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions +{ + [Serializable] + public class InvalidConsentException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 403 (Forbidden). + /// + /// + /// + public InvalidConsentException(string detail, string message) + : base(CdsError.ConsentIsInvalid, detail, HttpStatusCode.Forbidden, message) + { } + + public InvalidConsentException(string detail) + : base(CdsError.ConsentIsInvalid, detail, HttpStatusCode.Forbidden, null) + { } + + public InvalidConsentException() + : base(CdsError.ConsentIsInvalid, "The authorised consumer's consent is insufficient to execute the resource", HttpStatusCode.Forbidden, null) + { } + + protected InvalidConsentException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidEnergyAccountException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidEnergyAccountException.cs new file mode 100644 index 0000000..c5364ff --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidEnergyAccountException.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions +{ + [Serializable] + public class InvalidEnergyAccountException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 404 (Not Found). + /// + /// + /// + public InvalidEnergyAccountException(string detail, string message) + : base(CdsError.InvalidEnergyAccount, detail, System.Net.HttpStatusCode.NotFound, message) + { } + + public InvalidEnergyAccountException(string detail) + : base(CdsError.InvalidEnergyAccount, detail, System.Net.HttpStatusCode.NotFound, null) + { } + + protected InvalidEnergyAccountException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidFieldException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidFieldException.cs new file mode 100644 index 0000000..e98f53b --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidFieldException.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions +{ + [Serializable] + public class InvalidFieldException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + /// + /// + public InvalidFieldException(string detail, string message) + : base(CdsError.InvalidField, detail, System.Net.HttpStatusCode.BadRequest, message) + { } + + public InvalidFieldException(string detail) + : base(CdsError.InvalidField, detail, System.Net.HttpStatusCode.BadRequest, null) + { } + + protected InvalidFieldException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidHeaderException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidHeaderException.cs new file mode 100644 index 0000000..680d762 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidHeaderException.cs @@ -0,0 +1,27 @@ +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions +{ + [Serializable] + public class InvalidHeaderException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + /// + /// + public InvalidHeaderException(string detail, string message) + : base(CdsError.InvalidHeader, detail, System.Net.HttpStatusCode.BadRequest, message) + { + } + + public InvalidHeaderException(string detail) + : base(CdsError.InvalidHeader, detail, System.Net.HttpStatusCode.BadRequest, null) + { } + + protected InvalidHeaderException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidPageException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidPageException.cs new file mode 100644 index 0000000..d6a2d71 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidPageException.cs @@ -0,0 +1,27 @@ +using System.Net; +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions +{ + [Serializable] + public class InvalidPageException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 422 (Unprocessable Entity). + /// + /// + /// + public InvalidPageException(string detail, string message) + : base(CdsError.InvalidPage, detail, HttpStatusCode.UnprocessableEntity, message) + { } + + public InvalidPageException(string detail) + : base(CdsError.InvalidPage, detail, HttpStatusCode.UnprocessableEntity, null) + { } + + protected InvalidPageException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidPageSizeException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidPageSizeException.cs new file mode 100644 index 0000000..b8d7a0a --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidPageSizeException.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions +{ + [Serializable] + public class InvalidPageSizeException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + /// + /// + public InvalidPageSizeException(string detail, string message) + : base(CdsError.InvalidPageSize, detail, System.Net.HttpStatusCode.BadRequest, message) + { } + + public InvalidPageSizeException(string detail) + : base(CdsError.InvalidPageSize, detail, System.Net.HttpStatusCode.BadRequest, null) + { } + + protected InvalidPageSizeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidVersionException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidVersionException.cs new file mode 100644 index 0000000..9deb52d --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/InvalidVersionException.cs @@ -0,0 +1,30 @@ +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions +{ + [Serializable] + public class InvalidVersionException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + /// + /// + public InvalidVersionException(string detail, string message) + : base(CdsError.InvalidVersion, detail, System.Net.HttpStatusCode.BadRequest, message) + { } + + public InvalidVersionException(string detail) + : base(CdsError.InvalidVersion, detail, System.Net.HttpStatusCode.BadRequest, null) + { } + + public InvalidVersionException() + : base(CdsError.InvalidVersion, "Version is not a positive Integer.", System.Net.HttpStatusCode.BadRequest, null) + { } + + protected InvalidVersionException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/MissingRequiredFieldException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/MissingRequiredFieldException.cs new file mode 100644 index 0000000..af7d823 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/MissingRequiredFieldException.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions +{ + [Serializable] + public class MissingRequiredFieldException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + /// + /// + public MissingRequiredFieldException(string detail, string message) + : base(CdsError.MissingRequiredField, detail, System.Net.HttpStatusCode.BadRequest, message) + { } + + public MissingRequiredFieldException(string detail) + : base(CdsError.MissingRequiredField, detail, System.Net.HttpStatusCode.BadRequest, null) + { } + + protected MissingRequiredFieldException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/MissingRequiredHeaderException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/MissingRequiredHeaderException.cs new file mode 100644 index 0000000..357fa55 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/MissingRequiredHeaderException.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions +{ + [Serializable] + public class MissingRequiredHeaderException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 400 (Bad Request). + /// + /// + /// + public MissingRequiredHeaderException(string detail, string message) + : base(CdsError.MissingRequiredHeader, detail, System.Net.HttpStatusCode.BadRequest, message) + { } + + public MissingRequiredHeaderException(string detail) + : base(CdsError.MissingRequiredHeader, detail, System.Net.HttpStatusCode.BadRequest, null) + { } + + protected MissingRequiredHeaderException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/ResourceNotFoundException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/ResourceNotFoundException.cs new file mode 100644 index 0000000..15e1f4a --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/ResourceNotFoundException.cs @@ -0,0 +1,31 @@ +using System.Net; +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions +{ + [Serializable] + public class ResourceNotFoundException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 404 (Not Found). + /// + /// + /// + public ResourceNotFoundException(string detail, string message) + : base(CdsError.ResourceNotFound, detail, HttpStatusCode.NotFound, message) + { } + + public ResourceNotFoundException(string detail) + : base(CdsError.ResourceNotFound, detail, HttpStatusCode.NotFound, null) + { } + + public ResourceNotFoundException() + : base(CdsError.ResourceNotFound, "The authorised consumer's consent is insufficient to execute the resource", HttpStatusCode.NotFound, null) + { } + + protected ResourceNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/UnavailableBankingAccountException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/UnavailableBankingAccountException.cs new file mode 100644 index 0000000..a36f462 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/UnavailableBankingAccountException.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions +{ + [Serializable] + public class UnavailableBankingAccountException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 422 (Unprocessable Entity). + /// + /// + /// + public UnavailableBankingAccountException(string detail, string message) + : base(CdsError.UnavailableBankingAccount, detail, System.Net.HttpStatusCode.UnprocessableEntity, message) + { } + + public UnavailableBankingAccountException(string detail) + : base(CdsError.UnavailableBankingAccount, detail, System.Net.HttpStatusCode.UnprocessableEntity, null) + { } + + protected UnavailableBankingAccountException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/UnsupportedVersionException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/UnsupportedVersionException.cs new file mode 100644 index 0000000..fd30298 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/CdsExceptions/UnsupportedVersionException.cs @@ -0,0 +1,30 @@ +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions +{ + [Serializable] + public class UnsupportedVersionException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 406 (Not Acceptable). + /// + /// + /// + public UnsupportedVersionException(string detail, string message) + : base(CdsError.UnsupportedVersion, detail, System.Net.HttpStatusCode.NotAcceptable, message) + { } + + public UnsupportedVersionException(string detail) + : base(CdsError.UnsupportedVersion, detail, System.Net.HttpStatusCode.NotAcceptable, null) + { } + + public UnsupportedVersionException() + : base(CdsError.UnsupportedVersion, "Version requested is lower than the minimum version or greater than maximum version.", System.Net.HttpStatusCode.NotAcceptable, null) + { } + + protected UnsupportedVersionException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/DataHolderAuthoriseIncorrectCustomerIdException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/DataHolderAuthoriseIncorrectCustomerIdException.cs new file mode 100644 index 0000000..13713c2 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/DataHolderAuthoriseIncorrectCustomerIdException.cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions +{ + [Serializable] + public class DataHolderAuthoriseIncorrectCustomerIdException : Exception + { + protected DataHolderAuthoriseIncorrectCustomerIdException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/DataHolderAuthoriseIncorrectOneTimePasswordException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/DataHolderAuthoriseIncorrectOneTimePasswordException.cs new file mode 100644 index 0000000..3ea1223 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/DataHolderAuthoriseIncorrectOneTimePasswordException.cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions +{ + [Serializable] + public class DataHolderAuthoriseIncorrectOneTimePasswordException : Exception + { + protected DataHolderAuthoriseIncorrectOneTimePasswordException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/InvalidTokenException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/InvalidTokenException.cs new file mode 100644 index 0000000..3704e88 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/InvalidTokenException.cs @@ -0,0 +1,19 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions +{ + [Serializable] + public class InvalidTokenException : CdrException + { + /// + /// Initializes a new instance of the class. + /// Status code: 401 (Unauthorized). + /// + public InvalidTokenException() + : base("401", "Unauthorized", "invalid_token", System.Net.HttpStatusCode.Unauthorized, null) + { } + + protected InvalidTokenException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/PageElementNotFoundException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/PageElementNotFoundException.cs new file mode 100644 index 0000000..6485dd1 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Exceptions/PageElementNotFoundException.cs @@ -0,0 +1,15 @@ +using System.Runtime.Serialization; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UI.Pages.Authorisation; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions +{ + [Serializable] + public class PageElementNotFoundException : Exception + { + public PageElementNotFoundException(string page, string selector) : base($"{page} page element could not be found using selector: {selector}") + { } + + protected PageElementNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/DateTimeExtensions.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/DateTimeExtensions.cs new file mode 100644 index 0000000..b45a206 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/DateTimeExtensions.cs @@ -0,0 +1,26 @@ +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions +{ + public static class DateTimeExtensions + { + public static string? GetDateFromFapiDate(string? XFapiAuthDate) + { + return XFapiAuthDate switch + { + "DateTime.UtcNow" => DateTime.UtcNow.ToString(), + "DateTime.UtcNow+1" => DateTime.UtcNow.AddDays(1).ToString(), + "DateTime.Now.RFC1123" => DateTime.Now.ToUniversalTime().ToString("r"), + "DateTime.Now.RFC1123+1" => DateTime.Now.AddDays(1).ToUniversalTime().ToString("r"), + "foo" or "000" or "" or null => XFapiAuthDate, + _ => throw new ArgumentOutOfRangeException(nameof(XFapiAuthDate)).Log() + }; + } + + /// + /// Return datetime converted to Unix Epoch (number of seconds since 00:00:00 UTC on 1 Jan 1970) + /// + public static int UnixEpoch(this DateTime datetime) + { + return Convert.ToInt32(datetime.Subtract(DateTime.UnixEpoch).TotalSeconds); + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/EnumHelper.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/EnumHelper.cs new file mode 100644 index 0000000..ead7127 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/EnumHelper.cs @@ -0,0 +1,68 @@ +using System.Runtime.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions +{ + public static class EnumHelper + { + public static TEnum ToEnum(this string enumText, Exception? exception = null) + where TEnum : struct + { + if ((!Enum.TryParse(enumText, true, out var res) || !Enum.IsDefined(typeof(TEnum), res)) && exception != null) + { + throw exception; + } + + return res; + } + + public static IEnumerable GetAllValues() + where TEnum : struct, Enum + { + foreach (var i in Enum.GetValues(typeof(TEnum))) + { + yield return (int)i; + } + } + + public static IEnumerable GetAllMembers() + where TEnum : struct, Enum + { + foreach (var i in Enum.GetValues(typeof(TEnum))) + { + var item = Enum.Parse(i.ToString()); + yield return item.ToEnumMemberAttrValue(); + } + } + + public static string ToEnumMemberAttrValue(this Enum @enum) + { + var attr = + @enum.GetType().GetMember(@enum.ToString()).FirstOrDefault()?. + GetCustomAttributes(false).OfType(). + FirstOrDefault(); + + if (attr != null) + { + return attr?.Value ?? throw new InvalidOperationException($"Unable to get {nameof(EnumMemberAttribute)} value for enum: {@enum}"); + } + else + { + return @enum.ToString(); + } + } + + public static T? GetAttribute(this Enum @enum) + { + var attr = @enum?.GetType()?.GetMember(@enum.ToString())?.FirstOrDefault() + ?.GetCustomAttributes(false) + ?.OfType(); + + if (attr == null) + { + throw new InvalidOperationException($"Unable to get {nameof(T)} attribute for enum: {@enum}"); + } + + return attr.FirstOrDefault(); + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/ExceptionExtensions.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/ExceptionExtensions.cs new file mode 100644 index 0000000..2a05906 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/ExceptionExtensions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions +{ + public static class ExceptionExtensions + { + public static Exception Log(this Exception ex) + { + Serilog.Log.Error(ex.Message); + return ex; + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/JsonExtensions.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/JsonExtensions.cs new file mode 100644 index 0000000..fed8995 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/JsonExtensions.cs @@ -0,0 +1,70 @@ +using System.Text.Json; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions +{ + public static class JsonExtensions + { + public static void WriteJsonToFile(string filename, string json) + { + Log.Information("Calling {FunctionName} with Params: {P1}={V1},{P2}={V2}", nameof(WriteJsonToFile), nameof(filename), filename, nameof(json), json); + //TODO: Expect this is the System.Text.Json.JsonSerializer way to replace Newtonsoft, if we decide to do it (model property attributes would need to change) + var options = new JsonSerializerOptions + { + WriteIndented = true + }; + + var jsonObj = System.Text.Json.JsonSerializer.Deserialize(json); + var jsonStr = System.Text.Json.JsonSerializer.Serialize(jsonObj, options); + + //var jsonObj = JsonConvert.DeserializeObject(json); + //var jsonStr = JsonConvert.SerializeObject(jsonObj, Formatting.Indented); + File.WriteAllText(filename, jsonStr); + } + + /// + /// Strip comments from json string. + /// The json will be reserialized so it's formatting may change (ie whitespace/indentation etc) + /// + public static string JsonStripComments(this string json) + { + var options = new JsonSerializerOptions + { + ReadCommentHandling = JsonCommentHandling.Skip, + WriteIndented = true + }; + + var jsonObject = System.Text.Json.JsonSerializer.Deserialize(json, options); + + return System.Text.Json.JsonSerializer.Serialize(jsonObject); + } + + /// + /// Compare json. + /// Json is converted to JTokens prior to comparision, thus formatting is ignore. + /// Returns true if json is equivalent, otherwise false. + /// + public static bool JsonCompare(this string json, string jsonToCompare) + { + Log.Information("Calling {FunctionName} with Params: {P1}={V1},{P2}={V2}", nameof(JsonCompare), nameof(json), json, nameof(jsonToCompare), jsonToCompare); + + var jsonToken = JToken.Parse(json); + var jsonToCompareToken = JToken.Parse(jsonToCompare); + return JToken.DeepEquals(jsonToken, jsonToCompareToken); + } + + public static async Task DeserializeResponseAsync(HttpResponseMessage response) + { + var responseContent = await response.Content.ReadAsStringAsync(); + + if (string.IsNullOrEmpty(responseContent)) + { + return default(T); + } + + return JsonConvert.DeserializeObject(responseContent); + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/JwtSecurityTokenExtensions.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/JwtSecurityTokenExtensions.cs new file mode 100644 index 0000000..f776c4a --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/JwtSecurityTokenExtensions.cs @@ -0,0 +1,46 @@ +using FluentAssertions; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions +{ + static public class JwtSecurityTokenExtensions + { + /// + /// Get claim for claimType. Throws exception if no claim or multiple claims (ie must be a single claim for claimType). + /// + /// + static public Claim Claim(this JwtSecurityToken jwt, string claimType) + => jwt.Claims.Single(claim => claim.Type == claimType); + + + /// + /// Assert JWT contains a claim with given value. + /// + /// JWT to make the assertions on + /// The claim type to assert + /// The claim value to assert. If null then claim value can be anything (it is not checked) + /// If true then the claim itself is optional and doesn't need to exist in the claims + static public void AssertClaim(this JwtSecurityToken jwt, string claimType, string? claimValue, bool optional = false) + { + var claims = jwt.Claims.Where(claim => claim.Type == claimType); + + // Claim not found and it's optional so just exit + if (optional && (claims == null || !claims.Any())) + { + return; + } + + claims.Should().NotBeNull(claimType); + claims.Should().ContainSingle(claimType); + + // Check value value + if (claimValue != null) + { + var claim = claims.First(); + claim.Value.Should().Be(claimValue, claimType); + } + } + } + +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/PlaywrightExtensions.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/PlaywrightExtensions.cs new file mode 100644 index 0000000..c46909f --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/PlaywrightExtensions.cs @@ -0,0 +1,33 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UI.Pages.Authorisation; +using Microsoft.Playwright; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions +{ + public static class PlaywrightExtensions + { + public static ILocator Locator(this IPage? page, string selector, bool throwErrorIfNotFound) + { + var result = page?.Locator(selector); + + if (throwErrorIfNotFound && result == null) + { + throw new PageElementNotFoundException(nameof(AuthenticateLoginPage), selector).Log(); + } + + return result; + } + + public static async Task WaitForSelectorAsync(this IPage? page, string selector, bool throwErrorIfNotFound) + { + var result = await page?.WaitForSelectorAsync(selector); + + if (throwErrorIfNotFound && result == null) + { + throw new PageElementNotFoundException(nameof(AuthenticateLoginPage), selector).Log(); + } + + return result; + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/SQLExtensions.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/SQLExtensions.cs new file mode 100644 index 0000000..c217af2 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/SQLExtensions.cs @@ -0,0 +1,73 @@ +using Microsoft.Data.SqlClient; +using Serilog; +using System.Text; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions +{ + static public class SqlExtensions + { + /// + /// Execute scalar command and return result as Int32. Throw error if no results or conversion error + /// + static public Int32 ExecuteScalarInt32(this SqlCommand command) + { + var res = command.ExecuteScalar(); + + if (res == DBNull.Value || res == null) + { + // Build list of paramters + var sb = new StringBuilder(); + foreach (SqlParameter p in command.Parameters) + sb.Append($"{p.ParameterName}={p.Value.ToString()}"); + + // Throw exception + throw new System.Data.DataException($"Command returns no results - CommandText=\"{command.CommandText}\", Parameters: {sb.ToString()}"); + } + + return Convert.ToInt32(res); + } + + /// + /// Execute scalar command and return result as string. Throw error if no results or conversion error + /// + static public string ExecuteScalarString(this SqlCommand command) + { + var res = command.ExecuteScalar(); + + if (res == DBNull.Value || res == null) + { + + // Build list of paramters + var sb = new StringBuilder(); + foreach (SqlParameter p in command.Parameters) + sb.Append($"{p.ParameterName}={p.Value.ToString()}"); + + // Throw exception + throw new System.Data.DataException($"Command returns no results - CommandText=\"{command.CommandText}\", Parameters: {sb.ToString()}"); + } + + return Convert.ToString(res); + } + + static public Int32 ExecuteScalarInt32(this SqlConnection connection, string sql) + { + Log.Information("Calling {FunctionName} with SQL command: {sql}", nameof(ExecuteScalarInt32), sql); + using var command = new SqlCommand(sql, connection); + return command.ExecuteScalarInt32(); + } + + static public string ExecuteScalarString(this SqlConnection connection, string sql) + { + Log.Information("Calling {FunctionName} with SQL command: {sql}", nameof(ExecuteScalarString), sql); + using var command = new SqlCommand(sql, connection); + return command.ExecuteScalarString(); + } + + static public void ExecuteNonQuery(this SqlConnection connection, string sql) + { + Log.Information("Calling {FunctionName} with SQL command: {sql}", nameof(ExecuteNonQuery), sql); + using var command = new SqlCommand(sql, connection); + command.ExecuteNonQuery(); + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/ServiceCollectionExtensions.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..2bb255f --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,148 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddTestAutomationSettings(this IServiceCollection services, Action configureTestAutomationOptions) + { + Log.Information($"Configuring Test Automation settings."); + + services.Configure(configureTestAutomationOptions); + + var options = new TestAutomationOptions(); + configureTestAutomationOptions(options); + + try + { + //Doesn't check for Industry because it's an enum and defaults to banking if not supplied + if (options.SCOPE.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationOptions.SCOPE)} - required setting is missing from StartUp"); + } + + if (options.DH_MTLS_GATEWAY_URL.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationOptions.DH_MTLS_GATEWAY_URL)} - configuration setting not found"); + } + + if (options.DH_TLS_AUTHSERVER_BASE_URL.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationOptions.DH_TLS_AUTHSERVER_BASE_URL)} - configuration setting not found"); + } + + if (options.DH_TLS_PUBLIC_BASE_URL.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationOptions.DH_TLS_PUBLIC_BASE_URL)} - configuration setting not found"); + } + + if (options.REGISTER_MTLS_URL.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationOptions.REGISTER_MTLS_URL)} - configuration setting not found"); + } + + if (options.DATAHOLDER_CONNECTIONSTRING.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationOptions.DATAHOLDER_CONNECTIONSTRING)} - configuration setting not found"); + } + + if (options.AUTHSERVER_CONNECTIONSTRING.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationOptions.AUTHSERVER_CONNECTIONSTRING)} - configuration setting not found"); + } + + if (options.REGISTER_CONNECTIONSTRING.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationOptions.REGISTER_CONNECTIONSTRING)} - configuration setting not found"); + } + + if (options.MDH_INTEGRATION_TESTS_HOST.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationOptions.MDH_INTEGRATION_TESTS_HOST)} - configuration setting not found"); + } + + if (options.MDH_HOST.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationOptions.MDH_HOST)} - configuration setting not found"); + } + + if (options.CDRAUTHSERVER_SECUREBASEURI.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationOptions.CDRAUTHSERVER_SECUREBASEURI)} - configuration setting not found"); + } + + if (options.CREATE_MEDIA && options.MEDIA_FOLDER.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationOptions.MEDIA_FOLDER)} - configuration setting not found and must be provided when {nameof(TestAutomationOptions.CREATE_MEDIA)} is true"); + } + } + catch (Exception ex) + { + Log.Error(ex.Message); + throw; + } + + return services; + } + + public static IServiceCollection AddTestAutomationAuthServerSettings(this IServiceCollection services, Action configureTestAutomationAuthServerOptions) + { + services.Configure(configureTestAutomationAuthServerOptions); + + var options = new TestAutomationAuthServerOptions(); + configureTestAutomationAuthServerOptions(options); + + if (options.XTLSCLIENTCERTTHUMBPRINT.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationAuthServerOptions.XTLSCLIENTCERTTHUMBPRINT)} - configuration setting not found").Log(); + } + + if (options.XTLSADDITIONALCLIENTCERTTHUMBPRINT.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationAuthServerOptions.XTLSADDITIONALCLIENTCERTTHUMBPRINT)} - configuration setting not found").Log(); + } + + if (options.CDRAUTHSERVER_BASEURI.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException($"{nameof(TestAutomationAuthServerOptions.CDRAUTHSERVER_BASEURI)} - configuration setting not found").Log(); + } + + if (options.ACCESSTOKENLIFETIMESECONDS == null) + { + throw new InvalidOperationException($"{nameof(TestAutomationAuthServerOptions.ACCESSTOKENLIFETIMESECONDS)} - configuration setting not found").Log(); + } + + return services; + } + + public static IServiceCollection AddTestAutomationServices(this IServiceCollection services, IConfiguration configuration) + { + Log.Information($"Registering Test Automation shared services."); + + if (configuration == null) + { + throw new InvalidOperationException($"{nameof(AddTestAutomationServices)} - configuration cannot be null").Log(); + } + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(configuration); + + services.AddSingleton(configuration); + + return services; + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/StringExtensions.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/StringExtensions.cs new file mode 100644 index 0000000..befa6bf --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/StringExtensions.cs @@ -0,0 +1,40 @@ +using IdentityModel; +using Serilog; +using System.Runtime.Serialization; +using System.Security.Cryptography; +using System.Text; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions +{ + public static class StringExtensions + { + static public void WriteStringToFile(string filename, string? str) + { + Log.Information("Writing string value to filename: {filename}", filename); + File.WriteAllText(filename, str); + } + + /// + /// Convert string to int + /// + public static int ToInt(this string str) + { + return Convert.ToInt32(str); + } + + public static string CreatePkceChallenge(this string codeVerifier) + { + using (var sha256 = SHA256.Create()) + { + var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier)); + return Base64Url.Encode(challengeBytes); + } + } + + public static bool IsNullOrWhiteSpace(this string? str) + { + return string.IsNullOrWhiteSpace(str); + } + + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/TokenTypeExtensions.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/TokenTypeExtensions.cs new file mode 100644 index 0000000..05c45c8 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Extensions/TokenTypeExtensions.cs @@ -0,0 +1,61 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions +{ + + public static class TokenTypeExtensions + { + public static string GetUserIdByTokenType(this TokenType tokenType) + { + try + { + return tokenType switch + { + TokenType.JaneWilson => Constants.Users.Banking.UserIdJaneWilson, //Banking + TokenType.MaryMoss => Constants.Users.Energy.UserIdMaryMoss, //Energy + TokenType.HeddaHare => Constants.Users.Energy.UserIdHeddaHare, + TokenType.SteveKennedy => Constants.Users.UserIdSteveKennedy, + TokenType.DewayneSteve => Constants.Users.UserIdDewayneSteve, + TokenType.Business1 => Constants.Users.UserIdBusiness1, + TokenType.Business2 => Constants.Users.UserIdBusiness2, + TokenType.Beverage => Constants.Users.UserIdBeverage, + TokenType.KamillaSmith => Constants.Users.UserIdKamillaSmith, + _ => throw new ArgumentException($"{nameof(GetUserIdByTokenType)} failed for {nameof(TokenType)}={tokenType}.") + }; + } + catch (Exception ex) + { + Log.Error(ex.Message); + throw; + } + } + + public static string GetAllAccountIdsByTokenType(this TokenType tokenType) + { + try + { + return tokenType switch + { + TokenType.JaneWilson => Constants.Accounts.Banking.AccountIdsAllJaneWilson, //Banking + TokenType.MaryMoss => Constants.Accounts.Energy.AccountIdsAllMaryMoss, //Energy + TokenType.HeddaHare => Constants.Accounts.Energy.AccountIdsAllHeddaHare,//Energy + TokenType.SteveKennedy => Constants.Accounts.AccountIdsAllSteveKennedy, + TokenType.DewayneSteve => Constants.Accounts.AccountIdsAllDewayneSmith, + TokenType.Business1 => Constants.Accounts.AccountIdsAllBusiness1, + TokenType.Business2 => Constants.Accounts.AccountIdsAllBusiness2, + TokenType.Beverage => Constants.Accounts.AccountIdsAllBeverage, + TokenType.KamillaSmith => Constants.Accounts.AccountIdsAllKamillaSmith, + _ => throw new ArgumentException($"{nameof(GetAllAccountIdsByTokenType)} failed for {nameof(TokenType)}={tokenType}.") + }; + } + catch (Exception ex) + { + Log.Error(ex.Message); + throw; + } + } + } + + +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Fixtures/BaseFixture.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Fixtures/BaseFixture.cs new file mode 100644 index 0000000..0725400 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Fixtures/BaseFixture.cs @@ -0,0 +1,138 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using Dapper; +using Dapper.Contrib.Extensions; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Serilog; +using Xunit; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Fixtures +{ + /// + /// Patches Register SoftwareProduct RedirectURI and JwksURI. + /// Stands up JWKS endpoint. + /// + public class BaseFixture : IAsyncLifetime + { + private JwksEndpoint? _jwksEndpoint; + private readonly TestAutomationOptions _options; + + public BaseFixture(IOptions options) + { + Log.Information("Constructing {ClassName}.", nameof(BaseFixture)); + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + } + + public ServiceProvider ServiceProvider { get; private set; } + + public Task InitializeAsync() + { + Log.Information("Started {FunctionName} in {ClassName}.", nameof(InitializeAsync), nameof(BaseFixture)); + + // Patch Register + Helpers.AuthServer.PatchRedirectUriForRegister(_options); + Helpers.AuthServer.PatchJwksUriForRegister(_options); + if (_options.INDUSTRY.Equals(Industry.ENERGY)) + { + Register_PatchScopes(); + } + + // Stand-up JWKS endpoint + _jwksEndpoint = new JwksEndpoint(_options.SOFTWAREPRODUCT_JWKS_URI_FOR_INTEGRATION_TESTS, + Constants.Certificates.JwtCertificateFilename, + Constants.Certificates.JwtCertificatePassword); + _jwksEndpoint.Start(); + + //The Auth Server testing seeds specific data + if (_options.IS_AUTH_SERVER) + { + CdrAuthServer_SeedDatabase(); + } + + return Task.CompletedTask; + } + + public async Task DisposeAsync() + { + Log.Information("Started {FunctionName} in {ClassName}.", nameof(DisposeAsync), nameof(BaseFixture)); + + if (_jwksEndpoint != null) + await _jwksEndpoint.DisposeAsync(); + } + + /// + /// The seed data for the Register is using the loopback uri for jwksuri. + /// Since the integration tests stands up it's own data recipient jwks endpoint we need to + /// patch the jwks uri to match our endpoint. + /// + private void Register_PatchScopes() + { + Log.Information("Started {FunctionName} in {ClassName}.", nameof(Register_PatchScopes), nameof(BaseFixture)); + using var connection = new SqlConnection(_options.REGISTER_CONNECTIONSTRING); + connection.Open(); + + using var updateCommand = new SqlCommand("update softwareproduct set scope = 'openid profile bank:accounts.basic:read bank:accounts.detail:read bank:transactions:read bank:payees:read bank:regular_payments:read common:customer.basic:read common:customer.detail:read cdr:registration energy:accounts.basic:read energy:accounts.concessions:read' where lower(softwareproductid) = @id", connection); + updateCommand.Parameters.AddWithValue("@id", Constants.SoftwareProducts.SoftwareProductId.ToLower()); + updateCommand.ExecuteNonQuery(); + } + + private void CdrAuthServer_SeedDatabase() + { + Log.Information("Started {FunctionName} in {ClassName}.", nameof(CdrAuthServer_SeedDatabase), nameof(BaseFixture)); + using var connection = new SqlConnection(_options.AUTHSERVER_CONNECTIONSTRING); + + connection.Query("delete softwareproducts"); + if (connection.QuerySingle("select count(*) from softwareproducts") != 0) + { + throw new InvalidOperationException("Unable to delete softwareproducts"); + } + + connection.Insert(new SoftwareProduct() + { + SoftwareProductId = Constants.SoftwareProducts.SoftwareProductId, + SoftwareProductName = "Mock Data Recipient Software Product", + SoftwareProductDescription = "Mock Data Recipient Software Product", + LogoUri = "https://cdrsandbox.gov.au/logo192.png", + Status = "ACTIVE", + LegalEntityId = "18B75A76-5821-4C9E-B465-4709291CF0F4", + LegalEntityName = "Mock Data Recipient Legal Entity Name", + LegalEntityStatus = "ACTIVE", + BrandId = Constants.Brands.BrandId, + BrandName = "Mock Data Recipient Brand Name", + BrandStatus = "ACTIVE" + }); + + connection.Insert(new SoftwareProduct() + { + SoftwareProductId = Constants.SoftwareProducts.AdditionalSoftwareProductId, + SoftwareProductName = "Track Xpense", + SoftwareProductDescription = "Application to allow you to track your expenses", + LogoUri = "https://cdrsandbox.gov.au/foo.png", + Status = "ACTIVE", + LegalEntityId = "9d34ede4-2c76-4ecc-a31e-ea8392d31cc9", + LegalEntityName = "FintechX", + LegalEntityStatus = "ACTIVE", + BrandId = Constants.Brands.AdditionalBrandId, + BrandName = "Finance X", + BrandStatus = "ACTIVE" + }); + } + + private class SoftwareProduct + { + public string? SoftwareProductId { get; set; } + public string? SoftwareProductName { get; set; } + public string? SoftwareProductDescription { get; set; } + public string? LogoUri { get; set; } + public string? Status { get; set; } + public string? LegalEntityId { get; set; } + public string? LegalEntityName { get; set; } + public string? LegalEntityStatus { get; set; } + public string? BrandId { get; set; } + public string? BrandName { get; set; } + public string? BrandStatus { get; set; } + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Fixtures/PlaywrightFixture.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Fixtures/PlaywrightFixture.cs new file mode 100644 index 0000000..5b3b8c2 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Fixtures/PlaywrightFixture.cs @@ -0,0 +1,32 @@ +using Serilog; +using Xunit; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Fixtures +{ + public class PlaywrightFixture : IAsyncLifetime + { + static private bool RUNNING_IN_CONTAINER => Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER")?.ToUpper() == "TRUE"; + + virtual public Task InitializeAsync() + { + Log.Information("Started {FunctionName} in {ClassName}.", nameof(InitializeAsync), nameof(PlaywrightFixture)); + + // Only install Playwright if not running in container, since Dockerfile.e2e-tests already installed Playwright + if (!RUNNING_IN_CONTAINER) + { + // Ensure that Playwright has been fully installed. + Microsoft.Playwright.Program.Main(new string[] { "install" }); + Microsoft.Playwright.Program.Main(new string[] { "install-deps" }); + } + + return Task.CompletedTask; + } + + virtual public Task DisposeAsync() + { + Log.Information("Started {FunctionName} in {ClassName}.", nameof(DisposeAsync), nameof(PlaywrightFixture)); + + return Task.CompletedTask; + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Fixtures/RegisterSoftwareProductFixture.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Fixtures/RegisterSoftwareProductFixture.cs new file mode 100644 index 0000000..7c28749 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Fixtures/RegisterSoftwareProductFixture.cs @@ -0,0 +1,55 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using Microsoft.Extensions.Options; +using Serilog; +using Xunit; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Fixtures +{ + /// + /// Purges DataHolders AuthServer database and registers software product + /// (in addition to operations performed by TestFixture) + /// + public class RegisterSoftwareProductFixture : BaseFixture, IAsyncLifetime + { + private readonly TestAutomationOptions _options; + private readonly IDataHolderRegisterService _dataHolderRegisterService; + private readonly IDataHolderAccessTokenCache _dataHolderAccessTokenCache; + + public RegisterSoftwareProductFixture( + IOptions options, + IDataHolderRegisterService dataHolderRegisterService, + IDataHolderAccessTokenCache dataHolderAccessTokenCache) + : base(options) + { + Log.Information("Constructing {ClassName}.", nameof(RegisterSoftwareProductFixture)); + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _dataHolderRegisterService = dataHolderRegisterService ?? throw new ArgumentNullException(nameof(dataHolderRegisterService)); + _dataHolderAccessTokenCache = dataHolderAccessTokenCache ?? throw new ArgumentNullException(nameof(dataHolderAccessTokenCache)); + } + + new public async Task InitializeAsync() + { + Log.Information("Started {FunctionName} in {ClassName}.", nameof(InitializeAsync), nameof(RegisterSoftwareProductFixture)); + + // Any Access Tokens in cache will become invalid when the database is purged. + _dataHolderAccessTokenCache.ClearCache(); + + await base.InitializeAsync(); + + // Purge AuthServer + Helpers.AuthServer.PurgeAuthServerForDataholder(_options); + + // Register software product + await _dataHolderRegisterService.RegisterSoftwareProduct(responseType: "code,code id_token"); + } + + new public async Task DisposeAsync() + { + Log.Information("Started {FunctionName} in {ClassName}.", nameof(DisposeAsync), nameof(RegisterSoftwareProductFixture)); + + await base.DisposeAsync(); + + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Helpers.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Helpers.cs new file mode 100644 index 0000000..68e5755 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Helpers.cs @@ -0,0 +1,294 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Net; +using System.Net.Http.Headers; +using System.Security.Cryptography.X509Certificates; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using Jose; +using Microsoft.Data.SqlClient; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation +{ + public static class Helpers + { + public static class Web + { + public static HttpClient CreateHttpClient(string? certFilename = null, string? certPassword = null, bool allowAutoRedirect = true, IEnumerable? cookies = null, HttpRequestMessage? request = null, CookieContainer? cookieContainer = null) + { + var clientHandler = new HttpClientHandler + { + AllowAutoRedirect = allowAutoRedirect, + }; + clientHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; //sonarqube will raise this as a vulnerability as it is not away this is a test library only + + // Set cookie container + if (cookieContainer != null) + { + if (cookies != null) + { + throw new ArgumentOutOfRangeException(nameof(cookies),"Cookies and CookieContainer parameters cannot be provided at the same time.").Log(); + } + clientHandler.UseDefaultCredentials = true; //used with the Cookie Container + clientHandler.UseCookies = true; + clientHandler.CookieContainer = cookieContainer; + } + + // Set cookies + if (cookies != null) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request),"Request parameter cannot be null when Cookies parameter has been provided.").Log(); + } + + clientHandler.UseCookies = false; + request.Headers.Add("Cookie", cookies); + } + + // Attach client certificate + if (certFilename != null) + { + if (certPassword == null) + { + throw new ArgumentNullException(nameof(certPassword),"Certificate password parameter cannot be null when Certificate filename parameter has been provided.").Log(); + } + + clientHandler.ClientCertificates.Add(new X509Certificate2( + certFilename, + certPassword, + X509KeyStorageFlags.Exportable + )); + } + + return new HttpClient(new LoggingHandler(clientHandler)); + } + } + + public static class Jwt + { + public static string CreateJWT(string certificateFilename, string certificatePassword, Dictionary subject) + { + var payload = JsonConvert.SerializeObject(subject); + + return CreateJWT(certificateFilename, certificatePassword, payload); + } + + public static string CreateJWT(string certificateFilename, string certificatePassword, string payload) + { + var cert = new X509Certificate2(certificateFilename, certificatePassword); + + var securityKey = new X509SecurityKey(cert); + + var jwtHeader = new Dictionary() + { + { JwtHeaderParameterNames.Alg, "PS256" }, + { JwtHeaderParameterNames.Typ, "JWT" }, + { JwtHeaderParameterNames.Kid, securityKey.KeyId}, + }; + + var jwt = JWT.Encode(payload, cert.GetRSAPrivateKey(), JwsAlgorithm.PS256, jwtHeader); + + return jwt; + } + } + + public static class Jwk + { + /// + /// Build JWKS from certificate + /// + public static Jwks BuildJWKS(string certificateFilename, string certificatePassword) + { + var cert = new X509Certificate2(certificateFilename, certificatePassword); + + //Get credentials from certificate + var securityKey = new X509SecurityKey(cert); + var signingCredentials = new X509SigningCredentials(cert, SecurityAlgorithms.RsaSsaPssSha256); + var encryptingCredentials = new X509EncryptingCredentials(cert, SecurityAlgorithms.RsaOaepKeyWrap, SecurityAlgorithms.RsaOAEP); + + var rsaParams = signingCredentials?.Certificate?.GetRSAPublicKey()?.ExportParameters(false) ?? throw new Exception("Error getting RSA params").Log(); + var e = Base64UrlEncoder.Encode(rsaParams.Exponent); + var n = Base64UrlEncoder.Encode(rsaParams.Modulus); + + var jwkSign = new Models.Jwk() + { + Alg = signingCredentials.Algorithm, + Kid = signingCredentials.Kid, + // kid = signingCredentials.Key.KeyId, + Kty = securityKey.PublicKey.KeyExchangeAlgorithm, + N = n, + E = e, + Use = "sig" + }; + + var jwkEnc = new Models.Jwk() + { + Alg = encryptingCredentials.Enc, + Kid = encryptingCredentials.Key.KeyId, + Kty = securityKey.PublicKey.KeyExchangeAlgorithm, + N = n, + E = e, + Use = "enc" + }; + + return new Jwks() + { + Keys = new Models.Jwk[] { jwkSign, jwkEnc } + }; + } + } + + public static class AuthServer + { + // When running standalone CdrAuthServer (ie no MtlsGateway) we need to attach the X-TlsClientCertThumbprint required by ValidateMTLSAttribute + static public void AttachHeadersForStandAlone(string url, HttpHeaders headers, string dhMtlsGatewayUrl, string xtlsClientCertThumbprint, bool? isStandalone = false) + { + if (isStandalone.HasValue && isStandalone.Value) + { + if (dhMtlsGatewayUrl.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(dhMtlsGatewayUrl)).Log(); + } + + if (url.StartsWith(dhMtlsGatewayUrl) == true) + { + if (xtlsClientCertThumbprint.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(xtlsClientCertThumbprint)).Log(); + } + + headers.Add("X-TlsClientCertThumbprint", xtlsClientCertThumbprint); + } + } + } + + /// + /// Clear data from the Dataholder's AuthServer database + /// + /// + /// Only clear the persisted grants table + public static void PurgeAuthServerForDataholder(TestAutomationOptions options, bool onlyPersistedGrants = false) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(PurgeAuthServerForDataholder), nameof(Helpers.AuthServer)); + + using var connection = new SqlConnection(options.AUTHSERVER_CONNECTIONSTRING); + + void Purge(string table) + { + // Delete all rows + using var deleteCommand = new SqlCommand($"delete from {table}", connection); + deleteCommand.ExecuteNonQuery(); + + // Check all rows deleted + using var selectCommand = new SqlCommand($"select count(*) from {table}", connection); + var count = selectCommand.ExecuteScalarInt32(); + if (count != 0) + { + throw new InvalidOperationException($"Table {table} was not purged").Log(); + } + } + + try + { + connection.Open(); + + if (!onlyPersistedGrants) + { + Purge("ClientClaims"); + Purge("Clients"); + } + + Purge("Grants"); + } + catch (Exception ex) + { + Log.Error(ex.Message); + throw; + } + } + + /// + /// The seed data for the Register is using the loopback uri for redirecturi. + /// Since the integration tests stands up it's own data recipient consent/callback endpoint we need to + /// patch the redirect uri to match our callback. + /// + public static void PatchRedirectUriForRegister(TestAutomationOptions options, string softwareProductId = TestAutomation.Constants.SoftwareProducts.SoftwareProductId, string redirectURI = "") + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(PatchRedirectUriForRegister), nameof(Helpers.AuthServer)); + + if (redirectURI.IsNullOrWhiteSpace()) + { + redirectURI = options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS; + } + + try + { + using var connection = new SqlConnection(options.REGISTER_CONNECTIONSTRING); + connection.Open(); + + using var updateCommand = new SqlCommand("update softwareproduct set redirecturis = @uri where lower(softwareproductid) = @id", connection); + updateCommand.Parameters.AddWithValue("@uri", redirectURI); + updateCommand.Parameters.AddWithValue("@id", softwareProductId.ToLower()); + updateCommand.ExecuteNonQuery(); + + using var selectCommand = new SqlCommand($"select redirecturis from softwareproduct where lower(softwareproductid) = @id", connection); + selectCommand.Parameters.AddWithValue("@id", softwareProductId.ToLower()); + if (selectCommand.ExecuteScalarString() != redirectURI) + { + throw new InvalidOperationException($"softwareproduct.redirecturis is not '{redirectURI}'").Log(); + } + } + catch (Exception ex) + { + Log.Error(ex.Message); + throw; + } + + } + + + /// + /// The seed data for the Register is using the loopback uri for jwksuri. + /// Since the integration tests stands up it's own data recipient jwks endpoint we need to + /// patch the jwks uri to match our endpoint. + /// + public static void PatchJwksUriForRegister(TestAutomationOptions options, string softwareProductId = TestAutomation.Constants.SoftwareProducts.SoftwareProductId, string jwksURI = "") + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(PatchJwksUriForRegister), nameof(Helpers.AuthServer)); + + if (jwksURI.IsNullOrWhiteSpace()) + { + jwksURI = options.SOFTWAREPRODUCT_JWKS_URI_FOR_INTEGRATION_TESTS; + } + + try + { + using var connection = new SqlConnection(options.REGISTER_CONNECTIONSTRING); + connection.Open(); + + using var updateCommand = new SqlCommand("update softwareproduct set jwksuri = @uri where lower(softwareproductid) = @id", connection); + updateCommand.Parameters.AddWithValue("@uri", jwksURI); + updateCommand.Parameters.AddWithValue("@id", softwareProductId.ToLower()); + updateCommand.ExecuteNonQuery(); + + using var selectCommand = new SqlCommand($"select jwksuri from softwareproduct where lower(softwareproductid) = @id", connection); + selectCommand.Parameters.AddWithValue("@id", softwareProductId.ToLower()); + if (selectCommand.ExecuteScalarString() != jwksURI) + { + throw new InvalidOperationException($"softwareproduct.jwksuri is not '{jwksURI}'").Log(); + } + } + catch (Exception ex) + { + Log.Error(ex.Message); + throw; + } + } + } + } + +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IAccessTokenService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IAccessTokenService.cs new file mode 100644 index 0000000..c5a268c --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IAccessTokenService.cs @@ -0,0 +1,19 @@ +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces +{ + public interface IAccessTokenService + { + string Audience { get; init; } + string? CertificateFilename { get; set; } + string? CertificatePassword { get; set; } + string ClientAssertionType { get; init; } + string ClientId { get; init; } + string GrantType { get; init; } + string Issuer { get; init; } + string? JwtCertificateFilename { get; set; } + string? JwtCertificatePassword { get; set; } + string Scope { get; init; } + string URL { get; init; } + + Task GetAsync(string dhMtlsGatewayUrl, string xtlsClientCertThumbprint, bool isStandalone); + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IApiService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IApiService.cs new file mode 100644 index 0000000..7893836 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IApiService.cs @@ -0,0 +1,27 @@ +using System.Net.Http.Headers; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces +{ + public interface IApiService + { + string? Accept { get; } + string? AccessToken { get; } + string? CertificateFilename { get; } + string? CertificatePassword { get; } + HttpContent? Content { get; } + MediaTypeHeaderValue? ContentType { get; } + IEnumerable? Cookies { get; } + string? DhMtlsGatewayUrl { get; } + HttpMethod? HttpMethod { get; } + string? IfNoneMatch { get; } + bool IsStandalone { get; } + string? URL { get; } + string? XFapiAuthDate { get; } + string? XFapiInteractionId { get; } + string? XMinV { get; } + string? XtlsClientCertificateThumbprint { get; } + string? XV { get; } + + Task SendAsync(bool allowAutoRedirect = true, string? xtlsThumbprint = null); + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IApiServiceDirector.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IApiServiceDirector.cs new file mode 100644 index 0000000..bf6b0ad --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IApiServiceDirector.cs @@ -0,0 +1,23 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces +{ + public interface IApiServiceDirector + { + ApiService BuildAuthServerAuthorizeAPI(Dictionary queryString); + ApiService BuildAuthServerJWKSAPI(); + ApiService BuildAuthServerOpenIdConfigurationAPI(); + ApiService BuildCustomerResourceAPI(string? accessToken); + ApiService BuildDataHolderDiscoveryStatusAPI(); + ApiService BuildDataHolderDiscoveryOutagesAPI(); + ApiService BuildDataHolderBankingGetAccountsAPI(string? accessToken, string? xFapiAuthDate, string? xv = "1", string? xFapiInteractionId = null, string certFileName = Constants.Certificates.CertificateFilename, string certPassword = Constants.Certificates.CertificatePassword, string? url = null); + ApiService BuildDataHolderBankingGetTransactionsAPI(string? accessToken, string? xFapiAuthDate, string? encryptedAccountId = null, string? xv = "1", string? xFapiInteractionId = null, string certFileName = Constants.Certificates.CertificateFilename, string certPassword = Constants.Certificates.CertificatePassword, string? url = null); + ApiService BuildDataHolderCommonGetCustomerAPI(string? accessToken, string? xFapiAuthDate, string? xv = "1", string certFileName = Constants.Certificates.CertificateFilename, string certPassword = Constants.Certificates.CertificatePassword); + ApiService BuildDataholderRegisterAPI(string? accessToken, string? registrationRequest, HttpMethod? httpMethod, string clientId = ""); + ApiService BuildRegisterSSAAPI(Industry? industry, string brandId, string softwareProductId, string? accessToken, string? xv); + ApiService BuildUserInfoAPI(string? xv, string? accessToken, string? thumbprint, HttpMethod? httpMethod, string certFilename = Constants.Certificates.CertificateFilename, string certPassword = Constants.Certificates.CertificatePassword); + ApiService BuildDataHolderEnergyGetAccountsAPI(string? accessToken, string? xFapiAuthDate, string? xv = "1", string? xMinV = null, string? xFapiInteractionId = null, string certFileName = Constants.Certificates.CertificateFilename, string certPassword = Constants.Certificates.CertificatePassword, string? url = null); + ApiService BuildDataHolderEnergyGetConcessionsAPI(string? accessToken, string? xFapiAuthDate, string? encryptedAccountId = null, string? xv = "1", string certFileName = Constants.Certificates.CertificateFilename, string certPassword = Constants.Certificates.CertificatePassword, string? url = null); + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IBuilder.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IBuilder.cs new file mode 100644 index 0000000..a4a3e7f --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IBuilder.cs @@ -0,0 +1,17 @@ +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces +{ + public interface IBuilder + { + T Build(); + } + + public interface IAsyncBuilder + { + Task BuildAsync(); + } + + public interface IBuilder + { + Task Build(TBuildArg buildArg); + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/ICdrValidationException.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/ICdrValidationException.cs new file mode 100644 index 0000000..8f26d67 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/ICdrValidationException.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces +{ + public interface ICdrValidationException + { + List Items { get; set; } + + List ValidationErrors { get; set; } + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderAccessTokenCache.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderAccessTokenCache.cs new file mode 100644 index 0000000..9529945 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderAccessTokenCache.cs @@ -0,0 +1,14 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces +{ + public interface IDataHolderAccessTokenCache + { + int Hits { get; } + int Misses { get; } + + void ClearCache(); + Task GetAccessToken(TokenType tokenType, string? scope = null, bool useCache = true); + + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderAuthoriseService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderAuthoriseService.cs new file mode 100644 index 0000000..2857266 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderAuthoriseService.cs @@ -0,0 +1,27 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces +{ + public interface IDataHolderAuthoriseService + { + string? CdrArrangementId { get; } + string CertificateFilename { get; } + string CertificatePassword { get; } + string ClientId { get; } + string JwtCertificateFilename { get; } + string JwtCertificatePassword { get; } + string OTP { get; } + string RedirectURI { get; } + string? RequestUri { get; } + ResponseMode ResponseMode { get; } + ResponseType ResponseType { get; } + string Scope { get; } + string? SelectedAccountIds { get; } + int? SharingDuration { get; } + int TokenLifetime { get; } + string UserId { get; } + + Task<(string authCode, string idToken)> Authorise(); + Task AuthoriseForJarm(); + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderParService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderParService.cs new file mode 100644 index 0000000..8f74b6d --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderParService.cs @@ -0,0 +1,12 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; +using static ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services.DataHolderParService; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces +{ + public interface IDataHolderParService + { + Task GetRequestUri(string? scope, string? clientId = null, string jwtCertificateForClientAssertionFilename = Constants.Certificates.JwtCertificateFilename, string jwtCertificateForClientAssertionPassword = Constants.Certificates.JwtCertificatePassword, string jwtCertificateForRequestObjectFilename = Constants.Certificates.JwtCertificateFilename, string jwtCertificateForRequestObjectPassword = Constants.Certificates.JwtCertificatePassword, string? redirectUri = null, int? sharingDuration = Constants.AuthServer.SharingDuration, string? cdrArrangementId = null, ResponseMode responseMode = ResponseMode.Fragment); + Task SendRequest(string? scope, string? clientId = null, string clientAssertionType = Constants.ClientAssertionType, int? sharingDuration = Constants.AuthServer.SharingDuration, string? aud = null, int nbfOffsetSeconds = 0, int expOffsetSeconds = 0, bool addRequestObject = true, bool addNotBeforeClaim = true, bool addExpiryClaim = true, string? cdrArrangementId = null, string? redirectUri = null, string? clientAssertion = null, string codeVerifier = Constants.AuthServer.FapiPhase2CodeVerifier, string codeChallengeMethod = Constants.AuthServer.FapiPhase2CodeChallengeMethod, string? requestUri = null, ResponseMode? responseMode = ResponseMode.Fragment, string certificateFilename = Constants.Certificates.CertificateFilename, string certificatePassword = Constants.Certificates.CertificatePassword, string jwtCertificateForClientAssertionFilename = Constants.Certificates.JwtCertificateFilename, string jwtCertificateForClientAssertionPassword = Constants.Certificates.JwtCertificatePassword, string jwtCertificateForRequestObjectFilename = Constants.Certificates.JwtCertificateFilename, string jwtCertificateForRequestObjectPassword = Constants.Certificates.JwtCertificatePassword, ResponseType? responseType = ResponseType.CodeIdToken, string? state = null); + Task DeserializeResponse(HttpResponseMessage response); + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderRegisterService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderRegisterService.cs new file mode 100644 index 0000000..07f8fe7 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderRegisterService.cs @@ -0,0 +1,11 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces +{ + public interface IDataHolderRegisterService + { + string CreateRegistrationRequest(string ssa, string tokenEndpointAuthSigningAlg = "PS256", string[]? redirectUris = null, string jwtCertificateFilename = Constants.Certificates.JwtCertificateFilename, string jwtCertificatePassword = Constants.Certificates.JwtCertificatePassword, string applicationType = "web", string requestObjectSigningAlg = "PS256", string responseType = "code id_token", string[]? grantTypes = null, string? authorizationSignedResponseAlg = null, string? authorizationEncryptedResponseAlg = null, string? authorizationEncryptedResponseEnc = null, string? idTokenSignedResponseAlg = "PS256", string? idTokenEncryptedResponseAlg = "RSA-OAEP", string? idTokenEncryptedResponseEnc = "A256GCM"); + Task RegisterSoftwareProduct(string registrationRequest); + Task<(string ssa, string registration, string clientId)> RegisterSoftwareProduct(string brandId = Constants.Brands.BrandId, string softwareProductId = Constants.SoftwareProducts.SoftwareProductId, string jwtCertificateFilename = Constants.Certificates.JwtCertificateFilename, string jwtCertificatePassword = Constants.Certificates.JwtCertificatePassword, string responseType = "code id_token", string authorizationSignedResponseAlg = "PS256"); + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderTokenRevocationService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderTokenRevocationService.cs new file mode 100644 index 0000000..4fde555 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderTokenRevocationService.cs @@ -0,0 +1,11 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; +using System.Net.Http; +using System.Threading.Tasks; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces +{ + public interface IDataHolderTokenRevocationService + { + Task SendRequest(string? clientId = null, string? clientAssertionType = Constants.ClientAssertionType, string? clientAssertion = null, string? token = null, string? tokenTypeHint = null, string? certificateFilename = Constants.Certificates.CertificateFilename, string? certificatePassword = Constants.Certificates.CertificatePassword, string? jwtCertificateFilename = Constants.Certificates.JwtCertificateFilename, string? jwtCertificatePassword = Constants.Certificates.JwtCertificatePassword); + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderTokenService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderTokenService.cs new file mode 100644 index 0000000..6d3de7e --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IDataHolderTokenService.cs @@ -0,0 +1,13 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces +{ + public interface IDataHolderTokenService + { + Task GetAccessToken(string authCode); + Task GetResponse(string authCode, int? shareDuration = null, string? clientId = null, string? redirectUri = null, string certificateFilename = Constants.Certificates.CertificateFilename, string certificatePassword = Constants.Certificates.CertificatePassword, string jwkCertificateFilename = Constants.Certificates.JwtCertificateFilename, string jwkCertificatePassword = Constants.Certificates.JwtCertificatePassword, string? scope = null); + Task SendRequest(string? authCode = null, bool usePut = false, string grantType = "authorization_code", string? clientId = null, string? issuerClaim = null, string clientAssertionType = Constants.ClientAssertionType, bool useClientAssertion = true, int? shareDuration = null, string? refreshToken = null, string? customClientAssertion = null, string? scope = null, string? redirectUri = null, string certificateFilename = Constants.Certificates.CertificateFilename, string certificatePassword = Constants.Certificates.CertificatePassword, string jwkCertificateFilename = Constants.Certificates.JwtCertificateFilename, string jwkCertificatePassword = Constants.Certificates.JwtCertificatePassword); + Task DeserializeResponse(HttpResponseMessage response); + Task GetResponseUsingRefreshToken(string? refreshToken, string? scope = null); + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IPrivateKeyJwtService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IPrivateKeyJwtService.cs new file mode 100644 index 0000000..4f5c07b --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IPrivateKeyJwtService.cs @@ -0,0 +1,13 @@ +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces +{ + public interface IPrivateKeyJwtService + { + string Audience { get; set; } + string CertificateFilename { get; set; } + string CertificatePassword { get; set; } + string Issuer { get; set; } + bool RequireIssuer { get; init; } + + string Generate(); + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IRegisterSSAService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IRegisterSSAService.cs new file mode 100644 index 0000000..c081a97 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/IRegisterSSAService.cs @@ -0,0 +1,9 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces +{ + public interface IRegisterSsaService + { + Task GetSSA(string brandId, string softwareProductId, string xv = "3", string jwtCertificateFilename = Constants.Certificates.JwtCertificateFilename, string jwtCertificatePassword = Constants.Certificates.JwtCertificatePassword, Industry? industry = null); + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/ISqlQueryService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/ISqlQueryService.cs new file mode 100644 index 0000000..cbbaaac --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Interfaces/ISqlQueryService.cs @@ -0,0 +1,11 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces +{ + public interface ISqlQueryService + { + string GetClientId(string softwareProductId); + string GetStatus(EntityType entityType, string id); + void SetStatus(EntityType entityType, string id, string status); + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/JwksEndpoint.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/JwksEndpoint.cs new file mode 100644 index 0000000..6c17672 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/JwksEndpoint.cs @@ -0,0 +1,109 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation +{ + public partial class JwksEndpoint : IAsyncDisposable + { + /// + /// Emulate a JWKS endpoint on url returning a JWKS for the given certificate + /// + public JwksEndpoint(string url, string certificateFilename, string certificatePassword) + { + Url = url; + CertificateFilename = certificateFilename; + CertificatePassword = certificatePassword; + } + + public string Url { get; init; } + private string Url_PathAndQuery => new Uri(Url).PathAndQuery; + private int UrlPort => new Uri(Url).Port; + public string CertificateFilename { get; init; } + public string CertificatePassword { get; init; } + + private IWebHost? _host; + + public void Start() + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(Start), nameof(JwksEndpoint)); + + _host = new WebHostBuilder() + .UseKestrel(opts => + { + opts.ListenAnyIP(UrlPort, opts => opts.UseHttps()); // This will use the default developer certificate. Use "dotnet dev-certs https" to install if necessary + }) + .UseStartup(_ => new JWKSCallback_Startup(this)) + .Build(); + + _host.RunAsync(); + } + + public async Task Stop() + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(Stop), nameof(JwksEndpoint)); + + if (_host != null) + { + await _host.StopAsync(); + } + } + + bool _disposed; + public async ValueTask DisposeAsync() + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(DisposeAsync), nameof(JwksEndpoint)); + + if (!_disposed) + { + await Stop(); + _disposed = true; + } + + GC.SuppressFinalize(this); + } + + class JWKSCallback_Startup + { + private JwksEndpoint Endpoint { get; init; } + + public JWKSCallback_Startup(JwksEndpoint endpoint) + { + Endpoint = endpoint; + } + + /// + /// This is used by the JWKS endpoint for Startup + /// NOTE: You may see warnings about 0 references to this function, but it is used 'automatically' as part of the startup process + /// + /// + public void Configure(IApplicationBuilder app) + { + app.UseHttpsRedirection(); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGet(Endpoint.Url_PathAndQuery, async context => + { + // Build JWKS and return + var jwks = Helpers.Jwk.BuildJWKS(Endpoint.CertificateFilename, Endpoint.CertificatePassword); + await context.Response.WriteAsJsonAsync(jwks); + }); + }); + } + + /// + /// This is used by the JWKS endpoint for Startup + /// NOTE: You may see warnings about 0 references to this function, but it is used 'automatically' as part of the startup process + /// + /// + public static void ConfigureServices(IServiceCollection services) + { + services.AddRouting(); + } + } + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/KeyValuePairBuilder.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/KeyValuePairBuilder.cs new file mode 100644 index 0000000..8a1d9d9 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/KeyValuePairBuilder.cs @@ -0,0 +1,66 @@ +using System.Text; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation +{ + /// + /// Key/Value pair builder + /// + public class KeyValuePairBuilder + { + private readonly string _delimiter; + + private readonly StringBuilder _sb = new StringBuilder(); + /// + /// Get key/value pairs as string + /// + public string Value => _sb.ToString(); + + private int _count = 0; + /// + /// Number of key/value pairs + /// + public int Count => _count; + + public KeyValuePairBuilder(string delimiter = "&") + { + _delimiter = delimiter; + } + + /// + /// Append key/value pair + /// + /// Key to add + /// Value to add + public void Add(string key, string value) + { + if (_sb.Length > 0) + { + _sb.Append(_delimiter); + } + + _sb.Append($"{key}={value}"); + + _count++; + } + + /// + /// Add key/value pair + /// + /// Key to add + /// Value to add + public void Add(string key, int value) + { + Add(key, value.ToString()); + } + + /// + /// Add key/value pair + /// + /// Key to add + /// Value to add + public void Add(string key, long value) + { + Add(key, value.ToString()); + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/LoggingHandler.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/LoggingHandler.cs new file mode 100644 index 0000000..438af0f --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/LoggingHandler.cs @@ -0,0 +1,23 @@ +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation +{ + public class LoggingHandler : DelegatingHandler + { + public LoggingHandler(HttpMessageHandler innerHandler) + : base(innerHandler) + { + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Log.Debug("{Function} Request: {Request}", nameof(HttpClient.SendAsync), Newtonsoft.Json.JsonConvert.SerializeObject(request)); + + var response = await base.SendAsync(request, cancellationToken); + + Log.Debug("{Function} Response: StatusCode={StatusCode} \nContent={Content}", nameof(HttpClient.SendAsync), response.StatusCode, await response.Content.ReadAsStringAsync()); + + return response; + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/AccessToken.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/AccessToken.cs new file mode 100644 index 0000000..4b8357a --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/AccessToken.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + /// + /// Access token + /// + public class AccessToken + { + [JsonProperty("access_token")] + public string Token { get; set; } + [JsonProperty("expires_in")] + public int ExpiresIn { get; set; } + [JsonProperty("token_type")] + public string TokenType { get; set; } + [JsonProperty("Scope")] + public string Scope { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/AuthError.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/AuthError.cs new file mode 100644 index 0000000..e178559 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/AuthError.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + /// + /// This error is used for deserializing errors that we get during the authetication process, so that we can check the properties against exceptions instead of hard-coded strings + /// + public class AuthError + { + [JsonProperty("error")] + public string Code { get; set; } + + [JsonProperty("error_description")] + public string? Description { get; set; } + + public AuthError(string code, string? description = null) + { + this.Code = code; + this.Description = description; + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/CdrErrorAttribute.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/CdrErrorAttribute.cs new file mode 100644 index 0000000..340b0b6 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/CdrErrorAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + public class CdrErrorAttribute : Attribute + { + public CdrErrorAttribute(string title, string errorCode) + { + Title = title; + ErrorCode = errorCode; + } + + public string Title { get; set; } + + public string ErrorCode { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/CdsErrorInfo.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/CdsErrorInfo.cs new file mode 100644 index 0000000..5508c08 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/CdsErrorInfo.cs @@ -0,0 +1,9 @@ +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + public class CdsErrorInfo + { + public string Title { get; set; } + + public string ErrorCode { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/DataRecipientBrand.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/DataRecipientBrand.cs new file mode 100644 index 0000000..ffc9b77 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/DataRecipientBrand.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + public class DataRecipientBrand + { + [JsonProperty("dataRecipientBrandId")] + public string DataRecipientBrandId { get; set; } + [JsonProperty("brandName")] + public string BrandName { get; set; } + [JsonProperty("logoUri")] + public string LogoUri { get; set; } + [JsonProperty("status")] + public string Status { get; set; } + [JsonProperty("softwareProducts")] + public List SoftwareProducts { get; set; } + } + +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Accounts/BankingAccountV2.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Accounts/BankingAccountV2.cs new file mode 100644 index 0000000..22626b4 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Accounts/BankingAccountV2.cs @@ -0,0 +1,62 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders.Banking.Accounts +{ + + public class BankingAccountV2 + { + /// + /// A unique ID of the account adhering to the standards for ID permanence. + /// + [JsonProperty("accountId", Required = Required.Always)] + public string AccountId { get; set; } + /// + /// Date that the account was created (if known). + /// + [JsonProperty("creationDate")] + public string? CreationDate { get; set; } + /// + /// The display name of the account as defined by the bank. This should not incorporate account numbers or PANs. + /// If it does the values should be masked according to the rules of the MaskedAccountString common type. + /// + [JsonProperty("displayName", Required = Required.Always)] + public string DisplayName { get; set; } + /// + /// A customer supplied nick name for the account. + /// + [JsonProperty("nickname")] + public string? Nickname { get; set; } + /// + /// Open or closed status for the account. If not present then OPEN is assumed. + /// + [JsonProperty("openStatus")] + public string? OpenStatus { get; set; } + /// + /// Flag indicating that the customer associated with the authorisation is an owner of the account. Does not indicate sole ownership, however. If not present then 'true' is assumed. + /// + [JsonProperty("isOwned")] + public bool? IsOwned { get; set; } + /// + /// Value indicating the number of customers that have ownership of the account, according to the data holder's definition of account ownership. + /// Does not indicate that all account owners are eligible consumers. + /// NOTE: Currently not used but included to match latest standards. Actually mandatory but as we haven't implemented it yet, marked as optional + /// + [JsonProperty("accountOwnership")] + public bool? AccountOwnership { get; set; } + /// + /// A masked version of the account. Whether BSB/Account Number, Credit Card PAN or another number + /// + [JsonProperty("maskedNumber", Required = Required.Always)] + public string MaskedNumber { get; set; } + /// + /// The category to which a product or account belongs. + /// + [JsonProperty("productCategory", Required = Required.Always)] + public string ProductCategory { get; set; } + /// + /// The unique identifier of the account as defined by the data holder (akin to model number for the account). + /// + [JsonProperty("productName", Required = Required.Always)] + public string ProductName { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Accounts/Data.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Accounts/Data.cs new file mode 100644 index 0000000..f2b9735 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Accounts/Data.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders.Banking.Accounts +{ + public class Data + { + /// + /// Array of accounts + /// + [JsonProperty("accounts", Required = Required.Always)] + public BankingAccountV2[]? Accounts { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Accounts/ResponseBankingAccountListV2.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Accounts/ResponseBankingAccountListV2.cs new file mode 100644 index 0000000..29116f7 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Accounts/ResponseBankingAccountListV2.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders.Banking.Accounts +{ + public class ResponseBankingAccountListV2 + { + [JsonProperty("data", Required = Required.Always)] + public Data Data { get; set; } + + [JsonProperty("links", Required = Required.Always)] + public LinksPaginated Links { get; set; } + + [JsonProperty("meta", Required = Required.Always)] + public MetaPaginated Meta { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Transactions/BankingTransaction.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Transactions/BankingTransaction.cs new file mode 100644 index 0000000..41e5e21 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Transactions/BankingTransaction.cs @@ -0,0 +1,105 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders.Banking.Transactions +{ + public class BankingTransaction + { + /// + /// ID of the account for which transactions are provided + /// + [JsonProperty("accountId", Required = Required.Always)] + public string AccountId { get; set; } + /// + /// A unique ID of the transaction adhering to the standards for ID permanence. + /// This is mandatory (through hashing if necessary) unless there are specific and justifiable technical reasons why a transaction cannot be uniquely identified for a particular account type. + /// It is mandatory if isDetailAvailable is set to true. + /// + [JsonProperty("transactionId")] + public string? TransactionId { get; set; } + /// + /// True if extended information is available using the transaction detail end point. False if extended data is not available. + /// + [JsonProperty("isDetailAvailable", Required = Required.Always)] + public bool IsDetailAvailable { get; set; } + /// + /// The type of the transaction + /// + [JsonProperty("type", Required = Required.Always)] + public string Type { get; set; } + /// + /// Status of the transaction whether pending or posted. + /// Note that there is currently no provision in the standards to guarantee the ability to correlate a pending transaction with an associated posted transaction. + /// + [JsonProperty("status", Required = Required.Always)] + public string Status { get; set; } + /// + /// The transaction description as applied by the financial institution. + /// + [JsonProperty("description", Required = Required.Always)] + public string Description { get; set; } + /// + /// The time the transaction was posted. + /// This field is Mandatory if the transaction has status POSTED. This is the time that appears on a standard statement. + /// + [JsonProperty("postingDateTime")] + public string? PostingDateTime { get; set; } + /// + /// Date and time at which assets become available to the account owner in case of a credit entry, or cease to be available to the account owner in case of a debit transaction entry. + /// + [JsonProperty("valueDateTime")] + public string? ValueDateTime { get; set; } + /// + /// The time the transaction was executed by the originating customer, if available. + /// + [JsonProperty("executionDateTime")] + public string? ExecutionDateTime { get; set; } + /// + /// The value of the transaction. Negative values mean money was outgoing from the account. + /// + [JsonProperty("amount", Required = Required.Always)] + public string Amount { get; set; } + /// + /// The currency for the transaction amount. AUD assumed if not present. + /// + [JsonProperty("currency")] + public string? Currency { get; set; } + /// + /// The reference for the transaction provided by the originating institution. Empty string if no data provided. + /// + [JsonProperty("reference", Required = Required.Always)] + public string Reference { get; set; } + /// + /// Name of the merchant for an outgoing payment to a merchant. + /// + [JsonProperty("merchantName")] + public string? MerchantName { get; set; } + /// + /// The merchant category code (or MCC) for an outgoing payment to a merchant. + /// + [JsonProperty("merchantCategoryCode")] + public string? MerchantCategoryCode { get; set; } + /// + /// BPAY Biller Code for the transaction (if available). + /// + [JsonProperty("billerCode")] + public string? BillerCode { get; set; } + /// + /// Name of the BPAY biller for the transaction (if available) + /// + [JsonProperty("billerName")] + public string? BillerName { get; set; } + /// + /// BPAY CRN for the transaction (if available). + /// Where the CRN contains sensitive information, it should be masked in line with how the Data Holder currently displays account identifiers in their existing online banking channels. + /// If the contents of the CRN match the format of a Credit Card PAN they should be masked according to the rules applicable for MaskedPANString. + /// If the contents are otherwise sensitive, then it should be masked using the rules applicable for the MaskedAccountString common type. + /// + [JsonProperty("crn")] + public string? Crn { get; set; } + /// + /// 6 Digit APCA number for the initiating institution. The field is fixed-width and padded with leading zeros if applicable. + /// + [JsonProperty("apcaNumber")] + public string? ApcaNumber { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Transactions/Data.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Transactions/Data.cs new file mode 100644 index 0000000..917395a --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Transactions/Data.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders.Banking.Transactions +{ + public class Data + { + /// + /// Array of transactions + /// + [JsonProperty("transactions", Required = Required.Always)] + public BankingTransaction[] Transactions { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Transactions/ResponseBankingTransactionListV2.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Transactions/ResponseBankingTransactionListV2.cs new file mode 100644 index 0000000..364afd2 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Banking/Transactions/ResponseBankingTransactionListV2.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders.Banking.Transactions +{ + public class ResponseBankingTransactionListV2 + { + [JsonProperty("data", Required = Required.Always)] + public Data Data { get; set; } + + [JsonProperty("links", Required = Required.Always)] + public LinksPaginated Links { get; set; } + + [JsonProperty("meta", Required = Required.Always)] + public MetaPaginated Meta { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/Data.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/Data.cs new file mode 100644 index 0000000..153b1eb --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/Data.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders.Energy +{ + public class Data + { + /// + /// Array of accounts + /// + [JsonProperty("accounts", Required = Required.Always)] + public EnergyAccountV2[] Accounts { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/EnergyAccountV2.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/EnergyAccountV2.cs new file mode 100644 index 0000000..ff98f2d --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/EnergyAccountV2.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders.Energy +{ + public class EnergyAccountV2 + { + /// + /// A unique ID of the account adhering to the standards for ID permanence. + /// + [JsonProperty("accountId", Required = Required.Always)] + public string AccountId { get; set; } + /// + /// Optional identifier of the account as defined by the data holder. + /// This must be the value presented on physical statements (if it exists) and must not be used for the value of accountId. + /// + [JsonProperty("accountNumber")] + public string? AccountNumber { get; set; } + /// + /// The date that the account was created or opened. Mandatory if openStatus is OPEN + /// + [JsonProperty("creationDate")] + public string? CreationDate { get; set; } + /// + /// An optional display name for the account if one exists or can be derived. + /// The content of this field is at the discretion of the data holder. + /// + [JsonProperty("displayName")] + public string? DisplayName { get; set; } + /// + /// Open or closed status for the account. If not present then OPEN is assumed. + /// + [JsonProperty("openStatus")] + public string? OpenStatus { get; set; } + /// + /// The array of plans containing service points and associated plan details. + /// + [JsonProperty("plans", Required = Required.Always)] + public Plan[] Plans { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/Plan.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/Plan.cs new file mode 100644 index 0000000..76c0097 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/Plan.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders.Energy +{ + /// + /// Used by Energy Accounts + /// + public class Plan + { + /// + /// Optional display name for the plan provided by the customer to help differentiate multiple plans. + /// + [JsonProperty("nickname")] + public string? Nickname { get; set; } + /// + /// An array of servicePointIds, representing NMIs, that this plan is linked to. + /// If there are no service points allocated to this plan then an empty array would be expected. + /// + [JsonProperty("servicePointIds", Required = Required.Always)] + public string[] ServicePointIds { get; set; } + /// + /// Mandatory if openStatus is OPEN + /// + [JsonProperty("planOverview")] + public PlanOverview? PlanOverview { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/PlanOverview.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/PlanOverview.cs new file mode 100644 index 0000000..224bc4b --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/PlanOverview.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders.Energy +{ + /// + /// Used by Energy Accounts/Plans + /// + public class PlanOverview + { + /// + /// The name of the plan if one exists. + /// + [JsonProperty("displayName")] + public string? DisplayName { get; set; } + /// + /// The start date of the applicability of this plan + /// + [JsonProperty("startDate", Required = Required.Always)] + public string StartDate { get; set; } + /// + /// The end date of the applicability of this plan + /// + [JsonProperty("endDate")] + public string? EndDate { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/ResponseEnergyAccountListV2.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/ResponseEnergyAccountListV2.cs new file mode 100644 index 0000000..7613b7b --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/Energy/ResponseEnergyAccountListV2.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders.Energy +{ + public class ResponseEnergyAccountListV2 + { + /// + /// Array of accounts. + /// + [JsonProperty("data", Required = Required.Always)] + public Data Data { get; set; } + + [JsonProperty("links", Required = Required.Always)] + public LinksPaginated Links { get; set; } + + [JsonProperty("meta", Required = Required.Always)] + public MetaPaginated Meta { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/LinksPaginated.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/LinksPaginated.cs new file mode 100644 index 0000000..a960c71 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/LinksPaginated.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders +{ + public class LinksPaginated + { + /// + /// URI to the first page of this set. Mandatory if this response is not the first page. + /// + [JsonProperty("first")] + public string? First { get; set; } + /// + /// URI to the last page of this set. Mandatory if this response is not the last page. + /// + [JsonProperty("last")] + public string? Last { get; set; } + /// + /// URI to the next page of this set. Mandatory if this response is not the last page. + /// + [JsonProperty("next")] + public string? Next { get; set; } + /// + /// URI to the previous page of this set. Mandatory if this response is not the first page. + /// + [JsonProperty("prev")] + public string? Prev { get; set; } + /// + /// Fully qualified link that generated the current response document. + /// + [JsonProperty("self", Required = Required.Always)] + public string Self { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/MetaPaginated.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/MetaPaginated.cs new file mode 100644 index 0000000..a896033 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/MetaPaginated.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders +{ + public class MetaPaginated + { + /// + /// The total number of records in the full set. + /// + [JsonProperty("totalRecords", Required = Required.Always)] + public long TotalRecords { get; set; } + /// + /// The total number of pages in the full set. + /// + [JsonProperty("totalPages", Required = Required.Always)] + public long TotalPages { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/TokenResponse.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/TokenResponse.cs new file mode 100644 index 0000000..d9fc164 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Dataholders/TokenResponse.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders +{ + + public class TokenResponse + { + [JsonProperty("token_type")] + public string? TokenType { get; set; } + + [JsonProperty("expires_in")] + public int? ExpiresIn { get; set; } + + [JsonProperty("access_token")] + public string? AccessToken { get; set; } + + [JsonProperty("id_token")] + public string? IdToken { get; set; } + + [JsonProperty("refresh_token")] + public string? RefreshToken { get; set; } + + [JsonProperty("cdr_arrangement_id")] + public string? CdrArrangementId { get; set; } + + [JsonProperty("scope")] + public string? Scope { get; set; } + }; + +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Error.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Error.cs new file mode 100644 index 0000000..aa8f211 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Error.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + public class Error + { + public Error() + { + } + + public Error(string code, string title, string detail) + { + Code = code; + Title = title; + Detail = detail; + } + + /// + /// Error code. + /// + [Required] + public string Code { get; set; } + + /// + /// Error title. + /// + [Required] + public string Title { get; set; } + + /// + /// Error detail. + /// + [Required] + public string Detail { get; set; } + + /// + /// Optional additional data for specific error types. + /// + public object Meta { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/ErrorMetaV2.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/ErrorMetaV2.cs new file mode 100644 index 0000000..35a67c5 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/ErrorMetaV2.cs @@ -0,0 +1,12 @@ +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + public class ErrorMetaV2 + { + public ErrorMetaV2(string urn) + { + Urn = urn; + } + + public string Urn { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/ErrorV2.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/ErrorV2.cs new file mode 100644 index 0000000..2916e3d --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/ErrorV2.cs @@ -0,0 +1,44 @@ +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + public class ErrorV2 + { + public ErrorV2(string code, string title, string detail, string? metaUrn) + { + Code = code; + Title = title; + Detail = detail; + Meta = new object(); //TODO: This is a workaround. Meta should actually use the code below (matching with RAAP) but the PT APIs use this code, so until it is resolved we should match the test behaviour to the PT API. Bug 64152 + //Meta = metaUrn.IsNullOrWhiteSpace() ? null : new ErrorMetaV2(metaUrn); + } + + /// + /// Error code. + /// + [JsonProperty("code")] + [Required] + public string Code { get; set; } + + /// + /// Error title. + /// + [JsonProperty("title")] + [Required] + public string Title { get; set; } + + /// + /// Error detail. + /// + [JsonProperty("detail")] + [Required] + public string Detail { get; set; } + + /// + /// Optional additional data for specific error types. + /// + [JsonProperty("meta")] + public object Meta { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/JWK.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/JWK.cs new file mode 100644 index 0000000..f771914 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/JWK.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + public class Jwk + { + [JsonProperty("alg")] + public string? Alg { get; set; } + [JsonProperty("e")] + public string? E { get; set; } + //[JsonProperty("key_ops")] + // public string[]? key_ops { get; set; } + [JsonProperty("kid")] + public string? Kid { get; set; } + [JsonProperty("kty")] + public string? Kty { get; set; } + [JsonProperty("n")] + public string? N { get; set; } + [JsonProperty("use")] + public string? Use { get; set; } + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/JWKS.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/JWKS.cs new file mode 100644 index 0000000..b7a7c1e --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/JWKS.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + public class Jwks + { + [JsonProperty("keys")] + public Jwk[]? Keys { get; set; } + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/LegalEntity.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/LegalEntity.cs new file mode 100644 index 0000000..0b7fe85 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/LegalEntity.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + public class LegalEntity + { + [JsonProperty("legalEntityId")] + public string LegalEntityId { get; set; } + [JsonProperty("legalEntityName")] + public string LegalEntityName { get; set; } + [JsonProperty("logoUri")] + public string LogoUri { get; set; } + [JsonProperty("status")] + public string Status { get; set; } + [JsonProperty("dataRecipientBrands")] + public List DataRecipientBrands { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Options/TestAutomationAuthServerOptions.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Options/TestAutomationAuthServerOptions.cs new file mode 100644 index 0000000..b15bf32 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Options/TestAutomationAuthServerOptions.cs @@ -0,0 +1,28 @@ +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options +{ + public class TestAutomationAuthServerOptions + { + public string CDRAUTHSERVER_BASEURI { get; set; } + + // Running standalone CdrAuthServer? + public bool STANDALONE { get; set; } + + // X-TlsClientCertThumbprint header to add if running standalone + public string XTLSCLIENTCERTTHUMBPRINT { get; set; } + + // X-TlsClientCertThumbprint (for additional certificate) header to add if running standalone + public string XTLSADDITIONALCLIENTCERTTHUMBPRINT { get; set; } + + public int? ACCESSTOKENLIFETIMESECONDS { get; set; } + + /// + /// Running CdrAuthServer in headless mode (for authentication)? + /// + public bool HEADLESSMODE { get; set; } + + /// + /// Running CdrAuthServer with JARM encryption turned on? + /// + public bool JARM_ENCRYPTION_ON { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Options/TestAutomationOptions.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Options/TestAutomationOptions.cs new file mode 100644 index 0000000..ae2a301 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Options/TestAutomationOptions.cs @@ -0,0 +1,59 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options +{ + public class TestAutomationOptions + { + public bool IS_AUTH_SERVER { get; set; } = false; + public Industry INDUSTRY { get; set; } + public string SCOPE { get; set; } + public string DH_MTLS_GATEWAY_URL { get; set; } + + public string DH_MTLS_AUTHSERVER_TOKEN_URL => DH_MTLS_GATEWAY_URL + "/idp/connect/token"; + + public string DH_TLS_AUTHSERVER_BASE_URL { get; set; } + + public string DH_TLS_PUBLIC_BASE_URL { get; set; } + + public string REGISTER_MTLS_URL { get; set; } + + public string REGISTER_MTLS_TOKEN_URL => REGISTER_MTLS_URL + "/idp/connect/token"; + + public string REGISTRATION_AUDIENCE_URI => DH_TLS_AUTHSERVER_BASE_URL; + + //Migrated from Auth Server Settings as needed for DH tests + public string CDRAUTHSERVER_SECUREBASEURI { get; set; } + + + // Connection strings + public string DATAHOLDER_CONNECTIONSTRING { get; set; } + public string AUTHSERVER_CONNECTIONSTRING { get; set; } + public string REGISTER_CONNECTIONSTRING { get; set; } + + // Seed-data offset + public bool SEEDDATA_OFFSETDATES { get; set; } + + + public string MDH_INTEGRATION_TESTS_HOST { get; set; } + + public string SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS => $"{MDH_INTEGRATION_TESTS_HOST}:9999/consent/callback"; + + public string SOFTWAREPRODUCT_JWKS_URI_FOR_INTEGRATION_TESTS => $"{MDH_INTEGRATION_TESTS_HOST}:9998/jwks"; + + public string MDH_HOST { get; set; } + + public string ADDITIONAL_SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS => $"{MDH_INTEGRATION_TESTS_HOST}:9997/consent/callback"; + + public string ADDITIONAL_SOFTWAREPRODUCT_JWKS_URI_FOR_INTEGRATION_TESTS => $"{MDH_INTEGRATION_TESTS_HOST}:9996/jwks"; + + //For Playwright testing + public bool RUNNING_IN_CONTAINER { get; set; } = false; + public bool CREATE_MEDIA { get; set; } = false; + public int TEST_TIMEOUT { get; set; } = 30000; + public string MEDIA_FOLDER { get; set; } + + // Store dynamic client id for each successful Data Holder registration request + public string? LastRegisteredClientId { get; set; } + + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Person.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Person.cs new file mode 100644 index 0000000..8b70ed6 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/Person.cs @@ -0,0 +1,15 @@ +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + public class Person + { + public string PersonId { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string MiddleNames { get; set; } + public string Prefix { get; set; } + public string Suffix { get; set; } + public string OccupationCode { get; set; } + public string OccupationCodeVersion { get; set; } + public DateTime? LastUpdateTime { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/ResponseErrorList.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/ResponseErrorList.cs new file mode 100644 index 0000000..11e70a9 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/ResponseErrorList.cs @@ -0,0 +1,40 @@ +using System.ComponentModel.DataAnnotations; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions; +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + public class ResponseErrorList + { + public ResponseErrorList() + { + Errors = new List() { }; + } + + public ResponseErrorList(Error error) + { + Errors = new List() { error }; + } + + public ResponseErrorList(CdsError error, string errorDetail) + { + var errorInfo = error.GetErrorInfo(); + + Errors = new List() + { + new Error(errorInfo.ErrorCode, errorInfo.Title, errorDetail), + }; + } + + public ResponseErrorList(string errorCode, string errorTitle, string errorDetail) + { + var error = new Error(errorCode, errorTitle, errorDetail); + Errors = new List() { error }; + } + + [JsonProperty("errors")] + [Required] + public List Errors { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/ResponseErrorListV2.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/ResponseErrorListV2.cs new file mode 100644 index 0000000..f46996a --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/ResponseErrorListV2.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions; +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + public class ResponseErrorListV2 + { + public ResponseErrorListV2() + { + Errors = new List(); + } + + public ResponseErrorListV2(ErrorV2 error) + { + Errors = new List() { error }; + } + + public ResponseErrorListV2(string errorCode, string errorTitle, string errorDetail, string? metaUrn) + { + var error = new ErrorV2(errorCode, errorTitle, errorDetail, metaUrn); + Errors = new List() { error }; + } + + public ResponseErrorListV2(CdrException ex) + { + var error = new ErrorV2(ex.Code, ex.Title, ex.Detail, ex.Message); + Errors = new List() { error }; + } + + [JsonProperty("errors")] + [Required] + public List Errors { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/SoftwareProduct.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/SoftwareProduct.cs new file mode 100644 index 0000000..6b8b1b5 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Models/SoftwareProduct.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models +{ + public class SoftwareProduct + { + [JsonProperty("softwareProductId")] + public string SoftwareProductId { get; set; } + [JsonProperty("softwareProductName")] + public string SoftwareProductName { get; set; } + [JsonProperty("softwareProductDescription")] + public string SoftwareProductDescription { get; set; } + [JsonProperty("logoUri")] + public string LogoUri { get; set; } + [JsonProperty("status")] + public string Status { get; set; } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/README.md b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/README.md new file mode 100644 index 0000000..75779bc --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/README.md @@ -0,0 +1,57 @@ +![Consumer Data Right Logo](https://github.com/ConsumerDataRight/mock-solution-test-automation/blob/main/Assets/cdr-logo.png?raw=true) + +[![made-with-dotnet](https://img.shields.io/badge/Made%20with-.NET-1f425Ff.svg)](https://dotnet.microsoft.com/) +[![made-with-csharp](https://img.shields.io/badge/Made%20with-C%23-1f425Ff.svg)](https://docs.microsoft.com/en-us/dotnet/csharp/) +[![MIT License](https://img.shields.io/github/license/ConsumerDataRight/mock-solution-test-automation)](https://github.com/ConsumerDataRight/mock-solution-test-automation/blob/main/LICENSE) +[![Pull Requests Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/ConsumerDataRight/mock-solution-test-automation/blob/main/CONTRIBUTING.md) + +# Consumer Data Right - Mock Solution Test Automation + +**Note:** This repository is only relevant from version 1.1.0 of the [Authorisation Server](https://github.com/ConsumerDataRight/authorisation-server) and version 2.0.0 of the [Mock Data Holder](https://github.com/ConsumerDataRight/mock-data-holder) solutions. + +Check [here](https://github.com/ConsumerDataRight/authorisation-server/releases) for the latest Authorisation Server version and [here](https://github.com/ConsumerDataRight/mock-data-holder/releases) for the latest Mock Data Holder version. + + +This project includes source code and documentation for the Consumer Data Right (CDR) Mock Solution Test Automation NuGet package. + +It contains common source code that is used by the [Mock Data Holder](https://github.com/ConsumerDataRight/mock-data-holder) and [Authorisation Server](https://github.com/ConsumerDataRight/authorisation-server) test automation projects. + +The project is built and packaged as a NuGet package and available on [NuGet](https://www.nuget.org/packages/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation). + + +**Note:** This project is designed specifically for use in [CDR Mock Solutions](https://github.com/ConsumerDataRight) test projects. It is not intended to be used as a stand alone testing solution as it is tightly coupled with CDR Mock Solutions. + + +## Getting Started + +To get started, clone the source code. +``` +git clone https://github.com/ConsumerDataRight/mock-solution-test-automation.git +``` + +Documentation on how this project is used can be found in the test automation execution guides by following the links below: + +- [Authorisation Server Test Automation Execution Guide](https://github.com/ConsumerDataRight/authorisation-server/blob/main/Help/testing/HELP.md) +- [Mock Data Holder Test Automation Execution Guide](https://github.com/ConsumerDataRight/mock-data-holder/blob/main/Help/testing/HELP.md) + +## Technology Stack + +The following technologies have been used to build the Mock Solution Test Automation project: +- The source code has been written in `C#` using the `.Net 6` framework. +- `xUnit` is the framework used for writing and running tests. +- `Microsoft Playwright` is the framework used for Web Testing. + +# Contribute +We encourage contributions from the community. See our [contributing guidelines](https://github.com/ConsumerDataRight/mock-solution-test-automation/blob/main/CONTRIBUTING.md). + +# Code of Conduct +This project has adopted the **Contributor Covenant**. For more information see the [code of conduct](https://github.com/ConsumerDataRight/mock-solution-test-automation/blob/main/CODE_OF_CONDUCT.md). + + +# Security Policy +See our [security policy](https://github.com/ConsumerDataRight/mock-solution-test-automation/blob/main/SECURITY.md) for information on security controls, reporting a vulnerability and supported versions. +# License +[MIT License](https://github.com/ConsumerDataRight/mock-solution-test-automation/blob/main/LICENSE) + +# Notes +The Mock Solution Test Automation solution is provided as a development and testing tool that is used by the other mock solutions. \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/AccessTokenService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/AccessTokenService.cs new file mode 100644 index 0000000..b71b7a5 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/AccessTokenService.cs @@ -0,0 +1,133 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services +{ + public class AccessTokenService : IAccessTokenService + { + public AccessTokenService(string mtlsAuthServerTokenUrl) + { + URL = mtlsAuthServerTokenUrl; + Audience = mtlsAuthServerTokenUrl; + } + + private const string _defaultClientId = "86ecb655-9eba-409c-9be3-59e7adf7080d"; + + public string? CertificateFilename { get; set; } + public string? CertificatePassword { get; set; } + + public string? JwtCertificateFilename { get; set; } + public string? JwtCertificatePassword { get; set; } + + public string URL { get; init; } + public string Issuer { get; init; } = _defaultClientId; + public string Audience { get; init; } + public string Scope { get; init; } = "bank:accounts.basic:read"; + public string GrantType { get; init; } = ""; + public string ClientId { get; init; } = _defaultClientId; + public string ClientAssertionType { get; init; } = ""; + + /// + /// Get HttpRequestMessage for access token request + /// + private static HttpRequestMessage CreateAccessTokenRequest( + string url, + string jwtCertificateFilename, string jwtCertificatePassword, + string issuer, string audience, + string scope, string grantType, string clientId, string clientAssertionType) + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(CreateAccessTokenRequest), nameof(AccessTokenService)); + + static string BuildContent(string scope, string grant_type, string client_id, string client_assertion_type, string client_assertion) + { + var kvp = new KeyValuePairBuilder(); + + if (scope != null) + { + kvp.Add("scope", scope); + } + + if (grant_type != null) + { + kvp.Add("grant_type", grant_type); + } + + if (client_id != null) + { + kvp.Add("client_id", client_id); + } + + if (client_assertion_type != null) + { + kvp.Add("client_assertion_type", client_assertion_type); + } + + if (client_assertion != null) + { + kvp.Add("client_assertion", client_assertion); + } + + return kvp.Value; + } + + var client_assertion = new PrivateKeyJwtService + { + CertificateFilename = jwtCertificateFilename, + CertificatePassword = jwtCertificatePassword, + Issuer = issuer, + Audience = audience + }.Generate(); + + var request = new HttpRequestMessage(HttpMethod.Post, url) + { + Content = new StringContent( + BuildContent(scope, grantType, clientId, clientAssertionType, client_assertion), + Encoding.UTF8, + "application/json") + }; + + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + + return request; + } + + /// + /// Get an access token from Auth Server + /// + public async Task GetAsync(string dhMtlsGatewayUrl, string xtlsClientCertThumbprint, bool isStandalone) + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(GetAsync), nameof(AccessTokenService)); + + // Create HttpClient + using var client = Helpers.Web.CreateHttpClient(CertificateFilename, CertificatePassword); + + // Create an access token request + var request = CreateAccessTokenRequest( + URL, + JwtCertificateFilename ?? throw new InvalidOperationException($"{nameof(JwtCertificateFilename)} is null").Log(), + JwtCertificatePassword, + Issuer, Audience, + Scope, GrantType, ClientId, ClientAssertionType); + + Helpers.AuthServer.AttachHeadersForStandAlone(request.RequestUri?.AbsoluteUri ?? throw new NullReferenceException($"{nameof(request.RequestUri.AbsoluteUri)} is null").Log(), request.Headers, dhMtlsGatewayUrl, xtlsClientCertThumbprint, isStandalone); + + // Request the access token + var response = await client.SendAsync(request); + + if (response.StatusCode != HttpStatusCode.OK) + { + throw new InvalidOperationException($"{nameof(AccessTokenService)}.{nameof(GetAsync)} - Error getting access token - {response.StatusCode} - {await response.Content.ReadAsStringAsync()}").Log(); + } + + // Deserialize the access token from the response + var accessToken = Newtonsoft.Json.JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + + // And return the access token + return accessToken?.Token; + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/ApiService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/ApiService.cs new file mode 100644 index 0000000..db47a98 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/ApiService.cs @@ -0,0 +1,323 @@ +using System.Net.Http.Headers; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services +{ + /// + /// Call API + /// + public class ApiService : IApiService + { + /// + /// Filename of certificate to use. + /// If null then no certificate will be attached to the request. + /// + public string? CertificateFilename { get; private set; } = Constants.Certificates.CertificateFilename; + + /// + /// Password for certificate. + /// If null then no certificate password will be set. + /// + public string? CertificatePassword { get; private set; } = Constants.Certificates.CertificatePassword; + + /// + /// Access token. + /// If null then no access token will be attached to the request. + /// See the AccessToken class to generate an access token. + /// + public string? AccessToken { get; private set; } + + /// + /// The HttpMethod of the request. + /// + public HttpMethod? HttpMethod { get; private set; } + + /// + /// The URL of the request. + /// + public string? URL { get; private set; } + + /// + /// The x-v header. + /// If null then no x-v header will be set. + /// + public string? XV { get; private set; } + + /// + /// The x-min-v header. + /// If null then no x-min-v header will be set. + /// + public string? XMinV { get; private set; } + + /// + /// The If-None-Match header (an ETag). + /// If null then no If-None-Match header will be set. + /// + public string? IfNoneMatch { get; private set; } + + /// + /// The x_fapi_auth_date header. + /// If null then no x_fapi_auth_date header will be set. + /// + public string? XFapiAuthDate { get; private set; } + + /// + /// The x-fapi-interaction-id header. + /// If null then no x-fapi-interaction-id header will be set. + /// + public string? XFapiInteractionId { get; private set; } + + /// + /// Content + /// If null then no content is set. + /// + public HttpContent? Content { get; private set; } + + /// + /// Content.Headers.ContentType + /// If null then Content.Headers.ContentType is not set. + /// + public MediaTypeHeaderValue? ContentType { get; private set; } + + /// + /// Request.Headers.Accept + /// If null then Request.Headers.Accept is not set. + /// + public string? Accept { get; private set; } + + public IEnumerable? Cookies { get; private set; } + + /// + /// Set authentication header explicity. Can't be used if AccessToken is set + /// + public AuthenticationHeaderValue? AuthenticationHeaderValue { get; } + + /// + /// Running standalone CdrAuthServer (ie no MtlsGateway) + /// + public bool IsStandalone { get; private set; } = false; + + /// + /// Dh Mtls Gateway Url + /// Only needed if isStandalone is true + /// + public string? DhMtlsGatewayUrl { get; private set; } + + /// + /// Xtls Client Certificate Thumbprint + /// Only needed if isStandalone is true + /// + public string? XtlsClientCertificateThumbprint { get; private set; } + + private ApiService() { } //restricts class instantiation to builder only + + /// + /// Send a request to the API. + /// + /// The API response + public async Task SendAsync(bool allowAutoRedirect = true, string? xtlsThumbprint = null) + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(SendAsync), nameof(ApiService)); + // Build request + HttpRequestMessage BuildRequest() + { + if (HttpMethod == null) { throw new InvalidOperationException($"{nameof(ApiService)}.{nameof(SendAsync)}.{nameof(BuildRequest)} - {nameof(HttpMethod)} not set").Log(); } + if (URL == null) { throw new InvalidOperationException($"{nameof(ApiService)}.{nameof(SendAsync)}.{nameof(BuildRequest)} - {nameof(URL)} not set").Log(); } + + var request = new HttpRequestMessage(HttpMethod, URL); + + // Attach access token if provided + if (AccessToken != null) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken); + } + + // Set AuthenticationHeaderValue explicity + if (AuthenticationHeaderValue != null) + { + if (AccessToken != null) + { + throw new InvalidOperationException($"{nameof(ApiService)}.{nameof(SendAsync)} - Can't use both AccessToken and AuthenticationHeaderValue.").Log(); + } + + request.Headers.Authorization = AuthenticationHeaderValue; + } + + // Set x-v header if provided + if (XV != null) + { + request.Headers.Add("x-v", XV); + } + + // Set x-min-v header if provided + if (XMinV != null) + { + request.Headers.Add("x-min-v", XMinV); + } + + // Set If-None-Match header if provided + if (IfNoneMatch != null) + { + request.Headers.Add("If-None-Match", $"\"{IfNoneMatch}\""); + } + + // Set x-fapi-auth-date header if provided + if (XFapiAuthDate != null) + { + request.Headers.Add("x-fapi-auth-date", XFapiAuthDate); + } + + // Set x-fapi-interaction-id header if provided + if (XFapiInteractionId != null) + { + request.Headers.Add("x-fapi-interaction-id", XFapiInteractionId); + } + + // Set content + if (Content != null) + { + request.Content = Content; + + // Set content type + if (ContentType != null) + { + request.Content.Headers.ContentType = ContentType; + } + } + + // Set request Accept header + if (Accept != null) + { + request.Headers.TryAddWithoutValidation("Accept", Accept); + } + + return request; + } + + // Send request and return response + async Task SendRequest(HttpRequestMessage request) + { + using var client = Helpers.Web.CreateHttpClient(CertificateFilename, CertificatePassword, allowAutoRedirect, Cookies, request); + + Helpers.AuthServer.AttachHeadersForStandAlone(request.RequestUri?.AbsoluteUri ?? throw new NullReferenceException(), request.Headers, DhMtlsGatewayUrl, XtlsClientCertificateThumbprint, IsStandalone); + + var response = await client.SendAsync(request); + + return response; + } + + var request = BuildRequest(); + var response = await SendRequest(request); + return response; + } + + public class ApiServiceBuilder : IBuilder + { + public ApiServiceBuilder() { } + + private readonly ApiService _api = new ApiService(); + + public ApiServiceBuilder WithUrl(string? value) + { + _api.URL = value; + return this; + } + public ApiServiceBuilder WithXV(string? value) + { + _api.XV = value; + return this; + } + public ApiServiceBuilder WithXFapiAuthDate(string? value) + { + _api.XFapiAuthDate = value; + return this; + } + public ApiServiceBuilder WithAccessToken(string? value) + { + _api.AccessToken = value; + return this; + } + public ApiServiceBuilder WithXFapiInteractionId(string? value) + { + _api.XFapiInteractionId = value; + return this; + } + public ApiServiceBuilder WithHttpMethod(HttpMethod? value) + { + _api.HttpMethod = value; + return this; + } + + public ApiServiceBuilder WithContent(HttpContent? value) + { + _api.Content = value; + return this; + } + + public ApiServiceBuilder WithContentType(MediaTypeHeaderValue? value) + { + _api.ContentType = value; + return this; + } + public ApiServiceBuilder WithAccept(string? value) + { + _api.Accept = value; + return this; + } + + public ApiServiceBuilder WithXMinV(string? value) + { + _api.XMinV = value; + return this; + } + + public ApiServiceBuilder WithIfNoneMatch(string? value) + { + _api.IfNoneMatch = value; + return this; + } + + public ApiServiceBuilder WithCookies(IEnumerable? value) + { + _api.Cookies = value; + return this; + } + + public ApiServiceBuilder WithCertificateFilename(string? value) + { + _api.CertificateFilename = value; + return this; + } + + public ApiServiceBuilder WithCertificatePassword(string? value) + { + _api.CertificatePassword = value; + return this; + } + + public ApiServiceBuilder WithIsStandalone(bool value) + { + _api.IsStandalone = value; + return this; + } + public ApiServiceBuilder WithDhMtlsGatewayUrl(string? value) + { + _api.DhMtlsGatewayUrl = value; + return this; + } + public ApiServiceBuilder WithXtlsClientCertificateThumbprint(string? value) + { + _api.XtlsClientCertificateThumbprint = value; + return this; + } + + public ApiService Build() + { + Log.Information("Building a {BuiltClass} using {BuilderClass}.", nameof(ApiService), nameof(ApiServiceBuilder)); + return _api; + } + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/ApiServiceDirector.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/ApiServiceDirector.cs new file mode 100644 index 0000000..01c6493 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/ApiServiceDirector.cs @@ -0,0 +1,297 @@ +using System.Net.Http.Headers; +using System.Text; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Options; +using Serilog; +using static ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services.ApiService; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services +{ + public class ApiServiceDirector : IApiServiceDirector + { + private readonly TestAutomationOptions _options; + private readonly TestAutomationAuthServerOptions _authServerOptions; + private ApiServiceBuilder _builder; + + public ApiServiceDirector(IOptions options, IOptions authServerOptions) + { + _options = options.Value ?? throw new ArgumentNullException(nameof(options)); + _authServerOptions = authServerOptions.Value ?? throw new ArgumentNullException(nameof(authServerOptions)); + } + + public ApiService BuildUserInfoAPI(string? xv, string? accessToken, string? thumbprint, HttpMethod? httpMethod, string certFilename = Constants.Certificates.CertificateFilename, string certPassword = Constants.Certificates.CertificatePassword) + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(BuildUserInfoAPI), nameof(ApiServiceDirector)); + + _builder = new ApiServiceBuilder(); + + return _builder + .WithHttpMethod(HttpMethod.Get) + .WithUrl($"{_options.DH_MTLS_GATEWAY_URL}/connect/userinfo") + .WithXV(xv) + .WithXFapiAuthDate(DateTime.Now.ToUniversalTime().ToString("r")) + .WithAccessToken(accessToken) + .WithHttpMethod(httpMethod) + .WithCertificateFilename(certFilename) + .WithCertificatePassword(certPassword) + .WithIsStandalone(_authServerOptions.STANDALONE) + .WithDhMtlsGatewayUrl(_options.DH_MTLS_GATEWAY_URL) + .WithXtlsClientCertificateThumbprint(thumbprint) + .Build(); + } + + public ApiService BuildAuthServerAuthorizeAPI(Dictionary queryString) + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(BuildAuthServerAuthorizeAPI), nameof(ApiServiceDirector)); + + _builder = new ApiServiceBuilder(); + + return _builder + .WithHttpMethod(HttpMethod.Get) + .WithUrl(QueryHelpers.AddQueryString($"{_options.DH_TLS_AUTHSERVER_BASE_URL}/connect/authorize", queryString)) + .Build(); + } + + public ApiService BuildDataholderRegisterAPI(string? accessToken, string? registrationRequest, HttpMethod? httpMethod, string clientId = "") + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(BuildDataholderRegisterAPI), nameof(ApiServiceDirector)); + + _builder = new ApiServiceBuilder(); + + var additionalUrl = clientId.IsNullOrWhiteSpace() ? "" : $"/{clientId}"; + + _builder + .WithUrl($"{_options.DH_MTLS_GATEWAY_URL}/connect/register{additionalUrl}") + .WithHttpMethod(httpMethod) + .WithAccessToken(accessToken) + .WithIsStandalone(_authServerOptions.STANDALONE) + .WithDhMtlsGatewayUrl(_options.DH_MTLS_GATEWAY_URL) + .WithXtlsClientCertificateThumbprint(_authServerOptions.XTLSCLIENTCERTTHUMBPRINT); + + if (registrationRequest != null) + { + _builder + .WithContent(new StringContent(registrationRequest, Encoding.UTF8, "application/jwt")) + .WithContentType(MediaTypeHeaderValue.Parse("application/jwt")) + .WithAccept("application/json"); + } + + return _builder.Build(); + } + + public ApiService BuildRegisterSSAAPI(Industry? industry, string brandId, string softwareProductId, string? accessToken, string? xv) + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(BuildRegisterSSAAPI), nameof(ApiServiceDirector)); + + _builder = new ApiServiceBuilder(); + + return _builder + .WithUrl($"{_options.REGISTER_MTLS_URL}/cdr-register/v1/{industry}/data-recipients/brands/{brandId}/software-products/{softwareProductId}/ssa") + .WithHttpMethod(HttpMethod.Get) + .WithAccessToken(accessToken) + .WithXV(xv) + .WithIsStandalone(_authServerOptions.STANDALONE) + .WithDhMtlsGatewayUrl(_options.DH_MTLS_GATEWAY_URL) + .WithXtlsClientCertificateThumbprint(_authServerOptions.XTLSCLIENTCERTTHUMBPRINT) + .Build(); + } + public ApiService BuildAuthServerOpenIdConfigurationAPI() + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(BuildAuthServerOpenIdConfigurationAPI), nameof(ApiServiceDirector)); + + _builder = new ApiServiceBuilder(); + + return _builder + .WithUrl($"{_authServerOptions.CDRAUTHSERVER_BASEURI}/.well-known/openid-configuration") + .WithHttpMethod(HttpMethod.Get) + .WithIsStandalone(_authServerOptions.STANDALONE) + .WithDhMtlsGatewayUrl(_options.DH_MTLS_GATEWAY_URL) + .WithXtlsClientCertificateThumbprint(_authServerOptions.XTLSCLIENTCERTTHUMBPRINT) + .Build(); + } + public ApiService BuildCustomerResourceAPI(string? accessToken) + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(BuildCustomerResourceAPI), nameof(ApiServiceDirector)); + + _builder = new ApiServiceBuilder(); + + return _builder + .WithUrl($"{_options.DH_MTLS_GATEWAY_URL}/resource/cds-au/v1/common/customer") + .WithHttpMethod(HttpMethod.Get) + .WithXV("1") + .WithXFapiAuthDate(DateTime.Now.ToUniversalTime().ToString("r")) + .WithAccessToken(accessToken) + .WithIsStandalone(_authServerOptions.STANDALONE) + .WithDhMtlsGatewayUrl(_options.DH_MTLS_GATEWAY_URL) + .WithXtlsClientCertificateThumbprint(_authServerOptions.XTLSCLIENTCERTTHUMBPRINT) + .Build(); + } + + public ApiService BuildAuthServerJWKSAPI() + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(BuildAuthServerJWKSAPI), nameof(ApiServiceDirector)); + + _builder = new ApiServiceBuilder(); + + return _builder + .WithUrl($"{_authServerOptions.CDRAUTHSERVER_BASEURI}/.well-known/openid-configuration/jwks") + .WithHttpMethod(HttpMethod.Get) + .WithIsStandalone(_authServerOptions.STANDALONE) + .WithDhMtlsGatewayUrl(_options.DH_MTLS_GATEWAY_URL) + .WithXtlsClientCertificateThumbprint(_authServerOptions.XTLSCLIENTCERTTHUMBPRINT) + .Build(); + } + + public ApiService BuildDataHolderDiscoveryStatusAPI() + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(BuildDataHolderDiscoveryStatusAPI), nameof(ApiServiceDirector)); + + _builder = new ApiServiceBuilder(); + + return _builder + .WithHttpMethod(HttpMethod.Get) + .WithXV("1") + .WithUrl($"{_options.DH_TLS_PUBLIC_BASE_URL}/cds-au/v1/discovery/status") + .Build(); + } + + public ApiService BuildDataHolderDiscoveryOutagesAPI() + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(BuildDataHolderDiscoveryOutagesAPI), nameof(ApiServiceDirector)); + + _builder = new ApiServiceBuilder(); + + return _builder + .WithHttpMethod(HttpMethod.Get) + .WithXV("1") + .WithUrl($"{_options.DH_TLS_PUBLIC_BASE_URL}/cds-au/v1/discovery/outages") + .Build(); + } + + public ApiService BuildDataHolderCommonGetCustomerAPI(string? accessToken, string? xFapiAuthDate, string? xv = "1", string certFileName = Constants.Certificates.CertificateFilename, string certPassword = Constants.Certificates.CertificatePassword) + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(BuildDataHolderCommonGetCustomerAPI), nameof(ApiServiceDirector)); + + _builder = new ApiServiceBuilder(); + + return _builder + .WithCertificateFilename(certFileName) + .WithCertificatePassword(certPassword) + .WithHttpMethod(HttpMethod.Get) + .WithXV(xv) + .WithUrl($"{_options.DH_MTLS_GATEWAY_URL}/cds-au/v1/common/customer") + .WithXFapiAuthDate(xFapiAuthDate) + .WithAccessToken(accessToken) + .Build(); + } + public ApiService BuildDataHolderBankingGetAccountsAPI(string? accessToken, string? xFapiAuthDate, string? xv = "1", string? xFapiInteractionId = null, string certFileName = Constants.Certificates.CertificateFilename, string certPassword = Constants.Certificates.CertificatePassword, string? url = null) + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(BuildDataHolderBankingGetAccountsAPI), nameof(ApiServiceDirector)); + + _builder = new ApiServiceBuilder(); + + _builder = _builder + .WithCertificateFilename(certFileName) + .WithCertificatePassword(certPassword) + .WithHttpMethod(HttpMethod.Get) + .WithXV(xv) + .WithUrl(url ?? $"{_options.DH_MTLS_GATEWAY_URL}/cds-au/v1/banking/accounts") + .WithXFapiAuthDate(xFapiAuthDate) + .WithAccessToken(accessToken); + + if (!xFapiInteractionId.IsNullOrWhiteSpace()) + { + _builder = _builder.WithXFapiInteractionId(xFapiInteractionId); + } + + return _builder.Build(); + } + + public ApiService BuildDataHolderBankingGetTransactionsAPI(string? accessToken, string? xFapiAuthDate, string? encryptedAccountId = null, string? xv = "1", string? xFapiInteractionId = null, string certFileName = Constants.Certificates.CertificateFilename, string certPassword = Constants.Certificates.CertificatePassword, string? url = null) + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(BuildDataHolderBankingGetTransactionsAPI), nameof(ApiServiceDirector)); + + if (encryptedAccountId != null && url != null) + { + throw new ArgumentException($"Error: {nameof(BuildDataHolderBankingGetTransactionsAPI)} - Can't provide both {nameof(encryptedAccountId)} and {nameof(url)}"); + } + + _builder = new ApiServiceBuilder(); + + _builder = _builder + .WithCertificateFilename(certFileName) + .WithCertificatePassword(certPassword) + .WithHttpMethod(HttpMethod.Get) + .WithXV(xv) + .WithUrl(url ?? $"{_options.DH_MTLS_GATEWAY_URL}/cds-au/v1/banking/accounts/{encryptedAccountId}/transactions") + .WithAccessToken(accessToken); + + if (!xFapiAuthDate.IsNullOrWhiteSpace()) + { + _builder = _builder.WithXFapiAuthDate(xFapiAuthDate); + } + + if (!xFapiInteractionId.IsNullOrWhiteSpace()) + { + _builder = _builder.WithXFapiInteractionId(xFapiInteractionId); + } + + return _builder.Build(); + } + + public ApiService BuildDataHolderEnergyGetAccountsAPI(string? accessToken, string? xFapiAuthDate, string? xv = "1", string? xMinV = null, string? xFapiInteractionId = null, string certFileName = Constants.Certificates.CertificateFilename, string certPassword = Constants.Certificates.CertificatePassword, string? url = null) + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(BuildDataHolderEnergyGetAccountsAPI), nameof(ApiServiceDirector)); + + _builder = new ApiServiceBuilder(); + + _builder = _builder + .WithCertificateFilename(certFileName) + .WithCertificatePassword(certPassword) + .WithHttpMethod(HttpMethod.Get) + .WithXV(xv) + .WithUrl(url ?? $"{_options.DH_MTLS_GATEWAY_URL}/cds-au/v1/energy/accounts") + .WithXFapiAuthDate(xFapiAuthDate) + .WithAccessToken(accessToken); + + if (!xFapiInteractionId.IsNullOrWhiteSpace()) + { + _builder = _builder.WithXFapiInteractionId(xFapiInteractionId); + } + + if (!xMinV.IsNullOrWhiteSpace()) + { + _builder = _builder.WithXMinV(xMinV); + } + + return _builder.Build(); + } + + public ApiService BuildDataHolderEnergyGetConcessionsAPI(string? accessToken, string? xFapiAuthDate, string? encryptedAccountId = null, string? xv = "1", string certFileName = Constants.Certificates.CertificateFilename, string certPassword = Constants.Certificates.CertificatePassword, string? url = null) + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(BuildDataHolderEnergyGetConcessionsAPI), nameof(ApiServiceDirector)); + + _builder = new ApiServiceBuilder(); + + if (encryptedAccountId != null && url != null) + { + throw new ArgumentException($"Error: {nameof(BuildDataHolderEnergyGetConcessionsAPI)} - Can't provide both {nameof(encryptedAccountId)} and {nameof(url)}").Log(); + } + + _builder = _builder + .WithCertificateFilename(certFileName) + .WithCertificatePassword(certPassword) + .WithHttpMethod(HttpMethod.Get) + .WithXV(xv) + .WithUrl(url ?? $"{_options.DH_MTLS_GATEWAY_URL}/cds-au/v1/energy/accounts/{encryptedAccountId}/concessions") + .WithXFapiAuthDate(xFapiAuthDate) + .WithAccessToken(accessToken); + + return _builder.Build(); + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderAccessTokenCache.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderAccessTokenCache.cs new file mode 100644 index 0000000..8ace6ec --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderAccessTokenCache.cs @@ -0,0 +1,181 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using Microsoft.Extensions.Options; +using Serilog; +using static ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Constants; +using static System.Formats.Asn1.AsnWriter; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services +{ + /// + /// Get access token from DataHolder. + /// Cache request (user/selectedaccounts/scope) and accesstoken. + /// If cache miss then perform full E2E auth/consent flow to get accesstoken, cache it, and return access token. + /// If cache hit then use cached access token. + /// + public class DataHolderAccessTokenCache : IDataHolderAccessTokenCache + { + private readonly List _cache = new(); + private readonly IDataHolderParService _dataHolderParService; + private readonly IDataHolderTokenService _dataHolderTokenService; + private readonly IApiServiceDirector _apiServiceDirector; + private readonly TestAutomationOptions _options; + private readonly TestAutomationAuthServerOptions _authServerOptions; + + public int Hits { get; private set; } = 0; + public int Misses { get; private set; } = 0; + + public DataHolderAccessTokenCache(IOptions options, IOptions authServerOptions, IDataHolderParService dataHolderParService, IDataHolderTokenService dataHolderTokenService, IApiServiceDirector apiServiceDirector) + { + _dataHolderParService = dataHolderParService ?? throw new ArgumentNullException(nameof(dataHolderParService)); + _dataHolderTokenService = dataHolderTokenService ?? throw new ArgumentNullException(nameof(dataHolderTokenService)); + _apiServiceDirector = apiServiceDirector ?? throw new ArgumentNullException(nameof(apiServiceDirector)); + _options = options.Value ?? throw new ArgumentNullException(nameof(options)); + _authServerOptions = authServerOptions.Value ?? throw new ArgumentNullException(nameof(authServerOptions)); + } + + class CacheItem + { + public string? UserId { get; init; } + public string? SelectedAccounts { get; init; } + public string? Scope { get; init; } + + public string? AccessToken { get; set; } + } + + public async Task GetAccessToken(TokenType tokenType, string? scope = null, bool useCache = true) + { + Log.Information("Calling {FUNCTION} in {ClassName} with Params: {P1}={V1},{P2}={V2},{P3}={V3}.", nameof(GetAccessToken), nameof(DataHolderAccessTokenCache), nameof(tokenType), tokenType, nameof(scope), scope, nameof(useCache), useCache); + + switch (tokenType) + { + case TokenType.MaryMoss: //this was used by energy + case TokenType.HeddaHare: + case TokenType.JaneWilson: //this was used by banking + case TokenType.SteveKennedy: + case TokenType.DewayneSteve: + case TokenType.Business1: + case TokenType.Business2: + case TokenType.Beverage: + case TokenType.KamillaSmith: + { + return await GetAccessToken(tokenType.GetUserIdByTokenType(), tokenType.GetAllAccountIdsByTokenType(), scope, useCache); + } + + case TokenType.InvalidFoo: + return "foo"; + case TokenType.InvalidEmpty: + return ""; + case TokenType.InvalidOmit: + return null; + + default: + throw new ArgumentException($"{nameof(GetAccessToken)} failed for {nameof(TokenType)}={tokenType},{nameof(scope)}={scope},{nameof(useCache)}={useCache}.").Log(); + } + } + + private async Task GetAccessToken( + string userId, + string selectedAccounts, + string? scope = null, + bool useCache = true + ) + { + Log.Information("Calling {FUNCTION} in {ClassName} with Params: {P1}={V1},{P2}={V2},{P3}={V3},{P4}={V4}.", nameof(GetAccessToken), nameof(DataHolderAccessTokenCache), nameof(userId), userId, nameof(selectedAccounts), selectedAccounts, nameof(scope), scope, nameof(useCache), useCache); + + if (string.IsNullOrEmpty(scope)) + { + scope = _options.SCOPE; + } + + // Find refresh token in cache + CacheItem? cacheHit = null; + if (useCache) + { + cacheHit = _cache.Find(item => + item.UserId == userId && + item.SelectedAccounts == selectedAccounts && + item.Scope == scope); + } + + // Cache hit + if (cacheHit != null) + { + Log.Information("{UserId} token was found in the {cache}.", userId, nameof(DataHolderAccessTokenCache)); + Hits++; + + return cacheHit.AccessToken; + } + // Cache miss, so perform auth/consent flow to get accesstoken/refreshtoken + else + { + Log.Information("{UserId} token was not found in the {cache}. Performing AuthConsentFlow", userId, nameof(DataHolderAccessTokenCache)); + Misses++; + + (var accessToken, _) = await FromAuthConsentFlow(userId, selectedAccounts, scope); + + // Add refresh token to cache + _cache.Add(new CacheItem + { + UserId = userId, + SelectedAccounts = selectedAccounts, + Scope = scope, + AccessToken = accessToken + }); + + // Return access token + return accessToken; + } + } + + private async Task<(string accessToken, string refreshToken)> FromAuthConsentFlow(string userId, + string selectedAccounts, + string? scope = null) + { + if (string.IsNullOrEmpty(scope)) + { + scope = _options.SCOPE; + } + + DataHolderAuthoriseService authService; + if (_options.IS_AUTH_SERVER) + { + authService = await new DataHolderAuthoriseService.DataHolderAuthoriseServiceBuilder(_options, _dataHolderParService, _apiServiceDirector, false, _authServerOptions) + .WithUserId(userId) + .WithScope(scope) + .WithSelectedAccountIds(selectedAccounts) + .BuildAsync(); + } + else + { + authService = await new DataHolderAuthoriseService.DataHolderAuthoriseServiceBuilder(_options, _dataHolderParService, _apiServiceDirector) + .WithUserId(userId) + .WithScope(scope) + .WithSelectedAccountIds(selectedAccounts) + .WithResponseMode(ResponseMode.FormPost) + .BuildAsync(); + } + + (var authCode, _) = await authService.Authorise(); + + // use authcode to get access and refresh tokens + var tokenResponse = await _dataHolderTokenService.GetResponse(authCode); + + if (tokenResponse?.AccessToken == null) + throw new InvalidOperationException($"{nameof(FromAuthConsentFlow)} - access token is null").Log(); + + if (tokenResponse?.RefreshToken == null) + throw new InvalidOperationException($"{nameof(FromAuthConsentFlow)} - refresh token is null").Log(); + + return (tokenResponse.AccessToken, tokenResponse.RefreshToken); + } + public void ClearCache() + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(ClearCache), nameof(DataHolderAccessTokenCache)); + _cache.Clear(); + } + + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderAuthoriseService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderAuthoriseService.cs new file mode 100644 index 0000000..fc2ea13 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderAuthoriseService.cs @@ -0,0 +1,601 @@ +using System.Net; +using System.Security; +using System.Web; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.APIs; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UI; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UI.Pages.Authorisation; +using Dapper; +using FluentAssertions; +using HtmlAgilityPack; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Data.SqlClient; +using Microsoft.Playwright; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services +{ + public partial class DataHolderAuthoriseService : IDataHolderAuthoriseService + { + + private DataHolderAuthoriseService() { } //private constructor ensures that the builder must be used to instantiate a new service + + private string[]? SelectedAccountDisplayNamesCache { get; set; } = null; + private string? AuthoriseUrl { get; set; } + private TestAutomationOptions TestAutomationOptions { get; set; } + private TestAutomationAuthServerOptions? AuthServerOptions { get; set; } + + private IApiServiceDirector? ApiDirector { get; set; } + + /// + /// The customer's userid with the DataHolder - eg "jwilson" + /// + public string UserId { get; private set; } + + /// + /// The OTP (One-time password) that is sent to the customer (via sms etc) so the DataHolder can authenticate the Customer. + /// For the mock solution use "000789" + /// + public string OTP { get; private set; } = Constants.AuthoriseOTP; + + ///// + ///// Comma delimited list of account ids the user is granting consent for + ///// + public string? SelectedAccountIds { get; private set; } + + protected string[]? SelectedAccountIdsArray => SelectedAccountIds?.Split(","); + + /// + /// Scope + /// + public string Scope { get; private set; } + + /// + /// Lifetime (in seconds) of the access token + /// + public int TokenLifetime { get; private set; } = Constants.AuthServer.DefaultTokenLifetime; + + /// + /// Lifetime (in seconds) of the CDR arrangement. + /// SHARING_DURATION = 90 days + /// + public int? SharingDuration { get; private set; } = Constants.AuthServer.SharingDuration; + + public string? RequestUri { get; private set; } + + public string CertificateFilename { get; private set; } + public string CertificatePassword { get; private set; } + public string ClientId { get; private set; } + public string RedirectURI { get; private set; } + public string JwtCertificateFilename { get; private set; } + public string JwtCertificatePassword { get; private set; } + + public ResponseType ResponseType { get; private set; } + public ResponseMode ResponseMode { get; private set; } = ResponseMode.Fragment; + + public string? CdrArrangementId { get; private set; } + + public class DataHolderAuthoriseServiceBuilder : IAsyncBuilder + { + public DataHolderAuthoriseServiceBuilder(TestAutomationOptions options, IDataHolderParService dataHolderParService, IApiServiceDirector apiServiceDirector, bool useAdditionalDefaults = false, TestAutomationAuthServerOptions? authServerOptions = null) + { + _useAdditionalDefaults = useAdditionalDefaults; + _dataHolderParService = dataHolderParService ?? throw new ArgumentNullException(nameof(dataHolderParService)); + + _dataHolderAuthoriseService.TestAutomationOptions = options ?? throw new ArgumentNullException(nameof(options)); + _dataHolderAuthoriseService.ApiDirector = apiServiceDirector ?? throw new ArgumentNullException(nameof(apiServiceDirector)); + _dataHolderAuthoriseService.AuthServerOptions = authServerOptions; //no null check here because this is nullable + } + + private readonly DataHolderAuthoriseService _dataHolderAuthoriseService = new DataHolderAuthoriseService(); + private readonly bool _useAdditionalDefaults; + private readonly IDataHolderParService _dataHolderParService; + + public DataHolderAuthoriseServiceBuilder WithUserId(string value) + { + _dataHolderAuthoriseService.UserId = value; + return this; + } + + public DataHolderAuthoriseServiceBuilder WithOTP(string value) + { + _dataHolderAuthoriseService.OTP = value; + return this; + } + + public DataHolderAuthoriseServiceBuilder WithSelectedAccountIds(string value) + { + _dataHolderAuthoriseService.SelectedAccountIds = value; + return this; + } + + public DataHolderAuthoriseServiceBuilder WithScope(string value) + { + _dataHolderAuthoriseService.Scope = value; + return this; + } + + public DataHolderAuthoriseServiceBuilder WithTokenLifetime(int value) + { + _dataHolderAuthoriseService.TokenLifetime = value; + return this; + } + public DataHolderAuthoriseServiceBuilder WithSharingDuration(int? value) + { + _dataHolderAuthoriseService.SharingDuration = value; + return this; + } + public DataHolderAuthoriseServiceBuilder WithRequestUri(string value) + { + _dataHolderAuthoriseService.RequestUri = value; + return this; + } + public DataHolderAuthoriseServiceBuilder WithClientId(string value) + { + _dataHolderAuthoriseService.ClientId = value; + return this; + } + public DataHolderAuthoriseServiceBuilder WithRedirectUri(string value) + { + _dataHolderAuthoriseService.RedirectURI = value; + return this; + } + public DataHolderAuthoriseServiceBuilder WithResponseType(ResponseType value) + { + _dataHolderAuthoriseService.ResponseType = value; + return this; + } + public DataHolderAuthoriseServiceBuilder WithResponseMode(ResponseMode value) + { + _dataHolderAuthoriseService.ResponseMode = value; + return this; + } + public DataHolderAuthoriseServiceBuilder WithCertificateFilename(string value) + { + _dataHolderAuthoriseService.CertificateFilename = value; + return this; + } + public DataHolderAuthoriseServiceBuilder WithCertificatePassword(string value) + { + _dataHolderAuthoriseService.CertificatePassword = value; + return this; + } + public DataHolderAuthoriseServiceBuilder WithJwtCertificateFilename(string value) + { + _dataHolderAuthoriseService.JwtCertificateFilename = value; + return this; + } + public DataHolderAuthoriseServiceBuilder WithJwtCertificatePassword(string value) + { + _dataHolderAuthoriseService.JwtCertificatePassword = value; + return this; + } + public DataHolderAuthoriseServiceBuilder WithCdrArrangementId(string? value) + { + _dataHolderAuthoriseService.CdrArrangementId = value; + return this; + } + + public async Task BuildAsync() + { + Log.Information("Building a {BuiltClass} using {BuilderClass}.", nameof(DataHolderAuthoriseService), nameof(DataHolderAuthoriseServiceBuilder)); + + var _options = _dataHolderAuthoriseService.TestAutomationOptions; //purely to shorten reference + + FillMissingDefaults(); + Validate(); + + if (_options.IS_AUTH_SERVER) + { + if (_dataHolderAuthoriseService.RequestUri.IsNullOrWhiteSpace()) + { + _dataHolderAuthoriseService.RequestUri = await _dataHolderParService.GetRequestUri( + scope: _dataHolderAuthoriseService.Scope, + sharingDuration: _dataHolderAuthoriseService.SharingDuration, + clientId: _dataHolderAuthoriseService.ClientId, + cdrArrangementId: _dataHolderAuthoriseService.CdrArrangementId, + responseMode: _dataHolderAuthoriseService.ResponseMode + ); + } + + _dataHolderAuthoriseService.AuthoriseUrl = new AuthoriseUrl.AuthoriseUrlBuilder(_options) + .WithScope(_dataHolderAuthoriseService.Scope) + .WithRedirectURI(_dataHolderAuthoriseService.RedirectURI) + .WithRequestUri(_dataHolderAuthoriseService.RequestUri) + .WithClientId(_dataHolderAuthoriseService.ClientId) + .WithJWTCertificateFilename(_useAdditionalDefaults ? Constants.Certificates.AdditionalJwksCertificateFilename : Constants.Certificates.JwtCertificateFilename) + .WithJWTCertificatePassword(_useAdditionalDefaults ? Constants.Certificates.AdditionalJwksCertificatePassword : Constants.Certificates.JwtCertificatePassword) + .WithResponseType(_dataHolderAuthoriseService.ResponseType.ToEnumMemberAttrValue()) + .Build().Url; + } + else + { + if (_dataHolderAuthoriseService.RequestUri.IsNullOrWhiteSpace()) + { + _dataHolderAuthoriseService.RequestUri = await _dataHolderParService.GetRequestUri( + scope: _dataHolderAuthoriseService.Scope, + sharingDuration: _dataHolderAuthoriseService.SharingDuration, + clientId: _dataHolderAuthoriseService.ClientId, + responseMode: _dataHolderAuthoriseService.ResponseMode, + cdrArrangementId: _dataHolderAuthoriseService.CdrArrangementId + ); + } + } + + return _dataHolderAuthoriseService; + } + + private void FillMissingDefaults() + { + Log.Information("Filling missing defaults for public properties"); + + var _options = _dataHolderAuthoriseService.TestAutomationOptions; //purely to shorten reference + + //Anything that doesn't have a value which should...will get it's default value set + if (_dataHolderAuthoriseService.ClientId.IsNullOrWhiteSpace()) + { + _dataHolderAuthoriseService.ClientId = _options.LastRegisteredClientId ?? throw new InvalidOperationException($"{nameof(_options.LastRegisteredClientId)} should not be null.").Log(); + Log.Information("Assigned default value {VALUE} to Parameter {PARAM}", _dataHolderAuthoriseService.ClientId, nameof(ClientId)); + } + + if (_dataHolderAuthoriseService.RedirectURI.IsNullOrWhiteSpace()) + { + _dataHolderAuthoriseService.RedirectURI = _useAdditionalDefaults ? _options.ADDITIONAL_SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS : _options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS; + Log.Information("Assigned default value {VALUE} to Parameter {PARAM}", _dataHolderAuthoriseService.RedirectURI, nameof(RedirectURI)); + } + + if (_dataHolderAuthoriseService.Scope.IsNullOrWhiteSpace()) + { + _dataHolderAuthoriseService.Scope = _options.SCOPE; + Log.Information("Assigned default value {VALUE} to Parameter {PARAM}", _dataHolderAuthoriseService.Scope, nameof(Scope)); + } + + if (_dataHolderAuthoriseService.SelectedAccountIds.IsNullOrWhiteSpace()) + { + _dataHolderAuthoriseService.SelectedAccountIds = GetAccountIdsForUser(_dataHolderAuthoriseService.UserId); + Log.Information("Assigned default value {VALUE} to Parameter {PARAM}", _dataHolderAuthoriseService.SelectedAccountIds, nameof(SelectedAccountIds)); + } + + if (_dataHolderAuthoriseService.CertificateFilename.IsNullOrWhiteSpace()) + { + _dataHolderAuthoriseService.CertificateFilename = _useAdditionalDefaults ? Constants.Certificates.AdditionalCertificateFilename : Constants.Certificates.CertificateFilename; + Log.Information("Assigned default value {VALUE} to Parameter {PARAM}", _dataHolderAuthoriseService.CertificateFilename, nameof(CertificateFilename)); + } + + if (_dataHolderAuthoriseService.CertificatePassword.IsNullOrWhiteSpace()) + { + _dataHolderAuthoriseService.CertificatePassword = _useAdditionalDefaults ? Constants.Certificates.AdditionalCertificatePassword : Constants.Certificates.CertificatePassword; + Log.Information("Assigned default value {VALUE} to Parameter {PARAM}", _dataHolderAuthoriseService.CertificatePassword, nameof(CertificatePassword)); + } + + if (_dataHolderAuthoriseService.JwtCertificateFilename.IsNullOrWhiteSpace()) + { + _dataHolderAuthoriseService.JwtCertificateFilename = _useAdditionalDefaults ? Constants.Certificates.AdditionalJwksCertificateFilename : Constants.Certificates.JwtCertificateFilename; + Log.Information("Assigned default value {VALUE} to Parameter {PARAM}", _dataHolderAuthoriseService.JwtCertificateFilename, nameof(JwtCertificateFilename)); + } + + if (_dataHolderAuthoriseService.JwtCertificatePassword.IsNullOrWhiteSpace()) + { + _dataHolderAuthoriseService.JwtCertificatePassword = _useAdditionalDefaults ? Constants.Certificates.AdditionalJwksCertificatePassword : Constants.Certificates.JwtCertificatePassword; + Log.Information("Assigned default value {VALUE} to Parameter {PARAM}", _dataHolderAuthoriseService.JwtCertificatePassword, nameof(JwtCertificatePassword)); + } + } + + private void Validate() + { + Log.Information("Validating {BUILTOBJECT} mandatory properties.", nameof(DataHolderAuthoriseService)); + if (_dataHolderAuthoriseService.RedirectURI.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(RedirectURI)).Log(); + } + + if (_dataHolderAuthoriseService.ClientId.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(ClientId)).Log(); + } + + if (_dataHolderAuthoriseService.UserId.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(UserId)).Log(); + } + } + + public static string GetAccountIdsForUser(string userId) + { + return userId switch + { + Constants.Users.UserIdKamillaSmith => Constants.Accounts.AccountIdsAllKamillaSmith, + Constants.Users.Energy.UserIdMaryMoss => Constants.Accounts.Energy.AccountIdsAllMaryMoss, + Constants.Users.Banking.UserIdJaneWilson => Constants.Accounts.Banking.AccountIdsAllJaneWilson, + _ => throw new ArgumentException($"{nameof(GetAccountIdsForUser)} - Unsupported user id - {userId}.").Log() + }; + } + } + + public async Task<(string authCode, string idToken)> Authorise() + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(Authorise), nameof(DataHolderAuthoriseService)); + + if (TestAutomationOptions.IS_AUTH_SERVER) + { +#pragma warning disable CS8602 // Dereference of a possibly null reference.This has been disabled because AuthServerOptions is assigned in the constructor following a null check + if (AuthServerOptions.HEADLESSMODE) + { + return await AuthoriseHeadless(); + } + else + { + throw new NotImplementedException($"{nameof(Authorise)} Method only supports headless mode for AuthServer Integration Tests."); + } +#pragma warning restore CS8602 // Dereference of a possibly null reference. + } + else + { + var responseMode = Enums.ResponseMode.FormPost.ToEnumMemberAttrValue(); //to ensure it's clearly consistent + + Uri authRedirectUri = await Authorise_GetRedirectUri(responseMode); + + return await Authorize_Consent(authRedirectUri, responseMode); + } + } + + #region AuthServer + public async Task AuthoriseForJarm() + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(AuthoriseForJarm), nameof(DataHolderAuthoriseService)); + + var callback = new DataRecipientConsentCallback(RedirectURI); + callback.Start(); + try + { + var cookieContainer = new CookieContainer(); + var response = await AuthServer_Authorise(cookieContainer, false) ?? throw new NullReferenceException(); + return response; + } + finally + { + await callback.Stop(); + } + } + + /// + /// Perform authorisation and consent flow. Returns authCode and idToken + /// + private async Task<(string authCode, string idToken)> AuthoriseHeadless() + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(AuthoriseHeadless), nameof(DataHolderAuthoriseService)); + + var callback = new DataRecipientConsentCallback(RedirectURI); + callback.Start(); + try + { + var cookieContainer = new CookieContainer(); + + // "headless" workaround currently "{BaseTest.DH_TLS_AUTHSERVER_BASE_URL}/connect/authorize" redirects immediately to the callback uri (ie there's no UI) + var response = await AuthServer_Authorise(cookieContainer) ?? throw new NullReferenceException(); + + // Return authcode and idtoken + return ExtractAuthCodeIdToken(response); + } + finally + { + await callback.Stop(); + } + + static (string authCode, string idToken) ExtractAuthCodeIdToken(HttpResponseMessage response) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(ExtractAuthCodeIdToken), nameof(DataHolderAuthoriseService)); + + var fragment = response.RequestMessage?.RequestUri?.Fragment; + if (fragment == null) + { + throw new Exception($"{nameof(ExtractAuthCodeIdToken)} - response fragment is null").Log(); + } + + var query = HttpUtility.ParseQueryString(fragment.TrimStart('#')); + + Exception RaiseException(string errorMessage, string? authCode, string? idToken) + { + var responseRequestUri = response?.RequestMessage?.RequestUri; + return new SecurityException($"{errorMessage}\r\nauthCode={authCode},idToken={idToken},response.RequestMessage.RequestUri={responseRequestUri}"); + } + + string? authCode = query["code"]; + string? idToken = query["id_token"]; + + if (authCode == null) + { + throw RaiseException("authCode is null", authCode, idToken); + } + + if (idToken == null) + { + throw RaiseException("idToken is null", authCode, idToken); + } + + return (authCode, idToken); + } + } + + private async Task AuthServer_Authorise(CookieContainer cookieContainer, bool allowRedirect = true) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(AuthServer_Authorise), nameof(DataHolderAuthoriseService)); + + var request = new HttpRequestMessage(HttpMethod.Get, AuthoriseUrl); + +#pragma warning disable CS8602 // Dereference of a possibly null reference. This has been disabled because TestAutomationOptions is assigned in the constructor following a null check + Helpers.AuthServer.AttachHeadersForStandAlone(request.RequestUri?.AbsoluteUri ?? throw new NullReferenceException($"{nameof(request.RequestUri.AbsoluteUri)} is null").Log(), request.Headers, TestAutomationOptions.DH_MTLS_GATEWAY_URL, AuthServerOptions.XTLSCLIENTCERTTHUMBPRINT, AuthServerOptions.STANDALONE); +#pragma warning restore CS8602 // Dereference of a possibly null reference. + + var response = await Helpers.Web.CreateHttpClient(allowAutoRedirect: allowRedirect, cookieContainer: cookieContainer).SendAsync(request); + + return response; + } + #endregion + + #region Dataholder + // Call authorise endpoint, should respond with a redirect to auth UI, return the redirect URI + private async Task Authorise_GetRedirectUri(string responseMode) + { + Log.Information("Calling {FUNCTION} in {ClassName} with Params: {P1}={V1}.", nameof(Authorise_GetRedirectUri), nameof(DataHolderAuthoriseService), nameof(responseMode), responseMode); + + var queryString = new Dictionary + { + { "request_uri", RequestUri }, + { "response_type", ResponseType.CodeIdToken.ToEnumMemberAttrValue() }, + { "response_mode", responseMode }, + { "client_id", ClientId }, + { "redirect_uri", RedirectURI }, + { "scope", Scope }, + }; + +#pragma warning disable CS8602 // Dereference of a possibly null reference. This is disabled because there is a null check when assigning a value to this in the constructor + var api = ApiDirector.BuildAuthServerAuthorizeAPI(queryString); +#pragma warning restore CS8602 // Dereference of a possibly null reference. + + var response = await api.SendAsync(allowAutoRedirect: false); + + if (response.StatusCode != HttpStatusCode.Redirect) + { + var content = await response.Content.ReadAsStringAsync(); + var doc = new HtmlDocument(); + doc.LoadHtml(content); + var error = doc.DocumentNode.SelectSingleNode("//input[@name='error']").Attributes["value"].Value; + var errorDescription = doc.DocumentNode.SelectSingleNode("//input[@name='error_description']").Attributes["value"].Value; + + throw new AuthoriseException($"Expected {HttpStatusCode.Redirect} but got {response.StatusCode}", response.StatusCode, error, errorDescription); //TODO: This was the original code, but it's returning Ok when it should not be 200Ok. Bug 63710 + } + + return response.Headers.Location ?? throw new NullReferenceException(nameof(response.Headers.Location.AbsoluteUri)); + } + + + private async Task<(string authCode, string idToken)> Authorize_Consent(Uri authRedirectUri, string responseMode) + { + Log.Information("Calling {FUNCTION} in {ClassName} with Params: {P1}={V1},{P2}={V2}.", nameof(Authorize_Consent), nameof(DataHolderAuthoriseService), nameof(authRedirectUri), authRedirectUri, nameof(responseMode), responseMode); + + var authRedirectLeftPart = authRedirectUri.GetLeftPart(UriPartial.Authority) + "/ui"; + + string? code = null; + string? idtoken = null; + + PlaywrightDriver playwrightDriver = new PlaywrightDriver(); + + try + { + IBrowserContext browserContext = playwrightDriver.NewBrowserContext().Result; + + var page = await browserContext.NewPageAsync(); + + await page.GotoAsync(authRedirectUri.AbsoluteUri); // redirect user to Auth UI to login and consent to share accounts + + // Username + AuthenticateLoginPage authenticateLoginPage = new(page); + await authenticateLoginPage.EnterCustomerId(UserId ?? throw new NullReferenceException(nameof(UserId))); + await authenticateLoginPage.ClickContinue(); + + // OTP + OneTimePasswordPage oneTimePasswordPage = new(page); + await oneTimePasswordPage.EnterOtp(OTP ?? throw new NullReferenceException(nameof(OTP))); + await oneTimePasswordPage.ClickContinue(); + + // Select accounts + SelectAccountsPage selectAccountsPage = new(page); + + var selectedAccountDisplayNames = GetSelectedAccountDisplayNames(); + + await selectAccountsPage.SelectAccounts(selectedAccountDisplayNames); + await selectAccountsPage.ClickContinue(); + + // Confirmation - Click authorise and check callback response + ConfirmAccountSharingPage confirmAccountSharingPage = new(page); + + (code, idtoken) = await HybridFlow_HandleCallback(redirectUri: RedirectURI, responseMode: responseMode, page: page, setup: async (page) => + { + await confirmAccountSharingPage.ClickAuthorise(); + }); + } + catch (Exception) + { + throw; + } + finally + { + await playwrightDriver.DisposeAsync(); + } + + return ( + authCode: code ?? throw new NullReferenceException(nameof(code)), + idToken: idtoken ?? throw new NullReferenceException(nameof(idtoken)) + ); + } + + private delegate Task HybridFlow_HandleCallback_Setup(IPage page); + static private async Task<(string code, string idtoken)> HybridFlow_HandleCallback(string redirectUri, string responseMode, IPage page, HybridFlow_HandleCallback_Setup setup) + { + Log.Information("Calling {FUNCTION} in {ClassName}", nameof(HybridFlow_HandleCallback), nameof(DataHolderAuthoriseService)); + + var callback = new DataRecipientConsentCallback(redirectUri); + callback.Start(); + try + { + await setup(page); + + var callbackRequest = await callback.WaitForCallback(); + switch (responseMode) + { + case "form_post": + { + callbackRequest.Should().NotBeNull(); + callbackRequest?.received.Should().BeTrue(); + callbackRequest?.method.Should().Be(HttpMethod.Post); + callbackRequest?.body.Should().NotBeNullOrEmpty(); + + var body = QueryHelpers.ParseQuery(callbackRequest?.body); + var code = body["code"]; + var id_token = body["id_token"]; + return (code, id_token); + } + case "fragment": + case "query": + default: + throw new NotSupportedException(nameof(responseMode)); + } + } + finally + { + await callback.Stop(); + } + } + #endregion + + private string[] GetSelectedAccountDisplayNames() + { + if (SelectedAccountDisplayNamesCache != null) + { + return SelectedAccountDisplayNamesCache; + } + + List list = new(); + + if (SelectedAccountIdsArray != null) + { + using var connection = new SqlConnection(TestAutomationOptions.DATAHOLDER_CONNECTIONSTRING); + foreach (var accountId in SelectedAccountIdsArray) + { + var displayName = connection.QuerySingle("select displayName from account where accountId = @AccountId", new { AccountId = accountId }); + list.Add(displayName); + } + } + + SelectedAccountDisplayNamesCache = list.ToArray(); + + return SelectedAccountDisplayNamesCache; + } + } + +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderParService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderParService.cs new file mode 100644 index 0000000..9482651 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderParService.cs @@ -0,0 +1,236 @@ +using System.Net; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services +{ + public class DataHolderParService : IDataHolderParService + { + private readonly TestAutomationOptions _options; + private readonly TestAutomationAuthServerOptions _authServerOptions; + + public DataHolderParService(IOptions options, IOptions authServerOptions) + { + _options = options.Value ?? throw new ArgumentNullException(nameof(options)); + _authServerOptions = authServerOptions.Value ?? throw new ArgumentNullException(nameof(authServerOptions)); + } + + public class Response + { + [JsonProperty("request_uri")] + public string? RequestURI { get; set; } + + [JsonProperty("expires_in")] + public int? ExpiresIn { get; set; } + }; + + public async Task SendRequest( + string? scope, + string? clientId = null, + string clientAssertionType = Constants.ClientAssertionType, + int? sharingDuration = Constants.AuthServer.SharingDuration, + string? aud = null, + int nbfOffsetSeconds = 0, + int expOffsetSeconds = 0, + bool addRequestObject = true, + bool addNotBeforeClaim = true, + bool addExpiryClaim = true, + string? cdrArrangementId = null, + string? redirectUri = null, + string? clientAssertion = null, + + string codeVerifier = Constants.AuthServer.FapiPhase2CodeVerifier, + string codeChallengeMethod = Constants.AuthServer.FapiPhase2CodeChallengeMethod, + + string? requestUri = null, + ResponseMode? responseMode = ResponseMode.Fragment, + string certificateFilename = Constants.Certificates.CertificateFilename, + string certificatePassword = Constants.Certificates.CertificatePassword, + string jwtCertificateForClientAssertionFilename = Constants.Certificates.JwtCertificateFilename, + string jwtCertificateForClientAssertionPassword = Constants.Certificates.JwtCertificatePassword, + string jwtCertificateForRequestObjectFilename = Constants.Certificates.JwtCertificateFilename, + string jwtCertificateForRequestObjectPassword = Constants.Certificates.JwtCertificatePassword, + + ResponseType? responseType = ResponseType.CodeIdToken, + string? state = null) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(SendRequest), nameof(DataHolderParService)); + + if (string.IsNullOrWhiteSpace(redirectUri)) + { + redirectUri = _options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS; + } + + if (clientId == null) + { + clientId = _options.LastRegisteredClientId; + } + + var issuer = _options.DH_TLS_AUTHSERVER_BASE_URL; + + var parUrl = $"{_options.CDRAUTHSERVER_SECUREBASEURI}/connect/par"; + + var formFields = new List>(); + + if (clientAssertionType != null) + { + formFields.Add(new KeyValuePair("client_assertion_type", clientAssertionType)); + } + + if (requestUri != null) + { + formFields.Add(new KeyValuePair("request_uri", requestUri)); + } + + formFields.Add(new KeyValuePair("client_assertion", clientAssertion ?? + new PrivateKeyJwtService() + { + CertificateFilename = jwtCertificateForClientAssertionFilename, + CertificatePassword = jwtCertificateForClientAssertionPassword, + Issuer = clientId ?? throw new NullReferenceException(nameof(clientId)), + Audience = aud ?? issuer + }.Generate() + )); + + if (addRequestObject) + { + var iat = new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds(); + + var requestObject = new Dictionary { + { "iss", clientId ?? throw new NullReferenceException(nameof(clientId))}, + { "iat", iat }, + { "jti", Guid.NewGuid().ToString().Replace("-", string.Empty) }, + + { "response_mode", responseMode?.ToEnumMemberAttrValue()}, + { "aud", aud ?? _options.DH_TLS_AUTHSERVER_BASE_URL }, + { "client_id", clientId }, + { "redirect_uri", redirectUri }, + { "state", state ?? Guid.NewGuid().ToString() }, + { "nonce", Guid.NewGuid().ToString() }, + { "claims", new { + sharing_duration = $"{sharingDuration}", + cdr_arrangement_id = cdrArrangementId, + id_token = new { + acr = new { + essential = true, + values = new string[] { "urn:cds.au:cdr:2" } + } + }, + } + } + }; + + if (addNotBeforeClaim) + { + requestObject.Add("nbf", iat + nbfOffsetSeconds); + } + + if (addExpiryClaim) + { + requestObject.Add("exp", iat + expOffsetSeconds + 600); + } + + if (!scope.IsNullOrWhiteSpace()) + { + requestObject.Add("scope", scope); + } + + if (codeVerifier != null) + { + requestObject.Add("code_challenge", codeVerifier.CreatePkceChallenge()); + } + + if (codeChallengeMethod != null) + { + requestObject.Add("code_challenge_method", codeChallengeMethod); + } + + if (responseType != null) + { + requestObject.Add("response_type", responseType.ToEnumMemberAttrValue()); + } + + var jwt = Helpers.Jwt.CreateJWT(jwtCertificateForRequestObjectFilename ?? Constants.Certificates.JwtCertificateFilename, jwtCertificateForRequestObjectPassword ?? Constants.Certificates.JwtCertificatePassword, requestObject); + + formFields.Add(new KeyValuePair("request", jwt)); + } + + var content = new FormUrlEncodedContent(formFields); + + using var client = Helpers.Web.CreateHttpClient( + certificateFilename ?? throw new ArgumentNullException(nameof(certificateFilename)), + certificatePassword); + + Helpers.AuthServer.AttachHeadersForStandAlone(parUrl, content.Headers, _options.DH_MTLS_GATEWAY_URL, _authServerOptions.XTLSCLIENTCERTTHUMBPRINT, _authServerOptions.STANDALONE); + + var responseMessage = await client.PostAsync(parUrl, content); + + return responseMessage; + } + + public async Task GetRequestUri( + string? scope, + string? clientId = null, + string jwtCertificateForClientAssertionFilename = Constants.Certificates.JwtCertificateFilename, + string jwtCertificateForClientAssertionPassword = Constants.Certificates.JwtCertificatePassword, + string jwtCertificateForRequestObjectFilename = Constants.Certificates.JwtCertificateFilename, + string jwtCertificateForRequestObjectPassword = Constants.Certificates.JwtCertificatePassword, + string? redirectUri = null, + int? sharingDuration = Constants.AuthServer.SharingDuration, + string? cdrArrangementId = null, + ResponseMode responseMode = ResponseMode.Fragment) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(GetRequestUri), nameof(DataHolderParService)); + + if (clientId == null) + { + clientId = _options.LastRegisteredClientId; + } + + if (string.IsNullOrWhiteSpace(redirectUri)) + { + redirectUri = _options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS; + } + + var response = await SendRequest( + scope: scope, + clientId: clientId, + jwtCertificateForClientAssertionFilename: jwtCertificateForClientAssertionFilename, + jwtCertificateForClientAssertionPassword: jwtCertificateForClientAssertionPassword, + jwtCertificateForRequestObjectFilename: jwtCertificateForRequestObjectFilename, + jwtCertificateForRequestObjectPassword: jwtCertificateForRequestObjectPassword, + redirectUri: redirectUri, + sharingDuration: sharingDuration, + cdrArrangementId: cdrArrangementId, + responseMode: responseMode); + + if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Created) + { + throw new InvalidOperationException($"Statuscode={response.StatusCode} - Response.Content={await response.Content.ReadAsStringAsync()}").Log(); + } + + var json = await JsonExtensions.DeserializeResponseAsync(response); + + var requestUri = json?.RequestURI ?? throw new NullReferenceException("requestUri"); + + return requestUri; + } + + public async Task DeserializeResponse(HttpResponseMessage response) + { + var responseContent = await response.Content.ReadAsStringAsync(); + + if (string.IsNullOrEmpty(responseContent)) + { + return null; + } + + return JsonConvert.DeserializeObject(responseContent); + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderRegisterService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderRegisterService.cs new file mode 100644 index 0000000..0006bc9 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderRegisterService.cs @@ -0,0 +1,213 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Net; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services +{ + public class DataHolderRegisterService : IDataHolderRegisterService + { + private readonly TestAutomationOptions _options; + private readonly IRegisterSsaService _registerSSAService; + private readonly IApiServiceDirector _apiServiceDirector; + + private readonly string _latestSSAVersion = "3"; + + public DataHolderRegisterService(IOptions options, IRegisterSsaService registerSSAService, IApiServiceDirector apiServiceDirector) + { + _options = options.Value ?? throw new ArgumentNullException(nameof(options)); + _registerSSAService = registerSSAService ?? throw new ArgumentNullException(nameof(registerSSAService)); + _apiServiceDirector = apiServiceDirector ?? throw new ArgumentNullException(nameof(apiServiceDirector)); + } + /// + /// Create registration request JWT for SSA + /// + public string CreateRegistrationRequest( + string ssa, + string tokenEndpointAuthSigningAlg = "PS256", + string[]? redirectUris = null, + string jwtCertificateFilename = Constants.Certificates.JwtCertificateFilename, + string jwtCertificatePassword = Constants.Certificates.JwtCertificatePassword, + string applicationType = "web", + string requestObjectSigningAlg = "PS256", + string responseType = "code id_token", + string[]? grantTypes = null, + string? authorizationSignedResponseAlg = null, + string? authorizationEncryptedResponseAlg = null, + string? authorizationEncryptedResponseEnc = null, + string? idTokenSignedResponseAlg = "PS256", + string? idTokenEncryptedResponseAlg = "RSA-OAEP", + string? idTokenEncryptedResponseEnc = "A256GCM" + ) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(CreateRegistrationRequest), nameof(DataHolderRegisterService)); + + string[] responseTypes = responseType.Contains(",") ? responseType.Split(",") : new string[] { responseType }; + + grantTypes = grantTypes ?? new string[] { "client_credentials", "authorization_code", "refresh_token" }; + + JwtSecurityToken decodedSSA; + try + { + decodedSSA = new JwtSecurityTokenHandler().ReadJwtToken(ssa); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Error attempting to decode SSA.\r\nSSA={ssa}.\r\nException={ex.Message}").Log(); + } + + var softwareId = decodedSSA.Claims.First(claim => claim.Type == "software_id").Value; + + var iat = (int)DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds; + var exp = iat + 300; // expire 5 mins from now + + var subject = new Dictionary + { + { "iss", softwareId }, + { "iat", iat }, + { "exp", exp }, + { "jti", Guid.NewGuid().ToString() }, + { "aud", _options.REGISTRATION_AUDIENCE_URI }, + + // Get redirect_uris from SSA + { "redirect_uris", + redirectUris ?? + decodedSSA.Claims.Where(claim => claim.Type == "redirect_uris").Select(claim => claim.Value).ToArray() }, + + { "token_endpoint_auth_signing_alg", tokenEndpointAuthSigningAlg }, + { "token_endpoint_auth_method", "private_key_jwt" }, + { "grant_types", grantTypes }, + { "response_types", responseTypes }, + + //{ "id_token_signed_response_alg", "PS256" }, //TODO: Optional? + //{ "id_token_encrypted_response_alg", "RSA-OAEP" }, //TODO: Optional? + //{ "id_token_encrypted_response_enc", "A256GCM" }, //TODO: Optional? + //{ "application_type", applicationType }, // spec says optional + { "software_statement", ssa }, + + { "client_id",softwareId }, + }; + + // Optional fields. + if (!string.IsNullOrEmpty(applicationType)) + { + subject.Add("application_type", applicationType); + } + + if (!string.IsNullOrEmpty(requestObjectSigningAlg)) + { + subject.Add("request_object_signing_alg", requestObjectSigningAlg); + } + + if (authorizationSignedResponseAlg != null) + { + if (authorizationSignedResponseAlg == Constants.Null) + { + subject.Add("authorization_signed_response_alg", null); + } + else + { + subject.Add("authorization_signed_response_alg", authorizationSignedResponseAlg); + } + } + + if (authorizationEncryptedResponseAlg != null) + { + subject.Add("authorization_encrypted_response_alg", authorizationEncryptedResponseAlg); + } + if (authorizationEncryptedResponseEnc != null) + { + subject.Add("authorization_encrypted_response_enc", authorizationEncryptedResponseEnc); + } + + if (idTokenSignedResponseAlg != null) + { + subject.Add("id_token_signed_response_alg", idTokenSignedResponseAlg); + } + + if (idTokenEncryptedResponseAlg != null) + { + subject.Add("id_token_encrypted_response_alg", idTokenEncryptedResponseAlg); + } + if (idTokenEncryptedResponseEnc != null) + { + subject.Add("id_token_encrypted_response_enc", idTokenEncryptedResponseEnc); + } + + var jwt = Helpers.Jwt.CreateJWT( + jwtCertificateFilename, + jwtCertificatePassword, + subject); + + return jwt; + } + + /// + /// Register software product using registration request + /// + public async Task RegisterSoftwareProduct(string registrationRequest) + { + Log.Information("Calling {FUNCTION} in {ClassName} with Params: {P1}={V1}.", nameof(RegisterSoftwareProduct), nameof(DataHolderRegisterService), nameof(registrationRequest), registrationRequest); + + var url = $"{_options.DH_MTLS_GATEWAY_URL}/connect/register"; + + var accessToken = new PrivateKeyJwtService() + { + CertificateFilename = Constants.Certificates.JwtCertificateFilename, + CertificatePassword = Constants.Certificates.JwtCertificatePassword, + Issuer = Constants.SoftwareProducts.SoftwareProductId.ToLower(), + Audience = url + }.Generate(); + + // Post the request + var api = _apiServiceDirector.BuildDataholderRegisterAPI(accessToken, registrationRequest, HttpMethod.Post); + var response = await api.SendAsync(); + + return response; + } + + // Get SSA from the Register and register it with the DataHolder + public async Task<(string ssa, string registration, string clientId)> RegisterSoftwareProduct( + string brandId = Constants.Brands.BrandId, + string softwareProductId = Constants.SoftwareProducts.SoftwareProductId, + string jwtCertificateFilename = Constants.Certificates.JwtCertificateFilename, + string jwtCertificatePassword = Constants.Certificates.JwtCertificatePassword, + string responseType = "code id_token", + string authorizationSignedResponseAlg = "PS256") + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(RegisterSoftwareProduct), nameof(DataHolderRegisterService)); + + // Get SSA from Register + var ssa = await _registerSSAService.GetSSA(brandId, softwareProductId, _latestSSAVersion, jwtCertificateFilename, jwtCertificatePassword); + + // Register software product with DataHolder + var registrationRequest = CreateRegistrationRequest(ssa, + jwtCertificateFilename: jwtCertificateFilename, + jwtCertificatePassword: jwtCertificatePassword, + responseType: responseType, + authorizationSignedResponseAlg: authorizationSignedResponseAlg); + + var response = await RegisterSoftwareProduct(registrationRequest); + if (response.StatusCode != HttpStatusCode.Created) + { + throw new InvalidOperationException($"Unable to register software product - {softwareProductId} - {response.StatusCode} - {await response.Content.ReadAsStringAsync()}").Log(); + } + + string registration = await response.Content.ReadAsStringAsync(); + + // Extract clientId from registration + dynamic? clientId = JsonConvert.DeserializeObject(registration)?.client_id.ToString(); + if (string.IsNullOrEmpty(clientId)) + { + throw new InvalidOperationException($"Parameter: {nameof(clientId)} should not be empty.").Log(); + } + + _options.LastRegisteredClientId = clientId; + return (ssa, registration, clientId); + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderTokenRevocationService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderTokenRevocationService.cs new file mode 100644 index 0000000..07165a9 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderTokenRevocationService.cs @@ -0,0 +1,91 @@ +using System.Security.Cryptography.X509Certificates; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using Microsoft.Extensions.Options; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services +{ + public class DataHolderTokenRevocationService : IDataHolderTokenRevocationService + { + private readonly TestAutomationOptions _options; + private readonly TestAutomationAuthServerOptions? _authServerOptions; + + public DataHolderTokenRevocationService(IOptions options, IOptions? authServerOptions = null) + { + _options = options.Value ?? throw new ArgumentNullException(nameof(options)); + _authServerOptions = authServerOptions?.Value; //no null check here because this is nullable for Data Holder projects + } + + // Send token request, returning HttpResponseMessage + public async Task SendRequest( + string? clientId = null, + string clientAssertionType = Constants.ClientAssertionType, + string? clientAssertion = null, + string? token = null, + string? tokenTypeHint = null, + string certificateFilename = Constants.Certificates.CertificateFilename, + string certificatePassword = Constants.Certificates.CertificatePassword, + string jwtCertificateFilename = Constants.Certificates.JwtCertificateFilename, + string jwtCertificatePassword = Constants.Certificates.JwtCertificatePassword + ) + { + if (clientId == null) + { + clientId = _options.LastRegisteredClientId; + } + + var URL = $"{_options.DH_MTLS_GATEWAY_URL}/connect/revocation"; + + var formFields = new List>(); + + if (clientId != null) + { + formFields.Add(new KeyValuePair("client_id", clientId.ToLower())); + } + + if (clientAssertionType != null) + { + formFields.Add(new KeyValuePair("client_assertion_type", clientAssertionType)); + } + + formFields.Add(new KeyValuePair("client_assertion", clientAssertion ?? + new PrivateKeyJwtService + { + CertificateFilename = jwtCertificateFilename, + CertificatePassword = jwtCertificatePassword, + Issuer = clientId ?? throw new InvalidOperationException($"{nameof(clientId)} can not be null.").Log(), + Audience = URL + }.Generate()) + ); + + if (token != null) + { + formFields.Add(new KeyValuePair("token", token)); + } + + if (tokenTypeHint != null) + { + formFields.Add(new KeyValuePair("token_type_hint", tokenTypeHint)); + } + + var content = new FormUrlEncodedContent(formFields); + + using var clientHandler = new HttpClientHandler(); + clientHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; //sonarqube will raise this as a vulnerability as it is not away this is a test library only + + clientHandler.ClientCertificates.Add(new X509Certificate2( + certificateFilename ?? throw new ArgumentNullException(nameof(certificateFilename)), + certificatePassword, + X509KeyStorageFlags.Exportable)); + + using var client = new HttpClient(clientHandler); + + Helpers.AuthServer.AttachHeadersForStandAlone(URL, content.Headers, _options.DH_MTLS_GATEWAY_URL, _authServerOptions?.XTLSCLIENTCERTTHUMBPRINT, _authServerOptions?.STANDALONE); + + var responseMessage = await client.PostAsync(URL, content); + + return responseMessage; + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderTokenService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderTokenService.cs new file mode 100644 index 0000000..a1220a4 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/DataHolderTokenService.cs @@ -0,0 +1,248 @@ +using System.Net; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Dataholders; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services +{ + public partial class DataHolderTokenService : IDataHolderTokenService + { + private readonly TestAutomationOptions _options; + private readonly TestAutomationAuthServerOptions _authServeroptions; + + public DataHolderTokenService(IOptions options, IOptions authServerOptions) + { + _options = options.Value ?? throw new ArgumentNullException(nameof(options)); + _authServeroptions = authServerOptions.Value ?? throw new ArgumentNullException(nameof(authServerOptions)); + } + + // Send token request, returning HttpResponseMessage + public async Task SendRequest( + string? authCode = null, + bool usePut = false, + string grantType = "authorization_code", + string? clientId = null, + string? issuerClaim = null, + string clientAssertionType = Constants.ClientAssertionType, + bool useClientAssertion = true, + int? shareDuration = null, + string? refreshToken = null, + string? customClientAssertion = null, + string? scope = null, + string? redirectUri = null, + string certificateFilename = Constants.Certificates.CertificateFilename, + string certificatePassword = Constants.Certificates.CertificatePassword, + string jwkCertificateFilename = Constants.Certificates.JwtCertificateFilename, + string jwkCertificatePassword = Constants.Certificates.JwtCertificatePassword + ) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(SendRequest), nameof(DataHolderTokenService)); + + if (string.IsNullOrWhiteSpace(redirectUri)) + { + redirectUri = _options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS; + } + + if (clientId == null) + { + clientId = _options.LastRegisteredClientId; + } + + if (issuerClaim == null) + { + issuerClaim = _options.LastRegisteredClientId; + } + + var URL = $"{_options.DH_MTLS_GATEWAY_URL}/connect/token"; + + var formFields = new List> + { + new KeyValuePair("redirect_uri", redirectUri), + }; + + if (authCode != null) + { + formFields.Add(new KeyValuePair("code", authCode)); + } + + if (grantType != null) + { + formFields.Add(new KeyValuePair("grant_type", grantType)); + } + + if (clientId != Constants.Omit) + { + formFields.Add(new KeyValuePair("client_id", clientId?.ToLower() ?? throw new InvalidOperationException($"{nameof(clientId)} can not be null unless intentionally omitted.").Log())); + } + + if (clientAssertionType != null) + { + formFields.Add(new KeyValuePair("client_assertion_type", clientAssertionType)); + } + + if (customClientAssertion != null) + { + formFields.Add(new KeyValuePair("client_assertion", customClientAssertion)); + } + else if (useClientAssertion) //only check if we haven't provided custom client assertion + { + var clientAssertion = new PrivateKeyJwtService + { + CertificateFilename = jwkCertificateFilename, + CertificatePassword = jwkCertificatePassword, + + // Allow for clientId to be deliberately omitted from the JWT + Issuer = issuerClaim == Constants.Omit ? "" : issuerClaim ?? throw new InvalidOperationException($"{nameof(issuerClaim)} can not be empty unless intentionally omitted.").Log(), + + // Don't check for issuer if we are deliberately omitting clientId + RequireIssuer = clientId != Constants.Omit, + + Audience = URL + }.Generate(); + + formFields.Add(new KeyValuePair("client_assertion", clientAssertion)); + } + + if (shareDuration != null) + { + formFields.Add(new KeyValuePair("share_duration", shareDuration.ToString())); + } + + if (refreshToken != null) + { + formFields.Add(new KeyValuePair("refresh_token", refreshToken)); + } + + if (scope != null) + { + formFields.Add(new KeyValuePair("scope", scope)); + } + + formFields.Add(new KeyValuePair("code_verifier", Constants.AuthServer.FapiPhase2CodeVerifier)); + + var content = new FormUrlEncodedContent(formFields); + + Log.Information("Sending token request:\n {content}\n to {endpoint}.", content.ReadAsStringAsync().Result, URL); + + using var client = Helpers.Web.CreateHttpClient( + certificateFilename ?? throw new ArgumentNullException(nameof(certificateFilename)), + certificatePassword); + + Helpers.AuthServer.AttachHeadersForStandAlone(URL, content.Headers, _options.DH_MTLS_GATEWAY_URL, _authServeroptions.XTLSCLIENTCERTTHUMBPRINT, _authServeroptions.STANDALONE); + + var responseMessage = usePut == true ? + await client.PutAsync(URL, content) : + await client.PostAsync(URL, content); + + Log.Information("Response from endpoint:\n {content}", responseMessage.Content.ReadAsStringAsync().Result); + + return responseMessage; + } + + /// + /// Use authCode to get access token + /// + public async Task GetAccessToken(string authCode) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(GetAccessToken), nameof(DataHolderTokenService)); + + var responseMessage = await SendRequest(authCode); + if (responseMessage.StatusCode != HttpStatusCode.OK) + { + throw new InvalidOperationException($"{nameof(DataHolderTokenService)}.{nameof(GetAccessToken)} - Error getting access token - {responseMessage.StatusCode} - {await responseMessage.Content.ReadAsStringAsync()}").Log(); + } + + var tokenResponse = await JsonExtensions.DeserializeResponseAsync(responseMessage); + return tokenResponse?.AccessToken; + } + + /// + /// Use authCode to get tokens. + /// + public async Task GetResponse(string authCode, int? shareDuration = null, + string? clientId = null, + string? redirectUri = null, + string? certificateFilename = Constants.Certificates.CertificateFilename, + string? certificatePassword = Constants.Certificates.CertificatePassword, + string? jwkCertificateFilename = Constants.Certificates.JwtCertificateFilename, + string? jwkCertificatePassword = Constants.Certificates.JwtCertificatePassword, + string? scope = null //added for auth server tests + ) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(GetResponse), nameof(DataHolderTokenService)); + + if (string.IsNullOrWhiteSpace(redirectUri)) + { + redirectUri = _options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS; + } + + if (clientId == null) + { + clientId = _options.LastRegisteredClientId; + } + + var responseMessage = await SendRequest(authCode, shareDuration: shareDuration, + clientId: clientId, + issuerClaim: clientId, + redirectUri: redirectUri, + certificateFilename: certificateFilename, + certificatePassword: certificatePassword, + jwkCertificateFilename: jwkCertificateFilename, + jwkCertificatePassword: jwkCertificatePassword, + scope: scope + ); + + if (responseMessage.StatusCode != HttpStatusCode.OK) + { + throw new InvalidOperationException($"{nameof(DataHolderTokenService)}.{nameof(GetResponse)} - Error getting response - {responseMessage.StatusCode} - {await responseMessage.Content.ReadAsStringAsync()}").Log(); + } + + var response = await DeserializeResponse(responseMessage); + + return response; + } + + /// + /// Use refresh token to get tokens + /// + public async Task GetResponseUsingRefreshToken(string? refreshToken, string? scope = null) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(GetResponseUsingRefreshToken), nameof(DataHolderTokenService)); + + _ = refreshToken ?? throw new ArgumentNullException(nameof(refreshToken)); + + var tokenResponseMessage = await SendRequest( + grantType: "refresh_token", + refreshToken: refreshToken, + scope: scope + ); + + if (tokenResponseMessage.StatusCode != HttpStatusCode.OK) + { + var content = await tokenResponseMessage.Content.ReadAsStringAsync(); + throw new InvalidOperationException($"{nameof(DataHolderTokenService)}.{nameof(GetResponseUsingRefreshToken)} - Error getting response - {content}").Log(); + } + + var tokenResponse = await DeserializeResponse(tokenResponseMessage); + + return tokenResponse; + } + + public async Task DeserializeResponse(HttpResponseMessage response) + { + var responseContent = await response.Content.ReadAsStringAsync(); + + if (string.IsNullOrEmpty(responseContent)) + { + return null; + } + + return JsonConvert.DeserializeObject(responseContent); + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/PrivateKeyJwtService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/PrivateKeyJwtService.cs new file mode 100644 index 0000000..1841673 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/PrivateKeyJwtService.cs @@ -0,0 +1,63 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using Microsoft.IdentityModel.Tokens; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services +{ + public class PrivateKeyJwtService : IPrivateKeyJwtService + { + public bool RequireIssuer { get; init; } = true; + public string CertificateFilename { get; set; } + public string CertificatePassword { get; set; } + public string Issuer { get; set; } + public string Audience { get; set; } + + public string Generate() + { + var claims = new List + { + new Claim("sub", Issuer), + new Claim("jti", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer) + }; + + return Generate(claims, DateTime.UtcNow.AddMinutes(10)); + } + + private string Generate(IEnumerable claims, DateTime expires) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(Generate), nameof(PrivateKeyJwtService)); + + var certificate = new X509Certificate2(CertificateFilename, CertificatePassword, X509KeyStorageFlags.Exportable); + + var x509SigningCredentials = new X509SigningCredentials(certificate, SecurityAlgorithms.RsaSsaPssSha256); + + if (RequireIssuer) + { + if (string.IsNullOrEmpty(Issuer)) + { + throw new ArgumentException("issuer must be provided"); + } + } + + if (string.IsNullOrEmpty(Audience)) + { + throw new ArgumentException("audience must be provided"); + } + + var jwt = new JwtSecurityToken( + Issuer, + Audience, + claims, + expires: expires, + signingCredentials: x509SigningCredentials); + + var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); + + return jwtSecurityTokenHandler.WriteToken(jwt); + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/RegisterSSAService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/RegisterSSAService.cs new file mode 100644 index 0000000..c86b306 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/RegisterSSAService.cs @@ -0,0 +1,69 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using Microsoft.Extensions.Options; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services +{ + public class RegisterSsaService : IRegisterSsaService + { + private readonly TestAutomationOptions _options; + private readonly TestAutomationAuthServerOptions _authServerOptions; + private readonly IApiServiceDirector _apiServiceDirector; + + public RegisterSsaService(IOptions options, IOptions authServerOptions, IApiServiceDirector apiServiceDirector) + { + _options = options.Value ?? throw new ArgumentNullException(nameof(options)); + _authServerOptions = authServerOptions.Value ?? throw new ArgumentNullException(nameof(authServerOptions)); + _apiServiceDirector = apiServiceDirector ?? throw new ArgumentNullException(nameof(apiServiceDirector)); + } + /// + /// Get SSA from the Register + /// + public async Task GetSSA( + string brandId, + string softwareProductId, + string xv = "3", + string jwtCertificateFilename = Constants.Certificates.JwtCertificateFilename, + string jwtCertificatePassword = Constants.Certificates.JwtCertificatePassword, + Industry? industry = null) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(GetSSA), nameof(RegisterSsaService)); + + if (industry == null) + { + industry = _options.INDUSTRY switch + { + Industry.BANKING => Industry.BANKING, + Industry.ENERGY => Industry.ALL, //Energy was using the ALL parameter + _ => throw new ArgumentException($"{nameof(_options.INDUSTRY)}") + }; + } + + // Get access token + var registerAccessToken = await new AccessTokenService(_options.DH_MTLS_AUTHSERVER_TOKEN_URL) + { + URL = _options.REGISTER_MTLS_TOKEN_URL, + CertificateFilename = Constants.Certificates.CertificateFilename, + CertificatePassword = Constants.Certificates.CertificatePassword, + JwtCertificateFilename = jwtCertificateFilename, + JwtCertificatePassword = jwtCertificatePassword, + ClientId = softwareProductId, + Scope = "cdr-register:read", + ClientAssertionType = Constants.ClientAssertionType, + GrantType = "client_credentials", + Issuer = softwareProductId, + Audience = _options.REGISTER_MTLS_TOKEN_URL + }.GetAsync(_options.DH_MTLS_GATEWAY_URL, _authServerOptions.XTLSCLIENTCERTTHUMBPRINT, _authServerOptions.STANDALONE); + + // Get the SSA + var api = _apiServiceDirector.BuildRegisterSSAAPI(industry, brandId, softwareProductId, registerAccessToken, xv); + var response = await api.SendAsync(); + + var ssa = await response.Content.ReadAsStringAsync(); + + return ssa; + } + } +} \ No newline at end of file diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/SqlQueryService.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/SqlQueryService.cs new file mode 100644 index 0000000..a4224ee --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/Services/SqlQueryService.cs @@ -0,0 +1,80 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using Dapper; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Options; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services +{ + + public class SqlQueryService : ISqlQueryService + { + private readonly TestAutomationOptions _options; + + public SqlQueryService(IOptions options) + { + _options = options.Value ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Get DCR ClientId for a SoftwareProductID + /// + public string GetClientId(string softwareProductId) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(GetClientId), nameof(SqlQueryService)); + + using var connection = new SqlConnection(_options.AUTHSERVER_CONNECTIONSTRING); + + var clientId = connection.QuerySingle( + "select clientid from clientclaims where Upper(type)=@ClaimType and upper(value)=@ClaimValue", + new + { + ClaimType = "SOFTWARE_ID", + ClaimValue = softwareProductId.ToUpper() + }); + + if (string.IsNullOrEmpty(clientId)) + { + throw new InvalidOperationException($"{nameof(GetClientId)} - ClientId not found for SoftwareProductId {softwareProductId}").Log(); + } + + return clientId; + } + + public string GetStatus(EntityType entityType, string id) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(GetStatus), nameof(SqlQueryService)); + + using var connection = new SqlConnection(_options.AUTHSERVER_CONNECTIONSTRING); + connection.Open(); + + var statusColumnName = entityType == EntityType.SOFTWAREPRODUCT ? Status.STATUS.ToString() : $"{entityType}Status"; + using var selectCommand = new SqlCommand($"select {statusColumnName} from softwareproducts where {entityType}ID = @id", connection); + selectCommand.Parameters.AddWithValue("@id", id); + + return selectCommand.ExecuteScalarString(); + } + + public void SetStatus(EntityType entityType, string id, string status) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(SetStatus), nameof(SqlQueryService)); + + using var connection = new SqlConnection(_options.AUTHSERVER_CONNECTIONSTRING); + connection.Open(); + + var statusColumnName = entityType == EntityType.SOFTWAREPRODUCT ? Status.STATUS.ToString() : $"{entityType}Status"; + using var updateCommand = new SqlCommand($"update softwareproducts set {statusColumnName} = @status where {entityType}ID = @id", connection); + updateCommand.Parameters.AddWithValue("@id", id); + updateCommand.Parameters.AddWithValue("@status", status); + updateCommand.ExecuteNonQuery(); + + if (string.Compare(GetStatus(entityType, id), status.ToString()) != 0) + { + throw new InvalidOperationException("Status update failed").Log(); + } + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/SharedBaseTest.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/SharedBaseTest.cs new file mode 100644 index 0000000..746cfea --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/SharedBaseTest.cs @@ -0,0 +1,40 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Attributes; +using FluentAssertions.Execution; +using Microsoft.Extensions.Configuration; +using Serilog; +using Serilog.Extensions.Hosting; +using Xunit.DependencyInjection; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation +{ + [DisplayTestMethodName] + abstract public class SharedBaseTest + { + public IAssertionStrategy BaseTestAssertionStrategy { get; init; } + protected SharedBaseTest(ITestOutputHelperAccessor testOutputHelperAccessor, IConfiguration config) + { + BaseTestAssertionStrategy = new TestAssertionStrategy(); + + //Will only reload the first time, as it is frozen after that + ((ReloadableLogger)Log.Logger).Reload(lc => + { + return lc + .ReadFrom.Configuration(config) + .WriteTo.TestOutput(testOutputHelperAccessor.Output); + }); + + try + { + //Workaround as we can't tell if the Logger has been frozen, but it will fail if we try to freeze it + ((ReloadableLogger)Log.Logger).Freeze(); + } + catch (Exception) + { + //No need to add to log as the reconfiguration wasn't needde + return; + } + + Log.Information($"-Logger has been reconfigured to also write to TestOutput.-"); + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/TestAssertionStrategy.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/TestAssertionStrategy.cs new file mode 100644 index 0000000..05c8820 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/TestAssertionStrategy.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions.Execution; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation +{ + /// + /// A custom assertion strategy used for Participant Tooling assertion scopes which is a close copy of CollectingAssertionStratgy, but required due to the protection level of that class being internal + /// + public class TestAssertionStrategy : IAssertionStrategy + { + private readonly List _failureMessages = new List(); + + /// + /// Returns the messages for the assertion failures that happened until now. + /// + public IEnumerable FailureMessages => _failureMessages; + + /// + /// Discards and returns the failure messages that happened up to now. + /// + public IEnumerable DiscardFailures() + { + var discardedFailures = _failureMessages.ToArray(); + _failureMessages.Clear(); + return discardedFailures; + } + + /// + /// Will throw a combined exception for any failures have been collected. + /// Some additional logging has been added for better reporting of issues + /// + public void ThrowIfAny(IDictionary context) + { + if (_failureMessages.Any()) + { + var builder = new StringBuilder(); + builder.AppendJoin(Environment.NewLine, _failureMessages).AppendLine(); + + Log.Warning("Test Assertion failed with these issues: {ISSUES}.", _failureMessages); + + if (context.Any()) + { + foreach (KeyValuePair pair in context) + { + builder.AppendFormat(CultureInfo.InvariantCulture, "\nWith {0}:\n{1}", pair.Key, pair.Value); + } + } + + FluentAssertions.Common.Services.ThrowException(builder.ToString()); + } + } + + /// + /// Instructs the strategy to handle a assertion failure. + /// + public void HandleFailure(string message) + { + _failureMessages.Add(message); + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/Pages/Authorisation/AuthenticateLoginPage.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/Pages/Authorisation/AuthenticateLoginPage.cs new file mode 100644 index 0000000..8bf2e62 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/Pages/Authorisation/AuthenticateLoginPage.cs @@ -0,0 +1,63 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using Microsoft.Playwright; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UI.Pages.Authorisation +{ + public class AuthenticateLoginPage + { + private readonly IPage _page; + private readonly ILocator _hedMockDataHolderHeading; + private readonly ILocator _txtCustomerId; + private readonly ILocator _btnContinue; + private readonly ILocator _lblHelpForExampleUserNames; + + public AuthenticateLoginPage(IPage page, bool waitForPageToLoad = true) + { + _page = page; + _hedMockDataHolderHeading = _page.Locator("h6:has-text(\"Mock Data Holder\")", true); + _txtCustomerId = _page.Locator("id=mui-1", true); + _btnContinue = _page.Locator("button:has-text(\"Continue\")", true); + _lblHelpForExampleUserNames = _page.Locator("//div[@role='alert']", true); + + if (waitForPageToLoad) + { + _hedMockDataHolderHeading.WaitForAsync().Wait(); + } + } + + public async Task EnterCustomerId(string customerId) + { + await _txtCustomerId.WaitForAsync(); + await Task.Delay(1000); //require for JS delayed defaulting of field. It can sometimes overwrite the entered value. + await _txtCustomerId.FillAsync(""); + await _txtCustomerId.FillAsync(customerId); + } + + public async Task ClickContinue() + { + await _btnContinue.ClickAsync(); + } + + public async Task GetHelpForExampleUserNamesText() + { + return await _lblHelpForExampleUserNames.TextContentAsync(); + } + + public async Task CustomerIdErrorExists(string errorToCheckFor) + { + try + { + var element = await _page.WaitForSelectorAsync($"//p[text()='{errorToCheckFor}']", true); + + return await element.IsVisibleAsync(); + } + catch (TimeoutException) { } + { + Log.Error("A timeout exception was caught in {class}.{function}", nameof(AuthenticateLoginPage), nameof(CustomerIdErrorExists)); + return false; + } + } + + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/Pages/Authorisation/ConfirmAccountSharingPage.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/Pages/Authorisation/ConfirmAccountSharingPage.cs new file mode 100644 index 0000000..b31531d --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/Pages/Authorisation/ConfirmAccountSharingPage.cs @@ -0,0 +1,63 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using Microsoft.Playwright; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UI.Pages.Authorisation +{ + public class ConfirmAccountSharingPage + { + private readonly IPage _page; + private readonly ILocator _btnAuthorise; + private readonly ILocator _btnDeny; + + + public ConfirmAccountSharingPage(IPage page, bool waitForPageToLoad = true) + { + _page = page; + _btnAuthorise = _page.Locator("text=Authorise", true); + _btnDeny = _page.Locator("text=Deny", true); + + if (waitForPageToLoad) + { + _page.WaitForURLAsync($"**/confirmation"); + } + } + + public async Task ClickAuthorise() + { + await _btnAuthorise.ClickAsync(); + } + public async Task ClickDeny() + { + await _btnDeny.ClickAsync(); + } + + public async Task ClickCLusterHeadingToExpand(string clusterHeading) + { + await _page.Locator($"//a[text()='{clusterHeading}']", true).ClickAsync(); + } + + public async Task GetClusterDetail(string clusterHeading) + { + var stopAt = DateTime.Now.AddSeconds(10); + string clusterDetail = ""; + + // Need a custom sync to work around issue where cluster details sometimes do not load in time. + // Retry for up to 10 Seconds to get a non blank value for cluser details. + while (DateTime.Now < stopAt && String.IsNullOrEmpty(clusterDetail)) + { + await Task.Delay(100); + clusterDetail = await _page.Locator($"//div[@role='button' and .//a[text()='{clusterHeading}']]/..//p", true).InnerTextAsync(); + } + + return clusterDetail; + + } + + public async Task GetClusterCount() + { + var allClusterHeadings = await _page.QuerySelectorAllAsync("//div[contains(@class,'MuiAccordionSummary')]/a"); + return allClusterHeadings.Count; + } + + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/Pages/Authorisation/OneTimePasswordPage.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/Pages/Authorisation/OneTimePasswordPage.cs new file mode 100644 index 0000000..51bb1de --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/Pages/Authorisation/OneTimePasswordPage.cs @@ -0,0 +1,85 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using Microsoft.Playwright; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UI.Pages.Authorisation +{ + public class OneTimePasswordPage + { + private readonly IPage _page; + private readonly ILocator _txtOneTimePassword; + private readonly ILocator _btnCancel; + private readonly ILocator _btnContinue; + private readonly ILocator _divAlert; + private readonly ILocator _headDataHolderheading; + private readonly ILocator _btnCloseAlert; + + public OneTimePasswordPage(IPage page) + { + _page = page; + _txtOneTimePassword = _page.Locator("id=mui-2", true); + _btnCancel = _page.Locator("text=Cancel", true); + _btnContinue = _page.Locator("button:has-text(\"Continue\")", true); + _divAlert = _page.Locator("//div[@role='alert']", true); + _headDataHolderheading = _page.Locator("//h6", true); + _btnCloseAlert = _page.Locator("[role=\"alert\"]>>[title=\"Close\"]", true); + + } + public async Task EnterOtp(string otp) + { + await _txtOneTimePassword.WaitForAsync(); + await _txtOneTimePassword.FillAsync(otp); + } + + public async Task ClickContinue() + { + await _btnContinue.ClickAsync(); + } + + public async Task ClickCancel() + { + await _btnCancel.ClickAsync(); + } + + public async Task GetOneTimePasswordFieldValue() + { + return await _txtOneTimePassword.InputValueAsync(); + } + + public async Task GetAlertMessage() + { + return await _divAlert.InnerTextAsync(); + } + + public async Task GetDataHolderHeading() + { + return await _headDataHolderheading.InnerTextAsync(); + } + + public async Task AlertExists() + { + return await _divAlert.IsVisibleAsync(); + } + + public async Task CloseAlertMessage() + { + await _btnCloseAlert.ClickAsync(); + } + + public async Task OtpErrorExists(string errorToCheckFor) + { + try + { + var element = await _page.WaitForSelectorAsync($"//p[text()='{errorToCheckFor}']",true); + + return await element.IsVisibleAsync(); + } + catch (TimeoutException) { } + { + Log.Error("A timeout exception was caught in {class}.{function}",nameof(OneTimePasswordPage),nameof(OtpErrorExists)); + return false; + } + } + + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/Pages/Authorisation/SelectAccountsPage.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/Pages/Authorisation/SelectAccountsPage.cs new file mode 100644 index 0000000..445bd88 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/Pages/Authorisation/SelectAccountsPage.cs @@ -0,0 +1,93 @@ +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Playwright; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UI.Pages.Authorisation +{ + public class SelectAccountsPage + { + + private readonly IPage _page; + private readonly ILocator _btnContinue; + private readonly ILocator _btnCancel; + + public SelectAccountsPage(IPage page, bool waitForPageToLoad = true) + { + _page = page; + _btnContinue = _page.Locator("text=Continue", true); + _btnCancel = _page.Locator("text=Cancel", true); + if (waitForPageToLoad) + { + _page.WaitForURLAsync($"**/select-accounts"); + } + } + + public async Task SelectAccount(string accountToSelect) + { + if (accountToSelect.IsNullOrEmpty()) + { + throw new ArgumentOutOfRangeException(nameof(accountToSelect), "Parameter must contain a value.").Log(); + } + + await _page.Locator($"//input[@aria-labelledby='account-{accountToSelect}']", true).CheckAsync(); + } + public async Task SelectAccounts(string[] accountsToSelect) + { + foreach (string accountToSelect in accountsToSelect) + { + await SelectAccount(accountToSelect.Trim()); + } + + } + public async Task SelectAccounts(string accountsToSelectCsv) + { + if (accountsToSelectCsv.IsNullOrEmpty()) + { + throw new ArgumentOutOfRangeException(nameof(accountsToSelectCsv), "Parameter must contain a value.").Log(); + } + + string[]? accountsToSelectArray = accountsToSelectCsv?.Split(","); + + foreach (string accountToSelect in accountsToSelectArray) + { + await SelectAccount(accountToSelect.Trim()); + } + } + + public async Task SelectAllCheckboxes() + { + var allInputs = await _page.QuerySelectorAllAsync("//input[@type='checkbox']"); + + foreach (var input in allInputs) + { + await input.CheckAsync(); + } + + } + + public async Task ClickContinue() + { + await _btnContinue.ClickAsync(); + } + public async Task ClickCancel() + { + await _btnCancel.ClickAsync(); + } + public async Task NoAccountSelectedErrorExists() + { + try + { + var element = await _page.WaitForSelectorAsync($"//p[text()='Please select one or more Accounts']",true); + + return await element.IsVisibleAsync(); + } + catch (TimeoutException) { } + { + Log.Error("A timeout exception was caught in {class}.{function}", nameof(SelectAccountsPage), nameof(NoAccountSelectedErrorExists)); + return false; + } + } + + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/PlaywrightDriver.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/PlaywrightDriver.cs new file mode 100644 index 0000000..4ed928e --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/UI/PlaywrightDriver.cs @@ -0,0 +1,92 @@ +using Microsoft.Playwright; +using Serilog; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.UI +{ + public class PlaywrightDriver + { + public IBrowser Browser { get; set; } = null!; + private IPlaywright PlaywrightInstance { get; set; } = null!; + private IBrowserContext? _browserContext; + private string? _mediaFilePrefix; + private const string _mediaFilePath = "/testresults/media"; + + public async Task NewBrowserContext(string? mediaFilePrefix = null) + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(NewBrowserContext), nameof(PlaywrightDriver)); + + // mediaFilePrefix is usually set to the name of a test case. + // If media file prefix not provided, use a Guid. + if (String.IsNullOrEmpty(mediaFilePrefix)) + { + _mediaFilePrefix = Guid.NewGuid().ToString(); + } + else + { + _mediaFilePrefix = mediaFilePrefix; + } + + PlaywrightInstance = await Playwright.CreateAsync(); + Browser = await GetBrowser(); + + var options = new BrowserNewContextOptions + { + IgnoreHTTPSErrors = true, + }; + + options.ViewportSize = new ViewportSize + { + Width = 2000, + Height = 1000 + }; + + _browserContext = await Browser.NewContextAsync(options); + + await _browserContext.Tracing.StartAsync(new() + { + Screenshots = true, + Snapshots = true, + Sources = true, + }); + + return _browserContext; + } + + private async Task GetBrowser() + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(GetBrowser), nameof(PlaywrightDriver)); + + if (Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER")?.ToUpper() == "TRUE") + { + return await PlaywrightInstance.Firefox.LaunchAsync(new BrowserTypeLaunchOptions + { + Headless = true, + }); + } + else + { + return await PlaywrightInstance.Chromium.LaunchAsync(new BrowserTypeLaunchOptions + { + Headless = false, + }); + } + } + + public async Task DisposeAsync() + { + Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(DisposeAsync), nameof(PlaywrightDriver)); + + if (_browserContext != null) + { + await _browserContext.Tracing.StopAsync(new() + { + Path = $"{_mediaFilePath}/{_mediaFilePrefix}_trace.zip", + }); + + await Browser.CloseAsync(); + await Browser.DisposeAsync(); + PlaywrightInstance.Dispose(); + } + } + } +} diff --git a/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/XUnit/AlphabeticalOrderer.cs b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/XUnit/AlphabeticalOrderer.cs new file mode 100644 index 0000000..21bc9e3 --- /dev/null +++ b/Source/ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation/XUnit/AlphabeticalOrderer.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.XUnit +{ + public class AlphabeticalOrderer : ITestCaseOrderer + { + public IEnumerable OrderTestCases(IEnumerable testCases) where TTestCase : ITestCase => + testCases.OrderBy(testCase => testCase.TestMethod.Method.Name); + } +} \ No newline at end of file