From fe365d8345b49d9a6e3f9d27f00a86ff88b92c4c Mon Sep 17 00:00:00 2001 From: CDR-FarooqK <110141673+CDR-FarooqK@users.noreply.github.com> Date: Wed, 5 Oct 2022 10:26:42 +1100 Subject: [PATCH] v1.1.0 release --- .azuredevops/pipelines/build-dr-func-v2.yml | 134 +++++++ .azuredevops/pipelines/build-v2.yml | 21 +- CHANGELOG.md | 7 + README.md | 4 +- .../CDR.DataHolder.API.Gateway.mTLS.csproj | 6 +- ...Holder.API.Infrastructure.UnitTests.csproj | 6 +- .../CDR.DataHolder.API.Infrastructure.csproj | 9 +- .../CDR.DataHolder.API.Logger.csproj | 20 + .../IRequestResponseLogger.cs | 9 + .../LoggerExtensions.cs | 13 + .../RequestResponseLogger.cs | 54 +++ .../RequestResponseLoggingMiddleware.cs | 364 ++++++++++++++++++ .../CDR.DataHolder.Admin.API.csproj | 6 +- .../CDR.DataHolder.Domain.csproj | 6 +- ...DataHolder.IdentityServer.UnitTests.csproj | 6 +- .../CDR.DataHolder.IdentityServer.csproj | 9 +- .../CDR.DataHolder.IdentityServer/Program.cs | 2 + .../CDR.DataHolder.IdentityServer/Startup.cs | 12 + .../appsettings.Container.json | 123 +++++- .../appsettings.Development.json | 130 ++++++- .../appsettings.Release.json | 319 ++++++++++----- .../appsettings.json | 2 +- .../CDR.DataHolder.IntegrationTests.csproj | 6 +- ...MDH_InfosecProfileAPI_OIDCConfiguration.cs | 1 + .../integration.Release.runsettings | 10 + .../CDR.DataHolder.Manage.API.csproj | 6 +- .../CDR.DataHolder.Public.API.csproj | 6 +- .../CDR.DataHolder.Repository.csproj | 6 +- ...R.DataHolder.Resource.API.UnitTests.csproj | 6 +- .../CDR.DataHolder.Resource.API.csproj | 11 +- .../Controllers/ResourceController.cs | 3 + Source/CDR.DataHolder.Resource.API/Program.cs | 2 + Source/CDR.DataHolder.Resource.API/Startup.cs | 13 + .../appsettings.Container.json | 289 ++++++++++---- .../appsettings.Development.json | 296 ++++++++++---- .../appsettings.Release.json | 289 ++++++++++---- .../BaseTest.cs | 63 +++ ....GetDataRecipients.IntegrationTests.csproj | 49 +++ .../ConnectionStringCheck.cs | 85 ++++ .../DatabaseSeeder.cs | 315 +++++++++++++++ .../Fixtures/TestFixture.cs | 18 + .../US28391_GetDataRecipients.cs | 221 +++++++++++ .../XUnit/AlphabeticalOrderer.cs | 13 + .../appsettings.Development.json | 9 + .../appsettings.Release.json | 9 + .../appsettings.json | 2 + .../local.settings.json | 27 ++ .../test.http | 14 + .../CDR.GetDataRecipients.csproj | 6 +- .../GetDataRecipients.cs | 35 +- ...etDataRecipients_IntegrationTestsHelper.cs | 33 ++ Source/DataHolder.sln | 14 + Source/DockerCompose/docker-compose.yml | 2 + Source/Dockerfile | 1 + Source/Dockerfile.for-testing | 1 + Source/Dockerfile.get-data-recipients | 18 + ...file.get-data-recipients.integration-tests | 28 ++ ...ose.GetDataRecipients.IntegrationTests.yml | 116 ++++++ Source/integration.runsettings | 10 + ...-integration-tests-get-data-recipients.ps1 | 30 ++ Source/supervisord.conf | 7 + 61 files changed, 2921 insertions(+), 411 deletions(-) create mode 100644 .azuredevops/pipelines/build-dr-func-v2.yml create mode 100644 Source/CDR.DataHolder.API.Logger/CDR.DataHolder.API.Logger.csproj create mode 100644 Source/CDR.DataHolder.API.Logger/IRequestResponseLogger.cs create mode 100644 Source/CDR.DataHolder.API.Logger/LoggerExtensions.cs create mode 100644 Source/CDR.DataHolder.API.Logger/RequestResponseLogger.cs create mode 100644 Source/CDR.DataHolder.API.Logger/RequestResponseLoggingMiddleware.cs create mode 100644 Source/CDR.DataHolder.IntegrationTests/integration.Release.runsettings create mode 100644 Source/CDR.GetDataRecipients.IntegrationTests/BaseTest.cs create mode 100644 Source/CDR.GetDataRecipients.IntegrationTests/CDR.GetDataRecipients.IntegrationTests.csproj create mode 100644 Source/CDR.GetDataRecipients.IntegrationTests/ConnectionStringCheck.cs create mode 100644 Source/CDR.GetDataRecipients.IntegrationTests/DatabaseSeeder.cs create mode 100644 Source/CDR.GetDataRecipients.IntegrationTests/Fixtures/TestFixture.cs create mode 100644 Source/CDR.GetDataRecipients.IntegrationTests/US28391_GetDataRecipients.cs create mode 100644 Source/CDR.GetDataRecipients.IntegrationTests/XUnit/AlphabeticalOrderer.cs create mode 100644 Source/CDR.GetDataRecipients.IntegrationTests/appsettings.Development.json create mode 100644 Source/CDR.GetDataRecipients.IntegrationTests/appsettings.Release.json create mode 100644 Source/CDR.GetDataRecipients.IntegrationTests/appsettings.json create mode 100644 Source/CDR.GetDataRecipients.IntegrationTests/local.settings.json create mode 100644 Source/CDR.GetDataRecipients.IntegrationTests/test.http create mode 100644 Source/CDR.GetDataRecipients/GetDataRecipients_IntegrationTestsHelper.cs create mode 100644 Source/Dockerfile.get-data-recipients create mode 100644 Source/Dockerfile.get-data-recipients.integration-tests create mode 100644 Source/docker-compose.GetDataRecipients.IntegrationTests.yml create mode 100644 Source/integration.runsettings create mode 100644 Source/run-integration-tests-get-data-recipients.ps1 diff --git a/.azuredevops/pipelines/build-dr-func-v2.yml b/.azuredevops/pipelines/build-dr-func-v2.yml new file mode 100644 index 0000000..5f78ab6 --- /dev/null +++ b/.azuredevops/pipelines/build-dr-func-v2.yml @@ -0,0 +1,134 @@ + +resources: + repositories: + - repository: MockRegister + type: git + name: sb-mock-register + ref: develop + +trigger: + - develop + - main + - releases/* + +pool: + vmImage: ubuntu-latest + +steps: + - checkout: MockRegister + - checkout: self + + # Build mock-register + - task: Docker@2 + displayName: Build mock-register image + inputs: + command: build + Dockerfile: $(Build.SourcesDirectory)/sb-mock-register/Source/Dockerfile.for-testing + buildContext: $(Build.SourcesDirectory)/sb-mock-register/Source + repository: mock-register + tags: latest + + # Run integration tests + - task: DockerCompose@0 + displayName: Integration Tests - Up + condition: always() + inputs: + action: Run a Docker Compose command + dockerComposeFile: $(Build.SourcesDirectory)/sb-mock-data-holder/Source/docker-compose.GetDataRecipients.IntegrationTests.yml + dockerComposeCommand: up --abort-on-container-exit --exit-code-from getdatarecipients-integration-tests + + # Remove integration tests + - task: DockerCompose@0 + displayName: Integration Tests - Down + condition: always() + inputs: + action: Run a Docker Compose command + dockerComposeFile: $(Build.SourcesDirectory)/sb-mock-data-holder/Source/docker-compose.GetDataRecipients.IntegrationTests.yml + dockerComposeCommand: down + + # Publish mock-register logs + - publish: $(Build.SourcesDirectory)/sb-mock-data-holder/Source/_temp/mock-register/tmp + displayName: Publish MockRegister logs + condition: always() + artifact: Mock-Register - Logs + + # Publish mock-data-holder logs + - publish: $(Build.SourcesDirectory)/sb-mock-data-holder/Source/_temp/mock-data-holder/tmp + displayName: Publish MockDataHolder logs + condition: always() + artifact: Mock-Data-Holder - Logs + + # Login to ACR + - task: Docker@2 + displayName: Login to ACR + condition: always() + inputs: + command: login + containerRegistry: <> + + # Run trx formatter to output .MD and .CSV + - script: | + docker run \ + -v=$(Build.SourcesDirectory)/sb-mock-data-holder/Source/_temp/getdatarecipients-integration-tests/testresults/results.trx:/app/results.trx:ro \ + -v=$(Build.SourcesDirectory)/sb-mock-data-holder/Source/_temp/getdatarecipients-integration-tests/testresults/formatted/:/app/out/:rw \ + <>.azurecr.io/trx-formatter -i results.trx -t "MDH-GetDataRecipients" --outputprefix "MDH-GetDataRecipients" -o out/ + displayName: 'Run trx-formatter' + condition: always() + + # Publish getdatarecipients integration tests results + - publish: $(Build.SourcesDirectory)/sb-mock-data-holder/Source/_temp/getdatarecipients-integration-tests/testresults + displayName: Publish integration tests + condition: always() + artifact: GetDataRecipients - Integration tests + + - task: PublishTestResults@2 + displayName: 'Surface Integration Test TRX results to devops' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'VSTest' # Options: JUnit, NUnit, VSTest, xUnit, cTest + testResultsFiles: '**/results.trx' + searchFolder: $(Build.SourcesDirectory)/sb-mock-data-holder/Source/_temp/getdatarecipients-integration-tests/testresults + #mergeTestResults: false # Optional + #failTaskOnFailedTests: false # Optional + testRunTitle: 'getdatarecipients-integration-tests' # Optional + #buildPlatform: # Optional + #buildConfiguration: # Optional + publishRunAttachments: true # Optional + +# Tests have passed, so now build/publish the azure function + + - task: UseDotNet@2 + displayName: 'Install .NET 6 SDK' + inputs: + packageType: 'sdk' + version: '6.0.x' + performMultiLevelLookup: true + + - script: | + cd $(Build.SourcesDirectory)/sb-mock-data-holder/Source/CDR.GetDataRecipients + dotnet restore + dotnet build --configuration Release + displayName: 'Build CDR.GetDataRecipients' + + - task: DotNetCoreCLI@2 + inputs: + command: publish + arguments: '--configuration Release --output publish_output' + projects: '$(Build.SourcesDirectory)/sb-mock-data-holder/Source/CDR.GetDataRecipients/CDR.GetDataRecipients.csproj' + publishWebProjects: false + modifyOutputPath: false + zipAfterPublish: false + displayName: 'DotNet publish CDR.GetDataRecipients' + + - task: ArchiveFiles@2 + displayName: 'Archive CDR.GetDataRecipients' + inputs: + rootFolderOrFile: '$(System.DefaultWorkingDirectory)/publish_output' + includeRootFolder: false + archiveFile: '$(System.DefaultWorkingDirectory)/CDR.GetDataRecipients.zip' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish CDR.GetDataRecipients' + inputs: + PathToPublish: '$(System.DefaultWorkingDirectory)/CDR.GetDataRecipients.zip' + artifactName: 'functions' diff --git a/.azuredevops/pipelines/build-v2.yml b/.azuredevops/pipelines/build-v2.yml index efeaec9..19e257f 100644 --- a/.azuredevops/pipelines/build-v2.yml +++ b/.azuredevops/pipelines/build-v2.yml @@ -147,14 +147,29 @@ steps: condition: always() artifact: Mock-Data-Holder - Unit tests +# Login to ACR +- task: Docker@2 + displayName: Login to ACR + condition: always() + inputs: + command: login + containerRegistry: <> + +# Run trx formatter to output .MD and .CSV +- script: | + docker run \ + -v=$(Build.SourcesDirectory)/sb-mock-data-holder/Source/_temp/mock-data-holder-integration-tests/testresults/results.trx:/app/results.trx:ro \ + -v=$(Build.SourcesDirectory)/sb-mock-data-holder/Source/_temp/mock-data-holder-integration-tests/testresults/formatted/:/app/out/:rw \ + <>.azurecr.io/trx-formatter -i results.trx -t "MDH" --outputprefix "MDH" -o out/ + displayName: 'Run trx-formatter' + condition: always() + # Publish mock-data-holder integration tests results - publish: $(Build.SourcesDirectory)/sb-mock-data-holder/Source/_temp/mock-data-holder-integration-tests/testresults displayName: Publish integration tests condition: always() artifact: Mock-Data-Holder - Integration tests -# TODO - MJS - Run formatter over TRX to produce formatted report suitable for attaching to Devops US & publish as artifact - - task: UseDotNet@2 displayName: 'Use .NET 6 sdk' condition: always() @@ -199,4 +214,4 @@ steps: #testRunTitle: # Optional #buildPlatform: # Optional #buildConfiguration: # Optional - #publishRunAttachments: true # Optional \ No newline at end of file + #publishRunAttachments: true # Optional diff --git a/CHANGELOG.md b/CHANGELOG.md index a609cb5..75768ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.0] - 2022-10-05 +### Added +- Logging middleware to create a centralised list of all API requests and responses + +### Fixed +- Updated supported response modes in OIDC discovery endpoint. [Issue 46](https://github.com/ConsumerDataRight/mock-data-holder/issues/46) + ## [1.0.1] - 2022-08-30 ### Changed - Updated package references. diff --git a/README.md b/README.md index 25e4c8c..1452111 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Consumer Data Right Logo](https://raw.githubusercontent.com/ConsumerDataRight/mock-data-holder/main/cdr-logo.png) -[![Consumer Data Standards v1.17.0](https://img.shields.io/badge/Consumer%20Data%20Standards-v1.17.0-blue.svg)](https://consumerdatastandardsaustralia.github.io/standards/#introduction) +[![Consumer Data Standards v1.18.0](https://img.shields.io/badge/Consumer%20Data%20Standards-v1.18.0-blue.svg)](https://consumerdatastandardsaustralia.github.io/standards/#introduction) [![Conformance Test Suite 4.0](https://img.shields.io/badge/Conformance%20Test%20Suite-v4.0-darkblue.svg)](https://www.cdr.gov.au/for-providers/conformance-test-suite-data-holders) [![FAPI 1.0 Advanced Profile](https://img.shields.io/badge/FAPI%201.0-orange.svg)](https://openid.net/specs/openid-financial-api-part-2-1_0.html) [![made-with-dotnet](https://img.shields.io/badge/Made%20with-.NET-1f425Ff.svg)](https://dotnet.microsoft.com/) @@ -14,7 +14,7 @@ This project includes source code, documentation and instructions for a Consumer This repository contains a mock implementation of a Mock Data Holder and is offered to help the community in the development and testing of their CDR solutions. ## Mock Data Holder - Alignment -The Mock Data Holder aligns to [v1.17.0](https://consumerdatastandardsaustralia.github.io/standards/#introduction) of the [Consumer Data Standards](https://consumerdatastandardsaustralia.github.io/standards/#introduction). +The Mock Data Holder aligns to [v1.18.0](https://consumerdatastandardsaustralia.github.io/standards/#introduction) of the [Consumer Data Standards](https://consumerdatastandardsaustralia.github.io/standards/#introduction). The Mock Data Holder passed v4.0 of the [Conformance Test Suite for Data Holders](https://www.cdr.gov.au/for-providers/conformance-test-suite-data-holders). The Mock Data Holder is compliant with the [FAPI 1.0 Advanced Profile](https://openid.net/specs/openid-financial-api-part-2-1_0.html). The Mock Data Holder aligns to [FAPI 1.0 Migration Phase 1 and Phase 2](https://consumerdatastandardsaustralia.github.io/standards/#authentication-flows). Phase 1 requirements are switched on by default. Configuration has been added to allow switching on Phase 2 requirements. diff --git a/Source/CDR.DataHolder.API.Gateway.mTLS/CDR.DataHolder.API.Gateway.mTLS.csproj b/Source/CDR.DataHolder.API.Gateway.mTLS/CDR.DataHolder.API.Gateway.mTLS.csproj index f3776dd..454bdf5 100644 --- a/Source/CDR.DataHolder.API.Gateway.mTLS/CDR.DataHolder.API.Gateway.mTLS.csproj +++ b/Source/CDR.DataHolder.API.Gateway.mTLS/CDR.DataHolder.API.Gateway.mTLS.csproj @@ -3,9 +3,9 @@ net6.0 win-x64;linux-x64 - 1.0.1 - 1.0.1 - 1.0.1 + 1.1.0 + 1.1.0 + 1.1.0 diff --git a/Source/CDR.DataHolder.API.Infrastructure.UnitTests/CDR.DataHolder.API.Infrastructure.UnitTests.csproj b/Source/CDR.DataHolder.API.Infrastructure.UnitTests/CDR.DataHolder.API.Infrastructure.UnitTests.csproj index 2617cb4..d176ede 100644 --- a/Source/CDR.DataHolder.API.Infrastructure.UnitTests/CDR.DataHolder.API.Infrastructure.UnitTests.csproj +++ b/Source/CDR.DataHolder.API.Infrastructure.UnitTests/CDR.DataHolder.API.Infrastructure.UnitTests.csproj @@ -5,11 +5,11 @@ false - 1.0.1 + 1.1.0 - 1.0.1 + 1.1.0 - 1.0.1 + 1.1.0 diff --git a/Source/CDR.DataHolder.API.Infrastructure/CDR.DataHolder.API.Infrastructure.csproj b/Source/CDR.DataHolder.API.Infrastructure/CDR.DataHolder.API.Infrastructure.csproj index 3ac3ecd..a1175e7 100644 --- a/Source/CDR.DataHolder.API.Infrastructure/CDR.DataHolder.API.Infrastructure.csproj +++ b/Source/CDR.DataHolder.API.Infrastructure/CDR.DataHolder.API.Infrastructure.csproj @@ -2,16 +2,19 @@ net6.0 - 1.0.1 - 1.0.1 - 1.0.1 + 1.1.0 + 1.1.0 + 1.1.0 + + + diff --git a/Source/CDR.DataHolder.API.Logger/CDR.DataHolder.API.Logger.csproj b/Source/CDR.DataHolder.API.Logger/CDR.DataHolder.API.Logger.csproj new file mode 100644 index 0000000..b08e1ee --- /dev/null +++ b/Source/CDR.DataHolder.API.Logger/CDR.DataHolder.API.Logger.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + 1.1.0 + 1.1.0 + 1.1.0 + + + + + + + + + + + diff --git a/Source/CDR.DataHolder.API.Logger/IRequestResponseLogger.cs b/Source/CDR.DataHolder.API.Logger/IRequestResponseLogger.cs new file mode 100644 index 0000000..cf0b1e0 --- /dev/null +++ b/Source/CDR.DataHolder.API.Logger/IRequestResponseLogger.cs @@ -0,0 +1,9 @@ +namespace CDR.DataHolder.API.Logger +{ + using Serilog; + + public interface IRequestResponseLogger + { + ILogger Log { get; } + } +} \ No newline at end of file diff --git a/Source/CDR.DataHolder.API.Logger/LoggerExtensions.cs b/Source/CDR.DataHolder.API.Logger/LoggerExtensions.cs new file mode 100644 index 0000000..a85ab6e --- /dev/null +++ b/Source/CDR.DataHolder.API.Logger/LoggerExtensions.cs @@ -0,0 +1,13 @@ +namespace CDR.DataHolder.API.Logger +{ + using Microsoft.Extensions.DependencyInjection; + + public static class LoggerExtensions + { + public static IServiceCollection AddRequestResponseLogging(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } + } +} \ No newline at end of file diff --git a/Source/CDR.DataHolder.API.Logger/RequestResponseLogger.cs b/Source/CDR.DataHolder.API.Logger/RequestResponseLogger.cs new file mode 100644 index 0000000..8d1e210 --- /dev/null +++ b/Source/CDR.DataHolder.API.Logger/RequestResponseLogger.cs @@ -0,0 +1,54 @@ +namespace CDR.DataHolder.API.Logger +{ + using System.Diagnostics; + using Microsoft.Extensions.Configuration; + using Serilog; + using Serilog.Core; + + public class RequestResponseLogger : IRequestResponseLogger, IDisposable + { + private readonly Logger _logger; + private readonly IConfiguration _configuration; + + public ILogger Log { get { return _logger; } } + + public RequestResponseLogger(IConfiguration configuration) + { + + _configuration = configuration; + + _logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration, sectionName: "SerilogRequestResponseLogger") + .Enrich.WithProperty("RequestMethod", "") + .Enrich.WithProperty("RequestBody", "") + .Enrich.WithProperty("RequestHeaders", "") + .Enrich.WithProperty("RequestPath", "") + .Enrich.WithProperty("RequestQueryString", "") + .Enrich.WithProperty("StatusCode", "") + .Enrich.WithProperty("ElapsedTime", "") + .Enrich.WithProperty("ResponseHeaders", "") + .Enrich.WithProperty("ResponseBody", "") + .Enrich.WithProperty("RequestHost", "") + .Enrich.WithProperty("RequestIpAddress", "") + .Enrich.WithProperty("ClientId", "") + .Enrich.WithProperty("SoftwareId", "") + .Enrich.WithProperty("FapiInteractionId", "") + .CreateLogger(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Serilog.Log.CloseAndFlush(); + } + + } + } +} \ No newline at end of file diff --git a/Source/CDR.DataHolder.API.Logger/RequestResponseLoggingMiddleware.cs b/Source/CDR.DataHolder.API.Logger/RequestResponseLoggingMiddleware.cs new file mode 100644 index 0000000..9fb49c6 --- /dev/null +++ b/Source/CDR.DataHolder.API.Logger/RequestResponseLoggingMiddleware.cs @@ -0,0 +1,364 @@ +using System.Diagnostics; +using System.IdentityModel.Tokens.Jwt; +using System.Net; +using System.Net.Http.Headers; +using System.Web; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.IO; +using Microsoft.Net.Http.Headers; +using Serilog; +using Serilog.Events; + +namespace CDR.DataHolder.API.Logger +{ + public class RequestResponseLoggingMiddleware + { + const string httpSummaryMessageTemplate = + "HTTP {RequestMethod} {RequestScheme:l}://{RequestHost:l}{RequestPathBase:l}{RequestPath:l} responded {StatusCode} in {ElapsedTime:0.0000} ms."; + + const string httpSummaryExceptionMessageTemplate = + "HTTP {RequestMethod} {RequestScheme:l}://{RequestHost:l}{RequestPathBase:l}{RequestPath:l} encountered following error {error}"; + + private string? _requestMethod; + private string? _requestBody; + private string? _requestHeaders; + private string? _requestPath; + private string? _requestQueryString; + private string? _statusCode; + private string? _elapsedTime; + private string? _responseHeaders; + private string? _responseBody; + private string? _requestHost; + private string? _requestIpAddress; + private string? _requestScheme; + private string? _exceptionMessage; + private string? _requestPathBase; + private string? _clientId; + private string? _softwareId; + private string? _fapiInteractionId; + private readonly string? _currentProcessName; + + + private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; + readonly RequestDelegate _next; + private readonly ILogger _requestResponseLogger; + private readonly IConfiguration _configuration; + + + + public RequestResponseLoggingMiddleware(RequestDelegate next, IRequestResponseLogger requestResponseLogger, IConfiguration configuration) + { + _requestResponseLogger = requestResponseLogger.Log.ForContext(); + _next = next ?? throw new ArgumentNullException(nameof(next)); + _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); + _currentProcessName = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name; + _configuration = configuration; + } + + public async Task InvokeAsync(HttpContext context) + { + InitMembers(); + await ExtractRequestProperties(context); + await ExtractResponseProperties(context); + } + + private void InitMembers() + { + _requestMethod = _requestBody = _requestHeaders = _requestPath = _requestQueryString = + _statusCode = _elapsedTime = _responseHeaders = _responseBody = _requestHost = _requestScheme = + _exceptionMessage = _requestPathBase = _clientId = _softwareId = _fapiInteractionId = _requestIpAddress = string.Empty; + } + + private void LogWithContext() + { + var logger = _requestResponseLogger + .ForContext("SourceContext", GetSourceContext()) + .ForContext("RequestMethod", _requestMethod) + .ForContext("RequestHost", _requestHost) + .ForContext("RequestIpAddress", _requestIpAddress) + .ForContext("RequestBody", _requestBody) + .ForContext("RequestHeaders", _requestHeaders) + .ForContext("RequestPath", _requestPath) + .ForContext("RequestQueryString", _requestQueryString) + .ForContext("StatusCode", _statusCode) + .ForContext("ResponseHeaders", _responseHeaders) + .ForContext("ResponseBody", _responseBody) + .ForContext("ClientId", _clientId) + .ForContext("SoftwareId", _softwareId) + .ForContext("FapiInteractionId", _fapiInteractionId); + + if (!string.IsNullOrEmpty(_exceptionMessage)) + { + logger.Error(httpSummaryExceptionMessageTemplate, _requestMethod, _requestScheme, _requestHost, _requestPathBase, _requestPath, _exceptionMessage); + } + else + { + logger.Write(LogEventLevel.Information, httpSummaryMessageTemplate, _requestMethod, _requestScheme, _requestHost, _requestPathBase, _requestPath, _statusCode, _elapsedTime); + } + } + + + private async Task ExtractRequestProperties(HttpContext context) + { + try + { + context.Request.EnableBuffering(); + await using var requestStream = _recyclableMemoryStreamManager.GetStream(); + await context.Request.Body.CopyToAsync(requestStream); + + _requestBody = ReadStreamInChunks(requestStream); + context.Request.Body.Position = 0; + + _requestHost = GetHost(context.Request); + _requestIpAddress = GetIpAddress(context); + _requestMethod = context.Request.Method; + _requestScheme = context.Request.Scheme; + _requestPath = context.Request.Path; + _requestQueryString = context.Request.QueryString.ToString(); + _requestPathBase = context.Request.PathBase.ToString(); + + IEnumerable keyValues = context.Request.Headers.Keys.Select(key => key + ": " + string.Join(",", context.Request.Headers[key])); + _requestHeaders = string.Join(Environment.NewLine, keyValues); + + ExtractIdFromRequest(context.Request); + } + catch (Exception ex) + { + _exceptionMessage = ex.Message; + } + } + + class ClaimIdentifiers + { + public const string ClientId = "client_id"; + public const string SoftwareId = "software_id"; + public const string Iss = "iss"; + } + + void SetIdFromJwt(string jwt, string identifierType, ref string idToSet) + { + var handler = new JwtSecurityTokenHandler(); + if (handler.CanReadToken(jwt) == true) + { + var decodedJwt = handler.ReadJwtToken(jwt); + var id = decodedJwt.Claims.FirstOrDefault(x => x.Type == identifierType)?.Value ?? ""; + + idToSet = id; + } + } + + private void ExtractIdFromRequest(HttpRequest request) + { + try + { + //try fetching x-fapi-interaction-id. After fetching we don't return as we need other important ids. + _fapiInteractionId = string.Empty; + if (request.Headers.TryGetValue("x-fapi-interaction-id", out var interactionid)) + { + _fapiInteractionId = interactionid; + } + + //try fetching from the JWT in the authorization header + var authorization = request.Headers[HeaderNames.Authorization]; + if (AuthenticationHeaderValue.TryParse(authorization, out var headerValue) && string.IsNullOrEmpty(_clientId) == true) + { + var scheme = headerValue.Scheme; + var parameter = headerValue.Parameter; + + if (scheme == JwtBearerDefaults.AuthenticationScheme && parameter != null) + { + _clientId = String.Empty; + SetIdFromJwt(parameter, ClaimIdentifiers.ClientId, ref _clientId); + } + } + + //try fetching from the clientid in the body for connect/par + if (string.IsNullOrEmpty(_requestBody) == false && _requestBody.Contains("client_assertion=") == true && string.IsNullOrEmpty(_clientId) == true) + { + var nameValueCollection = HttpUtility.ParseQueryString(_requestBody); + if (nameValueCollection != null) + { + var assertion = nameValueCollection["client_assertion"]; + + if (assertion != null) + { + // in this case we set the iss to clientid + _clientId = String.Empty; + SetIdFromJwt(assertion, ClaimIdentifiers.Iss, ref _clientId); + } + } + + } + + //try fetching from the clientid in the body account/login, /consent, /token + if (string.IsNullOrEmpty(_requestBody) == false && _requestBody.Contains(ClaimIdentifiers.ClientId) == true && string.IsNullOrEmpty(_clientId) == true) + { + var decodedBody = HttpUtility.UrlDecode(_requestBody); + if (decodedBody.StartsWith("ReturnUrl=/connect/authorize/callback") == true) + { + var queryString = decodedBody["ReturnUrl=/connect/authorize/callback".Length..]; + var nameValueCollection = HttpUtility.ParseQueryString(queryString); + + if (nameValueCollection != null) + { + _clientId = nameValueCollection[ClaimIdentifiers.ClientId]; + } + } + + var parameterCollection = HttpUtility.ParseQueryString(decodedBody); + if (parameterCollection != null && string.IsNullOrEmpty(_clientId) == true) + { + _clientId = parameterCollection[ClaimIdentifiers.ClientId]; + return; + } + } + + + if (request.ContentType == "application/jwt") + { + //decode jwt sent to register + var handler = new JwtSecurityTokenHandler(); + if (handler.CanReadToken(_requestBody)) + { + var decodedRegisterJWT = handler.ReadJwtToken(_requestBody); + var softStatementValue = decodedRegisterJWT?.Claims?.FirstOrDefault(claim => claim.Type == "software_statement")?.Value; + + if (softStatementValue != null) + { + _softwareId = String.Empty; + SetIdFromJwt(softStatementValue, ClaimIdentifiers.SoftwareId, ref _softwareId); + return; + } + } + } + + //try fetching from query string, this should be the last place to check for client id. + if (request.QueryString.Value?.Contains(ClaimIdentifiers.ClientId) == true && string.IsNullOrEmpty(_clientId) == true) + { + var nameValueCollection = HttpUtility.ParseQueryString(request.QueryString.Value); + if (nameValueCollection != null) + { + _clientId = nameValueCollection[ClaimIdentifiers.ClientId]; + } + } + } + catch (Exception ex) + { + _exceptionMessage = ex.Message; + } + + } + + private string ReadStreamInChunks(Stream stream) + { + try + { + const int readChunkBufferLength = 4096; + stream.Seek(0, SeekOrigin.Begin); + using var textWriter = new StringWriter(); + using var reader = new StreamReader(stream); + var readChunk = new char[readChunkBufferLength]; + int readChunkLength; + do + { + readChunkLength = reader.ReadBlock(readChunk, 0, readChunkBufferLength); + textWriter.Write(readChunk, 0, readChunkLength); + } while (readChunkLength > 0); + return textWriter.ToString(); + } + catch (Exception ex) + { + _exceptionMessage = ex.Message; + } + + return ""; + } + + + private async Task ExtractResponseProperties(HttpContext httpContext) + { + + var originalBodyStream = httpContext.Response.Body; + await using var responseBody = _recyclableMemoryStreamManager.GetStream(); + httpContext.Response.Body = responseBody; + + var sw = Stopwatch.StartNew(); + + try + { + await _next(httpContext); + } + catch (Exception ex) + { + _exceptionMessage = ex.Message; + throw; + } + finally + { + sw.Stop(); + _elapsedTime = sw.ElapsedMilliseconds.ToString(); + + responseBody.Seek(0, SeekOrigin.Begin); + _responseBody = await new StreamReader(responseBody).ReadToEndAsync(); + responseBody.Seek(0, SeekOrigin.Begin); + + IEnumerable keyValues = httpContext.Response.Headers.Keys.Select(key => key + ": " + string.Join(",", httpContext.Response.Headers[key])); + _responseHeaders = string.Join(System.Environment.NewLine, keyValues); + + _statusCode = httpContext.Response.StatusCode.ToString(); + + LogWithContext(); + + // This is for middleware hooked before us to see our changes. + // Otherwise the original stream would be seen which cannot be read again. + await responseBody.CopyToAsync(originalBodyStream); + } + } + + private string GetHost(HttpRequest request) + { + // 1. check if the X-Forwarded-Host header has been provided -> use that + // 2. If not, use the request.Host + string hostHeaderKey = _configuration.GetValue("SerilogRequestResponseLogger:HostNameHeaderKey") ?? "X-Forwarded-Host"; + + if (!request.Headers.TryGetValue(hostHeaderKey, out var keys)) + { + return request.Host.ToString(); + } + + return keys.First(); + } + + private string? GetIpAddress(HttpContext context) + { + string ipHeaderKey = _configuration.GetValue("SerilogRequestResponseLogger:IPAddressHeaderKey") ?? "X-Forwarded-For"; + + if (!context.Request.Headers.TryGetValue(ipHeaderKey, out var keys)) + { + return context.Connection.RemoteIpAddress?.ToString(); + } + + // The Client IP address may contain a comma separated list of ip addresses based on the network devices + // the traffic traverses through. We get the first (and potentially only) ip address from the list as the client IP. + // We also remove any port numbers that may be included on the client IP. + return keys.First() + .Split(',')[0] // Get the first IP address in the list, in case there are multiple. + .Split(':')[0]; // Strip off the port number, in case it is attached to the IP address. + } + + private string GetSourceContext() + { + switch(_currentProcessName) + { + case "CDR.DataHolder.Resource.API": + return "SB-DHB-RES"; + case "CDR.DataHolder.IdentityServer": + return "SB-DHB-ID"; + } + return string.Empty; + } + + } +} diff --git a/Source/CDR.DataHolder.Admin.API/CDR.DataHolder.Admin.API.csproj b/Source/CDR.DataHolder.Admin.API/CDR.DataHolder.Admin.API.csproj index 5a1fabc..9d57399 100644 --- a/Source/CDR.DataHolder.Admin.API/CDR.DataHolder.Admin.API.csproj +++ b/Source/CDR.DataHolder.Admin.API/CDR.DataHolder.Admin.API.csproj @@ -3,9 +3,9 @@ net6.0 win-x64;linux-x64 - 1.0.1 - 1.0.1 - 1.0.1 + 1.1.0 + 1.1.0 + 1.1.0 diff --git a/Source/CDR.DataHolder.Domain/CDR.DataHolder.Domain.csproj b/Source/CDR.DataHolder.Domain/CDR.DataHolder.Domain.csproj index c5df528..3e4501b 100644 --- a/Source/CDR.DataHolder.Domain/CDR.DataHolder.Domain.csproj +++ b/Source/CDR.DataHolder.Domain/CDR.DataHolder.Domain.csproj @@ -2,9 +2,9 @@ net6.0 - 1.0.1 - 1.0.1 - 1.0.1 + 1.1.0 + 1.1.0 + 1.1.0 diff --git a/Source/CDR.DataHolder.IdentityServer.UnitTests/CDR.DataHolder.IdentityServer.UnitTests.csproj b/Source/CDR.DataHolder.IdentityServer.UnitTests/CDR.DataHolder.IdentityServer.UnitTests.csproj index 933dbf3..174b68e 100644 --- a/Source/CDR.DataHolder.IdentityServer.UnitTests/CDR.DataHolder.IdentityServer.UnitTests.csproj +++ b/Source/CDR.DataHolder.IdentityServer.UnitTests/CDR.DataHolder.IdentityServer.UnitTests.csproj @@ -3,9 +3,9 @@ net6.0 false - 1.0.1 - 1.0.1 - 1.0.1 + 1.1.0 + 1.1.0 + 1.1.0 diff --git a/Source/CDR.DataHolder.IdentityServer/CDR.DataHolder.IdentityServer.csproj b/Source/CDR.DataHolder.IdentityServer/CDR.DataHolder.IdentityServer.csproj index 0fef801..65f2e7e 100644 --- a/Source/CDR.DataHolder.IdentityServer/CDR.DataHolder.IdentityServer.csproj +++ b/Source/CDR.DataHolder.IdentityServer/CDR.DataHolder.IdentityServer.csproj @@ -3,9 +3,9 @@ net6.0 win-x64;linux-x64 - 1.0.1 - 1.0.1 - 1.0.1 + 1.1.0 + 1.1.0 + 1.1.0 @@ -88,7 +88,7 @@ - + @@ -100,6 +100,7 @@ + diff --git a/Source/CDR.DataHolder.IdentityServer/Program.cs b/Source/CDR.DataHolder.IdentityServer/Program.cs index 3395610..08f1b34 100644 --- a/Source/CDR.DataHolder.IdentityServer/Program.cs +++ b/Source/CDR.DataHolder.IdentityServer/Program.cs @@ -38,6 +38,8 @@ public static int Main(string[] args) .Enrich.WithProperty("Environment", Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")) .CreateLogger(); + Serilog.Debugging.SelfLog.Enable(msg => Log.Logger.Debug(msg)); + try { Log.Information("Starting web host", args); diff --git a/Source/CDR.DataHolder.IdentityServer/Startup.cs b/Source/CDR.DataHolder.IdentityServer/Startup.cs index 04d9802..bfc0267 100644 --- a/Source/CDR.DataHolder.IdentityServer/Startup.cs +++ b/Source/CDR.DataHolder.IdentityServer/Startup.cs @@ -2,7 +2,9 @@ using CDR.DataHolder.API.Infrastructure.Authorization; using CDR.DataHolder.API.Infrastructure.Filters; using CDR.DataHolder.API.Infrastructure.IdPermanence; +using CDR.DataHolder.API.Infrastructure.Middleware; using CDR.DataHolder.API.Infrastructure.Models; +using CDR.DataHolder.API.Logger; using CDR.DataHolder.Domain.Repositories; using CDR.DataHolder.IdentityServer.ClientAuthentication; using CDR.DataHolder.IdentityServer.Configuration; @@ -240,6 +242,12 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); + if (_configuration.GetSection("SerilogRequestResponseLogger") != null) + { + Log.Logger.Information("Adding request response logging middleware"); + services.AddRequestResponseLogging(); + } + } private bool UseDistributedCache() @@ -321,6 +329,8 @@ private static void AddAuthenticationAuthorization(IServiceCollection services, public static void Configure(IApplicationBuilder app, IWebHostEnvironment env, IConfiguration configuration, ILogger logger) { app.UseSerilogRequestLogging(); + app.UseMiddleware(); + var basePath = configuration.GetValue(Constants.ConfigurationKeys.BasePath, ""); if (!string.IsNullOrEmpty(basePath)) @@ -345,6 +355,7 @@ public static void Configure(IApplicationBuilder app, IWebHostEnvironment env, I // ExceptionHandlingMiddleware must be first in the line, so it will catch all unhandled exceptions. app.UseMiddleware(); app.UseMiddleware(); + // Allow sensitive data to be logged in dev environment only IdentityModelEventSource.ShowPII = env.IsDevelopment(); @@ -429,6 +440,7 @@ private static DiscoveryOptions ConfigureDiscoveryOptions(IConfiguration configu { CdsConstants.Discovery.TokenEndpointAuthenticationMethodsSupported, new string[] { CdsConstants.EndpointAuthenticationMethods.PrivateKeyJwt } }, { CdsConstants.Discovery.TlsClientCertificateBoundAccessTokens, true }, { CdsConstants.Discovery.ClaimsParameterSupported, true }, + { CdsConstants.Discovery.ResponseModesSupported , new string[] { CdsConstants.ResponseModes.FormPost, CdsConstants.ResponseModes.Fragment } }, }; foreach (var entry in extended) diff --git a/Source/CDR.DataHolder.IdentityServer/appsettings.Container.json b/Source/CDR.DataHolder.IdentityServer/appsettings.Container.json index 5f90461..5cf5da5 100644 --- a/Source/CDR.DataHolder.IdentityServer/appsettings.Container.json +++ b/Source/CDR.DataHolder.IdentityServer/appsettings.Container.json @@ -3,7 +3,8 @@ "DataHolder_Bank_DB": "Server=host.docker.internal;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", "DataHolder_Bank_Logging_DB": "Server=host.docker.internal;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", "DataHolder_Bank_IDP_DB": "Server=host.docker.internal;Database=cdr-idsvr;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", - "DataHolder_Bank_IDP_Migrations_DB": "Server=host.docker.internal;Database=cdr-idsvr;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True" + "DataHolder_Bank_IDP_Migrations_DB": "Server=host.docker.internal;Database=cdr-idsvr;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", + "DataHolder_Bank_RequestResponse_Logging_DB": "Server=host.docker.internal;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True" }, "Serilog": { "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.MSSqlServer" ], @@ -81,6 +82,126 @@ } ] }, + "SerilogRequestResponseLogger": { + "Using": [ "Serilog.Sinks.MSSqlServer" ], + "MinimumLevel": "Debug", + "IPAddressHeaderKey": "X-Forwarded-For", + "HostNameHeaderKey": "X-Forwarded-Host", + "WriteTo": [ + { + "Name": "MSSqlServer", + "Args": { + "connectionString": "DataHolder_Bank_RequestResponse_Logging_DB", + "sinkOptionsSection": { + "tableName": "LogEvents-RequestResponse", + "autoCreateSqlTable": true + }, + "restrictedToMinimumLevel": "Debug", + "batchPostingLimit": 1000, + "period": "0.00:00:10", + "columnOptionsSection": { + "disableTriggers": true, + "clusteredColumnstoreIndex": false, + "primaryKeyColumnName": "Id", + "removeStandardColumns": [ "MessageTemplate", "Properties" ], + "additionalColumns": [ + { + "ColumnName": "SourceContext", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 100 + }, + { + "ColumnName": "ClientId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "SoftwareId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "FapiInteractionId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "RequestMethod", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "RequestBody", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + }, + { + "ColumnName": "RequestHeaders", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + }, + { + "ColumnName": "RequestPath", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 2000 + }, + { + "ColumnName": "RequestQueryString", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "StatusCode", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "ElapsedTime", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "RequestHost", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "RequestIpAddress", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ResponseHeaders", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "ResponseBody", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + } + + ] + } + } + } + ] + }, "IssuerUri": "https://host.docker.internal:8001", "JwksUri": "https://host.docker.internal:8001/.well-known/openid-configuration/jwks", "AuthorizeUri": "https://host.docker.internal:8001/connect/authorize", diff --git a/Source/CDR.DataHolder.IdentityServer/appsettings.Development.json b/Source/CDR.DataHolder.IdentityServer/appsettings.Development.json index 143705c..fb851f6 100644 --- a/Source/CDR.DataHolder.IdentityServer/appsettings.Development.json +++ b/Source/CDR.DataHolder.IdentityServer/appsettings.Development.json @@ -4,7 +4,8 @@ "DataHolder_Bank_DB": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-mdh;Integrated Security=true", "DataHolder_Bank_Logging_DB": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-mdh;Integrated Security=true", "DataHolder_Bank_IDP_DB": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-idsvr;Integrated Security=true", - "DataHolder_Bank_IDP_Migrations_DB": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-idsvr;Integrated Security=true" + "DataHolder_Bank_IDP_Migrations_DB": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-idsvr;Integrated Security=true", + "DataHolder_Bank_RequestResponse_Logging_DB": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-mdh;Integrated Security=true" }, "Serilog": { "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.MSSqlServer" ], @@ -13,16 +14,16 @@ { "Name": "Console", "Args": { - "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" + "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} {Level} [{client_id}] [{SourceContext}] {Message}{NewLine}{Exception}" } }, { "Name": "File", "Args": { "path": "C:\\CDR\\Logs\\cdr-mdh-idsvr.log", - "outputTemplate": "{Timestamp:dd/MM/yyyy HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" + "outputTemplate": "{Timestamp:dd/MM/yyyy HH:mm:ss.fff zzz} {Level} [{client_id}] [{SourceContext}] {Message}{NewLine}{Exception}" } - }, + }, { "Name": "MSSqlServer", "Args": { @@ -75,7 +76,128 @@ "DataType": "nvarchar", "AllowNull": true, "DataLength": 100 + } + + ] + } + } + } + ] + }, + "SerilogRequestResponseLogger": { + "Using": [ "Serilog.Sinks.MSSqlServer" ], + "MinimumLevel": "Debug", + "IPAddressHeaderKey": "X-Forwarded-For", + "HostNameHeaderKey": "X-Forwarded-Host", + "WriteTo": [ + { + "Name": "MSSqlServer", + "Args": { + "connectionString": "DataHolder_Bank_RequestResponse_Logging_DB", + "sinkOptionsSection": { + "tableName": "LogEvents-RequestResponse", + "autoCreateSqlTable": true + }, + "restrictedToMinimumLevel": "Debug", + "batchPostingLimit": 1000, + "period": "0.00:00:10", + "columnOptionsSection": { + "disableTriggers": true, + "clusteredColumnstoreIndex": false, + "primaryKeyColumnName": "Id", + "removeStandardColumns": [ "MessageTemplate", "Properties" ], + "additionalColumns": [ + { + "ColumnName": "SourceContext", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 100 + }, + { + "ColumnName": "ClientId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "SoftwareId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "FapiInteractionId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "RequestMethod", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "RequestBody", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + }, + { + "ColumnName": "RequestHeaders", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + }, + { + "ColumnName": "RequestPath", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 2000 + }, + { + "ColumnName": "RequestQueryString", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "StatusCode", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "ElapsedTime", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "RequestHost", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "RequestIpAddress", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ResponseHeaders", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "ResponseBody", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 } + ] } } diff --git a/Source/CDR.DataHolder.IdentityServer/appsettings.Release.json b/Source/CDR.DataHolder.IdentityServer/appsettings.Release.json index 74b6cff..a865c2b 100644 --- a/Source/CDR.DataHolder.IdentityServer/appsettings.Release.json +++ b/Source/CDR.DataHolder.IdentityServer/appsettings.Release.json @@ -1,101 +1,222 @@ { - "ConnectionStrings": { - "DataHolder_Bank_DB": "Server=mssql;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", - "DataHolder_Bank_Logging_DB": "Server=mssql;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", - "DataHolder_Bank_IDP_DB": "Server=mssql;Database=cdr-idsvr;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", - "DataHolder_Bank_IDP_Migrations_DB": "Server=mssql;Database=cdr-idsvr;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True" - }, - "Serilog": { - "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.MSSqlServer" ], - "MinimumLevel": "Debug", - "WriteTo": [ - { - "Name": "Console", - "Args": { - "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" - } - }, - { - "Name": "File", - "Args": { - "path": "/tmp/cdr-mdh-idsvr.log", - "outputTemplate": "{Timestamp:dd/MM/yyyy HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" - } - }, - { - "Name": "MSSqlServer", - "Args": { - "connectionString": "DataHolder_Bank_Logging_DB", - "sinkOptionsSection": { - "tableName": "LogEvents-IdentityServer", - "autoCreateSqlTable": true - }, - "restrictedToMinimumLevel": "Verbose", - "batchPostingLimit": 1000, - "period": "0.00:00:10", - "columnOptionsSection": { - "disableTriggers": true, - "clusteredColumnstoreIndex": false, - "primaryKeyColumnName": "Id", - "removeStandardColumns": [ "MessageTemplate", "Properties" ], - "additionalColumns": [ - { - "ColumnName": "Environment", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ProcessId", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ProcessName", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ThreadId", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "MethodName", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "SourceContext", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 100 - } - ] - } - } - } - ] - }, - "AccessTokenLifetimeSeconds": 3600, - "IssuerUri": "https://mock-data-holder:8001", - "JwksUri": "https://mock-data-holder:8001/.well-known/openid-configuration/jwks", - "AuthorizeUri": "https://mock-data-holder:8001/connect/authorize", - "TokenUri": "https://mock-data-holder:8002/connect/token", - "IntrospectionUri": "https://mock-data-holder:8002/connect/introspect", - "UserinfoUri": "https://mock-data-holder:8002/connect/userinfo", - "RegisterUri": "https://mock-data-holder:8002/connect/register", - "ParUri": "https://mock-data-holder:8002/connect/par", - "RevocationUri": "https://mock-data-holder:8002/connect/revocation", - "ArrangementRevocationUri": "https://mock-data-holder:8002/connect/arrangements/revoke", - "Register": { - "SsaJwksUri": "https://mock-register:7000/cdr-register/v1/jwks" - }, - "Registration": { - "AudienceUri": "https://mock-data-holder:8001" - } + "ConnectionStrings": { + "DataHolder_Bank_DB": "Server=mssql;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", + "DataHolder_Bank_Logging_DB": "Server=mssql;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", + "DataHolder_Bank_IDP_DB": "Server=mssql;Database=cdr-idsvr;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", + "DataHolder_Bank_IDP_Migrations_DB": "Server=mssql;Database=cdr-idsvr;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", + "DataHolder_Bank_RequestResponse_Logging_DB": "Server=mssql;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True" + }, + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.MSSqlServer" ], + "MinimumLevel": "Debug", + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "/tmp/cdr-mdh-idsvr.log", + "outputTemplate": "{Timestamp:dd/MM/yyyy HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" + } + }, + { + "Name": "MSSqlServer", + "Args": { + "connectionString": "DataHolder_Bank_Logging_DB", + "sinkOptionsSection": { + "tableName": "LogEvents-IdentityServer", + "autoCreateSqlTable": true + }, + "restrictedToMinimumLevel": "Verbose", + "batchPostingLimit": 1000, + "period": "0.00:00:10", + "columnOptionsSection": { + "disableTriggers": true, + "clusteredColumnstoreIndex": false, + "primaryKeyColumnName": "Id", + "removeStandardColumns": [ "MessageTemplate", "Properties" ], + "additionalColumns": [ + { + "ColumnName": "Environment", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ProcessId", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ProcessName", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ThreadId", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "MethodName", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "SourceContext", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 100 + } + ] + } + } + } + ] + }, + "SerilogRequestResponseLogger": { + "Using": [ "Serilog.Sinks.MSSqlServer" ], + "MinimumLevel": "Debug", + "IPAddressHeaderKey": "X-Forwarded-For", + "HostNameHeaderKey": "X-Forwarded-Host", + "WriteTo": [ + { + "Name": "MSSqlServer", + "Args": { + "connectionString": "DataHolder_Bank_RequestResponse_Logging_DB", + "sinkOptionsSection": { + "tableName": "LogEvents-RequestResponse", + "autoCreateSqlTable": true + }, + "restrictedToMinimumLevel": "Debug", + "batchPostingLimit": 1000, + "period": "0.00:00:10", + "columnOptionsSection": { + "disableTriggers": true, + "clusteredColumnstoreIndex": false, + "primaryKeyColumnName": "Id", + "removeStandardColumns": [ "MessageTemplate", "Properties" ], + "additionalColumns": [ + { + "ColumnName": "SourceContext", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 100 + }, + { + "ColumnName": "ClientId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "SoftwareId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "FapiInteractionId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "RequestMethod", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "RequestBody", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + }, + { + "ColumnName": "RequestHeaders", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + }, + { + "ColumnName": "RequestPath", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 2000 + }, + { + "ColumnName": "RequestQueryString", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "StatusCode", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "ElapsedTime", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "RequestHost", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "RequestIpAddress", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ResponseHeaders", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "ResponseBody", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + } + + ] + } + } + } + ] + }, + "AccessTokenLifetimeSeconds": 3600, + "IssuerUri": "https://mock-data-holder:8001", + "JwksUri": "https://mock-data-holder:8001/.well-known/openid-configuration/jwks", + "AuthorizeUri": "https://mock-data-holder:8001/connect/authorize", + "TokenUri": "https://mock-data-holder:8002/connect/token", + "IntrospectionUri": "https://mock-data-holder:8002/connect/introspect", + "UserinfoUri": "https://mock-data-holder:8002/connect/userinfo", + "RegisterUri": "https://mock-data-holder:8002/connect/register", + "ParUri": "https://mock-data-holder:8002/connect/par", + "RevocationUri": "https://mock-data-holder:8002/connect/revocation", + "ArrangementRevocationUri": "https://mock-data-holder:8002/connect/arrangements/revoke", + "Register": { + "SsaJwksUri": "https://mock-register:7000/cdr-register/v1/jwks" + }, + "Registration": { + "AudienceUri": "https://mock-data-holder:8001" + } } \ No newline at end of file diff --git a/Source/CDR.DataHolder.IdentityServer/appsettings.json b/Source/CDR.DataHolder.IdentityServer/appsettings.json index 84c1b01..1d55a71 100644 --- a/Source/CDR.DataHolder.IdentityServer/appsettings.json +++ b/Source/CDR.DataHolder.IdentityServer/appsettings.json @@ -44,4 +44,4 @@ "Url": "", "Password": "" } -} \ No newline at end of file +} diff --git a/Source/CDR.DataHolder.IntegrationTests/CDR.DataHolder.IntegrationTests.csproj b/Source/CDR.DataHolder.IntegrationTests/CDR.DataHolder.IntegrationTests.csproj index ff434d5..8c88424 100644 --- a/Source/CDR.DataHolder.IntegrationTests/CDR.DataHolder.IntegrationTests.csproj +++ b/Source/CDR.DataHolder.IntegrationTests/CDR.DataHolder.IntegrationTests.csproj @@ -2,9 +2,9 @@ net6.0 false - 1.0.1 - 1.0.1 - 1.0.1 + 1.1.0 + 1.1.0 + 1.1.0 diff --git a/Source/CDR.DataHolder.IntegrationTests/US12962_MDH_InfosecProfileAPI_OIDCConfiguration.cs b/Source/CDR.DataHolder.IntegrationTests/US12962_MDH_InfosecProfileAPI_OIDCConfiguration.cs index 2e52a2b..16189b2 100644 --- a/Source/CDR.DataHolder.IntegrationTests/US12962_MDH_InfosecProfileAPI_OIDCConfiguration.cs +++ b/Source/CDR.DataHolder.IntegrationTests/US12962_MDH_InfosecProfileAPI_OIDCConfiguration.cs @@ -85,6 +85,7 @@ public async Task AC01_Get_ShouldRespondWith_200OK_OIDC() actual.token_endpoint_auth_methods_supported.Should().BeEquivalentTo(new[] { "private_key_jwt" }); actual.subject_types_supported.Should().BeEquivalentTo(new[] { "pairwise" }); actual.grant_types_supported.Should().BeEquivalentTo(new[] { "authorization_code", "client_credentials", "refresh_token" }); + actual.response_modes_supported.Should().BeEquivalentTo(new[] { "form_post", "fragment" }); } } } diff --git a/Source/CDR.DataHolder.IntegrationTests/integration.Release.runsettings b/Source/CDR.DataHolder.IntegrationTests/integration.Release.runsettings new file mode 100644 index 0000000..d6714e4 --- /dev/null +++ b/Source/CDR.DataHolder.IntegrationTests/integration.Release.runsettings @@ -0,0 +1,10 @@ + + + + + + + Release + + + \ No newline at end of file diff --git a/Source/CDR.DataHolder.Manage.API/CDR.DataHolder.Manage.API.csproj b/Source/CDR.DataHolder.Manage.API/CDR.DataHolder.Manage.API.csproj index 6b36d0c..eeec4d7 100644 --- a/Source/CDR.DataHolder.Manage.API/CDR.DataHolder.Manage.API.csproj +++ b/Source/CDR.DataHolder.Manage.API/CDR.DataHolder.Manage.API.csproj @@ -3,9 +3,9 @@ net6.0 win-x64;linux-x64 - 1.0.1 - 1.0.1 - 1.0.1 + 1.1.0 + 1.1.0 + 1.1.0 diff --git a/Source/CDR.DataHolder.Public.API/CDR.DataHolder.Public.API.csproj b/Source/CDR.DataHolder.Public.API/CDR.DataHolder.Public.API.csproj index 5a1fabc..9d57399 100644 --- a/Source/CDR.DataHolder.Public.API/CDR.DataHolder.Public.API.csproj +++ b/Source/CDR.DataHolder.Public.API/CDR.DataHolder.Public.API.csproj @@ -3,9 +3,9 @@ net6.0 win-x64;linux-x64 - 1.0.1 - 1.0.1 - 1.0.1 + 1.1.0 + 1.1.0 + 1.1.0 diff --git a/Source/CDR.DataHolder.Repository/CDR.DataHolder.Repository.csproj b/Source/CDR.DataHolder.Repository/CDR.DataHolder.Repository.csproj index ade85ea..4145134 100644 --- a/Source/CDR.DataHolder.Repository/CDR.DataHolder.Repository.csproj +++ b/Source/CDR.DataHolder.Repository/CDR.DataHolder.Repository.csproj @@ -2,9 +2,9 @@ net6.0 - 1.0.1 - 1.0.1 - 1.0.1 + 1.1.0 + 1.1.0 + 1.1.0 diff --git a/Source/CDR.DataHolder.Resource.API.UnitTests/CDR.DataHolder.Resource.API.UnitTests.csproj b/Source/CDR.DataHolder.Resource.API.UnitTests/CDR.DataHolder.Resource.API.UnitTests.csproj index 1e5b62a..746b6fa 100644 --- a/Source/CDR.DataHolder.Resource.API.UnitTests/CDR.DataHolder.Resource.API.UnitTests.csproj +++ b/Source/CDR.DataHolder.Resource.API.UnitTests/CDR.DataHolder.Resource.API.UnitTests.csproj @@ -5,11 +5,11 @@ false - 1.0.1 + 1.1.0 - 1.0.1 + 1.1.0 - 1.0.1 + 1.1.0 diff --git a/Source/CDR.DataHolder.Resource.API/CDR.DataHolder.Resource.API.csproj b/Source/CDR.DataHolder.Resource.API/CDR.DataHolder.Resource.API.csproj index c873c8a..20885f9 100644 --- a/Source/CDR.DataHolder.Resource.API/CDR.DataHolder.Resource.API.csproj +++ b/Source/CDR.DataHolder.Resource.API/CDR.DataHolder.Resource.API.csproj @@ -3,9 +3,9 @@ net6.0 win-x64;linux-x64 - 1.0.1 - 1.0.1 - 1.0.1 + 1.1.0 + 1.1.0 + 1.1.0 @@ -36,15 +36,16 @@ - + - + + diff --git a/Source/CDR.DataHolder.Resource.API/Controllers/ResourceController.cs b/Source/CDR.DataHolder.Resource.API/Controllers/ResourceController.cs index d16e573..16cc039 100644 --- a/Source/CDR.DataHolder.Resource.API/Controllers/ResourceController.cs +++ b/Source/CDR.DataHolder.Resource.API/Controllers/ResourceController.cs @@ -109,6 +109,9 @@ public async Task GetAccounts( // Each customer id is different for each ADR based on PPID. // Therefore we need to look up the CustomerClient table to find the actual customer id. // This can be done once we have a client id (Registration) and a valid access token. + + //LogContext.PushProperty("client_id", ((ClaimsIdentity)this.User.Identity).FindFirst("client_id")); + var customerId = GetCustomerId(this.User); if (customerId == Guid.Empty) { diff --git a/Source/CDR.DataHolder.Resource.API/Program.cs b/Source/CDR.DataHolder.Resource.API/Program.cs index ce1d95e..87edba1 100644 --- a/Source/CDR.DataHolder.Resource.API/Program.cs +++ b/Source/CDR.DataHolder.Resource.API/Program.cs @@ -37,6 +37,8 @@ public static int Main(string[] args) .Enrich.WithProperty("Environment", Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")) .CreateLogger(); + Serilog.Debugging.SelfLog.Enable(msg => Log.Logger.Debug(msg)); + try { Log.Information("Starting web host"); diff --git a/Source/CDR.DataHolder.Resource.API/Startup.cs b/Source/CDR.DataHolder.Resource.API/Startup.cs index cde895a..5ddbf0a 100644 --- a/Source/CDR.DataHolder.Resource.API/Startup.cs +++ b/Source/CDR.DataHolder.Resource.API/Startup.cs @@ -4,6 +4,7 @@ using CDR.DataHolder.API.Infrastructure.IdPermanence; using CDR.DataHolder.API.Infrastructure.Middleware; using CDR.DataHolder.API.Infrastructure.Models; +using CDR.DataHolder.API.Logger; using CDR.DataHolder.Domain.Repositories; using CDR.DataHolder.Repository; using CDR.DataHolder.Repository.Infrastructure; @@ -87,6 +88,12 @@ public void ConfigureServices(IServiceCollection services) services.AddAutoMapper(typeof(Startup), typeof(DataHolderDatabaseContext)); services.AddScoped(); + + if (Configuration.GetSection("SerilogRequestResponseLogger") != null) + { + Log.Logger.Information("Adding request response logging middleware"); + services.AddRequestResponseLogging(); + } } private static void AddAuthenticationAuthorization(IServiceCollection services, IConfiguration configuration) @@ -174,6 +181,7 @@ private static void AddAuthenticationAuthorization(IServiceCollection services, } }); }); + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -186,8 +194,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger< app.UseSerilogRequestLogging(); + app.UseMiddleware(); // ExceptionHandlingMiddleware must be first in the line, so it will catch all unhandled exceptions. app.UseMiddleware(); + + app.UseSwagger(); app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Mock Data Holder Discovery API v1")); @@ -199,6 +210,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger< app.UseAuthentication(); app.UseAuthorization(); + + // Add custom middleware app.UseInteractionId(); diff --git a/Source/CDR.DataHolder.Resource.API/appsettings.Container.json b/Source/CDR.DataHolder.Resource.API/appsettings.Container.json index a6c1aa5..6fa46b3 100644 --- a/Source/CDR.DataHolder.Resource.API/appsettings.Container.json +++ b/Source/CDR.DataHolder.Resource.API/appsettings.Container.json @@ -1,86 +1,207 @@ { - "ConnectionStrings": { - "DataHolder_Bank_DB": "Server=host.docker.internal;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", - "DataHolder_Bank_Logging_DB": "Server=host.docker.internal;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True" - }, - "Serilog": { - "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.MSSqlServer" ], - "MinimumLevel": "Debug", - "WriteTo": [ - { - "Name": "Console", - "Args": { - "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" - } - }, - { - "Name": "File", - "Args": { - "path": "/tmp/cdr-mdh-resource-api.log", - "outputTemplate": "{Timestamp:dd/MM/yyyy HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" - } - }, - { - "Name": "MSSqlServer", - "Args": { - "connectionString": "DataHolder_Bank_Logging_DB", - "sinkOptionsSection": { - "tableName": "LogEvents-Resource-API", - "autoCreateSqlTable": true - }, - "restrictedToMinimumLevel": "Verbose", - "batchPostingLimit": 1000, - "period": "0.00:00:10", - "columnOptionsSection": { - "disableTriggers": true, - "clusteredColumnstoreIndex": false, - "primaryKeyColumnName": "Id", - "removeStandardColumns": [ "MessageTemplate", "Properties" ], - "additionalColumns": [ - { - "ColumnName": "Environment", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ProcessId", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ProcessName", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ThreadId", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "MethodName", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "SourceContext", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 100 - } - ] - } - } - } - ] - }, - "AccessTokenIntrospectionEndpoint": "https://host.docker.internal:8001/connect/introspect-internal", - "IdentityServerIssuerUri": "https://host.docker.internal:8001", - "IdentityServerUrl": "https://host.docker.internal:8001", - "ResourceBaseUri": "https://host.docker.internal:8002" + "ConnectionStrings": { + "DataHolder_Bank_DB": "Server=host.docker.internal;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", + "DataHolder_Bank_Logging_DB": "Server=host.docker.internal;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", + "DataHolder_Bank_RequestResponse_Logging_DB": "Server=host.docker.internal;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True" + }, + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.MSSqlServer" ], + "MinimumLevel": "Debug", + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "/tmp/cdr-mdh-resource-api.log", + "outputTemplate": "{Timestamp:dd/MM/yyyy HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" + } + }, + { + "Name": "MSSqlServer", + "Args": { + "connectionString": "DataHolder_Bank_Logging_DB", + "sinkOptionsSection": { + "tableName": "LogEvents-Resource-API", + "autoCreateSqlTable": true + }, + "restrictedToMinimumLevel": "Verbose", + "batchPostingLimit": 1000, + "period": "0.00:00:10", + "columnOptionsSection": { + "disableTriggers": true, + "clusteredColumnstoreIndex": false, + "primaryKeyColumnName": "Id", + "removeStandardColumns": [ "MessageTemplate", "Properties" ], + "additionalColumns": [ + { + "ColumnName": "Environment", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ProcessId", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ProcessName", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ThreadId", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "MethodName", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "SourceContext", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 100 + } + ] + } + } + } + ] + }, + "SerilogRequestResponseLogger": { + "Using": [ "Serilog.Sinks.MSSqlServer" ], + "MinimumLevel": "Debug", + "IPAddressHeaderKey": "X-Forwarded-For", + "HostNameHeaderKey": "X-Forwarded-Host", + "WriteTo": [ + { + "Name": "MSSqlServer", + "Args": { + "connectionString": "DataHolder_Bank_RequestResponse_Logging_DB", + "sinkOptionsSection": { + "tableName": "LogEvents-RequestResponse", + "autoCreateSqlTable": true + }, + "restrictedToMinimumLevel": "Debug", + "batchPostingLimit": 1000, + "period": "0.00:00:10", + "columnOptionsSection": { + "disableTriggers": true, + "clusteredColumnstoreIndex": false, + "primaryKeyColumnName": "Id", + "removeStandardColumns": [ "MessageTemplate", "Properties" ], + "additionalColumns": [ + { + "ColumnName": "SourceContext", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 100 + }, + { + "ColumnName": "ClientId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50, + "NonClusteredIndex": true + }, + { + "ColumnName": "SoftwareId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "FapiInteractionId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "RequestMethod", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "RequestBody", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + }, + { + "ColumnName": "RequestHeaders", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + }, + { + "ColumnName": "RequestPath", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 2000 + }, + { + "ColumnName": "RequestQueryString", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "StatusCode", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "ElapsedTime", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "RequestHost", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "RequestIpAddress", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ResponseHeaders", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "ResponseBody", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + } + ] + } + } + } + ] + }, + "AccessTokenIntrospectionEndpoint": "https://host.docker.internal:8001/connect/introspect-internal", + "IdentityServerIssuerUri": "https://host.docker.internal:8001", + "IdentityServerUrl": "https://host.docker.internal:8001", + "ResourceBaseUri": "https://host.docker.internal:8002" } \ No newline at end of file diff --git a/Source/CDR.DataHolder.Resource.API/appsettings.Development.json b/Source/CDR.DataHolder.Resource.API/appsettings.Development.json index 67f3729..c16b025 100644 --- a/Source/CDR.DataHolder.Resource.API/appsettings.Development.json +++ b/Source/CDR.DataHolder.Resource.API/appsettings.Development.json @@ -1,86 +1,214 @@ { - "ConnectionStrings": { - "DataHolder_Bank_DB": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-mdh;Integrated Security=true", - "DataHolder_Bank_Logging_DB": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-mdh;Integrated Security=true" - }, - "Serilog": { - "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.MSSqlServer" ], - "MinimumLevel": "Debug", - "WriteTo": [ - { - "Name": "Console", - "Args": { - "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" - } - }, - { - "Name": "File", - "Args": { - "path": "C:\\CDR\\Logs\\cdr-mdh-resource-api.log", - "outputTemplate": "{Timestamp:dd/MM/yyyy HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" - } - }, - { - "Name": "MSSqlServer", - "Args": { - "connectionString": "DataHolder_Bank_Logging_DB", - "sinkOptionsSection": { - "tableName": "LogEvents-Resource-API", - "autoCreateSqlTable": true - }, - "restrictedToMinimumLevel": "Verbose", - "batchPostingLimit": 1000, - "period": "0.00:00:10", - "columnOptionsSection": { - "disableTriggers": true, - "clusteredColumnstoreIndex": false, - "primaryKeyColumnName": "Id", - "removeStandardColumns": [ "MessageTemplate", "Properties" ], - "additionalColumns": [ - { - "ColumnName": "Environment", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ProcessId", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ProcessName", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ThreadId", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "MethodName", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "SourceContext", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 100 - } - ] - } - } - } - ] - }, - "AccessTokenIntrospectionEndpoint": "https://localhost:8001/connect/introspect-internal", - "IdentityServerIssuerUri": "https://localhost:8001", - "IdentityServerUrl": "https://localhost:8001", - "ResourceBaseUri": "https://localhost:8002" + "ConnectionStrings": { + "DataHolder_Bank_DB": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-mdh;Integrated Security=true", + "DataHolder_Bank_Logging_DB": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-mdh;Integrated Security=true", + "DataHolder_Bank_RequestResponse_Logging_DB": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-mdh;Integrated Security=true" + }, + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.MSSqlServer" ], + "MinimumLevel": "Debug", + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} {Level} {client_id} [{SourceContext}] {Message}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "C:\\CDR\\Logs\\cdr-mdh-resource-api.log", + "outputTemplate": "{Timestamp:dd/MM/yyyy HH:mm:ss.fff zzz} {Level} {client_id} [{SourceContext}] {Message}{NewLine}{Exception}" + } + }, + { + "Name": "MSSqlServer", + "Args": { + "connectionString": "DataHolder_Bank_Logging_DB", + "sinkOptionsSection": { + "tableName": "LogEvents-Resource-API", + "autoCreateSqlTable": true + }, + "restrictedToMinimumLevel": "Verbose", + "batchPostingLimit": 1000, + "period": "0.00:00:10", + "columnOptionsSection": { + "disableTriggers": true, + "clusteredColumnstoreIndex": false, + "primaryKeyColumnName": "Id", + "removeStandardColumns": [ "MessageTemplate", "Properties" ], + "additionalColumns": [ + { + "ColumnName": "Environment", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ProcessId", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ProcessName", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ThreadId", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "MethodName", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "SourceContext", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 100 + }, + { + "ColumnName": "client_id", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50, + "NonClusteredIndex": true + } + ] + } + } + } + ] + }, + "SerilogRequestResponseLogger": { + "Using": [ "Serilog.Sinks.MSSqlServer" ], + "MinimumLevel": "Debug", + "IPAddressHeaderKey": "X-Forwarded-For", + "HostNameHeaderKey": "X-Forwarded-Host", + "WriteTo": [ + { + "Name": "MSSqlServer", + "Args": { + "connectionString": "DataHolder_Bank_RequestResponse_Logging_DB", + "sinkOptionsSection": { + "tableName": "LogEvents-RequestResponse", + "autoCreateSqlTable": true + }, + "restrictedToMinimumLevel": "Debug", + "batchPostingLimit": 1000, + "period": "0.00:00:10", + "columnOptionsSection": { + "disableTriggers": true, + "clusteredColumnstoreIndex": false, + "primaryKeyColumnName": "Id", + "removeStandardColumns": [ "MessageTemplate", "Properties" ], + "additionalColumns": [ + { + "ColumnName": "SourceContext", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 100 + }, + { + "ColumnName": "ClientId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50, + "NonClusteredIndex": true + }, + { + "ColumnName": "SoftwareId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "FapiInteractionId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "RequestMethod", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "RequestBody", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + }, + { + "ColumnName": "RequestHeaders", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + }, + { + "ColumnName": "RequestPath", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 2000 + }, + { + "ColumnName": "RequestQueryString", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "StatusCode", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "ElapsedTime", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "RequestHost", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "RequestIpAddress", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ResponseHeaders", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "ResponseBody", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + } + ] + } + } + } + ] + }, + "AccessTokenIntrospectionEndpoint": "https://localhost:8001/connect/introspect-internal", + "IdentityServerIssuerUri": "https://localhost:8001", + "IdentityServerUrl": "https://localhost:8001", + "ResourceBaseUri": "https://localhost:8002" } \ No newline at end of file diff --git a/Source/CDR.DataHolder.Resource.API/appsettings.Release.json b/Source/CDR.DataHolder.Resource.API/appsettings.Release.json index 01095a6..e64022a 100644 --- a/Source/CDR.DataHolder.Resource.API/appsettings.Release.json +++ b/Source/CDR.DataHolder.Resource.API/appsettings.Release.json @@ -1,86 +1,207 @@ { - "ConnectionStrings": { - "DataHolder_Bank_DB": "Server=mssql;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", - "DataHolder_Bank_Logging_DB": "Server=mssql;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True" - }, - "Serilog": { - "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.MSSqlServer" ], - "MinimumLevel": "Debug", - "WriteTo": [ - { - "Name": "Console", - "Args": { - "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" - } - }, - { - "Name": "File", - "Args": { - "path": "/tmp/cdr-mdh-resource-api.log", - "outputTemplate": "{Timestamp:dd/MM/yyyy HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" - } - }, - { - "Name": "MSSqlServer", - "Args": { - "connectionString": "DataHolder_Bank_Logging_DB", - "sinkOptionsSection": { - "tableName": "LogEvents-Resource-API", - "autoCreateSqlTable": true - }, - "restrictedToMinimumLevel": "Verbose", - "batchPostingLimit": 1000, - "period": "0.00:00:10", - "columnOptionsSection": { - "disableTriggers": true, - "clusteredColumnstoreIndex": false, - "primaryKeyColumnName": "Id", - "removeStandardColumns": [ "MessageTemplate", "Properties" ], - "additionalColumns": [ - { - "ColumnName": "Environment", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ProcessId", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ProcessName", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ThreadId", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "MethodName", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "SourceContext", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 100 - } - ] - } - } - } - ] - }, - "AccessTokenIntrospectionEndpoint": "https://mock-data-holder:8001/connect/introspect-internal", - "IdentityServerIssuerUri": "https://mock-data-holder:8001", - "IdentityServerUrl": "https://mock-data-holder:8001", - "ResourceBaseUri": "https://mock-data-holder:8002" + "ConnectionStrings": { + "DataHolder_Bank_DB": "Server=mssql;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", + "DataHolder_Bank_Logging_DB": "Server=mssql;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", + "DataHolder_Bank_RequestResponse_Logging_DB": "Server=mssql;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True" + }, + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.MSSqlServer" ], + "MinimumLevel": "Debug", + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "/tmp/cdr-mdh-resource-api.log", + "outputTemplate": "{Timestamp:dd/MM/yyyy HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" + } + }, + { + "Name": "MSSqlServer", + "Args": { + "connectionString": "DataHolder_Bank_Logging_DB", + "sinkOptionsSection": { + "tableName": "LogEvents-Resource-API", + "autoCreateSqlTable": true + }, + "restrictedToMinimumLevel": "Verbose", + "batchPostingLimit": 1000, + "period": "0.00:00:10", + "columnOptionsSection": { + "disableTriggers": true, + "clusteredColumnstoreIndex": false, + "primaryKeyColumnName": "Id", + "removeStandardColumns": [ "MessageTemplate", "Properties" ], + "additionalColumns": [ + { + "ColumnName": "Environment", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ProcessId", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ProcessName", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ThreadId", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "MethodName", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "SourceContext", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 100 + } + ] + } + } + } + ] + }, + "SerilogRequestResponseLogger": { + "Using": [ "Serilog.Sinks.MSSqlServer" ], + "MinimumLevel": "Debug", + "IPAddressHeaderKey": "X-Forwarded-For", + "HostNameHeaderKey": "X-Forwarded-Host", + "WriteTo": [ + { + "Name": "MSSqlServer", + "Args": { + "connectionString": "DataHolder_Bank_RequestResponse_Logging_DB", + "sinkOptionsSection": { + "tableName": "LogEvents-RequestResponse", + "autoCreateSqlTable": true + }, + "restrictedToMinimumLevel": "Debug", + "batchPostingLimit": 1000, + "period": "0.00:00:10", + "columnOptionsSection": { + "disableTriggers": true, + "clusteredColumnstoreIndex": false, + "primaryKeyColumnName": "Id", + "removeStandardColumns": [ "MessageTemplate", "Properties" ], + "additionalColumns": [ + { + "ColumnName": "SourceContext", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 100 + }, + { + "ColumnName": "ClientId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50, + "NonClusteredIndex": true + }, + { + "ColumnName": "SoftwareId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "FapiInteractionId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "RequestMethod", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "RequestBody", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + }, + { + "ColumnName": "RequestHeaders", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + }, + { + "ColumnName": "RequestPath", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 2000 + }, + { + "ColumnName": "RequestQueryString", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "StatusCode", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "ElapsedTime", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "RequestHost", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "RequestIpAddress", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ResponseHeaders", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "ResponseBody", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + } + ] + } + } + } + ] + }, + "AccessTokenIntrospectionEndpoint": "https://mock-data-holder:8001/connect/introspect-internal", + "IdentityServerIssuerUri": "https://mock-data-holder:8001", + "IdentityServerUrl": "https://mock-data-holder:8001", + "ResourceBaseUri": "https://mock-data-holder:8002" } diff --git a/Source/CDR.GetDataRecipients.IntegrationTests/BaseTest.cs b/Source/CDR.GetDataRecipients.IntegrationTests/BaseTest.cs new file mode 100644 index 0000000..65f6cbf --- /dev/null +++ b/Source/CDR.GetDataRecipients.IntegrationTests/BaseTest.cs @@ -0,0 +1,63 @@ +#undef DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON + +#nullable enable + +using Microsoft.Extensions.Configuration; +using System; +using System.IO; +using System.Reflection; +using Xunit; +using Xunit.Sdk; + +using CDR.GetDataRecipients.IntegrationTests.Fixtures; +using System.Configuration; + +namespace CDR.GetDataRecipients.IntegrationTests +{ + class DisplayTestMethodNameAttribute : BeforeAfterTestAttribute + { + static int count = 0; + + public override void Before(MethodInfo methodUnderTest) + { + Console.WriteLine($"Test #{++count} - {methodUnderTest.DeclaringType?.Name}.{methodUnderTest.Name}"); + } + + public override void After(MethodInfo methodUnderTest) + { + } + } + + // Put all tests in same collection because we need them to run sequentially since some tests are mutating DB. + [Collection("IntegrationTests")] + [TestCaseOrderer("CDR.GetDataRecipients.IntegrationTests.XUnit.Orderers.AlphabeticalOrderer", "CDR.GetDataRecipients.IntegrationTests")] + [DisplayTestMethodName] + abstract public class BaseTest : IClassFixture + { + public static string AZUREFUNCTIONS_URL => Configuration["URL:AZUREFUNCTIONS"] + ?? throw new ConfigurationErrorsException($"{nameof(AZUREFUNCTIONS_URL)} - configuration setting not found"); + + static public string CONNECTIONSTRING_REGISTER_RW => + ConnectionStringCheck.Check(Configuration.GetConnectionString("Register_RW")); + static public string CONNECTIONSTRING_MDH_RW => + ConnectionStringCheck.Check(Configuration.GetConnectionString("DataHolder_RW")); + + static private IConfigurationRoot? configuration; + static public IConfigurationRoot Configuration + { + get + { + if (configuration == null) + { + configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", true) + .Build(); + } + + return configuration; + } + } + } +} \ No newline at end of file diff --git a/Source/CDR.GetDataRecipients.IntegrationTests/CDR.GetDataRecipients.IntegrationTests.csproj b/Source/CDR.GetDataRecipients.IntegrationTests/CDR.GetDataRecipients.IntegrationTests.csproj new file mode 100644 index 0000000..6b04795 --- /dev/null +++ b/Source/CDR.GetDataRecipients.IntegrationTests/CDR.GetDataRecipients.IntegrationTests.csproj @@ -0,0 +1,49 @@ + + + net6.0 + false + 1.1.0 + 1.1.0 + 1.1.0 + + + + Always + + + Always + + + Always + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + \ No newline at end of file diff --git a/Source/CDR.GetDataRecipients.IntegrationTests/ConnectionStringCheck.cs b/Source/CDR.GetDataRecipients.IntegrationTests/ConnectionStringCheck.cs new file mode 100644 index 0000000..66c9716 --- /dev/null +++ b/Source/CDR.GetDataRecipients.IntegrationTests/ConnectionStringCheck.cs @@ -0,0 +1,85 @@ +#nullable enable + +using System; +using Xunit; +using FluentAssertions.Execution; +using FluentAssertions; + +namespace CDR.GetDataRecipients.IntegrationTests +{ + static public class ConnectionStringCheck + { + internal const string PRODUCTION_SERVER = "sql-cdrsandbox-prod.database.windows.net"; + + // TODO - MJS - Whitelist would be better since if production database ever changes server this blacklist will fail unless someone remembers to update + static readonly string[] Blacklist = new string[] { + PRODUCTION_SERVER + }; + + static public string Check(string connectionString) + { + if (!String.IsNullOrEmpty(connectionString)) + { + // Reject if blacklisted string found in connectionString + foreach (string blacklisted in Blacklist) + { + if (connectionString.ToUpper().Trim().Contains(blacklisted.ToUpper().Trim())) + { + throw new Exception($"{blacklisted} is blacklisted. Cannot connect to this server"); // nb: don't show connectionString since it contains password + } + } + } + + return connectionString; + } + } + + public class ConnectionStringCheckUnitTests + { + const string PRODUCTION_SERVER_FOO = "foo" + ConnectionStringCheck.PRODUCTION_SERVER + "foo"; // blacklist is checking for substrings, so surround with "foo" to ensure we are testing this + + [Theory] + [InlineData(PRODUCTION_SERVER_FOO)] + [InlineData(PRODUCTION_SERVER_FOO, true)] + public void WhenOnBlackList_ShouldThrowException(string connectionString, bool? uppercase = false) + { + if (uppercase == true) + { + connectionString = connectionString.ToUpper(); + } + + using (new AssertionScope()) + { + // Act/Assert + Action act = () => ConnectionStringCheck.Check(connectionString); + using (new AssertionScope()) + { + act.Should().Throw(); + } + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("foo")] + [InlineData("sql-cdrsandbox-dev.database.windows.net")] + [InlineData("sql-cdrsandbox-test.database.windows.net")] + [InlineData("localhost")] + [InlineData("mssql")] + public void WhenNotOnBlackList_ShouldNotThrowException(string connectionString) + { + using (new AssertionScope()) + { + // Act/Assert + string? returnedConnectionString = null; + Action act = () => returnedConnectionString = ConnectionStringCheck.Check(connectionString); + using (new AssertionScope()) + { + act.Should().NotThrow(); + returnedConnectionString?.Should().Be(connectionString); + } + } + } + } +} \ No newline at end of file diff --git a/Source/CDR.GetDataRecipients.IntegrationTests/DatabaseSeeder.cs b/Source/CDR.GetDataRecipients.IntegrationTests/DatabaseSeeder.cs new file mode 100644 index 0000000..33f1a03 --- /dev/null +++ b/Source/CDR.GetDataRecipients.IntegrationTests/DatabaseSeeder.cs @@ -0,0 +1,315 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Data.SqlClient; +using Dapper; + +#nullable enable + +namespace CDR.GetDataRecipients.IntegrationTests +{ + static public class DatabaseSeeder + { + private enum IndustryType { Banking, Energy, Telecommunications } + private enum ParticipantType { DH, DR } + + private static int nextRegisterLegalEntityId = 0; + private static int nextRegisterParticipationId = 0; + private static int nextRegisterBrandId = 0; + private static int nextRegisterSoftwareProductId = 0; + + private static int nextDataHolderLegalEntityId = 0; + private static int nextDataHolderBrandId = 0; + private static int nextDataHolderSoftwareProductId = 0; + + static public async Task Execute( + int registerLegalEntityCount, int registerBrandCount, int registerSoftwareProductCount, + int dataHolderLegalEntityCount, int dataHolderBrandCount, int dataHolderSoftwareProductCount, + bool registerModified, // simulate change to register records + bool dataHolderModified // simulate change to dataholder records + ) + { + // Database is purged so, reset next ids so that ids are consistent across tests + nextRegisterLegalEntityId = 0; + nextRegisterParticipationId = 0; + nextRegisterBrandId = 0; + nextRegisterSoftwareProductId = 0; + nextDataHolderLegalEntityId = 0; + nextDataHolderBrandId = 0; + nextDataHolderSoftwareProductId = 0; + + // Seed Register + using var registerConnection = new SqlConnection(BaseTest.CONNECTIONSTRING_REGISTER_RW); + registerConnection.Open(); + await RegisterPurge(registerConnection); + await RegisterInsert(registerConnection, registerLegalEntityCount, registerBrandCount, registerSoftwareProductCount, registerModified); + + // Seed MockDataHolder + using var dataHolderConnection = new SqlConnection(BaseTest.CONNECTIONSTRING_MDH_RW); + dataHolderConnection.Open(); + await DataHolderPurge(dataHolderConnection); + await DataHolderInsert(dataHolderConnection, dataHolderLegalEntityCount, dataHolderBrandCount, dataHolderSoftwareProductCount, dataHolderModified); + } + + // Purge register database but leave standing data intact + private static async Task RegisterPurge(SqlConnection connection) + { + await connection.ExecuteAsync("delete AuthDetail"); + await connection.ExecuteAsync("delete Brand"); + await connection.ExecuteAsync("delete Endpoint"); + await connection.ExecuteAsync("delete LegalEntity"); + await connection.ExecuteAsync("delete Participation"); + await connection.ExecuteAsync("delete SoftwareProduct"); + await connection.ExecuteAsync("delete SoftwareProductCertificate"); + } + + // Purge data holder database but leave standing data intact + private static async Task DataHolderPurge(SqlConnection connection) + { + await connection.ExecuteAsync("delete [Transaction]"); + await connection.ExecuteAsync("delete Account"); + await connection.ExecuteAsync("delete SoftwareProduct"); + await connection.ExecuteAsync("delete Brand"); + await connection.ExecuteAsync("delete Customer"); + await connection.ExecuteAsync("delete Organisation"); + await connection.ExecuteAsync("delete Person"); + await connection.ExecuteAsync("delete LegalEntity"); + } + + private static async Task RegisterInsert(SqlConnection connection, int legalEntityCount, int brandCount, int softwareProductCount, bool modified) + { + static async Task Register_InsertLegalEntity(SqlConnection connection, IndustryType industryType, bool modified) + { + var legalEntityId = new Guid($"00000000-0000-0000-0000-{++nextRegisterLegalEntityId:d012}"); + + string legalEntityName = $"LegalEntity_{legalEntityId}".ToString().Replace('-', '_'); + + await connection.ExecuteScalarAsync(@" + insert into LegalEntity(LegalEntityId, LegalEntityName, LogoUri, AnzsicDivision, OrganisationTypeId, LegalEntityStatusId, AccreditationLevelId, AccreditationNumber) + values(@LegalEntityId, @LegalEntityName, @LogoUri, @AnzsicDivision, @OrganisationTypeId, @LegalEntityStatusId, @AccreditationLevelId, @AccreditationNumber)", + new + { + LegalEntityId = legalEntityId, + LegalEntityName = modified ? "foo" : legalEntityName, + LogoUri = modified ? "foo" : $"https://www.{legalEntityName}.com/logo.jpg", + AnzsicDivision = industryType switch + { + IndustryType.Banking => "6221", + IndustryType.Energy => "2640", + IndustryType.Telecommunications => "5801", + _ => throw new NotSupportedException() + }, + OrganisationTypeId = "2", // company + LegalEntityStatusId = "1", // make it active by default + AccreditationLevelId = "1", // unrestricted + AccreditationNumber = $"ABC{nextRegisterLegalEntityId:d012}" + }); + + return legalEntityId; + } + + static async Task Register_InsertParticipation(SqlConnection connection, Guid legalEntityId, ParticipantType participantType, IndustryType industryType, bool modified) + { + var participationId = new Guid($"00000000-0000-0000-0000-{++nextRegisterParticipationId:d012}"); + + await connection.ExecuteScalarAsync(@" + insert into Participation(ParticipationId, LegalEntityId, ParticipationTypeId, IndustryId, StatusId) + values(@ParticipationId, @LegalEntityId, + (select ParticipationTypeId from ParticipationType where ParticipationTypeCode = @ParticipantTypeCode), + (select IndustryTypeId from IndustryType where IndustryTypeCode = @IndustryTypeCode), + (select ParticipationStatusId from ParticipationStatus where Upper(ParticipationStatusCode) = @ParticipationStatusCode))", + new + { + ParticipationId = participationId, + LegalEntityId = legalEntityId, + ParticipantTypeCode = participantType.ToString(), + ParticipationStatusCode = modified ? "INACTIVE" : "ACTIVE", + IndustryTypeCode = industryType.ToString() + }); + + return participationId; + } + + static async Task Register_InsertBrand(SqlConnection connection, Guid participationId, bool modified) + { + var brandId = new Guid($"00000000-0000-0000-0000-{++nextRegisterBrandId:d012}"); + + string brandName = $"Brand_{brandId}".ToString().Replace('-', '_'); + + await connection.ExecuteScalarAsync(@" + insert into Brand(BrandId, BrandName, LogoUri, BrandStatusId, ParticipationId, LastUpdated) + values(@BrandId, @BrandName, @LogoUri, + --(select BrandStatusId from BrandStatus where Upper(BrandStatusCode) = 'ACTIVE'), + @StatusId, + @ParticipationId, + @LastUpdated)", + new + { + BrandId = brandId, + BrandName = modified ? "foo" : brandName, + LogoUri = modified ? "foo" : $"https://www.{brandName}.com/logo.jpg", + StatusId = modified ? "2" : "1", // 1=active, 2=inactive + ParticipationId = participationId, + LastUpdated = DateTime.UtcNow + }); + + return brandId; + } + + static async Task Register_InsertSoftwareProduct(SqlConnection connection, Guid brandId, bool modified) + { + var softwareProductId = new Guid($"00000000-0000-0000-0000-{++nextRegisterSoftwareProductId:d012}"); + + string softwareProductName = $"SoftwareProduct_{softwareProductId}".ToString().Replace('-', '_'); + + await connection.ExecuteScalarAsync(@" + insert into SoftwareProduct( + SoftwareProductId, + SoftwareProductName, + SoftwareProductDescription, + LogoUri, + SectorIdentifierUri, + ClientUri, + RecipientBaseUri, + RevocationUri, + RedirectUris, + JwksUri, + Scope, + StatusId, + BrandId) + values( + @SoftwareProductId, + @SoftwareProductName, + @SoftwareProductDescription, + @LogoUri, + @SectorIdentifierUri, + @ClientUri, + @RecipientBaseUri, + @RevocationUri, + @RedirectUris, + @JwksUri, + @Scope, + --(select SoftwareProductStatusId from SoftwareProductStatus where Upper(SoftwareProductStatusCode) = 'ACTIVE'), + @StatusId, + @BrandId)", + new + { + SoftwareProductId = softwareProductId, + SoftwareProductName = modified ? "foo" : $"{softwareProductName}", + SoftwareProductDescription = modified ? "foo" : $"{softwareProductName} description", + LogoUri = modified ? "foo" : $"https://www.{softwareProductName}.com/logo.jpg", + SectorIdentifierUri = $"https://www.{softwareProductName}.com/sectoridentifier", + ClientUri = $"https://www.{softwareProductName}.com/client", + RecipientBaseUri = $"https://www.{softwareProductName}.com/recipientbase", + RevocationUri = $"https://www.{softwareProductName}.com/revocation", + RedirectUris = $"https://www.{softwareProductName}.com/redirect1,https://www.{softwareProductName}.com/redirect2", + JwksUri = $"https://www.{softwareProductName}.com/jwks", + Scope = "scope", + StatusId = modified ? "2" : "1", // 1=active, 2=inactive + BrandId = brandId, + }); + + return softwareProductId; + } + + // Insert legal entities + for (int ilegalEntity = 0; ilegalEntity < legalEntityCount; ilegalEntity++) + { + var register_LegalEntityId = await Register_InsertLegalEntity(connection, IndustryType.Banking, modified); + var register_ParticipationId = await Register_InsertParticipation(connection, register_LegalEntityId, ParticipantType.DR, IndustryType.Banking, modified); + + // Insert brands + for (int ibrandCount = 0; ibrandCount < brandCount; ibrandCount++) + { + var register_BrandId = await Register_InsertBrand(connection, register_ParticipationId, modified); + + // Insert software products + for (int isoftwareProductCount = 0; isoftwareProductCount < softwareProductCount; isoftwareProductCount++) + { + var register_SoftwareProductId = await Register_InsertSoftwareProduct(connection, register_BrandId, modified); + } + } + } + } + + private static async Task DataHolderInsert(SqlConnection connection, int legalEntityCount, int brandCount, int softwareProductCount, bool modified) + { + static async Task DataHolder_InsertLegalEntity(SqlConnection connection, bool modified) + { + var legalEntityId = new Guid($"00000000-0000-0000-0000-{++nextDataHolderLegalEntityId:d012}"); + + string legalEntityName = $"LegalEntity_{legalEntityId}".ToString().Replace('-', '_'); + + await connection.ExecuteScalarAsync(@" + insert into LegalEntity(LegalEntityId, LegalEntityName, LogoUri, Status) + values(@LegalEntityId, @LegalEntityName, @LogoUri, @Status)", + new + { + LegalEntityId = legalEntityId, + LegalEntityName = modified ? "foo" :legalEntityName, + LogoUri = modified ? "foo" : $"https://www.{legalEntityName}.com/logo.jpg", + Status = modified ? "REMOVED" : "ACTIVE" + }); + + return legalEntityId; + } + + static async Task DataHolder_InsertBrand(SqlConnection connection, Guid legalEntityId, bool modified) + { + var brandId = new Guid($"00000000-0000-0000-0000-{++nextDataHolderBrandId:d012}"); + + string brandName = $"Brand_{brandId}".ToString().Replace('-', '_'); + + await connection.ExecuteScalarAsync(@" + insert into Brand(BrandId, BrandName, LogoUri, Status, LegalEntityId) + values(@BrandId, @BrandName, @LogoUri, @Status, @LegalEntityId)", + new + { + BrandId = brandId, + BrandName = modified ? "foo" : brandName, + LogoUri = modified ? "foo" : $"https://www.{brandName}.com/logo.jpg", + Status = modified ? "INACTIVE" : "ACTIVE", + legalEntityId = legalEntityId + }); + + return brandId; + } + + static async Task DataHolder_InsertSoftwareProduct(SqlConnection connection, Guid brandId, bool modified) + { + var softwareProductId = new Guid($"00000000-0000-0000-0000-{++nextDataHolderSoftwareProductId:d012}"); + + string SoftwareProductName = $"SoftwareProduct_{softwareProductId}".ToString().Replace('-', '_'); + + await connection.ExecuteScalarAsync(@" + insert into SoftwareProduct(SoftwareProductId, SoftwareProductName, SoftwareProductDescription, LogoUri, Status, BrandId) + values(@SoftwareProductId, @SoftwareProductName, @SoftwareProductDescription, @LogoUri, @Status, @BrandId)", + new + { + SoftwareProductId = softwareProductId, + SoftwareProductName = modified ? "foo" : SoftwareProductName, + SoftwareProductDescription = modified ? "foo" : $"{SoftwareProductName} description", + LogoUri = modified ? "foo" : $"https://www.{SoftwareProductName}.com/logo.jpg", + Status = modified ? "INACTIVE" : "ACTIVE", + BrandId = brandId + }); + + return softwareProductId; + } + + for (int i = 1; i <= legalEntityCount; i++) + { + var dataholder_LegalEntityId = await DataHolder_InsertLegalEntity(connection, modified); + + for (int i2 = 1; i2 <= brandCount; i2++) + { + var dataholder_BrandId = await DataHolder_InsertBrand(connection, dataholder_LegalEntityId, modified); + + for (int i3 = 1; i3 <= brandCount; i3++) + { + var dataholder_SoftwareProductId = await DataHolder_InsertSoftwareProduct(connection, dataholder_BrandId, modified); + } + } + } + } + } +} diff --git a/Source/CDR.GetDataRecipients.IntegrationTests/Fixtures/TestFixture.cs b/Source/CDR.GetDataRecipients.IntegrationTests/Fixtures/TestFixture.cs new file mode 100644 index 0000000..4e4fe56 --- /dev/null +++ b/Source/CDR.GetDataRecipients.IntegrationTests/Fixtures/TestFixture.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using Xunit; + +namespace CDR.GetDataRecipients.IntegrationTests.Fixtures +{ + public class TestFixture : IAsyncLifetime + { + public Task InitializeAsync() + { + return Task.CompletedTask; + } + + public Task DisposeAsync() + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Source/CDR.GetDataRecipients.IntegrationTests/US28391_GetDataRecipients.cs b/Source/CDR.GetDataRecipients.IntegrationTests/US28391_GetDataRecipients.cs new file mode 100644 index 0000000..d78de57 --- /dev/null +++ b/Source/CDR.GetDataRecipients.IntegrationTests/US28391_GetDataRecipients.cs @@ -0,0 +1,221 @@ +// #define DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON + +using System; +using System.Threading.Tasks; +using Microsoft.Data.SqlClient; +using Dapper; +using Newtonsoft.Json; +using Xunit; +using FluentAssertions; +using FluentAssertions.Execution; +using System.Net.Http; +using System.Net; + +#nullable enable + +namespace CDR.GetDataRecipients.IntegrationTests +{ + // 28724 + public class US28391_GetDataRecipients : BaseTest + { + private async Task ExecuteAzureFunction() + { + var client = new HttpClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, $"{AZUREFUNCTIONS_URL}/INTEGRATIONTESTS_DATARECIPIENTS"); + + var response = await client.SendAsync(request); + + if (response.StatusCode != HttpStatusCode.OK) + { + throw new Exception($"Expected OK calling {request.RequestUri} but got {response.StatusCode}"); + } + } + + private async Task Test( + int registerLegalEntityCount, int registerBrandCount, int registerSoftwareProductCount, + int dataHolderLegalEntityCount, int dataHolderBrandCount, int dataHolderSoftwareProductCount, + bool registerModified = false, bool dataHolderModified = false) + { + // Arrange + await DatabaseSeeder.Execute( + registerLegalEntityCount, registerBrandCount, registerSoftwareProductCount, + dataHolderLegalEntityCount, dataHolderBrandCount, dataHolderSoftwareProductCount, + registerModified, dataHolderModified + ); + + // Act + await ExecuteAzureFunction(); + + // Assert + using (new AssertionScope()) + { + await Assert_RegisterAndDataHolderIsSynced(); + } + } + + [Theory] + [InlineData(0, 0, 0)] // no records + [InlineData(1, 0, 0)] // has DH legalentity - FAILS - doesnt delete extra legalentity + [InlineData(1, 1, 0)] // has DH legalentity & brand - FAILS - doesnt delete extra legalentity, brand + [InlineData(1, 1, 1)] // has DH legalentity & brand & softwareproduct - FAILS - doesnt delete extra legalentity, brand, softwareproduct + public async Task ACX01_WhenRegisterEmpty_ShouldSync(int dataHolderLegalEntityCount, int dataHolderBrandCount, int dataHolderSoftwareProductCount) + { + await Test(0, 0, 0, dataHolderLegalEntityCount, dataHolderBrandCount, dataHolderSoftwareProductCount); + } + + [Theory] + [InlineData(0, 0, 0)] // no records + [InlineData(1, 0, 0)] // has DH legalentity + [InlineData(1, 1, 0)] // has DH legalentity & brand + [InlineData(1, 1, 1)] // has DH legalentity & brand & softwareproduct + public async Task ACX01_WhenDataHolderEmpty_ShouldSync(int registerLegalEntityCount, int registerBrandCount, int registerSoftwareProductCount) + { + await Test(registerLegalEntityCount, registerBrandCount, registerSoftwareProductCount, 0, 0, 0); + } + + [Theory] + [InlineData(0, 0, 0)] // nothing + [InlineData(1, 0, 0)] // has legalentity + [InlineData(1, 1, 0)] // has legalentity & brand + [InlineData(1, 1, 1)] // has legalentity & brand & softwareproduct + public async Task ACX01_WhenRegisterAndDataHolderSame_ShouldSync(int legalEntityCount, int brandCount, int softwareProductCount) + { + await Test(legalEntityCount, brandCount, softwareProductCount, legalEntityCount, brandCount, softwareProductCount); + } + + [Theory] + [InlineData(2, 1, 1)] // extra legalentity + [InlineData(2, 2, 1)] // extra legalentity & brand + [InlineData(2, 2, 2)] // extra legalentity, brand & softwareproduct + public async Task ACX01_WhenAdditionalRegisterRecords_ShouldSync(int registerLegalEntityCount, int registerBrandCount, int registerSoftwareProductCount) + { + await Test(registerLegalEntityCount, registerBrandCount, registerSoftwareProductCount, 1, 1, 1); + } + + [Theory] + [InlineData(2, 1, 1)] // extra legalentity + [InlineData(2, 2, 1)] // extra legalentity & brand - FAILS - doesnt delete extra brand + [InlineData(2, 2, 2)] // extra legalentity, brand & softwareproduct - FAILS - doesnt delete extra software product + public async Task ACX01_WhenAdditionalDataHolderRecords_ShouldSync(int dataHolderLegalEntityCount, int dataHolderBrandCount, int dataHolderSoftwareProductCount) + { + await Test(1, 1, 1, dataHolderLegalEntityCount, dataHolderBrandCount, dataHolderSoftwareProductCount); + } + + [Fact] // FAILS - doesn't update legalentity.status in DH + public async Task ACX01_WhenRegisterChanged_ShouldSync() + { + await Test(1, 1, 1, 1, 1, 1, true, false); + } + + [Fact] + public async Task ACX01_WhenDataHolderChanged_ShouldSync() + { + await Test(1, 1, 1, 1, 1, 1, false, true); + } + + // No need to test 1001 records for DataRecipients as register does not implement paging for GetDataRecipients + // [Theory] + // [InlineData(1000)] + // [InlineData(1001)] // This will fail because Azure function is not using paging and Register can only return 1000 records max + // public async Task ACX02_WhenMoreThan1000DataRecipients_ShouldSync(int dataRecipientsInRegister) + // { + // // Arrange + // await DatabaseSeeder.Execute(dataRecipientsInRegister); + + // // Act + // await ExecuteAzureFunction(); + + // // Assert + // using (new AssertionScope()) + // { + // await Assert_RegisterAndDataHolderIsSynced(); + // } + // } + + static private async Task Assert_RegisterAndDataHolderIsSynced() + { + static async Task Assert_TableDataIsEqual( + SqlConnection registerConnection, string registerSql, + SqlConnection dataHolderConnection, string dataHolderSql, + string tableName) + { + var registerJson = JsonConvert.SerializeObject(await registerConnection.QueryAsync(registerSql)); + var dataHolderJson = JsonConvert.SerializeObject(await dataHolderConnection.QueryAsync(dataHolderSql)); + + // Assert data is same +#if DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON + File.WriteAllText($"c:/temp/expected_{tableName}.json", registerJson); + File.WriteAllText($"c:/temp/actual_{tableName}.json", dataHolderJson); +#endif + // registerJson.Should().Be(dataHolderJson); + dataHolderJson.Should().Be(registerJson); + } + + const string REGISTER_LEGALENTITY_SQL = @" + select + le.LegalEntityId, + le.LegalEntityName, + -- le.LegalEntityStatusId, + Upper(ps.ParticipationStatusCode) Status, + le.LogoUri + -- p.ParticipationTypeId, + -- p.IndustryId, + -- p.StatusId, + -- pt.ParticipationTypeCode + from LegalEntity le + left outer join LegalEntityStatus les on les.LegalEntityStatusId = le.LegalEntityStatusId + left outer join Participation p on p.LegalEntityId = le.LegalEntityId + left outer join ParticipationStatus ps on ps.ParticipationStatusId = p.StatusId + left outer join ParticipationType pt on pt.ParticipationTypeId = p.ParticipationTypeId + where pt.ParticipationTypeCode = 'DR' + order by le.LegalEntityId"; + + const string REGISTER_BRAND_SQL = @" + select + b.BrandId, + b.BrandName, + b.LogoUri, + bs.BrandStatusCode Status, + le.LegalEntityId LegalEntityId + from Brand b + left outer join Participation p on p.ParticipationId = b.ParticipationId + left outer join ParticipationType pt on pt.ParticipationTypeId = p.ParticipationTypeId + left outer join LegalEntity le on le.LegalEntityId = p.LegalEntityId + left outer join BrandStatus bs on bs.BrandStatusId = b.BrandStatusId + where pt.ParticipationTypeCode = 'DR' + order by BrandId"; + + const string REGISTER_SOFTWAREPRODUCT_SQL = @" + select + sp.SoftwareProductId, + sp.SoftwareProductName, + sp.SoftwareProductDescription, + sp.LogoUri, + sps.SoftwareProductStatusCode Status + from SoftwareProduct sp + left outer join Brand b on b.BrandId = sp.BrandId + left outer join Participation p on p.ParticipationId = b.ParticipationId + left outer join ParticipationType pt on pt.ParticipationTypeId = p.ParticipationTypeId + left outer join SoftwareProductStatus sps on sps.SoftwareProductStatusId = sp.StatusId + where pt.ParticipationTypeCode = 'DR' -- hardly necessary since only DRs have software products anyway + order by SoftwareProductId"; + + // just 'select *' incase new fields are added, at least test will fail and let someone investigate what other columns might need to be synced + var DATAHOLDER_LEGALENTITY_SQL = "select * from LegalEntity order by LegalEntityId"; + var DATAHOLDER_BRAND_SQL = "select * from Brand order by BrandId"; + var DATAHOLDER_SOFTWAREPRODUCT_SQL = "select SoftwareProductId, SoftwareProductName, SoftwareProductDescription, LogoUri, [Status] from SoftwareProduct order by SoftwareProductId"; + + using var registerConnection = new SqlConnection(BaseTest.CONNECTIONSTRING_REGISTER_RW); + registerConnection.Open(); + + using var dataHolderConnection = new SqlConnection(BaseTest.CONNECTIONSTRING_MDH_RW); + dataHolderConnection.Open(); + + // Assert + await Assert_TableDataIsEqual(registerConnection, REGISTER_LEGALENTITY_SQL, dataHolderConnection, DATAHOLDER_LEGALENTITY_SQL, "LegalEntity"); + await Assert_TableDataIsEqual(registerConnection, REGISTER_BRAND_SQL, dataHolderConnection, DATAHOLDER_BRAND_SQL, "Brand"); + await Assert_TableDataIsEqual(registerConnection, REGISTER_SOFTWAREPRODUCT_SQL, dataHolderConnection, DATAHOLDER_SOFTWAREPRODUCT_SQL, "SoftwareProduct"); + } + } +} diff --git a/Source/CDR.GetDataRecipients.IntegrationTests/XUnit/AlphabeticalOrderer.cs b/Source/CDR.GetDataRecipients.IntegrationTests/XUnit/AlphabeticalOrderer.cs new file mode 100644 index 0000000..dfa32bf --- /dev/null +++ b/Source/CDR.GetDataRecipients.IntegrationTests/XUnit/AlphabeticalOrderer.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace CDR.GetDataRecipients.IntegrationTests.XUnit.Orderers +{ + 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 diff --git a/Source/CDR.GetDataRecipients.IntegrationTests/appsettings.Development.json b/Source/CDR.GetDataRecipients.IntegrationTests/appsettings.Development.json new file mode 100644 index 0000000..c65d98b --- /dev/null +++ b/Source/CDR.GetDataRecipients.IntegrationTests/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "ConnectionStrings": { + "Register_RW": "Server=localhost,9933;Database=cdr-register;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", + "DataHolder_RW": "Server=localhost,9933;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True" + }, + "URL": { + "AZUREFUNCTIONS": "http://localhost:7074" + } +} \ No newline at end of file diff --git a/Source/CDR.GetDataRecipients.IntegrationTests/appsettings.Release.json b/Source/CDR.GetDataRecipients.IntegrationTests/appsettings.Release.json new file mode 100644 index 0000000..41c7506 --- /dev/null +++ b/Source/CDR.GetDataRecipients.IntegrationTests/appsettings.Release.json @@ -0,0 +1,9 @@ +{ + "ConnectionStrings": { + "Register_RW": "Server=mssql;Database=cdr-register;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True", + "DataHolder_RW": "Server=mssql;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True" + }, + "URL": { + "AZUREFUNCTIONS": "http://getdatarecipients:7074" + } +} \ No newline at end of file diff --git a/Source/CDR.GetDataRecipients.IntegrationTests/appsettings.json b/Source/CDR.GetDataRecipients.IntegrationTests/appsettings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/Source/CDR.GetDataRecipients.IntegrationTests/appsettings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/Source/CDR.GetDataRecipients.IntegrationTests/local.settings.json b/Source/CDR.GetDataRecipients.IntegrationTests/local.settings.json new file mode 100644 index 0000000..6804bed --- /dev/null +++ b/Source/CDR.GetDataRecipients.IntegrationTests/local.settings.json @@ -0,0 +1,27 @@ +// Settings required to run these Azure functions in docker-compose.GetDataRecipients.IntegrationTests stack +// See docker-compose.IntegrationTests.yml where this file is volume mapped over the existing CDR.GetDataRecipients local.settings.json file +{ + "IsEncrypted": false, + "Values": { + // "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "AzureWebJobsStorage": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;QueueEndpoint=http://azurite:10001/devstoreaccount1;TableEndpoint=http://azurite:10002/devstoreaccount1;", + // "StorageConnectionString": "UseDevelopmentStorage=true", + "StorageConnectionString": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;QueueEndpoint=http://azurite:10001/devstoreaccount1;TableEndpoint=http://azurite:10002/devstoreaccount1;", + "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "Schedule": "0-59 * * * *", + // "DataHolder_DB_ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-mdh;Integrated Security=true", + "DataHolder_DB_ConnectionString": "Server=mssql;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True;TrustServerCertificate=True", + // "DataHolder_Logging_DB_ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-mdh;Integrated Security=true", + "DataHolder_Logging_DB_ConnectionString": "Server=mssql;Database=cdr-mdh;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True;TrustServerCertificate=True", + // "Register_GetDataRecipients_Endpoint": "https://localhost:7000/cdr-register/v1/all/data-recipients", + "Register_GetDataRecipients_Endpoint": "https://mock-register:7000/cdr-register/v1/all/data-recipients", + "Register_GetDataRecipients_XV": "3", + // "Ignore_Server_Certificate_Errors": "false" + "Ignore_Server_Certificate_Errors": "true" + }, + "Host": { + "LocalHttpPort": 7074, + "CORS": "*", + "CORSCredentials": false + } +} \ No newline at end of file diff --git a/Source/CDR.GetDataRecipients.IntegrationTests/test.http b/Source/CDR.GetDataRecipients.IntegrationTests/test.http new file mode 100644 index 0000000..3deba6d --- /dev/null +++ b/Source/CDR.GetDataRecipients.IntegrationTests/test.http @@ -0,0 +1,14 @@ + +@func_host = http://localhost:7074 + +## + +GET {{func_host}}/INTEGRATIONTESTS_DATARECIPIENTS + +### + +GET https://mock-register:7000/cdr-register/v1/all/data-recipients + +### + +GET https://mock-register:7000/cdr-register/v1/banking/data-recipients \ No newline at end of file diff --git a/Source/CDR.GetDataRecipients/CDR.GetDataRecipients.csproj b/Source/CDR.GetDataRecipients/CDR.GetDataRecipients.csproj index 8427aa7..d3cbbda 100644 --- a/Source/CDR.GetDataRecipients/CDR.GetDataRecipients.csproj +++ b/Source/CDR.GetDataRecipients/CDR.GetDataRecipients.csproj @@ -3,9 +3,9 @@ net6.0 v4 <_FunctionsSkipCleanOutput>true - 1.0.1 - 1.0.1 - 1.0.1 + 1.1.0 + 1.1.0 + 1.1.0 diff --git a/Source/CDR.GetDataRecipients/GetDataRecipients.cs b/Source/CDR.GetDataRecipients/GetDataRecipients.cs index 30edc02..f02810e 100644 --- a/Source/CDR.GetDataRecipients/GetDataRecipients.cs +++ b/Source/CDR.GetDataRecipients/GetDataRecipients.cs @@ -148,6 +148,14 @@ private static async Task CompareDataRecipients(IList regDataRecipi } } } + // Delete all existing if there are no data recipients in register. + else if (dhDataRecipients.Any()) + { + foreach (var dhDr in dhDataRecipients) + { + delDataRecipients.Add(dhDr); + } + } // INSERT Register Data Recipients including its child Brands and Software Products into Data Holder repo if (insDataRecipients.Any()) @@ -250,7 +258,17 @@ private static async Task ProcessCompareDataRecipients(IList dhData return; } - foreach (var regDrBrand in regDataRecipient.Brands) + // Check if there are more brands than register + var dhExtraDrBrands = dhDataRecipient.Brands.ExceptBy(regDataRecipient.Brands.Select(b => b.BrandId), b => b.BrandId); + if (dhExtraDrBrands.Any()) + { + foreach (var brand in dhExtraDrBrands) + { + delBrands.Add(brand); + } + } + + foreach (var regDrBrand in regDataRecipient.Brands) { // Register Data Recipient -> Legal Entity and Brand ONLY - NO Software Products if (!regDrBrand.SoftwareProducts.Any()) @@ -274,7 +292,7 @@ private static async Task ProcessCompareDataRecipients(IList dhData // Register Data Recipient -> Legal Entity, Brands and Software Products foreach (var regDrSwProd in regDrBrand.SoftwareProducts) { - await CompareRegToDh(dhDataRecipients, regDataRecipient, regDrBrand, regDrSwProd, insBrands, insSwProds, delSwProds, updDataRecipients); + await CompareRegToDh(dhDataRecipients, regDataRecipient, regDrBrand, regDrSwProd, insBrands, insSwProds, delBrands, delSwProds, updDataRecipients); } } } @@ -285,7 +303,10 @@ private static async Task ProcessCompareDataRecipients(IList dhData } } - private static async Task CompareRegToDh(IList dhDataRecipients, LegalEntity regDr, Brand regDrBrand, SoftwareProduct regDrSwProd, IList insBrands, IList insSwProds, IList delSwProds, IList updDrs) + private static async Task CompareRegToDh( + IList dhDataRecipients, LegalEntity regDr, Brand regDrBrand, SoftwareProduct regDrSwProd, + IList insBrands, IList insSwProds, + IList delBrands, IList delSwProds, IList updDrs) { try { @@ -321,7 +342,13 @@ private static async Task CompareRegToDh(IList dhDataRecipients, Le // DOES the Data Holder repo differ to Register? foreach (var dhDrBrand in dhDataRecipient.Brands) { - if (dhDrBrand.SoftwareProducts.Count > regDrBrand.SoftwareProducts.Count) + // Check if the brand has already been marked for deletion + if (delBrands.Any(db => db.BrandId == dhDrBrand.BrandId)) + { + continue; + } + + if (dhDrBrand.SoftwareProducts.Count > regDrBrand.SoftwareProducts.Count) { foreach (var dhSwProd in dhDrBrand.SoftwareProducts) { diff --git a/Source/CDR.GetDataRecipients/GetDataRecipients_IntegrationTestsHelper.cs b/Source/CDR.GetDataRecipients/GetDataRecipients_IntegrationTestsHelper.cs new file mode 100644 index 0000000..4574055 --- /dev/null +++ b/Source/CDR.GetDataRecipients/GetDataRecipients_IntegrationTestsHelper.cs @@ -0,0 +1,33 @@ + +#if INTEGRATION_TESTS + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; + +namespace CDR.GetDataRecipients +{ + public static class GetDataRecipients_IntegrationTestsHelper + { + // This http trigger is used the integration tests so that DATARECIPIENTS can be triggered on demand and not wait for timer + [FunctionName("INTEGRATIONTESTS_DATARECIPIENTS")] + public static async Task INTEGRATIONTESTS_DATARECIPIENTS( + // [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, + ILogger log, + ExecutionContext context) + { + log.LogInformation($"{nameof(GetDataRecipients_IntegrationTestsHelper)}.{nameof(INTEGRATIONTESTS_DATARECIPIENTS)}"); + + // Call the actual Azure function + await GetDataRecipientsFunction.DATARECIPIENTS(null, log, context); + + return new OkResult(); + } + } +} + +#endif \ No newline at end of file diff --git a/Source/DataHolder.sln b/Source/DataHolder.sln index c6940ae..e23a511 100644 --- a/Source/DataHolder.sln +++ b/Source/DataHolder.sln @@ -65,6 +65,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CDR.GetDataRecipients", "CD EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "070 Functions", "070 Functions", "{FE6797E5-74EB-411B-9517-4862E5C0FA4B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CDR.GetDataRecipients.IntegrationTests", "CDR.GetDataRecipients.IntegrationTests\CDR.GetDataRecipients.IntegrationTests.csproj", "{E2AC9628-B7E0-4FFA-A218-544770C8D4E4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CDR.DataHolder.API.Logger", "CDR.DataHolder.API.Logger\CDR.DataHolder.API.Logger.csproj", "{CA30C6CF-B5A2-401A-B0E4-2015F86058D8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -127,6 +131,14 @@ Global {D333CBF2-8B13-4123-9761-F2D1825D7B19}.Debug|Any CPU.Build.0 = Debug|Any CPU {D333CBF2-8B13-4123-9761-F2D1825D7B19}.Release|Any CPU.ActiveCfg = Release|Any CPU {D333CBF2-8B13-4123-9761-F2D1825D7B19}.Release|Any CPU.Build.0 = Release|Any CPU + {E2AC9628-B7E0-4FFA-A218-544770C8D4E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2AC9628-B7E0-4FFA-A218-544770C8D4E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2AC9628-B7E0-4FFA-A218-544770C8D4E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2AC9628-B7E0-4FFA-A218-544770C8D4E4}.Release|Any CPU.Build.0 = Release|Any CPU + {CA30C6CF-B5A2-401A-B0E4-2015F86058D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA30C6CF-B5A2-401A-B0E4-2015F86058D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA30C6CF-B5A2-401A-B0E4-2015F86058D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA30C6CF-B5A2-401A-B0E4-2015F86058D8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -147,6 +159,8 @@ Global {09D871DE-970E-439B-BE6E-B2FCFC7C6604} = {8EAABA54-71FF-4D68-BDF9-6753989DA976} {594639B6-050B-433E-8686-19EAA83230AC} = {334094B2-2055-4954-9953-F731DC8C857F} {D333CBF2-8B13-4123-9761-F2D1825D7B19} = {FE6797E5-74EB-411B-9517-4862E5C0FA4B} + {E2AC9628-B7E0-4FFA-A218-544770C8D4E4} = {334094B2-2055-4954-9953-F731DC8C857F} + {CA30C6CF-B5A2-401A-B0E4-2015F86058D8} = {4B38DDC0-9F63-4B9B-84A2-C97B40EA2D23} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AE8E874B-11F7-494D-9902-AD374F1EC3B3} diff --git a/Source/DockerCompose/docker-compose.yml b/Source/DockerCompose/docker-compose.yml index 64b9a84..bbd69a7 100644 --- a/Source/DockerCompose/docker-compose.yml +++ b/Source/DockerCompose/docker-compose.yml @@ -68,6 +68,8 @@ services: image: consumerdataright/mock-data-recipient ports: - "9001:9001" + extra_hosts: + - "host.docker.internal:host-gateway" environment: - ASPNETCORE_ENVIRONMENT=Container healthcheck: diff --git a/Source/Dockerfile b/Source/Dockerfile index e972ad4..41ad793 100644 --- a/Source/Dockerfile +++ b/Source/Dockerfile @@ -22,6 +22,7 @@ COPY ./CDR.DataHolder.Public.API/. /app/CDR.DataHolder.Public.API COPY ./CDR.DataHolder.Resource.API/. /app/CDR.DataHolder.Resource.API COPY ./CDR.DataHolder.IdentityServer/. /app/CDR.DataHolder.IdentityServer COPY ./CDR.DataHolder.API.Gateway.mTLS/. /app/CDR.DataHolder.API.Gateway.mTLS +COPY ./CDR.DataHolder.API.Logger/. /app/CDR.DataHolder.API.Logger WORKDIR /app/CDR.DataHolder.Admin.API RUN dotnet publish -c Release -o /app/publish/admin diff --git a/Source/Dockerfile.for-testing b/Source/Dockerfile.for-testing index 0357188..78f9d09 100644 --- a/Source/Dockerfile.for-testing +++ b/Source/Dockerfile.for-testing @@ -22,6 +22,7 @@ COPY ./CDR.DataHolder.Public.API/. /app/CDR.DataHolder.Public.API COPY ./CDR.DataHolder.Resource.API/. /app/CDR.DataHolder.Resource.API COPY ./CDR.DataHolder.IdentityServer/. /app/CDR.DataHolder.IdentityServer COPY ./CDR.DataHolder.API.Gateway.mTLS/. /app/CDR.DataHolder.API.Gateway.mTLS +COPY ./CDR.DataHolder.API.Logger/. /app/CDR.DataHolder.API.Logger WORKDIR /app/CDR.DataHolder.Admin.API RUN dotnet publish -c Release -o /app/publish/admin diff --git a/Source/Dockerfile.get-data-recipients b/Source/Dockerfile.get-data-recipients new file mode 100644 index 0000000..392aac9 --- /dev/null +++ b/Source/Dockerfile.get-data-recipients @@ -0,0 +1,18 @@ +# Dockerfile for running GetDataRecipients for integration testing + +FROM mcr.microsoft.com/azure-functions/dotnet:4-dotnet6-core-tools AS build + +COPY . /src + +WORKDIR /src/CDR.GetDataRecipients + +# We are building for integration tests +RUN dotnet build /p:DefineConstants="INTEGRATION_TESTS" + +ENV AZURE_FUNCTIONS_ENVIRONMENT=Development + +# We only want the INTEGRATIONTESTS_DATARECIPIENTS function to load, since the integration tests will be triggering DATARECIPIENTS directly (and not relying on a timer trigger) +# also don't rebuild but tell func where to find output from previous build above +ENTRYPOINT ["func", "start", "--functions", "INTEGRATIONTESTS_DATARECIPIENTS", "--no-build", "--prefix", "bin/Debug/net6.0"] + +# ENTRYPOINT ["tail", "-f", "/dev/null"] \ No newline at end of file diff --git a/Source/Dockerfile.get-data-recipients.integration-tests b/Source/Dockerfile.get-data-recipients.integration-tests new file mode 100644 index 0000000..071d02f --- /dev/null +++ b/Source/Dockerfile.get-data-recipients.integration-tests @@ -0,0 +1,28 @@ +# Dockerfile for running GetDataRecipient integration tests + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +# EXPOSE 9999 +# EXPOSE 9998 +# EXPOSE 9997 +# EXPOSE 9996 + +# Default ASPNETCORE_ENVIRONMENT to Release +ENV ASPNETCORE_ENVIRONMENT=Release + +# Copy source +COPY . ./ + +# Install developer certificate +RUN dotnet dev-certs https + +# # Install ca certificate +# RUN apt-get update && apt-get install -y sudo +# RUN sudo cp ./CDR.DataHolder.API.Gateway.mTLS/Certificates/ca.crt /usr/local/share/ca-certificates/ca.crt +# RUN sudo update-ca-certificates + +# Run tests +WORKDIR /src/CDR.GetDataRecipients.IntegrationTests +RUN dotnet build --configuration Release + +ENTRYPOINT ["dotnet", "test", "--configuration", "Release", "--no-build", "--logger", "trx;verbosity=detailed;LogFileName=results.trx", "-r", "/testresults"] diff --git a/Source/docker-compose.GetDataRecipients.IntegrationTests.yml b/Source/docker-compose.GetDataRecipients.IntegrationTests.yml new file mode 100644 index 0000000..878b26b --- /dev/null +++ b/Source/docker-compose.GetDataRecipients.IntegrationTests.yml @@ -0,0 +1,116 @@ +version: '3.8' + +services: + mssql: + container_name: sql1 + image: 'mcr.microsoft.com/mssql/server:2019-latest' + ports: + - "1433:1433" + - "9933:1433" + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=Pa{}w0rd2019 + healthcheck: + test: /opt/mssql-tools/bin/sqlcmd -S . -U sa -P "Pa{}w0rd2019" -Q "SELECT 1" || exit 1 + timeout: 5s + interval: 5s + retries: 50 + + mock-register: + container_name: mock-register + image: mock-register + hostname: mock-register + ports: + - "7000:7000" + - "7001:7001" + - "7006:7006" + environment: + - ASPNETCORE_ENVIRONMENT=Release + volumes: + - "./_temp/mock-register/tmp:/tmp" + healthcheck: + test: test -f /app/admin/_healthcheck_ready || exit 1 + timeout: 5s + interval: 5s + retries: 50 + depends_on: + mssql: + condition: service_healthy + + mock-data-holder: + container_name: mock-data-holder + image: mock-data-holder + hostname: mock-data-holder + ports: + - "8000:8000" + - "8001:8001" + - "8002:8002" + - "8005:8005" + build: + context: . + dockerfile: Dockerfile.for-testing + # dockerfile: Dockerfile + environment: + - ASPNETCORE_ENVIRONMENT=Release + volumes: + - "./_temp/mock-data-holder/tmp:/tmp" + healthcheck: + test: test -f /app/manage/_healthcheck_ready || exit 1 + timeout: 5s + interval: 5s + retries: 50 + depends_on: + mssql: + condition: service_healthy + mock-register: + condition: service_healthy + + azurite: + container_name: azurite + hostname: azurite + image: 'mcr.microsoft.com/azure-storage/azurite' + ports: + - '10000:10000' + - '10001:10001' + - '10002:10002' + # healthcheck: # FIXME - MJS + + getdatarecipients: + container_name: getdatarecipients + image: getdatarecipients + hostname: getdatarecipients + build: + context: . + dockerfile: Dockerfile.get-data-recipients + ports: + - "7074:7074" + volumes: + # use test specific local.settings.json + # - ./CDR.GetDataRecipients.IntegrationTests/local.settings.json:/src/CDR.GetDataRecipients/local.settings.json + - ./CDR.GetDataRecipients.IntegrationTests/local.settings.json:/src/CDR.GetDataRecipients/bin/Debug/net6.0/local.settings.json + # healthcheck: # FIXME - MJS + depends_on: + azurite: + condition: service_started # FIXME - MJS - service healthy + mock-register: + condition: service_healthy + mock-data-holder: + condition: service_healthy + # mock-data-holder-energy: + # condition: service_healthy + + getdatarecipients-integration-tests: + container_name: getdatarecipients-integration-tests + image: getdatarecipients-integration-tests + build: + context: . + dockerfile: Dockerfile.get-data-recipients.integration-tests + environment: + - ASPNETCORE_ENVIRONMENT=Release + volumes: + - "./_temp/getdatarecipients-integration-tests/testresults:/testresults" + depends_on: + getdatarecipients: + condition: service_started # FIXME - MJS - service healthy + + diff --git a/Source/integration.runsettings b/Source/integration.runsettings new file mode 100644 index 0000000..a4d1912 --- /dev/null +++ b/Source/integration.runsettings @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Source/run-integration-tests-get-data-recipients.ps1 b/Source/run-integration-tests-get-data-recipients.ps1 new file mode 100644 index 0000000..acd7bc7 --- /dev/null +++ b/Source/run-integration-tests-get-data-recipients.ps1 @@ -0,0 +1,30 @@ +#Requires -PSEdition Core + +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +Write-Output "***********************************************************" +Write-Output "GetDataRecipients integration tests" +Write-Output "" +Write-Output "⚠ WARNING: Integration tests for GetDataRecipients will use the existing 'mock-register' image found on this machine. Rebuild that image if you wish to test with latest code changes for MockRegister" +Write-Output "***********************************************************" + +# Run integration tests +docker-compose -f docker-compose.GetDataRecipients.IntegrationTests.yml up --build --abort-on-container-exit --exit-code-from getdatarecipients-integration-tests +$_lastExitCode = $LASTEXITCODE + +# Stop containers +docker-compose -f docker-compose.GetDataRecipients.IntegrationTests.yml down + +if ($_lastExitCode -eq 0) { + Write-Output "***********************************************************" + Write-Output "✔ SUCCESS: GetDataRecipients integration tests passed" + Write-Output "***********************************************************" + exit 0 +} +else { + Write-Output "***********************************************************" + Write-Output "❌ FAILURE: GetDataRecipients integration tests failed" + Write-Output "***********************************************************" + exit 1 +} diff --git a/Source/supervisord.conf b/Source/supervisord.conf index 6a3371d..3d64b9d 100644 --- a/Source/supervisord.conf +++ b/Source/supervisord.conf @@ -38,3 +38,10 @@ stdout_logfile=/usr/bin/stdout stdout_logfile_maxbytes=0 directory=/app/idsvr command=/usr/bin/dotnet /app/idsvr/CDR.DataHolder.IdentityServer.dll + +; uncomment below lines to view the live logging within continer. +; you would still need to map the port to 9999 on the host to connect to the http server +;[inet_http_server] +;port=*:9999 +;username=user +;password=123 \ No newline at end of file