diff --git a/LCU.API.Internal.Document/.gitignore b/LCU.API.Internal.Document/.gitignore new file mode 100644 index 0000000..49c1fd9 --- /dev/null +++ b/LCU.API.Internal.Document/.gitignore @@ -0,0 +1,364 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd +/.idea/ +/.vscode/ diff --git a/LCU.API.Internal.Document/Dockerfile b/LCU.API.Internal.Document/Dockerfile new file mode 100644 index 0000000..b6c34b2 --- /dev/null +++ b/LCU.API.Internal.Document/Dockerfile @@ -0,0 +1,37 @@ +# .NET Core SDK +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS aspnetcore-build + +# Copy Projects +COPY LCU.API.Internal.Document/LCU.API.Internal.Document.csproj ./source/LCU.API.Internal.Document/ +COPY LCU.API.Internal.DocumentLib/LCU.API.Internal.DocumentLib.csproj ./source/LCU.API.Internal.DocumentLib/ +COPY LCU.Library.Internal.DocumentModels/LCU.Library.Internal.DocumentModels.csproj ./source/LCU.Library.Internal.DocumentModels/ + +# Install Credential Provider and set env variables to enable Nuget restore with auth +ARG PAT=bh2lq3jibktw67b2wdbqyfln7cd3je6lk52kijntpj43v6kimpgq +RUN curl -L https://raw.githubusercontent.com/Microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh | sh + +ENV NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED true +ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS "{\"endpointCredentials\": [{\"endpoint\":\"https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/nuget/v3/index.json\", \"password\":\"${PAT}\"}]}" + +RUN dotnet restore -s "https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/nuget/v3/index.json" -s "https://api.nuget.org/v3/index.json" ./source/LCU.API.Internal.Document/LCU.API.Internal.Document.csproj + +# Copy All Files +COPY . ./source/ + +LABEL test=true +COPY LCU.API.Internal.DocumentTest/LCU.API.Internal.DocumentTest.csproj ./source/LCU.API.Internal.DocumentTest/ +RUN dotnet restore ./source/LCU.API.Internal.DocumentTest/LCU.API.Internal.DocumentTest.csproj +RUN dotnet test -c Release --results-directory /testresults --logger "trx;LogFileName=test_results.trx" ./source/LCU.API.Internal.DocumentTest/LCU.API.Internal.DocumentTest.csproj + +# .NET Core Publish +RUN dotnet publish ./source/LCU.API.Internal.Document/LCU.API.Internal.Document.csproj -c Release -o /dist + +# ASP.NET Core +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.20 +RUN apk add --no-cache icu-libs && apk add --no-cache tzdata; +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false +ENV ASPNETCORE_ENVIRONMENT=Development + +WORKDIR /app +COPY --from=aspnetcore-build /dist . +ENTRYPOINT ["dotnet", "LCU.API.Internal.Document.dll"] diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/API/ApiFactory.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/API/ApiFactory.cs new file mode 100644 index 0000000..039c4f4 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/API/ApiFactory.cs @@ -0,0 +1,11 @@ +namespace LCU.API.Internal.Document.PerformanceTest.API; + +public static class ApiFactory +{ + public static IDocumentApi Api => Environment.Type switch + { + EnvironmentType.Local => new DirectInMemoryDocumentApi(), + EnvironmentType.Azure => new DirectInMemoryDocumentApi(), + _ => throw new NotImplementedException() + }; +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/API/DirectInMemoryDocumentApi.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/API/DirectInMemoryDocumentApi.cs new file mode 100644 index 0000000..1013688 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/API/DirectInMemoryDocumentApi.cs @@ -0,0 +1,49 @@ +using LCU.API.Internal.Document.Configurations; +using LCU.API.Internal.Document.Controllers; +using LCU.API.Internal.Document.PerformanceTest.DB; +using LCU.API.Internal.DocumentLib.Configurations; +using LCU.API.Internal.DocumentLib.DataAccess; +using LCU.Library.Internal.DocumentModels.Requests.Search; +using LCU.Library.Internal.DocumentModels.Responses; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Newtonsoft.Json; + +namespace LCU.API.Internal.Document.PerformanceTest.API; + +public class DirectInMemoryDocumentApi : IDocumentApi +{ + private IServiceProvider? _services; + + public Task SpinUp(DatabaseConfiguration databaseConfiguration) + { + if (_services is not null) return Task.CompletedTask; + + var services = new ServiceCollection(); + ServiceRegistrations.AddDocumentServices(services); + ServiceRegistrations.AddDocumentDataAccesses(services); + ServiceRegistrations.AddDocumentRepositories(services); + services.AddTransient(); + services.TryAddSingleton(databaseConfiguration); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + _services = services.BuildServiceProvider(); + + return Task.CompletedTask; + } + + public async Task> GetDocumentInformationByFilter(DocumentInformationFilter filter) + { + var controller = _services?.GetRequiredService() ?? throw new InvalidOperationException(); + var response = await controller.PostDocumentInformationByFilter(new DocumentInformationFilteredSearch{ Filter = filter }); + + return (response as OkObjectResult)?.Value as IList ?? throw new InvalidOperationException(); + } + + public Task TearDown() + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/API/IDocumentApi.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/API/IDocumentApi.cs new file mode 100644 index 0000000..4730994 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/API/IDocumentApi.cs @@ -0,0 +1,12 @@ +using LCU.API.Internal.DocumentLib.Configurations; +using LCU.Library.Internal.DocumentModels.Requests.Search; +using LCU.Library.Internal.DocumentModels.Responses; + +namespace LCU.API.Internal.Document.PerformanceTest.API; + +public interface IDocumentApi +{ + Task SpinUp(DatabaseConfiguration databaseConfiguration); + Task> GetDocumentInformationByFilter(DocumentInformationFilter filter); + Task TearDown(); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/AzureCosmosDb.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/AzureCosmosDb.cs new file mode 100644 index 0000000..50e2190 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/AzureCosmosDb.cs @@ -0,0 +1,99 @@ +using Azure; +using Azure.Core; +using Azure.Identity; +using Azure.ResourceManager; +using Azure.ResourceManager.CosmosDB; +using Azure.ResourceManager.CosmosDB.Models; +using Azure.ResourceManager.Resources; +using MongoDB.Driver; + +namespace LCU.API.Internal.Document.PerformanceTest.DB; + +public class AzureCosmosDb : IDatabase +{ + private readonly string _performanceTestingResourceName = "Perf_MongoDb"; + + public async Task SpinUp() + { + Console.WriteLine("Currently in Azure Spin Up."); + //todo test this - cd: I'm not connected to the landmark subscription in the portal, only AD + var azureClient = new ArmClient(new DefaultAzureCredential()); + var subscription = await azureClient.GetDefaultSubscriptionAsync(); + var resourceGroup = ( + await subscription + .GetResourceGroups() + .CreateOrUpdateAsync + ( + WaitUntil.Completed, + _performanceTestingResourceName, + new ResourceGroupData(AzureLocation.CentralUS) + ) + ) + .Value; + + var cosmosAccount = ( + await resourceGroup + .GetCosmosDBAccounts() + .CreateOrUpdateAsync + ( + WaitUntil.Completed, + _performanceTestingResourceName.ToLower().Replace("_",""), + new CosmosDBAccountCreateOrUpdateContent + ( + AzureLocation.CentralUS, + new CosmosDBAccountLocation[] { + new CosmosDBAccountLocation { + LocationName = AzureLocation.CentralUS, + FailoverPriority = 0, + IsZoneRedundant = false + }, + new CosmosDBAccountLocation { + LocationName = AzureLocation.EastUS2, + FailoverPriority = 1, + IsZoneRedundant = false + } + } + ) + { Kind = "MongoDB"} + ) + ) + .Value; + + var mongoDb = cosmosAccount.GetMongoDBDatabases(); + + await mongoDb + .CreateOrUpdateAsync + ( + WaitUntil.Completed, + _performanceTestingResourceName, + new MongoDBDatabaseCreateOrUpdateContent + ( + AzureLocation.CentralUS, + new MongoDBDatabaseResourceInfo(_performanceTestingResourceName) + ) + ); + + //todo test to ensure that there's only one connection string or that it can be determined which is the one we need for mongo + var allConnections = cosmosAccount + .GetConnectionStrings() + .AsPages() + .ToArray() + .SelectMany(p => p.Values) + .ToArray(); + if (allConnections.Length == 0) throw new Exception("No Connection Strings Found."); + _connectionString = cosmosAccount.GetConnectionStrings().AsPages().ToArray()[0].Values[0].ConnectionString; + + return new MongoClient(ConnectionString); + } + + public async Task TearDown() + { + var azureClient = new ArmClient(new DefaultAzureCredential()); + var subscription = await azureClient.GetDefaultSubscriptionAsync(); + var resourceGroup = (await subscription.GetResourceGroupAsync(_performanceTestingResourceName)).Value; + await resourceGroup.DeleteAsync(WaitUntil.Completed); + } + + public string ConnectionString => _connectionString ?? throw new InvalidOperationException("spin up the database first"); + private string? _connectionString; +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/DatabaseFactory.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/DatabaseFactory.cs new file mode 100644 index 0000000..ef0c1d1 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/DatabaseFactory.cs @@ -0,0 +1,11 @@ +namespace LCU.API.Internal.Document.PerformanceTest.DB; + +public static class DatabaseFactory +{ + public static IDatabase Database => Environment.Type switch + { + EnvironmentType.Local => new LocalMongoDatabase(), + EnvironmentType.Azure => new AzureCosmosDb(), + _ => throw new NotImplementedException() + }; +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/IDatabase.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/IDatabase.cs new file mode 100644 index 0000000..f233351 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/IDatabase.cs @@ -0,0 +1,10 @@ +using MongoDB.Driver; + +namespace LCU.API.Internal.Document.PerformanceTest.DB; + +public interface IDatabase +{ + Task SpinUp(); + Task TearDown(); + string ConnectionString { get; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/LocalMongoDatabase.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/LocalMongoDatabase.cs new file mode 100644 index 0000000..8f7bca3 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/LocalMongoDatabase.cs @@ -0,0 +1,26 @@ +using Mongo2Go; +using MongoDB.Driver; + +namespace LCU.API.Internal.Document.PerformanceTest.DB; + +public class LocalMongoDatabase : IDatabase +{ + private MongoDbRunner? _mongoDb; + + public Task SpinUp() + { + _mongoDb ??= MongoDbRunner.Start(); + + return Task.FromResult(new MongoClient(ConnectionString)); + } + + public Task TearDown() + { + _mongoDb?.Dispose(); + _mongoDb = null; + + return Task.CompletedTask; + } + + public string ConnectionString => _mongoDb?.ConnectionString ?? throw new InvalidOperationException("spin up the database first"); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/NullBlobStorageClient.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/NullBlobStorageClient.cs new file mode 100644 index 0000000..c1c7233 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/DB/NullBlobStorageClient.cs @@ -0,0 +1,9 @@ +using Azure.Storage.Blobs; +using LCU.API.Internal.DocumentLib.DataAccess; + +namespace LCU.API.Internal.Document.PerformanceTest.DB; + +public class NullBlobStorageClient : IBlobStorageClient +{ + public BlobContainerClient GetClient() => null!; +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/Environment.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/Environment.cs new file mode 100644 index 0000000..a11053f --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/Environment.cs @@ -0,0 +1,12 @@ +namespace LCU.API.Internal.Document.PerformanceTest; + +public enum EnvironmentType +{ + Local, + Azure +} + +public static class Environment +{ + public static EnvironmentType Type { get; set; } = EnvironmentType.Local; +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/GetDocumentInformationByFilterLoadTest.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/GetDocumentInformationByFilterLoadTest.cs new file mode 100644 index 0000000..6c22d5f --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/GetDocumentInformationByFilterLoadTest.cs @@ -0,0 +1,120 @@ +using BenchmarkDotNet.Attributes; +using LCU.API.Internal.Document.PerformanceTest.API; +using LCU.API.Internal.Document.PerformanceTest.DB; +using LCU.API.Internal.DocumentLib.Configurations; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.Library.Internal.DocumentModels.Requests.Search; +using MongoDB.Driver; + +namespace LCU.API.Internal.Document.PerformanceTest; + +[RPlotExporter] +public class GetDocumentInformationByFilterLoadTest +{ + static GetDocumentInformationByFilterLoadTest() + { + Environment.Type = EnvironmentType.Local; + } + + //[Params(1, 10, 100, 1000, 10000, 100000, 250000, 500000, 750000, 1000000, 2000000)] + [Params(1, 10, 100, 1000, 10000, 100000, 250000, 500000, 750000, 1000000)] + public int DocumentCount { get; set; } + + private DocumentInformation _document = null!; + + private readonly DatabaseConfiguration _config; + private readonly IDatabase _database = DatabaseFactory.Database; + private readonly IDocumentApi _api = ApiFactory.Api; + + private IMongoClient _client = null!; + + public GetDocumentInformationByFilterLoadTest() + { + _config = new DatabaseConfiguration + { + DatabaseName = $"{DocumentCount}Documents", + DocumentContainerName = "Documents", + AuditContainerName = "Audits" + }; + } + + [GlobalSetup] + public void GlobalSetup() + { + _client = _database.SpinUp().Result; + _config.ConnectionString = _database.ConnectionString; + + var db = _client.GetDatabase(_config.DatabaseName); + db.CreateCollection(_config.DocumentContainerName); + db.CreateCollection(_config.AuditContainerName); + var documentCollection = db.GetCollection(_config.DocumentContainerName); + + Task.WaitAll + ( + _api.SpinUp(_config), + documentCollection.InsertManyAsync(Enumerable.Repeat(() => DocumentFactory(), DocumentCount).Select(f => f())) + ); + } + + [IterationSetup] + public void Setup() + { + _document = DocumentFactory(); + + _client.GetDatabase(_config.DatabaseName) + .GetCollection(_config.DocumentContainerName) + .InsertOne(_document); + } + + [Benchmark] + public void GetDocumentInformationByFilterRun() + { + var oneThirdAttributes = _document.Attributes.Count / 3; + var filter = new DocumentInformationFilter + { + AttributesContains = _document.Attributes.ToArray()[..oneThirdAttributes].ToDictionary(p => p.Key, p => p.Value), + AttributesContainsAny = _document.Attributes.ToArray()[oneThirdAttributes..^oneThirdAttributes].ToDictionary(p => p.Key, p => p.Value), + AttributesEquals = _document.Attributes.ToArray()[^oneThirdAttributes..].ToDictionary(p => p.Key, p => p.Value), + DataSource = _document.DataSource, + Name = _document.Name, + Type = _document.Type, + Source = _document.Source, + ScanStatus = _document.ScanStatus, + }; + + _api.GetDocumentInformationByFilter(filter).Wait(); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + _client.DropDatabase(_config.DatabaseName); + + var dbDestruction = _database.TearDown(); + var apiDestruction = _api.TearDown(); + + Task.WaitAll(dbDestruction, apiDestruction); + } + + private static readonly Random Random = new Random(DateTime.Now.Millisecond); + private static DocumentInformation DocumentFactory() => new() + { + Name = Guid.NewGuid().ToString(), + Type = Random.Next().ToString(), + Source = Random.Next().ToString(), + DataSource = Random.Next().ToString(), + FileSize = Random.Next(), + ScanStatus = Random.Next() % 2 == 0, + UploadDate = DateTime.Now.AddDays(-Random.Next(1, 10)), + LastModifiedDate = DateTime.Now.AddDays(-Random.Next(1, 10)), + CreatedDate = DateTime.Now.AddDays(-Random.Next(1, 10)), + Attributes = Enumerable + .Repeat + ( + () => new KeyValuePair(Guid.NewGuid().ToString(), Random.Next().ToString()), + Random.Next(10, 100) + ) + .Select(f => f()) + .ToDictionary(p => p.Key, p => p.Value) + }; +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/IndexingStrategyPerformanceTest.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/IndexingStrategyPerformanceTest.cs new file mode 100644 index 0000000..fdf3c1c --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/IndexingStrategyPerformanceTest.cs @@ -0,0 +1,130 @@ +using System.Configuration; +using Azure.Identity; +using BenchmarkDotNet.Attributes; +using LCU.API.Internal.Document.PerformanceTest.API; +using LCU.API.Internal.DocumentLib.Configurations; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.Library.Internal.DocumentModels.Requests.Search; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace LCU.API.Internal.Document.PerformanceTest; + +public enum Indexing +{ + Default, + FieldSpecific, + Wildcard +} + +//todo it would be great to get the RU cost output +[RPlotExporter] +[ShortRunJob] +public class IndexingStrategyPerformanceTest +{ + [Params(Indexing.Default, Indexing.FieldSpecific, Indexing.Wildcard)] + public Indexing Strategy { get; set; } + + private readonly IDocumentApi _api = ApiFactory.Api; + private readonly DatabaseConfiguration _config = new(); + + public IndexingStrategyPerformanceTest() + { + var configBuilder = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .AddJsonFile("appsettings.Development.json"); + var baseConfig = configBuilder.Build(); + + var clientSecretCredentials = new ClientSecretCredential + ( + baseConfig["TenantId"], + baseConfig["ClientId"], + baseConfig["ClientSecret"] + ); + var vaultLocation = new Uri($"https://{baseConfig["AzureKeyVaultUrl"]}.vault.azure.net/"); + configBuilder + .AddAzureKeyVault(vaultLocation, clientSecretCredentials) + .Build() + .Bind("MongoDatabase", _config); + } + + [GlobalSetup] + public void GlobalSetup() + { + switch (Strategy) + { + case Indexing.Default: + break; + case Indexing.FieldSpecific: + new MongoClient(_config.ConnectionString) + .GetDatabase(_config.DatabaseName) + .GetCollection(_config.DocumentContainerName) + .Indexes + .CreateOne + ( + new CreateIndexModel + ( + Builders.IndexKeys.Ascending(d => d.Name), + new CreateIndexOptions { Name = nameof(DocumentInformation.Name) } + ) + ); + break; + case Indexing.Wildcard: + new MongoClient(_config.ConnectionString) + .GetDatabase(_config.DatabaseName) + .GetCollection(_config.DocumentContainerName) + .Indexes + .CreateOne + ( + new CreateIndexModel + ( + Builders.IndexKeys.Wildcard(), + new CreateIndexOptions { Name = "wildcard" } + ) + ); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + _api.SpinUp(_config).Wait(); + } + + [Benchmark] + //todo report the RU cost in the benchmark + public async Task DocumentSearch_ByName() + { + var filter = new DocumentInformationFilter + { + Name = "5498-SA" + }; + + await _api.GetDocumentInformationByFilter(filter); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + switch (Strategy) + { + case Indexing.Default: + break; + case Indexing.FieldSpecific: + new MongoClient(_config.ConnectionString) + .GetDatabase(_config.DatabaseName) + .GetCollection(_config.DocumentContainerName) + .Indexes + .DropOne(nameof(DocumentInformation.Name)); + break; + case Indexing.Wildcard: + new MongoClient(_config.ConnectionString) + .GetDatabase(_config.DatabaseName) + .GetCollection(_config.DocumentContainerName) + .Indexes + .DropOne("wildcard"); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/LCU.API.Internal.Document.PerformanceTest.csproj b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/LCU.API.Internal.Document.PerformanceTest.csproj new file mode 100644 index 0000000..cf8879e --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/LCU.API.Internal.Document.PerformanceTest.csproj @@ -0,0 +1,29 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + PreserveNewest + + + + + + + + + + diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/Program.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/Program.cs new file mode 100644 index 0000000..d5c6277 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/Program.cs @@ -0,0 +1,5 @@ +using BenchmarkDotNet.Running; +using LCU.API.Internal.Document.PerformanceTest; + +//BenchmarkRunner.Run(); +BenchmarkRunner.Run(); \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/ResultPlots/2022.09-28-LCU.API.Internal.Document.PerformanceTest.IndexingStrategyPerformanceTest-boxplot.png b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/ResultPlots/2022.09-28-LCU.API.Internal.Document.PerformanceTest.IndexingStrategyPerformanceTest-boxplot.png new file mode 100644 index 0000000..c14dc2c Binary files /dev/null and b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/ResultPlots/2022.09-28-LCU.API.Internal.Document.PerformanceTest.IndexingStrategyPerformanceTest-boxplot.png differ diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/ResultPlots/2022.09.12-localmongodb-newregex-GetDocumentInformationByFilterLoadTest-boxplot.png b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/ResultPlots/2022.09.12-localmongodb-newregex-GetDocumentInformationByFilterLoadTest-boxplot.png new file mode 100644 index 0000000..80af050 Binary files /dev/null and b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/ResultPlots/2022.09.12-localmongodb-newregex-GetDocumentInformationByFilterLoadTest-boxplot.png differ diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/ResultPlots/2022.09.12-localmongodb-oldregex-GetDocumentInformationByFilterLoadTest-boxplot.png b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/ResultPlots/2022.09.12-localmongodb-oldregex-GetDocumentInformationByFilterLoadTest-boxplot.png new file mode 100644 index 0000000..6dd8cf9 Binary files /dev/null and b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/ResultPlots/2022.09.12-localmongodb-oldregex-GetDocumentInformationByFilterLoadTest-boxplot.png differ diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/readme.md b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/readme.md new file mode 100644 index 0000000..5a2cd8a --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.PerformanceTest/readme.md @@ -0,0 +1,122 @@ +# Running Benchmarks + +`dotnet run -c Release` + +## past results + +### 2022.09.09 - local mongodb - "old" regex matching +note: default mongodb indexing is not the same as CosmosDB Mongo default indexing +``` ini + +BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19042.1826/20H2/October2020Update) +11th Gen Intel Core i5-1145G7 2.60GHz, 1 CPU, 8 logical and 4 physical cores +.NET SDK=6.0.400 + [Host] : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2 + Job-HPFGMU : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2 + +InvocationCount=1 UnrollFactor=1 + +``` +| Method | DocumentCount | Mean | Error | StdDev | +|---------------------------------- |-------------- |--------------:|--------------:|--------------:| +| **GetDocumentInformationByFilterRun** | **1** | **3.756 ms** | **0.6389 ms** | **1.874 ms** | +| **GetDocumentInformationByFilterRun** | **10** | **3.868 ms** | **0.6174 ms** | **1.820 ms** | +| **GetDocumentInformationByFilterRun** | **100** | **8.027 ms** | **1.0094 ms** | **2.976 ms** | +| **GetDocumentInformationByFilterRun** | **1000** | **41.938 ms** | **7.9785 ms** | **23.525 ms** | +| **GetDocumentInformationByFilterRun** | **10000** | **337.205 ms** | **51.8841 ms** | **152.167 ms** | +| **GetDocumentInformationByFilterRun** | **100000** | **3,429.605 ms** | **632.3863 ms** | **1,864.605 ms** | +| **GetDocumentInformationByFilterRun** | **250000** | **7,896.811 ms** | **1,431.8274 ms** | **4,221.775 ms** | +| **GetDocumentInformationByFilterRun** | **500000** | **16,629.300 ms** | **2,749.5569 ms** | **8,107.131 ms** | +| **GetDocumentInformationByFilterRun** | **750000** | **23,883.445 ms** | **4,083.3474 ms** | **11,911.316 ms** | +| **GetDocumentInformationByFilterRun** | **1000000** | **32,725.371 ms** | **5,751.8744 ms** | **16,778.487 ms** | +![](./ResultPlots/2022.09.12-localmongodb-oldregex-GetDocumentInformationByFilterLoadTest-boxplot.png =500x) + +### 2022.09.12 - local mongodb - "new" regex matching +``` ini + +BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19042.1826/20H2/October2020Update) +11th Gen Intel Core i5-1145G7 2.60GHz, 1 CPU, 8 logical and 4 physical cores +.NET SDK=6.0.400 + [Host] : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2 + Job-ZGOEWK : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2 + +InvocationCount=1 UnrollFactor=1 + +``` +| Method | DocumentCount | Mean | Error | StdDev | Median | +|---------------------------------- |-------------- |--------------:|--------------:|--------------:|--------------:| +| **GetDocumentInformationByFilterRun** | **1** | **3.924 ms** | **0.6009 ms** | **1.724 ms** | **3.724 ms** | +| **GetDocumentInformationByFilterRun** | **10** | **3.675 ms** | **0.6997 ms** | **2.063 ms** | **3.096 ms** | +| **GetDocumentInformationByFilterRun** | **100** | **5.679 ms** | **0.8358 ms** | **2.464 ms** | **5.332 ms** | +| **GetDocumentInformationByFilterRun** | **1000** | **35.936 ms** | **5.6629 ms** | **16.697 ms** | **37.860 ms** | +| **GetDocumentInformationByFilterRun** | **10000** | **305.437 ms** | **53.4368 ms** | **157.560 ms** | **291.339 ms** | +| **GetDocumentInformationByFilterRun** | **100000** | **2,946.267 ms** | **483.7081 ms** | **1,418.632 ms** | **2,793.991 ms** | +| **GetDocumentInformationByFilterRun** | **250000** | **8,456.390 ms** | **1,315.9931 ms** | **3,796.941 ms** | **8,575.654 ms** | +| **GetDocumentInformationByFilterRun** | **500000** | **15,775.558 ms** | **2,830.8954 ms** | **8,167.781 ms** | **15,527.663 ms** | +| **GetDocumentInformationByFilterRun** | **750000** | **24,948.355 ms** | **3,861.0450 ms** | **11,323.777 ms** | **25,630.684 ms** | +| **GetDocumentInformationByFilterRun** | **1000000** | **30,470.661 ms** | **5,342.0572 ms** | **15,583.032 ms** | **29,073.994 ms** | +![](./ResultPlots/2022.09.12-localmongodb-newregex-GetDocumentInformationByFilterLoadTest-boxplot.png =500x) + +### 2022.09.28 - indexing strategies +``` ini + +BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19042.1826/20H2/October2020Update) +11th Gen Intel Core i5-1145G7 2.60GHz, 1 CPU, 8 logical and 4 physical cores +.NET SDK=6.0.400 + [Host] : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2 + ShortRun : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2 + +Job=ShortRun IterationCount=3 LaunchCount=1 +WarmupCount=3 + +``` +| Method | Strategy | Mean | Error | StdDev | +|---------------------- |-------------- |----------:|---------:|---------:| +| **DocumentSearch_ByName** | **Default** | **171.51 ms** | **68.40 ms** | **3.749 ms** | +| **DocumentSearch_ByName** | **FieldSpecific** | **91.36 ms** | **29.19 ms** | **1.600 ms** | +| **DocumentSearch_ByName** | **Wildcard** | **94.74 ms** | **78.58 ms** | **4.307 ms** | +![](/ResultPlots/2022.09-28-LCU.API.Internal.Document.PerformanceTest.IndexingStrategyPerformanceTest-boxplot.png) + +## required tools +- dotnet 6+ +-- `choco install dotnet` +-- probably installed with visual studio +- R +-- `choco install r.project` then add "C:\Program Files\R\R-4.2.1\bin" to your path, assuming you used the default install directory and the version is 4.2.1 +--- better to manually add to the path because setx will only give you 1024 characters. Your path is likely already longer than that and will get truncated. +-- this is for outputting visual graphs, not just text tables into standard out + +## output +- written to the console +- `.\BenchmarkDotNet.Artifacts` relative to where ever you ran the code from + +### graphs +The plots should be produced automatically and you'll find them in the results folder. + +If not you can manually generate them: +- `cd .\BenchmarkDotNet.Artifacts\results` +- `rscript BuildPlots.R` + +## benchmarkdotnet docs +https://benchmarkdotnet.org/ + +## gotchas + +### Debugging +This will be more difficult than you might be accustomed to. By default benchmark dotnet takes control of a lot of parts of the runtime and even rebuilds some parts of the assembly to ensure isolation and consistent optimzation. See their docs for enabling debugging. + +It's possible to run or attach to a release optimized assembly. Some of the things you might expect to be there won't be availabe to the debugger; you're missing debug symbols and the IL has been optimized. + +There's always good old console output and file logging. + +You might also want to try setting up the problematic scenario in a unit test and then debug *that* instead of debugging the benchmark itself. + +### Singletons +Benchmarkdotnet ends up invoking msbuild in order to build separate assemblies (for controlling optimization and isolation). This means that, e.g. `Environment.Type = EnvironmentType.Azure;` in program.cs will not affect the benchmarks when they are run; Environment.Type will be recreated every benchmark because benchmarks are running in a different assembly than the `BenchmarkRunner.Run();` statement which caused the benchmark to run. + +One option to deal with this is setting the option inside the benchmark class e.g. in a static constructor (ensuring the value is set before anything else). There are almost surely other options, perhaps a way to pass arguments (other than `[Params()]`) to the benchmark class. + +### Azure Rate Limiting +Despite having an Azure environment with code to spin it up and tear it down trying to run a benchmark against a real Azure CosmosDB will likely run you into rate limiting issues. Specifically inserting enough documents to produce a notable database load in a short amount of time. As of 2022.09.13 the rate limit is 1000 records per 5 minutes... You can see where this might produce a problem trying to produce a database on the order of hundreds of thousands of records. + +See https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/request-limits-and-throttling#network-throttling for current information. \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document.sln b/LCU.API.Internal.Document/LCU.API.Internal.Document.sln new file mode 100644 index 0000000..1973eb8 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document.sln @@ -0,0 +1,87 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32112.339 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.API.Internal.Document", "LCU.API.Internal.Document\LCU.API.Internal.Document.csproj", "{424A4E9B-D5C8-4ABF-A073-30CCE4759E57}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.API.Internal.DocumentTest", "LCU.API.Internal.DocumentTest\LCU.API.Internal.DocumentTest.csproj", "{AB05AA6E-F86D-45D0-BF61-4536D6EE6495}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.Library.Internal.DocumentModels", "LCU.Library.Internal.DocumentModels\LCU.Library.Internal.DocumentModels.csproj", "{8D9A978B-DEA8-41A7-8F52-BE0A78E08511}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.API.Internal.DocumentLib", "LCU.API.Internal.DocumentLib\LCU.API.Internal.DocumentLib.csproj", "{A4C80FE4-0880-4884-ABDD-88DA6DDA9EC5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.API.Internal.Document.PerformanceTest", "LCU.API.Internal.Document.PerformanceTest\LCU.API.Internal.Document.PerformanceTest.csproj", "{9E11130D-8E7A-4A3A-8128-64B2FDDF214B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Migrations", "Migrations", "{410AA7F0-3C37-4F31-BA3D-2FF85163FB41}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OwnerNumberMigration", "OwnerNumberMigration\OwnerNumberMigration.csproj", "{00B4C125-83E7-4375-9F57-5D5BE17E24B2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OwnerNumberMigrationTest", "OwnerNumberMigrationTest\OwnerNumberMigrationTest.csproj", "{F21E3FE1-4587-4FE7-ACDD-7ABA5E6FA121}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{25142763-0DF7-4898-A4C6-395DAF0456F9}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + azure-pipelines-iis.yml = azure-pipelines-iis.yml + azure-pipelines.yml = azure-pipelines.yml + README.md = README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.API.Internal.DocumentLibTest", "LCU.API.Internal.DocumentLibTest\LCU.API.Internal.DocumentLibTest.csproj", "{7755B890-92CC-4811-861F-6C99ABB210CE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LCU.Library.Internal.DocumentModelsTest", "LCU.Library.Internal.DocumentModelsTest\LCU.Library.Internal.DocumentModelsTest.csproj", "{82F351A4-6C78-493D-A76A-5DF57CAC709D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {424A4E9B-D5C8-4ABF-A073-30CCE4759E57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {424A4E9B-D5C8-4ABF-A073-30CCE4759E57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {424A4E9B-D5C8-4ABF-A073-30CCE4759E57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {424A4E9B-D5C8-4ABF-A073-30CCE4759E57}.Release|Any CPU.Build.0 = Release|Any CPU + {AB05AA6E-F86D-45D0-BF61-4536D6EE6495}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB05AA6E-F86D-45D0-BF61-4536D6EE6495}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB05AA6E-F86D-45D0-BF61-4536D6EE6495}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB05AA6E-F86D-45D0-BF61-4536D6EE6495}.Release|Any CPU.Build.0 = Release|Any CPU + {8D9A978B-DEA8-41A7-8F52-BE0A78E08511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D9A978B-DEA8-41A7-8F52-BE0A78E08511}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D9A978B-DEA8-41A7-8F52-BE0A78E08511}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D9A978B-DEA8-41A7-8F52-BE0A78E08511}.Release|Any CPU.Build.0 = Release|Any CPU + {A4C80FE4-0880-4884-ABDD-88DA6DDA9EC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4C80FE4-0880-4884-ABDD-88DA6DDA9EC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4C80FE4-0880-4884-ABDD-88DA6DDA9EC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4C80FE4-0880-4884-ABDD-88DA6DDA9EC5}.Release|Any CPU.Build.0 = Release|Any CPU + {9E11130D-8E7A-4A3A-8128-64B2FDDF214B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E11130D-8E7A-4A3A-8128-64B2FDDF214B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E11130D-8E7A-4A3A-8128-64B2FDDF214B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E11130D-8E7A-4A3A-8128-64B2FDDF214B}.Release|Any CPU.Build.0 = Release|Any CPU + {00B4C125-83E7-4375-9F57-5D5BE17E24B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00B4C125-83E7-4375-9F57-5D5BE17E24B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00B4C125-83E7-4375-9F57-5D5BE17E24B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00B4C125-83E7-4375-9F57-5D5BE17E24B2}.Release|Any CPU.Build.0 = Release|Any CPU + {F21E3FE1-4587-4FE7-ACDD-7ABA5E6FA121}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F21E3FE1-4587-4FE7-ACDD-7ABA5E6FA121}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F21E3FE1-4587-4FE7-ACDD-7ABA5E6FA121}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F21E3FE1-4587-4FE7-ACDD-7ABA5E6FA121}.Release|Any CPU.Build.0 = Release|Any CPU + {7755B890-92CC-4811-861F-6C99ABB210CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7755B890-92CC-4811-861F-6C99ABB210CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7755B890-92CC-4811-861F-6C99ABB210CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7755B890-92CC-4811-861F-6C99ABB210CE}.Release|Any CPU.Build.0 = Release|Any CPU + {82F351A4-6C78-493D-A76A-5DF57CAC709D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82F351A4-6C78-493D-A76A-5DF57CAC709D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82F351A4-6C78-493D-A76A-5DF57CAC709D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82F351A4-6C78-493D-A76A-5DF57CAC709D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {00B4C125-83E7-4375-9F57-5D5BE17E24B2} = {410AA7F0-3C37-4F31-BA3D-2FF85163FB41} + {F21E3FE1-4587-4FE7-ACDD-7ABA5E6FA121} = {410AA7F0-3C37-4F31-BA3D-2FF85163FB41} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AB145BEF-D8D6-4CB9-BE2C-79F06F523B0D} + EndGlobalSection +EndGlobal diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Configurations/ApiConfiguration.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Configurations/ApiConfiguration.cs new file mode 100644 index 0000000..cc38623 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Configurations/ApiConfiguration.cs @@ -0,0 +1,12 @@ +namespace LCU.API.Internal.Document.Configurations; + +public class ApiConfiguration +{ + public string AppName { get; set; } + public string SeqUrl { get; set; } + public string SeqApiKey { get; set; } + public int FileUploadCutoffHours { get; set; } + public string[] CorsAllowedOrigins { get; set; } + public List MaliciousFileFoundEmailList { get; set; } + public bool IsAksDeploy { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Controllers/DocumentController.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Controllers/DocumentController.cs new file mode 100644 index 0000000..8729e5f --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Controllers/DocumentController.cs @@ -0,0 +1,588 @@ +using LCU.API.Internal.Document.Services; +using LCU.Library.Internal.ApiSecurity.Attributes; +using LCU.Library.Internal.DocumentModels.Requests; +using LCU.Library.Internal.DocumentModels.Requests.Search; +using LCU.Library.Internal.DocumentModels.Responses; +using LCU.Library.Internal.Exceptions; +using Microsoft.AspNetCore.Mvc; +using Serilog; +using Microsoft.AspNetCore.Authorization; + +namespace LCU.API.Internal.Document.Controllers; + +[ApiController] +[Route("[controller]")] +public class DocumentController : ControllerBase +{ + private readonly IDocumentService _documentService; + private readonly IDocumentAuditService _documentAuditService; + private readonly List _allowedExternalActions = new() + { + DocumentAction.Download + }; + + public DocumentController(IDocumentService documentService, + IDocumentAuditService documentAuditService) + { + _documentService = documentService; + _documentAuditService = documentAuditService; + } + + [ApiKey] + [HttpGet("{documentId:guid}")] + [ProducesResponseType(typeof(DocumentResponse), 200)] + [ProducesResponseType(401)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task GetDocumentById(Guid documentId) + { + try + { + var result = await _documentService.GetDocument(documentId); + + if (result == null) + { + return NotFound($"The document with DocumentId:{documentId} could not be found."); + } + + return Ok(result); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [HttpGet("{personOrOrgNumber:long}/{documentId:guid}")] + [ProducesResponseType(typeof(DocumentResponse), 200)] + [ProducesResponseType(401)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task GetMemberDocumentById(long personOrOrgNumber, Guid documentId) + { + try + { + var result = await _documentService.GetDocument(personOrOrgNumber, documentId); + + if (result == null) + { + return NotFound($"The document with DocumentId:{documentId} could not be found."); + } + + return Ok(result); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [Obsolete] + [HttpGet("GetDocumentByBlobId/{documentBlobId:guid}")] + [ProducesResponseType(typeof(byte[]), 200)] + [ProducesResponseType(401)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task GetDocumentByBlobId(Guid documentBlobId) + { + try + { + var result = await _documentService.GetDocumentByBlobId(documentBlobId); + + if (result == null) + { + return NotFound($"The document with DocumentBlobId:{documentBlobId} could not be found."); + } + + return Ok(result); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [HttpGet("Content/{documentBlobId:guid}")] + [ProducesResponseType(typeof(byte[]), 200)] + [ProducesResponseType(401)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task GetDocumentContentByBlobId(Guid documentBlobId) + { + try + { + var result = await _documentService.GetDocumentByBlobId(documentBlobId); + + if (result == null) + { + return NotFound($"The document with DocumentBlobId:{documentBlobId} could not be found."); + } + + return Ok(result); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [HttpGet("{personOrOrgNumber:long}/Content/{documentBlobId:guid}")] + [ProducesResponseType(typeof(byte[]), 200)] + [ProducesResponseType(401)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task GetMemberDocumentContentByBlobId(long personOrOrgNumber, Guid documentBlobId) + { + try + { + var result = await _documentService.GetDocumentByBlobId(personOrOrgNumber, documentBlobId); + + if (result == null) + { + return NotFound($"The document with DocumentBlobId:{documentBlobId} could not be found."); + } + + return Ok(result); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [HttpPost("Search")] + [ProducesResponseType(typeof(List),200)] + [ProducesResponseType(204)] + [ProducesResponseType(401)] + [ProducesResponseType(500)] + public async Task PostDocumentInformationByFilter([FromBody] DocumentInformationFilteredSearch documentInformationFilteredSearch) + { + try + { + var result = await _documentService.SearchDocumentInformation(documentInformationFilteredSearch.Filter, documentInformationFilteredSearch.Skip, documentInformationFilteredSearch.Take, documentInformationFilteredSearch.SortDefinition); + + if (!result.Any()) + { + return NoContent(); + } + + return Ok(result); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [HttpGet("AuditInformation/{documentId:guid}")] + [ProducesResponseType(typeof(List),200)] + [ProducesResponseType(204)] + [ProducesResponseType(401)] + [ProducesResponseType(500)] + public async Task GetDocumentAuditInformationById(Guid documentId) + { + try + { + var result = await _documentAuditService.GetDocumentAudits(documentId); + + if (!result.Any()) + { + return NoContent(); + } + + return Ok(result); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [HttpGet("AuditInformation")] + [ProducesResponseType(typeof(List), 200)] + [ProducesResponseType(204)] + [ProducesResponseType(401)] + [ProducesResponseType(500)] + public async Task GetDocumentAuditInformationByIds([FromQuery]Guid[] documentIds) + { + try + { + var result = await _documentAuditService.GetDocumentAudits(documentIds); + + return Ok(result); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [Authorize] + [HttpPost("{documentId:guid}/Actions")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task ExternalActionPerformed([FromRoute]Guid documentId, [FromBody] DocumentActionNotificationRequest action) + { + try + { + if (!_allowedExternalActions.Contains(action.Action)) + { + throw new BadHttpRequestException($"{action.Action.ToString()} action type not allowed externally."); + } + + await _documentAuditService.AuditAction(documentId, + HttpContext.User.Claims.Single(c => c.Type == "name").Value, action.Source, action.Action); + + return Ok(); + } + catch (BadHttpRequestException ex) + { + return StatusCode(StatusCodes.Status400BadRequest, ex.Message); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [HttpPost] + [ProducesResponseType(201)] + [ProducesResponseType(400)] + [ProducesResponseType(401)] + [ProducesResponseType(500)] + public async Task UploadDocument([FromBody] DocumentRequest request) + { + try + { + var result = await _documentService.UploadDocument(request); + return CreatedAtAction(nameof(GetDocumentById), new { documentId = result.DocumentId }, result); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [Authorize] + [HttpPut("{documentId:guid}")] + [ProducesResponseType(typeof(DocumentInformationResponse),200)] + [ProducesResponseType(400)] + [ProducesResponseType(401)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task UpdateDocument([FromBody] UpdateDocumentRequest request, Guid documentId) + { + try + { + var result = await _documentService.UpdateDocument(request, documentId, HttpContext.User.Claims.Single(c => c.Type == "name").Value); + + if (result == null) + { + return NotFound($"The document with DocumentId:{documentId} could not be found."); + } + + return Ok(result); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [Authorize] + [HttpPut("DocumentContent/{documentId:guid}")] + [ProducesResponseType(typeof(DocumentInformationResponse),200)] + [ProducesResponseType(400)] + [ProducesResponseType(401)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task UpdateDocumentContent([FromBody] UpdateDocumentContentRequest request, Guid documentId) + { + try + { + var result = await _documentService.UpdateDocumentContent(request, documentId, HttpContext.User.Claims.Single(c => c.Type == "name").Value); + + if (result == null) + { + return NotFound($"The document with DocumentId:{documentId} could not be found."); + } + + return Ok(result); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [Authorize] + [HttpPut("DocumentInformation/{documentId:guid}")] + [ProducesResponseType(typeof(DocumentInformationResponse),200)] + [ProducesResponseType(400)] + [ProducesResponseType(401)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task UpdateDocumentInformation([FromBody] UpdateDocumentInformationRequest request, + Guid documentId) + { + try + { + var result = await _documentService.UpdateDocumentInformation(request, documentId, HttpContext.User.Claims.Single(c => c.Type == "name").Value); + + if (result == null) + { + return NotFound($"The document with DocumentId:{documentId} could not be found."); + } + + return Ok(result); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [HttpGet("DocumentInformation/{documentId:guid}")] + [ProducesResponseType(typeof(DocumentInformationResponse),200)] + [ProducesResponseType(401)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task GetDocumentInformation(Guid documentId) + { + try + { + var result = await _documentService.GetDocumentInformation(documentId); + + if (result == null) + { + return NotFound($"The document with DocumentId:{documentId} could not be found."); + } + + return Ok(result); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [Authorize] + [HttpPut("DocumentScanStatus")] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + [ProducesResponseType(401)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task UpdateDocumentScanStatus([FromBody] UpdateDocumentScanStatusRequest request) + { + try + { + await _documentService.UpdateDocumentScanStatus(request.ScanStatus, request.DocumentBlobIds, HttpContext.User.Claims.Single(c => c.Type == "name").Value); + return Ok(); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + /// + /// This is currently a developer endpoint, still waiting on guidance on how the 'Delete' Operation + /// should work. This will remove both the documents contents from blob storage and its information from cosmos. + /// + /// + /// + [ApiKey] + [HttpDelete("{documentId:guid}")] + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(401)] + [ProducesResponseType(500)] + public async Task DeleteDocument(Guid documentId) + { + try + { + await _documentService.DeleteDocument(documentId); + return Ok(); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [HttpDelete("DeleteDocumentContent")] + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(401)] + [ProducesResponseType(500)] + public async Task DeleteDocumentContent([FromBody] DeleteDocumentContentRequest request) + { + try + { + await _documentService.DeleteDocumentContent(request); + return Ok(); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception exception) + { + Log.Error(exception, $"Unexpected Exception in {GetType().Name}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [ApiKey] + [HttpPost("Count")] + [ProducesResponseType(typeof(long), 200)] + [ProducesResponseType(401)] + [ProducesResponseType(500)] + public async Task CountDocuments(DocumentInformationFilter filter) + { + try + { + var count = await _documentService.CountDocuments(filter); + + return Ok(count); + + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError); + } + catch (Exception exception) + { + Log.Error(exception, "Unexpected Exception"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + + [ApiKey] + [HttpPost("GetDocumentsForCleanup")] + [ProducesResponseType(typeof(IEnumerable), 200)] + [ProducesResponseType(204)] + [ProducesResponseType(401)] + [ProducesResponseType(500)] + public async Task GetDocumentsForCleanup([FromBody] GetDocumentCleanupRequest request) + { + try + { + var documents = await _documentService.GetDocumentsForCleanup(request); + + if (!documents.Any()) + { + return NoContent(); + } + + return Ok(documents); + } + catch (ServiceException ex) + { + return StatusCode(StatusCodes.Status500InternalServerError); + } + catch (Exception exception) + { + Log.Error(exception, "Unexpected Exception"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } +} diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Controllers/StatusController.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Controllers/StatusController.cs new file mode 100644 index 0000000..41ec831 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Controllers/StatusController.cs @@ -0,0 +1,36 @@ +using System.Net; +using LCU.API.Internal.Document.Models; +using Microsoft.AspNetCore.Mvc; +using Serilog; +using LCU.Library.Internal.ApiSecurity.Attributes; + +namespace LCU.API.Internal.Document.Controllers; + +[Route("[controller]")] +[ApiController] +public class StatusController : ControllerBase +{ + private readonly IEnvironmentView _environmentView; + + public StatusController(IEnvironmentView environmentView) + { + _environmentView = environmentView; + } + + [HttpGet] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public IActionResult Get() + { + try + { + Log.Verbose($"{_environmentView.Application}'s Status Endpoint Was Hit"); + return Ok(_environmentView); + } + catch (Exception ex) + { + Log.Error(ex, "Failure to return status"); + return StatusCode((int)HttpStatusCode.InternalServerError); + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Exceptions/BlobApiServiceException.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Exceptions/BlobApiServiceException.cs new file mode 100644 index 0000000..fe289f7 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Exceptions/BlobApiServiceException.cs @@ -0,0 +1,12 @@ +namespace LCU.API.Internal.Document.Exceptions; + +public class BlobApiServiceException : Exception +{ + public BlobApiServiceException(string message) : base(message) + { + } + + public BlobApiServiceException(string message, Exception exception) : base(message, exception) + { + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Exceptions/DocumentAuditServiceException.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Exceptions/DocumentAuditServiceException.cs new file mode 100644 index 0000000..8b0877b --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Exceptions/DocumentAuditServiceException.cs @@ -0,0 +1,12 @@ +namespace LCU.API.Internal.Document.Exceptions; + +public class DocumentAuditServiceException : Exception +{ + public DocumentAuditServiceException(string message) : base(message) + { + } + + public DocumentAuditServiceException(string message, Exception exception) : base(message, exception) + { + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Exceptions/DocumentInformationServiceException.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Exceptions/DocumentInformationServiceException.cs new file mode 100644 index 0000000..65a5926 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Exceptions/DocumentInformationServiceException.cs @@ -0,0 +1,12 @@ +namespace LCU.API.Internal.Document.Exceptions; + +public class DocumentInformationServiceException : Exception +{ + public DocumentInformationServiceException(string message) : base(message) + { + } + + public DocumentInformationServiceException(string message, Exception exception) : base(message, exception) + { + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/LCU.API.Internal.Document.csproj b/LCU.API.Internal.Document/LCU.API.Internal.Document/LCU.API.Internal.Document.csproj new file mode 100644 index 0000000..bb6a5b6 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/LCU.API.Internal.Document.csproj @@ -0,0 +1,41 @@ + + + + net8.0 + enable + enable + + + + 1701;1702; 8618; 1591 + + + + 1701;1702; 8618; 1591 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Models/EnvironmentView.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Models/EnvironmentView.cs new file mode 100644 index 0000000..94d36b0 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Models/EnvironmentView.cs @@ -0,0 +1,35 @@ +using System.Reflection; + +namespace LCU.API.Internal.Document.Models; + +public class EnvironmentView : IEnvironmentView +{ + private readonly IWebHostEnvironment _env; + + public EnvironmentView(IWebHostEnvironment env) + { + _env = env; + } + + public string Application => _env.ApplicationName; + public DateTime CurrentDate => DateTime.Now; + public string BuildVersion => AssembleBuildVersion(); + public DateTime BuildDate => AssemblyBuildDate(); + public string Environment => _env.EnvironmentName; + + private static DateTime AssemblyBuildDate() + { + //https://stackoverflow.com/a/804310/266594 + var assembly = Assembly.GetExecutingAssembly(); + var fileInfo = new FileInfo(assembly.Location); + var lastModified = fileInfo.LastWriteTime; + + return lastModified; + } + + private static string AssembleBuildVersion() + { + var version = Assembly.GetExecutingAssembly().GetName().Version; + return version == null ? "-.-.-" : version.ToString(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Models/IEnvironmentView.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Models/IEnvironmentView.cs new file mode 100644 index 0000000..952b592 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Models/IEnvironmentView.cs @@ -0,0 +1,10 @@ +namespace LCU.API.Internal.Document.Models; + +public interface IEnvironmentView +{ + string Application { get; } + DateTime CurrentDate { get; } + string BuildVersion { get; } + DateTime BuildDate { get; } + string Environment { get; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Program.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Program.cs new file mode 100644 index 0000000..00b6229 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Program.cs @@ -0,0 +1,263 @@ +using System.Text.Json.Serialization; +using Azure.Identity; +using LCU.API.Internal.Document.Configurations; +using LCU.API.Internal.Document.Models; +using LCU.API.Internal.Document.Services; +using LCU.API.Internal.DocumentLib.Configurations; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.API.Internal.DocumentLib.DataAccess; +using LCU.API.Internal.DocumentLib.Repositories; +using LCU.Library.Internal.Logging; +using LCU.Library.Internal.Logging.Middleware; +using LCU.Library.Internal.Logging.Models; +using Microsoft.OpenApi.Models; +using LCU.Library.Internal.ApiSecurity; +using Serilog; +using LCU.API.Internal.DocumentLib.Utilities; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; +using MongoDB.Bson.Serialization; +using LCU.Library.Internal.Configuration; +using Microsoft.FeatureManagement; + +var MyAllowSpecificOrigins = "_myAllowSpecificOrigins"; + +var builder = BuildBuilder(args); +BuildServicesAndConfigurations(builder); +BuildAppAndRun(builder); + +WebApplicationBuilder BuildBuilder(string[] args) +{ + var builder = WebApplication.CreateBuilder(args); + builder.Configuration.AddJsonFile("/app/config/mapped-appsettings/primary.json", true, true); + builder.Configuration.AddJsonFile("/vault/secrets/documentapi_configuration.json", true, true); + builder.Configuration.AddJsonFile("/vault/secrets/document-api-azure-client-id.json", optional: true, reloadOnChange: true); + builder.Configuration.AddJsonFile("/vault/secrets/document-api-azure-tenant-id.json", optional: true, reloadOnChange: true); + builder.Configuration.AddJsonFile("/vault/secrets/document-api-client-id.json", optional: true, reloadOnChange: true); + builder.Configuration.AddJsonFile("/vault/secrets/document-api-client-secret.json", optional: true, reloadOnChange: true); + builder.Configuration.AddJsonFile("/vault/secrets/document-api-tenant-id.json", optional: true, reloadOnChange: true); + builder.Configuration.AddJsonFile("/vault/secrets/document-api-swagger-oauth-scope.json", optional: true, reloadOnChange: true); + builder.Configuration.AddJsonFile("/vault/secrets/document-api-seq-api-key.json", optional: true, reloadOnChange: true); + builder.Configuration.AddJsonFile("/vault/secrets/document-api-blob-storage-connection-string.json", optional: true, reloadOnChange: true); + builder.Configuration.AddJsonFile("/vault/secrets/document-api-mongo-database-connection-string.json", optional: true, reloadOnChange: true); + builder.Configuration.AddJsonFile("/vault/secrets/document-api-type-uploader-api-key.json", optional: true, reloadOnChange: true); + builder.Configuration.AddJsonFile("/vault/secrets/document-api-type-retriever-api-key.json", optional: true, reloadOnChange: true); + builder.Configuration.AddJsonFile("/vault/secrets/document-api-type-cleanup-api-key.json", optional: true, reloadOnChange: true); + + if (!bool.Parse(builder.Configuration.GetSection("Primary")["IsAksDeploy"])) + { + var clientSecretCredentials = new ClientSecretCredential(builder.Configuration["TenantId"], builder.Configuration["ClientId"], builder.Configuration["ClientSecret"]); + + builder.Configuration.AddAzureKeyVault( + new Uri($"https://{builder.Configuration["AzureKeyVaultUrl"]}.vault.azure.net/"), clientSecretCredentials); + } + + builder.Host.UseSerilog(); + return builder; +} + +void BuildServicesAndConfigurations(WebApplicationBuilder builder) +{ + var services = builder.Services; + var configuration = builder.Configuration; + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(configuration.GetSection("AzureAd")); + services.AddAuthorization(options => + { + options.AddPolicy("user_token", policy => + { + policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme); + policy.RequireAuthenticatedUser(); + }); + options.DefaultPolicy = options.GetPolicy("user_token")!; + }); + + services.AddFeatureManagement(); + + var documentTypeConfiguration = configuration.Bind(services, "Primary"); + configuration.Bind(services, "BlobStorage"); + configuration.Bind(services, "MongoDatabase"); + configuration.Bind(services); + + var loggerConfiguration = new LoggingConfiguration(documentTypeConfiguration.AppName, + documentTypeConfiguration.SeqApiKey, + documentTypeConfiguration.SeqUrl); + + var lcuLogging = new LcuLogging(loggerConfiguration); + lcuLogging.UseLcuLogging(); + + ServiceRegistrations.AddDocumentServices(services); + ServiceRegistrations.AddDocumentDataAccesses(services); + ServiceRegistrations.AddDocumentRepositories(services); + + BsonSerializer.RegisterDiscriminatorConvention(typeof(DocumentAudit), new DocumentAuditDiscriminatorConvention()); + + services.AddControllers().AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(s => + { + s.SwaggerDoc("v1", new OpenApiInfo { Title = $"{documentTypeConfiguration.AppName}", Version = "v1" }); + s.UseOneOfForPolymorphism(); + + var authority = $"https://login.microsoftonline.com/{configuration.GetSection("AzureAd")["TenantId"]}"; + s.AddSecurityDefinition + ( + nameof(SecuritySchemeType.OpenIdConnect), + new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows + { + AuthorizationCode = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri($"{authority}/oauth2/v2.0/authorize"), + TokenUrl = new Uri($"{authority}/oauth2/v2.0/token") + } + } + //24.10.2022 these SHOULD have enabled OIDC discovery (the discovery info is returned from this URI) but swagger UI is not displaying the auth options + //oidc discovery does work, I have set it up with IdentityServer4 in the past + //Type = SecuritySchemeType.OpenIdConnect, + //OpenIdConnectUrl = new Uri($"https://login.microsoftonline.com/{configuration.GetSection("AzureAd")["TenantId"]}/v2.0/.well-known/openid-configuration") + } + ); + s.AddSecurityRequirement + ( + new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = nameof(SecuritySchemeType.OpenIdConnect) + } + }, + Array.Empty() + } + } + ); + + s.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Name = "ApiKey" + }); + s.AddSecurityDefinition("ApiClient", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Name = "Client" + }); + s.AddSecurityRequirement + ( + new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "ApiKey" + } + }, + Array.Empty() + } + } + ); + s.AddSecurityRequirement + ( + new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "ApiClient" + } + }, + Array.Empty() + } + } + ); + }); + + services.AddApiKeySecurity(configuration); +} + +void BuildAppAndRun(WebApplicationBuilder builder) +{ + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) app.UseDeveloperExceptionPage(); + + app.UseSwagger(s => + { + s.PreSerializeFilters.Add((doc, request) => + { + doc.Servers = new List { new OpenApiServer { Url = $"{request.Scheme}://{request.Host.Value}{request.PathBase}" } }; + }); + }); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("v1/swagger.json", "LCU.API.Internal.Document v1"); + + c.OAuthClientId(app.Configuration.GetSection("AzureAd")["ClientId"]); + c.OAuthUsePkce(); + c.OAuthScopes(app.Configuration.GetSection("Swagger")["OAuthScope"]); + }); + app.UseMiddleware(); + app.UseSerilogRequestLogging(options => + { + options.EnrichDiagnosticContext = RequestResponseLoggingMiddleware.RequestDataParser.EnrichFromRequest; + }); + + app.UseRouting(); + app.UseCors(MyAllowSpecificOrigins); + app.UseHttpsRedirection(); + app.UseAuthentication().UseAuthorization(); + app.MapControllers(); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + + app.Run(); +} + +public static class ServiceRegistrations +{ + public static void AddDocumentServices(IServiceCollection services) + { + services.AddScoped(p => + { + const string checkDocumentOwner = "GetDocumentCheckOwnership"; + + return p.GetRequiredService().IsEnabledAsync(checkDocumentOwner).Result + ? DocumentService.Ownership.CheckPersonOrOrgNumber.Strategy + : DocumentService.Ownership.Unchecked.Strategy; + }); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + public static void AddDocumentDataAccesses(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } + + public static void AddDocumentRepositories(IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Properties/launchSettings.json b/LCU.API.Internal.Document/LCU.API.Internal.Document/Properties/launchSettings.json new file mode 100644 index 0000000..aab492b --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:9009", + "sslPort": 44312 + } + }, + "profiles": { + "LCU.API.Internal.Document": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7037;http://localhost:5037", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/BlobApiService.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/BlobApiService.cs new file mode 100644 index 0000000..d456865 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/BlobApiService.cs @@ -0,0 +1,96 @@ +using LCU.API.Internal.Document.Exceptions; +using LCU.API.Internal.DocumentLib.DataAccess; +using LCU.Library.Internal.Exceptions; +using Serilog; + +namespace LCU.API.Internal.Document.Services; + +public class BlobApiService : IBlobApiService +{ + private readonly IBlobApiDao _blobApiDao; + + public BlobApiService(IBlobApiDao blobApiDao) + { + _blobApiDao = blobApiDao; + } + + public async Task GetDocumentContent(Guid? documentBlobId) + { + + try + { + if (documentBlobId is null || documentBlobId == Guid.Empty) + { + return Array.Empty(); + } + + var result = await _blobApiDao.GetDocumentFromBlob(documentBlobId ?? Guid.Empty); + + if (result?.Value == null){ + throw new ServiceException($"Could not find the Document Content for DocumentBlobId:{documentBlobId}"); + } + + await using var memoryStream = new MemoryStream(); + await result.Value.Content.CopyToAsync(memoryStream); + return memoryStream.ToArray(); + } + catch (ServiceException exception){ + Log.Error(exception.Message); + throw new BlobApiServiceException(exception.Message); + } + catch (Exception exception){ + var message = $"Failure to Retrieve Document Content with DocumentBlobId: {documentBlobId}."; + Log.Error(exception, message); + throw new BlobApiServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task UploadDocumentContent(byte[] content) + { + try + { + return await _blobApiDao.SaveDocumentToBlob(content); + } + catch (Exception exception){ + const string message = "Failure to Upload Document Content."; + Log.Error(exception, message); + throw new BlobApiServiceException($"{message}. Exception Message: {exception.Message}"); + } + } + + public async Task UpdateDocumentContent(Guid? documentBlobId, byte[] content) + { + try + { + if (documentBlobId is null || documentBlobId == Guid.Empty) + { + throw new Exception("DocumentBlobId cannot be null."); + } + + await _blobApiDao.UpdateDocumentInBlob(documentBlobId ?? Guid.Empty, content); + } + catch (Exception exception){ + var message = $"Failure to Update Document with DocumentBlobId: {documentBlobId}"; + Log.Error(exception, message); + throw new BlobApiServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task DeleteDocumentContent(Guid? documentBlobId) + { + try + { + if (documentBlobId is null || documentBlobId == Guid.Empty) + { + throw new Exception("DocumentBlobId cannot be null."); + } + + await _blobApiDao.RemoveDocumentFromBlob(documentBlobId ?? Guid.Empty); + } + catch (Exception exception){ + var message = $"Failure to Delete Document with DocumentBlobId: {documentBlobId}"; + Log.Error(exception, message); + throw new BlobApiServiceException($"{message} Exception Message: {exception.Message}"); + } + } +} diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/DocumentAuditService.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/DocumentAuditService.cs new file mode 100644 index 0000000..b98ece4 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/DocumentAuditService.cs @@ -0,0 +1,243 @@ +using LCU.API.Internal.Document.Exceptions; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.API.Internal.DocumentLib.Repositories; +using LCU.Library.Internal.DocumentModels.Requests; +using LCU.Library.Internal.DocumentModels.Responses; +using Serilog; + +namespace LCU.API.Internal.Document.Services; + +public class DocumentAuditService : IDocumentAuditService +{ + private const string NameField = "Name"; + private const string TypeField = "Type"; + private const string AttributesField = "Attributes"; + private const string ScanStatusField = "ScanStatus"; + private const string ContentField = "Content"; + private readonly IDocumentAuditRepository _documentAuditRepository; + + public DocumentAuditService(IDocumentAuditRepository documentAuditRepository) + { + _documentAuditRepository = documentAuditRepository; + } + + public async Task> GetDocumentAudits(params Guid[] documentIds) + { + + try + { + static DocumentAuditResponse ResponseFactory(DocumentAudit audit) => audit switch + { + DocumentActionAudit actionAudit => new DocumentActionAuditResponse + { + Action = actionAudit.Action, + ActionDescription = actionAudit.ActionDescription, + Date = actionAudit.Date, + DocumentId = actionAudit.DocumentId, + Source = actionAudit.Source, + UserId = actionAudit.UserId + }, + DocumentFieldAudit fieldAudit => new DocumentFieldAuditResponse + { + Field = fieldAudit.Field, + CurrentValue = fieldAudit.CurrentValue, + PreviousValue = fieldAudit.PreviousValue, + Date = fieldAudit.Date, + DocumentId = fieldAudit.DocumentId, + Source = fieldAudit.Source, + UserId = fieldAudit.UserId + }, + _ => throw new ArgumentOutOfRangeException(nameof(audit)) + }; + + return (await _documentAuditRepository.GetDocumentAudits(documentIds)) + .Select(ResponseFactory) + .ToList(); + } + catch (Exception exception) + { + var message = $"Failure to Retrieve Document Audit Information for DocumentIds: {documentIds}"; + Log.Error(exception, message); + throw new DocumentAuditServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task> CreateDocumentUploadedAudit(Guid documentId, DateTime uploadDate) + { + + try + { + var audits = new DocumentAudit[] + { + new DocumentActionAudit + ( + documentId, + "System", + uploadDate, + "System", + DocumentAction.Upload, + "Document Uploaded" + ) + }; + + return await _documentAuditRepository.SaveDocumentAudits(audits); + } + catch (Exception exception) + { + var message = $"Failure to Create Document Audit for DocumentId: {documentId}"; + Log.Error(exception, message); + throw new DocumentAuditServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + + public async Task> CreateDocumentDeletedAudit(Guid documentId, + DateTime deleteDate) + { + + try + { + var audits = new DocumentAudit[] + { + new DocumentActionAudit + ( + documentId, + "System", + deleteDate, + "System", + DocumentAction.Delete, + "Document Deleted" + ) + }; + + return await _documentAuditRepository.SaveDocumentAudits(audits); + } + catch (Exception exception) + { + var message = $"Failure to Create Document Audit for DocumentId: {documentId}"; + Log.Error(exception, message); + throw new DocumentAuditServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + + public async Task> CreateDocumentContentDeletedAudit(Guid documentId, + DateTime deleteDate, + string? reason) + { + try + { + var audits = new DocumentAudit[] + { + new DocumentActionAudit + ( + documentId, + "System", + deleteDate, + "System", + DocumentAction.DeleteContent, + "DocumentContent Deleted" + (!string.IsNullOrWhiteSpace(reason) ? $". Reason: {reason}" : string.Empty) + ) + }; + + return await _documentAuditRepository.SaveDocumentAudits(audits); + } + catch (Exception exception) + { + var message = $"Failure to Create Document Audit for DocumentId: {documentId}"; + Log.Error(exception, message); + throw new DocumentAuditServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task> CreateDocumentAudits(DocumentInformation oldDocumentInformation, + DocumentInformation newDocumentInformation, bool wasDocumentContentUpdated, DateTime updateDate, string userId) + { + DocumentFieldAudit AuditLogFactory(string field, object? oldValue, object? newValue) => new + ( + newDocumentInformation.DocumentId, + newDocumentInformation.Source, + updateDate, + userId, + field, + oldValue, + newValue + ); + + var documentAudits = new List(); + + try + { + if (oldDocumentInformation.Name != newDocumentInformation.Name){ + documentAudits.Add(AuditLogFactory(NameField, oldDocumentInformation.Name, newDocumentInformation.Name)); + } + + if (oldDocumentInformation.Type != newDocumentInformation.Type){ + documentAudits.Add(AuditLogFactory(TypeField, oldDocumentInformation.Type, newDocumentInformation.Type)); + } + + if (oldDocumentInformation.ScanStatus != newDocumentInformation.ScanStatus){ + documentAudits.Add(AuditLogFactory(ScanStatusField, oldDocumentInformation.ScanStatus, newDocumentInformation.ScanStatus)); + } + + if (!AttributesAreEqual(oldDocumentInformation.Attributes, newDocumentInformation.Attributes)){ + documentAudits.Add(AuditLogFactory(AttributesField, oldDocumentInformation.Attributes, newDocumentInformation.Attributes)); + } + + if (wasDocumentContentUpdated){ + documentAudits.Add(AuditLogFactory(ContentField, "NOT CAPTURED", "NOT CAPTURED")); + } + + if (!documentAudits.Any()){ + return new List(); + } + + return await _documentAuditRepository.SaveDocumentAudits(documentAudits.ToArray()); + } + catch (Exception exception){ + var message = + $"Failure to Create Document Audit Information for DocumentId: {oldDocumentInformation.DocumentId}"; + Log.Error(exception, message); + throw new DocumentAuditServiceException($"{message}. Exception Message: {exception.Message}"); + } + } + + public async Task AuditAction(Guid documentId, string userId, string actionSource, DocumentAction action) + { + try + { + var description = action switch + { + DocumentAction.Download => $"Document downloaded from {actionSource}.", + DocumentAction.Upload => "Document Uploaded", + DocumentAction.Delete => "Document Deleted", + DocumentAction.DeleteContent => "DocumentContent Deleted", + _ => throw new ArgumentOutOfRangeException() + }; + + await _documentAuditRepository.SaveDocumentAudits(new DocumentActionAudit(documentId, actionSource, DateTime.Now, userId, action, description)); + } + catch (Exception exception) + { + var message = + $"Failure to Create Document Audit Information for DocumentId: {documentId} on DocumentAction: {action} from source {actionSource}"; + Log.Error(exception, message); + + throw new DocumentAuditServiceException($"{message}. Exception Message: {exception.Message}"); + } + } + + private static bool AttributesAreEqual(Dictionary? oldAttributes, Dictionary? newAttributes) + { + oldAttributes ??= new Dictionary(); + newAttributes ??= new Dictionary(); + + if (oldAttributes.Count != newAttributes.Count){ + return false; + } + + var attributes = oldAttributes + .Where(d2 => newAttributes.Any(d1 => d1.Key == d2.Key && d2.Value.Equals(d1.Value))).ToList(); + return attributes.Count == oldAttributes.Count; + } +} diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/DocumentInformationService.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/DocumentInformationService.cs new file mode 100644 index 0000000..84548dd --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/DocumentInformationService.cs @@ -0,0 +1,164 @@ +using LCU.API.Internal.Document.Configurations; +using LCU.API.Internal.Document.Exceptions; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.API.Internal.DocumentLib.Repositories; +using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination; +using LCU.Library.Internal.DocumentModels.Requests.Search; +using Serilog; + +namespace LCU.API.Internal.Document.Services; + +public class DocumentInformationService : IDocumentInformationService +{ + private readonly IDocumentInformationRepository _documentInformationRepository; + private readonly ApiConfiguration _apiConfiguration; + + public DocumentInformationService(IDocumentInformationRepository documentInformationRepository, + ApiConfiguration apiConfiguration) + { + _documentInformationRepository = documentInformationRepository; + _apiConfiguration = apiConfiguration; + } + + public async Task GetDocumentInformation(Guid documentId) + { + try + { + var documentInformation = await _documentInformationRepository.GetDocumentInformation(documentId); + + if (documentInformation == null || + documentInformation.UploadDate >= DateTime.UtcNow.AddHours(_apiConfiguration.FileUploadCutoffHours) || + documentInformation.ScanStatus == false) + { + return null; + } + + return documentInformation; + } + catch (Exception exception) + { + var message = $"Failure to Retrieve DocumentInformation with DocumentId: {documentId}."; + Log.Error(exception, message); + throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task GetDocumentInformationByBlobId(Guid? documentBlobId) + { + try + { + if (documentBlobId is null || documentBlobId == Guid.Empty) + { + return null; + } + + var documentInformation = await _documentInformationRepository.GetDocumentInformationByBlobId(documentBlobId ?? Guid.Empty); + return documentInformation ?? null; + } + catch (Exception exception) + { + var message = $"Failure to Retrieve DocumentInformation with DocumentBlobId: {documentBlobId}."; + Log.Error(exception, message); + throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task DeleteDocumentInformation(Guid documentId) + { + try + { + var documentInformation = await _documentInformationRepository.GetDocumentInformation(documentId); + + if (documentInformation == null) + { + return; + } + + await _documentInformationRepository.RemoveDocumentInformation(documentId); + } + catch (Exception exception) + { + var message = $"Failure to Delete DocumentInformation with DocumentId: {documentId}"; + Log.Error(exception, message); + throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task UploadDocumentInformation(DocumentInformation information) + { + try{ + return await _documentInformationRepository.SaveDocumentInformation(information); + } + catch (Exception exception) + { + var message = $"Failure to Upload DocumentInformation with Name: {information.Name} of type: {information.Type} from source: {information.Source}"; + Log.Error(exception, message); + throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task UpdateDocumentInformation(Guid documentId, DocumentInformation information) + { + try + { + var documentInformation = await _documentInformationRepository.GetDocumentInformation(documentId); + + if (documentInformation == null) + { + return null; + } + + return await _documentInformationRepository.UpdateDocumentInformation(documentId, information); + } + catch (Exception exception) + { + var message = $"Failure to Update DocumentInformation with DocumentId: {documentId} and Name: {information.Name} of type: {information.Type}."; + Log.Error(exception, message); + throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List? sortDefinition = null) + { + try + { + var documentInformationFound = await _documentInformationRepository.SearchDocumentInformation(filter, skip, take, sortDefinition); + return documentInformationFound ?? new List(); + } + catch (Exception exception) + { + const string message = "Failure to Search Document Information"; + Log.Error(exception, message); + throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task CountDocuments(DocumentInformationFilter filter) + { + try + { + return await _documentInformationRepository.CountDocuments(filter); + } + catch (Exception exception) + { + const string message = "Failure to Count Document Information"; + Log.Error(exception, message); + + throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task> GetDocumentsForCleanup(GetDocumentCleanupRequest request) + { + try + { + return await _documentInformationRepository.GetDocumentsForCleanup(request); + } + catch (Exception exception) + { + const string message = "Failure to GetDocumentsForCleanup"; + Log.Error(exception, message); + throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}"); + } + } +} diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/DocumentService.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/DocumentService.cs new file mode 100644 index 0000000..e1acebf --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/DocumentService.cs @@ -0,0 +1,748 @@ +using System.Numerics; +using System.Text.Json; +using LCU.API.Internal.Document.Exceptions; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.Library.Internal.DocumentModels.Requests; +using LCU.Library.Internal.DocumentModels.Requests.Search; +using LCU.Library.Internal.DocumentModels.Responses; +using LCU.Library.Internal.Exceptions; +using LCU.Api.Internal.MaidBotLib.Models; +using Serilog; +using LCU.API.Internal.NotificationsApiLib.Models; +using LCU.API.Internal.Document.Configurations; +using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination; + +namespace LCU.API.Internal.Document.Services; + +// TODO: Add hash of content to document information so the content can be compared before updates. +// Any basic hash should be fine since it will only be used as a checksum to the content. +// Example can be found https://stackoverflow.com/questions/10520048/calculate-md5-checksum-for-a-file/10520086#10520086 +public class DocumentService : IDocumentService +{ + private readonly IBlobApiService _blobApiService; + private readonly IDocumentInformationService _documentInformationService; + private readonly IDocumentAuditService _documentAuditService; + private readonly INotificationApiService _notificationApiService; + private readonly ApiConfiguration _apiConfiguration; + private readonly DocumentIsForMemberStrategy DocumentIsForMember; + + public DocumentService + ( + IBlobApiService blobApiService, + IDocumentInformationService documentInformationService, + IDocumentAuditService documentAuditService, + INotificationApiService notificationApiService, + ApiConfiguration apiConfiguration, + DocumentIsForMemberStrategy documentIsForMember + ) + { + _blobApiService = blobApiService; + _documentInformationService = documentInformationService; + _documentAuditService = documentAuditService; + _notificationApiService = notificationApiService; + _apiConfiguration = apiConfiguration; + DocumentIsForMember = documentIsForMember; + } + + public async Task GetDocument(Guid documentId) + { + try{ + var documentInformation = await _documentInformationService.GetDocumentInformation(documentId); + + if (documentInformation == null) + { + return null; + } + + return new DocumentResponse + { + Content = await _blobApiService.GetDocumentContent(documentInformation.DocumentBlobId), + DocumentInformation = ConvertDocumentInformationToDocumentInformationResponse(documentInformation) + }; + } + catch (DocumentInformationServiceException exception){ + var message = $"Failure to Retrieve Document Information. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (BlobApiServiceException exception){ + var message = + $"Failure to Retrieve Document Content from Blob Storage. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (Exception exception){ + var message = $"Failure to Retrieve Document with DocumentId: {documentId}."; + Log.Error(exception, message); + throw new ServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task GetDocument(long personOrOrgNumber, Guid documentId) + { + try{ + var documentInformation = await _documentInformationService.GetDocumentInformation(documentId); + + if (documentInformation is null) + { + return null; + } + + if(!DocumentIsForMember(documentInformation, personOrOrgNumber)) return null; + + return new DocumentResponse + { + Content = await _blobApiService.GetDocumentContent(documentInformation.DocumentBlobId), + DocumentInformation = ConvertDocumentInformationToDocumentInformationResponse(documentInformation) + }; + } + catch (DocumentInformationServiceException exception){ + var message = $"Failure to Retrieve Document Information. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (BlobApiServiceException exception){ + var message = + $"Failure to Retrieve Document Content from Blob Storage. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (Exception exception){ + var message = $"Failure to Retrieve Document with DocumentId: {documentId}."; + Log.Error(exception, message); + throw new ServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task GetDocumentByBlobId(Guid documentBlobId) + { + try + { + return await _blobApiService.GetDocumentContent(documentBlobId); + } + catch (BlobApiServiceException exception) + { + var message = + $"Failure to Retrieve Document Content from Blob Storage. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (Exception exception) + { + var message = $"Failure to Retrieve Document with DocumentBlobId: {documentBlobId}."; + Log.Error(exception, message); + throw new ServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task GetDocumentByBlobId(long personOrOrgNumber, Guid documentBlobId) + { + try + { + var document = await _documentInformationService.GetDocumentInformationByBlobId(documentBlobId); + if(document is null) return null; + + if(!DocumentIsForMember(document, personOrOrgNumber)) return null; + + return await _blobApiService.GetDocumentContent(documentBlobId); + } + catch (BlobApiServiceException exception) + { + var message = + $"Failure to Retrieve Document Content from Blob Storage. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (Exception exception) + { + var message = $"Failure to Retrieve Document with DocumentBlobId: {documentBlobId}."; + Log.Error(exception, message); + throw new ServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task DeleteDocument(Guid documentId) + { + try{ + var deleteDate = DateTime.UtcNow; + + var documentInformation = await _documentInformationService.GetDocumentInformation(documentId); + if (documentInformation == null) + { + return; + } + + await _documentInformationService.DeleteDocumentInformation(documentId); + await _blobApiService.DeleteDocumentContent(documentInformation.DocumentBlobId); + await _documentAuditService.CreateDocumentDeletedAudit(documentInformation.DocumentId, deleteDate); + } + catch (DocumentInformationServiceException exception){ + var message = $"Failure to Remove Document Information. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (BlobApiServiceException exception){ + var message = + $"Failure to Remove Document Content from Blob Storage, but DocumentInformation was removed. Exception Message: {exception.Message}"; + Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group), + Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment)).Error(exception, message); + throw new ServiceException(message); + } + catch (Exception exception){ + var message = $"Failure to Delete Document with DocumentId: {documentId}"; + Log.Error(exception, message); + throw new ServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task DeleteDocumentContent(DeleteDocumentContentRequest request) + { + try + { + var deleteDate = DateTime.UtcNow; + + var documentInformation = await _documentInformationService.GetDocumentInformation(request.DocumentId); + if (documentInformation?.DocumentBlobId is null) + { + return; + } + + await _blobApiService.DeleteDocumentContent(documentInformation.DocumentBlobId); + + documentInformation.DocumentBlobId = null; + await _documentInformationService.UpdateDocumentInformation(documentInformation.DocumentId, documentInformation); + + await _documentAuditService.CreateDocumentContentDeletedAudit(documentInformation.DocumentId, deleteDate, request.Reason); + } + catch (DocumentInformationServiceException exception){ + var message = + $"Failure to Remove Document Content from Blob Storage. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (BlobApiServiceException exception){ + var message = + $"Failure to Remove Document Content from Blob Storage. Exception Message: {exception.Message}"; + Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group), + Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment)).Error(exception, message); + throw new ServiceException(message); + } + catch (Exception exception){ + var message = $"Failure to Delete Document Content with DocumentId: {request.DocumentId}"; + Log.Error(exception, message); + throw new ServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task UploadDocument(DocumentRequest request) + { + var docBlobId = Guid.Empty; + try{ + docBlobId = await _blobApiService.UploadDocumentContent(request.Content); + var fileSize = CalculateFileSize(request.Content); + var uploadDate = DateTime.UtcNow; + + var documentInformation = new DocumentInformation + { + DocumentId = Guid.NewGuid(), + DocumentBlobId = docBlobId, + TraceId = request.TraceId, + Name = request.Name, + Type = request.Type, + Source = request.Source, + DataSource = request.DataSource, + PersonNumbers = request.PersonNumbers, + OrganizationNumbers = request.OrganizationNumbers, + AccountNumber = request.AccountNumber, + Attributes = request.Attributes, + CreatedDate = request.CreatedDate, + FileSize = fileSize, + UploadDate = DateTime.UtcNow, + ScanStatus = null + }; + + var uploadedDocumentInformation = await _documentInformationService.UploadDocumentInformation(documentInformation); + await _documentAuditService.CreateDocumentUploadedAudit(documentInformation.DocumentId, uploadDate); + return ConvertDocumentInformationToDocumentInformationResponse(uploadedDocumentInformation); + } + catch (BlobApiServiceException exception){ + var message = $"Failure to Upload Content to Blob Storage. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (DocumentInformationServiceException exception){ + await RollBackDocumentContent(docBlobId); + + var message = $"Failure to Upload Document Information. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (Exception exception){ + var message = + $"Failure to Upload Document Name: {request.Name} of type: {request.Type} from source: {request.Source}"; + Log.Error(exception, message); + throw new ServiceException($"{message}. Exception Message: {exception.Message}"); + } + } + + public async Task UpdateDocument(UpdateDocumentRequest request, Guid documentId, string userId) + { + DocumentInformation? documentInformation = null; + var modifiedDate = DateTime.UtcNow; + + try{ + documentInformation = await _documentInformationService.GetDocumentInformation(documentId); + + if (documentInformation == null) + { + return null; + } + + var content = request.Content; + var fileSize = CalculateFileSize(content); + + var updatedDocumentInformation = new DocumentInformation + { + DocumentId = documentId, + DocumentBlobId = documentInformation.DocumentBlobId, + TraceId = documentInformation.TraceId, + UploadDate = documentInformation.UploadDate, + Source = documentInformation.Source, + DataSource = documentInformation.DataSource, + CreatedDate = documentInformation.CreatedDate, + Name = request.Name, + Type = request.Type, + PersonNumbers = request.PersonNumbers, + OrganizationNumbers = request.OrganizationNumbers, + AccountNumber = request.AccountNumber, + Attributes = request.Attributes, + LastModifiedDate = modifiedDate, + FileSize = fileSize, + ScanStatus = null + }; + + var docInfo = await _documentInformationService.UpdateDocumentInformation(documentId, updatedDocumentInformation); + await _documentAuditService.CreateDocumentAudits(documentInformation, updatedDocumentInformation, true, modifiedDate, userId); + await _blobApiService.UpdateDocumentContent(documentInformation.DocumentBlobId, content); + return ConvertDocumentInformationToDocumentInformationResponse(docInfo); + } + catch (DocumentInformationServiceException exception){ + var message = $"Failure to Update Document Information for DocumentId: {documentId}. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (DocumentAuditServiceException exception){ + await RollbackDocumentInformation(documentId, documentInformation); + + var message = $"Failure to Create Document Audits. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (BlobApiServiceException exception){ + await RollbackDocumentInformation(documentId, documentInformation); + + var message = $"Failure to Update Document Content from Blob Storage. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (Exception exception){ + var message = $"Failure to Update Document with DocumentId: {documentId} Name: {request.Name} of type: {request.Type} from source: {request.UpdateSource}."; + Log.Error(exception, message); + throw new ServiceException($"{message}. Exception Message: {exception.Message}"); + } + } + + public async Task GetDocumentInformation(Guid documentId) + { + try{ + var documentInformation = await _documentInformationService.GetDocumentInformation(documentId); + + if (documentInformation == null) + { + return null; + } + + return ConvertDocumentInformationToDocumentInformationResponse(documentInformation); + } + catch (DocumentInformationServiceException exception){ + var message = $"Failure to Retrieve Document Information. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (Exception exception){ + var message = $"Failure to Retrieve Document with DocumentId: {documentId}."; + Log.Error(exception, message); + throw new ServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task UpdateDocumentContent(UpdateDocumentContentRequest request, Guid documentId, string userId) + { + DocumentInformation? documentInformation = null; + var modifiedDate = DateTime.UtcNow; + + try{ + documentInformation = await _documentInformationService.GetDocumentInformation(documentId); + + if (documentInformation == null) + { + return null; + } + + var content = request.Content; + var fileSize = CalculateFileSize(content); + + var updatedDocumentInformation = new DocumentInformation + { + DocumentId = documentId, + DocumentBlobId = documentInformation.DocumentBlobId, + TraceId = documentInformation.TraceId, + Name = documentInformation.Name, + Type = documentInformation.Type, + Source = documentInformation.Source, + DataSource = documentInformation.DataSource, + PersonNumbers = documentInformation.PersonNumbers, + OrganizationNumbers = documentInformation.OrganizationNumbers, + AccountNumber = documentInformation.AccountNumber, + Attributes = documentInformation.Attributes, + UploadDate = documentInformation.UploadDate, + CreatedDate = documentInformation.CreatedDate, + LastModifiedDate = modifiedDate, + FileSize = fileSize, + ScanStatus = null + }; + + var docInfo = await _documentInformationService.UpdateDocumentInformation(documentId, updatedDocumentInformation); + await _documentAuditService.CreateDocumentAudits(documentInformation, updatedDocumentInformation, true, modifiedDate, userId); + await _blobApiService.UpdateDocumentContent(documentInformation.DocumentBlobId, content); + return ConvertDocumentInformationToDocumentInformationResponse(docInfo); + } + catch (DocumentInformationServiceException exception){ + var message = $"Failure to Update Document Information for DocumentId: {documentId}. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (DocumentAuditServiceException exception){ + await RollbackDocumentInformation(documentId, documentInformation); + + var message = $"Failure to Create Document Audits. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (BlobApiServiceException exception){ + await RollbackDocumentInformation(documentId, documentInformation); + + var message = $"Failure to Update Document Content from Blob Storage. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (Exception exception){ + var message = $"Failure to Update Document Content for DocumentId: {documentId}."; + Log.Error(exception, message); + throw new ServiceException($"{message}. Exception Message: {exception.Message}"); + } + } + + public async Task UpdateDocumentInformation(UpdateDocumentInformationRequest request, Guid documentId, string userId) + { + DocumentInformation? documentInformation = null; + var modifiedDate = DateTime.UtcNow; + + try{ + documentInformation = await _documentInformationService.GetDocumentInformation(documentId); + if (documentInformation == null) + { + return null; + } + + var updatedDocumentInformation = new DocumentInformation + { + DocumentId = documentId, + DocumentBlobId = documentInformation.DocumentBlobId, + TraceId = documentInformation.TraceId, + Source = documentInformation.Source, + DataSource = documentInformation.DataSource, + CreatedDate = documentInformation.CreatedDate, + Name = request.Name, + Type = request.Type, + PersonNumbers = request.PersonNumbers, + OrganizationNumbers = request.OrganizationNumbers, + AccountNumber = request.AccountNumber, + Attributes = request.Attributes, + UploadDate = documentInformation.UploadDate, + LastModifiedDate = modifiedDate, + FileSize = documentInformation.FileSize, + ScanStatus = documentInformation.ScanStatus + }; + + var docInfo = await _documentInformationService.UpdateDocumentInformation(documentId, updatedDocumentInformation); + await _documentAuditService.CreateDocumentAudits(documentInformation, updatedDocumentInformation, false, modifiedDate, userId); + return ConvertDocumentInformationToDocumentInformationResponse(docInfo); + } + catch (DocumentInformationServiceException exception){ + var message = $"Failure to Update Document Information for DocumentId: {documentId} with Name: {request.Name} of type: {request.Type} from source: {request.UpdateSource}. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (DocumentAuditServiceException exception){ + await RollbackDocumentInformation(documentId, documentInformation); + + var message = $"Failure to Create Document Audits. Exception Message: {exception.Message}"; + Log.Error(exception, message); + throw new ServiceException(message); + } + catch (Exception exception){ + var message = $"Failure to Update Document Information for DocumentId: {documentId} with Name: {request.Name} of type: {request.Type} from source: {request.UpdateSource}."; + Log.Error(exception, message); + throw new ServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List? sortDefinition = null) + { + try{ + var documentInformationFound = await _documentInformationService.SearchDocumentInformation(filter, skip, take, sortDefinition); + + return documentInformationFound.Select(ConvertDocumentInformationToDocumentInformationResponse).ToList(); + } + catch (DocumentInformationServiceException exception){ + Log.Error(exception, exception.Message); + throw new ServiceException(exception.Message); + } + catch (Exception exception){ + const string message = "Failure to Search Document Information"; + Log.Error(exception, message); + throw new ServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task UpdateDocumentScanStatus(bool status, List documentBlobIds, string userId) + { + foreach (var documentBlobId in documentBlobIds){ + DocumentInformation? documentInformation = null; + var modifiedDate = DateTime.UtcNow; + + try{ + documentInformation = await _documentInformationService.GetDocumentInformationByBlobId(documentBlobId); + if (documentInformation == null) + { + continue; + } + + var updatedDocumentInformation = new DocumentInformation + { + DocumentBlobId = documentBlobId, + + DocumentId = documentInformation.DocumentId, + Source = documentInformation.Source, + DataSource = documentInformation.DataSource, + TraceId = documentInformation.TraceId, + Name = documentInformation.Name, + Type = documentInformation.Type, + PersonNumbers = documentInformation.PersonNumbers, + OrganizationNumbers = documentInformation.OrganizationNumbers, + AccountNumber = documentInformation.AccountNumber, + Attributes = documentInformation.Attributes, + UploadDate = documentInformation.UploadDate, + CreatedDate = documentInformation.CreatedDate, + FileSize = documentInformation.FileSize, + + ScanStatus = status, + LastModifiedDate = modifiedDate + }; + + await _documentInformationService.UpdateDocumentInformation(updatedDocumentInformation.DocumentId, + updatedDocumentInformation); + await _documentAuditService.CreateDocumentAudits(documentInformation, updatedDocumentInformation, false, + modifiedDate, userId); + + if (!status) + { + await NotifyAboutMaliciousFile(documentInformation); + } + } + catch (DocumentInformationServiceException exception){ + Log.Error(exception, $"Failure to Update Document Scan Status for DocumentBlobId: {documentBlobId}"); + } + catch (DocumentAuditServiceException exception){ + await RollbackDocumentInformation(documentInformation.DocumentId, documentInformation); + + var message = $"Failure to Create Document Audits. Exception Message: {exception.Message}"; + Log.Error(exception, message); + } + catch (Exception exception){ + Log.Error(exception,$"Failure to Update Document Scan Status for DocumentBlobId: {documentBlobId}, caused by unexpected exception"); + } + } + } + + public async Task CountDocuments(DocumentInformationFilter filter) + { + try + { + return await _documentInformationService.CountDocuments(filter); + } + catch (DocumentInformationServiceException exception) + { + Log.Error(exception, exception.Message); + + throw new ServiceException(exception.Message); + } + catch (Exception exception) + { + const string message = "Failure to Count Document Information"; + Log.Error(exception, message); + + throw new ServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + public async Task> GetDocumentsForCleanup(GetDocumentCleanupRequest request) + { + try + { + return await _documentInformationService.GetDocumentsForCleanup(request); + } + catch (DocumentInformationServiceException exception) + { + Log.Error(exception, exception.Message); + + throw new ServiceException(exception.Message); + } + catch (Exception exception) + { + const string message = "Failure GetDocumentsForCleanup"; + Log.Error(exception, message); + + throw new ServiceException($"{message} Exception Message: {exception.Message}"); + } + } + + private async Task RollBackDocumentContent(Guid docBlobId) + { + try{ + Log.Information($"Removing {docBlobId} from Blob Storage, as part of a rollback transaction."); + await _blobApiService.DeleteDocumentContent(docBlobId); + } + catch (Exception inception){ + Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group), + Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment)) + .Error(inception, + $"Failed to remove {docBlobId} from Blob Storage, an inconsistency in data now exists between DocumentInformation and Content. Exception Message: {inception.Message}"); + } + } + + private async Task RollbackDocumentInformation(Guid documentId, DocumentInformation documentInformation) + { + try{ + Log.Information( + $"Reverting Document Information for DocumentId:{documentId}, as part of a rollback transaction."); + await _documentInformationService.UpdateDocumentInformation(documentId, documentInformation); + } + catch (Exception inception){ + Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group), + Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment)) + .Error($"Failed to update document information for DocumentId:{documentId}, an inconsistency in data now exists between DocumentInformation. DocumentInformation should be updated to: {JsonSerializer.Serialize(documentInformation)} Exception Message: {inception.Message}"); + } + } + + private static BigInteger CalculateFileSize(byte[] bytes) + { + var fileBase64Value = Convert.ToBase64String(bytes); + var size = fileBase64Value.Length / 4 * 3; + + if (fileBase64Value.EndsWith("==")) + size -= 2; + else if (fileBase64Value.EndsWith("=")) size -= 1; + + return size; + } + + private static DocumentInformationResponse ConvertDocumentInformationToDocumentInformationResponse( + DocumentInformation updatedDocumentInformation) + { + return new DocumentInformationResponse + { + DocumentId = updatedDocumentInformation.DocumentId, + DocumentBlobId = updatedDocumentInformation.DocumentBlobId, + TraceId = updatedDocumentInformation.TraceId, + Name = updatedDocumentInformation.Name, + Type = updatedDocumentInformation.Type, + Source = updatedDocumentInformation.Source, + DataSource = updatedDocumentInformation.DataSource, + FileSize = updatedDocumentInformation.FileSize, + ScanStatus = updatedDocumentInformation.ScanStatus, + PersonNumbers = updatedDocumentInformation.PersonNumbers, + OrganizationNumbers = updatedDocumentInformation.OrganizationNumbers, + AccountNumber = updatedDocumentInformation.AccountNumber, + Attributes = updatedDocumentInformation.Attributes, + UploadDate = updatedDocumentInformation.UploadDate, + LastModifiedDate = updatedDocumentInformation.LastModifiedDate, + CreatedDate = updatedDocumentInformation.CreatedDate + }; + } + + private async Task NotifyAboutMaliciousFile(DocumentInformation documentInformation) + { + try + { + var options = new JsonSerializerOptions { WriteIndented = true }; + var documentAttributesSerialized = + JsonSerializer.Serialize( + documentInformation.Attributes, + new JsonSerializerOptions { WriteIndented = true } + ); + + var emailRequest = new EmailRequest() + { + Subject = "DocumentApi: Malicious File Reported", + Recipient = _apiConfiguration.MaliciousFileFoundEmailList, + Body = "This is an auto generated message.
" + + "
" + + "Azure Defender has returned a malicious file from scan
" + + "
" + + "Document Attributes:
" + + $"{documentAttributesSerialized}" + }; + + await _notificationApiService.SendEmailAsync(emailRequest); + } + catch (Exception ex) + { + var message = $"Failure to send email. Azure Defender returned a file as malicious. DocumentInformation: {JsonSerializer.Serialize(documentInformation)}. Exception Message: {ex.Message}"; + Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group), + Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment)).Error(ex, message); + throw new ServiceException(message); + } + } + + public delegate bool DocumentIsForMemberStrategy(DocumentInformation document, long personOrOrgNumber); + + public static class Ownership + { + public static class Unchecked + { + public static DocumentIsForMemberStrategy Strategy => DocumentIsForMember; + + private static bool DocumentIsForMember(DocumentInformation document, long personOrOrgNumber) + { + var personOrOrgNumbers = (document.PersonNumbers ?? Array.Empty()) + .Concat(document.OrganizationNumbers ?? Array.Empty()) + .Select(n => long.TryParse(n, out var number) ? number : -1); + Log.Information("checking {personOrOrgNumber} on {document.DocumentId} in {personOrOrgNumbers} ignored", personOrOrgNumber, document.DocumentId, JsonSerializer.Serialize(personOrOrgNumbers)); + + return true; + } + } + + public static class CheckPersonOrOrgNumber + { + public static DocumentIsForMemberStrategy Strategy => DocumentIsForMember; + + private static bool DocumentIsForMember(DocumentInformation document, long personOrOrgNumber) => (document.PersonNumbers ?? Array.Empty()) + .Concat(document.OrganizationNumbers ?? Array.Empty()) + .Select(n => long.TryParse(n, out var number) ? number : -1) + .Contains(personOrOrgNumber); + } + } +} diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/IBlobApiService.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/IBlobApiService.cs new file mode 100644 index 0000000..4a21f27 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/IBlobApiService.cs @@ -0,0 +1,9 @@ +namespace LCU.API.Internal.Document.Services; + +public interface IBlobApiService +{ + Task GetDocumentContent(Guid? documentBlobId); + Task UploadDocumentContent(byte[] content); + Task UpdateDocumentContent(Guid? documentBlobId, byte[] content); + Task DeleteDocumentContent(Guid? documentBlobId); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/IDocumentAuditService.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/IDocumentAuditService.cs new file mode 100644 index 0000000..8fd11b3 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/IDocumentAuditService.cs @@ -0,0 +1,15 @@ +using LCU.API.Internal.DocumentLib.Containers; +using LCU.Library.Internal.DocumentModels.Requests; +using LCU.Library.Internal.DocumentModels.Responses; + +namespace LCU.API.Internal.Document.Services; + +public interface IDocumentAuditService +{ + Task> GetDocumentAudits(params Guid[] documentIds); + Task> CreateDocumentAudits(DocumentInformation oldDocumentInformation, DocumentInformation newDocumentInformation, bool wasDocumentContentUpdated, DateTime updateDate, string userId); + public Task> CreateDocumentUploadedAudit(Guid documentId, DateTime uploadDate); + public Task> CreateDocumentDeletedAudit(Guid documentId, DateTime deleteDate); + public Task> CreateDocumentContentDeletedAudit(Guid documentId, DateTime deleteDate, string? reason); + Task AuditAction(Guid documentId, string userId, string actionSource, DocumentAction action); +} diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/IDocumentInformationService.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/IDocumentInformationService.cs new file mode 100644 index 0000000..4b7d269 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/IDocumentInformationService.cs @@ -0,0 +1,17 @@ +using LCU.API.Internal.DocumentLib.Containers; +using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination; +using LCU.Library.Internal.DocumentModels.Requests.Search; + +namespace LCU.API.Internal.Document.Services; + +public interface IDocumentInformationService +{ + Task GetDocumentInformation(Guid documentId); + Task GetDocumentInformationByBlobId(Guid? documentBlobId); + Task DeleteDocumentInformation(Guid documentId); + Task UploadDocumentInformation(DocumentInformation information); + Task UpdateDocumentInformation(Guid documentId, DocumentInformation information); + Task> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List? sortDefinition = null); + Task CountDocuments(DocumentInformationFilter filter); + Task> GetDocumentsForCleanup(GetDocumentCleanupRequest request); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/IDocumentService.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/IDocumentService.cs new file mode 100644 index 0000000..a728fab --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/IDocumentService.cs @@ -0,0 +1,25 @@ +using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination; +using LCU.Library.Internal.DocumentModels.Requests; +using LCU.Library.Internal.DocumentModels.Requests.Search; +using LCU.Library.Internal.DocumentModels.Responses; + +namespace LCU.API.Internal.Document.Services; + +public interface IDocumentService +{ + Task GetDocument(Guid documentId); + Task GetDocument(long personOrOrgNumber, Guid documentId); + Task GetDocumentByBlobId(Guid documentBlobId); + Task GetDocumentByBlobId(long personOrOrgNumber, Guid documentBlobId); + Task> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List? sortDefinition = null); + Task DeleteDocument(Guid documentId); + Task DeleteDocumentContent(DeleteDocumentContentRequest request); + Task UploadDocument(DocumentRequest request); + Task GetDocumentInformation(Guid documentId); + Task UpdateDocument(UpdateDocumentRequest request, Guid documentId, string userId); + Task UpdateDocumentContent(UpdateDocumentContentRequest request, Guid documentId, string userId); + Task UpdateDocumentInformation(UpdateDocumentInformationRequest request, Guid documentId, string userId); + Task UpdateDocumentScanStatus(bool status, List documentBlobIds, string userId); + Task CountDocuments(DocumentInformationFilter filter); + Task> GetDocumentsForCleanup(GetDocumentCleanupRequest request); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/INotificationApiService.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/INotificationApiService.cs new file mode 100644 index 0000000..6c6e6da --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/INotificationApiService.cs @@ -0,0 +1,9 @@ +using LCU.API.Internal.NotificationsApiLib.Models; + +namespace LCU.API.Internal.Document.Services +{ + public interface INotificationApiService + { + Task SendEmailAsync(EmailRequest emailRequest); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/NotificationApiService.cs b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/NotificationApiService.cs new file mode 100644 index 0000000..e26a9da --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/Services/NotificationApiService.cs @@ -0,0 +1,30 @@ +using LCU.API.Internal.DocumentLib.DataAccess; +using LCU.API.Internal.NotificationsApiLib.Models; +using LCU.Library.Internal.Exceptions; +using Serilog; + +namespace LCU.API.Internal.Document.Services +{ + public class NotificationApiService : INotificationApiService + { + private readonly INotificationApiDao _notificationServiceApiDao; + + public NotificationApiService(INotificationApiDao notificationApiServiceDao) + { + _notificationServiceApiDao = notificationApiServiceDao; + } + + public async Task SendEmailAsync(EmailRequest emailRequest) + { + try + { + await _notificationServiceApiDao.SendEmailAsync(emailRequest); + } + catch (Exception exception) + { + Log.Error(exception, $"Failure to send email. Subject: {emailRequest.Subject}; Recipients: {String.Join(", ", emailRequest.Recipient)}"); + throw new ServiceException($"Failure to send email. Subject: {emailRequest.Subject}; Recipients: {String.Join(", ", emailRequest.Recipient)}; Exception Message: {exception.Message}"); + } + } + } +} diff --git a/LCU.API.Internal.Document/LCU.API.Internal.Document/appsettings.json b/LCU.API.Internal.Document/LCU.API.Internal.Document/appsettings.json new file mode 100644 index 0000000..60c8af1 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.Document/appsettings.json @@ -0,0 +1,58 @@ +{ + "AzureAd": { + "ClientId": "c92072ab-987f-4b67-9923-730e06b2766f", + "Instance": "https://login.microsoftonline.com/", + "Domain": "landmarkcu.onmicrosoft.com", + "TenantId": "63cd1554-2783-4a86-ae50-b9e6c21c9499", + "CallbackPath": "/signin-oidc" + }, + "AzureKeyVaultUrl": "qa-DocumentApi-kv", + "ClientId": "c92072ab-987f-4b67-9923-730e06b2766f", + "ClientSecret": "OjS8Q~TuByD~pylYMu5pbw9UGNG5fldT6eP9YcBI", + "TenantId": "63cd1554-2783-4a86-ae50-b9e6c21c9499", + "Swagger": { + "OAuthScope": "api://c92072ab-987f-4b67-9923-730e06b2766f/user.groups" + }, + "Primary": { + "AppName": "Document Api", + "SeqUrl": "https://qa-seq.lcu-internal.com", + "SeqApiKey": "ftd9RlWVEI1ctg3dqusa", + "FileUploadCutoffHours": -2, + "IsAksDeploy": "false", + "CorsAllowedOrigins": [ + "http://lcu-internal.com", + "https://lcu-internal.com", + "https://qa-api.lcu-internal.com" + ], + "MaliciousFileFoundEmailList": [ + "ITDevelopment@landmarkcu.onmicrosoft.com", + "ITSecurity@landmarkcu.onmicrosoft.com" + ] + }, + "BlobStorage": { + "connectionString": "VALUE-IN-KEYVAULT", + "containerName": "documents" + }, + "MongoDatabase": { + "connectionString": "VALUE-IN-KEYVAULT", + "databaseName": "DocumentsApi", + "documentContainerName": "DocumentsInformation", + "auditContainerName": "AuditInformation" + }, + "ApiSecurityConfiguration": { + "ApiKeyAuthenticationFailureBehavior": "Error", + "ExternalApiKeySet": { + "DocumentTypeUploader": "VALUE-IN-KEYVAULT", + "DocumentTypeRetriever": "VALUE-IN-KEYVAULT", + "DocumentTypeCleanup": "VALUE-IN-KEYVAULT" + }, + "InternalApiKeySet": {} + }, + "NotificationApiConfiguration": { + "Url": "https://qa-notificationsapi.lcu-internal.com", + "Email": "/api/Email" + }, + "FeatureManagement": { + "GetDocumentCheckOwnership": true + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Configurations/BlobStorageConfiguration.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Configurations/BlobStorageConfiguration.cs new file mode 100644 index 0000000..882548d --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Configurations/BlobStorageConfiguration.cs @@ -0,0 +1,7 @@ +namespace LCU.API.Internal.DocumentLib.Configurations; + +public class BlobStorageConfiguration +{ + public string ConnectionString { get; set; } + public string ContainerName { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Configurations/DatabaseConfiguration.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Configurations/DatabaseConfiguration.cs new file mode 100644 index 0000000..b9b34a8 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Configurations/DatabaseConfiguration.cs @@ -0,0 +1,11 @@ +namespace LCU.API.Internal.DocumentLib.Configurations; + +public class DatabaseConfiguration +{ + public string ConnectionString { get; set; } + public string DatabaseName { get; set; } + + public string DocumentContainerName { get; set; } + + public string AuditContainerName { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Configurations/NotificationApiConfiguration.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Configurations/NotificationApiConfiguration.cs new file mode 100644 index 0000000..72109ce --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Configurations/NotificationApiConfiguration.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace LCU.API.Internal.DocumentLib.Configurations; +public class NotificationApiConfiguration +{ + public string Url { get; set; } + public string Email { get; set; } + + [NotMapped] + public string EmailUrl + { + get + { + return Url + Email; + } + } +} diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Containers/DocumentAudit.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Containers/DocumentAudit.cs new file mode 100644 index 0000000..806c20b --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Containers/DocumentAudit.cs @@ -0,0 +1,55 @@ +using System; +using LCU.Library.Internal.DocumentModels.Requests; +using MongoDB.Bson.Serialization.Attributes; + +namespace LCU.API.Internal.DocumentLib.Containers; + +[BsonKnownTypes(typeof(DocumentFieldAudit), typeof(DocumentActionAudit))] +public abstract class DocumentAudit +{ + protected DocumentAudit(Guid documentId, string source, DateTime date, string userId) + { + DocumentId = documentId; + Source = source; + Date = date; + UserId = userId; + } + + public Guid DocumentId { get; set; } + public string Source { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime Date { get; set; } + + public string UserId { get; set; } +} + +[BsonIgnoreExtraElements] +public class DocumentFieldAudit : DocumentAudit +{ + public DocumentFieldAudit(Guid documentId, string source, DateTime date, string userId, string field, object previousValue, object currentValue) + : base(documentId, source, date, userId) + { + Field = field; + PreviousValue = previousValue; + CurrentValue = currentValue; + } + + public string Field { get; set; } + public object PreviousValue { get; set; } + public object CurrentValue { get; set; } +} + +[BsonIgnoreExtraElements] +public class DocumentActionAudit : DocumentAudit +{ + public DocumentActionAudit(Guid documentId, string source, DateTime date, string userId, DocumentAction action, string description) + : base(documentId, source, date, userId) + { + Action = action; + ActionDescription = description; + } + + public DocumentAction Action { get; set; } + public string ActionDescription { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Containers/DocumentAuditDiscriminatorConvention.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Containers/DocumentAuditDiscriminatorConvention.cs new file mode 100644 index 0000000..afdd6ba --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Containers/DocumentAuditDiscriminatorConvention.cs @@ -0,0 +1,40 @@ +using System; +using MongoDB.Bson; +using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization.Conventions; + +namespace LCU.API.Internal.DocumentLib.Containers; + +public class DocumentAuditDiscriminatorConvention : IDiscriminatorConvention +{ + public static string Discriminator = StandardDiscriminatorConvention.Scalar.ElementName; + public string ElementName => Discriminator; + + public Type GetActualType(IBsonReader bsonReader, Type nominalType) + { + var bookmark = bsonReader.GetBookmark(); + + try + { + bsonReader.ReadStartDocument(); + + if (!bsonReader.FindElement(ElementName)) return typeof(DocumentFieldAudit); + + return bsonReader.ReadString() switch + { + nameof(DocumentFieldAudit) => typeof(DocumentFieldAudit), + nameof(DocumentActionAudit) => typeof(DocumentActionAudit), + _ => throw new NotSupportedException() + }; + } + finally + { + bsonReader.ReturnToBookmark(bookmark); + } + } + + public BsonValue GetDiscriminator(Type nominalType, Type actualType) + { + return actualType.Name; + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Containers/DocumentInformation.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Containers/DocumentInformation.cs new file mode 100644 index 0000000..8c4924c --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Containers/DocumentInformation.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson.Serialization.Serializers; + +namespace LCU.API.Internal.DocumentLib.Containers; + +[BsonIgnoreExtraElements] +public class DocumentInformation +{ + public Guid DocumentId { get; set; } + public Guid? DocumentBlobId { get; set; } + public Guid? TraceId { get; set; } + public string Name { get; set; } + public string Type { get; set; } + public string Source { get; set; } + + public string DataSource { get; set; } + + [BsonSerializer(typeof(BigIntSerializer))] + public BigInteger FileSize { get; set; } + + public bool? ScanStatus { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime UploadDate { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime? LastModifiedDate { get; set; } + + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CreatedDate { get; set; } + public string[] PersonNumbers { get; set; } + public string[] OrganizationNumbers { get; set; } + public string AccountNumber { get; set; } + public Dictionary Attributes { get; set; } +} + + +public class BigIntSerializer : SerializerBase +{ + public override BigInteger Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var val = context.Reader.ReadString(); + return BigInteger.Parse(val); + } + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, BigInteger value) + { + context.Writer.WriteString(value.ToString()); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/BlobApiDao.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/BlobApiDao.cs new file mode 100644 index 0000000..13de634 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/BlobApiDao.cs @@ -0,0 +1,44 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Azure; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; + +namespace LCU.API.Internal.DocumentLib.DataAccess; + +public class BlobApiDao : IBlobApiDao +{ + private readonly BlobContainerClient _client; + + public BlobApiDao(IBlobStorageClient storageClient) + { + _client = storageClient.GetClient(); + } + + public async Task SaveDocumentToBlob(byte[] fileData) + { + var blobDocumentId = Guid.NewGuid(); + + using var stream = new MemoryStream(fileData, false); + await _client.GetBlobClient(blobDocumentId.ToString()).UploadAsync(stream); + return blobDocumentId; + } + + public async Task UpdateDocumentInBlob(Guid blobDocumentId, byte[] fileData) + { + using var stream = new MemoryStream(fileData, false); + await _client.GetBlobClient(blobDocumentId.ToString()).UploadAsync(stream, true); + } + + public async Task RemoveDocumentFromBlob(Guid blobDocumentId) + { + return await _client.GetBlobClient(blobDocumentId.ToString()) + .DeleteAsync(DeleteSnapshotsOption.IncludeSnapshots); + } + + public async Task> GetDocumentFromBlob(Guid blobDocumentId) + { + return await _client.GetBlobClient(blobDocumentId.ToString()).DownloadStreamingAsync(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/BlobStorageClient.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/BlobStorageClient.cs new file mode 100644 index 0000000..d35c9fb --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/BlobStorageClient.cs @@ -0,0 +1,20 @@ +using Azure.Storage.Blobs; +using LCU.API.Internal.DocumentLib.Configurations; + +namespace LCU.API.Internal.DocumentLib.DataAccess; + +public class BlobStorageClient : IBlobStorageClient +{ + private readonly BlobContainerClient _containerClient; + + public BlobStorageClient(BlobStorageConfiguration config) + { + var serviceClient = new BlobServiceClient(config.ConnectionString); + _containerClient = serviceClient.GetBlobContainerClient(config.ContainerName); + } + + public BlobContainerClient GetClient() + { + return _containerClient; + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/IBlobApiDao.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/IBlobApiDao.cs new file mode 100644 index 0000000..f6a0d48 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/IBlobApiDao.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading.Tasks; +using Azure; +using Azure.Storage.Blobs.Models; + +namespace LCU.API.Internal.DocumentLib.DataAccess; + +public interface IBlobApiDao +{ + Task SaveDocumentToBlob(byte[] fileData); + Task UpdateDocumentInBlob(Guid blobDocumentId, byte[] fileData); + Task RemoveDocumentFromBlob(Guid blobDocumentId); + Task> GetDocumentFromBlob(Guid blobDocumentId); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/IBlobStorageClient.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/IBlobStorageClient.cs new file mode 100644 index 0000000..db045aa --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/IBlobStorageClient.cs @@ -0,0 +1,8 @@ +using Azure.Storage.Blobs; + +namespace LCU.API.Internal.DocumentLib.DataAccess; + +public interface IBlobStorageClient +{ + BlobContainerClient GetClient(); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/INotificationApiDao.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/INotificationApiDao.cs new file mode 100644 index 0000000..f31daae --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/INotificationApiDao.cs @@ -0,0 +1,10 @@ +using LCU.API.Internal.NotificationsApiLib.Models; +using System.Threading.Tasks; + +namespace LCU.API.Internal.DocumentLib.DataAccess +{ + public interface INotificationApiDao + { + Task SendEmailAsync(EmailRequest emailRequest); + } +} diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/NotificationApiDao.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/NotificationApiDao.cs new file mode 100644 index 0000000..031520c --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/DataAccess/NotificationApiDao.cs @@ -0,0 +1,23 @@ +using LCU.API.Internal.DocumentLib.Configurations; +using LCU.API.Internal.DocumentLib.Utilities; +using LCU.API.Internal.NotificationsApiLib.Models; +using System.Threading.Tasks; +using System; + +namespace LCU.API.Internal.DocumentLib.DataAccess +{ + public class NotificationApiDao : INotificationApiDao + { + private readonly NotificationApiConfiguration _notificationApiConfiguration; + public NotificationApiDao(NotificationApiConfiguration notificationApiConfiguration) + { + _notificationApiConfiguration = notificationApiConfiguration; + } + + public async Task SendEmailAsync(EmailRequest emailRequest) + { + await HttpUtilities.PostAsync(new Uri(_notificationApiConfiguration.EmailUrl), + emailRequest); + } + } +} diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/LCU.API.Internal.DocumentLib.csproj b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/LCU.API.Internal.DocumentLib.csproj new file mode 100644 index 0000000..6e46873 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/LCU.API.Internal.DocumentLib.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.0 + latest + + + + + + + + + + + + + + + + diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/DataClient.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/DataClient.cs new file mode 100644 index 0000000..599cfc1 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/DataClient.cs @@ -0,0 +1,19 @@ +using LCU.API.Internal.DocumentLib.Configurations; +using MongoDB.Driver; + +namespace LCU.API.Internal.DocumentLib.Repositories; + +public class DataClient : IDataClient +{ + private readonly MongoClient _client; + + public DataClient(DatabaseConfiguration config) + { + _client = new MongoClient(config.ConnectionString); + } + + public MongoClient GetClient() + { + return _client; + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/DocumentAuditRepository.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/DocumentAuditRepository.cs new file mode 100644 index 0000000..6c993bd --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/DocumentAuditRepository.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.API.Internal.DocumentLib.Utilities; +using MongoDB.Driver; + +namespace LCU.API.Internal.DocumentLib.Repositories; + +public class DocumentAuditRepository : IDocumentAuditRepository +{ + private readonly IMongoCollection _documentAudits; + + public DocumentAuditRepository(IMongoCollectionFactory collectionFactory) + { + _documentAudits = collectionFactory.GetCollection(); + } + + public async Task> GetDocumentAudits(params Guid[] documentIds) + { + return await (await _documentAudits.FindAsync(doc => documentIds.Contains(doc.DocumentId))).ToListAsync(); + } + + public async Task> SaveDocumentAudits(params DocumentAudit[] documentAudit) + { + await _documentAudits.InsertManyAsync(documentAudit); + + return documentAudit; + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/DocumentInformationRepository.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/DocumentInformationRepository.cs new file mode 100644 index 0000000..6391247 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/DocumentInformationRepository.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.API.Internal.DocumentLib.Utilities; +using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination; +using LCU.Library.Internal.DocumentModels.Requests.Search; +using Microsoft.Azure.Cosmos.Linq; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace LCU.API.Internal.DocumentLib.Repositories; + +public class DocumentInformationRepository : IDocumentInformationRepository +{ + private readonly IMongoCollection _documents; + + public DocumentInformationRepository(IMongoCollectionFactory collectionFactory) + { + _documents = collectionFactory.GetCollection(); + } + + public async Task GetDocumentInformation(Guid documentId) + { + return await _documents.Find(doc => doc.DocumentId == documentId) + .SingleOrDefaultAsync(); + } + + public async Task> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List? sortDefinition = null) + { + var pipelineFilter = FilterFactory(filter); + var sorter = Sorter.GetSorter(sortDefinition); + if (sorter != null) pipelineFilter = pipelineFilter.Sort(sorter); + if (skip.HasValue) pipelineFilter = pipelineFilter.Skip(skip.Value); + if (take.HasValue) pipelineFilter = pipelineFilter.Limit(take.Value); + + var documentsInformation = await _documents.AggregateAsync(pipelineFilter); + return await documentsInformation.ToListAsync(); + } + + public async Task CountDocuments(DocumentInformationFilter filter) + { + return (await _documents.AggregateAsync(FilterFactory(filter).Count())).Single().Count; + } + + public async Task GetDocumentInformationByBlobId(Guid documentBlobId) + { + return await _documents + .Find(doc => doc.DocumentBlobId == documentBlobId) + .SingleOrDefaultAsync(); + } + + public async Task SaveDocumentInformation(DocumentInformation document) + { + await _documents.InsertOneAsync(document); + return document; + } + + public async Task UpdateDocumentInformation(Guid documentId, DocumentInformation newDocument) + { + await _documents.FindOneAndReplaceAsync(doc => doc.DocumentId == documentId, newDocument); + return newDocument; + } + + public async Task RemoveDocumentInformation(Guid documentId) + { + return await _documents.FindOneAndDeleteAsync(document => document.DocumentId == documentId); + } + + public async Task> GetDocumentsForCleanup(GetDocumentCleanupRequest request) + { + IList> filterConcat = new List>(); + foreach(var documentCodeDetail in request.DocumentCodeDetailInfos) + { + var adjustedDate = DateTime.Now.AddMonths(documentCodeDetail.InternalRetentionMonths * -1); + + var typeFilter = Builders.Filter.Eq(x => x.Type, documentCodeDetail.DocumentCode); + var dateFilter = Builders.Filter.Lt(x => x.CreatedDate, adjustedDate); + var groupFilter = Builders.Filter.And(typeFilter, dateFilter); + + filterConcat.Add(groupFilter); + } + + var filter = Builders.Filter; + var oneConcat = filter.Empty; + oneConcat &= filter.Or(filterConcat); + + var nullFilter = Builders.Filter.Ne(x => x.DocumentBlobId, null); + oneConcat &= filter.And(nullFilter); + + var pipelineFilter = new EmptyPipelineDefinition() + .Match(oneConcat) + .Limit(request.Limit); + + var documentsInformation = await _documents.AggregateAsync(pipelineFilter); + return (await documentsInformation.ToListAsync()).Select(document => document.DocumentId).ToList(); + } + + private PipelineDefinition FilterFactory(DocumentInformationFilter filter) + { + var builder = Builders.Filter; + IList> stageOneFiltersAnd = new List>(); + IList> stageOneFiltersOr = new List>(); + IList> stageTwoFiltersAnd = new List>(); + IList> stageTwoFiltersOr = new List>(); + + if (!string.IsNullOrWhiteSpace(filter.Name)) + { + stageOneFiltersAnd.Add(builder.Eq("Name", filter.Name)); + } + + if (!string.IsNullOrWhiteSpace(filter.Type)) + { + stageOneFiltersAnd.Add(builder.Eq("Type", filter.Type)); + } + + if (filter.TypeContainsAny != null && filter.TypeContainsAny.Any()) + { + stageOneFiltersAnd.Add(builder.In("Type", filter.TypeContainsAny)); + } + + if (filter.ScanStatus != null) + { + stageOneFiltersAnd.Add(builder.Eq("ScanStatus", filter.ScanStatus)); + } + + if (!string.IsNullOrWhiteSpace(filter.Source)) + { + stageOneFiltersAnd.Add(builder.Eq("Source", filter.Source)); + } + + if (!string.IsNullOrWhiteSpace(filter.DataSource)) + { + stageOneFiltersAnd.Add(builder.Eq("DataSource", filter.DataSource)); + } + + if (filter.TraceId != null) + { + stageOneFiltersAnd.Add(builder.Eq("TraceId", filter.TraceId)); + } + + if (!string.IsNullOrWhiteSpace(filter.PersonNumbersEquals)) + { + stageOneFiltersAnd.Add(builder.Eq("PersonNumbers", filter.PersonNumbersEquals)); + } + + if (filter.PersonNumbersContains != null && filter.PersonNumbersContains.Any()) + { + stageOneFiltersAnd.Add(builder.All("PersonNumbers", filter.PersonNumbersContains)); + } + + if (filter.PersonNumbersContainsAny != null && filter.PersonNumbersContainsAny.Any()) + { + stageOneFiltersAnd.Add(builder.In("PersonNumbers", filter.PersonNumbersContainsAny)); + } + + if (filter.OrganizationNumbersEquals != null && filter.OrganizationNumbersEquals.Any()) + { + stageOneFiltersAnd.Add(builder.Eq("OrganizationNumbers", filter.OrganizationNumbersEquals)); + } + + if (filter.OrganizationNumbersContains != null && filter.OrganizationNumbersContains.Any()) + { + stageOneFiltersAnd.Add(builder.All("OrganizationNumbers", filter.OrganizationNumbersContains)); + } + + if (filter.OrganizationNumbersContainsAny != null && filter.OrganizationNumbersContainsAny.Any()) + { + stageOneFiltersAnd.Add(builder.In("OrganizationNumbers", filter.OrganizationNumbersContainsAny)); + } + + if (filter.AccountNumberEquals != null && filter.AccountNumberEquals.Any()) + { + stageOneFiltersAnd.Add(builder.Eq("AccountNumber", filter.AccountNumberEquals)); + } + + if (filter.AccountNumberContainsAny != null && filter.AccountNumberContainsAny.Any()) + { + stageOneFiltersAnd.Add(builder.In("AccountNumber", filter.AccountNumberContainsAny)); + } + + if (filter.AttributesEquals != null && filter.AttributesEquals.Any()) + { + foreach (var keyValuePair in filter.AttributesEquals) + { + stageOneFiltersAnd.Add(builder.Eq($"Attributes.{keyValuePair.Key.Trim()}", keyValuePair.Value.Trim())); + } + } + + if (filter.AttributesContains is not null) + { + foreach (var keyValuePair in filter.AttributesContains) + { + var queryExpr = + new BsonRegularExpression( + new Regex($"\\b({keyValuePair.Value.Trim()})\\b", RegexOptions.IgnoreCase)); + stageTwoFiltersAnd.Add(builder.Regex($"Attributes.{keyValuePair.Key.Trim()}", queryExpr)); + } + } + + if (filter.AttributesContainsAny is not null) + { + foreach (var keyValuePair in filter.AttributesContainsAny) + { + foreach (var value in keyValuePair.Value.Split(',')) + { + var queryExpr = new BsonRegularExpression(new Regex($"^{value.Trim()}$", RegexOptions.IgnoreCase)); + stageTwoFiltersOr.Add(builder.Regex($"Attributes.{keyValuePair.Key.Trim()}", queryExpr)); + } + } + } + + if (filter.CreatedDateMinimum != null) + { + stageOneFiltersAnd.Add(builder.Gte("CreatedDate", filter.CreatedDateMinimum)); + } + + if (filter.CreatedDateMaximum != null) + { + stageOneFiltersAnd.Add(builder.Lte("CreatedDate", filter.CreatedDateMaximum)); + } + + if (filter.UploadDateMinimum != null) + { + stageOneFiltersAnd.Add(builder.Gte("UploadDate", filter.UploadDateMinimum)); + } + + if (filter.UploadDateMaximum != null) + { + stageOneFiltersAnd.Add(builder.Lte("UploadDate", filter.UploadDateMaximum)); + } + + if (filter.LastModifiedMinimum != null) + { + stageOneFiltersAnd.Add(builder.Gte("LastModifiedDate", filter.LastModifiedMinimum)); + } + + if (filter.LastModifiedDateMaximum != null) + { + stageOneFiltersAnd.Add(builder.Lte("LastModifiedDate", filter.LastModifiedDateMaximum)); + } + + var stageOneFilterConcat = builder.Empty; + if (stageOneFiltersAnd.Any()) + { + stageOneFilterConcat &= builder.And(stageOneFiltersAnd); + } + if (stageOneFiltersOr.Any()) + { + stageOneFilterConcat &= builder.Or(stageOneFiltersOr); + } + + var stageTwoFilterConcat = builder.Empty; + if (stageTwoFiltersAnd.Any()) + { + stageOneFilterConcat &= builder.And(stageTwoFiltersAnd); + } + if (stageTwoFiltersOr.Any()) + { + stageTwoFilterConcat &= builder.Or(stageTwoFiltersOr); + } + + return new EmptyPipelineDefinition() + .Match(stageOneFilterConcat) + .Match(stageTwoFilterConcat); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/IDataClient.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/IDataClient.cs new file mode 100644 index 0000000..688c96e --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/IDataClient.cs @@ -0,0 +1,8 @@ +using MongoDB.Driver; + +namespace LCU.API.Internal.DocumentLib.Repositories; + +public interface IDataClient +{ + MongoClient GetClient(); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/IDocumentAuditRepository.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/IDocumentAuditRepository.cs new file mode 100644 index 0000000..55e4afa --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/IDocumentAuditRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LCU.API.Internal.DocumentLib.Containers; + +namespace LCU.API.Internal.DocumentLib.Repositories; + +public interface IDocumentAuditRepository +{ + Task> GetDocumentAudits(params Guid[] documentIds); + Task> SaveDocumentAudits(params DocumentAudit[] documentAudit); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/IDocumentInformationRepository.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/IDocumentInformationRepository.cs new file mode 100644 index 0000000..e448cf2 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Repositories/IDocumentInformationRepository.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination; +using LCU.Library.Internal.DocumentModels.Requests.Search; + +namespace LCU.API.Internal.DocumentLib.Repositories; + +public interface IDocumentInformationRepository +{ + Task GetDocumentInformation(Guid documentId); + Task GetDocumentInformationByBlobId(Guid documentBlobId); + Task> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List? sortDefinition = null); + Task SaveDocumentInformation(DocumentInformation document); + Task UpdateDocumentInformation(Guid documentId, DocumentInformation newDocument); + Task RemoveDocumentInformation(Guid documentId); + Task CountDocuments(DocumentInformationFilter filter); + Task> GetDocumentsForCleanup(GetDocumentCleanupRequest request); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Utilities/HttpUtilities.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Utilities/HttpUtilities.cs new file mode 100644 index 0000000..d15ca72 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Utilities/HttpUtilities.cs @@ -0,0 +1,105 @@ +using LCU.Library.Internal.Exceptions; +using Newtonsoft.Json; +using Serilog; +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace LCU.API.Internal.DocumentLib.Utilities; + +public class HttpUtilities +{ + public static HttpClient CreateClient(Uri uri) + { + var handler = new HttpClientHandler + { + SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | + System.Security.Authentication.SslProtocols.Tls11 | + System.Security.Authentication.SslProtocols.Tls, + ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true + }; + + var httpClient = new HttpClient(handler) + { + Timeout = TimeSpan.FromSeconds(60), + BaseAddress = uri + }; + + return httpClient; + } + + public static async Task PostAsync(Uri uri, TRequest request) + { + try + { + var client = CreateClient(uri); + var jsonBody = JsonSerializer.Serialize(request); + var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + var response = await client.PostAsync(uri, content); + + if (!response.IsSuccessStatusCode) + { + throw new ServiceException($"StatusCode:{response.StatusCode}, " + + $"Content: {response.Content.ReadAsStringAsync().Result}"); + } + } + catch (Exception exception) + { + Log.Error(exception, "Failure during PostAsync."); + throw new ServiceException($"Failure during Document Upload. Exception Message: {exception.Message}"); + } + } + + public static async Task PostAsync(Uri uri, TRequest request) + { + try + { + var client = CreateClient(uri); + var jsonBody = JsonSerializer.Serialize(request); + var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + var response = await client.PostAsync(uri, content); + + if (!response.IsSuccessStatusCode) + { + throw new ServiceException($"StatusCode:{response.StatusCode}, " + + $"Content: {response.Content.ReadAsStringAsync().Result}"); + } + + var stringResponse = await response.Content.ReadAsStringAsync(); + + return JsonSerializer.Deserialize(stringResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } + catch (Exception exception) + { + Log.Error(exception, "Failure during PostAsync."); + throw new ServiceException($"Failure during PostAsync. Exception Message: {exception.Message}"); + } + } + + public static async Task GetAsync(string url) + { + try + { + var uri = new Uri(url); + var client = CreateClient(uri); + + var response = await client.GetAsync(uri); + if (!response.IsSuccessStatusCode) + { + throw new ServiceException($"StatusCode:{response.StatusCode}, " + + $"Content: {response.Content.ReadAsStringAsync().Result}"); + } + + var stringResponse = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(stringResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } + catch (Exception exception) + { + Log.Error(exception, "Failure during GetAsync."); + throw new ServiceException($"Failure during GetAsync. Exception Message: {exception.Message}"); + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Utilities/IMongoCollectionFactory.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Utilities/IMongoCollectionFactory.cs new file mode 100644 index 0000000..7bf71b2 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Utilities/IMongoCollectionFactory.cs @@ -0,0 +1,8 @@ +using MongoDB.Driver; + +namespace LCU.API.Internal.DocumentLib.Utilities; + +public interface IMongoCollectionFactory +{ + IMongoCollection GetCollection(); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Utilities/MongoCollectionFactory.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Utilities/MongoCollectionFactory.cs new file mode 100644 index 0000000..210ca23 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLib/Utilities/MongoCollectionFactory.cs @@ -0,0 +1,37 @@ +using System; +using LCU.API.Internal.DocumentLib.Configurations; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.API.Internal.DocumentLib.Repositories; +using MongoDB.Driver; + +namespace LCU.API.Internal.DocumentLib.Utilities; + +public class MongoCollectionFactory : IMongoCollectionFactory +{ + private readonly IDataClient _mongoDataClient; + private readonly DatabaseConfiguration _config; + + public MongoCollectionFactory(IDataClient mongoDataClient, DatabaseConfiguration config) + { + _mongoDataClient = mongoDataClient; + _config = config; + } + + public IMongoCollection GetCollection() + { + return _mongoDataClient + .GetClient() + .GetDatabase(_config.DatabaseName) + .GetCollection(CollectionNameFor()); + } + + private string CollectionNameFor() + { + return typeof(T) switch + { + var t when t == typeof(DocumentInformation) => _config.DocumentContainerName, + var t when t == typeof(DocumentAudit) => _config.AuditContainerName, + _ => throw new NotImplementedException() + }; + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/Containers/DocumentAuditDiscriminatorConventionTest.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/Containers/DocumentAuditDiscriminatorConventionTest.cs new file mode 100644 index 0000000..077b1fb --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/Containers/DocumentAuditDiscriminatorConventionTest.cs @@ -0,0 +1,185 @@ +using FluentAssertions; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.API.Internal.DocumentLibTest.TestDoubles; +using LCU.Library.Internal.DocumentModels.Requests; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; + +namespace LCU.API.Internal.DocumentLibTest.Containers; + +public class DocumentAuditDiscriminatorConventionTest : IDisposable +{ + private readonly FakeMongoCollectionFactory _collectionFactory = new(); + private readonly IMongoCollection _collection; + + public DocumentAuditDiscriminatorConventionTest() + { + _collection = _collectionFactory.GetCollection(); + } + + private static bool _registered = false; + + [SetUp] + public void Setup() + { + if (_registered) return; + + BsonSerializer.RegisterDiscriminatorConvention + ( + typeof(DocumentAudit), + new DocumentAuditDiscriminatorConvention() + ); + _registered = true; + } + + [Test] + public async Task FindAuditDocuments_DocumentMissingDiscriminator_DeserializesAsFieldAudit() + { + var audit = FieldAuditFactory(); + await _collection.InsertOneAsync(audit); + await _collection.FindOneAndUpdateAsync + ( + a => a.DocumentId == audit.DocumentId, + Builders.Update.Unset(DocumentAuditDiscriminatorConvention.Discriminator) + ); + + var retrievedAudit = await + (await _collection.FindAsync(a => a.DocumentId == audit.DocumentId)) + .SingleOrDefaultAsync(); + + Assert.IsInstanceOf(retrievedAudit); + retrievedAudit.Should().BeEquivalentTo(audit, a => a.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task FindAuditDocuments_DiscriminatorIsFieldAudit_DeserializesAsFieldAudit() + { + var audit = FieldAuditFactory(); + await _collection.InsertOneAsync(audit); + await _collection.FindOneAndUpdateAsync + ( + a => a.DocumentId == audit.DocumentId, + Builders.Update.Set + ( + DocumentAuditDiscriminatorConvention.Discriminator, + nameof(DocumentFieldAudit) + ) + ); + + var retrievedAudit = await + (await _collection.FindAsync(a => a.DocumentId == audit.DocumentId)) + .SingleOrDefaultAsync(); + + Assert.IsInstanceOf(retrievedAudit); + retrievedAudit.Should().BeEquivalentTo(audit, a => a.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task InsertFieldAuditDocuments_NewDocumentWithConventionRegistered_DiscriminatorIsFieldAudit() + { + var audit = FieldAuditFactory(); + + await _collection.InsertOneAsync(audit); + + var audits = await + (await _collection.FindAsync + ( + Builders.Filter.Eq + ( + DocumentAuditDiscriminatorConvention.Discriminator, + nameof(DocumentFieldAudit) + ) + ) + ) + .ToListAsync(); + audits.Should().ContainEquivalentOf(audit, a => a.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task FindAuditDocuments_DiscriminatorIsActionAudit_DeserializesAsActionAudit() + { + var audit = ActionAuditFactory(); + await _collection.InsertOneAsync(audit); + await _collection.FindOneAndUpdateAsync + ( + a => a.DocumentId == audit.DocumentId, + Builders.Update.Set + ( + DocumentAuditDiscriminatorConvention.Discriminator, + nameof(DocumentActionAudit) + ) + ); + + var retrievedAudit = await + (await _collection.FindAsync(a => a.DocumentId == audit.DocumentId)) + .SingleOrDefaultAsync(); + + Assert.IsInstanceOf(retrievedAudit); + retrievedAudit.Should().BeEquivalentTo(audit, a => a.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task InsertActionAuditDocuments_NewDocumentWithConventionRegistered_DiscriminatorIsActionAudit() + { + var audit = ActionAuditFactory(); + + await _collection.InsertOneAsync(audit); + + var audits = await + (await _collection.FindAsync + ( + Builders.Filter.Eq + ( + DocumentAuditDiscriminatorConvention.Discriminator, + nameof(DocumentActionAudit) + ) + ) + ) + .ToListAsync(); + audits.Should().ContainEquivalentOf(audit, a => a.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task FindAuditDocuments_UnknownDiscriminator_ThrowsException() + { + var audit = FieldAuditFactory(); + await _collection.InsertOneAsync(audit); + await _collection.FindOneAndUpdateAsync + ( + a => a.DocumentId == audit.DocumentId, + Builders.Update.Set + ( + DocumentAuditDiscriminatorConvention.Discriminator, + Guid.NewGuid().ToString() + ) + ); + + Assert.ThrowsAsync(() => _collection.FindAsync(a => a.DocumentId == audit.DocumentId)); + } + + private static DocumentFieldAudit FieldAuditFactory() => new + ( + Guid.NewGuid(), + Guid.NewGuid().ToString(), + DateTime.UtcNow, + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid(), + Guid.NewGuid() + ); + + private static DocumentActionAudit ActionAuditFactory() => new + ( + Guid.NewGuid(), + Guid.NewGuid().ToString(), + DateTime.UtcNow, + Guid.NewGuid().ToString(), + DocumentAction.Download, + Guid.NewGuid().ToString() + ); + + public void Dispose() + { + _collectionFactory.Dispose(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/DateTimeComparison.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/DateTimeComparison.cs new file mode 100644 index 0000000..2ee3db7 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/DateTimeComparison.cs @@ -0,0 +1,31 @@ +using FluentAssertions; +using FluentAssertions.Equivalency; + +namespace LCU.API.Internal.DocumentLibTest; + +public static class DateTimeComparison +{ + public static IEquivalencyStep Truncated { get; } = new TruncatedDateTimeStep(); + + public static DateTime Truncate(this DateTime date) => date.AddTicks(-(date.Ticks % 10000)); + + private class TruncatedDateTimeStep : IEquivalencyStep + { + public EquivalencyResult Handle + ( + Comparands comparands, + IEquivalencyValidationContext context, + IEquivalencyValidator nestedValidator + ) + { + if (comparands.Subject is not DateTime subject || comparands.Expectation is not DateTime expectation) + return EquivalencyResult.ContinueWithNext; + + subject.Truncate() + .Should() + .Be(expectation.Truncate(), context.Reason.FormattedMessage, context.Reason.Arguments); + + return EquivalencyResult.AssertionCompleted; + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/LCU.API.Internal.DocumentLibTest.csproj b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/LCU.API.Internal.DocumentLibTest.csproj new file mode 100644 index 0000000..2e60ee0 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/LCU.API.Internal.DocumentLibTest.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + + + + + diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/Repositories/DocumentAuditRepositoryTest.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/Repositories/DocumentAuditRepositoryTest.cs new file mode 100644 index 0000000..17276dc --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/Repositories/DocumentAuditRepositoryTest.cs @@ -0,0 +1,124 @@ +using FluentAssertions; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.API.Internal.DocumentLib.Repositories; +using LCU.API.Internal.DocumentLibTest.TestDataFactories; +using LCU.API.Internal.DocumentLibTest.TestDoubles; +using LCU.Library.Internal.DocumentModels.Requests; +using MongoDB.Driver; + +namespace LCU.API.Internal.DocumentLibTest.Repositories; + +public class DocumentAuditRepositoryTest : IDisposable +{ + private readonly IDocumentAuditRepository _repository; + + private readonly FakeMongoCollectionFactory _collectionFactory = new(); + + public DocumentAuditRepositoryTest() + { + _repository = new DocumentAuditRepository(_collectionFactory); + } + + [Test] + public async Task GetDocumentAudits_FieldAuditRecordsExistForId_ReturnsMatchedLogsById() + { + var audits = EnumerableDataSeriesFactory.CreateSeries(() => FieldAuditFactory(), 5).ToArray(); + await _collectionFactory.GetCollection().InsertManyAsync(audits); + var expectedAudits = new[]{ audits.First(), audits.Last() }; + var documentIds = expectedAudits.Select(a => a.DocumentId).ToArray(); + + var retrievedAudits = await _repository.GetDocumentAudits(documentIds); + + retrievedAudits.Should().HaveSameCount(documentIds); + expectedAudits.Should().BeEquivalentTo(retrievedAudits, a => a.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task GetDocumentAudits_ActionAuditRecordsExistForId_ReturnsMatchedLogsById() + { + var audits = EnumerableDataSeriesFactory.CreateSeries(() => ActionAuditFactory(), 5).ToArray(); + await _collectionFactory.GetCollection().InsertManyAsync(audits); + var expectedAudits = new[] { audits.First(), audits.Last() }; + var documentIds = expectedAudits.Select(a => a.DocumentId).ToArray(); + + var retrievedAudits = await _repository.GetDocumentAudits(documentIds); + + retrievedAudits.Should().HaveSameCount(documentIds); + expectedAudits.Should().BeEquivalentTo(retrievedAudits, a => a.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SaveDocumentAudits_FieldAuditsSubmitted_SavesAll() + { + var audits = EnumerableDataSeriesFactory.CreateSeries(() => FieldAuditFactory(), 5).ToArray(); + + await _repository.SaveDocumentAudits(audits); + + var savedAudits = await ( + await _collectionFactory + .GetCollection() + .FindAsync(Builders.Filter.In + ( + nameof(DocumentAudit.DocumentId), + audits.Select(a => a.DocumentId)) + ) + ).ToListAsync(); + audits.Should().BeEquivalentTo(savedAudits, a => a.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SaveDocumentAudits_ActionAuditsSubmitted_SavesAll() + { + var audits = EnumerableDataSeriesFactory.CreateSeries(() => ActionAuditFactory(), 5).ToArray(); + + await _repository.SaveDocumentAudits(audits); + + var savedAudits = await ( + await _collectionFactory + .GetCollection() + .FindAsync(Builders.Filter.In(nameof(DocumentAudit.DocumentId), + audits.Select(a => a.DocumentId))) + ).ToListAsync(); + audits.Should().BeEquivalentTo(savedAudits, a => a.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SaveDocumentAudits_MultipleTypesOfAudits_CanRetrieveAll() + { + var documentId = Guid.NewGuid(); + var fieldAudits = EnumerableDataSeriesFactory.CreateSeries(() => FieldAuditFactory(documentId), 5); + var actionAudits = EnumerableDataSeriesFactory.CreateSeries(() => ActionAuditFactory(documentId), 5); + var audits = fieldAudits.Concat(actionAudits).ToArray(); + + await _repository.SaveDocumentAudits(audits); + var savedAudits = await _repository.GetDocumentAudits(documentId); + + audits.Should().BeEquivalentTo(savedAudits, a => a.Using(DateTimeComparison.Truncated)); + } + + private static DocumentFieldAudit FieldAuditFactory(Guid? documentId = null) => new + ( + documentId ?? Guid.NewGuid(), + Guid.NewGuid().ToString(), + DateTime.UtcNow, + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString() + ); + + private static DocumentActionAudit ActionAuditFactory(Guid? documentId = null) => new + ( + documentId ?? Guid.NewGuid(), + Guid.NewGuid().ToString(), + DateTime.UtcNow, + Guid.NewGuid().ToString(), + (DocumentAction)DateTime.Now.Millisecond, + Guid.NewGuid().ToString() + ); + + public void Dispose() + { + _collectionFactory.Dispose(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/Repositories/DocumentInformationRepositoryTest.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/Repositories/DocumentInformationRepositoryTest.cs new file mode 100644 index 0000000..0956aed --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/Repositories/DocumentInformationRepositoryTest.cs @@ -0,0 +1,1230 @@ +using FluentAssertions; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.API.Internal.DocumentLib.Repositories; +using LCU.API.Internal.DocumentLibTest.TestDataFactories; +using LCU.API.Internal.DocumentLibTest.TestDoubles; +using LCU.Library.DocumentType.Models.Responses; +using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination; +using LCU.Library.Internal.DocumentModels.Requests.Search; +using MongoDB.Driver; + +namespace LCU.API.Internal.DocumentLibTest.Repositories; + +public class DocumentInformationRepositoryTest : IDisposable +{ + private readonly IDocumentInformationRepository _repository; + + private readonly FakeMongoCollectionFactory _collectionFactory = new(); + + public DocumentInformationRepositoryTest() + { + _repository = new DocumentInformationRepository(_collectionFactory); + } + + [SetUp] + public async Task SetUp() + { + var collection = _collectionFactory.GetCollection(); + await collection.DeleteManyAsync(Builders.Filter.Empty); + } + + [Test] + public async Task GetDocumentInformation_NoMatchingId_ReturnsNull() + { + await SearchableDocumentFactory(d => d.CreatedDate = DateTime.Now.AddDays(1)); + var actualDocument = await _repository.GetDocumentInformation(Guid.NewGuid()); + + Assert.Null(actualDocument); + } + + [Test] + public async Task SearchDocumentInformation_NameMatchedExactly_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + Name = document.Name + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_NameNotMatched_DoesNotReturnDocument() + { + await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + Name = Guid.NewGuid().ToString() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_TypeMatchedExactly_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + Type = document.Type + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_TypeNotMatched_DoesNotReturnDocument() + { + await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + Type = Guid.NewGuid().ToString() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_ScanStatusMatchedExactly_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + ScanStatus = document.ScanStatus + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_ScanStatusNotMatched_DoesNotReturnDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + ScanStatus = !document.ScanStatus + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_SourceMatchedExactly_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + Source = document.Source + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_SourceNotMatched_DoesNotReturnDocument() + { + await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + Source = Guid.NewGuid().ToString() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_DataSourceMatchedExactly_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + DataSource = document.DataSource + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_DataSourceNotMatched_DoesNotReturnDocument() + { + await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + DataSource = Guid.NewGuid().ToString() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_TraceIdMatchedExactly_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + TraceId = document.TraceId + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_TraceIdNotMatched_DoesNotReturnDocument() + { + await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + TraceId = Guid.NewGuid() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_PersonNumbersEqualsMatchedExactly_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + PersonNumbersEquals = document.PersonNumbers.FirstOrDefault() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_PersonNumbersEqualsNotMatched_DoesNotReturnDocument() + { + await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + PersonNumbersEquals = Guid.NewGuid().ToString() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_PersonNumbersContainsMatchedExactly_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + PersonNumbersContains = document.PersonNumbers + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_PersonNumbersContainsPartiallyMatched_DoesNotReturnDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + PersonNumbersContains = EnumerableDataSeriesFactory.CreateSeries(() => Guid.NewGuid().ToString(), 5).Concat(document.PersonNumbers).ToArray() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_PersonNumbersContainsNotMatched_DoesNotReturnDocument() + { + await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + PersonNumbersContains = EnumerableDataSeriesFactory.CreateSeries(() => Guid.NewGuid().ToString(), 5).ToArray() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_PersonNumbersContainsAnyMatchedExactly_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + PersonNumbersContainsAny = document.PersonNumbers + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_PersonNumbersContainsAnyPartiallyMatched_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + PersonNumbersContainsAny = EnumerableDataSeriesFactory.CreateSeries(() => Guid.NewGuid().ToString(), 5).Concat(document.PersonNumbers).ToArray() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_PersonNumbersContainsAnyNoneMatched_DoesNotReturnDocument() + { + await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + PersonNumbersContainsAny = EnumerableDataSeriesFactory.CreateSeries(() => Guid.NewGuid().ToString(), 5).ToArray() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_OrganizationNumbersEqualsMatchedExactly_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + OrganizationNumbersEquals = document.OrganizationNumbers.FirstOrDefault() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_OrganizationNumbersEqualsNotMatched_DoesNotReturnDocument() + { + await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + OrganizationNumbersEquals = Guid.NewGuid().ToString() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_OrganizationNumbersContainsMatchedExactly_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + OrganizationNumbersContains = new [] { document.OrganizationNumbers.First() } + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_OrganizationNumbersContainsPartiallyMatched_DoesNotReturnDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + OrganizationNumbersContains = EnumerableDataSeriesFactory.CreateSeries(() => Guid.NewGuid().ToString(), 5).Concat(document.OrganizationNumbers).ToArray() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_OrganizationNumbersContainsNotMatched_DoesNotReturnDocument() + { + await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + OrganizationNumbersContains = EnumerableDataSeriesFactory.CreateSeries(() => Guid.NewGuid().ToString(), 5).ToArray() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_OrganizationNumbersContainsAnyMatchedExactly_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + OrganizationNumbersContainsAny = document.OrganizationNumbers + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_OrganizationNumbersContainsAnyPartiallyMatched_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + OrganizationNumbersContainsAny = EnumerableDataSeriesFactory.CreateSeries(() => Guid.NewGuid().ToString(), 5).Concat(document.OrganizationNumbers).ToArray() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_OrganizationNumbersContainsAnyNoneMatched_DoesNotReturnDocument() + { + await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + OrganizationNumbersContainsAny = EnumerableDataSeriesFactory.CreateSeries(() => Guid.NewGuid().ToString(), 5).ToArray() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_AccountNumberEqualsMatchedExactly_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + AccountNumberEquals = document.AccountNumber + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_AccountNumberEqualsNotMatched_DoesNotReturnDocument() + { + await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + AccountNumberEquals = Guid.NewGuid().ToString() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_AccountNumberContainsAnyNumberInFilter_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + AccountNumberContainsAny = EnumerableDataSeriesFactory.CreateSeries(() => Guid.NewGuid().ToString(), 5).Concat(new[]{ document.AccountNumber }).ToArray() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_AccountNumberContainsAnyNumberNotInFilter_DoesNotReturnDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + AccountNumberContainsAny = EnumerableDataSeriesFactory.CreateSeries(() => Guid.NewGuid().ToString(), 5).Concat(document.OrganizationNumbers).ToArray() + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_AttributesEqualsMatchedExactly_ReturnsDocument() + { + var attribute = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var document = await SearchableDocumentFactory(d => d.Attributes[attribute] = value); + var filter = new DocumentInformationFilter + { + AttributesEquals = new Dictionary + { + [attribute] = value + } + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_AttributesEqualsNotMatched_DoesNotReturnDocument() + { + var attribute = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + await SearchableDocumentFactory(d => d.Attributes[attribute] = value); + var filter = new DocumentInformationFilter + { + AttributesEquals = new Dictionary + { + [attribute] = Guid.NewGuid().ToString() + } + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_AttributesContainsAndAttributeMatchesExactly_ReturnsDocument() + { + var attribute = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var document = await SearchableDocumentFactory(d => d.Attributes[attribute] = value); + var filter = new DocumentInformationFilter + { + AttributesContains = new Dictionary + { + [attribute] = value + } + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_AttributesContainsAndAttributeMatchesDifferentCase_ReturnsDocument() + { + var attribute = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var document = await SearchableDocumentFactory(d => d.Attributes[attribute] = value); + var filter = new DocumentInformationFilter + { + AttributesContains = new Dictionary + { + [attribute] = value.ToLower() + } + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_AttributesContainsAndAttributeMatchesOnWordBoundary_ReturnsDocument() + { + var attribute = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var document = await SearchableDocumentFactory(d => d.Attributes[attribute] = $"{Guid.NewGuid()} {value},{Guid.NewGuid()}"); + var filter = new DocumentInformationFilter + { + AttributesContains = new Dictionary + { + [attribute] = value + } + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_AttributesContainsAndAttributeMatchesWithinAnotherWord_DoesNotReturnDocument() + { + var attribute = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + await SearchableDocumentFactory(d => d.Attributes[attribute] = $"{Guid.NewGuid()}{value}{Guid.NewGuid()}"); + var filter = new DocumentInformationFilter + { + AttributesContains = new Dictionary + { + [attribute] = value + } + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_AttributeContainsAnyAttributeMatchesExactlyOne_ReturnsDocument() + { + var attribute = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var document = await SearchableDocumentFactory(d => d.Attributes[attribute] = value); + var filter = new DocumentInformationFilter + { + AttributesContainsAny = new Dictionary + { + [attribute] = $"{Guid.NewGuid()},{value},{Guid.NewGuid()}" + } + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_AttributeContainsAnyAttributeMatchesOneDifferentCase_ReturnsDocument() + { + var attribute = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var document = await SearchableDocumentFactory(d => d.Attributes[attribute] = value); + var filter = new DocumentInformationFilter + { + AttributesContainsAny = new Dictionary + { + [attribute] = $"{Guid.NewGuid()},{value.ToLower()},{Guid.NewGuid()}" + } + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_AttributeContainsAnyAttributeMatchesNone_DoesNotReturnDocument() + { + var attribute = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + await SearchableDocumentFactory(d => d.Attributes[attribute] = value); + var filter = new DocumentInformationFilter + { + AttributesContainsAny = new Dictionary + { + [attribute] = $"{Guid.NewGuid()},{Guid.NewGuid()},{Guid.NewGuid()}" + } + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_AttributesContainsMatchAndAttributeContainsAnyMatch_ReturnsDocument() + { + var attributeContains = Guid.NewGuid().ToString(); + var attributeContainsAny = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var document = await SearchableDocumentFactory(d => + { + d.Attributes[attributeContains] = $"{Guid.NewGuid()},{value},{Guid.NewGuid()}"; + d.Attributes[attributeContainsAny] = value; + }); + var filter = new DocumentInformationFilter + { + AttributesContains = new Dictionary + { + [attributeContains] = value + }, + AttributesContainsAny = new Dictionary + { + [attributeContainsAny] = $"{Guid.NewGuid()},{value},{Guid.NewGuid()}" + } + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_AttributesContainsMatchAndAttributeContainsAnyNotMatched_DoesNotReturnDocument() + { + var attributeContains = Guid.NewGuid().ToString(); + var attributeContainsAny = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + await SearchableDocumentFactory(d => + { + d.Attributes[attributeContains] = $"{Guid.NewGuid()},{value},{Guid.NewGuid()}"; + d.Attributes[attributeContainsAny] = value; + }); + var filter = new DocumentInformationFilter + { + AttributesContains = new Dictionary + { + [attributeContains] = value + }, + AttributesContainsAny = new Dictionary + { + [attributeContainsAny] = $"{Guid.NewGuid()},{Guid.NewGuid()},{Guid.NewGuid()}" + } + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_AttributesContainsNotMatchedAndAttributeContainsAnyMatch_DoesNotReturnDocument() + { + var attributeContains = Guid.NewGuid().ToString(); + var attributeContainsAny = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + await SearchableDocumentFactory(d => + { + d.Attributes[attributeContains] = $"{Guid.NewGuid()},{value},{Guid.NewGuid()}"; + d.Attributes[attributeContainsAny] = value; + }); + var filter = new DocumentInformationFilter + { + AttributesContains = new Dictionary + { + [attributeContains] = Guid.NewGuid().ToString() + }, + AttributesContainsAny = new Dictionary + { + [attributeContainsAny] = $"{Guid.NewGuid()},{value},{Guid.NewGuid()}" + } + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_CreatedDateMinimumLessThanCreatedDate_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + CreatedDateMinimum = document.CreatedDate.AddDays(-1) + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_CreatedDateMinimumGreaterThanCreatedDate_DoesNotReturnDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + CreatedDateMinimum = document.CreatedDate.AddDays(1) + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_CreatedDateMaximumGreaterThanCreatedDate_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + CreatedDateMaximum = document.CreatedDate.AddDays(1) + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_CreatedDateMaximumLessThanCreatedDate_DoesNotReturnDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + CreatedDateMaximum = document.CreatedDate.AddDays(-1) + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_UploadDateMinimumLessThanCreatedDate_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + UploadDateMinimum = document.CreatedDate.AddDays(-1) + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_UploadDateMinimumGreaterThanCreatedDate_DoesNotReturnDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + UploadDateMinimum = document.CreatedDate.AddDays(1) + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_UploadDateMaximumGreaterThanCreatedDate_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + UploadDateMaximum = document.CreatedDate.AddDays(1) + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_UploadDateMaximumLessThanCreatedDate_DoesNotReturnDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + UploadDateMaximum = document.CreatedDate.AddDays(-1) + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_LastModifiedMinimumLessThanCreatedDate_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + LastModifiedMinimum = document.CreatedDate.AddDays(-1) + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_LastModifiedMinimumGreaterThanCreatedDate_DoesNotReturnDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + LastModifiedMinimum = document.CreatedDate.AddDays(1) + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_LastModifiedDateMaximumGreaterThanCreatedDate_ReturnsDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + LastModifiedDateMaximum = document.CreatedDate.AddDays(1) + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(1, documents.Count); + documents.Single().Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task SearchDocumentInformation_LastModifiedMaximumLessThanCreatedDate_DoesNotReturnDocument() + { + var document = await SearchableDocumentFactory(); + var filter = new DocumentInformationFilter + { + LastModifiedDateMaximum = document.CreatedDate.AddDays(-1) + }; + + var documents = await _repository.SearchDocumentInformation(filter); + + Assert.AreEqual(0, documents.Count); + } + + [Test] + public async Task SearchDocumentInformation_Skip_BypassesTheSkippedDocuments() + { + const int skip = 5; + var documents = await Task.WhenAll(EnumerableDataSeriesFactory.CreateSeries(async () => await SearchableDocumentFactory(), skip * 2)); + var filter = new DocumentInformationFilter { AccountNumberContainsAny = documents.Select(d => d.AccountNumber).ToArray() }; + + var searchResult = await _repository.SearchDocumentInformation(filter, skip); + + searchResult.Should().HaveSameCount(documents[skip..]); + searchResult.Should().AllSatisfy(r => documents.Should().ContainEquivalentOf(r, d => d.Using(DateTimeComparison.Truncated))); + } + + [Test] + public async Task SearchDocumentInformation_Take_OnlyReturnsThatManyDocuments() + { + const int take = 5; + var documents = await Task.WhenAll(EnumerableDataSeriesFactory.CreateSeries(async () => await SearchableDocumentFactory(), take * 2)); + var filter = new DocumentInformationFilter { AccountNumberContainsAny = documents.Select(d => d.AccountNumber).ToArray() }; + + var searchResult = await _repository.SearchDocumentInformation(filter, null, take); + + searchResult.Should().HaveCount(take); + searchResult.Should().AllSatisfy(r => documents.Should().ContainEquivalentOf(r, d => d.Using(DateTimeComparison.Truncated))); + } + + [Test] + public async Task GetDocumentInformationByBlobId_NoMatchingId_ReturnsNull() + { + await SearchableDocumentFactory(); + + var actualDocument = await _repository.GetDocumentInformationByBlobId(Guid.NewGuid()); + + Assert.Null(actualDocument); + } + + [Test] + public async Task SaveDocumentInformation_AllCases_InsertsDocumentAndReturnsIt() + { + var document = DocumentInformationFactory.CreateDocumentInformation(); + + var inserted = await _repository.SaveDocumentInformation(document); + + inserted.Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + var found = (await + (await _collectionFactory + .GetCollection() + .FindAsync(d => d.DocumentId == document.DocumentId)) + .ToListAsync() + ).SingleOrDefault(); + inserted.Should().BeEquivalentTo(found, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task UpdateDocumentInformation_DocumentDoesNotExist_ThrowsException() + { + var id = Guid.NewGuid(); + var document = DocumentInformationFactory.CreateDocumentInformation(); + + await _repository.UpdateDocumentInformation(id, document); + + var found = await _collectionFactory + .GetCollection() + .CountDocumentsAsync(d => d.DocumentId == id); + Assert.AreEqual(0, found); + } + + [Test] + public async Task UpdateDocumentInformation_DocumentExists_FullyReplacesWithNewAndReturnsNew() + { + var existingDocument = await SearchableDocumentFactory(); + var updatedDocument = DocumentInformationFactory.CreateDocumentInformation(); + updatedDocument.DocumentId = existingDocument.DocumentId; + + var updated = await _repository.UpdateDocumentInformation(existingDocument.DocumentId, updatedDocument); + + updated.Should().BeEquivalentTo(updatedDocument, d => d.Using(DateTimeComparison.Truncated)); + var found = (await + (await _collectionFactory + .GetCollection() + .FindAsync(d => d.DocumentId == existingDocument.DocumentId)) + .ToListAsync() + ).SingleOrDefault(); + updated.Should().BeEquivalentTo(found, d => d.Using(DateTimeComparison.Truncated)); + } + + [Test] + public async Task RemoveDocumentInformation_DocumentDoesNotExist_ReturnsNull() + { + var removed = await _repository.RemoveDocumentInformation(Guid.NewGuid()); + + Assert.IsNull(removed); + } + + [Test] + public async Task RemoveDocumentInformation_DocumentExists_DeletesAndReturnsDeleted() + { + var document = await SearchableDocumentFactory(); + + var removed = await _repository.RemoveDocumentInformation(document.DocumentId); + + removed.Should().BeEquivalentTo(document, d => d.Using(DateTimeComparison.Truncated)); + var found = await _collectionFactory + .GetCollection() + .CountDocumentsAsync(d => d.DocumentId == document.DocumentId); + Assert.AreEqual(0, found); + } + + [Test] + public async Task SearchDocumentInformation_SortsDocumentsWithOneSort() + { + var numberOfDocuments = 25; + var documents = await SearchableMultiDocumentFactory(numberOfDocuments); + + // Using UploadDate as CreatedDate has AddDays(i % 3) to create duplicates. + // If sorted by just CreatedDate, the order may be different when it is pulled from the collection causing the test to fail + var sort = new List + { + new() + { + Field = "UploadDate", + Position = 0, + IsDescending = true + } + }; + + // Sort descending on upload date + documents.Sort((a, b) => b.UploadDate.CompareTo(a.UploadDate)); + + var searchResult = await _repository.SearchDocumentInformation(new DocumentInformationFilter(), null, numberOfDocuments, sort); + + documents.Should().BeInDescendingOrder(a => a.UploadDate); + searchResult.Should().BeInDescendingOrder(a => a.UploadDate); + searchResult.Should().BeEquivalentTo(documents, d => d.Using(DateTimeComparison.Truncated).WithStrictOrdering()); + } + + [Test] + public async Task SearchDocumentInformation_SortsDocumentsWithMultipleSortsAndMaintainsPositionOrder() + { + var numberOfDocuments = 25; + var documents = await SearchableMultiDocumentFactory(numberOfDocuments); + + var sort = new List + { + new() + { + Field = "Name", + Position = 2, + IsDescending = false + }, + new() + { + Field = "CreatedDate", + Position = 0, + IsDescending = false + }, + new() + { + Field = "UploadDate", + Position = 1, + IsDescending = false + } + }; + + // Sort ascending on created date, then ascending on upload date, then ascending on name + documents = documents.OrderBy(x => x.CreatedDate).ThenBy(x => x.UploadDate).ThenBy(x => x.Name).ToList(); + + var searchResult = await _repository.SearchDocumentInformation(new DocumentInformationFilter(), null, numberOfDocuments, sort); + + searchResult.Should().BeEquivalentTo(documents, d => d.Using(DateTimeComparison.Truncated).WithStrictOrdering()); + } + + [Test] + public async Task GetDocumentsForCleanup_Success() + { + var documents = new List(); + + for (var i=0; i < 3; i++) + { + var document = DocumentInformationFactory.CreateDocumentInformation(); + document.CreatedDate = document.CreatedDate.AddMonths(i % 3 * -1); + document.UploadDate = document.UploadDate.AddDays(i); + + documents.Add(document); + } + + documents[1].DocumentBlobId = null; + documents = await SearchableMultiDocumentFactory(documents); + + var request = new GetDocumentCleanupRequest + { + DocumentCodeDetailInfos = new List + { + new() + { + DocumentCode = documents[0].Type, + InternalRetentionMonths = 3 + }, + new() + { + DocumentCode = documents[1].Type, + InternalRetentionMonths = 1 + }, + new() + { + DocumentCode = documents[2].Type, + InternalRetentionMonths = 1 + } + }, + Limit = 10 + }; + var expectedResults = documents.Where(document => + { + var docCodeDetail = request.DocumentCodeDetailInfos + .FirstOrDefault(docCodeDetail => document.Type == docCodeDetail.DocumentCode); + + return DateTime.Now >= document.CreatedDate.AddMonths(docCodeDetail!.InternalRetentionMonths) + && document.DocumentBlobId is not null; + }).Select(document => document.DocumentId).ToList(); + + var result = await _repository.GetDocumentsForCleanup(request); + + result.Should().BeEquivalentTo(expectedResults); + } + + [Test] + public async Task GetDocumentsForCleanup_Success_NoDocumentsFound() + { + var request = new GetDocumentCleanupRequest + { + DocumentCodeDetailInfos = new List + { + new() + { + DocumentCode = Guid.NewGuid().ToString(), + InternalRetentionMonths = 3 + } + }, + Limit = 10 + }; + + var result = await _repository.GetDocumentsForCleanup(request); + + Assert.IsEmpty(result); + } + + private async Task SearchableDocumentFactory(Action? documentSetup = null) + { + var document = DocumentInformationFactory.CreateDocumentInformation(); + documentSetup?.Invoke(document); + + var collection = _collectionFactory.GetCollection(); + await collection.InsertOneAsync(document); + + return document; + } + + private async Task> SearchableMultiDocumentFactory(int numberOfDocuments = 10, Action? documentSetup = null) + { + var documents = new List(); + + for(var i = 0; i < numberOfDocuments; i++) + { + var document = DocumentInformationFactory.CreateDocumentInformation(); + + document.CreatedDate = document.CreatedDate.AddMonths(i % 3 * -1); + document.UploadDate = document.UploadDate.AddDays(i); + documentSetup?.Invoke(document); + + documents.Add(document); + } + + var collection = _collectionFactory.GetCollection(); + await collection.InsertManyAsync(documents); + + return documents; + } + + private async Task> SearchableMultiDocumentFactory(List documents) + { + var collection = _collectionFactory.GetCollection(); + await collection.InsertManyAsync(documents); + + return documents; + } + + public void Dispose() + { + _collectionFactory.Dispose(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/TestDataFactories/DocumentInformationFactory.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/TestDataFactories/DocumentInformationFactory.cs new file mode 100644 index 0000000..faa97f9 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/TestDataFactories/DocumentInformationFactory.cs @@ -0,0 +1,33 @@ +using LCU.API.Internal.DocumentLib.Containers; + +namespace LCU.API.Internal.DocumentLibTest.TestDataFactories; + +public static class DocumentInformationFactory +{ + public static DocumentInformation CreateDocumentInformation() => new() + { + DocumentId = Guid.NewGuid(), + DocumentBlobId = Guid.NewGuid(), + TraceId = Guid.NewGuid(), + Name = Guid.NewGuid().ToString(), + Type = Guid.NewGuid().ToString(), + Source = Guid.NewGuid().ToString(), + DataSource = Guid.NewGuid().ToString(), + FileSize = DateTime.Now.Millisecond, + ScanStatus = DateTime.Now.Millisecond % 2 == 0, + UploadDate = DateTime.UtcNow, + LastModifiedDate = DateTime.UtcNow, + CreatedDate = DateTime.UtcNow, + PersonNumbers = new[] { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + OrganizationNumbers = new []{ Guid.NewGuid().ToString() , Guid.NewGuid().ToString() }, + AccountNumber = Guid.NewGuid().ToString(), + Attributes = new Dictionary + { + ["PersonNumber"] = Guid.NewGuid().ToString(), + ["PersonNumbers"] = $"{Guid.NewGuid()},{Guid.NewGuid()}", + ["OrganizationNumber"] = Guid.NewGuid().ToString(), + ["OrganizationNumbers"] = $"{Guid.NewGuid()},{Guid.NewGuid()}", + ["AccountNumber"] = Guid.NewGuid().ToString() + }, + }; +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/TestDataFactories/EnumerableDataSeriesFactory.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/TestDataFactories/EnumerableDataSeriesFactory.cs new file mode 100644 index 0000000..feecf91 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/TestDataFactories/EnumerableDataSeriesFactory.cs @@ -0,0 +1,8 @@ +namespace LCU.API.Internal.DocumentLibTest.TestDataFactories; + +public static class EnumerableDataSeriesFactory +{ + public static IEnumerable CreateSeries(Func singleFactory, int count) => Enumerable + .Repeat(singleFactory, count) + .Select(f => f()); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/TestDoubles/FakeMongoCollectionFactory.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/TestDoubles/FakeMongoCollectionFactory.cs new file mode 100644 index 0000000..7831a03 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/TestDoubles/FakeMongoCollectionFactory.cs @@ -0,0 +1,19 @@ +using LCU.API.Internal.DocumentLib.Utilities; +using Mongo2Go; +using MongoDB.Driver; + +namespace LCU.API.Internal.DocumentLibTest.TestDoubles; + +public class FakeMongoCollectionFactory : IMongoCollectionFactory, IDisposable +{ + private readonly MongoDbRunner _runner = MongoDbRunner.Start(); + + public IMongoCollection GetCollection() => new MongoClient(_runner.ConnectionString) + .GetDatabase(typeof(T).Name) + .GetCollection(typeof(T).Name); + + public void Dispose() + { + _runner.Dispose(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/Usings.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/Usings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentLibTest/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Controllers/DocumentControllerTest.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Controllers/DocumentControllerTest.cs new file mode 100644 index 0000000..464f7dc --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Controllers/DocumentControllerTest.cs @@ -0,0 +1,1426 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using LCU.API.Internal.Document.Controllers; +using LCU.API.Internal.Document.Services; +using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination; +using LCU.Library.Internal.DocumentModels.Requests; +using LCU.Library.Internal.DocumentModels.Requests.Search; +using LCU.Library.Internal.DocumentModels.Responses; +using LCU.Library.Internal.Exceptions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Cosmos; +using Moq; +using NUnit.Framework; + +namespace LCU.API.Internal.DocumentTest.Controllers; + +public class DocumentControllerTest +{ + private DocumentController _controller = null!; + private Mock _mockDocumentService = null!; + private Mock _mockDocumentAuditService = null!; + + [SetUp] + public void Setup() + { + _mockDocumentService = new Mock(); + _mockDocumentAuditService = new Mock(); + + _controller = new DocumentController(_mockDocumentService.Object, + _mockDocumentAuditService.Object) + { + ControllerContext = + { + HttpContext = new DefaultHttpContext() + } + }; + } + + [Test] + public async Task Get_Document_OK() + { + var docId = Guid.NewGuid(); + var documentResponse = GenerateDocumentResponse(docId); + + _mockDocumentService.Setup(service => service.GetDocument(docId)).ReturnsAsync(documentResponse).Verifiable(); + + var result = await _controller.GetDocumentById(docId); + Assert.NotNull(result); + + var okResult = result as OkObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(200, okResult!.StatusCode); + + var okValue = okResult.Value as DocumentResponse; + Assert.NotNull(okValue); + + var expectedDocContent = documentResponse.Content; + var expectedDocInformation = documentResponse.DocumentInformation; + var returnedDocInformation = okValue!.DocumentInformation; + + Assert.AreEqual(expectedDocContent, okValue.Content); + Assert.AreEqual(expectedDocInformation.DocumentId, returnedDocInformation.DocumentId); + Assert.AreEqual(expectedDocInformation.Name, returnedDocInformation.Name); + Assert.AreEqual(expectedDocInformation.Type, returnedDocInformation.Type); + Assert.AreEqual(expectedDocInformation.Source, returnedDocInformation.Source); + Assert.AreEqual(expectedDocInformation.ScanStatus, returnedDocInformation.ScanStatus); + Assert.AreEqual(expectedDocInformation.UploadDate, returnedDocInformation.UploadDate); + Assert.AreEqual(expectedDocInformation.LastModifiedDate, returnedDocInformation.LastModifiedDate); + Assert.AreEqual(expectedDocInformation.Attributes, returnedDocInformation.Attributes); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Get_Document_Null_NoContent() + { + var docId = Guid.NewGuid(); + + _mockDocumentService.Setup(service => service.GetDocument(docId)).ReturnsAsync((DocumentResponse)null).Verifiable(); + + var result = await _controller.GetDocumentById(docId); + Assert.NotNull(result); + + var noContent = result as NotFoundObjectResult; + Assert.NotNull(noContent); + Assert.AreEqual(404, noContent!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Get_Document_ServiceException() + { + var docId = Guid.NewGuid(); + + _mockDocumentService.Setup(service => service.GetDocument(docId)) + .ThrowsAsync(new ServiceException("Thrown from unit test")).Verifiable(); + + var result = await _controller.GetDocumentById(docId); + Assert.NotNull(result); + + var okResult = result as ObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Get_Document_UnexpectedException() + { + var docId = Guid.NewGuid(); + + _mockDocumentService.Setup(service => service.GetDocument(docId)) + .ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + var result = await _controller.GetDocumentById(docId); + Assert.NotNull(result); + + var okResult = result as StatusCodeResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Get_DocumentByBlobId_Null_NoContent() + { + var blobId = Guid.NewGuid(); + + _mockDocumentService.Setup(service => service.GetDocumentByBlobId(blobId)).ReturnsAsync((byte[]?)null).Verifiable(); + + var result = await _controller.GetDocumentByBlobId(blobId); + Assert.NotNull(result); + + var noContent = result as NotFoundObjectResult; + Assert.NotNull(noContent); + Assert.AreEqual(404, noContent!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Get_DocumentByBlobId_ServiceException() + { + var blobId = Guid.NewGuid(); + + _mockDocumentService.Setup(service => service.GetDocumentByBlobId(blobId)) + .ThrowsAsync(new ServiceException("Thrown from unit test")).Verifiable(); + + var result = await _controller.GetDocumentByBlobId(blobId); + Assert.NotNull(result); + + var okResult = result as ObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Get_DocumentByBlobId_UnexpectedException() + { + var blobId = Guid.NewGuid(); + + _mockDocumentService.Setup(service => service.GetDocumentByBlobId(blobId)) + .ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + var result = await _controller.GetDocumentByBlobId(blobId); + Assert.NotNull(result); + + var okResult = result as StatusCodeResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Post_SearchDocumentInformation_Content_Ok() + { + var random = new Random(DateTime.Now.Millisecond); + var searchRequest = new DocumentInformationFilteredSearch + { + Filter = new DocumentInformationFilter(), + Skip = random.Next(), + Take = random.Next(), + SortDefinition = new List + { + new() + { + Field = "CreatedDate", + Position = 0, + IsDescending = false + } + } + }; + var docIdOne = Guid.NewGuid(); + var docIdTwo = Guid.NewGuid(); + var documentInformationResponse = new List + { + new() + { + DocumentId = docIdOne + }, + new() + { + DocumentId = docIdTwo + } + }; + _mockDocumentService + .Setup(x => x.SearchDocumentInformation(searchRequest.Filter, searchRequest.Skip, searchRequest.Take, searchRequest.SortDefinition)) + .ReturnsAsync(documentInformationResponse) + .Verifiable(); + + var result = await _controller.PostDocumentInformationByFilter(searchRequest); + + var okResult = result as OkObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(200, okResult!.StatusCode); + var documentsInformation = okResult.Value as List; + CollectionAssert.AreEqual(documentInformationResponse, documentsInformation); + } + + [Test] + public async Task Post_SearchDocumentInformation_Null_NoContent() + { + var documentInformationResponse = new List + { + Capacity = 0 + }; + var searchRequest = new DocumentInformationFilteredSearch + { + Filter = new DocumentInformationFilter() + }; + _mockDocumentService + .Setup(x => x.SearchDocumentInformation(searchRequest.Filter, It.IsAny(), It.IsAny(), It.IsAny?>())) + .ReturnsAsync(documentInformationResponse) + .Verifiable(); + + var result = await _controller.PostDocumentInformationByFilter(searchRequest); + + var noContentResult = result as NoContentResult; + Assert.NotNull(noContentResult); + Assert.AreEqual(204, noContentResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Post_SearchDocumentInformation_ServiceException() + { + var searchRequest = new DocumentInformationFilteredSearch + { + Filter = new DocumentInformationFilter() + }; + _mockDocumentService + .Setup(x => x.SearchDocumentInformation(searchRequest.Filter, It.IsAny(), It.IsAny(), It.IsAny?>())) + .ThrowsAsync(new ServiceException("Thrown from unit test")) + .Verifiable(); + + var result = await _controller.PostDocumentInformationByFilter(searchRequest); + + var okResult = result as ObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Post_SearchDocumentInformation_UnexpectedException() + { + var searchRequest = new DocumentInformationFilteredSearch + { + Filter = new DocumentInformationFilter() + }; + _mockDocumentService + .Setup(x => x.SearchDocumentInformation(searchRequest.Filter, It.IsAny(), It.IsAny(), It.IsAny?>())) + .ThrowsAsync(new Exception("Thrown from unit test")) + .Verifiable(); + + var result = await _controller.PostDocumentInformationByFilter(searchRequest); + + var statusCodeResult = result as StatusCodeResult; + Assert.NotNull(statusCodeResult); + Assert.AreEqual(500, statusCodeResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Get_DocumentAudit_Content_Ok() + { + var docId = Guid.NewGuid(); + var documentAuditResponse = new List + { + new DocumentFieldAuditResponse + { + DocumentId = docId, + Source = "Unit Test", + Date = DateTime.Parse("2022-04-28T17:32:35.093Z"), + UserId = "Test", + Field = "Test", + PreviousValue = "0", + CurrentValue = "1" + }, + new DocumentActionAuditResponse + { + DocumentId = docId, + Source = "Unit Test", + Date = DateTime.Parse("2022-04-28T17:32:35.093Z"), + UserId = "Test", + Action = DocumentAction.Download, + ActionDescription = "Testing" + } + }; + _mockDocumentAuditService + .Setup(service => service.GetDocumentAudits(docId)) + .ReturnsAsync(documentAuditResponse); + + var result = await _controller.GetDocumentAuditInformationById(docId); + + Assert.NotNull(result); + var okResult = result as OkObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(200, okResult!.StatusCode); + Assert.AreSame(documentAuditResponse, okResult.Value!); + } + + [Test] + public async Task Get_DocumentAudit_NoContent_Ok() + { + var docId = Guid.NewGuid(); + var documentAuditResponse = new List(); + _mockDocumentAuditService + .Setup(service => service.GetDocumentAudits(docId)) + .ReturnsAsync(documentAuditResponse); + + var result = await _controller.GetDocumentAuditInformationById(docId); + + Assert.NotNull(result); + var okResult = result as NoContentResult; + Assert.NotNull(okResult); + Assert.AreEqual(204, okResult!.StatusCode); + } + + [Test] + public async Task Get_DocumentAudit_ServiceException() + { + var docId = Guid.NewGuid(); + + _mockDocumentAuditService.Setup(service => service.GetDocumentAudits(docId)) + .ThrowsAsync(new ServiceException("Thrown from unit test")).Verifiable(); + + var result = await _controller.GetDocumentAuditInformationById(docId); + Assert.NotNull(result); + + var okResult = result as ObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Get_DocumentAudit_UnexpectedException() + { + var docId = Guid.NewGuid(); + + _mockDocumentAuditService.Setup(service => service.GetDocumentAudits(docId)) + .ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + var result = await _controller.GetDocumentAuditInformationById(docId); + Assert.NotNull(result); + + var okResult = result as StatusCodeResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentAuditService.Verify(); + } + + [Test] + public async Task GetDocumentAuditInformationByIds_AuditServiceThrowsServiceException_Returns500() + { + var documentIds = new[] { Guid.NewGuid(), Guid.NewGuid() }; + _mockDocumentAuditService + .Setup(s => s.GetDocumentAudits(documentIds)) + .ThrowsAsync(new ServiceException(string.Empty)); + + var response = await _controller.GetDocumentAuditInformationByIds(documentIds); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status500InternalServerError, ((ObjectResult)response).StatusCode); + } + + [Test] + public async Task GetDocumentAuditInformationByIds_AuditServiceThrowsException_Returns500() + { + var documentIds = new[] { Guid.NewGuid(), Guid.NewGuid() }; + _mockDocumentAuditService + .Setup(s => s.GetDocumentAudits(documentIds)) + .ThrowsAsync(new Exception()); + + var response = await _controller.GetDocumentAuditInformationByIds(documentIds); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status500InternalServerError, ((StatusCodeResult)response).StatusCode); + } + + [Test] + public async Task GetDocumentAuditInformationByIds_AuditServiceDoesNotThrow_ReturnsOkWithLogs() + { + var documentIds = new[] { Guid.NewGuid(), Guid.NewGuid() }; + var auditLogs = new List + { + new DocumentFieldAuditResponse(), + new DocumentActionAuditResponse() + }; + _mockDocumentAuditService + .Setup(s => s.GetDocumentAudits(documentIds)) + .ReturnsAsync(auditLogs); + + var response = await _controller.GetDocumentAuditInformationByIds(documentIds); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status200OK, ((OkObjectResult)response).StatusCode); + Assert.AreEqual(auditLogs, ((OkObjectResult)response).Value); + } + + [Test] + public async Task ExternalActionPerformed_ExternalActionNotAllowed_Returns400() + { + var documentId = Guid.NewGuid(); + var request = new DocumentActionNotificationRequest + { + Action = (DocumentAction)DateTime.Now.Millisecond, + Source = Guid.NewGuid().ToString() + }; + + var response = await _controller.ExternalActionPerformed(documentId, request); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status400BadRequest, ((ObjectResult)response).StatusCode); + } + + [Test] + public async Task ExternalActionPerformed_AuditServiceThrowsException_Returns500() + { + var userId = Guid.NewGuid().ToString(); + var documentId = Guid.NewGuid(); + var request = new DocumentActionNotificationRequest + { + Action = DocumentAction.Download, + Source = Guid.NewGuid().ToString() + }; + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + _mockDocumentAuditService + .Setup(s => s.AuditAction(documentId, userId, request.Source, request.Action)) + .ThrowsAsync(new Exception()); + var response = await _controller.ExternalActionPerformed(documentId, request); + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status500InternalServerError, ((StatusCodeResult)response).StatusCode); + } + + [Test] + public async Task ExternalActionPerformed_AuditServiceDoesNotThrow_ReturnsOk() + { + var userId = Guid.NewGuid().ToString(); + var documentId = Guid.NewGuid(); + var request = new DocumentActionNotificationRequest + { + Action = DocumentAction.Download, + Source = Guid.NewGuid().ToString() + }; + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + _mockDocumentAuditService + .Setup(s => s.AuditAction(documentId, userId, request.Source, request.Action)) + .Verifiable(); + var response = await _controller.ExternalActionPerformed(documentId, request); + _mockDocumentAuditService.Verify(); + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status200OK, ((OkResult)response).StatusCode); + } + + [Test] + public async Task Upload_Document_OK() + { + var docId = Guid.NewGuid(); + var docRequest = GenerateDocumentRequest(); + + var docInformation = new DocumentInformationResponse + { + DocumentId = docId + }; + + _mockDocumentService.Setup(service => service.UploadDocument(docRequest)).ReturnsAsync(docInformation) + .Verifiable(); + + var result = await _controller.UploadDocument(docRequest); + Assert.NotNull(result); + + var okResult = result as CreatedAtActionResult; + Assert.NotNull(okResult); + Assert.AreEqual(201, okResult!.StatusCode); + + var okValue = okResult.Value as DocumentInformationResponse; + Assert.NotNull(okValue); + + Assert.AreEqual(docInformation.DocumentId, okValue!.DocumentId); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Upload_Document_ServiceException() + { + var docRequest = GenerateDocumentRequest(); + + _mockDocumentService.Setup(service => service.UploadDocument(docRequest)) + .ThrowsAsync(new ServiceException("Thrown from unit test")).Verifiable(); + + var result = await _controller.UploadDocument(docRequest); + Assert.NotNull(result); + + var okResult = result as ObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Upload_Document_UnexpectedException() + { + var docRequest = GenerateDocumentRequest(); + + _mockDocumentService.Setup(service => service.UploadDocument(docRequest)) + .ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + var result = await _controller.UploadDocument(docRequest); + Assert.NotNull(result); + + var okResult = result as StatusCodeResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_Document_OK() + { + var userId = Guid.NewGuid().ToString(); + var docId = Guid.NewGuid(); + var updateDocRequest = GenerateUpdateDocumentRequest(); + + var docInformation = new DocumentInformationResponse + { + DocumentId = docId + }; + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocument(updateDocRequest, docId, userId)) + .ReturnsAsync(docInformation).Verifiable(); + + var result = await _controller.UpdateDocument(updateDocRequest, docId); + Assert.NotNull(result); + + var okResult = result as OkObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(200, okResult!.StatusCode); + + var okValue = okResult.Value as DocumentInformationResponse; + Assert.NotNull(okValue); + + Assert.AreEqual(docInformation.DocumentId, okValue!.DocumentId); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_Document_Null_NotFound() + { + var userId = Guid.NewGuid().ToString(); + var docId = Guid.NewGuid(); + var updateDocRequest = GenerateUpdateDocumentRequest(); + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocument(updateDocRequest, docId, userId)) + .ReturnsAsync((DocumentInformationResponse?)null).Verifiable(); + + var result = await _controller.UpdateDocument(updateDocRequest, docId); + Assert.NotNull(result); + + var okResult = result as NotFoundObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(404, okResult!.StatusCode); + + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_Document_ServiceException() + { + var userId = Guid.NewGuid().ToString(); + var docId = Guid.NewGuid(); + var updateDocRequest = GenerateUpdateDocumentRequest(); + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocument(updateDocRequest, docId, userId)) + .ThrowsAsync(new ServiceException("Thrown from unit test")).Verifiable(); + + var result = await _controller.UpdateDocument(updateDocRequest, docId); + Assert.NotNull(result); + + var okResult = result as ObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_Document_UnexpectedException() + { + var userId = Guid.NewGuid().ToString(); + var docId = Guid.NewGuid(); + var updateDocRequest = GenerateUpdateDocumentRequest(); + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocument(updateDocRequest, docId, userId)) + .ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + var result = await _controller.UpdateDocument(updateDocRequest, docId); + Assert.NotNull(result); + + var okResult = result as StatusCodeResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_DocumentInformation_OK() + { + var userId = Guid.NewGuid().ToString(); + var docId = Guid.NewGuid(); + var updateDocInformationRequest = GenerateUpdateDocumentInformationRequest(); + + var docInformation = new DocumentInformationResponse + { + DocumentId = docId + }; + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocumentInformation(updateDocInformationRequest, docId, userId)) + .ReturnsAsync(docInformation).Verifiable(); + + var result = await _controller.UpdateDocumentInformation(updateDocInformationRequest, docId); + Assert.NotNull(result); + + var okResult = result as OkObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(200, okResult!.StatusCode); + + var okValue = okResult.Value as DocumentInformationResponse; + Assert.NotNull(okValue); + + Assert.AreEqual(docInformation.DocumentId, okValue!.DocumentId); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_DocumentInformation_Null_NotFound() + { + var userId = Guid.NewGuid().ToString(); + var docId = Guid.NewGuid(); + var updateDocInformationRequest = GenerateUpdateDocumentInformationRequest(); + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocumentInformation(updateDocInformationRequest, docId, userId)) + .ReturnsAsync((DocumentInformationResponse?)null).Verifiable(); + + var result = await _controller.UpdateDocumentInformation(updateDocInformationRequest, docId); + Assert.NotNull(result); + + var okResult = result as NotFoundObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(404, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_DocumentInformation_ServiceException() + { + var userId = Guid.NewGuid().ToString(); + var docId = Guid.NewGuid(); + var updateDocInformationRequest = GenerateUpdateDocumentInformationRequest(); + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocumentInformation(updateDocInformationRequest, docId, userId)) + .ThrowsAsync(new ServiceException("Thrown from unit test")).Verifiable(); + + var result = await _controller.UpdateDocumentInformation(updateDocInformationRequest, docId); + Assert.NotNull(result); + + var okResult = result as ObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_DocumentInformation_UnexpectedException() + { + var userId = Guid.NewGuid().ToString(); + var docId = Guid.NewGuid(); + var updateDocInformationRequest = GenerateUpdateDocumentInformationRequest(); + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocumentInformation(updateDocInformationRequest, docId, userId)) + .ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + var result = await _controller.UpdateDocumentInformation(updateDocInformationRequest, docId); + Assert.NotNull(result); + + var okResult = result as StatusCodeResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Get_DocumentInformation_OK() + { + var docId = Guid.NewGuid(); + var documentResponse = GenerateDocumentResponse(docId).DocumentInformation; + + _mockDocumentService.Setup(service => service.GetDocumentInformation(docId)).ReturnsAsync(documentResponse).Verifiable(); + + var result = await _controller.GetDocumentInformation(docId); + Assert.NotNull(result); + + var okResult = result as OkObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(200, okResult!.StatusCode); + + var okValue = okResult.Value as DocumentInformationResponse; + Assert.NotNull(okValue); + + var expectedDocInformation = documentResponse; + var returnedDocInformation = okValue!; + + Assert.AreEqual(expectedDocInformation.DocumentId, returnedDocInformation.DocumentId); + Assert.AreEqual(expectedDocInformation.Name, returnedDocInformation.Name); + Assert.AreEqual(expectedDocInformation.Type, returnedDocInformation.Type); + Assert.AreEqual(expectedDocInformation.Source, returnedDocInformation.Source); + Assert.AreEqual(expectedDocInformation.ScanStatus, returnedDocInformation.ScanStatus); + Assert.AreEqual(expectedDocInformation.UploadDate, returnedDocInformation.UploadDate); + Assert.AreEqual(expectedDocInformation.LastModifiedDate, returnedDocInformation.LastModifiedDate); + Assert.AreEqual(expectedDocInformation.Attributes, returnedDocInformation.Attributes); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Get_DocumentInformation_Null_NotFound() + { + var docId = Guid.NewGuid(); + + _mockDocumentService.Setup(service => service.GetDocumentInformation(docId)).ReturnsAsync((DocumentInformationResponse)null).Verifiable(); + + var result = await _controller.GetDocumentInformation(docId); + Assert.NotNull(result); + + var notFound = result as NotFoundObjectResult; + Assert.NotNull(notFound); + Assert.AreEqual(404, notFound!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Get_DocumentInformation_ServiceException() + { + var docId = Guid.NewGuid(); + + _mockDocumentService.Setup(service => service.GetDocumentInformation(docId)) + .ThrowsAsync(new ServiceException("Thrown from unit test")).Verifiable(); + + var result = await _controller.GetDocumentInformation(docId); + Assert.NotNull(result); + + var okResult = result as ObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Get_DocumentInformation_UnexpectedException() + { + var docId = Guid.NewGuid(); + + _mockDocumentService.Setup(service => service.GetDocumentInformation(docId)) + .ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + var result = await _controller.GetDocumentInformation(docId); + Assert.NotNull(result); + + var okResult = result as StatusCodeResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_DocumentSetStatus_OK() + { + var userId = Guid.NewGuid().ToString(); + var updateDocumentScanStatusRequest = GenerateUpdateDocumentScanStatusRequest(); + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocumentScanStatus(true, updateDocumentScanStatusRequest.DocumentBlobIds, userId)) + .Verifiable(); + + var result = await _controller.UpdateDocumentScanStatus(updateDocumentScanStatusRequest); + Assert.NotNull(result); + + var okResult = result as OkResult; + Assert.NotNull(okResult); + Assert.AreEqual(200, okResult!.StatusCode); + + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_DocumentSetStatus_ServiceException() + { + var userId = Guid.NewGuid().ToString(); + var updateDocumentScanStatusRequest = GenerateUpdateDocumentScanStatusRequest(); + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocumentScanStatus(true, updateDocumentScanStatusRequest.DocumentBlobIds, userId)) + .ThrowsAsync(new ServiceException("Thrown from unit test")).Verifiable(); + + var result = await _controller.UpdateDocumentScanStatus(updateDocumentScanStatusRequest); + Assert.NotNull(result); + + var okResult = result as ObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_DocumentSetStatus_UnexpectedException() + { + var userId = Guid.NewGuid().ToString(); + var updateDocumentScanStatusRequest = GenerateUpdateDocumentScanStatusRequest(); + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocumentScanStatus(true, updateDocumentScanStatusRequest.DocumentBlobIds, userId)) + .ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + var result = await _controller.UpdateDocumentScanStatus(updateDocumentScanStatusRequest); + Assert.NotNull(result); + + var okResult = result as StatusCodeResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_DocumentContent_OK() + { + var userId = Guid.NewGuid().ToString(); + var docId = Guid.NewGuid(); + var updateDocContentRequest = GenerateUpdateDocumentContentRequest(); + + var docInformation = new DocumentInformationResponse + { + DocumentId = docId + }; + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocumentContent(updateDocContentRequest, docId, userId)) + .ReturnsAsync(docInformation).Verifiable(); + + var result = await _controller.UpdateDocumentContent(updateDocContentRequest, docId); + Assert.NotNull(result); + + var okResult = result as OkObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(200, okResult!.StatusCode); + + var okValue = okResult.Value as DocumentInformationResponse; + Assert.NotNull(okValue); + + Assert.AreEqual(docInformation.DocumentId, okValue!.DocumentId); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_DocumentContent_Null_NotFound() + { + var userId = Guid.NewGuid().ToString(); + var docId = Guid.NewGuid(); + var updateDocContentRequest = GenerateUpdateDocumentContentRequest(); + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocumentContent(updateDocContentRequest, docId, userId)) + .ReturnsAsync((DocumentInformationResponse?)null).Verifiable(); + + var result = await _controller.UpdateDocumentContent(updateDocContentRequest, docId); + Assert.NotNull(result); + + var okResult = result as NotFoundObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(404, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_DocumentContent_ServiceException() + { + var userId = Guid.NewGuid().ToString(); + var docId = Guid.NewGuid(); + var updateDocContentRequest = GenerateUpdateDocumentContentRequest(); + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocumentContent(updateDocContentRequest, docId, userId)) + .ThrowsAsync(new ServiceException("Thrown from unit test")).Verifiable(); + + var result = await _controller.UpdateDocumentContent(updateDocContentRequest, docId); + Assert.NotNull(result); + + var okResult = result as ObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Update_DocumentContent_UnexpectedException() + { + var userId = Guid.NewGuid().ToString(); + var docId = Guid.NewGuid(); + var updateDocContentRequest = GenerateUpdateDocumentContentRequest(); + + _controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("name", userId) })); + + _mockDocumentService.Setup(service => service.UpdateDocumentContent(updateDocContentRequest, docId, userId)) + .ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + var result = await _controller.UpdateDocumentContent(updateDocContentRequest, docId); + Assert.NotNull(result); + + var okResult = result as StatusCodeResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Delete_Document_OK() + { + var docId = Guid.NewGuid(); + + _mockDocumentService.Setup(service => service.DeleteDocument(docId)).Verifiable(); + + var result = await _controller.DeleteDocument(docId); + Assert.NotNull(result); + + var okResult = result as OkResult; + Assert.AreEqual(200, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Delete_Document_ServiceException() + { + var docId = Guid.NewGuid(); + + _mockDocumentService.Setup(service => service.DeleteDocument(docId)) + .ThrowsAsync(new ServiceException("Thrown from unit test")).Verifiable(); + + var result = await _controller.DeleteDocument(docId); + Assert.NotNull(result); + + var okResult = result as ObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Delete_Document_UnexpectedException() + { + var docId = Guid.NewGuid(); + + _mockDocumentService.Setup(service => service.DeleteDocument(docId)) + .ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + var result = await _controller.DeleteDocument(docId); + Assert.NotNull(result); + + var okResult = result as StatusCodeResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Delete_DocumentContent_OK() + { + var request = new DeleteDocumentContentRequest() + { + DocumentId = Guid.NewGuid(), + Reason = Guid.NewGuid().ToString() + }; + + _mockDocumentService.Setup(service => service.DeleteDocumentContent(request)).Verifiable(); + + var result = await _controller.DeleteDocumentContent(request); + Assert.NotNull(result); + + var okResult = result as OkResult; + Assert.AreEqual(200, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Delete_DocumentContent_ServiceException() + { + var request = new DeleteDocumentContentRequest() + { + DocumentId = Guid.NewGuid(), + Reason = Guid.NewGuid().ToString() + }; + + _mockDocumentService.Setup(service => service.DeleteDocumentContent(request)) + .ThrowsAsync(new ServiceException("Thrown from unit test")).Verifiable(); + + var result = await _controller.DeleteDocumentContent(request); + Assert.NotNull(result); + + var okResult = result as ObjectResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task Delete_DocumentContent_UnexpectedException() + { + var request = new DeleteDocumentContentRequest() + { + DocumentId = Guid.NewGuid(), + Reason = Guid.NewGuid().ToString() + }; + + _mockDocumentService.Setup(service => service.DeleteDocumentContent(request)) + .ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + var result = await _controller.DeleteDocumentContent(request); + Assert.NotNull(result); + + var okResult = result as StatusCodeResult; + Assert.NotNull(okResult); + Assert.AreEqual(500, okResult!.StatusCode); + _mockDocumentService.Verify(); + } + + [Test] + public async Task CountDocuments_ServiceThrowsServiceException_Returns500() + { + var filter = new DocumentInformationFilter(); + _mockDocumentService + .Setup(d => d.CountDocuments(filter)) + .ThrowsAsync(new ServiceException(string.Empty)); + + var response = await _controller.CountDocuments(filter); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status500InternalServerError, ((StatusCodeResult)response).StatusCode); + } + + [Test] + public async Task CountDocuments_ServiceThrowsException_Returns500() + { + var filter = new DocumentInformationFilter(); + _mockDocumentService + .Setup(d => d.CountDocuments(filter)) + .ThrowsAsync(new Exception()); + + var response = await _controller.CountDocuments(filter); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status500InternalServerError, ((StatusCodeResult)response).StatusCode); + } + + [Test] + public async Task CountDocuments_ServiceDoesNotThrow_ReturnsOkAndCount() + { + var filter = new DocumentInformationFilter(); + var expectedCount = DateTime.Now.Millisecond; + _mockDocumentService + .Setup(d => d.CountDocuments(filter)) + .ReturnsAsync(expectedCount); + + var response = await _controller.CountDocuments(filter); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status200OK, ((OkObjectResult)response).StatusCode); + Assert.AreEqual(expectedCount, ((OkObjectResult)response).Value); + } + + [Test] + public async Task GetMemberDocumentById_DocumentServiceReturnsNull_Returns404() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + _mockDocumentService + .Setup(d => d.GetDocument(number, documentId)) + .ReturnsAsync((DocumentResponse?)null); + + var response = await _controller.GetMemberDocumentById(number, documentId); + + Assert.IsInstanceOf(response); + } + + [Test] + public async Task GetMemberDocumentById_DocumentServiceReturnsDocumentResponse_Returns200WithDocumentResponse() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + var document = new DocumentResponse(); + _mockDocumentService + .Setup(d => d.GetDocument(number, documentId)) + .ReturnsAsync(document); + + var response = await _controller.GetMemberDocumentById(number, documentId); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status200OK, ((ObjectResult)response).StatusCode); + Assert.AreSame(document, ((OkObjectResult)response).Value); + } + + [Test] + public async Task GetMemberDocumentById_DocumentServiceThrowsServiceException_Returns500() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + _mockDocumentService + .Setup(d => d.GetDocument(number, documentId)) + .ThrowsAsync(new ServiceException(string.Empty)); + + var response = await _controller.GetMemberDocumentById(number, documentId); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status500InternalServerError, ((ObjectResult)response).StatusCode); + } + + [Test] + public async Task GetMemberDocumentById_DocumentServiceThrowsException_Returns500() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + _mockDocumentService + .Setup(d => d.GetDocument(number, documentId)) + .ThrowsAsync(new Exception()); + + var response = await _controller.GetMemberDocumentById(number, documentId); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status500InternalServerError, ((StatusCodeResult)response).StatusCode); + } + + [Test] + public async Task GetDocumentContentByBlobId_DocumentServiceReturnsNull_Returns404() + { + var documentId = Guid.NewGuid(); + _mockDocumentService + .Setup(d => d.GetDocumentByBlobId(documentId)) + .ReturnsAsync((byte[]?)null); + + var response = await _controller.GetDocumentContentByBlobId(documentId); + + Assert.IsInstanceOf(response); + } + + [Test] + public async Task GetDocumentContentByBlobId_DocumentServiceReturnsBytes_Returns200WithBytes() + { + var documentId = Guid.NewGuid(); + var bytes = Encoding.Default.GetBytes(Guid.NewGuid().ToString()); + _mockDocumentService + .Setup(d => d.GetDocumentByBlobId(documentId)) + .ReturnsAsync(bytes); + + var response = await _controller.GetDocumentContentByBlobId(documentId); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status200OK, ((ObjectResult)response).StatusCode); + Assert.AreSame(bytes, ((OkObjectResult)response).Value); + } + + [Test] + public async Task GetDocumentContentByBlobId_DocumentServiceThrowsServiceException_Returns500() + { + var documentId = Guid.NewGuid(); + _mockDocumentService + .Setup(d => d.GetDocumentByBlobId(documentId)) + .ThrowsAsync(new ServiceException(string.Empty)); + + var response = await _controller.GetDocumentContentByBlobId(documentId); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status500InternalServerError, ((ObjectResult)response).StatusCode); + } + + [Test] + public async Task GetDocumentContentByBlobId_DocumentServiceThrowsException_Returns500() + { + var documentId = Guid.NewGuid(); + _mockDocumentService + .Setup(d => d.GetDocumentByBlobId(documentId)) + .ThrowsAsync(new Exception()); + + var response = await _controller.GetDocumentContentByBlobId(documentId); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status500InternalServerError, ((StatusCodeResult)response).StatusCode); + } + + [Test] + public async Task GetMemberDocumentContentByBlobId_DocumentServiceReturnsNull_Returns404() + { + var documentId = Guid.NewGuid(); + var number = DateTime.Now.Millisecond; + _mockDocumentService + .Setup(d => d.GetDocumentByBlobId(number, documentId)) + .ReturnsAsync((byte[]?)null); + + var response = await _controller.GetMemberDocumentContentByBlobId(number, documentId); + + Assert.IsInstanceOf(response); + } + + [Test] + public async Task GetMemberDocumentContentByBlobId_DocumentServiceReturnsBytes_Returns200WithBytes() + { + var documentId = Guid.NewGuid(); + var number = DateTime.Now.Millisecond; + var bytes = Encoding.Default.GetBytes(Guid.NewGuid().ToString()); + _mockDocumentService + .Setup(d => d.GetDocumentByBlobId(number, documentId)) + .ReturnsAsync(bytes); + + var response = await _controller.GetMemberDocumentContentByBlobId(number, documentId); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status200OK, ((ObjectResult)response).StatusCode); + Assert.AreSame(bytes, ((OkObjectResult)response).Value); + } + + [Test] + public async Task GetMemberDocumentContentByBlobId_DocumentServiceThrowsServiceException_Returns500() + { + var documentId = Guid.NewGuid(); + var number = DateTime.Now.Millisecond; + _mockDocumentService + .Setup(d => d.GetDocumentByBlobId(number, documentId)) + .ThrowsAsync(new ServiceException(string.Empty)); + + var response = await _controller.GetMemberDocumentContentByBlobId(number, documentId); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status500InternalServerError, ((ObjectResult)response).StatusCode); + } + + [Test] + public async Task GetMemberDocumentContentByBlobId_DocumentServiceThrowsException_Returns500() + { + var documentId = Guid.NewGuid(); + var number = DateTime.Now.Millisecond; + _mockDocumentService + .Setup(d => d.GetDocumentByBlobId(number, documentId)) + .ThrowsAsync(new Exception()); + + var response = await _controller.GetMemberDocumentContentByBlobId(number, documentId); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status500InternalServerError, ((StatusCodeResult)response).StatusCode); + } + + [Test] + public async Task GetDocumentsForCleanup_Success() + { + var documents = new List { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + _mockDocumentService.Setup(x => x.GetDocumentsForCleanup(It.IsAny())) + .ReturnsAsync(documents).Verifiable(); + + var response = await _controller.GetDocumentsForCleanup(It.IsAny()); + + Assert.AreSame(((OkObjectResult)response).Value, documents); + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status200OK, ((OkObjectResult)response).StatusCode); + _mockDocumentService.Verify(x => x.GetDocumentsForCleanup(It.IsAny()), Times.Once); + } + + [Test] + public async Task GetDocumentsForCleanup_Success_NoDocumentsFound() + { + var documents = new List(); + _mockDocumentService.Setup(x => x.GetDocumentsForCleanup(It.IsAny())) + .ReturnsAsync(documents).Verifiable(); + + var response = await _controller.GetDocumentsForCleanup(It.IsAny()); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status204NoContent, ((StatusCodeResult)response).StatusCode); + _mockDocumentService.Verify(x => x.GetDocumentsForCleanup(It.IsAny()), Times.Once); + } + + [Test] + public async Task GetDocumentsForCleanup_Failure_Exception() + { + _mockDocumentService.Setup(x => x.GetDocumentsForCleanup(It.IsAny())) + .ThrowsAsync(new Exception()); + + var response = await _controller.GetDocumentsForCleanup(It.IsAny()); + + Assert.IsInstanceOf(response); + Assert.AreEqual(StatusCodes.Status500InternalServerError, ((StatusCodeResult)response).StatusCode); + _mockDocumentService.Verify(x => x.GetDocumentsForCleanup(It.IsAny()), Times.Once); + } + + private static DocumentRequest GenerateDocumentRequest() + { + return new DocumentRequest + { + Name = "TestFile.txt", + Type = "Test", + Content = new byte[10], + Source = "Unit Test", + Attributes = new Dictionary + { + { "IsTest", "true" } + } + }; + } + + private static UpdateDocumentRequest GenerateUpdateDocumentRequest() + { + return new UpdateDocumentRequest + { + Content = new byte[10], + Name = "TestFile.txt", + Type = "Test", + UpdateSource = "Unit Test", + Attributes = new Dictionary + { + { "IsTest", "true" } + } + }; + } + + private static UpdateDocumentInformationRequest GenerateUpdateDocumentInformationRequest() + { + return new UpdateDocumentInformationRequest + { + Name = "TestFile.txt", + Type = "Test", + UpdateSource = "Unit Test", + Attributes = new Dictionary + { + { "IsTest", "true" } + } + }; + } + + private static UpdateDocumentScanStatusRequest GenerateUpdateDocumentScanStatusRequest() + { + return new UpdateDocumentScanStatusRequest + { + ScanStatus = true, + DocumentBlobIds = new List() + }; + } + + private static UpdateDocumentContentRequest GenerateUpdateDocumentContentRequest() + { + return new UpdateDocumentContentRequest + { + Content = new byte[10], + UpdateSource = "Unit Test" + }; + } + + private static DocumentResponse GenerateDocumentResponse(Guid docId) + { + return new DocumentResponse + { + DocumentInformation = new DocumentInformationResponse + { + DocumentId = docId, + Name = "TestFile.txt", + Type = "Test", + Source = "Developer's Laptop", + ScanStatus = null, + UploadDate = DateTime.Parse("2022-03-25T19:16:12.3665642Z"), + LastModifiedDate = DateTime.Parse("0001-01-01T00:00:00"), + Attributes = new Dictionary + { + { "IsTest", "true" }, + { "Owner", "Me Myself" } + } + } + }; + } +} diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/DataAccess/BlobApiDaoTest.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/DataAccess/BlobApiDaoTest.cs new file mode 100644 index 0000000..d5cab0a --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/DataAccess/BlobApiDaoTest.cs @@ -0,0 +1,119 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Azure; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using LCU.API.Internal.DocumentLib.DataAccess; +using Moq; +using NUnit.Framework; + +namespace LCU.API.Internal.DocumentTest.DataAccess; + +public class BlobApiDaoTest +{ + + private BlobApiDao _dao; + private Mock _mockClient; + + [SetUp] + public void Setup() + { + var mockBlobClient = new Mock(); + var mockBlobContainerClient = new Mock(); + mockBlobClient.Setup(client => client.GetClient()).Returns(mockBlobContainerClient.Object); + + _dao = new BlobApiDao(mockBlobClient.Object); + _mockClient = new Mock(); + mockBlobContainerClient.Setup(client => client.GetBlobClient(It.IsAny())).Returns(_mockClient.Object); + } + + [Test] + public async Task SaveDocumentToBlob_Success() + { + var fileData = Encoding.UTF8.GetBytes("test"); + _mockClient.Setup( mock => mock.UploadAsync(It.IsAny())).Verifiable(); + + var result = await _dao.SaveDocumentToBlob(fileData); + + Assert.NotNull(result); + _mockClient.Verify(); + } + + [Test] + public void SaveDocumentToBlob_Exception_ThrowsException() + { + var fileData = Encoding.UTF8.GetBytes("test"); + _mockClient.Setup( mock => mock.UploadAsync(It.IsAny())).ThrowsAsync(new Exception("Exception thrown from unit test.")).Verifiable(); + + Assert.ThrowsAsync(async () => await _dao.SaveDocumentToBlob(fileData)); + _mockClient.Verify(); + } + + [Test] + public async Task UpdateDocumentInBlob_Success() + { + var blobDocId = Guid.NewGuid(); + var fileData = Encoding.UTF8.GetBytes("test"); + _mockClient.Setup( mock => mock.UploadAsync(It.IsAny(), true, CancellationToken.None)).Verifiable(); + + await _dao.UpdateDocumentInBlob(blobDocId, fileData); + + _mockClient.Verify(); + } + + [Test] + public void UpdateDocumentInBlob_Exception_ThrowsException() + { + var blobDocId = Guid.NewGuid(); + var fileData = Encoding.UTF8.GetBytes("test"); + _mockClient.Setup( mock => mock.UploadAsync(It.IsAny(), true, CancellationToken.None)).ThrowsAsync(new Exception("Exception thrown from unit test.")).Verifiable(); + + Assert.ThrowsAsync(async () => await _dao.UpdateDocumentInBlob(blobDocId, fileData)); + _mockClient.Verify(); + } + + [Test] + public async Task RemoveDocumentFromBlob_Success() + { + var blobDocId = Guid.NewGuid(); + _mockClient.Setup(mock => mock.DeleteAsync(DeleteSnapshotsOption.IncludeSnapshots, It.IsAny(), CancellationToken.None)).Verifiable(); + + await _dao.RemoveDocumentFromBlob(blobDocId); + + _mockClient.Verify(); + } + + [Test] + public void RemoveDocumentFromBlob_Exception_ThrowsException() + { + var blobDocId = Guid.NewGuid(); + _mockClient.Setup( mock => mock.DeleteAsync(DeleteSnapshotsOption.IncludeSnapshots, It.IsAny(), CancellationToken.None)).ThrowsAsync(new Exception("Exception thrown from unit test.")).Verifiable(); + + Assert.ThrowsAsync(async () => await _dao.RemoveDocumentFromBlob(blobDocId)); + _mockClient.Verify(); + } + + [Test] + public async Task GetDocumentFromBlob_Success() + { + var blobDocId = Guid.NewGuid(); + _mockClient.Setup(mock => mock.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(),It.IsAny())).Verifiable(); + + await _dao.GetDocumentFromBlob(blobDocId); + + _mockClient.Verify(); + } + + [Test] + public void GetDocumentFromBlob_Exception_ThrowsException() + { + var blobDocId = Guid.NewGuid(); + _mockClient.Setup( mock => mock.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())).ThrowsAsync(new Exception("Exception thrown from unit test.")).Verifiable(); + + Assert.ThrowsAsync(async () => await _dao.GetDocumentFromBlob(blobDocId)); + _mockClient.Verify(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/LCU.API.Internal.DocumentTest.csproj b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/LCU.API.Internal.DocumentTest.csproj new file mode 100644 index 0000000..c2b6fef --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/LCU.API.Internal.DocumentTest.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + + false + + latest + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/BlobApiServiceTest.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/BlobApiServiceTest.cs new file mode 100644 index 0000000..0973274 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/BlobApiServiceTest.cs @@ -0,0 +1,175 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Azure; +using Azure.Storage.Blobs.Models; +using LCU.API.Internal.Document.Exceptions; +using LCU.API.Internal.Document.Services; +using LCU.API.Internal.DocumentLib.DataAccess; +using Moq; +using NUnit.Framework; + +namespace LCU.API.Internal.DocumentTest.Services; + +public class BlobApiServiceTest +{ + private BlobApiService _blobApiService; + private Mock _mockBlobDao; + + [SetUp] + public void Setup() + { + _mockBlobDao = new Mock(); + _blobApiService = new BlobApiService(_mockBlobDao.Object); + } + + [Test] + public async Task GetDocumentContent_Success() + { + var docBlobId = Guid.NewGuid(); + await using var testStream = new MemoryStream(Encoding.UTF8.GetBytes("whatever")); + var mockContent = BlobsModelFactory.BlobDownloadStreamingResult(testStream); + var mockContentResponse = new Mock>(); + + mockContentResponse.Setup(content => content.Value).Returns(mockContent).Verifiable(); + _mockBlobDao + .Setup(x => x.GetDocumentFromBlob(docBlobId)).ReturnsAsync(mockContentResponse.Object) + .Verifiable(); + + var returnedDocInfo = await _blobApiService.GetDocumentContent(docBlobId); + + Assert.NotNull(returnedDocInfo); + _mockBlobDao.Verify(); + } + + [Test] + public void GetDocument_ContentValueIsNull_DocumentBlobServiceExceptionThrown() + { + var docBlobId = Guid.NewGuid(); + + _mockBlobDao + .Setup(x => x.GetDocumentFromBlob(docBlobId)).ReturnsAsync((Response)null) + .Verifiable(); + + Assert.ThrowsAsync(async() => await _blobApiService.GetDocumentContent(docBlobId)); + _mockBlobDao.Verify(); + } + + [Test] + public void GetDocument_ExceptionThrown_DocumentBlobServiceExceptionThrown() + { + var docBlobId = Guid.NewGuid(); + + _mockBlobDao + .Setup(x => x.GetDocumentFromBlob(docBlobId)).ThrowsAsync(new Exception("Thrown from unit test")) + .Verifiable(); + + Assert.ThrowsAsync(async() => await _blobApiService.GetDocumentContent(docBlobId)); + _mockBlobDao.Verify(); + } + + [Test] + public async Task UploadDocumentContent_Success() + { + var docContent = new byte[10]; + _mockBlobDao.Setup(x => x.SaveDocumentToBlob(docContent)).Verifiable(); + + await _blobApiService.UploadDocumentContent(docContent); + _mockBlobDao.Verify(); + } + + [Test] + public void UploadDocumentContent_Exception_ThrowsBlobApiServiceException() + { + var docContent = new byte[10]; + _mockBlobDao.Setup(x => x.SaveDocumentToBlob(docContent)).ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + Assert.ThrowsAsync(async() => await _blobApiService.UploadDocumentContent(docContent)); + _mockBlobDao.Verify(); + } + + [Test] + public async Task UpdateDocumentContent_Success() + { + var docBlobId = Guid.NewGuid(); + var docContent = new byte[10]; + _mockBlobDao.Setup(x => x.UpdateDocumentInBlob(docBlobId, docContent)).Verifiable(); + + await _blobApiService.UpdateDocumentContent(docBlobId, docContent); + _mockBlobDao.Verify(); + } + + [Test] + public void UpdateDocumentContent_Exception_ThrowsBlobApiServiceException() + { + var docBlobId = Guid.NewGuid(); + var docContent = new byte[10]; + _mockBlobDao.Setup(x => x.UpdateDocumentInBlob(docBlobId, docContent)).ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + Assert.ThrowsAsync(async() => await _blobApiService.UpdateDocumentContent(docBlobId, docContent)); + _mockBlobDao.Verify(); + } + + [Test] + public void UpdateDocumentContent_Exception_DocumentBlobIdIsNull() + { + Guid? docBlobId = null; + var docContent = new byte[10]; + + Assert.ThrowsAsync(async() => await _blobApiService.UpdateDocumentContent(docBlobId, docContent)); + } + + + [Test] + public async Task DeleteDocumentContent_Success() + { + var docBlobId = Guid.NewGuid(); + _mockBlobDao.Setup(x => x.RemoveDocumentFromBlob(docBlobId)).Verifiable(); + + await _blobApiService.DeleteDocumentContent(docBlobId); + _mockBlobDao.Verify(); + } + + [Test] + public void DeleteDocumentContent_Exception_ThrowsBlobApiServiceException() + { + var docBlobId = Guid.NewGuid(); + _mockBlobDao.Setup(x => x.RemoveDocumentFromBlob(docBlobId)).ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + Assert.ThrowsAsync(async() => await _blobApiService.DeleteDocumentContent(docBlobId)); + _mockBlobDao.Verify(); + } + + [Test] + public void DeleteDocumentContent_Exception_DocumentBlobIdIsNull() + { + Guid? docBlobId = null; + + Assert.ThrowsAsync(async() => await _blobApiService.DeleteDocumentContent(docBlobId)); + } + + [Test] + public async Task GetDocument_BlobGuidIsEmpty_ReturnsEmptyContent() + { + var docBlobId = Guid.Empty; + var emptyContent = Array.Empty(); + + var result = await _blobApiService.GetDocumentContent(docBlobId); + + Assert.AreEqual(emptyContent, result); + _mockBlobDao.Verify(s => s.GetDocumentFromBlob(It.IsAny()), Times.Never); + } + + [Test] + public async Task GetDocument_BlobGuidIsNull_ReturnsEmptyContent() + { + Guid? docBlobId = null; + var emptyContent = Array.Empty(); + + var result = await _blobApiService.GetDocumentContent(docBlobId); + + Assert.AreEqual(emptyContent, result); + _mockBlobDao.Verify(s => s.GetDocumentFromBlob(It.IsAny()), Times.Never); + } +} diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/DocumentAuditServiceTest.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/DocumentAuditServiceTest.cs new file mode 100644 index 0000000..6a6c310 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/DocumentAuditServiceTest.cs @@ -0,0 +1,557 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using LCU.API.Internal.Document.Exceptions; +using LCU.API.Internal.Document.Services; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.API.Internal.DocumentLib.Repositories; +using LCU.Library.Internal.DocumentModels.Requests; +using Moq; +using NUnit.Framework; + +namespace LCU.API.Internal.DocumentTest.Services; + +public class DocumentAuditServiceTest +{ + private DocumentAuditService _documentAuditService; + private Mock _mockDocumentAuditRepository; + + [SetUp] + public void Setup() + { + _mockDocumentAuditRepository = new Mock(); + _documentAuditService = new DocumentAuditService(_mockDocumentAuditRepository.Object); + } + + [Test] + public async Task GetDocumentAudits_RepositoryReturnsActionAudits_ReturnsAuditsTransformedToResponse() + { + var documentId = Guid.NewGuid(); + var actionAudits = new List + { + new DocumentActionAudit + ( + documentId, + Guid.NewGuid().ToString(), + DateTime.Now, + Guid.NewGuid().ToString(), + Valid(), + Guid.NewGuid().ToString() + ) + }; + _mockDocumentAuditRepository + .Setup(r => r.GetDocumentAudits(documentId)) + .ReturnsAsync(actionAudits); + + var audits = await _documentAuditService.GetDocumentAudits(documentId); + + actionAudits.Should().HaveSameCount(audits); + actionAudits.Should().AllSatisfy(a => audits.Should().ContainEquivalentOf(a)); + } + + [Test] + public async Task GetDocumentAudits_RepositoryReturnsFieldAudits_ReturnsAuditsTransformedToResponse() + { + var documentId = Guid.NewGuid(); + var actionAudits = new List + { + new DocumentFieldAudit + ( + documentId, + Guid.NewGuid().ToString(), + DateTime.Now, + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid(), + Guid.NewGuid() + ) + }; + _mockDocumentAuditRepository + .Setup(r => r.GetDocumentAudits(documentId)) + .ReturnsAsync(actionAudits); + + var audits = await _documentAuditService.GetDocumentAudits(documentId); + + actionAudits.Should().HaveSameCount(audits); + actionAudits.Should().AllSatisfy(a => audits.Should().ContainEquivalentOf(a)); + } + + [Test] + public async Task GetDocumentAudits_RepositoryReturnsAnyAuditsOfUnknownType_ThrowsDocumentAuditServiceException() + { + var documentId = Guid.NewGuid(); + var actionAudits = new List + { + new UnknownAuditType + ( + documentId, + Guid.NewGuid().ToString(), + DateTime.Now, + Guid.NewGuid().ToString() + ) + }; + _mockDocumentAuditRepository + .Setup(r => r.GetDocumentAudits(documentId)) + .ReturnsAsync(actionAudits); + + Assert.ThrowsAsync(() => _documentAuditService.GetDocumentAudits(documentId)); + } + + [Test] + public void GetDocumentAudits_DocumentAuditRepositoryThrowsException_ThrowsDocumentAuditServiceExceptionThrown() + { + var docId = Guid.NewGuid(); + _mockDocumentAuditRepository + .Setup(x => x.GetDocumentAudits(docId)) + .ThrowsAsync(new Exception("Thrown From Unit Test")) + .Verifiable(); + + Assert.ThrowsAsync(async () => await _documentAuditService.GetDocumentAudits(docId)); + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public void GetDocumentAudits_Exception_DocumentAuditServiceExceptionThrown() + { + var docId = Guid.NewGuid(); + _mockDocumentAuditRepository + .Setup(x => x.GetDocumentAudits(docId)) + .ThrowsAsync(new Exception("Thrown From Unit Test")) + .Verifiable(); + + Assert.ThrowsAsync(async () => await _documentAuditService.GetDocumentAudits(docId)); + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public async Task CreateDocumentUploadedAudit_Success() + { + var uploadDate = DateTime.Parse("2022-04-28T17:32:35.093Z"); + var documentInformation = GenerateDocumentInformation(); + + var audits = new DocumentAudit[] + { + new DocumentActionAudit + ( + documentInformation.DocumentId, + "System", + uploadDate, + "System", + DocumentAction.Upload, + "Document Uploaded" + ) + }; + + _mockDocumentAuditRepository + .Setup(x => x.SaveDocumentAudits(It.Is(list => + list.Length == 1 + && list.All(a => a is DocumentActionAudit) + && ((DocumentActionAudit)list.FirstOrDefault()!).Action == DocumentAction.Upload + && ((DocumentActionAudit)list.FirstOrDefault()!).UserId == "System" + && ((DocumentActionAudit)list.FirstOrDefault()!).Source == "System" + && ((DocumentActionAudit)list.FirstOrDefault()!).ActionDescription == "Document Uploaded"))) + .ReturnsAsync(audits) + .Verifiable(); + + var result = await _documentAuditService.CreateDocumentUploadedAudit(documentInformation.DocumentId, uploadDate); + var resultArray = result.ToArray(); + + Assert.NotNull(result); + audits.Should().HaveSameCount(resultArray); + audits.Should().AllSatisfy(a => resultArray.Should().ContainEquivalentOf(a)); + + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public void CreateDocumentUploadedAudit_DocumentAuditRepositoryThrowsException_ThrowsNewDocumentAuditServiceExceptionThrown() + { + var uploadDate = DateTime.Parse("2022-04-28T17:32:35.093Z"); + var documentInformation = GenerateDocumentInformation(); + + _mockDocumentAuditRepository + .Setup(x => x.SaveDocumentAudits(It.Is(list => list.Any()))) + .ThrowsAsync(new DocumentAuditServiceException("Thrown From Unit Test")) + .Verifiable(); + + Assert.ThrowsAsync(() => _documentAuditService.CreateDocumentUploadedAudit(documentInformation.DocumentId, uploadDate)); + + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public void CreateDocumentUploadedAudit_Exception_DocumentAuditServiceExceptionThrown() + { + var uploadDate = DateTime.Parse("2022-04-28T17:32:35.093Z"); + var documentInformation = GenerateDocumentInformation(); + + _mockDocumentAuditRepository + .Setup(x => x.SaveDocumentAudits(It.Is(list => list.Any()))) + .ThrowsAsync(new Exception("Thrown From Unit Test")) + .Verifiable(); + + Assert.ThrowsAsync(() => _documentAuditService.CreateDocumentUploadedAudit(documentInformation.DocumentId, uploadDate)); + + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public async Task CreateDocumentDeletedAudit_Success() + { + var deleteDate = DateTime.Parse("2022-04-28T17:32:35.093Z"); + var documentInformation = GenerateDocumentInformation(); + + var audits = new DocumentAudit[] + { + new DocumentActionAudit + ( + documentInformation.DocumentId, + "System", + deleteDate, + "System", + DocumentAction.Delete, + "Document Deleted" + ) + }; + + _mockDocumentAuditRepository + .Setup(x => x.SaveDocumentAudits(It.Is(list => + list.Length == 1 + && list.All(a => a is DocumentActionAudit) + && ((DocumentActionAudit)list.FirstOrDefault()!).Action == DocumentAction.Delete + && ((DocumentActionAudit)list.FirstOrDefault()!).UserId == "System" + && ((DocumentActionAudit)list.FirstOrDefault()!).Source == "System" + && ((DocumentActionAudit)list.FirstOrDefault()!).ActionDescription == "Document Deleted"))) + .ReturnsAsync(audits) + .Verifiable(); + + var result = await _documentAuditService.CreateDocumentDeletedAudit(documentInformation.DocumentId, deleteDate); + var resultArray = result.ToArray(); + + Assert.NotNull(result); + audits.Should().HaveSameCount(resultArray); + audits.Should().AllSatisfy(a => resultArray.Should().ContainEquivalentOf(a)); + + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public void CreateDocumentDeletedAudit_DocumentAuditRepositoryThrowsException_ThrowsNewDocumentAuditServiceExceptionThrown() + { + var deleteDate = DateTime.Parse("2022-04-28T17:32:35.093Z"); + var documentInformation = GenerateDocumentInformation(); + + _mockDocumentAuditRepository + .Setup(x => x.SaveDocumentAudits(It.Is(list => list.Any()))) + .ThrowsAsync(new DocumentAuditServiceException("Thrown From Unit Test")) + .Verifiable(); + + Assert.ThrowsAsync(() => _documentAuditService.CreateDocumentDeletedAudit(documentInformation.DocumentId, deleteDate)); + + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public void CreateDocumentDeletedAudit_Exception_DocumentAuditServiceExceptionThrown() + { + var deleteDate = DateTime.Parse("2022-04-28T17:32:35.093Z"); + var documentInformation = GenerateDocumentInformation(); + + _mockDocumentAuditRepository + .Setup(x => x.SaveDocumentAudits(It.Is(list => list.Any()))) + .ThrowsAsync(new Exception("Thrown From Unit Test")) + .Verifiable(); + + Assert.ThrowsAsync(() => _documentAuditService.CreateDocumentDeletedAudit(documentInformation.DocumentId, deleteDate)); + + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public async Task CreateDocumentContentDeletedAudit_Success() + { + var deleteDate = DateTime.Parse("2022-04-28T17:32:35.093Z"); + var documentInformation = GenerateDocumentInformation(); + var reason = Guid.NewGuid().ToString(); + var reasonFull = $"DocumentContent Deleted. Reason: {reason}"; + + var audits = new DocumentAudit[] + { + new DocumentActionAudit + ( + documentInformation.DocumentId, + "System", + deleteDate, + "System", + DocumentAction.DeleteContent, + reasonFull + ) + }; + + _mockDocumentAuditRepository + .Setup(x => x.SaveDocumentAudits(It.Is(list => + list.Length == 1 + && list.All(a => a is DocumentActionAudit) + && ((DocumentActionAudit)list.FirstOrDefault()!).Action == DocumentAction.DeleteContent + && ((DocumentActionAudit)list.FirstOrDefault()!).UserId == "System" + && ((DocumentActionAudit)list.FirstOrDefault()!).Source == "System" + && ((DocumentActionAudit)list.FirstOrDefault()!).ActionDescription == reasonFull))) + .ReturnsAsync(audits) + .Verifiable(); + + var result = await _documentAuditService.CreateDocumentContentDeletedAudit(documentInformation.DocumentId, deleteDate, reason); + var resultArray = result.ToArray(); + + Assert.NotNull(result); + audits.Should().HaveSameCount(resultArray); + audits.Should().AllSatisfy(a => resultArray.Should().ContainEquivalentOf(a)); + + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public void CreateDocumentContentDeletedAudit_DocumentAuditRepositoryThrowsException_ThrowsNewDocumentAuditServiceExceptionThrown() + { + var deleteDate = DateTime.Parse("2022-04-28T17:32:35.093Z"); + var documentInformation = GenerateDocumentInformation(); + var reason = Guid.NewGuid().ToString(); + + _mockDocumentAuditRepository + .Setup(x => x.SaveDocumentAudits(It.Is(list => list.Any()))) + .ThrowsAsync(new DocumentAuditServiceException("Thrown From Unit Test")) + .Verifiable(); + + Assert.ThrowsAsync(() => _documentAuditService.CreateDocumentContentDeletedAudit(documentInformation.DocumentId, deleteDate, reason)); + + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public void CreateDocumentContentDeletedAudit_Exception_DocumentAuditServiceExceptionThrown() + { + var deleteDate = DateTime.Parse("2022-04-28T17:32:35.093Z"); + var documentInformation = GenerateDocumentInformation(); + var reason = Guid.NewGuid().ToString(); + + _mockDocumentAuditRepository + .Setup(x => x.SaveDocumentAudits(It.Is(list => list.Any()))) + .ThrowsAsync(new Exception("Thrown From Unit Test")) + .Verifiable(); + + Assert.ThrowsAsync(() => _documentAuditService.CreateDocumentContentDeletedAudit(documentInformation.DocumentId, deleteDate, reason)); + + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public async Task CreateDocumentAudits_No_Differences_Success() + { + var updateDate = DateTime.Parse("2022-04-28T17:32:35.093Z"); + var docInformationOld = GenerateDocumentInformation(); + var docInformationNew = GenerateDocumentInformation(); + + var result = await _documentAuditService.CreateDocumentAudits(docInformationOld, docInformationNew, false, updateDate, "testUser"); + + Assert.NotNull(result); + Assert.IsEmpty(result); + _mockDocumentAuditRepository.Verify(x => x.SaveDocumentAudits(It.IsAny()), Times.Never); + } + + [Test] + public async Task CreateDocumentAudits_No_Differences_ContentChange_Success() + { + var updateDate = DateTime.Parse("2022-04-28T17:32:35.093Z"); + var userId = Guid.NewGuid().ToString(); + var docInformationOld = GenerateDocumentInformation(); + var docInformationNew = GenerateDocumentInformation(); + _mockDocumentAuditRepository + .Setup(x => x.SaveDocumentAudits(It.Is(list => + list.Length == 1 + && list.All(a => a is DocumentFieldAudit) + && ((DocumentFieldAudit)list.FirstOrDefault()!).Field == "Content" + && list.All(audit => string.Equals(userId, ((DocumentFieldAudit)audit).UserId))))) + .ReturnsAsync(new List()) + .Verifiable(); + + await _documentAuditService.CreateDocumentAudits(docInformationOld, docInformationNew, true, updateDate, userId); + + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public async Task CreateDocumentAudits_All_Differences_Success() + { + var updateDate = DateTime.Parse("2022-04-28T17:32:35.093Z"); + var docInformationOld = GenerateDocumentInformation(); + var docInformationEverythingChanges = GenerateDocumentInformation(); + var userId = Guid.NewGuid().ToString(); + docInformationEverythingChanges.Name = "BrandNewName"; + docInformationEverythingChanges.Type = "BrandNewType"; + docInformationEverythingChanges.ScanStatus = true; + docInformationEverythingChanges.DataSource = "NEW SOURCE"; + docInformationEverythingChanges.Attributes = new Dictionary + { + { "NEW_FIELD", "NEW_VALUE" } + }; + _mockDocumentAuditRepository + .Setup(x => x.SaveDocumentAudits(It.Is(list => + list.Length == 4 + && list.All(a => a is DocumentFieldAudit) + && list.Any(audit => string.Equals("Name", ((DocumentFieldAudit)audit).Field)) + && list.Any(audit => string.Equals("Type", ((DocumentFieldAudit)audit).Field)) + && list.Any(audit => string.Equals("ScanStatus", ((DocumentFieldAudit)audit).Field)) + && list.Any(audit => string.Equals("Attributes", ((DocumentFieldAudit)audit).Field)) + && list.All(audit => string.Equals(userId, ((DocumentFieldAudit)audit).UserId))))) + .ReturnsAsync(new List()) + .Verifiable(); + + await _documentAuditService.CreateDocumentAudits(docInformationOld, docInformationEverythingChanges, false, updateDate, userId); + + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public void CreateDocumentAudits_DocumentAuditRepositoryThrowsException_ThrowsNewDocumentAuditServiceExceptionThrown() + { + var updateDate = DateTime.Parse("2022-04-28T17:32:35.093Z"); + var docInformationOld = GenerateDocumentInformation(); + var docInformationNew = GenerateDocumentInformation(); + var userId = Guid.NewGuid().ToString(); + docInformationNew.Name = "NewName.txt"; + + _mockDocumentAuditRepository + .Setup(x => x.SaveDocumentAudits(It.Is(list => list.Any()))) + .ThrowsAsync(new DocumentAuditServiceException("Thrown From Unit Test")) + .Verifiable(); + + Assert.ThrowsAsync(() => _documentAuditService.CreateDocumentAudits(docInformationOld, docInformationNew, false, updateDate, userId)); + + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public void CreateDocumentAudits_Exception_DocumentAuditServiceExceptionThrown() + { + + var updateDate = DateTime.Parse("2022-04-28T17:32:35.093Z"); + var docInformationOld = GenerateDocumentInformation(); + var docInformationNew = GenerateDocumentInformation(); + var userId = Guid.NewGuid().ToString(); + docInformationNew.Name = "NewName.txt"; + _mockDocumentAuditRepository + .Setup(x => x.SaveDocumentAudits(It.Is(list => list.Any()))) + .ThrowsAsync(new Exception("Thrown From Unit Test")) + .Verifiable(); + + Assert.ThrowsAsync(() => _documentAuditService.CreateDocumentAudits(docInformationOld, docInformationNew, false, updateDate, userId)); + + _mockDocumentAuditRepository.Verify(); + } + + [Test] + public void AuditAction_RepositoryThrowsException_ThrowsDocumentAuditServiceException() + { + var documentId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var source = Guid.NewGuid().ToString(); + var action = Valid(); + _mockDocumentAuditRepository + .Setup(r => r.SaveDocumentAudits(It.Is(a => + a.DocumentId == documentId + && a.UserId == userId + && a.Source == source + && a.Action == action))) + .ThrowsAsync(new Exception()); + + Assert.ThrowsAsync(() => _documentAuditService.AuditAction(documentId, userId, source, action)); + } + + [Test] + public void AuditAction_UnknownActionType_ThrowsDocumentAuditServiceException() + { + var documentId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var source = Guid.NewGuid().ToString(); + var action = Invalid(); + + Assert.ThrowsAsync(() => _documentAuditService.AuditAction(documentId, userId, source, action)); + } + + private static DocumentAction[] AllDocumentActions => AllValid(); + + [TestCaseSource(nameof(AllDocumentActions))] + public async Task AuditAction_ValidAction_SavesActionAudit(DocumentAction action) + { + var documentId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var source = Guid.NewGuid().ToString(); + _mockDocumentAuditRepository + .Setup(r => r.SaveDocumentAudits(It.Is(a => + a.DocumentId == documentId + && a.UserId == userId + && a.Source == source + && a.Action == action))) + .Verifiable(); + + await _documentAuditService.AuditAction(documentId, userId, source, action); + + _mockDocumentAuditRepository.Verify(); + } + + private static List GenerateDocumentAudits() + { + return new List + { + new DocumentFieldAudit(Guid.NewGuid(), "UnitTest", DateTime.Parse("2022-04-28T17:32:35.093Z"), "Test", "Test", "0", "1") + }; + } + + private static DocumentInformation GenerateDocumentInformation() + { + return new DocumentInformation + { + DocumentId = Guid.NewGuid(), + Name = "TestFile.txt", + Type = "Test", + Source = "Developer's Laptop", + ScanStatus = null, + UploadDate = DateTime.Parse("2022-03-25T19:16:12.3665642Z"), + LastModifiedDate = DateTime.Parse("0001-01-01T00:00:00"), + Attributes = new Dictionary + { + { "IsTest", "true" }, + { "Owner", "Me Myself" } + } + }; + } + + private static T[] AllValid() + where T : struct, Enum + { + return Enum.GetValues(); + } + + private static T Valid() + where T : struct, Enum + { + var values = Enum.GetValues(); + + return values[DateTime.Now.Millisecond % values.Length]; + } + + private static T Invalid() + where T : struct, Enum + { + return (T)Convert.ChangeType(Enum.GetValues().Length + 1, typeof(int)); + } + + private class UnknownAuditType : DocumentAudit + { + public UnknownAuditType(Guid documentId, string source, DateTime date, string userId) + : base(documentId, source, date, userId) + { + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/DocumentInformationServiceTest.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/DocumentInformationServiceTest.cs new file mode 100644 index 0000000..bc01c52 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/DocumentInformationServiceTest.cs @@ -0,0 +1,455 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LCU.API.Internal.Document.Configurations; +using LCU.API.Internal.Document.Exceptions; +using LCU.API.Internal.Document.Services; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.API.Internal.DocumentLib.Repositories; +using LCU.Library.Internal.DocumentModels.Requests.Search; +using Moq; +using NUnit.Framework; + +namespace LCU.API.Internal.DocumentTest.Services; + +public class DocumentInformationServiceTest +{ + private DocumentInformationService _documentInformationService; + private Mock _mockDocumentRepository; + private ApiConfiguration _apiConfiguration; + + [SetUp] + public void Setup() + { + _mockDocumentRepository = new Mock(); + _apiConfiguration = new ApiConfiguration() { FileUploadCutoffHours = -2 }; + _documentInformationService = new DocumentInformationService(_mockDocumentRepository.Object, + _apiConfiguration); + } + + [Test] + public async Task GetDocumentInformation_Success() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + docInformation.UploadDate = DateTime.UtcNow.AddDays(-1); + docInformation.ScanStatus = true; + + _mockDocumentRepository + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + + var returnedDocInfo = await _documentInformationService.GetDocumentInformation(docId); + + Assert.NotNull(returnedDocInfo); + + Assert.AreEqual(docInformation.DocumentId, returnedDocInfo?.DocumentId); + Assert.AreEqual(docInformation.Name, returnedDocInfo?.Name); + Assert.AreEqual(docInformation.Type, returnedDocInfo?.Type); + Assert.AreEqual(docInformation.Source, returnedDocInfo?.Source); + Assert.AreEqual(docInformation.ScanStatus, returnedDocInfo?.ScanStatus); + Assert.AreEqual(docInformation.UploadDate, returnedDocInfo?.UploadDate); + Assert.AreEqual(docInformation.LastModifiedDate, returnedDocInfo?.LastModifiedDate); + Assert.AreEqual(docInformation.PersonNumbers, returnedDocInfo?.PersonNumbers); + Assert.AreEqual(docInformation.OrganizationNumbers, returnedDocInfo?.OrganizationNumbers); + Assert.AreEqual(docInformation.AccountNumber, returnedDocInfo?.AccountNumber); + Assert.AreEqual(docInformation.Attributes, returnedDocInfo?.Attributes); + _mockDocumentRepository.Verify(); + } + + [Test] + public async Task GetDocument_NoInformationPresent_Null() + { + var docId = Guid.NewGuid(); + + _mockDocumentRepository + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(null as DocumentInformation) + .Verifiable(); + + var result = await _documentInformationService.GetDocumentInformation(docId); + + Assert.Null(result); + _mockDocumentRepository.Verify(); + } + + [Test] + public async Task GetDocument_ScanStatusIsFalse_DocumentIsNull() + { + var docId = Guid.NewGuid(); + + var docInformation = GenerateDocumentInformation(); + docInformation.ScanStatus = false; + + _mockDocumentRepository + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + + var result = await _documentInformationService.GetDocumentInformation(docId); + + Assert.IsNull(result); + _mockDocumentRepository.Verify(); + } + + [Test] + public void GetDocument_ExceptionThrown_ThrowDocumentInformationServiceException() + { + var docId = Guid.NewGuid(); + + _mockDocumentRepository + .Setup(x => x.GetDocumentInformation(docId)) + .ThrowsAsync(new Exception("Thrown from unit test")) + .Verifiable(); + + Assert.ThrowsAsync(async () => await _documentInformationService.GetDocumentInformation(docId)); + _mockDocumentRepository.Verify(); + } + + [Test] + public async Task GetDocumentInformationByBlobId_Success() + { + var docBlobId = Guid.NewGuid(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + docInformation.ScanStatus = true; + + _mockDocumentRepository + .Setup(x => x.GetDocumentInformationByBlobId(docBlobId)).ReturnsAsync(docInformation) + .Verifiable(); + + var returnedDocInfo = await _documentInformationService.GetDocumentInformationByBlobId(docBlobId); + + Assert.NotNull(returnedDocInfo); + + Assert.AreEqual(docInformation.DocumentId, returnedDocInfo?.DocumentId); + Assert.AreEqual(docInformation.Name, returnedDocInfo?.Name); + Assert.AreEqual(docInformation.Type, returnedDocInfo?.Type); + Assert.AreEqual(docInformation.Source, returnedDocInfo?.Source); + Assert.AreEqual(docInformation.ScanStatus, returnedDocInfo?.ScanStatus); + Assert.AreEqual(docInformation.UploadDate, returnedDocInfo?.UploadDate); + Assert.AreEqual(docInformation.LastModifiedDate, returnedDocInfo?.LastModifiedDate); + Assert.AreEqual(docInformation.PersonNumbers, returnedDocInfo?.PersonNumbers); + Assert.AreEqual(docInformation.OrganizationNumbers, returnedDocInfo?.OrganizationNumbers); + Assert.AreEqual(docInformation.AccountNumber, returnedDocInfo?.AccountNumber); + Assert.AreEqual(docInformation.Attributes, returnedDocInfo?.Attributes); + _mockDocumentRepository.Verify(); + } + + [Test] + public async Task GetDocumentByBlobId_NoInformationPresent_Null() + { + var docBlobId = Guid.NewGuid(); + + _mockDocumentRepository + .Setup(x => x.GetDocumentInformationByBlobId(docBlobId)).ReturnsAsync(null as DocumentInformation) + .Verifiable(); + + var result = await _documentInformationService.GetDocumentInformationByBlobId(docBlobId); + + Assert.Null(result); + _mockDocumentRepository.Verify(); + } + + [Test] + public void GetDocumentByBlobId_ExceptionThrown_ThrowDocumentInformationServiceException() + { + var docBlobId = Guid.NewGuid(); + + _mockDocumentRepository + .Setup(x => x.GetDocumentInformationByBlobId(docBlobId)) + .ThrowsAsync(new Exception("Thrown from unit test")) + .Verifiable(); + + Assert.ThrowsAsync(async () => await _documentInformationService.GetDocumentInformationByBlobId(docBlobId)); + _mockDocumentRepository.Verify(); + } + + [Test] + public async Task DeleteDocument_Success() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentRepository + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + _mockDocumentRepository + .Setup(x => x.RemoveDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + + await _documentInformationService.DeleteDocumentInformation(docId); + _mockDocumentRepository.Verify(); + } + + [Test] + public async Task DeleteDocument_NoInformationPresent_Null() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentRepository + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(null as DocumentInformation) + .Verifiable(); + + await _documentInformationService.DeleteDocumentInformation(docId); + + _mockDocumentRepository.Verify(x => x.RemoveDocumentInformation(It.IsAny()), Times.Never); + _mockDocumentRepository.Verify(); + } + + [Test] + public void DeleteDocument_RemoveDocumentInfoThrowsException_ThrowDocumentInformationServiceException() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentRepository + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + + _mockDocumentRepository.Setup(x => x.RemoveDocumentInformation(docId)) + .ThrowsAsync(new Exception("thrown from unit test")).Verifiable(); + + Assert.ThrowsAsync(async () => await _documentInformationService.DeleteDocumentInformation(docId)); + _mockDocumentRepository.Verify(); + } + + [Test] + public async Task UploadDocument_Success() + { + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentRepository + .Setup(x => x.SaveDocumentInformation(It.Is(info => + info.DocumentBlobId == docBlobId))) + .ReturnsAsync(docInformation) + .Verifiable(); + + var result = await _documentInformationService.UploadDocumentInformation(docInformation); + + Assert.AreEqual(docInformation, result); + _mockDocumentRepository.Verify(); + } + + [Test] + public void UploadDocument_ExceptionThrown_ThrowDocumentInformation() + { + var docInformation = GenerateDocumentInformation(); + + _mockDocumentRepository.Setup(x => x.SaveDocumentInformation(It.IsAny())) + .ThrowsAsync(new Exception("Error thrown from unit test.")).Verifiable(); + + Assert.ThrowsAsync(async () => await _documentInformationService.UploadDocumentInformation(docInformation)); + _mockDocumentRepository.Verify(); + } + + [Test] + public async Task UpdateDocument_Success() + { + var docId = Guid.NewGuid(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + + _mockDocumentRepository.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable(); + _mockDocumentRepository + .Setup(x => x.UpdateDocumentInformation(docId, It.Is(info => + info.DocumentId == docId))).ReturnsAsync(docInformation) + .Verifiable(); + + await _documentInformationService.UpdateDocumentInformation(docId, docInformation); + _mockDocumentRepository.Verify(); + } + + [Test] + public async Task UpdateDocument_NoInformation_Null() + { + var docId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + + _mockDocumentRepository + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(null as DocumentInformation) + .Verifiable(); + + var result = await _documentInformationService.UpdateDocumentInformation(docId, docInformation); + + Assert.Null(result); + _mockDocumentRepository.Verify(); + } + + [Test] + public void UpdateDocument_ExceptionThrown_ThrowDocumentInformationServiceException() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentRepository.Setup(x => x.GetDocumentInformation(docId)).ThrowsAsync(new Exception("Thrown from unit test")).Verifiable(); + + Assert.ThrowsAsync(async () => await _documentInformationService.UpdateDocumentInformation(docId, docInformation)); + + _mockDocumentRepository.Verify( + x => x.UpdateDocumentInformation(It.IsAny(), It.IsAny()), Times.Never); + _mockDocumentRepository.Verify(); + } + + [Test] + public async Task SearchDocumentInformation_Success() + { + var searchFilter = GenerateDocumentInformationFilter(); + var random = new Random(DateTime.Now.Millisecond); + var skip = random.Next(); + var take = random.Next(); + var docInformation = new List { GenerateDocumentInformation() }; + _mockDocumentRepository + .Setup(x => x.SearchDocumentInformation(searchFilter, skip, take, null)) + .ReturnsAsync(docInformation); + + var result = await _documentInformationService.SearchDocumentInformation(searchFilter, skip, take); + + Assert.AreEqual(docInformation, result); + } + + [Test] + public async Task SearchDocumentInformation_NoneFound_EmptyList() + { + var searchFilter = GenerateDocumentInformationFilter(); + _mockDocumentRepository + .Setup(x => x.SearchDocumentInformation(searchFilter, It.IsAny(), It.IsAny(), null)) + .ReturnsAsync((List)null) + .Verifiable(); + + var result = await _documentInformationService.SearchDocumentInformation(searchFilter); + + _mockDocumentRepository.Verify(); + Assert.IsEmpty(result); + } + + [Test] + public void SearchDocumentInformation_ThrowsException_DocumentInformationServiceException() + { + var searchFilter = GenerateDocumentInformationFilter(); + _mockDocumentRepository + .Setup(x => x.SearchDocumentInformation(searchFilter, It.IsAny(), It.IsAny(), null)) + .ThrowsAsync(new Exception("Thrown from unit test")) + .Verifiable(); + + Assert.ThrowsAsync(async() => await _documentInformationService.SearchDocumentInformation(searchFilter)); + + _mockDocumentRepository.Verify(); + } + + [Test] + public void CountDocuments_RepositoryCountThrows_ThrowsDocumentInformationServiceException() + { + var filter = new DocumentInformationFilter(); + _mockDocumentRepository + .Setup(r => r.CountDocuments(filter)) + .ThrowsAsync(new Exception()); + + Assert.ThrowsAsync(() => _documentInformationService.CountDocuments(filter)); + } + + [Test] + public async Task CountDocuments_RepositoryReturnsCount_ReturnsCount() + { + var filter = new DocumentInformationFilter(); + var expectedCount = DateTime.Now.Millisecond; + _mockDocumentRepository + .Setup(r => r.CountDocuments(filter)) + .ReturnsAsync(expectedCount); + + var count = await _documentInformationService.CountDocuments(filter); + + Assert.AreEqual(expectedCount, count); + } + + [Test] + public async Task GetDocumentsForCleanup_Success() + { + var documents = new List { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + _mockDocumentRepository.Setup(x => x.GetDocumentsForCleanup(It.IsAny())) + .ReturnsAsync(documents).Verifiable(); + + var result = await _documentInformationService.GetDocumentsForCleanup(It.IsAny()); + + Assert.NotNull(result); + Assert.IsNotEmpty(result); + Assert.AreSame(documents, result); + _mockDocumentRepository.Verify(x => x.GetDocumentsForCleanup(It.IsAny()), Times.Once); + } + + [Test] + public async Task GetDocumentsForCleanup_Success_NoDocumentsFound() + { + var documents = new List(); + _mockDocumentRepository.Setup(x => x.GetDocumentsForCleanup(It.IsAny())) + .ReturnsAsync(documents).Verifiable(); + + var result = await _documentInformationService.GetDocumentsForCleanup(It.IsAny()); + + Assert.NotNull(result); + Assert.IsEmpty(result); + Assert.AreSame(documents, result); + _mockDocumentRepository.Verify(x => x.GetDocumentsForCleanup(It.IsAny()), Times.Once); + } + + [Test] + public void GetDocumentsForCleanup_ThrowsDocumentInformationServiceException() + { + _mockDocumentRepository + .Setup(r => r.GetDocumentsForCleanup(It.IsAny())) + .ThrowsAsync(new Exception()); + + Assert.ThrowsAsync(() => + _documentInformationService.GetDocumentsForCleanup(It.IsAny())); + } + + private static DocumentInformationFilter GenerateDocumentInformationFilter() + { + return new DocumentInformationFilter + { + Name = "TestSearch" + }; + } + + private static DocumentInformation GenerateDocumentInformation() + { + return new DocumentInformation + { + DocumentId = Guid.NewGuid(), + Name = "TestFile.txt", + Type = "Test", + Source = "Developer's Laptop", + DataSource = "DNA", + ScanStatus = null, + UploadDate = DateTime.Parse("2022-03-25T19:16:12.3665642Z"), + LastModifiedDate = DateTime.Parse("0001-01-01T00:00:00"), + PersonNumbers = new string[] { "1", "2" }, + OrganizationNumbers = new string[] { "3", "4" }, + AccountNumber = "5", + Attributes = new Dictionary + { + { "IsTest", "true" }, + { "Owner", "Me Myself" } + } + }; + } +} diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/DocumentServiceTest.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/DocumentServiceTest.cs new file mode 100644 index 0000000..657a4f1 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/DocumentServiceTest.cs @@ -0,0 +1,1630 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using LCU.API.Internal.Document.Configurations; +using LCU.API.Internal.Document.Exceptions; +using LCU.API.Internal.Document.Services; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.Library.Internal.DocumentModels.Requests; +using LCU.Library.Internal.DocumentModels.Requests.Search; +using LCU.Library.Internal.DocumentModels.Responses; +using LCU.Library.Internal.Exceptions; +using Moq; +using NUnit.Framework; + +namespace LCU.API.Internal.DocumentTest.Services; + +public class DocumentServiceTest +{ + private IDocumentService _documentService; + private IDocumentService DocumentServiceFactory(DocumentService.DocumentIsForMemberStrategy documentIsForMember) => new DocumentService + ( + _mockBlobApiService.Object, + _mockDocumentInformationService.Object, + _mockDocumentAuditService.Object, + _mockNotificationApiService.Object, + _apiConfiguration, + documentIsForMember + ); + + private Mock _mockBlobApiService; + private Mock _mockDocumentInformationService; + private Mock _mockDocumentAuditService; + private Mock _mockNotificationApiService; + private ApiConfiguration _apiConfiguration; + + [SetUp] + public void Setup() + { + _mockBlobApiService = new Mock(); + _mockDocumentInformationService = new Mock(); + _mockDocumentAuditService = new Mock(); + _mockNotificationApiService = new Mock(); + _apiConfiguration = new ApiConfiguration(); + + _documentService = DocumentServiceFactory(DocumentService.Ownership.CheckPersonOrOrgNumber.Strategy); + } + + [Test] + public async Task GetDocument_Success() + { + var docId = Guid.NewGuid(); + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + var docContent = new byte[10]; + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + + _mockBlobApiService.Setup(x => x.GetDocumentContent(docBlobId)).ReturnsAsync(docContent).Verifiable(); + + var result = await _documentService.GetDocument(docId); + + Assert.NotNull(result); + var returnedDocInfo = result!.DocumentInformation; + + Assert.AreEqual(docInformation.DocumentId, returnedDocInfo.DocumentId); + Assert.AreEqual(docInformation.Name, returnedDocInfo.Name); + Assert.AreEqual(docInformation.Type, returnedDocInfo.Type); + Assert.AreEqual(docInformation.Source, returnedDocInfo.Source); + Assert.AreEqual(docInformation.ScanStatus, returnedDocInfo.ScanStatus); + Assert.AreEqual(docInformation.UploadDate, returnedDocInfo.UploadDate); + Assert.AreEqual(docInformation.LastModifiedDate, returnedDocInfo.LastModifiedDate); + Assert.AreEqual(docInformation.CreatedDate, returnedDocInfo.CreatedDate); + Assert.AreEqual(docInformation.Attributes, returnedDocInfo.Attributes); + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public async Task GetDocument_NoInformationPresent_Null() + { + var docId = Guid.NewGuid(); + var docBlobId = Guid.NewGuid(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(null as DocumentInformation) + .Verifiable(); + + var result = await _documentService.GetDocument(docId); + + Assert.Null(result); + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public void GetDocument_DocumentInformationServiceExceptionThrown_ServiceException() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)) + .ThrowsAsync(new DocumentInformationServiceException("Thrown from unit test")) + .Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.GetDocument(docId)); + + _mockBlobApiService.Verify(x => x.GetDocumentContent(docBlobId), Times.Never); + _mockDocumentInformationService.Verify(); + } + + [Test] + public void GetDocument_BlobApiServiceException_ServiceException() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + + _mockBlobApiService.Setup(x => x.GetDocumentContent(docBlobId)) + .ThrowsAsync(new BlobApiServiceException("Thrown from Unit Test")).Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.GetDocument(docId)); + + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public void GetDocument_Exception_ServiceException() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + + _mockBlobApiService.Setup(x => x.GetDocumentContent(docBlobId)) + .ThrowsAsync(new Exception("Thrown from Unit Test")).Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.GetDocument(docId)); + + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public async Task DeleteDocument_Success() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + _mockDocumentInformationService + .Setup(x => x.DeleteDocumentInformation(docId)).Verifiable(); + _mockBlobApiService.Setup(x => x.DeleteDocumentContent(docBlobId)).Verifiable(); + + await _documentService.DeleteDocument(docId); + + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + _mockDocumentAuditService.Verify(x => x.CreateDocumentDeletedAudit(It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task DeleteDocument_NoInformationPresent_Null() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(null as DocumentInformation) + .Verifiable(); + + await _documentService.DeleteDocument(docId); + + _mockBlobApiService.Verify(x => x.DeleteDocumentContent(It.IsAny()), Times.Never); + _mockDocumentInformationService.Verify(x => x.DeleteDocumentInformation(It.IsAny()), Times.Never); + _mockDocumentAuditService.Verify(x => x.CreateDocumentDeletedAudit(It.IsAny(), It.IsAny()), Times.Never); + _mockDocumentInformationService.Verify(); + } + + [Test] + public void DeleteDocument_RemoveDocumentInfoThrowsDocumentInformationServiceException_ServiceException() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + + _mockDocumentInformationService.Setup(x => x.DeleteDocumentInformation(docId)) + .ThrowsAsync(new DocumentInformationServiceException("thrown from unit test")).Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.DeleteDocument(docId)); + + _mockBlobApiService.Verify(x => x.DeleteDocumentContent(It.IsAny()), Times.Never); + _mockDocumentAuditService.Verify(x => x.CreateDocumentDeletedAudit(It.IsAny(), It.IsAny()), Times.Never); + _mockDocumentInformationService.Verify(); + } + + [Test] + public void DeleteDocument_RemoveDocumentInfoThrowsException_ServiceException() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + + _mockDocumentInformationService.Setup(x => x.DeleteDocumentInformation(docId)) + .ThrowsAsync(new Exception("thrown from unit test")).Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.DeleteDocument(docId)); + + _mockBlobApiService.Verify(x => x.DeleteDocumentContent(It.IsAny()), Times.Never); + _mockDocumentAuditService.Verify(x => x.CreateDocumentDeletedAudit(It.IsAny(), It.IsAny()), Times.Never); + _mockDocumentInformationService.Verify(); + } + + [Test] + public void DeleteDocument_RemoveDocumentContentThrowsException_ServiceException() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + + _mockDocumentInformationService.Setup(x => x.DeleteDocumentInformation(docId)).Verifiable(); + _mockBlobApiService.Setup(x => x.DeleteDocumentContent(docBlobId)) + .ThrowsAsync(new BlobApiServiceException("thrown from test.")).Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.DeleteDocument(docId)); + + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public async Task DeleteDocumentContent_Success() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + var request = new DeleteDocumentContentRequest() + { + DocumentId = docId, + Reason = Guid.NewGuid().ToString() + }; + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + _mockBlobApiService.Setup(x => x.DeleteDocumentContent(docBlobId)).Verifiable(); + + await _documentService.DeleteDocumentContent(request); + + _mockDocumentInformationService.Verify(x => x.GetDocumentInformation(It.IsAny()), Times.Once); + _mockBlobApiService.Verify(x => x.DeleteDocumentContent(It.IsAny()), Times.Once); + _mockDocumentAuditService.Verify(x => x.CreateDocumentContentDeletedAudit(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task DeleteDocumentContent_NoInformationPresent_Null() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + var request = new DeleteDocumentContentRequest() + { + DocumentId = docId, + Reason = Guid.NewGuid().ToString() + }; + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(null as DocumentInformation) + .Verifiable(); + + await _documentService.DeleteDocumentContent(request); + + _mockBlobApiService.Verify(x => x.DeleteDocumentContent(It.IsAny()), Times.Never); + _mockDocumentInformationService.Verify(x => x.DeleteDocumentInformation(It.IsAny()), Times.Never); + _mockDocumentAuditService.Verify(x => x.CreateDocumentContentDeletedAudit(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + _mockDocumentInformationService.Verify(); + } + + [Test] + public void DeleteDocumentContent_RemoveDocumentContentThrowsException_ServiceException() + { + var docId = Guid.NewGuid(); + + var docBlobId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentBlobId = docBlobId; + + var request = new DeleteDocumentContentRequest() + { + DocumentId = docId, + Reason = Guid.NewGuid().ToString() + }; + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + + _mockBlobApiService.Setup(x => x.DeleteDocumentContent(docBlobId)) + .ThrowsAsync(new BlobApiServiceException("thrown from test.")).Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.DeleteDocumentContent(request)); + + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public async Task UploadDocument_Success() + { + var docBlobId = Guid.NewGuid(); + var uploadRequest = GenerateDocumentRequest(); + var docInformation = GenerateDocumentInformation(); + + _mockBlobApiService.Setup(x => x.UploadDocumentContent(uploadRequest.Content)).ReturnsAsync(docBlobId).Verifiable(); + _mockDocumentInformationService + .Setup(x => x.UploadDocumentInformation(It.Is(info => + info.DocumentBlobId == docBlobId && info.UploadDate.Date == DateTime.UtcNow.Date))) + .ReturnsAsync(docInformation) + .Verifiable(); + + var result = await _documentService.UploadDocument(uploadRequest); + + Assert.AreEqual(docInformation.DocumentId, result.DocumentId); + Assert.AreEqual(uploadRequest.Name, result.Name); + Assert.AreEqual(uploadRequest.Type, result.Type); + Assert.AreEqual(docInformation.Source, result.Source); + Assert.IsNull(docInformation.ScanStatus); + Assert.AreEqual(docInformation.UploadDate, result.UploadDate); + Assert.AreEqual(docInformation.CreatedDate, result.CreatedDate); + + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public void UploadDocument_ExceptionThrown_DuringSaveDocumentToBlob() + { + var uploadRequest = GenerateDocumentRequest(); + + _mockBlobApiService.Setup(x => x.UploadDocumentContent(uploadRequest.Content)) + .ThrowsAsync(new Exception("Error thrown from unit test.")).Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.UploadDocument(uploadRequest)); + + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(x => x.UploadDocumentInformation(It.IsAny()), Times.Never); + } + + [Test] + public void UploadDocument_BlobApiDaoExceptionThrown_DuringSaveDocumentToBlob() + { + var uploadRequest = GenerateDocumentRequest(); + + _mockBlobApiService.Setup(x => x.UploadDocumentContent(uploadRequest.Content)) + .ThrowsAsync(new BlobApiServiceException("Error thrown from unit test.")).Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.UploadDocument(uploadRequest)); + + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(x => x.UploadDocumentInformation(It.IsAny()), Times.Never); + } + + [Test] + public void UploadDocument_DocumentInformationServiceExceptionThrown_DuringSaveDocumentInformation() + { + var docBlobId = Guid.NewGuid(); + var uploadRequest = GenerateDocumentRequest(); + + _mockBlobApiService.Setup(x => x.UploadDocumentContent(uploadRequest.Content)).ReturnsAsync(docBlobId).Verifiable(); + _mockBlobApiService.Setup(x => x.DeleteDocumentContent(docBlobId)).Verifiable(); + + _mockDocumentInformationService + .Setup(x => x.UploadDocumentInformation(It.Is(info => + info.DocumentBlobId == docBlobId && info.UploadDate.Date == DateTime.UtcNow.Date))) + .ThrowsAsync(new DocumentInformationServiceException("Error thrown from unit test.")) + .Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.UploadDocument(uploadRequest)); + + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public async Task UpdateDocument_Success() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentRequest(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable(); + _mockBlobApiService.Setup(x => x.UpdateDocumentContent(docBlobId, updateRequest.Content)).Verifiable(); + _mockDocumentInformationService + .Setup(x => x.UpdateDocumentInformation(docId, It.Is(info => + info.DocumentId == docId && info.DocumentBlobId == docBlobId && + info.LastModifiedDate!.Value.Date == DateTime.UtcNow.Date))).ReturnsAsync(docInformation) + .Verifiable(); + _mockDocumentAuditService.Setup(x => x.CreateDocumentAudits(It.IsAny(), + It.IsAny(), true, It.Is(time => time.Date == DateTime.UtcNow.Date), It.Is(name => name == userId))) + .Verifiable(); + + await _documentService.UpdateDocument(updateRequest, docId, userId); + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + _mockDocumentAuditService.Verify(); + } + + [Test] + public async Task UpdateDocument_NoInformation_Null() + { + var docId = Guid.NewGuid(); + var updateRequest = GenerateUpdateDocumentRequest(); + var userId = Guid.NewGuid().ToString(); + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(null as DocumentInformation) + .Verifiable(); + + var result = await _documentService.UpdateDocument(updateRequest, docId, userId); + + Assert.Null(result); + _mockBlobApiService.Verify(x => x.UpdateDocumentContent(It.IsAny(), It.IsAny()), Times.Never); + _mockDocumentInformationService.Verify(); + } + + [Test] + public void UpdateDocument_BlobApiDaoExceptionThrown_DuringSaveDocumentToBlob() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentRequest(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable(); + + _mockBlobApiService.Setup(x => x.UpdateDocumentContent(docBlobId, updateRequest.Content)) + .ThrowsAsync(new BlobApiServiceException("Error thrown from unit test.")).Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.UpdateDocument(updateRequest, docId, userId)); + + _mockBlobApiService.Verify(); + + // Two calls total with one call to revert document information + _mockDocumentInformationService.Verify( + x => x.UpdateDocumentInformation(It.IsAny(), It.IsAny()), Times.Exactly(2)); + _mockDocumentInformationService.Verify( + x => x.UpdateDocumentInformation(It.IsAny(), docInformation), Times.Once); + + + } + [Test] + public void UpdateDocument_DocumentAuditRepositoryExceptionThrown_DuringCreateDocumentAudits() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentRequest(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable(); + + _mockDocumentAuditService.Setup(x => x.CreateDocumentAudits(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.Is(name => name == userId))) + .ThrowsAsync(new DocumentAuditServiceException("Error thrown from unit test.")).Verifiable(); + + Assert.ThrowsAsync(async () => + await _documentService.UpdateDocument(updateRequest, docId, userId)); + + // Two calls total with one call to revert document information + _mockDocumentInformationService.Verify( + x => x.UpdateDocumentInformation(It.IsAny(), It.IsAny()), Times.Exactly(2)); + _mockDocumentInformationService.Verify( + x => x.UpdateDocumentInformation(It.IsAny(), docInformation), Times.Once); + _mockDocumentAuditService.Verify(); + } + + [Test] + public void UpdateDocument_DocumentInformationRepositoryExceptionThrown_DuringSaveDocumentInformation() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentRequest(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable(); + _mockDocumentInformationService + .Setup(x => x.UpdateDocumentInformation(docId, It.Is(info => + info.DocumentBlobId == docBlobId && info.LastModifiedDate!.Value.Date == DateTime.UtcNow.Date))) + .ThrowsAsync(new DocumentInformationServiceException("Error thrown from unit test.")) + .Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.UpdateDocument(updateRequest, docId, userId)); + + // _mockBlobApiDao.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public void UpdateDocument_ExceptionThrown_ServiceExceptionThrown() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentRequest(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformation(docId)) + .ThrowsAsync(new Exception("Exception thrown from unit test")).Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.UpdateDocument(updateRequest, docId, userId)); + + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public async Task UpdateDocumentContent_Success() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentContentRequest(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable(); + _mockBlobApiService.Setup(x => x.UpdateDocumentContent(docBlobId, updateRequest.Content)).Verifiable(); + _mockDocumentInformationService + .Setup(x => x.UpdateDocumentInformation(docId, It.Is(info => + info.DocumentId == docId && info.DocumentBlobId == docBlobId && + info.LastModifiedDate!.Value.Date == DateTime.UtcNow.Date))).ReturnsAsync(docInformation) + .Verifiable(); + _mockDocumentAuditService.Setup(x => x.CreateDocumentAudits(It.IsAny(), + It.IsAny(), true, It.Is(time => time.Date == DateTime.UtcNow.Date), It.Is(name => name == userId))) + .Verifiable(); + + await _documentService.UpdateDocumentContent(updateRequest, docId, userId); + + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + _mockDocumentAuditService.Verify(); + } + + [Test] + public async Task UpdateDocumentContent_NoInformation_Null() + { + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentContentRequest(); + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(null as DocumentInformation) + .Verifiable(); + + var result = await _documentService.UpdateDocumentContent(updateRequest, docId, userId); + + Assert.Null(result); + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public void UpdateDocumentContent_BlobApiDaoExceptionThrown_DuringUpdateDocumentBlob() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentContentRequest(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable(); + + _mockBlobApiService.Setup(x => x.UpdateDocumentContent(docBlobId, updateRequest.Content)) + .ThrowsAsync(new BlobApiServiceException("Error thrown from unit test.")).Verifiable(); + + Assert.ThrowsAsync(async () => + await _documentService.UpdateDocumentContent(updateRequest, docId, userId)); + + _mockBlobApiService.Verify(); + + // Two calls total with one call to revert document information + _mockDocumentInformationService.Verify( + x => x.UpdateDocumentInformation(It.IsAny(), It.IsAny()), Times.Exactly(2)); + _mockDocumentInformationService.Verify( + x => x.UpdateDocumentInformation(It.IsAny(), docInformation), Times.Once); + } + + [Test] + public void UpdateDocumentContent_DocumentAuditExceptionThrown_DuringCreateDocumentAudits() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentContentRequest(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable(); + _mockDocumentAuditService.Setup(x => x.CreateDocumentAudits(docInformation, It.IsAny(), It.IsAny(), It.IsAny(), It.Is(name => name == userId))) + .ThrowsAsync(new DocumentAuditServiceException("Error thrown from unit test.")).Verifiable(); + + Assert.ThrowsAsync(async () => + await _documentService.UpdateDocumentContent(updateRequest, docId, userId)); + + // Two calls total with one call to revert document information + _mockDocumentInformationService.Verify( + x => x.UpdateDocumentInformation(It.IsAny(), It.IsAny()), Times.Exactly(2)); + _mockDocumentInformationService.Verify( + x => x.UpdateDocumentInformation(It.IsAny(), docInformation), Times.Once); + } + + [Test] + public void UpdateDocumentContent_DocumentInformationRepositoryExceptionThrown_DuringUpdateDocumentInformation() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentContentRequest(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable(); + _mockDocumentInformationService + .Setup(x => x.UpdateDocumentInformation(docId, It.Is(info => + info.DocumentBlobId == docBlobId && info.LastModifiedDate!.Value.Date == DateTime.UtcNow.Date))) + .ThrowsAsync(new DocumentInformationServiceException("Error thrown from unit test.")) + .Verifiable(); + + Assert.ThrowsAsync(async () => + await _documentService.UpdateDocumentContent(updateRequest, docId, userId)); + + _mockDocumentInformationService.Verify(); + } + + [Test] + public void UpdateDocumentContent_ExceptionThrown_DuringUpdateDocumentInformation() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentContentRequest(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable(); + _mockDocumentInformationService + .Setup(x => x.UpdateDocumentInformation(docId, It.Is(info => + info.DocumentBlobId == docBlobId && info.LastModifiedDate!.Value.Date == DateTime.UtcNow.Date))) + .ThrowsAsync(new Exception("Error thrown from unit test.")) + .Verifiable(); + + Assert.ThrowsAsync(async () => + await _documentService.UpdateDocumentContent(updateRequest, docId, userId)); + + _mockDocumentInformationService.Verify(); + } + + [Test] + public async Task UpdateDocumentInformation_Success() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentInformationRequest(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable(); + _mockDocumentInformationService + .Setup(x => x.UpdateDocumentInformation(docId, It.Is(info => + info.DocumentId == docId && info.DocumentBlobId == docBlobId && + info.LastModifiedDate!.Value.Date == DateTime.UtcNow.Date))).ReturnsAsync(docInformation) + .Verifiable(); + _mockDocumentAuditService.Setup(x => x.CreateDocumentAudits(It.IsAny(), + It.IsAny(), false, It.Is(time => time.Date == DateTime.UtcNow.Date), It.Is(name => name == userId))) + .Verifiable(); + + await _documentService.UpdateDocumentInformation(updateRequest, docId, userId); + + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + _mockDocumentAuditService.Verify(); + } + + [Test] + public async Task UpdateDocumentInformation_NoInformation_Null() + { + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentInformationRequest(); + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(null as DocumentInformation) + .Verifiable(); + + var result = await _documentService.UpdateDocumentInformation(updateRequest, docId, userId); + + Assert.Null(result); + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public void UpdateDocumentInformation_DocumentInformationRepositoryExceptionThrown_DuringUpdateDocumentInformation() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentInformationRequest(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable(); + _mockDocumentInformationService + .Setup(x => x.UpdateDocumentInformation(docId, It.Is(info => + info.DocumentBlobId == docBlobId && info.LastModifiedDate!.Value.Date == DateTime.UtcNow.Date))) + .ThrowsAsync(new DocumentInformationServiceException("Error thrown from unit test.")) + .Verifiable(); + + Assert.ThrowsAsync(async () => + await _documentService.UpdateDocumentInformation(updateRequest, docId, userId)); + + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public void UpdateDocumentInformation_DocumentAuditRepositoryExceptionThrown_DuringCreateDocumentAudits() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentInformationRequest(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable(); + _mockDocumentAuditService.Setup(x => x.CreateDocumentAudits(docInformation, It.IsAny(), It.IsAny(), It.IsAny(), It.Is(name => name == userId))) + .ThrowsAsync(new DocumentAuditServiceException("Error thrown from unit test.")).Verifiable(); + + Assert.ThrowsAsync(async () => + await _documentService.UpdateDocumentInformation(updateRequest, docId, userId)); + + // Two calls total with one call to revert document information + _mockDocumentInformationService.Verify( + x => x.UpdateDocumentInformation(It.IsAny(), It.IsAny()), Times.Exactly(2)); + _mockDocumentInformationService.Verify( + x => x.UpdateDocumentInformation(It.IsAny(), docInformation), Times.Once); + } + + [Test] + public void UpdateDocumentInformation_ExceptionThrown_ServiceExceptionThrown() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + var updateRequest = GenerateUpdateDocumentInformationRequest(); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable(); + _mockDocumentInformationService + .Setup(x => x.UpdateDocumentInformation(docId, It.Is(info => + info.DocumentBlobId == docBlobId && info.LastModifiedDate!.Value.Date == DateTime.UtcNow.Date))) + .ThrowsAsync(new Exception("Error thrown from unit test.")) + .Verifiable(); + + Assert.ThrowsAsync(async () => + await _documentService.UpdateDocumentInformation(updateRequest, docId, userId)); + + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public async Task GetDocumentInformation_Success() + { + var docId = Guid.NewGuid(); + var docInformation = GenerateDocumentInformation(); + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation) + .Verifiable(); + + var result = await _documentService.GetDocumentInformation(docId); + + Assert.NotNull(result); + var returnedDocInfo = result!; + + Assert.AreEqual(docInformation.DocumentId, returnedDocInfo.DocumentId); + Assert.AreEqual(docInformation.Name, returnedDocInfo.Name); + Assert.AreEqual(docInformation.Type, returnedDocInfo.Type); + Assert.AreEqual(docInformation.Source, returnedDocInfo.Source); + Assert.AreEqual(docInformation.ScanStatus, returnedDocInfo.ScanStatus); + Assert.AreEqual(docInformation.UploadDate, returnedDocInfo.UploadDate); + Assert.AreEqual(docInformation.LastModifiedDate, returnedDocInfo.LastModifiedDate); + Assert.AreEqual(docInformation.CreatedDate, returnedDocInfo.CreatedDate); + Assert.AreEqual(docInformation.Attributes, returnedDocInfo.Attributes); + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public async Task GetDocumentInformation_NoInformationPresent_Null() + { + var docId = Guid.NewGuid(); + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(null as DocumentInformation) + .Verifiable(); + + var result = await _documentService.GetDocumentInformation(docId); + + Assert.Null(result); + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public void GetDocumentInformation_DocumentInformationServiceExceptionThrown_ServiceException() + { + var docId = Guid.NewGuid(); + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)) + .ThrowsAsync(new DocumentInformationServiceException("Thrown from unit test")) + .Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.GetDocumentInformation(docId)); + + _mockDocumentInformationService.Verify(); + } + + [Test] + public void GetDocumentInformation_Exception_ServiceException() + { + var docId = Guid.NewGuid(); + + _mockDocumentInformationService + .Setup(x => x.GetDocumentInformation(docId)) + .ThrowsAsync(new Exception("Thrown from Unit Test")).Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.GetDocumentInformation(docId)); + + _mockDocumentInformationService.Verify(); + } + + [Test] + public async Task UpdateDocumentScanStatus_Success() + { + var docBlobId = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + List docBlobIds = new() + { + docBlobId + }; + var updateRequest = GenerateUpdateDocumentScanStatusRequest(docBlobIds); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformationByBlobId(docBlobId)).ReturnsAsync(docInformation) + .Verifiable(); + _mockDocumentAuditService.Setup(x => x.CreateDocumentAudits(It.IsAny(), + It.IsAny(), false, It.Is(time => time.Date == DateTime.UtcNow.Date), It.Is(name => name == userId))) + .Verifiable(); + _mockDocumentInformationService + .Setup(x => x.UpdateDocumentInformation(docId, It.Is(info => + info.ScanStatus == true + && info.LastModifiedDate!.Value.Date == DateTime.UtcNow.Date))) + .ReturnsAsync(docInformation) + .Verifiable(); + await _documentService.UpdateDocumentScanStatus(true, updateRequest.DocumentBlobIds, userId); + + _mockDocumentAuditService.Verify(); + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + } + + [Test] + public async Task UpdateDocumentScanStatus_FailureThenSuccess() + { + var docBlobIdSuccessful = Guid.NewGuid(); + var docBlobIdFailure = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + List docBlobIds = new() + { + docBlobIdFailure, + docBlobIdSuccessful + }; + var updateRequest = GenerateUpdateDocumentScanStatusRequest(docBlobIds); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobIdSuccessful; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformationByBlobId(docBlobIdFailure)) + .ReturnsAsync((DocumentInformation?)null).Verifiable(); + _mockDocumentInformationService.Setup(x => x.GetDocumentInformationByBlobId(docBlobIdSuccessful)) + .ReturnsAsync(docInformation).Verifiable(); + _mockDocumentAuditService.Setup(x => x.CreateDocumentAudits(It.IsAny(), + It.IsAny(), false, It.Is(time => time.Date == DateTime.UtcNow.Date), It.Is(name => name == userId))) + .Verifiable(); + _mockDocumentInformationService + .Setup(x => x.UpdateDocumentInformation(docId, It.Is(info => + info.ScanStatus == true && info.LastModifiedDate!.Value.Date == DateTime.UtcNow.Date))) + .ReturnsAsync(docInformation) + .Verifiable(); + await _documentService.UpdateDocumentScanStatus(true, updateRequest.DocumentBlobIds, userId); + + _mockDocumentAuditService.Verify(); + _mockDocumentAuditService.Verify( + x => x.CreateDocumentAudits(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.Is(name => name == userId)), Times.Once); + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + _mockDocumentInformationService.Verify(x => x.UpdateDocumentInformation(docId, It.IsAny()), + Times.Once); + } + + [Test] + public async Task UpdateDocumentScanStatus_FailureByServiceExceptionThenSuccess() + { + var docBlobIdSuccessful = Guid.NewGuid(); + var docBlobIdFailure = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + List docBlobIds = new() + { + docBlobIdFailure, + docBlobIdSuccessful + }; + var updateRequest = GenerateUpdateDocumentScanStatusRequest(docBlobIds); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobIdSuccessful; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformationByBlobId(docBlobIdFailure)) + .ThrowsAsync(new DocumentInformationServiceException("thrown from unit test")).Verifiable(); + _mockDocumentInformationService.Setup(x => x.GetDocumentInformationByBlobId(docBlobIdSuccessful)) + .ReturnsAsync(docInformation).Verifiable(); + _mockDocumentAuditService.Setup(x => x.CreateDocumentAudits(It.IsAny(), + It.IsAny(), false, It.Is(time => time.Date == DateTime.UtcNow.Date), It.Is(name => name == userId))) + .Verifiable(); + _mockDocumentInformationService + .Setup(x => x.UpdateDocumentInformation(docId, It.Is(info => + info.ScanStatus == true && info.LastModifiedDate!.Value.Date == DateTime.UtcNow.Date))) + .ReturnsAsync(docInformation) + .Verifiable(); + await _documentService.UpdateDocumentScanStatus(true, updateRequest.DocumentBlobIds, userId); + + _mockDocumentAuditService.Verify(); + _mockDocumentAuditService.Verify( + x => x.CreateDocumentAudits(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.Is(name => name == userId)), Times.Once); + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + _mockDocumentInformationService.Verify(x => x.UpdateDocumentInformation(docId, It.IsAny()), + Times.Once); + } + + [Test] + public async Task UpdateDocumentScanStatus_FailureByExceptionThenSuccess() + { + var docBlobIdSuccessful = Guid.NewGuid(); + var docBlobIdFailure = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + List docBlobIds = new() + { + docBlobIdFailure, + docBlobIdSuccessful + }; + var updateRequest = GenerateUpdateDocumentScanStatusRequest(docBlobIds); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobIdSuccessful; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformationByBlobId(docBlobIdFailure)) + .ThrowsAsync(new Exception("thrown from unit test")).Verifiable(); + _mockDocumentInformationService.Setup(x => x.GetDocumentInformationByBlobId(docBlobIdSuccessful)) + .ReturnsAsync(docInformation).Verifiable(); + _mockDocumentAuditService.Setup(x => x.CreateDocumentAudits(It.IsAny(), + It.IsAny(), false, It.Is(time => time.Date == DateTime.UtcNow.Date), It.Is(name => name == userId))) + .Verifiable(); + _mockDocumentInformationService + .Setup(x => x.UpdateDocumentInformation(docId, It.Is(info => + info.ScanStatus == true && info.LastModifiedDate!.Value.Date == DateTime.UtcNow.Date))) + .ReturnsAsync(docInformation) + .Verifiable(); + await _documentService.UpdateDocumentScanStatus(true, updateRequest.DocumentBlobIds, userId); + + _mockDocumentAuditService.Verify(); + _mockDocumentAuditService.Verify( + x => x.CreateDocumentAudits(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.Is(name => name == userId)), Times.Once); + _mockBlobApiService.Verify(); + _mockDocumentInformationService.Verify(); + _mockDocumentInformationService.Verify(x => x.UpdateDocumentInformation(docId, It.IsAny()), + Times.Once); + } + + [Test] + public async Task UpdateDocumentScanStatus_FailureByDocumentInformationServiceException() + { + var docBlobId1 = Guid.NewGuid(); + var docBlobId2 = Guid.NewGuid(); + var docId = Guid.NewGuid(); + var userId = Guid.NewGuid().ToString(); + List docBlobIds = new() + { + docBlobId1, + docBlobId2 + }; + + var updateRequest = GenerateUpdateDocumentScanStatusRequest(docBlobIds); + + var docInformation = GenerateDocumentInformation(); + docInformation.DocumentId = docId; + docInformation.DocumentBlobId = docBlobId1; + + _mockDocumentInformationService.Setup(x => x.GetDocumentInformationByBlobId(It.IsAny())) + .ReturnsAsync(docInformation).Verifiable(); + + _mockDocumentInformationService + .Setup(x => x.UpdateDocumentInformation(It.IsAny(), It.IsAny())) + .ReturnsAsync(docInformation) + .Verifiable(); + + _mockDocumentAuditService.Setup(x => x.CreateDocumentAudits(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new DocumentAuditServiceException("Error thrown from unit test.")).Verifiable(); + + await _documentService.UpdateDocumentScanStatus(true, updateRequest.DocumentBlobIds, userId); + + // There should be two calls to repository, one initial update and one to rollback for both Ids + _mockDocumentInformationService.Verify(x => x.UpdateDocumentInformation(It.IsAny(), It.IsAny()), + Times.Exactly(4)); + } + + [Test] + public async Task SearchDocumentInformation_Success() + { + var searchRequest = GenerateDocumentInformationFilter(); + var random = new Random(DateTime.Now.Millisecond); + var skip = random.Next(); + var take = random.Next(); + var docInformation = new List { GenerateDocumentInformation() }; + _mockDocumentInformationService + .Setup(x => x.SearchDocumentInformation(searchRequest, skip, take, null)) + .ReturnsAsync(docInformation); + + var result = await _documentService.SearchDocumentInformation(searchRequest, skip, take); + + DocumentInformationResponse AsInformationResponse(DocumentInformation information) + { + var response = new DocumentInformationResponse(); + foreach (var property in typeof(DocumentInformation).GetProperties()) + { + response.GetType().GetProperty(property.Name)?.SetValue(response, property.GetValue(information)); + } + + return response; + } + docInformation.Select(AsInformationResponse).Should().BeEquivalentTo(result); + } + + [Test] + public void SearchDocumentInformation_DocumentInformationException_ServiceExceptionThrown() + { + var searchRequest = GenerateDocumentInformationFilter(); + + _mockDocumentInformationService + .Setup(x => x.SearchDocumentInformation(searchRequest, It.IsAny(), It.IsAny(), null)) + .ThrowsAsync(new DocumentInformationServiceException("Thrown from unit test")) + .Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.SearchDocumentInformation(searchRequest)); + + _mockDocumentInformationService.Verify(); + } + + [Test] + public void SearchDocumentInformation_UnexpectedException_ServiceExceptionThrown() + { + var searchRequest = GenerateDocumentInformationFilter(); + _mockDocumentInformationService + .Setup(x => x.SearchDocumentInformation(searchRequest, It.IsAny(), It.IsAny(), null)) + .ThrowsAsync(new Exception("Thrown from unit test")) + .Verifiable(); + + Assert.ThrowsAsync(async () => await _documentService.SearchDocumentInformation(searchRequest)); + + _mockDocumentInformationService.Verify(); + } + + [Test] + public void CountDocuments_DocumentServiceThrowsDocumentInformationServiceException_ThrowsServiceException() + { + var filter = new DocumentInformationFilter(); + _mockDocumentInformationService + .Setup(s => s.CountDocuments(filter)) + .ThrowsAsync(new DocumentInformationServiceException(string.Empty)); + + Assert.ThrowsAsync(() => _documentService.CountDocuments(filter)); + } + + [Test] + public void CountDocuments_DocumentServiceThrowsException_ThrowsServiceException() + { + var filter = new DocumentInformationFilter(); + _mockDocumentInformationService + .Setup(s => s.CountDocuments(filter)) + .ThrowsAsync(new Exception()); + + Assert.ThrowsAsync(() => _documentService.CountDocuments(filter)); + } + + [Test] + public async Task CountDocuments_DocumentServiceReturnsCount_ReturnsCount() + { + var filter = new DocumentInformationFilter(); + var expectedCount = DateTime.Now.Millisecond; + _mockDocumentInformationService + .Setup(s => s.CountDocuments(filter)) + .ReturnsAsync(expectedCount); + + var count = await _documentService.CountDocuments(filter); + + Assert.AreEqual(expectedCount, count); + } + + [Test] + public async Task GetDocumentWithNumber_InformationServiceReturnsNull_ReturnsNull() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + _mockDocumentInformationService + .Setup(s => s.GetDocumentInformation(documentId)) + .ReturnsAsync((DocumentInformation?)null); + + var document = await _documentService.GetDocument(number, documentId); + + Assert.IsNull(document); + } + + [Test] + public void GetDocumentWithNumber_InformationServiceThrowsDocumentInformationServiceException_ThrowsServiceException() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + _mockDocumentInformationService + .Setup(s => s.GetDocumentInformation(documentId)) + .ThrowsAsync(new DocumentInformationServiceException(string.Empty)); + + Assert.ThrowsAsync(() => _documentService.GetDocument(number, documentId)); + } + + [Test] + public void GetDocumentWithNumber_InformationServiceThrowsBlobApiServiceException_ThrowsServiceException() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + _mockDocumentInformationService + .Setup(s => s.GetDocumentInformation(documentId)) + .ThrowsAsync(new BlobApiServiceException(string.Empty)); + + Assert.ThrowsAsync(() => _documentService.GetDocument(number, documentId)); + } + + [Test] + public void GetDocumentWithNumber_InformationServiceThrowsException_ThrowsServiceException() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + _mockDocumentInformationService + .Setup(s => s.GetDocumentInformation(documentId)) + .ThrowsAsync(new Exception()); + + Assert.ThrowsAsync(() => _documentService.GetDocument(number, documentId)); + } + + [Test] + public async Task GetDocumentWithNumber_InformationServiceReturnsDocumentWithNeitherPersonNorOrgNumberMatchingNumber_ReturnsNull() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + var information = new DocumentInformation + { + PersonNumbers = new [] { (number + 1).ToString(), (number - 1).ToString() }, + OrganizationNumbers = new [] { (number + 2).ToString(), (number - 2).ToString() } + }; + _mockDocumentInformationService + .Setup(s => s.GetDocumentInformation(documentId)) + .ReturnsAsync(information); + + var document = await DocumentServiceFactory(DocumentService.Ownership.CheckPersonOrOrgNumber.Strategy).GetDocument(number, documentId); + + Assert.IsNull(document); + } + + [Test] + public async Task GetDocumentWithNumberUnchecked_InformationServiceReturnsDocumentWithNeitherPersonNorOrgNumberMatchingNumber_ReturnsDocumentAnyways() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + var information = new DocumentInformation + { + PersonNumbers = new [] { (number + 1).ToString(), (number - 1).ToString() }, + OrganizationNumbers = new [] { (number + 2).ToString(), (number - 2).ToString() } + }; + var content = Encoding.Default.GetBytes(Guid.NewGuid().ToString()); + _mockDocumentInformationService + .Setup(s => s.GetDocumentInformation(documentId)) + .ReturnsAsync(information); + _mockBlobApiService + .Setup(b => b.GetDocumentContent(information.DocumentBlobId)) + .ReturnsAsync(content); + + var document = await DocumentServiceFactory(DocumentService.Ownership.Unchecked.Strategy).GetDocument(number, documentId); + + Assert.AreSame(content, document.Content); + Assert.AreEqual(information.DocumentId, document.DocumentInformation.DocumentId); + Assert.AreEqual(information.DocumentBlobId, document.DocumentInformation.DocumentBlobId); + Assert.AreEqual(information.TraceId, document.DocumentInformation.TraceId); + Assert.AreEqual(information.Name, document.DocumentInformation.Name); + Assert.AreEqual(information.Type, document.DocumentInformation.Type); + Assert.AreEqual(information.Source, document.DocumentInformation.Source); + Assert.AreEqual(information.DataSource, document.DocumentInformation.DataSource); + Assert.AreEqual(information.FileSize, document.DocumentInformation.FileSize); + Assert.AreEqual(information.ScanStatus, document.DocumentInformation.ScanStatus); + Assert.AreEqual(information.PersonNumbers, document.DocumentInformation.PersonNumbers); + Assert.AreEqual(information.DocumentId, document.DocumentInformation.DocumentId); + Assert.AreEqual(information.OrganizationNumbers, document.DocumentInformation.OrganizationNumbers); + Assert.AreEqual(information.AccountNumber, document.DocumentInformation.AccountNumber); + Assert.AreEqual(information.Attributes, document.DocumentInformation.Attributes); + Assert.AreEqual(information.UploadDate, document.DocumentInformation.UploadDate); + Assert.AreEqual(information.LastModifiedDate, document.DocumentInformation.LastModifiedDate); + Assert.AreEqual(information.CreatedDate, document.DocumentInformation.CreatedDate); + } + + private static IEnumerable InformationMatchingNumber() + { + var number = DateTime.Now.Millisecond; + var matchingPerson = new DocumentInformation + { + PersonNumbers = new [] { (number + 1).ToString(), number.ToString() }, + OrganizationNumbers = new [] { (number + 2).ToString(), (number - 2).ToString() } + }; + var matchingOrganization = new DocumentInformation + { + PersonNumbers = new [] { (number + 1).ToString(), (number - 1).ToString() }, + OrganizationNumbers = new [] { (number + 2).ToString(), number.ToString() } + }; + + yield return new object[] { number, matchingPerson }; + yield return new object[] { number, matchingOrganization }; + } + + [TestCaseSource(nameof(InformationMatchingNumber))] + public async Task GetDocumentWithNumber_InformationServiceReturnsDocumentMatchingNumber_ReturnsDocumentResponse(long number, DocumentInformation information) + { + var documentId = Guid.NewGuid(); + var content = Encoding.Default.GetBytes(Guid.NewGuid().ToString()); + information.DocumentBlobId = Guid.NewGuid(); + _mockDocumentInformationService + .Setup(s => s.GetDocumentInformation(documentId)) + .ReturnsAsync(information); + _mockBlobApiService + .Setup(b => b.GetDocumentContent(information.DocumentBlobId)) + .ReturnsAsync(content); + + var document = await _documentService.GetDocument(number, documentId); + + Assert.AreSame(content, document.Content); + Assert.AreEqual(information.DocumentId, document.DocumentInformation.DocumentId); + Assert.AreEqual(information.DocumentBlobId, document.DocumentInformation.DocumentBlobId); + Assert.AreEqual(information.TraceId, document.DocumentInformation.TraceId); + Assert.AreEqual(information.Name, document.DocumentInformation.Name); + Assert.AreEqual(information.Type, document.DocumentInformation.Type); + Assert.AreEqual(information.Source, document.DocumentInformation.Source); + Assert.AreEqual(information.DataSource, document.DocumentInformation.DataSource); + Assert.AreEqual(information.FileSize, document.DocumentInformation.FileSize); + Assert.AreEqual(information.ScanStatus, document.DocumentInformation.ScanStatus); + Assert.AreEqual(information.PersonNumbers, document.DocumentInformation.PersonNumbers); + Assert.AreEqual(information.DocumentId, document.DocumentInformation.DocumentId); + Assert.AreEqual(information.OrganizationNumbers, document.DocumentInformation.OrganizationNumbers); + Assert.AreEqual(information.AccountNumber, document.DocumentInformation.AccountNumber); + Assert.AreEqual(information.Attributes, document.DocumentInformation.Attributes); + Assert.AreEqual(information.UploadDate, document.DocumentInformation.UploadDate); + Assert.AreEqual(information.LastModifiedDate, document.DocumentInformation.LastModifiedDate); + Assert.AreEqual(information.CreatedDate, document.DocumentInformation.CreatedDate); + } + + [Test] + public async Task GetDocumentByBlobId_InformationServiceReturnsNull_ReturnsNull() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + _mockDocumentInformationService + .Setup(s => s.GetDocumentInformationByBlobId(documentId)) + .ReturnsAsync((DocumentInformation?)null); + + var content = await _documentService.GetDocumentByBlobId(number, documentId); + + Assert.IsNull(content); + } + + [Test] + public void GetDocumentByBlobId_InformationServiceThrowsBlobApiServiceException_ThrowsServiceException() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + _mockDocumentInformationService + .Setup(s => s.GetDocumentInformationByBlobId(documentId)) + .ThrowsAsync(new BlobApiServiceException(string.Empty)); + + Assert.ThrowsAsync(() => _documentService.GetDocumentByBlobId(number, documentId)); + } + + [Test] + public void GetDocumentByBlobId_InformationServiceThrowsException_ThrowsServiceException() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + _mockDocumentInformationService + .Setup(s => s.GetDocumentInformationByBlobId(documentId)) + .ThrowsAsync(new Exception()); + + Assert.ThrowsAsync(() => _documentService.GetDocumentByBlobId(number, documentId)); + } + + [Test] + public async Task GetDocumentByBlobId_InformationServiceReturnsDocumentWithNeitherPersonNorOrgNumberMatchingNumber_ReturnsNull() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + var information = new DocumentInformation + { + PersonNumbers = new [] { (number + 1).ToString(), (number - 1).ToString() }, + OrganizationNumbers = new [] { (number + 2).ToString(), (number - 2).ToString() } + }; + _mockDocumentInformationService + .Setup(s => s.GetDocumentInformationByBlobId(documentId)) + .ReturnsAsync(information); + + var content = await DocumentServiceFactory(DocumentService.Ownership.CheckPersonOrOrgNumber.Strategy).GetDocumentByBlobId(number, documentId); + + Assert.IsNull(content); + } + + [Test] + public async Task GetDocumentByBlobIdWithNumberUnchecked_InformationServiceReturnsDocumentWithNeitherPersonNorOrgNumberMatchingNumber_ReturnsBytesAnyways() + { + var number = DateTime.Now.Millisecond; + var documentId = Guid.NewGuid(); + var information = new DocumentInformation + { + PersonNumbers = new [] { (number + 1).ToString(), (number - 1).ToString() }, + OrganizationNumbers = new [] { (number + 2).ToString(), (number - 2).ToString() } + }; + var content = Encoding.Default.GetBytes(Guid.NewGuid().ToString()); + _mockDocumentInformationService + .Setup(s => s.GetDocumentInformationByBlobId(documentId)) + .ReturnsAsync(information); + _mockBlobApiService + .Setup(b => b.GetDocumentContent(documentId)) + .ReturnsAsync(content); + + var documentContent = await DocumentServiceFactory(DocumentService.Ownership.Unchecked.Strategy).GetDocumentByBlobId(number, documentId); + + Assert.AreSame(content, documentContent); + } + + [Test] + public async Task GetDocumentsForCleanup_Success() + { + var documents = new List { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + _mockDocumentInformationService.Setup(x => x.GetDocumentsForCleanup(It.IsAny())) + .ReturnsAsync(documents).Verifiable(); + + var result = await _documentService.GetDocumentsForCleanup(It.IsAny()); + + Assert.IsNotEmpty(result); + Assert.IsNotNull(result); + Assert.AreSame(documents, result); + _mockDocumentInformationService.Verify(x => x.GetDocumentsForCleanup(It.IsAny()), Times.Once); + } + + + [Test] + public async Task GetDocumentsForCleanup_Success_NoDocumentsFound() + { + var documents = new List(); + _mockDocumentInformationService.Setup(x => x.GetDocumentsForCleanup(It.IsAny())) + .ReturnsAsync(documents).Verifiable(); + + var result = await _documentService.GetDocumentsForCleanup(It.IsAny()); + + Assert.IsEmpty(result); + Assert.IsNotNull(result); + Assert.AreSame(documents, result); + _mockDocumentInformationService.Verify(x => x.GetDocumentsForCleanup(It.IsAny()), Times.Once); + } + + [Test] + public void GetDocumentsForCleanup_ThrowsServiceException() + { + _mockDocumentInformationService + .Setup(s => s.GetDocumentsForCleanup(It.IsAny())) + .ThrowsAsync(new Exception("Test")); + + Assert.ThrowsAsync(() => _documentService.GetDocumentsForCleanup(It.IsAny())); + } + + [Test] + public void GetDocumentsForCleanup_ThrowsDocumentInformationServiceException() + { + _mockDocumentInformationService + .Setup(s => s.GetDocumentsForCleanup(It.IsAny())) + .ThrowsAsync(new DocumentInformationServiceException("Test")); + + Assert.ThrowsAsync(() => _documentService.GetDocumentsForCleanup(It.IsAny())); + } + + [TestCaseSource(nameof(InformationMatchingNumber))] + public async Task GetDocumentByBlobId_InformationServiceReturnsDocumentMatchingNumber_GetsAndReturnsDocumentBytes(long number, DocumentInformation information) + { + var documentId = Guid.NewGuid(); + var content = Encoding.Default.GetBytes(Guid.NewGuid().ToString()); + _mockDocumentInformationService + .Setup(s => s.GetDocumentInformationByBlobId(documentId)) + .ReturnsAsync(information); + _mockBlobApiService + .Setup(b => b.GetDocumentContent(documentId)) + .ReturnsAsync(content); + + var documentContent = await _documentService.GetDocumentByBlobId(number, documentId); + + Assert.AreSame(content, documentContent); + } + + private static DocumentInformationFilter GenerateDocumentInformationFilter() + { + return new DocumentInformationFilter + { + Name = "TestSearch" + }; + } + + private static DocumentRequest GenerateDocumentRequest() + { + return new DocumentRequest + { + Name = "TestFile.txt", + Type = "Test", + Content = new byte[10], + Source = "Unit Test", + CreatedDate = DateTime.Parse("2022-02-28T17:32:35.093Z"), + Attributes = new Dictionary + { + { "IsTest", "true" } + } + }; + } + + private static UpdateDocumentRequest GenerateUpdateDocumentRequest() + { + return new UpdateDocumentRequest + { + Content = new byte[10], + Name = "TestFile.txt", + Type = "Test", + UpdateSource = "Unit Test", + Attributes = new Dictionary + { + { "IsTest", "true" } + } + }; + } + + private static UpdateDocumentInformationRequest GenerateUpdateDocumentInformationRequest() + { + return new UpdateDocumentInformationRequest + { + Name = "TestFile.txt", + Type = "Test", + UpdateSource = "Unit Test", + Attributes = new Dictionary + { + { "IsTest", "true" } + } + }; + } + + private static UpdateDocumentScanStatusRequest GenerateUpdateDocumentScanStatusRequest(List docBlobIds) + { + return new UpdateDocumentScanStatusRequest + { + ScanStatus = true, + DocumentBlobIds = docBlobIds + }; + } + + private static UpdateDocumentContentRequest GenerateUpdateDocumentContentRequest() + { + return new UpdateDocumentContentRequest + { + Content = new byte[10] + }; + } + + private static DocumentInformation GenerateDocumentInformation() + { + return new DocumentInformation + { + DocumentId = Guid.NewGuid(), + Name = "TestFile.txt", + Type = "Test", + Source = "Developer's Laptop", + ScanStatus = null, + UploadDate = DateTime.Parse("2022-03-25T19:16:12.3665642Z"), + LastModifiedDate = DateTime.Parse("0001-01-01T00:00:00"), + CreatedDate = DateTime.Parse("2022-02-28T17:32:35.093Z"), + Attributes = new Dictionary + { + { "IsTest", "true" }, + { "Owner", "Me Myself" } + } + }; + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/NotificationApiServiceTest.cs b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/NotificationApiServiceTest.cs new file mode 100644 index 0000000..0294bff --- /dev/null +++ b/LCU.API.Internal.Document/LCU.API.Internal.DocumentTest/Services/NotificationApiServiceTest.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; +using System; +using Moq; +using NUnit.Framework; +using LCU.Library.Internal.Exceptions; +using LCU.API.Internal.Document.Services; +using LCU.API.Internal.DocumentLib.DataAccess; +using LCU.API.Internal.NotificationsApiLib.Models; +using System.Collections.Generic; + +namespace LCU.API.Internal.DocumentTest.Services; +public class NotificationApiServiceTest +{ + private NotificationApiService _notificationApiService; + private Mock _mockNotificationApiDao; + + [SetUp] + public void Setup() + { + _mockNotificationApiDao = new Mock(); + _notificationApiService = new NotificationApiService(_mockNotificationApiDao.Object); + } + + [Test] + public void SendEmailAsync_Exception() + { + _mockNotificationApiDao.Setup(x => x.SendEmailAsync(It.IsAny())) + .ThrowsAsync(new Exception("Error")); + + var request = new EmailRequest() + { + Subject = "", + Body = "", + Recipient = new List(new string[] {""}) + }; + + Assert.ThrowsAsync(async () => await _notificationApiService.SendEmailAsync(request)); + _mockNotificationApiDao.Verify(x => x.SendEmailAsync(It.IsAny()), Times.Once); + } + + [Test] + public async Task SendEmailAsync_Return() + { + _mockNotificationApiDao.Setup(x => x.SendEmailAsync(It.IsAny())); + + var request = new EmailRequest() + { + Subject = "", + Body = "", + Recipient = new List(new string[] { "" }) + }; + + await _notificationApiService.SendEmailAsync(request); + _mockNotificationApiDao.Verify(x => x.SendEmailAsync(It.IsAny()), Times.Once); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/BigIntegerConverter.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/BigIntegerConverter.cs new file mode 100644 index 0000000..eee12d1 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/BigIntegerConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Globalization; +using System.Numerics; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace LCU.Library.Internal.DocumentModels.Converter; + +public class BigIntegerConverter : JsonConverter +{ + public override BigInteger Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.Number) + throw new JsonException($"Found token {reader.TokenType} but expected token {JsonTokenType.Number}"); + using var doc = JsonDocument.ParseValue(ref reader); + return BigInteger.Parse(doc.RootElement.GetRawText(), NumberFormatInfo.InvariantInfo); + } + + public override void Write(Utf8JsonWriter writer, BigInteger value, JsonSerializerOptions options) + { + var s = value.ToString(NumberFormatInfo.InvariantInfo); + using var doc = JsonDocument.Parse(s); + doc.WriteTo(writer); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/DynamicObjectJsonConverter.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/DynamicObjectJsonConverter.cs new file mode 100644 index 0000000..d826bbc --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/DynamicObjectJsonConverter.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace LCU.Library.Internal.DocumentModels.Converter; + +public class DynamicObjectJsonConverter : Newtonsoft.Json.JsonConverter +{ + public override void WriteJson(JsonWriter writer, dynamic value, JsonSerializer serializer) + { + serializer.Serialize(writer, value); + } + + public override dynamic ReadJson(JsonReader reader, Type objectType, dynamic existingValue, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartObject) return reader.Value; + + var node = JObject.Load(reader); + + var definition = new ExpandoObject(); + var definitionPropertyBag = definition as IDictionary; + foreach (var property in node.Properties()) + { + definitionPropertyBag[property.Name] = property.Value.ToObject(); + } + + return JsonConvert.DeserializeAnonymousType(node.ToString(), definition); + } + + public override bool CanConvert(Type _) => true; +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/JsonKnownTypesAttribute.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/JsonKnownTypesAttribute.cs new file mode 100644 index 0000000..a41044a --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/JsonKnownTypesAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace LCU.Library.Internal.DocumentModels.Converter; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)] +public class JsonKnownTypesAttribute : Attribute +{ + public Type[] Subclasses { get; } + + public JsonKnownTypesAttribute(params Type[] knownTypes) + { + Subclasses = knownTypes; + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/JsonQueryBinder.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/JsonQueryBinder.cs new file mode 100644 index 0000000..206d8ea --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/JsonQueryBinder.cs @@ -0,0 +1,48 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace LCU.Library.Internal.DocumentModels.Converter +{ + public class FromJsonQueryAttribute : ModelBinderAttribute + { + public FromJsonQueryAttribute() + { + BinderType = typeof(JsonQueryBinder); + } + } + + public class JsonQueryBinder : IModelBinder + { + public JsonQueryBinder() + { + } + + public Task BindModelAsync(ModelBindingContext bindingContext) + { + var value = bindingContext.ValueProvider.GetValue(bindingContext.FieldName).FirstValue; + if (value == null) + { + return Task.CompletedTask; + } + + try + { + var parsed = JsonSerializer.Deserialize( + value, + bindingContext.ModelType, + new JsonSerializerOptions(JsonSerializerDefaults.Web) + ); + bindingContext.Result = ModelBindingResult.Success(parsed); + } + catch (Exception e) + { + bindingContext.Result = ModelBindingResult.Failed(); + } + + return Task.CompletedTask; + } + } +} diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/PolymorphicJsonConverter.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/PolymorphicJsonConverter.cs new file mode 100644 index 0000000..3d58c64 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Converter/PolymorphicJsonConverter.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace LCU.Library.Internal.DocumentModels.Converter; + +internal static class PolymorphicJsonConverter +{ + public const string TypeProperty = "_t"; + + public static Exception NotSupported() => new NotSupportedException($"{typeof(T).Name} must specify known deserialization types"); + + public static bool IsSerializationSupportedFor(Type actualType) => typeof(T).GetCustomAttribute() is { } knownTypes + && knownTypes.Subclasses.Contains(actualType); + + public static Type DeserializationTypeFor(string actualType) => typeof(T) + .GetCustomAttribute() + .Subclasses + .Single(t => t.Name == actualType); +} + +public class SystemPolymorphicJsonConverter : System.Text.Json.Serialization.JsonConverter +{ + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var document = JsonDocument.ParseValue(ref reader); + var type = document.RootElement.GetProperty(PolymorphicJsonConverter.TypeProperty).GetString(); + var deserializationType = PolymorphicJsonConverter.DeserializationTypeFor(type); + + return (T)document.Deserialize(deserializationType, options); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var actualType = value.GetType(); + if (!PolymorphicJsonConverter.IsSerializationSupportedFor(actualType)) + { + throw PolymorphicJsonConverter.NotSupported(); + } + + writer.WriteStartObject(); + writer.WriteString(PolymorphicJsonConverter.TypeProperty, actualType.Name); + + var actualValue = JsonSerializer.Serialize(value, actualType, options); + writer.WriteRawValue(actualValue.Substring(1, actualValue.Length - 2), true); + + writer.WriteEndObject(); + } +} + +public class NewtonsoftPolymorphicJsonConverter : Newtonsoft.Json.JsonConverter +{ + public override bool CanRead => _canRead; + private bool _canRead = true; + + public override T ReadJson(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue, Newtonsoft.Json.JsonSerializer serializer) + { + var document = JObject.Load(reader); + var type = document.GetValue(PolymorphicJsonConverter.TypeProperty)!.Value(); + var deserializationType = PolymorphicJsonConverter.DeserializationTypeFor(type); + + T result; + lock (this) + { + _canRead = false; + result = (T)JsonConvert.DeserializeObject(document.ToString(), deserializationType); + _canRead = true; + } + + return result; + } + + public override bool CanWrite => _canWrite; + private bool _canWrite = true; + + public override void WriteJson(JsonWriter writer, T value, Newtonsoft.Json.JsonSerializer serializer) + { + var actualType = value.GetType(); + if (!PolymorphicJsonConverter.IsSerializationSupportedFor(actualType)) + { + throw PolymorphicJsonConverter.NotSupported(); + } + + writer.WriteStartObject(); + writer.WritePropertyName(PolymorphicJsonConverter.TypeProperty); + writer.WriteValue(actualType.Name); + + JObject jValue; + lock (this) + { + _canWrite = false; + jValue = JObject.FromObject(value); + _canWrite = true; + } + + foreach (var property in jValue.Properties()) + { + property.WriteTo(writer); + } + + writer.WriteEndObject(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/LCU.Library.Internal.DocumentModels.csproj b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/LCU.Library.Internal.DocumentModels.csproj new file mode 100644 index 0000000..dbe81f7 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/LCU.Library.Internal.DocumentModels.csproj @@ -0,0 +1,86 @@ + + + + netstandard2.0 + true + 1.8.4 + 1.8.5 + + 1.0.0 - Initial Setup + 1.0.1 - Delete Date is now an optional field in requests and responses, and source on update requests has been renamed to updateSource + 1.0.2 - TraceId has been added as an optional field to documentInformation + 1.0.3 - DocumentSearchRequest object has been added + 1.0.4 - EncodeDocumentInformationFilterUri added to DocumentInformationFilter object + 1.0.5 - Updating DocumentInformationResponse to have a JsonConverter for BigInt + 1.0.6 - Separating JsonConverter for BigInt to own class + 1.0.7 - Adding in DataSource field in request and response models and search filter + 1.0.8 - Adding in CreatedDate field in request and response models and search filter + 1.0.9 - Updating Filter Query Creation method to not shorten dates + 1.1.0 - Add AccountNumbers, PersonNumbers and OrganizationNumbers to models and search filter + 1.1.1 - Add document action notification route for actions on documents not fulfilled by Document API directly + 1.1.2 - contract type definition for document audit logs + 1.1.3 - document filter query string serialization fix + 1.2.0 - Add Sorter to easily create sortable fields + 1.2.1 - Change PersonNumberEquals and OrganizationNumberEquals to singular string + 1.3.0 - Add DocumentInformationFilteredSearch + 1.3.1 - fixed multiple serialization for DocumentFieldAudit Current and Previous Value properties + 1.3.2 - fixed serialization of Current and Previous Value properties that aren't objects (e.g. null or boolean) + 1.4.0 - document search filters can no longer be used in query strings, method EncodeDocumentInformationFilterUri removed + 1.5.0 - Added document action for upload + 1.5.1 - Removed ArchiveDate and DeleteDate from DocumentInformation + 1.6.0 - Add GetDocumentCleanupRequest + 1.7.0 - Add DocumentAction for Delete and DeleteContent, Add DeleteDocumentContentRequest + 1.8.0 - Make DocumentInformation.DocumentBlobId a nullable Guid for deletion + 1.8.1 - Add TypeContainsAny + 1.8.2 - Add BusinessName to DocumentInformationFilter + 1.8.3 - Reverted MongoDB.Driver version + 1.8.4 - MongoDB.Driver version + 1.8.5 - Nuget update DocumentIngestor Model + + + 1.0.0 - Initial Setup + 1.0.1 - Delete Date is now an optional field in requests and responses, and source on update requests has been renamed to updateSource + 1.0.2 - TraceId has been added as an optional field to documentInformation + 1.0.3 - DocumentSearchRequest object has been added + 1.0.4 - EncodeDocumentInformationFilterUri added to DocumentInformationFilter object + 1.0.5 - Updating DocumentInformationResponse to have a JsonConverter for BigInt + 1.0.6 - Separating JsonConverter for BigInt to own class + 1.0.7 - Adding in DataSource field in request and response models and search filter + 1.0.8 - Adding in CreatedDate field in request and response models and search filter + 1.0.9 - Updating Filter Query Creation method to not shorten dates + 1.1.0 - Add AccountNumbers, PersonNumbers and OrganizationNumbers to models and search filter + 1.1.1 - Add document action notification route for actions on documents not fulfilled by Document API directly + 1.1.2 - contract type definition for document audit logs + 1.1.3 - document filter query string serialization fix + 1.2.0 - Add Sorter to easily create sortable fields + 1.2.1 - Change PersonNumberEquals and OrganizationNumberEquals to singular string + 1.3.0 - Add DocumentInformationFilteredSearch + 1.3.1 - fixed multiple serialization for DocumentFieldAudit Current and Previous Value properties + 1.3.2 - fixed serialization of Current and Previous Value properties that aren't objects (e.g. null or boolean) + 1.4.0 - document search filters can no longer be used in query strings, method EncodeDocumentInformationFilterUri removed + 1.5.0 - Added document action for upload + 1.5.1 - Removed ArchiveDate and DeleteDate from DocumentInformation + 1.6.0 - Add GetDocumentCleanupRequest + 1.7.0 - Add DocumentAction for Delete and DeleteContent, Add DeleteDocumentContentRequest + 1.8.0 - Make DocumentInformation.DocumentBlobId a nullable Guid for deletion + 1.8.1 - Add TypeContainsAny + 1.8.2 - Add BusinessName to DocumentInformationFilter + 1.8.3 - Reverted MongoDB.Driver version + 1.8.4 - MongoDB.Driver version + 1.8.5 - Nuget update DocumentIngestor Model + + LCU.Library.Internal.DocumentModels + latest + + + + + + + + + + + + + diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/DeleteDocumentContentRequest.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/DeleteDocumentContentRequest.cs new file mode 100644 index 0000000..72cc180 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/DeleteDocumentContentRequest.cs @@ -0,0 +1,11 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace LCU.Library.Internal.DocumentModels.Requests; + +public class DeleteDocumentContentRequest +{ + [Required] public Guid DocumentId { get; set; } + public string? Reason { get; set; } + public string UserName { get; set; } = "System"; +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/DocumentAction.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/DocumentAction.cs new file mode 100644 index 0000000..55d4ad4 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/DocumentAction.cs @@ -0,0 +1,9 @@ +namespace LCU.Library.Internal.DocumentModels.Requests; + +public enum DocumentAction +{ + Download, + Upload, + Delete, + DeleteContent +} diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/DocumentActionNotificationRequest.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/DocumentActionNotificationRequest.cs new file mode 100644 index 0000000..a993fcb --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/DocumentActionNotificationRequest.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace LCU.Library.Internal.DocumentModels.Requests; + +public class DocumentActionNotificationRequest +{ + [Required] public DocumentAction Action { get; set; } + [Required] public string Source { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/DocumentRequest.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/DocumentRequest.cs new file mode 100644 index 0000000..69bdb8c --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/DocumentRequest.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace LCU.Library.Internal.DocumentModels.Requests; + +public class DocumentRequest +{ + [Required] public string Name { get; set; } + + [Required] public string Type { get; set; } + + [Required] public byte[] Content { get; set; } + + [Required] public string Source { get; set; } + + [Required] public string DataSource { get; set; } + + [Required] public DateTime CreatedDate { get; set; } + + public Guid? TraceId { get; set; } + + public string[] PersonNumbers { get; set; } + + public string[] OrganizationNumbers { get; set; } + + public string AccountNumber { get; set; } + + public Dictionary Attributes { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/Search/DocumentInformationFilter.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/Search/DocumentInformationFilter.cs new file mode 100644 index 0000000..cdf42ed --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/Search/DocumentInformationFilter.cs @@ -0,0 +1,37 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Web; +using Microsoft.AspNetCore.Mvc; + +namespace LCU.Library.Internal.DocumentModels.Requests.Search; + +public class DocumentInformationFilter +{ + public string? Name { get; set; } + public string? Type { get; set; } + public string? BusinessName { get; set; } + public string[]? TypeContainsAny { get; set; } + public bool? ScanStatus { get; set; } + public string? Source { get; set; } + public string? DataSource { get; set; } + public Guid? TraceId { get; set; } + public string? PersonNumbersEquals { get; set; } + public string[]? PersonNumbersContains { get; set; } + public string[]? PersonNumbersContainsAny { get; set; } + public string? OrganizationNumbersEquals { get; set; } + public string[]? OrganizationNumbersContains { get; set; } + public string[]? OrganizationNumbersContainsAny { get; set; } + public string? AccountNumberEquals { get; set; } + public string[]? AccountNumberContainsAny { get; set; } + public Dictionary? AttributesEquals { get; set; } + public Dictionary? AttributesContains { get; set; } + public Dictionary? AttributesContainsAny { get; set; } + public DateTime? CreatedDateMinimum { get; set; } + public DateTime? CreatedDateMaximum { get; set; } + public DateTime? UploadDateMinimum { get; set; } + public DateTime? UploadDateMaximum { get; set; } + public DateTime? LastModifiedMinimum { get; set; } + public DateTime? LastModifiedDateMaximum { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/Search/DocumentInformationFilteredSearch.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/Search/DocumentInformationFilteredSearch.cs new file mode 100644 index 0000000..f738a8d --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/Search/DocumentInformationFilteredSearch.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination; + +namespace LCU.Library.Internal.DocumentModels.Requests.Search; + +public class DocumentInformationFilteredSearch +{ + public DocumentInformationFilter Filter { get; set; } + public int? Skip { get; set; } + public int? Take { get; set; } + public List? SortDefinition { get; set; } +} diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/Search/GetDocumentCleanupRequest.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/Search/GetDocumentCleanupRequest.cs new file mode 100644 index 0000000..09ba9f8 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/Search/GetDocumentCleanupRequest.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using LCU.Library.DocumentType.Models.Responses; + +namespace LCU.Library.Internal.DocumentModels.Requests.Search; + +public class GetDocumentCleanupRequest +{ + public List DocumentCodeDetailInfos { get; set; } + public int Limit { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/Search/Sorter.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/Search/Sorter.cs new file mode 100644 index 0000000..4bf4272 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/Search/Sorter.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using MongoDB.Driver; +using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination; + +namespace LCU.Library.Internal.DocumentModels.Requests.Search; + +public class Sorter +{ + public static SortDefinition GetSorter(List fields) + { + SortDefinition? sortDefinition = null; + + if(fields == null) + { + return sortDefinition; + } + + fields.Sort((a, b) => a.Position.CompareTo(b.Position)); + + foreach (var field in fields) + { + if (sortDefinition == null) + { + sortDefinition = field.IsDescending ? Builders.Sort.Descending(field.Field) : Builders.Sort.Ascending(field.Field); + } + else + { + sortDefinition = field.IsDescending ? sortDefinition.Descending(field.Field) : sortDefinition.Ascending(field.Field); + } + } + + return sortDefinition; + } +} diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/UpdateDocumentContentRequest.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/UpdateDocumentContentRequest.cs new file mode 100644 index 0000000..7f8a4a5 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/UpdateDocumentContentRequest.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace LCU.Library.Internal.DocumentModels.Requests; + +public class UpdateDocumentContentRequest +{ + [Required] public byte[] Content { get; set; } + + [Required] public string UpdateSource { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/UpdateDocumentInformationRequest.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/UpdateDocumentInformationRequest.cs new file mode 100644 index 0000000..b5d48b5 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/UpdateDocumentInformationRequest.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace LCU.Library.Internal.DocumentModels.Requests; + +public class UpdateDocumentInformationRequest +{ + [Required] public string Name { get; set; } + + [Required] public string Type { get; set; } + + [Required] public string UpdateSource { get; set; } + + public string[] PersonNumbers { get; set; } + + public string[] OrganizationNumbers { get; set; } + + public string AccountNumber { get; set; } + + public Dictionary Attributes { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/UpdateDocumentRequest.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/UpdateDocumentRequest.cs new file mode 100644 index 0000000..8fa14f9 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/UpdateDocumentRequest.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace LCU.Library.Internal.DocumentModels.Requests; + +public class UpdateDocumentRequest +{ + [Required] public string Name { get; set; } + + [Required] public string Type { get; set; } + + [Required] public byte[] Content { get; set; } + + [Required] public string UpdateSource { get; set; } + + public string[] PersonNumbers { get; set; } + + public string[] OrganizationNumbers { get; set; } + + public string AccountNumber { get; set; } + + public Dictionary Attributes { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/UpdateDocumentScanStatusRequest.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/UpdateDocumentScanStatusRequest.cs new file mode 100644 index 0000000..121d07d --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Requests/UpdateDocumentScanStatusRequest.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace LCU.Library.Internal.DocumentModels.Requests; + +public class UpdateDocumentScanStatusRequest +{ + [Required] public List DocumentBlobIds { get; set; } + [Required] public bool ScanStatus { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Responses/DocumentAuditResponse.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Responses/DocumentAuditResponse.cs new file mode 100644 index 0000000..0506640 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Responses/DocumentAuditResponse.cs @@ -0,0 +1,33 @@ +using LCU.Library.Internal.DocumentModels.Requests; +using System; +using LCU.Library.Internal.DocumentModels.Converter; + +namespace LCU.Library.Internal.DocumentModels.Responses; + +[JsonKnownTypes(typeof(DocumentFieldAuditResponse), typeof(DocumentActionAuditResponse))] +[System.Text.Json.Serialization.JsonConverter(typeof(SystemPolymorphicJsonConverter))] +[Newtonsoft.Json.JsonConverter(typeof(NewtonsoftPolymorphicJsonConverter))] +public abstract class DocumentAuditResponse +{ + public Guid DocumentId { get; set; } + public string Source { get; set; } + + public DateTime Date { get; set; } + + public string UserId { get; set; } +} + +public class DocumentFieldAuditResponse : DocumentAuditResponse +{ + public string Field { get; set; } + [Newtonsoft.Json.JsonConverter(typeof(DynamicObjectJsonConverter))] + public dynamic PreviousValue { get; set; } + [Newtonsoft.Json.JsonConverter(typeof(DynamicObjectJsonConverter))] + public dynamic CurrentValue { get; set; } +} + +public class DocumentActionAuditResponse : DocumentAuditResponse +{ + public DocumentAction Action { get; set; } + public string ActionDescription { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Responses/DocumentInformationResponse.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Responses/DocumentInformationResponse.cs new file mode 100644 index 0000000..16262da --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Responses/DocumentInformationResponse.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text.Json.Serialization; +using LCU.Library.Internal.DocumentModels.Converter; + +namespace LCU.Library.Internal.DocumentModels.Responses; + +public class DocumentInformationResponse +{ + public Guid DocumentId { get; set; } + public Guid? DocumentBlobId { get; set; } + public Guid? TraceId { get; set; } + public string Name { get; set; } + public string Type { get; set; } + public string Source { get; set; } + public string DataSource { get; set; } + + [JsonConverter(typeof(BigIntegerConverter))] + public BigInteger FileSize { get; set; } + public bool? ScanStatus { get; set; } + public DateTime UploadDate { get; set; } + public DateTime? LastModifiedDate { get; set; } + + public DateTime CreatedDate { get; set; } + public string[] PersonNumbers { get; set; } + public string[] OrganizationNumbers { get; set; } + public string AccountNumber { get; set; } + public Dictionary Attributes { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Responses/DocumentResponse.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Responses/DocumentResponse.cs new file mode 100644 index 0000000..2a3ea2e --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModels/Responses/DocumentResponse.cs @@ -0,0 +1,7 @@ +namespace LCU.Library.Internal.DocumentModels.Responses; + +public class DocumentResponse +{ + public byte[] Content { get; set; } + public DocumentInformationResponse DocumentInformation { get; set; } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModelsTest/Converter/PolymorphicJsonConverterTest.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModelsTest/Converter/PolymorphicJsonConverterTest.cs new file mode 100644 index 0000000..8c7ef70 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModelsTest/Converter/PolymorphicJsonConverterTest.cs @@ -0,0 +1,215 @@ +using FluentAssertions; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.Library.Internal.DocumentModels.Converter; +using LCU.Library.Internal.DocumentModels.Responses; +using Newtonsoft.Json; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace LCU.Library.Internal.DocumentModelsTest.Converter; + +public class PolymorphicJsonConverterTest +{ + public class SystemPolymorphicJsonConverterTest + { + [Test] + public void WriteThenRead_AbstractBaseClassImplementation_DeserializesAsImplementation() + { + AbstractBaseClass original = new ConcreteClass(); + + var serialized = JsonSerializer.Serialize(original); + var deserialized = JsonSerializer.Deserialize(serialized); + + Assert.IsInstanceOf(deserialized); + deserialized.Should().BeEquivalentTo(original); + } + + [Test] + public void Write_TypeDoesNotSpecifyKnownSubclasses_ThrowsException() + { + DoesNotSpecifyKnownTypes original = new UnknownSubtype(); + + Assert.Throws(() => JsonSerializer.Serialize(original)); + } + } + + public class NewtonsoftPolymorphicJsonConverterTest + { + [Test] + public void WriteThenRead_AbstractBaseClassImplementation_DeserializesAsImplementation() + { + AbstractBaseClass original = new ConcreteClass(); + + var serialized = JsonConvert.SerializeObject(original); + var deserialized = JsonConvert.DeserializeObject(serialized); + + Assert.IsInstanceOf(deserialized); + deserialized.Should().BeEquivalentTo(original); + } + + [Test] + public void Write_TypeDoesNotSpecifyKnownSubclasses_ThrowsException() + { + DoesNotSpecifyKnownTypes original = new UnknownSubtype(); + + Assert.Throws(() => JsonConvert.SerializeObject(original)); + } + } + + public class NewtonsoftToSystemPolymorphicJsonConverterTest + { + [Test] + public void WriteThenRead_AbstractBaseClassImplementation_DeserializesAsImplementation() + { + AbstractBaseClass original = new ConcreteClass(); + + var serialized = JsonConvert.SerializeObject(original); + var deserialized = JsonSerializer.Deserialize(serialized); + + Assert.IsInstanceOf(deserialized); + deserialized.Should().BeEquivalentTo(original); + } + } + + public class SystemToNewtonsoftPolymorphicJsonConverterTest + { + [Test] + public void WriteThenRead_AbstractBaseClassImplementation_DeserializesAsImplementation() + { + AbstractBaseClass original = new ConcreteClass(); + + var serialized = JsonSerializer.Serialize(original); + var deserialized = JsonConvert.DeserializeObject(serialized); + + Assert.IsInstanceOf(deserialized); + deserialized.Should().BeEquivalentTo(original); + } + + [Test] + [Category("Integration")] + public void WriteThenRead_DocumentFieldAuditWithValues_DeserializesCurrentAndPrevious() + { + DocumentAuditResponse original = new DocumentFieldAuditResponse + { + DocumentId = Guid.NewGuid(), + Source = Guid.NewGuid().ToString(), + Date = DateTime.Now, + UserId = Guid.NewGuid().ToString(), + Field = nameof(DocumentInformation.Attributes), + CurrentValue = new + { + PersonNumber = Guid.NewGuid().ToString(), + AccountNumber = Guid.NewGuid().ToString() + }, + PreviousValue = new + { + PersonNumber = Guid.NewGuid().ToString(), + AccountNumber = Guid.NewGuid().ToString() + } + }; + + var deserialized = JsonConvert.DeserializeObject(JsonSerializer.Serialize(original)); + for (var i = 0; i <= 5; i++) + { + deserialized = JsonConvert.DeserializeObject(JsonSerializer.Serialize(deserialized)); + } + + var originalFieldAudit = original as DocumentFieldAuditResponse; + var deserializedFieldAudit = deserialized as DocumentFieldAuditResponse; + //caution: fluent assertions object comparison does not compare expando objects correctly as of v6.7.0 + //it will report the below four properties as missing, despite them having made it through two serializations + Assert.AreEqual(originalFieldAudit.CurrentValue.PersonNumber, deserializedFieldAudit.CurrentValue.PersonNumber); + Assert.AreEqual(originalFieldAudit.CurrentValue.AccountNumber, deserializedFieldAudit.CurrentValue.AccountNumber); + Assert.AreEqual(originalFieldAudit.PreviousValue.PersonNumber, deserializedFieldAudit.PreviousValue.PersonNumber); + Assert.AreEqual(originalFieldAudit.PreviousValue.AccountNumber, deserializedFieldAudit.PreviousValue.AccountNumber); + } + + [Test] + [Category("Integration")] + public void WriteThenRead_DocumentFieldAuditWithNullValues_DeserializesCurrentAndPrevious() + { + DocumentAuditResponse original = new DocumentFieldAuditResponse + { + DocumentId = Guid.NewGuid(), + Source = Guid.NewGuid().ToString(), + Date = DateTime.Now, + UserId = Guid.NewGuid().ToString(), + Field = nameof(DocumentInformation.Attributes), + CurrentValue = null, + PreviousValue = null + }; + + var deserialized = JsonConvert.DeserializeObject(JsonSerializer.Serialize(original)); + for (var i = 0; i <= 5; i++) + { + deserialized = JsonConvert.DeserializeObject(JsonSerializer.Serialize(deserialized)); + } + + var originalFieldAudit = original as DocumentFieldAuditResponse; + var deserializedFieldAudit = deserialized as DocumentFieldAuditResponse; + //caution: fluent assertions object comparison does not compare expando objects correctly as of v6.7.0 + //it will report the below four properties as missing, despite them having made it through two serializations + Assert.AreEqual(originalFieldAudit.CurrentValue, deserializedFieldAudit.CurrentValue); + Assert.AreEqual(originalFieldAudit.PreviousValue, deserializedFieldAudit.PreviousValue); + } + + [Test] + [Category("Integration")] + public void WriteThenRead_DocumentFieldAuditWithBooleanValues_DeserializesCurrentAndPrevious() + { + DocumentAuditResponse original = new DocumentFieldAuditResponse + { + DocumentId = Guid.NewGuid(), + Source = Guid.NewGuid().ToString(), + Date = DateTime.Now, + UserId = Guid.NewGuid().ToString(), + Field = nameof(DocumentInformation.Attributes), + CurrentValue = true, + PreviousValue = false + }; + + var deserialized = JsonConvert.DeserializeObject(JsonSerializer.Serialize(original)); + for (var i = 0; i <= 5; i++) + { + deserialized = JsonConvert.DeserializeObject(JsonSerializer.Serialize(deserialized)); + } + + var originalFieldAudit = original as DocumentFieldAuditResponse; + var deserializedFieldAudit = deserialized as DocumentFieldAuditResponse; + //caution: fluent assertions object comparison does not compare expando objects correctly as of v6.7.0 + //it will report the below four properties as missing, despite them having made it through two serializations + Assert.AreEqual(originalFieldAudit.CurrentValue, deserializedFieldAudit.CurrentValue); + Assert.AreEqual(originalFieldAudit.PreviousValue, deserializedFieldAudit.PreviousValue); + } + } + + + [System.Text.Json.Serialization.JsonConverter(typeof(SystemPolymorphicJsonConverter))] + [Newtonsoft.Json.JsonConverter(typeof(NewtonsoftPolymorphicJsonConverter))] + [JsonKnownTypes(typeof(ConcreteClass))] + private abstract class AbstractBaseClass + { + public Guid AbstractId { get; init; } = Guid.NewGuid(); + + public NestedType Nested { get; init; } = new(); + } + + private class ConcreteClass : AbstractBaseClass + { + public Guid ConcreteId { get; init; } = Guid.NewGuid(); + } + + private class NestedType + { + public Guid NestedId { get; init; } = Guid.NewGuid(); + } + + [System.Text.Json.Serialization.JsonConverter(typeof(SystemPolymorphicJsonConverter))] + [Newtonsoft.Json.JsonConverter(typeof(NewtonsoftPolymorphicJsonConverter))] + private abstract class DoesNotSpecifyKnownTypes + { + } + + private class UnknownSubtype : DoesNotSpecifyKnownTypes + { + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModelsTest/LCU.Library.Internal.DocumentModelsTest.csproj b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModelsTest/LCU.Library.Internal.DocumentModelsTest.csproj new file mode 100644 index 0000000..8a4525b --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModelsTest/LCU.Library.Internal.DocumentModelsTest.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + + + + + diff --git a/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModelsTest/Usings.cs b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModelsTest/Usings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/LCU.API.Internal.Document/LCU.Library.Internal.DocumentModelsTest/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/AttributeMigration.cs b/LCU.API.Internal.Document/OwnerNumberMigration/AttributeMigration.cs new file mode 100644 index 0000000..0daac8e --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/AttributeMigration.cs @@ -0,0 +1,121 @@ +using System.Text.RegularExpressions; +using LCU.API.Internal.DocumentLib.Containers; +using LCU.API.Internal.DocumentLib.Utilities; +using MongoDB.Driver; +using OwnerNumberMigration.ValueParsing; + +namespace OwnerNumberMigration; + +public class AttributeMigration : IAttributeMigration +{ + private readonly IDocumentFilterFactory _filterFactory; + private readonly IMongoCollectionFactory _collectionFactory; + private readonly string _attributeName; + private readonly IValueParsingStrategy _valueParsingStrategy; + private readonly string _destinationProperty; + private readonly IObserver _observer; + + public AttributeMigration + ( + IMongoCollectionFactory collectionFactory, + string attributeName, + IValueParsingStrategy valueParsingStrategy, + string destinationProperty, + IObserver observer, + IDocumentFilterFactory filterFactory + ) + { + _attributeName = attributeName; + _valueParsingStrategy = valueParsingStrategy; + _destinationProperty = destinationProperty; + _observer = observer; + _collectionFactory = collectionFactory; + _filterFactory = filterFactory; + } + + public async Task Migrate(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) return; + + try + { + if (typeof(DocumentInformation).GetProperty(_destinationProperty) is null) + { + throw new InvalidOperationException(); + } + + var hasAttribute = _filterFactory.AppliesForMigrationFilterFactory(_attributeName, _destinationProperty); + var collection = _collectionFactory.GetCollection(); + var documents = await RetryOnTooManyRequests(async () => await collection.FindAsync(hasAttribute, cancellationToken: cancellationToken), cancellationToken); + + var done = false; + while (!done) + { + try + { + while (!cancellationToken.IsCancellationRequested && await documents.MoveNextAsync(cancellationToken)) + { + foreach (var document in documents.Current) + { + if (cancellationToken.IsCancellationRequested) break; + + _observer.OnNext(document); + + var value = _valueParsingStrategy.Parse(document.Attributes[_attributeName]); + await RetryOnTooManyRequests(async () => await collection.FindOneAndUpdateAsync + ( + d => d.DocumentId == document.DocumentId, + Builders.Update.Set(_destinationProperty, value), + cancellationToken: cancellationToken + ), cancellationToken); + + _observer.OnCompleted(); + } + } + + done = true; + } + catch (MongoCursorNotFoundException e) + { + _observer.OnError(e); + + documents = await RetryOnTooManyRequests(async () => await collection.FindAsync(hasAttribute, cancellationToken: cancellationToken), cancellationToken); + } + } + } + catch (Exception e) + { + _observer.OnError(e); + throw; + } + } + + private async Task RetryOnTooManyRequests(Func> action, CancellationToken cancellationToken) + where TU : class + { + TU? result = null; + while (result is null) + { + try + { + result = await action(); + } + catch (MongoCommandException e) + { + if (e.Code == 16500) + { + _observer.OnError(e); + + var wait = int.Parse(Regex.Match(e.Result.GetValue("errmsg").AsString, "\\bRetryAfterMs=(\\d+)\\b").Groups[1].ValueSpan); + await Task.Delay(wait, cancellationToken); + } + else + { + throw; + } + } + } + + return result; + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/DocumentFilterFactory.cs b/LCU.API.Internal.Document/OwnerNumberMigration/DocumentFilterFactory.cs new file mode 100644 index 0000000..a2325c9 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/DocumentFilterFactory.cs @@ -0,0 +1,13 @@ +using LCU.API.Internal.DocumentLib.Containers; +using MongoDB.Driver; + +namespace OwnerNumberMigration; + +public class DocumentFilterFactory : IDocumentFilterFactory +{ + public FilterDefinition AppliesForMigrationFilterFactory(string attributeName, string destinationAttributeName) + { + return Builders.Filter.Exists($"Attributes.{attributeName}") + & !Builders.Filter.Exists(destinationAttributeName); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/IAttributeMigration.cs b/LCU.API.Internal.Document/OwnerNumberMigration/IAttributeMigration.cs new file mode 100644 index 0000000..f0ee872 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/IAttributeMigration.cs @@ -0,0 +1,15 @@ +using OwnerNumberMigration.ValueParsing; + +namespace OwnerNumberMigration; + +public delegate IAttributeMigration AttributeMigrationFactory +( + string attributeName, + IValueParsingStrategy valueParsingStrategy, + string destinationProperty +); + +public interface IAttributeMigration +{ + Task Migrate(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/IDocumentFilterFactory.cs b/LCU.API.Internal.Document/OwnerNumberMigration/IDocumentFilterFactory.cs new file mode 100644 index 0000000..8078a22 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/IDocumentFilterFactory.cs @@ -0,0 +1,9 @@ +using LCU.API.Internal.DocumentLib.Containers; +using MongoDB.Driver; + +namespace OwnerNumberMigration; + +public interface IDocumentFilterFactory +{ + FilterDefinition AppliesForMigrationFilterFactory(string attributeName, string destinationAttributeName); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/Logging/AttributeMigrationLoggingDecorator.cs b/LCU.API.Internal.Document/OwnerNumberMigration/Logging/AttributeMigrationLoggingDecorator.cs new file mode 100644 index 0000000..5d9e29a --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/Logging/AttributeMigrationLoggingDecorator.cs @@ -0,0 +1,62 @@ +using LCU.API.Internal.DocumentLib.Containers; +using LCU.API.Internal.DocumentLib.Utilities; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace OwnerNumberMigration.Logging; + +public delegate IAttributeMigration AttributeMigrationLoggingDecoratorFactory +( + IAttributeMigration migration, + string attributeName, + string destinationAttributeName +); + +public class AttributeMigrationLoggingDecorator : IAttributeMigration +{ + private readonly IAttributeMigration _migration; + private readonly ILogger _logger; + private readonly IMongoCollection _documents; + private readonly string _attributeName; + private readonly string _destinationAttributeName; + private readonly IDocumentFilterFactory _filterFactory; + + public AttributeMigrationLoggingDecorator + ( + IAttributeMigration migration, + ILogger logger, + IMongoCollectionFactory collectionFactory, + string attributeName, + string destinationAttributeName, + IDocumentFilterFactory filterFactory + ) + { + _migration = migration; + _logger = logger; + _documents = collectionFactory.GetCollection(); + _attributeName = attributeName; + _destinationAttributeName = destinationAttributeName; + _filterFactory = filterFactory; + } + + public async Task Migrate(CancellationToken cancellationToken) + { + var countToMigration = await _documents.CountDocumentsAsync + ( + _filterFactory.AppliesForMigrationFilterFactory(_attributeName, _destinationAttributeName), + cancellationToken: cancellationToken + ); + _logger.LogInformation + ( + "starting to migration {attribute} with {count} documents", + _attributeName, + countToMigration + ); + var start = DateTime.Now; + + await _migration.Migrate(cancellationToken); + + var duration = DateTime.Now - start; + _logger.LogInformation("finished migration for {attribute} in {elapsed}", _attributeName, duration); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/Logging/AttributeMigrationLoggingObserver.cs b/LCU.API.Internal.Document/OwnerNumberMigration/Logging/AttributeMigrationLoggingObserver.cs new file mode 100644 index 0000000..719230c --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/Logging/AttributeMigrationLoggingObserver.cs @@ -0,0 +1,33 @@ +using LCU.API.Internal.DocumentLib.Containers; +using Microsoft.Extensions.Logging; + +namespace OwnerNumberMigration.Logging; + +public delegate IObserver AttributeMigrationLoggingObserverFactory(string attributeName); + +public class AttributeMigrationLoggingObserver : IObserver +{ + private readonly ILogger _logger; + private readonly string _attributeName; + + public AttributeMigrationLoggingObserver(ILogger logger, string attributeName) + { + _logger = logger; + _attributeName = attributeName; + } + + public void OnCompleted() + { + _logger.LogInformation("completed one migration for {attribute}", _attributeName); + } + + public void OnError(Exception error) + { + _logger.LogError("migration error {error} in migration for {attribute}", error, _attributeName); + } + + public void OnNext(DocumentInformation value) + { + _logger.LogInformation("starting one migration for {attribute} on {documentId}", _attributeName, value.DocumentId); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/AccountNumberMigration.cs b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/AccountNumberMigration.cs new file mode 100644 index 0000000..484cc31 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/AccountNumberMigration.cs @@ -0,0 +1,24 @@ +using LCU.API.Internal.DocumentLib.Containers; +using OwnerNumberMigration.ValueParsing; + +namespace OwnerNumberMigration.Migrations; + +public class AccountNumberMigration : AttributeMigrationTask +{ + public AccountNumberMigration + ( + AttributeMigrationFactory factory, + IParsingStrategyFactory strategyFactory, + IMigrationManager manager + ) + : base + ( + "AccountNumber", + nameof(DocumentInformation.AccountNumber), + factory, + strategyFactory, + manager + ) + { + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/AttributeMigrationTask.cs b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/AttributeMigrationTask.cs new file mode 100644 index 0000000..15137e1 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/AttributeMigrationTask.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.Hosting; +using OwnerNumberMigration.ValueParsing; + +namespace OwnerNumberMigration.Migrations; + +public abstract class AttributeMigrationTask : IHostedService +{ + private readonly IAttributeMigration _migration; + private readonly CancellationTokenSource _cancellation = new(); + private readonly IMigrationManager _manager; + + private Task? _runningMigration; + + protected AttributeMigrationTask + ( + string sourceAttribute, + string destinationAttribute, + AttributeMigrationFactory factory, + IParsingStrategyFactory strategyFactory, + IMigrationManager manager + ) + { + _manager = manager; + _migration = factory + ( + sourceAttribute, + strategyFactory.CreateParsingStrategy(sourceAttribute), + destinationAttribute + ); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) return Task.CompletedTask; + + _manager.Start(_runningMigration = _migration.Migrate(_cancellation.Token)); + + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (_runningMigration is not null) + { + if (!_runningMigration.IsCompleted) + { + _cancellation.Cancel(); + try + { + await _runningMigration; + } + catch (TaskCanceledException) + { + } + } + + _runningMigration.Dispose(); + } + + _cancellation.Dispose(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/IMigrationManager.cs b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/IMigrationManager.cs new file mode 100644 index 0000000..484727b --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/IMigrationManager.cs @@ -0,0 +1,6 @@ +namespace OwnerNumberMigration.Migrations; + +public interface IMigrationManager +{ + public void Start(Task migrationTask); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/MigrationManager.cs b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/MigrationManager.cs new file mode 100644 index 0000000..bcf4890 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/MigrationManager.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.Hosting; + +namespace OwnerNumberMigration.Migrations; + +public class MigrationManager : IMigrationManager +{ + private readonly IHostApplicationLifetime _appLifetime; + private readonly IList _migrationTasks = new List(); + + private static readonly IReadOnlySet CompletedStatuses = new HashSet + { + TaskStatus.Faulted, + TaskStatus.Canceled, + TaskStatus.RanToCompletion + }; + + public MigrationManager(IHostApplicationLifetime appLifetime) + { + _appLifetime = appLifetime; + } + + public void Start(Task migrationTask) + { + _migrationTasks.Add(migrationTask); + + migrationTask.ContinueWith(_ => + { + lock (_migrationTasks) + { + if (_migrationTasks.All(t => CompletedStatuses.Contains(t.Status))) + { + _appLifetime.StopApplication(); + } + } + }); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/OrganizationNumberMigration.cs b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/OrganizationNumberMigration.cs new file mode 100644 index 0000000..6abe61e --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/OrganizationNumberMigration.cs @@ -0,0 +1,24 @@ +using LCU.API.Internal.DocumentLib.Containers; +using OwnerNumberMigration.ValueParsing; + +namespace OwnerNumberMigration.Migrations; + +public class OrganizationNumberMigration : AttributeMigrationTask +{ + public OrganizationNumberMigration + ( + AttributeMigrationFactory factory, + IParsingStrategyFactory strategyFactory, + IMigrationManager manager + ) + : base + ( + "OrganizationNumber", + nameof(DocumentInformation.OrganizationNumbers), + factory, + strategyFactory, + manager + ) + { + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/OrganizationNumbersMigration.cs b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/OrganizationNumbersMigration.cs new file mode 100644 index 0000000..5ac0907 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/OrganizationNumbersMigration.cs @@ -0,0 +1,24 @@ +using LCU.API.Internal.DocumentLib.Containers; +using OwnerNumberMigration.ValueParsing; + +namespace OwnerNumberMigration.Migrations; + +public class OrganizationNumbersMigration : AttributeMigrationTask +{ + public OrganizationNumbersMigration + ( + AttributeMigrationFactory factory, + IParsingStrategyFactory strategyFactory, + IMigrationManager manager + ) + : base + ( + "OrganizationNumbers", + nameof(DocumentInformation.OrganizationNumbers), + factory, + strategyFactory, + manager + ) + { + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/PersonNumberMigration.cs b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/PersonNumberMigration.cs new file mode 100644 index 0000000..b18077e --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/PersonNumberMigration.cs @@ -0,0 +1,24 @@ +using LCU.API.Internal.DocumentLib.Containers; +using OwnerNumberMigration.ValueParsing; + +namespace OwnerNumberMigration.Migrations; + +public class PersonNumberMigration : AttributeMigrationTask +{ + public PersonNumberMigration + ( + AttributeMigrationFactory factory, + IParsingStrategyFactory strategyFactory, + IMigrationManager manager + ) + : base + ( + "PersonNumber", + nameof(DocumentInformation.PersonNumbers), + factory, + strategyFactory, + manager + ) + { + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/PersonNumbersMigration.cs b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/PersonNumbersMigration.cs new file mode 100644 index 0000000..7738f11 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/Migrations/PersonNumbersMigration.cs @@ -0,0 +1,24 @@ +using LCU.API.Internal.DocumentLib.Containers; +using OwnerNumberMigration.ValueParsing; + +namespace OwnerNumberMigration.Migrations; + +public class PersonNumbersMigration : AttributeMigrationTask +{ + public PersonNumbersMigration + ( + AttributeMigrationFactory factory, + IParsingStrategyFactory strategyFactory, + IMigrationManager manager + ) + : base + ( + "PersonNumbers", + nameof(DocumentInformation.PersonNumbers), + factory, + strategyFactory, + manager + ) + { + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/OwnerNumberMigration.csproj b/LCU.API.Internal.Document/OwnerNumberMigration/OwnerNumberMigration.csproj new file mode 100644 index 0000000..c828cb6 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/OwnerNumberMigration.csproj @@ -0,0 +1,33 @@ + + + + Exe + net8.0 + enable + enable + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/Program.cs b/LCU.API.Internal.Document/OwnerNumberMigration/Program.cs new file mode 100644 index 0000000..6837135 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/Program.cs @@ -0,0 +1,137 @@ +using LCU.API.Internal.DocumentLib.Configurations; +using LCU.API.Internal.DocumentLib.Utilities; +using LCU.Library.Internal.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OwnerNumberMigration; +using OwnerNumberMigration.Logging; +using OwnerNumberMigration.Migrations; +using ILogger = Microsoft.Extensions.Logging.ILogger; +using Azure.Identity; +using LCU.API.Internal.DocumentLib.Containers; +using Microsoft.Extensions.Logging; +using OwnerNumberMigration.ValueParsing; +using Serilog; +using LCU.Library.Internal.Logging.Models; +using LCU.Library.Internal.Logging; + +await Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((host, configuration) => + { + var vaultConfiguration = configuration.Build(); + var clientSecretCredentials = new ClientSecretCredential + ( + vaultConfiguration["TenantId"], + vaultConfiguration["ClientId"], + vaultConfiguration["ClientSecret"] + ); + var vaultLocation = new Uri($"https://{vaultConfiguration["AzureKeyVaultUrl"]}.vault.azure.net/"); + + configuration.AddAzureKeyVault(vaultLocation, clientSecretCredentials); + }) +/* remove for local */ + .UseSerilog() +/* to here */ +/* local: + .ConfigureLogging(logging => + { + logging + .AddConsole() + .AddFile("Logs/OwnerMigration-{Date}.log", isJson: true) + .SetMinimumLevel(LogLevel.Information); + }) +*/ + .ConfigureServices((host, services) => + { + services.RegisterOwnerMigrationServices(); + + host.Configuration.Bind(services, "MongoDatabase"); + host.Configuration.Bind(services, "NotificationApiConfiguration"); + +/* remove for local */ + var loggingConfiguration = host.Configuration.GetSection("Primary"); + var loggerConfiguration = new LoggingConfiguration + ( + loggingConfiguration["AppName"], + loggingConfiguration["SeqUrl"], + loggingConfiguration["SeqApiKey"] + ); + var lcuLogging = new LcuLogging(loggerConfiguration); + lcuLogging.UseLcuLogging(); +/* to here */ + }) + .RunConsoleAsync(); + +public static class ServiceRegistrationExtension +{ + public static IServiceCollection RegisterOwnerMigrationServices(this IServiceCollection services) + { + ServiceRegistrations.AddDocumentRepositories(services); + services.AddTransient(p => p.GetRequiredService>()); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(p => (migration, name, destination) => + new AttributeMigrationLoggingDecorator + ( + migration, + p.GetRequiredService(), + p.GetRequiredService(), + name, + destination, + p.GetRequiredService() + ) + ); + services.AddTransient(p => name => + new AttributeMigrationLoggingObserver + ( + p.GetRequiredService(), + name + ) + ); + services.AddTransient>(p => (name, strategy, property) => + p.GetRequiredService() + ( + new AttributeMigration + ( + p.GetRequiredService(), + name, + strategy, + property, + p.GetRequiredService()(name), + p.GetRequiredService() + ), + name, + property + ) + ); + services.AddTransient>(p => (name, strategy, property) => + p.GetRequiredService() + ( + new AttributeMigration + ( + p.GetRequiredService(), + name, + strategy, + property, + p.GetRequiredService()(name), + p.GetRequiredService() + ), + name, + property + ) + ); + + services.AddSingleton(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + + return services; + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/Properties/launchSettings.json b/LCU.API.Internal.Document/OwnerNumberMigration/Properties/launchSettings.json new file mode 100644 index 0000000..f9ee814 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "OwnerNumberMigration": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/CommaStringArrayStrategy.cs b/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/CommaStringArrayStrategy.cs new file mode 100644 index 0000000..54e3948 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/CommaStringArrayStrategy.cs @@ -0,0 +1,6 @@ +namespace OwnerNumberMigration.ValueParsing; + +public class CommaStringArrayStrategy : IValueParsingStrategy +{ + public string[] Parse(string value) => value.Split(","); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/IParsingStrategyFactory.cs b/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/IParsingStrategyFactory.cs new file mode 100644 index 0000000..81d9084 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/IParsingStrategyFactory.cs @@ -0,0 +1,6 @@ +namespace OwnerNumberMigration.ValueParsing; + +public interface IParsingStrategyFactory +{ + IValueParsingStrategy CreateParsingStrategy(string attribute); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/IValueParsingStrategy.cs b/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/IValueParsingStrategy.cs new file mode 100644 index 0000000..4bb804b --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/IValueParsingStrategy.cs @@ -0,0 +1,6 @@ +namespace OwnerNumberMigration.ValueParsing; + +public interface IValueParsingStrategy +{ + T Parse(string value); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/ParsingStrategyFactory.cs b/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/ParsingStrategyFactory.cs new file mode 100644 index 0000000..31b704b --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/ParsingStrategyFactory.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace OwnerNumberMigration.ValueParsing; + +public class ParsingStrategyFactory : IParsingStrategyFactory +{ + private readonly IServiceProvider _services; + + public ParsingStrategyFactory(IServiceProvider services) + { + _services = services; + } + + public IValueParsingStrategy CreateParsingStrategy(string attribute) => attribute switch + { + "PersonNumber" => (IValueParsingStrategy)_services.GetRequiredService(), + "PersonNumbers" => (IValueParsingStrategy)_services.GetRequiredService(), + "OrganizationNumber" => (IValueParsingStrategy)_services.GetRequiredService(), + "OrganizationNumbers" => (IValueParsingStrategy)_services.GetRequiredService(), + "AccountNumber" => (IValueParsingStrategy)_services.GetRequiredService(), + _ => throw new NotImplementedException() + }; +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/SingleValueArrayStrategy.cs b/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/SingleValueArrayStrategy.cs new file mode 100644 index 0000000..84d96a0 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/SingleValueArrayStrategy.cs @@ -0,0 +1,6 @@ +namespace OwnerNumberMigration.ValueParsing; + +public class SingleValueArrayStrategy : IValueParsingStrategy +{ + public string[] Parse(string value) => new[] { value }; +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/SingleValueStrategy.cs b/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/SingleValueStrategy.cs new file mode 100644 index 0000000..3c75be4 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigration/ValueParsing/SingleValueStrategy.cs @@ -0,0 +1,6 @@ +namespace OwnerNumberMigration.ValueParsing; + +public class SingleValueStrategy : IValueParsingStrategy +{ + public string Parse(string value) => value; +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/AttributeMigrationTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/AttributeMigrationTest.cs new file mode 100644 index 0000000..6dbe1c6 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/AttributeMigrationTest.cs @@ -0,0 +1,201 @@ +using LCU.API.Internal.DocumentLib.Containers; +using MongoDB.Driver; +using OwnerNumberMigration; +using OwnerNumberMigrationTest.TestDataFactories; +using OwnerNumberMigrationTest.TestDoubles; + +namespace OwnerNumberMigrationTest; + +public class AttributeMigrationTest : IDisposable +{ + private readonly IAttributeMigration _migration; + + private readonly string _attributeName = Guid.NewGuid().ToString(); + private const string DestinationProperty = nameof(DocumentInformation.PersonNumbers); + private string[] GetDestinationProperty(DocumentInformation document) => (string[])typeof(DocumentInformation).GetProperty(DestinationProperty)!.GetValue(document)!; + private readonly FakeMongoCollectionFactory _collectionFactory = new(); + private readonly FakeParsingStrategy _parsingStrategy = new(Array.Empty); + private readonly FakeMigrationObserver _observer = new(); + private readonly FakeDocumentFilterFactory _filterFactory = new(); + + public AttributeMigrationTest() + { + _migration = new AttributeMigration + ( + _collectionFactory, + _attributeName, + _parsingStrategy, + DestinationProperty, + _observer, + _filterFactory + ); + } + + [Test] + public async Task Migrate_AlreadyCancelled_ReturnsImmediately() + { + var cancellation = new CancellationTokenSource(); + cancellation.Cancel(); + var document = await MigrateableDocumentFactory(); + + await _migration.Migrate(cancellation.Token); + + var documentRecord = (await _collectionFactory + .GetCollection() + .FindAsync(Builders.Filter.Eq(d => d.DocumentId, document.DocumentId))) + .Single(); + var unmigratedDestinationProperty = GetDestinationProperty(document); + var migratedDestinationProperty = GetDestinationProperty(documentRecord); + CollectionAssert.AreEquivalent(unmigratedDestinationProperty, migratedDestinationProperty); + } + + [Test] + public void Migrate_DocumentInformationMissingDestination_ThrowsException() + { + var migration = new AttributeMigration + ( + _collectionFactory, + _attributeName, + _parsingStrategy, + Guid.NewGuid().ToString(), + _observer, + _filterFactory + ); + + Assert.ThrowsAsync(() => migration.Migrate(new CancellationToken())); + } + + [Test] + public void Migrate_DocumentInformationMissingDestination_NotifiesObserver() + { + var migration = new AttributeMigration + ( + _collectionFactory, + _attributeName, + _parsingStrategy, + Guid.NewGuid().ToString(), + _observer, + _filterFactory + ); + + var thrown = Assert.ThrowsAsync(() => migration.Migrate(new CancellationToken())); + + Assert.Contains(thrown, _observer.Errors); + } + + [Test] + public async Task Migrate_DocumentInformationMissingDestination_DoesNotMigrate() + { + var document = await MigrateableDocumentFactory(); + var migration = new AttributeMigration + ( + _collectionFactory, + _attributeName, + _parsingStrategy, + Guid.NewGuid().ToString(), + _observer, + _filterFactory + ); + + Assert.ThrowsAsync(() => migration.Migrate(new CancellationToken())); + + var documentRecord = (await _collectionFactory + .GetCollection() + .FindAsync(Builders.Filter.Eq(d => d.DocumentId, document.DocumentId))) + .Single(); + var unmigratedDestinationProperty = GetDestinationProperty(document); + var migratedDestinationProperty = GetDestinationProperty(documentRecord); + CollectionAssert.AreEquivalent(unmigratedDestinationProperty, migratedDestinationProperty); + } + + [Test] + public async Task Migrate_NotCancelled_NotifiesObserverOfDocument() + { + var document = await MigrateableDocumentFactory(); + + await _migration.Migrate(new CancellationToken()); + + var documentsStartedMigrating = _observer.Documents.Select(d => d.doc.DocumentId); + CollectionAssert.Contains(documentsStartedMigrating, document.DocumentId); + } + + [Test] + public async Task Migrate_NotCancelled_MovesValueFromParseStrategyIntoDestinationAndSaves() + { + var document = await MigrateableDocumentFactory(); + var parsedOwners = _parsingStrategy.Values[document.Attributes[_attributeName]] = + new[] + { + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString() + }; + + await _migration.Migrate(new CancellationToken()); + + var documentRecord = (await _collectionFactory + .GetCollection() + .FindAsync(Builders.Filter.Eq(d => d.DocumentId, document.DocumentId))) + .Single(); + var migratedDestinationProperty = GetDestinationProperty(documentRecord); + CollectionAssert.AreEquivalent(parsedOwners, migratedDestinationProperty); + } + + [Test] + public async Task Migrate_AfterDocumentMigrates_NotifiesObserver() + { + var document = await MigrateableDocumentFactory(); + + await _migration.Migrate(new CancellationToken()); + + var ended = DateTime.Now; + var migrationStart = _observer.Documents + .Where(d => d.doc.DocumentId == document.DocumentId) + .Select(d => d.started) + .Single(); + Assert.AreEqual(1, _observer.Completions.Count); + var migrationCompleted = _observer.Completions.Single(); + Assert.That(migrationCompleted, Is.InRange(migrationStart, ended)); + } + + [Test] + public async Task Migrate_TooManyRequests_SleepsForIndicatedTimeThenResumes() + { + var document = await MigrateableDocumentFactory(); + var parsedOwners = _parsingStrategy.Values[document.Attributes[_attributeName]] = + new[] + { + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString() + }; + _collectionFactory.TooManyRequestsOnWriteFor = TimeSpan.FromMilliseconds(25); + + await _migration.Migrate(new CancellationToken()); + + _collectionFactory.TooManyRequestsOnWriteFor = null; + var documentRecord = (await _collectionFactory + .GetCollection() + .FindAsync(Builders.Filter.Eq(d => d.DocumentId, document.DocumentId))) + .Single(); + var migratedDestinationProperty = GetDestinationProperty(documentRecord); + CollectionAssert.AreEquivalent(parsedOwners, migratedDestinationProperty); + } + + private async Task MigrateableDocumentFactory() + { + var document = DocumentInformationFactory.CreateDocumentInformation(); + document.Attributes[_attributeName] = Guid.NewGuid().ToString(); + + _filterFactory.ApplicableDocuments[(_attributeName, DestinationProperty)] = new[] { document }; + + await _collectionFactory + .GetCollection() + .InsertOneAsync(document); + + return document; + } + + public void Dispose() + { + _collectionFactory.Dispose(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/DocumentFilterFactoryTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/DocumentFilterFactoryTest.cs new file mode 100644 index 0000000..522ba6a --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/DocumentFilterFactoryTest.cs @@ -0,0 +1,74 @@ +using LCU.API.Internal.DocumentLib.Containers; +using MongoDB.Driver; +using OwnerNumberMigration; +using OwnerNumberMigrationTest.TestDataFactories; +using OwnerNumberMigrationTest.TestDoubles; + +namespace OwnerNumberMigrationTest; + +public class DocumentFilterFactoryTest : IDisposable +{ + private readonly IDocumentFilterFactory _factory = new DocumentFilterFactory(); + + private readonly FakeMongoCollectionFactory _collectionFactory = new(); + private readonly string _attributeName = "PersonNumber"; + private readonly string _destinationAttributeName = nameof(DocumentInformation.PersonNumbers); + + [Test] + public async Task AppliesForMigrationFilterFactory_DocumentContainsDestinationAttribute_FilteredOut() + { + var document = DocumentInformationFactory.CreateDocumentInformation(); + typeof(DocumentInformation) + .GetProperty(_destinationAttributeName) + ?.SetValue(document, new[]{ Guid.NewGuid().ToString() }); + _collectionFactory.DatabasePostFix = Guid.NewGuid().ToString(); + var collection = _collectionFactory.GetCollection(); + await collection.InsertOneAsync(document); + + var filter = _factory.AppliesForMigrationFilterFactory(_attributeName, _destinationAttributeName); + + var documents = await collection.CountDocumentsAsync(filter); + Assert.AreEqual(0, documents); + } + + [Test] + public async Task AppliesForMigrationFilterFactory_DocumentMissingSourceAttributeAttribute_FilteredOut() + { + var document = DocumentInformationFactory.CreateDocumentInformation(); + document.Attributes.Remove(_attributeName); + _collectionFactory.DatabasePostFix = Guid.NewGuid().ToString(); + var collection = _collectionFactory.GetCollection(); + await collection.InsertOneAsync(document); + + var filter = _factory.AppliesForMigrationFilterFactory(_attributeName, _destinationAttributeName); + + var documents = await collection.CountDocumentsAsync(filter); + Assert.AreEqual(0, documents); + } + + [Test] + public async Task + AppliesForMigrationFilterFactory_DocumentContainsSourceAttributeAttributeAndNotDestinationAttribute_Included() + { + var document = DocumentInformationFactory.CreateDocumentInformation(); + document.Attributes[_attributeName] = Guid.NewGuid().ToString(); + _collectionFactory.DatabasePostFix = Guid.NewGuid().ToString(); + var collection = _collectionFactory.GetCollection(); + await collection.InsertOneAsync(document); + await collection.UpdateOneAsync + ( + d => d.DocumentId == document.DocumentId, + Builders.Update.Unset(_destinationAttributeName) + ); + + var filter = _factory.AppliesForMigrationFilterFactory(_attributeName, _destinationAttributeName); + + var documents = await collection.CountDocumentsAsync(filter); + Assert.AreEqual(1, documents); + } + + public void Dispose() + { + _collectionFactory.Dispose(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/Logging/AttributeMigrationLoggingDecoratorTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Logging/AttributeMigrationLoggingDecoratorTest.cs new file mode 100644 index 0000000..c4e1a5e --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Logging/AttributeMigrationLoggingDecoratorTest.cs @@ -0,0 +1,94 @@ +using System.Text.RegularExpressions; +using LCU.API.Internal.DocumentLib.Containers; +using Microsoft.Extensions.Logging; +using Moq; +using OwnerNumberMigration; +using OwnerNumberMigration.Logging; +using OwnerNumberMigrationTest.TestDataFactories; +using OwnerNumberMigrationTest.TestDoubles; + +namespace OwnerNumberMigrationTest.Logging; + +public class AttributeMigrationLoggingDecoratorTest : IDisposable +{ + private readonly IAttributeMigration _decoratedMigration; + + private readonly string _attributeName = Guid.NewGuid().ToString(); + private readonly string _destinationAttributeName = Guid.NewGuid().ToString(); + private readonly Mock _rawMigration = new(); + private readonly FakeLogger _logger = new(); + private readonly FakeMongoCollectionFactory _collectionFactory = new(); + private readonly FakeDocumentFilterFactory _filterFactory = new(); + + public AttributeMigrationLoggingDecoratorTest() + { + _decoratedMigration = new AttributeMigrationLoggingDecorator + ( + _rawMigration.Object, + _logger, + _collectionFactory, + _attributeName, + _destinationAttributeName, + _filterFactory + ); + } + + [Test] + public async Task Migrate_AllCases_LogsCountToMigrate() + { + var documentCount = new Random(DateTime.Now.Millisecond).Next(3, 10); + var documents = EnumerableDataSeriesFactory + .CreateSeries(DocumentInformationFactory.CreateDocumentInformation, documentCount) + .ToArray(); + await _collectionFactory.GetCollection().InsertManyAsync(documents); + _filterFactory.ApplicableDocuments[(_attributeName, _destinationAttributeName)] = documents; + + await _decoratedMigration.Migrate(new CancellationToken()); + + Func logMatch = l => l.LogLevel == LogLevel.Information + && l.Message.Contains(_attributeName) + && new Regex($"\\b{documentCount}\\b").IsMatch(l.Message); + Assert.IsTrue(_logger.Logs.Count(logMatch) == 1); + } + + [Test] + public async Task Migrate_AllCases_InvokesInnerMigration() + { + var cancellation = new CancellationToken(); + + await _decoratedMigration.Migrate(cancellation); + + _rawMigration.Verify(m => m.Migrate(cancellation), Times.Once); + } + + [Test] + public async Task Migrate_InnerMigrationCompletes_LogsElapsedTime() + { + var migrationDuration = TimeSpan.FromMilliseconds(100); + _rawMigration + .Setup(m => m.Migrate(It.IsAny())) + .Returns(() => Task.Delay(migrationDuration)); + var actionStart = DateTime.Now; + + await _decoratedMigration.Migrate(new CancellationToken()); + + var elapsed = DateTime.Now - actionStart; + Func logMatch = l => + { + var timespan = Regex.Match(l.Message, "\\d\\d:\\d\\d:\\d\\d\\.\\d\\d\\d\\d\\d\\d\\d"); + if (!timespan.Success) return false; + var duration = TimeSpan.Parse(timespan.ValueSpan); + + return l.LogLevel == LogLevel.Information + && l.Message.Contains(_attributeName) + && duration >= migrationDuration + && duration <= elapsed; + }; + Assert.IsTrue(_logger.Logs.Count(logMatch) == 1); + } + + public void Dispose() + { + _collectionFactory.Dispose(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/Logging/AttributeMigrationLoggingObserverTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Logging/AttributeMigrationLoggingObserverTest.cs new file mode 100644 index 0000000..123a117 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Logging/AttributeMigrationLoggingObserverTest.cs @@ -0,0 +1,57 @@ +using LCU.API.Internal.DocumentLib.Containers; +using Microsoft.Extensions.Logging; +using OwnerNumberMigration.Logging; +using OwnerNumberMigrationTest.TestDataFactories; +using OwnerNumberMigrationTest.TestDoubles; + +namespace OwnerNumberMigrationTest.Logging; + +public class AttributeMigrationLoggingObserverTest +{ + private readonly IObserver _observer; + + private readonly FakeLogger _logger = new(); + private readonly string _attributeName = Guid.NewGuid().ToString(); + + public AttributeMigrationLoggingObserverTest() + { + _observer = new AttributeMigrationLoggingObserver(_logger, _attributeName); + } + + [Test] + public void OnCompleted_AllCases_InfoLogsCompletedAttribute() + { + _observer.OnCompleted(); + + Func logMatch = l => l.LogLevel == LogLevel.Information + && l.Message.Contains("completed", StringComparison.OrdinalIgnoreCase) + && l.Message.Contains(_attributeName); + Assert.IsTrue(_logger.Logs.Count(logMatch) == 1); + } + + [Test] + public void OnError_AllCases_ErrorLogsExceptionAndAttribute() + { + var error = new Exception(Guid.NewGuid().ToString()); + + _observer.OnError(error); + + Func logMatch = l => l.LogLevel == LogLevel.Error + && l.Message.Contains(error.Message) + && l.Message.Contains(_attributeName); + Assert.IsTrue(_logger.Logs.Count(logMatch) == 1); + } + + [Test] + public void OnNext_AllCases_InfoLogsStartingAttributeAndDocumentId() + { + var document = DocumentInformationFactory.CreateDocumentInformation(); + + _observer.OnNext(document); + + Func logMatch = l => l.LogLevel == LogLevel.Information + && l.Message.Contains(document.DocumentId.ToString()) + && l.Message.Contains(_attributeName); + Assert.IsTrue(_logger.Logs.Count(logMatch) == 1); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/AccountNumberMigrationTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/AccountNumberMigrationTest.cs new file mode 100644 index 0000000..745c472 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/AccountNumberMigrationTest.cs @@ -0,0 +1,25 @@ +using LCU.API.Internal.DocumentLib.Containers; +using Microsoft.Extensions.Hosting; +using OwnerNumberMigration.Migrations; +using OwnerNumberMigration.ValueParsing; +using OwnerNumberMigration; + +namespace OwnerNumberMigrationTest.Migrations; + +public class AccountNumberMigrationTest : AttributeMigrationTaskTest +{ + protected override string SourceAttribute => "AccountNumber"; + + protected override string DestinationAttribute => nameof(DocumentInformation.AccountNumber); + + protected override IHostedService MigrationTaskFactory + ( + AttributeMigrationFactory factory, + IParsingStrategyFactory strategyFactory, + IMigrationManager manager + ) => new AccountNumberMigration(factory, strategyFactory, manager); + + public AccountNumberMigrationTest() : base(() => string.Empty) + { + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/AttributeMigrationTaskTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/AttributeMigrationTaskTest.cs new file mode 100644 index 0000000..66d1515 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/AttributeMigrationTaskTest.cs @@ -0,0 +1,170 @@ +using Microsoft.Extensions.Hosting; +using Moq; +using OwnerNumberMigration; +using OwnerNumberMigration.Migrations; +using OwnerNumberMigration.ValueParsing; +using OwnerNumberMigrationTest.TestDoubles; + +namespace OwnerNumberMigrationTest.Migrations; + +public abstract class AttributeMigrationTaskTest +{ + protected abstract IHostedService MigrationTaskFactory + ( + AttributeMigrationFactory factory, + IParsingStrategyFactory strategyFactory, + IMigrationManager manager + ); + private IHostedService MigrationServiceFactory() => MigrationTaskFactory + ( + (_, _, _) => _migration.Object, + StrategyFactory.Object, + _manager.Object + ); + + protected abstract string SourceAttribute { get; } + protected abstract string DestinationAttribute { get; } + + protected readonly Mock StrategyFactory = new(); + private readonly FakeParsingStrategy _parsingStrategy; + private readonly Mock _migration = new(); + private readonly Mock _manager = new(); + + protected AttributeMigrationTaskTest(Func newT) + { + _parsingStrategy = new FakeParsingStrategy(newT); + } + + [SetUp] + public void SetUp() + { + StrategyFactory + .Setup(f => f.CreateParsingStrategy(SourceAttribute)) + .Returns((IValueParsingStrategy)_parsingStrategy); + + _migration.Reset(); + } + + [Test] + public void Constructor_AllCases_CreatesMigrationWithJobTypeAttributes() + { + string? sourceAttribute = null; + string? destinationAttribute = null; + IValueParsingStrategy? parsingStrategy = null; + + MigrationTaskFactory + ( + (source, strategy, destination) => + { + sourceAttribute = source; + parsingStrategy = strategy; + destinationAttribute = destination; + + return _migration.Object; + }, + StrategyFactory.Object, + _manager.Object + ); + + Assert.AreEqual(SourceAttribute, sourceAttribute); + Assert.AreSame(_parsingStrategy, parsingStrategy); + Assert.AreEqual(DestinationAttribute, destinationAttribute); + } + + [Test] + public async Task StartAsync_TokenCancelled_DoesNotStartMigration() + { + var cancellation = new CancellationToken(true); + var migration = MigrationServiceFactory(); + + await migration.StartAsync(cancellation); + + _migration.Verify(m => m.Migrate(It.IsAny()), Times.Never); + } + + [Test] + public async Task StartAsync_NotCanceled_StartsMigrationTaskAndReturns() + { + var cancellation = new CancellationToken(); + _migration + .Setup(m => m.Migrate(It.IsAny())) + .Returns(async token => + { + await Task.Delay(TimeSpan.MaxValue, token); + } + ); + var migration = MigrationServiceFactory(); + + await migration.StartAsync(cancellation); + + _migration.Verify(m => m.Migrate(It.IsAny()), Times.Once); + } + + [Test] + public async Task StartAsync_NotCanceled_RegistersMigrationTaskWithManager() + { + var cancellation = new CancellationToken(); + var migrationTask = Task.Run(() => { }); + _migration + .Setup(m => m.Migrate(It.IsAny())) + .Returns(migrationTask); + var migration = MigrationServiceFactory(); + + await migration.StartAsync(cancellation); + + _manager.Verify(m => m.Start(migrationTask), Times.Once); + } + + [Test] + public async Task StopAsync_NotStarted_ReturnsWithoutError() + { + var cancellation = new CancellationToken(); + var migration = MigrationServiceFactory(); + + Assert.DoesNotThrowAsync(() => migration.StopAsync(cancellation)); + } + + [Test] + public async Task StopAsync_AlreadyCompleted_CleansUpMigrationTask() + { + var cancellation = new CancellationToken(); + Task? migrationTask = null; + _migration + .Setup(m => m.Migrate(It.IsAny())) + .Returns(token => migrationTask = Task.Run + ( + () => { }, + token + )); + var migration = MigrationServiceFactory(); + + await migration.StartAsync(cancellation); + await migrationTask; + await migration.StopAsync(cancellation); + + Assert.AreEqual(TaskStatus.RanToCompletion, migrationTask.Status); + } + + [Test] + public async Task StopAsync_NotCompleted_CancelsMigrationTaskThenCleansUp() + { + var cancellation = new CancellationToken(); + Task? migrationTask = null; + _migration + .Setup(m => m.Migrate(It.IsAny())) + .Returns(token => migrationTask = Task.Run + ( + async () => + { + await Task.Delay(-1, token); + }, + token + )); + var migration = MigrationServiceFactory(); + + await migration.StartAsync(cancellation); + await migration.StopAsync(cancellation); + + Assert.AreEqual(TaskStatus.Canceled, migrationTask.Status); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/MigrationManagerTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/MigrationManagerTest.cs new file mode 100644 index 0000000..366010f --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/MigrationManagerTest.cs @@ -0,0 +1,102 @@ +using Microsoft.Extensions.Hosting; +using Moq; +using OwnerNumberMigration.Migrations; +using OwnerNumberMigrationTest.TestDataFactories; + +namespace OwnerNumberMigrationTest.Migrations; + +public class MigrationManagerTest +{ + private readonly IMigrationManager _manager; + + private readonly Mock _lifetime = new(); + + public MigrationManagerTest() + { + _manager = new MigrationManager(_lifetime.Object); + } + + [SetUp] + public void SetUp() + { + _lifetime.Reset(); + } + + [Test] + public async Task Start_OneTaskThatFinishes_EndsApplication() + { + var applicationStopped = new SemaphoreSlim(0); + _lifetime + .Setup(l => l.StopApplication()) + .Callback(() => applicationStopped.Release()); + var task = Task.CompletedTask; + + _manager.Start(task); + + await applicationStopped.WaitAsync(TimeSpan.FromMilliseconds(50)); + _lifetime.Verify(l => l.StopApplication(), Times.AtLeastOnce); + } + + [Test] + public async Task Start_OneTaskThatCancels_EndsApplication() + { + var applicationStopped = new SemaphoreSlim(0); + _lifetime + .Setup(l => l.StopApplication()) + .Callback(() => applicationStopped.Release()); + var task = Task.FromCanceled(new CancellationToken(true)); + + _manager.Start(task); + + await applicationStopped.WaitAsync(TimeSpan.FromMilliseconds(50)); + _lifetime.Verify(l => l.StopApplication(), Times.AtLeastOnce); + } + + [Test] + public async Task Start_OneTaskThatErrors_EndsApplication() + { + var applicationStopped = new SemaphoreSlim(0); + _lifetime + .Setup(l => l.StopApplication()) + .Callback(() => applicationStopped.Release()); + _lifetime + .Setup(l => l.StopApplication()) + .Callback(() => applicationStopped.Release()); + var task = Task.FromException(new Exception()); + + _manager.Start(task); + + await applicationStopped.WaitAsync(TimeSpan.FromMilliseconds(50)); + _lifetime.Verify(l => l.StopApplication(), Times.AtLeastOnce); + } + + [Test] + public async Task Start_MultipleTasksThatFinish_EndsApplicationAfterAll() + { + var applicationStopped = new SemaphoreSlim(0); + _lifetime + .Setup(l => l.StopApplication()) + .Callback(() => applicationStopped.Release()); + var semaphores = EnumerableDataSeriesFactory + .CreateSeries(() => new SemaphoreSlim(0), 5) + .ToArray(); + var tasks = semaphores + .Select(s => Task.Run(async () => await s.WaitAsync())) + .ToArray(); + + foreach(var task in tasks) _manager.Start(task); + + _lifetime.Verify(l => l.StopApplication(), Times.Never); + foreach (var semaphore in semaphores[..^1]) + { + semaphore.Release(); + _lifetime.Verify(l => l.StopApplication(), Times.Never); + } + foreach (var semaphore in semaphores[^1..]) + { + semaphore.Release(); + } + await applicationStopped.WaitAsync(TimeSpan.FromMilliseconds(50)); + _lifetime.Verify(l => l.StopApplication(), Times.AtLeastOnce); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/OrganizationNumberMigrationTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/OrganizationNumberMigrationTest.cs new file mode 100644 index 0000000..4cae4cf --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/OrganizationNumberMigrationTest.cs @@ -0,0 +1,25 @@ +using LCU.API.Internal.DocumentLib.Containers; +using Microsoft.Extensions.Hosting; +using OwnerNumberMigration; +using OwnerNumberMigration.Migrations; +using OwnerNumberMigration.ValueParsing; + +namespace OwnerNumberMigrationTest.Migrations; + +public class OrganizationNumberMigrationTest : AttributeMigrationTaskTest +{ + protected override string SourceAttribute => "OrganizationNumber"; + + protected override string DestinationAttribute => nameof(DocumentInformation.OrganizationNumbers); + + protected override IHostedService MigrationTaskFactory + ( + AttributeMigrationFactory factory, + IParsingStrategyFactory strategyFactory, + IMigrationManager manager + ) => new OrganizationNumberMigration(factory, strategyFactory, manager); + + public OrganizationNumberMigrationTest() : base(Array.Empty) + { + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/OrganizationNumbersMigrationTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/OrganizationNumbersMigrationTest.cs new file mode 100644 index 0000000..01fa489 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/OrganizationNumbersMigrationTest.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Hosting; +using OwnerNumberMigration.Migrations; +using OwnerNumberMigration; +using OwnerNumberMigration.ValueParsing; +using LCU.API.Internal.DocumentLib.Containers; + +namespace OwnerNumberMigrationTest.Migrations; + +public class OrganizationNumbersMigrationTest : AttributeMigrationTaskTest +{ + protected override string SourceAttribute => "OrganizationNumbers"; + + protected override string DestinationAttribute => nameof(DocumentInformation.OrganizationNumbers); + + protected override IHostedService MigrationTaskFactory + ( + AttributeMigrationFactory factory, + IParsingStrategyFactory strategyFactory, + IMigrationManager manager + ) => new OrganizationNumbersMigration(factory, strategyFactory, manager); + + public OrganizationNumbersMigrationTest() : base(Array.Empty) + { + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/PersonNumberMigrationTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/PersonNumberMigrationTest.cs new file mode 100644 index 0000000..de73f82 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/PersonNumberMigrationTest.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Hosting; +using OwnerNumberMigration.Migrations; +using OwnerNumberMigration; +using OwnerNumberMigration.ValueParsing; +using LCU.API.Internal.DocumentLib.Containers; + +namespace OwnerNumberMigrationTest.Migrations; + +public class PersonNumberMigrationTest : AttributeMigrationTaskTest +{ + protected override string SourceAttribute => "PersonNumber"; + + protected override string DestinationAttribute => nameof(DocumentInformation.PersonNumbers); + + protected override IHostedService MigrationTaskFactory + ( + AttributeMigrationFactory factory, + IParsingStrategyFactory strategyFactory, + IMigrationManager manager + ) => new PersonNumberMigration(factory, strategyFactory, manager); + + public PersonNumberMigrationTest() : base(Array.Empty) + { + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/PersonNumbersMigrationTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/PersonNumbersMigrationTest.cs new file mode 100644 index 0000000..45c2626 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Migrations/PersonNumbersMigrationTest.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Hosting; +using OwnerNumberMigration.Migrations; +using OwnerNumberMigration; +using OwnerNumberMigration.ValueParsing; +using LCU.API.Internal.DocumentLib.Containers; + +namespace OwnerNumberMigrationTest.Migrations; + +public class PersonNumbersMigrationTest : AttributeMigrationTaskTest +{ + protected override string SourceAttribute => "PersonNumbers"; + + protected override string DestinationAttribute => nameof(DocumentInformation.PersonNumbers); + + protected override IHostedService MigrationTaskFactory + ( + AttributeMigrationFactory factory, + IParsingStrategyFactory strategyFactory, + IMigrationManager manager + ) => new PersonNumbersMigration(factory, strategyFactory, manager); + + public PersonNumbersMigrationTest() : base(Array.Empty) + { + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/OwnerNumberMigrationIntegrationTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/OwnerNumberMigrationIntegrationTest.cs new file mode 100644 index 0000000..67f5f45 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/OwnerNumberMigrationIntegrationTest.cs @@ -0,0 +1,197 @@ +using LCU.API.Internal.DocumentLib.Configurations; +using LCU.API.Internal.DocumentLib.Containers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using MongoDB.Driver; +using OwnerNumberMigrationTest.TestDataFactories; +using OwnerNumberMigrationTest.TestDoubles; + +namespace OwnerNumberMigrationTest; + +[Category("Integration")] +public class OwnerNumberMigrationIntegrationTest : IDisposable +{ + private readonly FakeMongoCollectionFactory _collectionFactory = new(); + + private readonly DocumentInformation _personNumber; + private readonly DocumentInformation _personNumbers; + private readonly DocumentInformation _organizationNumber; + private readonly DocumentInformation _organizationNumbers; + private readonly DocumentInformation _accountNumber; + + private readonly DocumentInformation _personNumberMigrated; + private readonly DocumentInformation _personNumbersMigrated; + private readonly DocumentInformation _organizationNumberMigrated; + private readonly DocumentInformation _organizationNumbersMigrated; + private readonly DocumentInformation _accountNumberMigrated; + + public OwnerNumberMigrationIntegrationTest() + { + static string SingleOwnerFactory() => Guid.NewGuid().ToString(); + + static string MultipleOwnersFactory() => string.Join + ( + ",", + EnumerableDataSeriesFactory.CreateSeries(SingleOwnerFactory, 3) + ); + + _personNumber = DocumentInformationFactory.CreateDocumentInformation(); + _personNumber.Attributes.Clear(); + _personNumber.Attributes["PersonNumber"] = SingleOwnerFactory(); + _personNumbers = DocumentInformationFactory.CreateDocumentInformation(); + _personNumbers.Attributes.Clear(); + _personNumbers.Attributes["PersonNumbers"] = MultipleOwnersFactory(); + _organizationNumber = DocumentInformationFactory.CreateDocumentInformation(); + _organizationNumber.Attributes.Clear(); + _organizationNumber.Attributes["OrganizationNumber"] = SingleOwnerFactory(); + _organizationNumbers = DocumentInformationFactory.CreateDocumentInformation(); + _organizationNumbers.Attributes.Clear(); + _organizationNumbers.Attributes["OrganizationNumbers"] = MultipleOwnersFactory(); + _accountNumber = DocumentInformationFactory.CreateDocumentInformation(); + _accountNumber.Attributes.Clear(); + _accountNumber.Attributes["AccountNumber"] = SingleOwnerFactory(); + + _personNumberMigrated = DocumentInformationFactory.CreateDocumentInformation(); + _personNumberMigrated.Attributes.Clear(); + _personNumberMigrated.Attributes["PersonNumber"] = SingleOwnerFactory(); + _personNumberMigrated.PersonNumbers = _personNumberMigrated.Attributes["PersonNumber"].Split(","); + _personNumbersMigrated = DocumentInformationFactory.CreateDocumentInformation(); + _personNumbersMigrated.Attributes.Clear(); + _personNumbersMigrated.Attributes["PersonNumbers"] = MultipleOwnersFactory(); + _personNumbersMigrated.PersonNumbers = _personNumbersMigrated.Attributes["PersonNumbers"].Split(","); + _organizationNumberMigrated = DocumentInformationFactory.CreateDocumentInformation(); + _organizationNumberMigrated.Attributes.Clear(); + _organizationNumberMigrated.Attributes["OrganizationNumber"] = SingleOwnerFactory(); + _organizationNumberMigrated.OrganizationNumbers = + _organizationNumberMigrated.Attributes["OrganizationNumber"].Split(","); + _organizationNumbersMigrated = DocumentInformationFactory.CreateDocumentInformation(); + _organizationNumbersMigrated.Attributes.Clear(); + _organizationNumbersMigrated.Attributes["OrganizationNumbers"] = MultipleOwnersFactory(); + _organizationNumbersMigrated.OrganizationNumbers = + _organizationNumbersMigrated.Attributes["OrganizationNumbers"].Split(","); + _accountNumberMigrated = DocumentInformationFactory.CreateDocumentInformation(); + _accountNumberMigrated.Attributes.Clear(); + _accountNumberMigrated.Attributes["AccountNumber"] = SingleOwnerFactory(); + _accountNumberMigrated.AccountNumber = _accountNumberMigrated.Attributes["AccountNumber"]; + } + + [SetUp] + public async Task Setup() + { + var collection = _collectionFactory.GetCollection(); + + await collection.InsertManyAsync(new[] + { + _personNumber, + _personNumbers, + _organizationNumber, + _organizationNumbers, + _accountNumber, + _personNumberMigrated, + _personNumbersMigrated, + _organizationNumberMigrated, + _organizationNumbersMigrated, + _accountNumberMigrated + }); + + await collection.UpdateManyAsync + ( + Builders.Filter.In + ( + d => d.DocumentId, + new[] + { + _personNumber.DocumentId, + _personNumbers.DocumentId, + _organizationNumber.DocumentId, + _organizationNumbers.DocumentId, + _accountNumber.DocumentId + } + ), + Builders.Update + .Unset(nameof(DocumentInformation.PersonNumbers)) + .Unset(nameof(DocumentInformation.OrganizationNumbers)) + .Unset(nameof(DocumentInformation.AccountNumber)) + ); + } + + [Test] + public async Task Program_AllCases_MigratesDocuments() + { + await Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.RegisterOwnerMigrationServices(); + + services.AddSingleton(NullLogger.Instance); + + services.AddSingleton(new DatabaseConfiguration + { + ConnectionString = _collectionFactory.ConnectionString, + DatabaseName = _collectionFactory.DatabaseName(), + DocumentContainerName = _collectionFactory.CollectionName() + }); + }) + .Build() + .RunAsync(); + + var migratedDocuments = (await + (await + _collectionFactory + .GetCollection() + .FindAsync(Builders.Filter.Empty) + ).ToListAsync()) + .ToDictionary(d => d.DocumentId); + + CollectionAssert.AreEqual(_personNumber.Attributes["PersonNumber"].Split(","), + migratedDocuments[_personNumber.DocumentId].PersonNumbers); + CollectionAssert.AreEqual(_personNumbers.Attributes["PersonNumbers"].Split(","), + migratedDocuments[_personNumbers.DocumentId].PersonNumbers); + CollectionAssert.AreEqual(_organizationNumber.Attributes["OrganizationNumber"].Split(","), + migratedDocuments[_organizationNumber.DocumentId].OrganizationNumbers); + CollectionAssert.AreEqual(_organizationNumbers.Attributes["OrganizationNumbers"].Split(","), + migratedDocuments[_organizationNumbers.DocumentId].OrganizationNumbers); + Assert.AreEqual(_accountNumber.Attributes["AccountNumber"], + migratedDocuments[_accountNumber.DocumentId].AccountNumber); + + CollectionAssert.AreEqual(_personNumber.Attributes["PersonNumber"], + migratedDocuments[_personNumber.DocumentId].Attributes["PersonNumber"]); + CollectionAssert.AreEqual(_personNumbers.Attributes["PersonNumbers"], + migratedDocuments[_personNumbers.DocumentId].Attributes["PersonNumbers"]); + CollectionAssert.AreEqual(_organizationNumber.Attributes["OrganizationNumber"], + migratedDocuments[_organizationNumber.DocumentId].Attributes["OrganizationNumber"]); + CollectionAssert.AreEqual(_organizationNumbers.Attributes["OrganizationNumbers"], + migratedDocuments[_organizationNumbers.DocumentId].Attributes["OrganizationNumbers"]); + Assert.AreEqual(_accountNumber.Attributes["AccountNumber"], + migratedDocuments[_accountNumber.DocumentId].Attributes["AccountNumber"]); + + CollectionAssert.AreEqual(_personNumberMigrated.PersonNumbers, + migratedDocuments[_personNumberMigrated.DocumentId].PersonNumbers); + CollectionAssert.AreEqual(_personNumbersMigrated.PersonNumbers, + migratedDocuments[_personNumbersMigrated.DocumentId].PersonNumbers); + CollectionAssert.AreEqual(_organizationNumberMigrated.OrganizationNumbers, + migratedDocuments[_organizationNumberMigrated.DocumentId].OrganizationNumbers); + CollectionAssert.AreEqual(_organizationNumbersMigrated.OrganizationNumbers, + migratedDocuments[_organizationNumbersMigrated.DocumentId].OrganizationNumbers); + CollectionAssert.AreEqual(_accountNumberMigrated.OrganizationNumbers, + migratedDocuments[_accountNumberMigrated.DocumentId].OrganizationNumbers); + + CollectionAssert.AreEqual(_personNumberMigrated.Attributes["PersonNumber"], + migratedDocuments[_personNumberMigrated.DocumentId].Attributes["PersonNumber"]); + CollectionAssert.AreEqual(_personNumbersMigrated.Attributes["PersonNumbers"], + migratedDocuments[_personNumbersMigrated.DocumentId].Attributes["PersonNumbers"]); + CollectionAssert.AreEqual(_organizationNumberMigrated.Attributes["OrganizationNumber"], + migratedDocuments[_organizationNumberMigrated.DocumentId].Attributes["OrganizationNumber"]); + CollectionAssert.AreEqual(_organizationNumbersMigrated.Attributes["OrganizationNumbers"], + migratedDocuments[_organizationNumbersMigrated.DocumentId].Attributes["OrganizationNumbers"]); + CollectionAssert.AreEqual(_accountNumberMigrated.Attributes["AccountNumber"], + migratedDocuments[_accountNumberMigrated.DocumentId].Attributes["AccountNumber"]); + } + + public void Dispose() + { + _collectionFactory.Dispose(); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/OwnerNumberMigrationTest.csproj b/LCU.API.Internal.Document/OwnerNumberMigrationTest/OwnerNumberMigrationTest.csproj new file mode 100644 index 0000000..634085a --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/OwnerNumberMigrationTest.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + + + + + diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDataFactories/DocumentInformationFactory.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDataFactories/DocumentInformationFactory.cs new file mode 100644 index 0000000..95b1b95 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDataFactories/DocumentInformationFactory.cs @@ -0,0 +1,33 @@ +using LCU.API.Internal.DocumentLib.Containers; + +namespace OwnerNumberMigrationTest.TestDataFactories; + +public static class DocumentInformationFactory +{ + public static DocumentInformation CreateDocumentInformation() => new() + { + DocumentId = Guid.NewGuid(), + DocumentBlobId = Guid.NewGuid(), + TraceId = Guid.NewGuid(), + Name = Guid.NewGuid().ToString(), + Type = Guid.NewGuid().ToString(), + Source = Guid.NewGuid().ToString(), + DataSource = Guid.NewGuid().ToString(), + FileSize = DateTime.Now.Millisecond, + ScanStatus = DateTime.Now.Millisecond % 2 == 0, + UploadDate = DateTime.Now, + LastModifiedDate = DateTime.Now, + CreatedDate = DateTime.Now, + PersonNumbers = new[] { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + OrganizationNumbers = new []{ Guid.NewGuid().ToString() , Guid.NewGuid().ToString() }, + AccountNumber = Guid.NewGuid().ToString(), + Attributes = new Dictionary + { + ["PersonNumber"] = Guid.NewGuid().ToString(), + ["PersonNumbers"] = $"{Guid.NewGuid()},{Guid.NewGuid()}", + ["OrganizationNumber"] = Guid.NewGuid().ToString(), + ["OrganizationNumbers"] = $"{Guid.NewGuid()},{Guid.NewGuid()}", + ["AccountNumber"] = Guid.NewGuid().ToString() + }, + }; +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDataFactories/EnumerableDataSeriesFactory.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDataFactories/EnumerableDataSeriesFactory.cs new file mode 100644 index 0000000..ba8398b --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDataFactories/EnumerableDataSeriesFactory.cs @@ -0,0 +1,8 @@ +namespace OwnerNumberMigrationTest.TestDataFactories; + +public static class EnumerableDataSeriesFactory +{ + public static IEnumerable CreateSeries(Func singleFactory, int count) => Enumerable + .Repeat(singleFactory, count) + .Select(f => f()); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeDocumentFilterFactory.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeDocumentFilterFactory.cs new file mode 100644 index 0000000..a075f72 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeDocumentFilterFactory.cs @@ -0,0 +1,28 @@ +using LCU.API.Internal.DocumentLib.Containers; +using MongoDB.Driver; +using OwnerNumberMigration; + +namespace OwnerNumberMigrationTest.TestDoubles; + +public class FakeDocumentFilterFactory : IDocumentFilterFactory +{ + public IDictionary<(string attribute, string destination), DocumentInformation[]> ApplicableDocuments { get; } = + new Dictionary<(string, string), DocumentInformation[]>(); + + public FilterDefinition AppliesForMigrationFilterFactory + ( + string attributeName, + string destinationAttributeName + ) + { + var applicable = ApplicableDocuments.TryGetValue((attributeName, destinationAttributeName), out var documents) + ? documents + : Array.Empty(); + + return Builders.Filter.In + ( + d => d.DocumentId, + applicable.Select(d => d.DocumentId) + ); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeLogger.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeLogger.cs new file mode 100644 index 0000000..349d413 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeLogger.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Logging; + +namespace OwnerNumberMigrationTest.TestDoubles; + +public record Log(LogLevel LogLevel, Exception? Exception, string Message); + +public class FakeLogger : ILogger +{ + public IList Logs { get; }= new List(); + + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + throw new NotImplementedException(); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + Logs.Add(new Log(logLevel, exception, formatter(state, exception))); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeMigrationObserver.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeMigrationObserver.cs new file mode 100644 index 0000000..53d563e --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeMigrationObserver.cs @@ -0,0 +1,27 @@ +using LCU.API.Internal.DocumentLib.Containers; + +namespace OwnerNumberMigrationTest.TestDoubles; + +public class FakeMigrationObserver : IObserver +{ + public List<(DocumentInformation doc, DateTime started)> Documents { get; } = new(); + + public List Errors { get; } = new(); + + public List Completions { get; } = new(); + + public void OnCompleted() + { + Completions.Add(DateTime.Now); + } + + public void OnError(Exception error) + { + Errors.Add(error); + } + + public void OnNext(DocumentInformation value) + { + Documents.Add((value, DateTime.Now)); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeMongoCollectionFactory.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeMongoCollectionFactory.cs new file mode 100644 index 0000000..0322998 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeMongoCollectionFactory.cs @@ -0,0 +1,739 @@ +using System.Net; +using LCU.API.Internal.DocumentLib.Utilities; +using Mongo2Go; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using MongoDB.Driver.Core.Connections; +using MongoDB.Driver.Core.Servers; + +namespace OwnerNumberMigrationTest.TestDoubles; + +public class FakeMongoCollectionFactory : IMongoCollectionFactory, IDisposable +{ + private readonly MongoDbRunner _runner = MongoDbRunner.Start(); + public string ConnectionString => _runner.ConnectionString; + public string DatabaseName() => typeof(T).Name + DatabasePostFix; + public string CollectionName() => typeof(T).Name; + public string DatabasePostFix { get; set; } = string.Empty; + public TimeSpan? TooManyRequestsOnWriteFor { get; set; } + + public IMongoCollection GetCollection() + { + var collection = new MongoClient(_runner.ConnectionString) + .GetDatabase(DatabaseName()) + .GetCollection(CollectionName()); + + if (TooManyRequestsOnWriteFor is not null) + { + collection = new TooManyRequestsOnWriteCollectionDecorator(collection, TooManyRequestsOnWriteFor.Value); + } + + return collection; + } + + public void Dispose() + { + _runner.Dispose(); + } +} + +public class TooManyRequestsOnWriteCollectionDecorator : IMongoCollection +{ + private IMongoCollection _collection; + + private readonly TimeSpan _tooManyRequestsDuring; + private DateTime? _firstRequest = null; + + public TooManyRequestsOnWriteCollectionDecorator(IMongoCollection collection, TimeSpan tooManyRequestsDuring) + { + _collection = collection; + _tooManyRequestsDuring = tooManyRequestsDuring; + } + + private void CheckTimout() + { + if (_firstRequest is null) + { + _firstRequest = DateTime.Now; + + var dns = new DnsEndPoint("localhost", 27017); + var cluster = _collection.Database.Client.Cluster.ClusterId; + var server = new ServerId(cluster, dns); + var connection = new ConnectionId(server); + + var result = new BsonDocument(); + result["code"] = 16500; + result["errmsg"] = $"{Guid.NewGuid()} RetryAfterMs={_tooManyRequestsDuring.TotalMilliseconds} {Guid.NewGuid()}"; + + throw new MongoCommandException(connection, "", new BsonDocument(), result); + } + + if (DateTime.Now - _firstRequest < _tooManyRequestsDuring) + { + throw new Exception("cool it, mate"); + } + } + + public IAsyncCursor Aggregate(PipelineDefinition pipeline, AggregateOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.Aggregate(pipeline, options, cancellationToken); + } + + public IAsyncCursor Aggregate(IClientSessionHandle session, PipelineDefinition pipeline, + AggregateOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.Aggregate(session, pipeline, options, cancellationToken); + } + + public Task> AggregateAsync(PipelineDefinition pipeline, AggregateOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.AggregateAsync(pipeline, options, cancellationToken); + } + + public Task> AggregateAsync(IClientSessionHandle session, PipelineDefinition pipeline, AggregateOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.AggregateAsync(session, pipeline, options, cancellationToken); + } + + public void AggregateToCollection(PipelineDefinition pipeline, AggregateOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + _collection.AggregateToCollection(pipeline, options, cancellationToken); + } + + public void AggregateToCollection(IClientSessionHandle session, PipelineDefinition pipeline, + AggregateOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + _collection.AggregateToCollection(session, pipeline, options, cancellationToken); + } + + public Task AggregateToCollectionAsync(PipelineDefinition pipeline, AggregateOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.AggregateToCollectionAsync(pipeline, options, cancellationToken); + } + + public Task AggregateToCollectionAsync(IClientSessionHandle session, PipelineDefinition pipeline, + AggregateOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.AggregateToCollectionAsync(session, pipeline, options, cancellationToken); + } + + public BulkWriteResult BulkWrite(IEnumerable> requests, BulkWriteOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.BulkWrite(requests, options, cancellationToken); + } + + public BulkWriteResult BulkWrite(IClientSessionHandle session, IEnumerable> requests, BulkWriteOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.BulkWrite(session, requests, options, cancellationToken); + } + + public Task> BulkWriteAsync(IEnumerable> requests, BulkWriteOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.BulkWriteAsync(requests, options, cancellationToken); + } + + public Task> BulkWriteAsync(IClientSessionHandle session, IEnumerable> requests, BulkWriteOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.BulkWriteAsync(session, requests, options, cancellationToken); + } + + public long Count(FilterDefinition filter, CountOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.Count(filter, options, cancellationToken); + } + + public long Count(IClientSessionHandle session, FilterDefinition filter, CountOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.Count(session, filter, options, cancellationToken); + } + + public Task CountAsync(FilterDefinition filter, CountOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.CountAsync(filter, options, cancellationToken); + } + + public Task CountAsync(IClientSessionHandle session, FilterDefinition filter, CountOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.CountAsync(session, filter, options, cancellationToken); + } + + public long CountDocuments(FilterDefinition filter, CountOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.CountDocuments(filter, options, cancellationToken); + } + + public long CountDocuments(IClientSessionHandle session, FilterDefinition filter, CountOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.CountDocuments(session, filter, options, cancellationToken); + } + + public Task CountDocumentsAsync(FilterDefinition filter, CountOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.CountDocumentsAsync(filter, options, cancellationToken); + } + + public Task CountDocumentsAsync(IClientSessionHandle session, FilterDefinition filter, CountOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.CountDocumentsAsync(session, filter, options, cancellationToken); + } + + public DeleteResult DeleteMany(FilterDefinition filter, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.DeleteMany(filter, cancellationToken); + } + + public DeleteResult DeleteMany(FilterDefinition filter, DeleteOptions options, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.DeleteMany(filter, options, cancellationToken); + } + + public DeleteResult DeleteMany(IClientSessionHandle session, FilterDefinition filter, DeleteOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.DeleteMany(session, filter, options, cancellationToken); + } + + public Task DeleteManyAsync(FilterDefinition filter, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.DeleteManyAsync(filter, cancellationToken); + } + + public Task DeleteManyAsync(FilterDefinition filter, DeleteOptions options, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.DeleteManyAsync(filter, options, cancellationToken); + } + + public Task DeleteManyAsync(IClientSessionHandle session, FilterDefinition filter, DeleteOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.DeleteManyAsync(session, filter, options, cancellationToken); + } + + public DeleteResult DeleteOne(FilterDefinition filter, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.DeleteOne(filter, cancellationToken); + } + + public DeleteResult DeleteOne(FilterDefinition filter, DeleteOptions options, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.DeleteOne(filter, options, cancellationToken); + } + + public DeleteResult DeleteOne(IClientSessionHandle session, FilterDefinition filter, DeleteOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.DeleteOne(session, filter, options, cancellationToken); + } + + public Task DeleteOneAsync(FilterDefinition filter, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.DeleteOneAsync(filter, cancellationToken); + } + + public Task DeleteOneAsync(FilterDefinition filter, DeleteOptions options, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.DeleteOneAsync(filter, options, cancellationToken); + } + + public Task DeleteOneAsync(IClientSessionHandle session, FilterDefinition filter, DeleteOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.DeleteOneAsync(session, filter, options, cancellationToken); + } + + public IAsyncCursor Distinct(FieldDefinition field, FilterDefinition filter, DistinctOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.Distinct(field, filter, options, cancellationToken); + } + + public IAsyncCursor Distinct(IClientSessionHandle session, FieldDefinition field, FilterDefinition filter, + DistinctOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.Distinct(session, field, filter, options, cancellationToken); + } + + public Task> DistinctAsync(FieldDefinition field, FilterDefinition filter, DistinctOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.DistinctAsync(field, filter, options, cancellationToken); + } + + public Task> DistinctAsync(IClientSessionHandle session, FieldDefinition field, FilterDefinition filter, + DistinctOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.DistinctAsync(session, field, filter, options, cancellationToken); + } + + public long EstimatedDocumentCount(EstimatedDocumentCountOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.EstimatedDocumentCount(options, cancellationToken); + } + + public Task EstimatedDocumentCountAsync(EstimatedDocumentCountOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.EstimatedDocumentCountAsync(options, cancellationToken); + } + + public IAsyncCursor FindSync(FilterDefinition filter, FindOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.FindSync(filter, options, cancellationToken); + } + + public IAsyncCursor FindSync(IClientSessionHandle session, FilterDefinition filter, FindOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.FindSync(session, filter, options, cancellationToken); + } + + public Task> FindAsync(FilterDefinition filter, FindOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.FindAsync(filter, options, cancellationToken); + } + + public Task> FindAsync(IClientSessionHandle session, FilterDefinition filter, FindOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.FindAsync(session, filter, options, cancellationToken); + } + + public TProjection FindOneAndDelete(FilterDefinition filter, FindOneAndDeleteOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.FindOneAndDelete(filter, options, cancellationToken); + } + + public TProjection FindOneAndDelete(IClientSessionHandle session, FilterDefinition filter, + FindOneAndDeleteOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.FindOneAndDelete(session, filter, options, cancellationToken); + } + + public Task FindOneAndDeleteAsync(FilterDefinition filter, FindOneAndDeleteOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.FindOneAndDeleteAsync(filter, options, cancellationToken); + } + + public Task FindOneAndDeleteAsync(IClientSessionHandle session, FilterDefinition filter, + FindOneAndDeleteOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.FindOneAndDeleteAsync(session, filter, options, cancellationToken); + } + + public TProjection FindOneAndReplace(FilterDefinition filter, T replacement, + FindOneAndReplaceOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.FindOneAndReplace(filter, replacement, options, cancellationToken); + } + + public TProjection FindOneAndReplace(IClientSessionHandle session, FilterDefinition filter, T replacement, + FindOneAndReplaceOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.FindOneAndReplace(session, filter, replacement, options, cancellationToken); + } + + public Task FindOneAndReplaceAsync(FilterDefinition filter, T replacement, + FindOneAndReplaceOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.FindOneAndReplaceAsync(filter, replacement, options, cancellationToken); + } + + public Task FindOneAndReplaceAsync(IClientSessionHandle session, FilterDefinition filter, T replacement, + FindOneAndReplaceOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.FindOneAndReplaceAsync(session, filter, replacement, options, cancellationToken); + } + + public TProjection FindOneAndUpdate(FilterDefinition filter, UpdateDefinition update, + FindOneAndUpdateOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.FindOneAndUpdate(filter, update, options, cancellationToken); + } + + public TProjection FindOneAndUpdate(IClientSessionHandle session, FilterDefinition filter, + UpdateDefinition update, FindOneAndUpdateOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.FindOneAndUpdate(session, filter, update, options, cancellationToken); + } + + public Task FindOneAndUpdateAsync(FilterDefinition filter, UpdateDefinition update, + FindOneAndUpdateOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.FindOneAndUpdateAsync(filter, update, options, cancellationToken); + } + + public Task FindOneAndUpdateAsync(IClientSessionHandle session, FilterDefinition filter, UpdateDefinition update, + FindOneAndUpdateOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.FindOneAndUpdateAsync(session, filter, update, options, cancellationToken); + } + + public void InsertOne(T document, InsertOneOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + _collection.InsertOne(document, options, cancellationToken); + } + + public void InsertOne(IClientSessionHandle session, T document, InsertOneOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + _collection.InsertOne(session, document, options, cancellationToken); + } + + public Task InsertOneAsync(T document, CancellationToken _cancellationToken) + { + CheckTimout(); + + return _collection.InsertOneAsync(document, _cancellationToken); + } + + public Task InsertOneAsync(T document, InsertOneOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.InsertOneAsync(document, options, cancellationToken); + } + + public Task InsertOneAsync(IClientSessionHandle session, T document, InsertOneOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.InsertOneAsync(session, document, options, cancellationToken); + } + + public void InsertMany(IEnumerable documents, InsertManyOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + _collection.InsertMany(documents, options, cancellationToken); + } + + public void InsertMany(IClientSessionHandle session, IEnumerable documents, InsertManyOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + _collection.InsertMany(session, documents, options, cancellationToken); + } + + public Task InsertManyAsync(IEnumerable documents, InsertManyOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.InsertManyAsync(documents, options, cancellationToken); + } + + public Task InsertManyAsync(IClientSessionHandle session, IEnumerable documents, InsertManyOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.InsertManyAsync(session, documents, options, cancellationToken); + } + + public IAsyncCursor MapReduce(BsonJavaScript map, BsonJavaScript reduce, MapReduceOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.MapReduce(map, reduce, options, cancellationToken); + } + + public IAsyncCursor MapReduce(IClientSessionHandle session, BsonJavaScript map, BsonJavaScript reduce, + MapReduceOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.MapReduce(session, map, reduce, options, cancellationToken); + } + + public Task> MapReduceAsync(BsonJavaScript map, BsonJavaScript reduce, MapReduceOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.MapReduceAsync(map, reduce, options, cancellationToken); + } + + public Task> MapReduceAsync(IClientSessionHandle session, BsonJavaScript map, BsonJavaScript reduce, + MapReduceOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.MapReduceAsync(session, map, reduce, options, cancellationToken); + } + + public IFilteredMongoCollection OfType() where TDerivedDocument : T + { + return _collection.OfType(); + } + + public ReplaceOneResult ReplaceOne(FilterDefinition filter, T replacement, ReplaceOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.ReplaceOne(filter, replacement, options, cancellationToken); + } + + public ReplaceOneResult ReplaceOne(FilterDefinition filter, T replacement, UpdateOptions options, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.ReplaceOne(filter, replacement, options, cancellationToken); + } + + public ReplaceOneResult ReplaceOne(IClientSessionHandle session, FilterDefinition filter, T replacement, + ReplaceOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.ReplaceOne(session, filter, replacement, options, cancellationToken); + } + + public ReplaceOneResult ReplaceOne(IClientSessionHandle session, FilterDefinition filter, T replacement, UpdateOptions options, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.ReplaceOne(session, filter, replacement, options, cancellationToken); + } + + public Task ReplaceOneAsync(FilterDefinition filter, T replacement, ReplaceOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.ReplaceOneAsync(filter, replacement, options, cancellationToken); + } + + public Task ReplaceOneAsync(FilterDefinition filter, T replacement, UpdateOptions options, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.ReplaceOneAsync(filter, replacement, options, cancellationToken); + } + + public Task ReplaceOneAsync(IClientSessionHandle session, FilterDefinition filter, T replacement, + ReplaceOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.ReplaceOneAsync(session, filter, replacement, options, cancellationToken); + } + + public Task ReplaceOneAsync(IClientSessionHandle session, FilterDefinition filter, T replacement, UpdateOptions options, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.ReplaceOneAsync(session, filter, replacement, options, cancellationToken); + } + + public UpdateResult UpdateMany(FilterDefinition filter, UpdateDefinition update, UpdateOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.UpdateMany(filter, update, options, cancellationToken); + } + + public UpdateResult UpdateMany(IClientSessionHandle session, FilterDefinition filter, UpdateDefinition update, + UpdateOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.UpdateMany(session, filter, update, options, cancellationToken); + } + + public Task UpdateManyAsync(FilterDefinition filter, UpdateDefinition update, UpdateOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.UpdateManyAsync(filter, update, options, cancellationToken); + } + + public Task UpdateManyAsync(IClientSessionHandle session, FilterDefinition filter, UpdateDefinition update, + UpdateOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.UpdateManyAsync(session, filter, update, options, cancellationToken); + } + + public UpdateResult UpdateOne(FilterDefinition filter, UpdateDefinition update, UpdateOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.UpdateOne(filter, update, options, cancellationToken); + } + + public UpdateResult UpdateOne(IClientSessionHandle session, FilterDefinition filter, UpdateDefinition update, + UpdateOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.UpdateOne(session, filter, update, options, cancellationToken); + } + + public Task UpdateOneAsync(FilterDefinition filter, UpdateDefinition update, UpdateOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.UpdateOneAsync(filter, update, options, cancellationToken); + } + + public Task UpdateOneAsync(IClientSessionHandle session, FilterDefinition filter, UpdateDefinition update, + UpdateOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + CheckTimout(); + + return _collection.UpdateOneAsync(session, filter, update, options, cancellationToken); + } + + public IChangeStreamCursor Watch(PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.Watch(pipeline, options, cancellationToken); + } + + public IChangeStreamCursor Watch(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, + ChangeStreamOptions options = null, CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.Watch(session, pipeline, options, cancellationToken); + } + + public Task> WatchAsync(PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.WatchAsync(pipeline, options, cancellationToken); + } + + public Task> WatchAsync(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return _collection.WatchAsync(session, pipeline, options, cancellationToken); + } + + public IMongoCollection WithReadConcern(ReadConcern readConcern) + { + return _collection.WithReadConcern(readConcern); + } + + public IMongoCollection WithReadPreference(ReadPreference readPreference) + { + return _collection.WithReadPreference(readPreference); + } + + public IMongoCollection WithWriteConcern(WriteConcern writeConcern) + { + return _collection.WithWriteConcern(writeConcern); + } + + public CollectionNamespace CollectionNamespace => _collection.CollectionNamespace; + + public IMongoDatabase Database => _collection.Database; + + public IBsonSerializer DocumentSerializer => _collection.DocumentSerializer; + + public IMongoIndexManager Indexes => _collection.Indexes; + + public MongoCollectionSettings Settings => _collection.Settings; +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeParsingStrategy.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeParsingStrategy.cs new file mode 100644 index 0000000..af99d10 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/TestDoubles/FakeParsingStrategy.cs @@ -0,0 +1,19 @@ +using OwnerNumberMigration.ValueParsing; + +namespace OwnerNumberMigrationTest.TestDoubles; + +public class FakeParsingStrategy : IValueParsingStrategy +{ + public IDictionary Values { get; } = new Dictionary(); + + private readonly Func _newT; + + public FakeParsingStrategy(Func newT) + { + this._newT = newT; + } + + public T Parse(string value) => Values.TryGetValue(value, out var parsed) + ? parsed + : _newT(); +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/Usings.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Usings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/ValueParsing/CommaStringArrayStrategyTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/ValueParsing/CommaStringArrayStrategyTest.cs new file mode 100644 index 0000000..91fd322 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/ValueParsing/CommaStringArrayStrategyTest.cs @@ -0,0 +1,32 @@ +using OwnerNumberMigration.ValueParsing; +using OwnerNumberMigrationTest.TestDataFactories; + +namespace OwnerNumberMigrationTest.ValueParsing; + +public class CommaStringArrayStrategyTest +{ + private readonly IValueParsingStrategy _parsingStrategy = new CommaStringArrayStrategy(); + + [Test] + public void Parse_StringContainsSingleValue_ReturnsAnArrayContainingOnlyTheValue() + { + var owners = Guid.NewGuid().ToString(); + + var parsedOwners = _parsingStrategy.Parse(owners); + + Assert.AreEqual(1, parsedOwners.Length); + CollectionAssert.Contains(parsedOwners, owners); + } + + [Test] + public void Parse_StringContainsCommas_ReturnsAnArrayContainingTheValueSplitOnComma() + { + var ownerCount = new Random(DateTime.Now.Millisecond).Next(3, 10); + var owners = string.Join(",", EnumerableDataSeriesFactory.CreateSeries(() => Guid.NewGuid().ToString(), ownerCount)); + + var parsedOwners = _parsingStrategy.Parse(owners); + + Assert.AreEqual(ownerCount, parsedOwners.Length); + CollectionAssert.AreEquivalent(parsedOwners, owners.Split(",")); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/ValueParsing/ParsingStrategyFactoryTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/ValueParsing/ParsingStrategyFactoryTest.cs new file mode 100644 index 0000000..e8b920c --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/ValueParsing/ParsingStrategyFactoryTest.cs @@ -0,0 +1,81 @@ +using Microsoft.Extensions.DependencyInjection; +using OwnerNumberMigration.ValueParsing; + +namespace OwnerNumberMigrationTest.ValueParsing; + +public class ParsingStrategyFactoryTest +{ + private readonly IParsingStrategyFactory _factory; + + private readonly SingleValueArrayStrategy _singleArray = new(); + private readonly CommaStringArrayStrategy _comma = new(); + private readonly SingleValueStrategy _single = new(); + + public ParsingStrategyFactoryTest() + { + var services = new ServiceCollection(); + services.AddSingleton(_singleArray); + services.AddSingleton(_comma); + services.AddSingleton(_single); + + _factory = new ParsingStrategyFactory(services.BuildServiceProvider()); + } + + [Test] + public void CreateParsingStrategy_PersonNumber_ReturnsSingleArrayStrategy() + { + const string Attribute = "PersonNumber"; + + var strategy = _factory.CreateParsingStrategy(Attribute); + + Assert.AreSame(_singleArray, strategy); + } + + [Test] + public void CreateParsingStrategy_PersonNumbers_ReturnsCommaStrategy() + { + const string Attribute = "PersonNumbers"; + + var strategy = _factory.CreateParsingStrategy(Attribute); + + Assert.AreSame(_comma, strategy); + } + + [Test] + public void CreateParsingStrategy_OrganizationNumber_ReturnsSingleArrayStrategy() + { + const string Attribute = "OrganizationNumber"; + + var strategy = _factory.CreateParsingStrategy(Attribute); + + Assert.AreSame(_singleArray, strategy); + } + + [Test] + public void CreateParsingStrategy_OrganizationNumbers_ReturnsCommaStrategy() + { + const string Attribute = "OrganizationNumbers"; + + var strategy = _factory.CreateParsingStrategy(Attribute); + + Assert.AreSame(_comma, strategy); + } + + [Test] + public void CreateParsingStrategy_AccountNumber_ReturnsSingleValueStrategy() + { + const string Attribute = "AccountNumber"; + + var strategy = _factory.CreateParsingStrategy(Attribute); + + Assert.AreSame(_single, strategy); + } + + [Test] + public void CreateParsingStrategy_UnkownValue_ThrowsException() + { + var attribute = Guid.NewGuid().ToString(); + + Assert.Throws(() => _factory.CreateParsingStrategy(attribute)); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/ValueParsing/SingleValueArrayStrategyTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/ValueParsing/SingleValueArrayStrategyTest.cs new file mode 100644 index 0000000..e9d21a3 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/ValueParsing/SingleValueArrayStrategyTest.cs @@ -0,0 +1,19 @@ +using OwnerNumberMigration.ValueParsing; + +namespace OwnerNumberMigrationTest.ValueParsing; + +public class SingleValueArrayStrategyTest +{ + private readonly IValueParsingStrategy _parsingStrategy = new SingleValueArrayStrategy(); + + [Test] + public void ParseOwners_AllCases_ReturnsAnArrayContainingOnlyTheValue() + { + var owners = Guid.NewGuid().ToString(); + + var parsedOwners = _parsingStrategy.Parse(owners); + + Assert.AreEqual(1, parsedOwners.Length); + CollectionAssert.Contains(parsedOwners, owners); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/OwnerNumberMigrationTest/ValueParsing/SingleValueStrategyTest.cs b/LCU.API.Internal.Document/OwnerNumberMigrationTest/ValueParsing/SingleValueStrategyTest.cs new file mode 100644 index 0000000..8c54924 --- /dev/null +++ b/LCU.API.Internal.Document/OwnerNumberMigrationTest/ValueParsing/SingleValueStrategyTest.cs @@ -0,0 +1,18 @@ +using OwnerNumberMigration.ValueParsing; + +namespace OwnerNumberMigrationTest.ValueParsing; + +public class SingleValueStrategyTest +{ + private readonly IValueParsingStrategy _parsingStrategy = new SingleValueStrategy(); + + [Test] + public void ParseOwners_AllCases_ReturnsAnArrayContainingOnlyTheValue() + { + var account = Guid.NewGuid().ToString(); + + var parsedAccount = _parsingStrategy.Parse(account); + + Assert.AreEqual(account, parsedAccount); + } +} \ No newline at end of file diff --git a/LCU.API.Internal.Document/README.md b/LCU.API.Internal.Document/README.md new file mode 100644 index 0000000..1e9239f --- /dev/null +++ b/LCU.API.Internal.Document/README.md @@ -0,0 +1,117 @@ +# Landmark Credit Union Document API + +--- + +This API is the layer that connects Landmarks Azure Blob Storage instance that stores document content and its Cosmos Mongo DB database +that stores relative DocumentInformation and an AuditHistory for all documents in its Blob Storage. It's currently being developed as the +central storage location for landmarks documentation. It has an audit log that captures and details changes to document information as an added level of security. + +You should always review and update this document to encompass your changes. + +# Getting Started + +--- + +The required environment is .NET Core 6.0, Git, and something to work with C#. Code is checked out from the Azure DevOps Repository. + +### Suggestion for IDE + +- JetBrains Rider +- Visual Studio 2022 +- Visual Studio Code + +### Pre Pull Request + +- Did you commit with a commitizen style message? +- Did you add all needed unit testing, and ensure all existing tests are passing? +- Make sure you didn't accidentally commit and secrets or passwords. + +# Configuration + +--- + +Environment changes are stored and applied as Azure Pipeline Variables, per environment. + +### MaliciousFileFoundEmailList + +This is a set list of emails in the configuration. These emails will be notified through the UpdateDocumentScanStatus endpoint if Azure Defender scans a document and determines it is a malicious file. + +### FeatureManagement + +- GetDocumentCheckOwnership + - This is a boolean flag, that enables a check on document ownership on the GetDocumentByBlobId and GetDocument endpoints. It will compare the given Person or Organization number to see if the returned DocumentInformation contains that number. + +# Libraries + +--- + +This application contains the library 'LCU.Library.Internal.DocumentModels' that holds the current request and response models used to communicate with the DocumentAPI. +This model is used by the DocumentTypeUploader and DocumentRetriever APIs in their communication with DocumentApi. + +# Build and Test + +--- + +Current testing for this project are: +- LCU.API.Internal.DocumentTest +- LCU.API.Internal.DocumentLibTest +- LCU.API.Internal.DocumentModelsTest +- LCU.API.Internal.Document.PerformanceTest + +### PerformanceTest + +This project utilizes BenchmarkDotNet. + +Further documentation can be found here: [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) + +# Database + +--- + +There are two main databases that this API communicates with. DocumentService will return one one of these, or join the two in the response model. + +### Cosmos Mongo DB: + +Database: "DocumentInformation" + +This holds the DocumentInformation model. This is the metadata of the document, holding any ownership or document type information. + +### Azure Blob Storage: + +Container: "documents" + +This is the content byte array of the document itself. It is referenced in the Cosmos Mongo DB "DocumentsInformation" container as DocumentBlobId. + +# Data Migration + +--- + +Using a document DB there is no schema in the database (it's in application code) therefore, unlike a relational database, an ALTER -> UPDATE transaction isn't possible. Data changes like an alter, update have to be done with application code which can read the old schema and translate it into the new one or the consuming application code needs to be able to handle data in both the old and the new schema. +Past migration code can be found in the migrations solution folder. + +### Past Migrations + +- OwnerNumberMigration + - Document owning numbers (PersonNumbers, OrganizationNumbers) were previously being held in the Attributes, and then parsed and searched through the filter. This was creating performance issues. + To fix this, we had to move all of the numbers into new string list properties. + - AccountNumber was also moved into its own property to help with performance. + - We now use these for AccountOwner type document searches. PrimaryOwner type documents are still searched by Attributes.PersonNumber or Attributes.OrganizationNumber (singular). Both searches use the new AccountNumber property. + +# Helpful Documents + +--- + +[DocumentManager Project File Repository](https://landmarkcu.sharepoint.com/sites/ITProgrammingDevelopmentTeam/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FITProgrammingDevelopmentTeam%2FShared%20Documents%2FGeneral%2FProjects%2F2022%5FDocument%20Manager&viewid=99d45851%2D1c5c%2D483e%2Dbdda%2D7d5bee978feb) +| +[Platform Wiki](https://landmarkcu.visualstudio.com/Platform/_wiki/wikis/Platform.wiki/241/Welcome) + +### Project Files + +- [DocumentManager Overview](https://landmarkcu.sharepoint.com/:p:/r/sites/ITProgrammingDevelopmentTeam/_layouts/15/Doc.aspx?sourcedoc=%7BF7A54712-F5E5-4F9D-9008-3F8CE3BFFA3F%7D&file=Document%20Manager%20Overview.pptx&action=edit&mobileredirect=true) +- [DocumentManager High-Level Workflows](https://landmarkcu.sharepoint.com/:u:/r/sites/ITProgrammingDevelopmentTeam/_layouts/15/Doc.aspx?sourcedoc=%7BCEE7D32E-6768-413A-8FA3-D7A660791F4F%7D&file=Document%20Manager%20High%20Level%20WorkFlows.vsdx&action=default&mobileredirect=true) +- [DocumentManager Components](https://landmarkcu.sharepoint.com/sites/ITProgrammingDevelopmentTeam/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FITProgrammingDevelopmentTeam%2FShared%20Documents%2FGeneral%2FProjects%2F2022%5FDocument%20Manager%2F05%5FDocumentation%2FDocument%5FManager%5FComponents%2Epdf&viewid=99d45851%2D1c5c%2D483e%2Dbdda%2D7d5bee978feb&parent=%2Fsites%2FITProgrammingDevelopmentTeam%2FShared%20Documents%2FGeneral%2FProjects%2F2022%5FDocument%20Manager%2F05%5FDocumentation) +- [DocumentType Attributes](https://landmarkcu.sharepoint.com/:x:/r/sites/ITProgrammingDevelopmentTeam/_layouts/15/Doc.aspx?sourcedoc=%7B39809990-5B33-404B-89D3-6E8EFDDEEA75%7D&file=Document%20Type%20Attributes.xlsx&action=default&mobileredirect=true) +- [DocumentCodeDetails](https://landmarkcu.sharepoint.com/:x:/r/sites/ITProgrammingDevelopmentTeam/_layouts/15/Doc.aspx?sourcedoc=%7B1BE0B75A-8377-437C-A228-5F68588B3761%7D&file=DocTypes.xlsx&action=default&mobileredirect=true) + +### Workflow Designs: +- [Original DocumentIngestor Workflow Design](https://landmarkcu.sharepoint.com/:u:/r/sites/ITProgrammingDevelopmentTeam/_layouts/15/Doc.aspx?sourcedoc=%7BFA3B785D-10DF-46DE-9B66-509B2BA820F5%7D&file=Ingestor_Design_WorkFlow.vsdx&action=default&mobileredirect=true&DefaultItemOpen=1) \ No newline at end of file diff --git a/LCU.API.Internal.Document/azdo_pipelines/azure-k8s-pipeline.yaml b/LCU.API.Internal.Document/azdo_pipelines/azure-k8s-pipeline.yaml new file mode 100644 index 0000000..c520683 --- /dev/null +++ b/LCU.API.Internal.Document/azdo_pipelines/azure-k8s-pipeline.yaml @@ -0,0 +1,55 @@ +parameters: +- name: target_environment + displayName: Target Environment + type: string + default: qa + values: + - qa + - uat + - prd +- name: k8s_namespace + displayName: K8s Namespace to deploy to + type: string + default: landmarkcu + values: + - landmarkcu +variables: +- template: ./config-files/${{ parameters.target_environment }}-pvars.yaml +- name: source_branch_name + value: $[replace(replace(variables['Build.SourceBranch'], 'refs/heads/', ''), '/', '-')] +- name: docker_container_repository_name + value: landmarkcu/lcu.api.internal.document-document +resources: + repositories: + - repository: azdo_pipeline_templates + type: git + name: Infrastructure Platform/azdo-pipeline-templates + ref: main +stages: +# DOTNET BUILD AND NUGET PUSH +- template: dotnet-nuget-build-push.yaml@azdo_pipeline_templates + parameters: + azdo_agent_pool: $(pvar_azdo_agent_pool_nuget) + azure_artifacts_feed: 'LCU-Internal' +# DOCKER BUILD AND PUSH +- template: docker-build-push.yaml@azdo_pipeline_templates + parameters: + azdo_agent_pool: $(pvar_azdo_agent_pool) + docker_registry_service_connection: $(pvar_docker_registry_service_connection) + docker_container_repository_name: $(docker_container_repository_name) + dockerfile_path: ./Dockerfile + docker_image_tags: | + build-$(Build.BuildId) + $(source_branch_name) +# K8S DEPLOY +- template: k8s-deploy.yaml@azdo_pipeline_templates + parameters: + dependsOn: CI + condition: or(eq(variables['Build.SourceBranchName'], 'develop'), eq(variables['Build.SourceBranchName'], 'master')) + azdo_agent_pool: $(pvar_azdo_agent_pool) + kubectl_version: $(pvar_kubectl_version) + k8s_service_connection: $(pvar_k8s_service_connection) + k8s_namespace: ${{ parameters.k8s_namespace }} + k8s_kustomization_path: ./kustomize/overlays/${{ parameters.target_environment }} + docker_registry_service_connection: $(pvar_docker_registry_service_connection) + docker_image_to_deploy: $(docker_container_repository_name):build-$(Build.BuildId) diff --git a/LCU.API.Internal.Document/azdo_pipelines/azure-pipelines-feature-branches.yaml b/LCU.API.Internal.Document/azdo_pipelines/azure-pipelines-feature-branches.yaml new file mode 100644 index 0000000..0dd52f4 --- /dev/null +++ b/LCU.API.Internal.Document/azdo_pipelines/azure-pipelines-feature-branches.yaml @@ -0,0 +1,10 @@ +trigger: + branches: + include: + - '*' + exclude: + - 'master' + - 'develop' +extends: + template: azure-k8s-pipeline.yaml + diff --git a/LCU.API.Internal.Document/azdo_pipelines/azure-pipelines-prd.yaml b/LCU.API.Internal.Document/azdo_pipelines/azure-pipelines-prd.yaml new file mode 100644 index 0000000..bc94844 --- /dev/null +++ b/LCU.API.Internal.Document/azdo_pipelines/azure-pipelines-prd.yaml @@ -0,0 +1,9 @@ +trigger: + branches: + include: + - 'master' +extends: + template: azure-k8s-pipeline.yaml + parameters: + target_environment: prd + k8s_namespace: landmarkcu diff --git a/LCU.API.Internal.Document/azdo_pipelines/azure-pipelines-qa.yaml b/LCU.API.Internal.Document/azdo_pipelines/azure-pipelines-qa.yaml new file mode 100644 index 0000000..f581dd4 --- /dev/null +++ b/LCU.API.Internal.Document/azdo_pipelines/azure-pipelines-qa.yaml @@ -0,0 +1,9 @@ +trigger: + branches: + include: + - 'develop' +extends: + template: azure-k8s-pipeline.yaml + parameters: + target_environment: qa + k8s_namespace: landmarkcu diff --git a/LCU.API.Internal.Document/azdo_pipelines/azure-pipelines-uat.yaml b/LCU.API.Internal.Document/azdo_pipelines/azure-pipelines-uat.yaml new file mode 100644 index 0000000..76ca2ae --- /dev/null +++ b/LCU.API.Internal.Document/azdo_pipelines/azure-pipelines-uat.yaml @@ -0,0 +1,9 @@ +trigger: + branches: + include: + - 'master' +extends: + template: azure-k8s-pipeline.yaml + parameters: + target_environment: uat + k8s_namespace: landmarkcu diff --git a/LCU.API.Internal.Document/azdo_pipelines/config-files/prd-pvars.yaml b/LCU.API.Internal.Document/azdo_pipelines/config-files/prd-pvars.yaml new file mode 100644 index 0000000..34ac540 --- /dev/null +++ b/LCU.API.Internal.Document/azdo_pipelines/config-files/prd-pvars.yaml @@ -0,0 +1,7 @@ +--- +variables: + pvar_azdo_agent_pool: vmss-sub-df-lcu-apps-prd-lz-uscz + pvar_docker_registry_service_connection: Docker Registry LMCU + pvar_k8s_service_connection: svcon-sub-df-lcu-apps-prd-aks-k8s-uscz + pvar_kubectl_version: 1.25.9 + pvar_azdo_agent_pool_nuget: LCUOnPrem \ No newline at end of file diff --git a/LCU.API.Internal.Document/azdo_pipelines/config-files/qa-pvars.yaml b/LCU.API.Internal.Document/azdo_pipelines/config-files/qa-pvars.yaml new file mode 100644 index 0000000..a74cdd2 --- /dev/null +++ b/LCU.API.Internal.Document/azdo_pipelines/config-files/qa-pvars.yaml @@ -0,0 +1,7 @@ +--- +variables: + pvar_azdo_agent_pool: vmss-sub-df-lcu-apps-qa-lz-uscz + pvar_docker_registry_service_connection: Docker Registry LMCU + pvar_k8s_service_connection: svcon-sub-df-lcu-apps-qa-aks-k8s-uscz + pvar_kubectl_version: 1.25.9 + pvar_azdo_agent_pool_nuget: LCUOnPrem \ No newline at end of file diff --git a/LCU.API.Internal.Document/azdo_pipelines/config-files/uat-pvars.yaml b/LCU.API.Internal.Document/azdo_pipelines/config-files/uat-pvars.yaml new file mode 100644 index 0000000..10d7ce3 --- /dev/null +++ b/LCU.API.Internal.Document/azdo_pipelines/config-files/uat-pvars.yaml @@ -0,0 +1,7 @@ +--- +variables: + pvar_azdo_agent_pool: vmss-sub-df-lcu-apps-uat-lz-uscz + pvar_docker_registry_service_connection: Docker Registry LMCU + pvar_k8s_service_connection: svcon-sub-df-lcu-apps-uat-aks-k8s-uscz + pvar_kubectl_version: 1.25.9 + pvar_azdo_agent_pool_nuget: LCUOnPrem diff --git a/LCU.API.Internal.Document/azure-pipelines-iis.yml b/LCU.API.Internal.Document/azure-pipelines-iis.yml new file mode 100644 index 0000000..5ab7fb9 --- /dev/null +++ b/LCU.API.Internal.Document/azure-pipelines-iis.yml @@ -0,0 +1,35 @@ +name: 'LCU.API.Internal.Document.$(Build.SourceBranchName).$(Date:yyyyMMdd)$(Rev:.r)' +trigger: + branches: + include: + - '*' + +pool: + name: LCUOnPrem + demands: + - Agent.Name -equals D1UTL01Q + +variables: + buildConfiguration: 'Release' + +steps: + - task: DotNetCoreCLI@2 + inputs: + command: 'restore' + feedsToUse: 'select' + vstsFeed: '58ebbac4-8e98-4281-bb3f-58003852f4a7' + + - task: DotNetCoreCLI@2 + inputs: + command: 'build' + projects: '**/*.csproj' + arguments: '--configuration $(buildConfiguration)' + + - task: NuGetCommand@2 + condition: or(eq(variables['Build.SourceBranchName'], 'develop'), eq(variables['Build.SourceBranchName'], 'master')) + displayName: 'NuGet Push' + inputs: + command: 'push' + packagesToPush: '$(Pipeline.Workspace)/**/*.nupkg' + publishVstsFeed: '58ebbac4-8e98-4281-bb3f-58003852f4a7' + allowPackageConflicts: true \ No newline at end of file diff --git a/LCU.API.Internal.Document/azure-pipelines.yml b/LCU.API.Internal.Document/azure-pipelines.yml new file mode 100644 index 0000000..62a48d0 --- /dev/null +++ b/LCU.API.Internal.Document/azure-pipelines.yml @@ -0,0 +1,49 @@ +name: 'LCU.API.Internal.Document.$(Build.SourceBranchName).$(Date:yyyyMMdd)$(Rev:.r)' +trigger: + branches: + include: + - '*' + +pool: + name: LCUOnPrem + demands: + - Agent.Name -equals D1UTL01Q + +variables: + buildConfiguration: 'Release' + +steps: +- task: DotNetCoreCLI@2 + inputs: + command: 'restore' + feedsToUse: 'select' + vstsFeed: '58ebbac4-8e98-4281-bb3f-58003852f4a7' + +- task: DotNetCoreCLI@2 + inputs: + command: 'build' + projects: '**/*.csproj' + arguments: '--configuration $(buildConfiguration)' + +- task: DotNetCoreCLI@2 + displayName: Test + inputs: + command: test + projects: '**/*Test.csproj' + arguments: '--configuration $(BuildConfiguration) --collect "Code coverage"' + +- task: DotNetCoreCLI@2 + displayName: 'dotnet publish $(buildConfiguration)' + inputs: + command: 'publish' + publishWebProjects: false + projects: '**/*.csproj' + arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)' + zipAfterPublish: true + +- task: PublishBuildArtifacts@1 + condition: and (succeeded(), or( eq(variables['Build.SourceBranchName'], 'develop'), eq(variables['Build.SourceBranchName'], 'master')) ) + displayName: 'Publish Artifact: dist' + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)' + ArtifactName: 'dist' diff --git a/LCU.API.Internal.Document/kustomize/base/Deployment.yaml b/LCU.API.Internal.Document/kustomize/base/Deployment.yaml new file mode 100644 index 0000000..c8752e4 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/base/Deployment.yaml @@ -0,0 +1,107 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: document-api +spec: + replicas: 1 + selector: + matchLabels: + app: document-api + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + app: document-api + spec: + containers: + - name: document-api + image: document-api + resources: + limits: + memory: "1536Mi" + cpu: "500m" + ports: + - containerPort: 8080 + livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + httpGet: + path: /status + port: 8080 + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + httpGet: + path: /status + port: 8080 + volumeMounts: + - name: volume-lcu-internal-cert + mountPath: /etc/ssl/certs/lcu-internal-cert.pem + - name: volume-appsettings + mountPath: /app/config/mapped-appsettings + - name: document-api-secrets + mountPath: /vault/secrets/document-api-type-cleanup-api-key.json + subPath: document-api-type-cleanup-api-key + readOnly: true + - name: document-api-secrets + mountPath: /vault/secrets/document-api-type-retriever-api-key.json + subPath: document-api-type-retriever-api-key + readOnly: true + - name: document-api-secrets + mountPath: /vault/secrets/document-api-type-uploader-api-key.json + subPath: document-api-type-uploader-api-key + readOnly: true + - name: document-api-secrets + mountPath: /vault/secrets/document-api-mongo-database-connection-string.json + subPath: document-api-mongo-database-connection-string + readOnly: true + - name: document-api-secrets + mountPath: /vault/secrets/document-api-blob-storage-connection-string.json + subPath: document-api-blob-storage-connection-string + readOnly: true + - name: document-api-secrets + mountPath: /vault/secrets/document-api-seq-api-key.json + subPath: document-api-seq-api-key + readOnly: true + - name: document-api-secrets + mountPath: /vault/secrets/document-api-swagger-oauth-scope.json + subPath: document-api-swagger-oauth-scope + readOnly: true + - name: document-api-secrets + mountPath: /vault/secrets/document-api-tenant-id.json + subPath: document-api-tenant-id + readOnly: true + - name: document-api-secrets + mountPath: /vault/secrets/document-api-client-secret.json + subPath: document-api-client-secret + readOnly: true + - name: document-api-secrets + mountPath: /vault/secrets/document-api-client-id.json + subPath: document-api-client-id + readOnly: true + - name: document-api-secrets + mountPath: /vault/secrets/document-api-azure-tenant-id.json + subPath: document-api-azure-tenant-id + readOnly: true + - name: document-api-secrets + mountPath: /vault/secrets/document-api-azure-client-id.json + subPath: document-api-azure-client-id + readOnly: true + volumes: + - name: document-api-secrets + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: document-api-csi-key-vault-secrets-provider + - name: volume-lcu-internal-cert + configMap: + name: configmap-lcu-internal-cert + - name: volume-appsettings + configMap: + name: documentapi-configmap diff --git a/LCU.API.Internal.Document/kustomize/base/Service.yaml b/LCU.API.Internal.Document/kustomize/base/Service.yaml new file mode 100644 index 0000000..2e387e6 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/base/Service.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: document-api +spec: + selector: + app: document-api + ports: + - port: 8080 + targetPort: 8080 diff --git a/LCU.API.Internal.Document/kustomize/base/kustomization.yaml b/LCU.API.Internal.Document/kustomize/base/kustomization.yaml new file mode 100644 index 0000000..9bde637 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/base/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - Deployment.yaml + - Service.yaml + - virtual-service.yaml + - secrets-provider-class.yaml diff --git a/LCU.API.Internal.Document/kustomize/base/secrets-provider-class.yaml b/LCU.API.Internal.Document/kustomize/base/secrets-provider-class.yaml new file mode 100644 index 0000000..f630e48 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/base/secrets-provider-class.yaml @@ -0,0 +1,61 @@ +--- +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: document-api-csi-key-vault-secrets-provider +spec: + provider: azure + parameters: + usePodIdentity: "false" + useVMManagedIdentity: "true" + tenantId: 63cd1554-2783-4a86-ae50-b9e6c21c9499 + objects: | + array: + - | + objectName: document-api-azure-client-id + objectType: secret + objectVersion: "" + - | + objectName: document-api-azure-tenant-id + objectType: secret + objectVersion: "" + - | + objectName: document-api-client-id + objectType: secret + objectVersion: "" + - | + objectName: document-api-client-secret + objectType: secret + objectVersion: "" + - | + objectName: document-api-tenant-id + objectType: secret + objectVersion: "" + - | + objectName: document-api-swagger-oauth-scope + objectType: secret + objectVersion: "" + - | + objectName: document-api-seq-api-key + objectType: secret + objectVersion: "" + - | + objectName: document-api-blob-storage-connection-string + objectType: secret + objectVersion: "" + - | + objectName: document-api-mongo-database-connection-string + objectType: secret + objectVersion: "" + - | + objectName: document-api-type-uploader-api-key + objectType: secret + objectVersion: "" + - | + objectName: document-api-type-retriever-api-key + objectType: secret + objectVersion: "" + - | + objectName: document-api-type-cleanup-api-key + objectType: secret + objectVersion: "" diff --git a/LCU.API.Internal.Document/kustomize/base/virtual-service.yaml b/LCU.API.Internal.Document/kustomize/base/virtual-service.yaml new file mode 100644 index 0000000..e854071 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/base/virtual-service.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: document-api +spec: + gateways: + - istio-system/ingress-gateway + http: + - name: document-api + match: + - uri: + prefix: / + route: + - destination: + host: document-api + port: + number: 8080 diff --git a/LCU.API.Internal.Document/kustomize/overlays/local/common/kustomization.yaml b/LCU.API.Internal.Document/kustomize/overlays/local/common/kustomization.yaml new file mode 100644 index 0000000..f368b07 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/local/common/kustomization.yaml @@ -0,0 +1,8 @@ +resources: + - ../../../base +patchesStrategicMerge: + - set_appsettings_deployment.yaml + - set_secrets_provider_class.yaml +images: +- name: document-api + newName: landmarkcu/lcu.api.internal.document-document diff --git a/LCU.API.Internal.Document/kustomize/overlays/local/common/set_appsettings_deployment.yaml b/LCU.API.Internal.Document/kustomize/overlays/local/common/set_appsettings_deployment.yaml new file mode 100644 index 0000000..8ae8e52 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/local/common/set_appsettings_deployment.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: document-api +spec: + template: + spec: + containers: + - name: document-api + env: + - name: ASPNETCORE_ENVIRONMENT + value: "Development" diff --git a/LCU.API.Internal.Document/kustomize/overlays/local/common/set_secrets_provider_class.yaml b/LCU.API.Internal.Document/kustomize/overlays/local/common/set_secrets_provider_class.yaml new file mode 100644 index 0000000..229a93a --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/local/common/set_secrets_provider_class.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: document-api-csi-key-vault-secrets-provider +spec: + parameters: + userAssignedIdentityID: 3286ed9a-f85c-4c50-b742-dbd3c1c3fe1f + keyvaultName: kv-lz-apps-qa-uscz diff --git a/LCU.API.Internal.Document/kustomize/overlays/local/kustomization.yaml b/LCU.API.Internal.Document/kustomize/overlays/local/kustomization.yaml new file mode 100644 index 0000000..25a65b6 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/local/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - normal diff --git a/LCU.API.Internal.Document/kustomize/overlays/local/normal/configmap.yaml b/LCU.API.Internal.Document/kustomize/overlays/local/normal/configmap.yaml new file mode 100644 index 0000000..753bf3f --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/local/normal/configmap.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: documentapi-configmap +data: + primary.json: | + { + "AzureKeyVaultUrl": "qa-DocumentApi-kv", + "Primary": { + "SeqUrl": "https://qa-seq.lcu-internal.com", + "IsAksDeploy": true, + "CorsAllowedOrigins": [ + "http://localhost:3000", + "http://lcu-internal.com", + "https://lcu-internal.com", + "https://qa-api.lcu-internal.com" + ] + }, + "NotificationApiConfiguration": { + "Url": "https://qa-notificationsapi.lcu-internal.com", + }, + "FeatureManagement": { + "GetDocumentCheckOwnership": true + } + } diff --git a/LCU.API.Internal.Document/kustomize/overlays/local/normal/kustomization.yaml b/LCU.API.Internal.Document/kustomize/overlays/local/normal/kustomization.yaml new file mode 100644 index 0000000..6dd0c3b --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/local/normal/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - configmap.yaml + - ../common +patchesStrategicMerge: + - set_virtual_service.yaml diff --git a/LCU.API.Internal.Document/kustomize/overlays/local/normal/set_virtual_service.yaml b/LCU.API.Internal.Document/kustomize/overlays/local/normal/set_virtual_service.yaml new file mode 100644 index 0000000..99aae52 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/local/normal/set_virtual_service.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: document-api +spec: + hosts: + - qa-document-api.apps.azure.lcu-internal.com diff --git a/LCU.API.Internal.Document/kustomize/overlays/prd/common/kustomization.yaml b/LCU.API.Internal.Document/kustomize/overlays/prd/common/kustomization.yaml new file mode 100644 index 0000000..f368b07 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/prd/common/kustomization.yaml @@ -0,0 +1,8 @@ +resources: + - ../../../base +patchesStrategicMerge: + - set_appsettings_deployment.yaml + - set_secrets_provider_class.yaml +images: +- name: document-api + newName: landmarkcu/lcu.api.internal.document-document diff --git a/LCU.API.Internal.Document/kustomize/overlays/prd/common/set_appsettings_deployment.yaml b/LCU.API.Internal.Document/kustomize/overlays/prd/common/set_appsettings_deployment.yaml new file mode 100644 index 0000000..8682fc7 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/prd/common/set_appsettings_deployment.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: document-api +spec: + template: + spec: + containers: + - name: document-api + env: + - name: ASPNETCORE_ENVIRONMENT + value: "Production" diff --git a/LCU.API.Internal.Document/kustomize/overlays/prd/common/set_secrets_provider_class.yaml b/LCU.API.Internal.Document/kustomize/overlays/prd/common/set_secrets_provider_class.yaml new file mode 100644 index 0000000..4ff2f89 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/prd/common/set_secrets_provider_class.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: document-api-csi-key-vault-secrets-provider +spec: + parameters: + userAssignedIdentityID: 9eaefc49-e5d3-48d4-a2b9-b42cf2d8ca03 + keyvaultName: kv-lz-apps-prod-uscz diff --git a/LCU.API.Internal.Document/kustomize/overlays/prd/kustomization.yaml b/LCU.API.Internal.Document/kustomize/overlays/prd/kustomization.yaml new file mode 100644 index 0000000..25a65b6 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/prd/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - normal diff --git a/LCU.API.Internal.Document/kustomize/overlays/prd/normal/configmap.yaml b/LCU.API.Internal.Document/kustomize/overlays/prd/normal/configmap.yaml new file mode 100644 index 0000000..42dba44 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/prd/normal/configmap.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: documentapi-configmap +data: + primary.json: | + { + "AzureKeyVaultUrl": "prd-DocumentApi-kv", + "Primary": { + "SeqUrl": "https://seq.lcu-internal.com", + "IsAksDeploy": true, + "CorsAllowedOrigins": [ + "http://lcu-internal.com", + "https://lcu-internal.com", + "https://api.lcu-internal.com" + ] + }, + "NotificationApiConfiguration": { + "Url": "https://prd-notifications-api.apps.azure.lcu-internal.com", + }, + "FeatureManagement": { + "GetDocumentCheckOwnership": true + } + } diff --git a/LCU.API.Internal.Document/kustomize/overlays/prd/normal/kustomization.yaml b/LCU.API.Internal.Document/kustomize/overlays/prd/normal/kustomization.yaml new file mode 100644 index 0000000..6dd0c3b --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/prd/normal/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - configmap.yaml + - ../common +patchesStrategicMerge: + - set_virtual_service.yaml diff --git a/LCU.API.Internal.Document/kustomize/overlays/prd/normal/set_virtual_service.yaml b/LCU.API.Internal.Document/kustomize/overlays/prd/normal/set_virtual_service.yaml new file mode 100644 index 0000000..9df1717 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/prd/normal/set_virtual_service.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: document-api +spec: + hosts: + - prd-document-api.apps.azure.lcu-internal.com diff --git a/LCU.API.Internal.Document/kustomize/overlays/qa/common/kustomization.yaml b/LCU.API.Internal.Document/kustomize/overlays/qa/common/kustomization.yaml new file mode 100644 index 0000000..f368b07 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/qa/common/kustomization.yaml @@ -0,0 +1,8 @@ +resources: + - ../../../base +patchesStrategicMerge: + - set_appsettings_deployment.yaml + - set_secrets_provider_class.yaml +images: +- name: document-api + newName: landmarkcu/lcu.api.internal.document-document diff --git a/LCU.API.Internal.Document/kustomize/overlays/qa/common/set_appsettings_deployment.yaml b/LCU.API.Internal.Document/kustomize/overlays/qa/common/set_appsettings_deployment.yaml new file mode 100644 index 0000000..8ae8e52 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/qa/common/set_appsettings_deployment.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: document-api +spec: + template: + spec: + containers: + - name: document-api + env: + - name: ASPNETCORE_ENVIRONMENT + value: "Development" diff --git a/LCU.API.Internal.Document/kustomize/overlays/qa/common/set_secrets_provider_class.yaml b/LCU.API.Internal.Document/kustomize/overlays/qa/common/set_secrets_provider_class.yaml new file mode 100644 index 0000000..229a93a --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/qa/common/set_secrets_provider_class.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: document-api-csi-key-vault-secrets-provider +spec: + parameters: + userAssignedIdentityID: 3286ed9a-f85c-4c50-b742-dbd3c1c3fe1f + keyvaultName: kv-lz-apps-qa-uscz diff --git a/LCU.API.Internal.Document/kustomize/overlays/qa/kustomization.yaml b/LCU.API.Internal.Document/kustomize/overlays/qa/kustomization.yaml new file mode 100644 index 0000000..25a65b6 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/qa/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - normal diff --git a/LCU.API.Internal.Document/kustomize/overlays/qa/normal/configmap.yaml b/LCU.API.Internal.Document/kustomize/overlays/qa/normal/configmap.yaml new file mode 100644 index 0000000..233b783 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/qa/normal/configmap.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: documentapi-configmap +data: + primary.json: | + { + "AzureKeyVaultUrl": "qa-DocumentApi-kv", + "Primary": { + "SeqUrl": "https://qa-seq.lcu-internal.com", + "IsAksDeploy": true, + "CorsAllowedOrigins": [ + "http://localhost:3000", + "http://lcu-internal.com", + "https://lcu-internal.com", + "https://qa-api.lcu-internal.com" + ] + }, + "NotificationApiConfiguration": { + "Url": "https://qa-notifications-api.apps.azure.lcu-internal.com", + }, + "FeatureManagement": { + "GetDocumentCheckOwnership": true + } + } diff --git a/LCU.API.Internal.Document/kustomize/overlays/qa/normal/kustomization.yaml b/LCU.API.Internal.Document/kustomize/overlays/qa/normal/kustomization.yaml new file mode 100644 index 0000000..6dd0c3b --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/qa/normal/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - configmap.yaml + - ../common +patchesStrategicMerge: + - set_virtual_service.yaml diff --git a/LCU.API.Internal.Document/kustomize/overlays/qa/normal/set_virtual_service.yaml b/LCU.API.Internal.Document/kustomize/overlays/qa/normal/set_virtual_service.yaml new file mode 100644 index 0000000..99aae52 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/qa/normal/set_virtual_service.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: document-api +spec: + hosts: + - qa-document-api.apps.azure.lcu-internal.com diff --git a/LCU.API.Internal.Document/kustomize/overlays/uat/common/kustomization.yaml b/LCU.API.Internal.Document/kustomize/overlays/uat/common/kustomization.yaml new file mode 100644 index 0000000..f368b07 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/uat/common/kustomization.yaml @@ -0,0 +1,8 @@ +resources: + - ../../../base +patchesStrategicMerge: + - set_appsettings_deployment.yaml + - set_secrets_provider_class.yaml +images: +- name: document-api + newName: landmarkcu/lcu.api.internal.document-document diff --git a/LCU.API.Internal.Document/kustomize/overlays/uat/common/set_appsettings_deployment.yaml b/LCU.API.Internal.Document/kustomize/overlays/uat/common/set_appsettings_deployment.yaml new file mode 100644 index 0000000..c0d11d1 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/uat/common/set_appsettings_deployment.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: document-api +spec: + template: + spec: + containers: + - name: document-api + env: + - name: ASPNETCORE_ENVIRONMENT + value: "Staging" diff --git a/LCU.API.Internal.Document/kustomize/overlays/uat/common/set_secrets_provider_class.yaml b/LCU.API.Internal.Document/kustomize/overlays/uat/common/set_secrets_provider_class.yaml new file mode 100644 index 0000000..3fed6bc --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/uat/common/set_secrets_provider_class.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: document-api-csi-key-vault-secrets-provider +spec: + parameters: + userAssignedIdentityID: 64ba01ed-1ecc-4c9b-b56e-8d618f2b81ce + keyvaultName: kv-lz-apps-uat-uscz diff --git a/LCU.API.Internal.Document/kustomize/overlays/uat/kustomization.yaml b/LCU.API.Internal.Document/kustomize/overlays/uat/kustomization.yaml new file mode 100644 index 0000000..25a65b6 --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/uat/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - normal diff --git a/LCU.API.Internal.Document/kustomize/overlays/uat/normal/configmap.yaml b/LCU.API.Internal.Document/kustomize/overlays/uat/normal/configmap.yaml new file mode 100644 index 0000000..688545d --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/uat/normal/configmap.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: documentapi-configmap +data: + primary.json: | + { + "AzureKeyVaultUrl": "uat-DocumentApi-kv", + "Primary": { + "SeqUrl": "https://uat-seq.lcu-internal.com", + "IsAksDeploy": true, + "CorsAllowedOrigins": [ + "http://localhost:3000", + "http://lcu-internal.com", + "https://lcu-internal.com", + "https://uat-api.lcu-internal.com" + ] + }, + "NotificationApiConfiguration": { + "Url": "https://uat-notifications-api.apps.azure.lcu-internal.com", + }, + "FeatureManagement": { + "GetDocumentCheckOwnership": true + } + } diff --git a/LCU.API.Internal.Document/kustomize/overlays/uat/normal/kustomization.yaml b/LCU.API.Internal.Document/kustomize/overlays/uat/normal/kustomization.yaml new file mode 100644 index 0000000..6dd0c3b --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/uat/normal/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - configmap.yaml + - ../common +patchesStrategicMerge: + - set_virtual_service.yaml diff --git a/LCU.API.Internal.Document/kustomize/overlays/uat/normal/set_virtual_service.yaml b/LCU.API.Internal.Document/kustomize/overlays/uat/normal/set_virtual_service.yaml new file mode 100644 index 0000000..935ae9f --- /dev/null +++ b/LCU.API.Internal.Document/kustomize/overlays/uat/normal/set_virtual_service.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: document-api +spec: + hosts: + - uat-document-api.apps.azure.lcu-internal.com