diff --git a/src/Api/ExportRequestDataMessage.cs b/src/Api/ExportRequestDataMessage.cs old mode 100644 new mode 100755 index 2c87e729f..6246f93db --- a/src/Api/ExportRequestDataMessage.cs +++ b/src/Api/ExportRequestDataMessage.cs @@ -47,6 +47,11 @@ public string ExportTaskId get { return _exportRequest.ExportTaskId; } } + public string WorkflowInstanceId + { + get { return _exportRequest.WorkflowInstanceId; } + } + public string CorrelationId { get { return _exportRequest.CorrelationId; } diff --git a/src/Api/Storage/RemoteAppExecution.cs b/src/Api/Storage/RemoteAppExecution.cs new file mode 100755 index 000000000..79d9542b3 --- /dev/null +++ b/src/Api/Storage/RemoteAppExecution.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using FellowOakDicom; +using Monai.Deploy.Messaging.Events; + +namespace Monai.Deploy.InformaticsGateway.Api.Storage +{ + public class RemoteAppExecution + { + public DateTime RequestTime { get; set; } = DateTime.UtcNow; + public string ExportTaskId { get; set; } = string.Empty; + public string WorkflowInstanceId { get; set; } = string.Empty; + public string CorrelationId { get; set; } = string.Empty; + public string? StudyUid { get; set; } + public string? OutgoingStudyUid { get; set; } + public List ExportDetails { get; set; } = new(); + public List Files { get; set; } = new(); + public FileExportStatus Status { get; set; } + public Dictionary OriginalValues { get; set; } = new(); + } +} diff --git a/src/Client/Test/packages.lock.json b/src/Client/Test/packages.lock.json old mode 100644 new mode 100755 diff --git a/src/Database/Api/Repositories/IRemoteAppExecutionRepository.cs b/src/Database/Api/Repositories/IRemoteAppExecutionRepository.cs new file mode 100755 index 000000000..c6897a56c --- /dev/null +++ b/src/Database/Api/Repositories/IRemoteAppExecutionRepository.cs @@ -0,0 +1,14 @@ + +using Monai.Deploy.InformaticsGateway.Api.Storage; + +namespace Monai.Deploy.InformaticsGateway.Database.Api.Repositories +{ + public interface IRemoteAppExecutionRepository + { + Task AddAsync(RemoteAppExecution item, CancellationToken cancellationToken = default); + + Task GetAsync(string OutgoingStudyUid, CancellationToken cancellationToken = default); + + Task RemoveAsync(string OutgoingStudyUid, CancellationToken cancellationToken = default); + } +} diff --git a/src/Database/DatabaseManager.cs b/src/Database/DatabaseManager.cs old mode 100644 new mode 100755 index 05191d1b2..4a297f588 --- a/src/Database/DatabaseManager.cs +++ b/src/Database/DatabaseManager.cs @@ -78,6 +78,7 @@ public static IServiceCollection ConfigureDatabase(this IServiceCollection servi services.AddScoped(typeof(IStorageMetadataRepository), typeof(EntityFramework.Repositories.StorageMetadataWrapperRepository)); services.AddScoped(typeof(IPayloadRepository), typeof(EntityFramework.Repositories.PayloadRepository)); services.AddScoped(typeof(IDicomAssociationInfoRepository), typeof(EntityFramework.Repositories.DicomAssociationInfoRepository)); + services.AddScoped(typeof(IRemoteAppExecutionRepository), typeof(EntityFramework.Repositories.RemoteAppExecutionRepository)); return services; case DbType_MongoDb: @@ -91,6 +92,7 @@ public static IServiceCollection ConfigureDatabase(this IServiceCollection servi services.AddScoped(typeof(IStorageMetadataRepository), typeof(MongoDB.Repositories.StorageMetadataWrapperRepository)); services.AddScoped(typeof(IPayloadRepository), typeof(MongoDB.Repositories.PayloadRepository)); services.AddScoped(typeof(IDicomAssociationInfoRepository), typeof(MongoDB.Repositories.DicomAssociationInfoRepository)); + services.AddScoped(typeof(IRemoteAppExecutionRepository), typeof(MongoDB.Repositories.RemoteAppExecutionRepository)); return services; diff --git a/src/Database/EntityFramework/Repositories/RemoteAppExecutionRepository.cs b/src/Database/EntityFramework/Repositories/RemoteAppExecutionRepository.cs new file mode 100755 index 000000000..b7b9aaa78 --- /dev/null +++ b/src/Database/EntityFramework/Repositories/RemoteAppExecutionRepository.cs @@ -0,0 +1,124 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ardalis.GuardClauses; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Logging; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Polly; +using Polly.Retry; + +namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Repositories +{ + public class RemoteAppExecutionRepository : IRemoteAppExecutionRepository, IDisposable + { + private readonly ILogger _logger; + private readonly IServiceScope _scope; + private readonly InformaticsGatewayContext _informaticsGatewayContext; + private readonly AsyncRetryPolicy _retryPolicy; + private readonly DbSet _dataset; + private bool _disposedValue; + + public RemoteAppExecutionRepository( + IServiceScopeFactory serviceScopeFactory, + ILogger logger, + IOptions options) + { + Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); + Guard.Against.Null(options, nameof(options)); + + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _scope = serviceScopeFactory.CreateScope(); + _informaticsGatewayContext = _scope.ServiceProvider.GetRequiredService(); + _retryPolicy = Policy.Handle().WaitAndRetryAsync( + options.Value.Database.Retries.RetryDelays, + (exception, timespan, count, context) => _logger.DatabaseErrorRetry(timespan, count, exception)); + _dataset = _informaticsGatewayContext.Set(); + } + + public async Task AddAsync(RemoteAppExecution item, CancellationToken cancellationToken = default) + { + Guard.Against.Null(item, nameof(item)); + + return await _retryPolicy.ExecuteAsync(async () => + { + var result = await _dataset.AddAsync(item, cancellationToken).ConfigureAwait(false); + await _informaticsGatewayContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + return true; + }).ConfigureAwait(false); + } + + public async Task RemoveAsync(string OriginalStudyUid, CancellationToken cancellationToken = default) + { + Guard.Against.Null(OriginalStudyUid, nameof(OriginalStudyUid)); + + return await _retryPolicy.ExecuteAsync(async () => + { + var result = await _dataset.SingleOrDefaultAsync(p => p.OutgoingStudyUid == OriginalStudyUid).ConfigureAwait(false); + if (result is not null) + { + _dataset.Remove(result); + await _informaticsGatewayContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + return 1; + } + return 0; + }).ConfigureAwait(false); + } + + public async Task GetAsync(string OutgoingStudyUid, CancellationToken cancellationToken = default) + { + Guard.Against.Null(OutgoingStudyUid, nameof(OutgoingStudyUid)); + + return await _retryPolicy.ExecuteAsync(async () => + { + var result = await _dataset.SingleOrDefaultAsync(p => p.OutgoingStudyUid == OutgoingStudyUid).ConfigureAwait(false); + if (result is not null) + { + return result; + } + return default; + }).ConfigureAwait(false); + } + + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _informaticsGatewayContext.Dispose(); + _scope.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Database/MongoDB/Repositories/RemoteAppExecutionRepository.cs b/src/Database/MongoDB/Repositories/RemoteAppExecutionRepository.cs new file mode 100755 index 000000000..8fbf51348 --- /dev/null +++ b/src/Database/MongoDB/Repositories/RemoteAppExecutionRepository.cs @@ -0,0 +1,104 @@ +using Ardalis.GuardClauses; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Logging; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations; +using MongoDB.Driver; +using Polly; +using Polly.Retry; + +namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Repositories +{ + public class RemoteAppExecutionRepository : IRemoteAppExecutionRepository, IDisposable + { + private readonly ILogger _logger; + private readonly IServiceScope _scope; + private readonly AsyncRetryPolicy _retryPolicy; + private readonly IMongoCollection _collection; + private bool _disposedValue; + + public RemoteAppExecutionRepository(IServiceScopeFactory serviceScopeFactory, + ILogger logger, + IOptions options, + IOptions mongoDbOptions) + { + Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); + Guard.Against.Null(options, nameof(options)); + Guard.Against.Null(mongoDbOptions, nameof(mongoDbOptions)); + + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _scope = serviceScopeFactory.CreateScope(); + _retryPolicy = Policy.Handle().WaitAndRetryAsync( + options.Value.Database.Retries.RetryDelays, + (exception, timespan, count, context) => _logger.DatabaseErrorRetry(timespan, count, exception)); + + var mongoDbClient = _scope.ServiceProvider.GetRequiredService(); + var mongoDatabase = mongoDbClient.GetDatabase(mongoDbOptions.Value.DaatabaseName); + _collection = mongoDatabase.GetCollection(nameof(RemoteAppExecution)); + CreateIndexes(); + } + + private void CreateIndexes() + { + var options = new CreateIndexOptions { Unique = true, ExpireAfter = TimeSpan.FromDays(7), Name = "RequestTime" }; + var indexDefinitionState = Builders.IndexKeys.Ascending(_ => _.OutgoingStudyUid); + var indexModel = new CreateIndexModel(indexDefinitionState, options); + + _collection.Indexes.CreateOne(indexModel); + } + + public async Task AddAsync(RemoteAppExecution item, CancellationToken cancellationToken = default) + { + Guard.Against.Null(item, nameof(item)); + + return await _retryPolicy.ExecuteAsync(async () => + { + await _collection.InsertOneAsync(item, cancellationToken: cancellationToken).ConfigureAwait(false); + return true; + }).ConfigureAwait(false); + } + + public async Task RemoveAsync(string OutgoingStudyUid, CancellationToken cancellationToken = default) + { + return await _retryPolicy.ExecuteAsync(async () => + { + var results = await _collection.DeleteManyAsync(Builders.Filter.Where(p => p.OutgoingStudyUid == OutgoingStudyUid), cancellationToken).ConfigureAwait(false); + return Convert.ToInt32(results.DeletedCount); + }).ConfigureAwait(false); + } + + public async Task GetAsync(string OutgoingStudyUid, CancellationToken cancellationToken = default) + { + return await _retryPolicy.ExecuteAsync(async () => + { + return await _collection.Find(p => p.OutgoingStudyUid == OutgoingStudyUid).FirstOrDefaultAsync().ConfigureAwait(false); + }).ConfigureAwait(false); + } + + + public void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _scope.Dispose(); + } + + _disposedValue = true; + } + } + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + } +} diff --git a/src/InformaticsGateway/ExecutionPlugins/ExternalAppIncoming.cs b/src/InformaticsGateway/ExecutionPlugins/ExternalAppIncoming.cs new file mode 100755 index 000000000..f202a1aac --- /dev/null +++ b/src/InformaticsGateway/ExecutionPlugins/ExternalAppIncoming.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using FellowOakDicom; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; + +namespace Monai.Deploy.InformaticsGateway.ExecutionPlugins +{ + public class ExternalAppIncoming : IInputDataPlugin + { + private readonly ILogger _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; + + public ExternalAppIncoming( + ILogger logger, + IServiceScopeFactory serviceScopeFactory) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + } + + public async Task<(DicomFile dicomFile, FileStorageMetadata fileMetadata)> Execute(DicomFile dicomFile, FileStorageMetadata fileMetadata) + { + var scope = _serviceScopeFactory.CreateScope(); + var repository = scope.ServiceProvider.GetRequiredService(); + + var incommingStudyUid = dicomFile.Dataset.GetString(DicomTag.StudyInstanceUID); + var remoteAppExecution = await repository.GetAsync(incommingStudyUid); + if (remoteAppExecution is null) + { + _logger.LogOriginalStudyUidNotFound(incommingStudyUid); + return (dicomFile, fileMetadata); + } + foreach (var key in remoteAppExecution.OriginalValues.Keys) + { + dicomFile.Dataset.AddOrUpdate(key, remoteAppExecution.OriginalValues[key]); + } + dicomFile.Dataset.AddOrUpdate(DicomTag.StudyInstanceUID, remoteAppExecution.StudyUid); + fileMetadata.WorkflowInstanceId = remoteAppExecution.WorkflowInstanceId; + fileMetadata.TaskId = remoteAppExecution.ExportTaskId; + + return (dicomFile, fileMetadata); + } + } +} diff --git a/src/InformaticsGateway/ExecutionPlugins/ExternalAppOutgoing.cs b/src/InformaticsGateway/ExecutionPlugins/ExternalAppOutgoing.cs new file mode 100755 index 000000000..bef3d57b5 --- /dev/null +++ b/src/InformaticsGateway/ExecutionPlugins/ExternalAppOutgoing.cs @@ -0,0 +1,98 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FellowOakDicom; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; + +namespace Monai.Deploy.InformaticsGateway.ExecutionPlugins +{ + public class ExternalAppOutgoing : IOutputDataPlugin + { + private readonly ILogger _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; + + public ExternalAppOutgoing( + ILogger logger, + IServiceScopeFactory serviceScopeFactory) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + } + + public async Task<(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage)> Execute(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage) + { + //these are the standard tags, but this needs moving into config. + DicomTag[] tags = { DicomTag.StudyInstanceUID, DicomTag.AccessionNumber, DicomTag.SeriesInstanceUID, DicomTag.SOPInstanceUID }; + + var scope = _serviceScopeFactory.CreateScope(); + var repository = scope.ServiceProvider.GetRequiredService(); + + var remoteAppExecution = await GetRemoteAppExecution(exportRequestDataMessage, tags).ConfigureAwait(false); + remoteAppExecution.StudyUid = dicomFile.Dataset.GetString(DicomTag.StudyInstanceUID); + + await repository.AddAsync(remoteAppExecution).ConfigureAwait(false); + _logger.LogStudyUidChanged(remoteAppExecution.StudyUid, remoteAppExecution.OutgoingStudyUid); + + foreach (var tag in tags) + { + if (tag.Equals(DicomTag.StudyInstanceUID) is false) + { + remoteAppExecution.OriginalValues.Add(tag, dicomFile.Dataset.GetString(tag)); + dicomFile.Dataset.AddOrUpdate(tag, DicomUIDGenerator.GenerateDerivedFromUUID()); + } + } + + dicomFile.Dataset.AddOrUpdate(DicomTag.StudyInstanceUID, remoteAppExecution.OutgoingStudyUid); + + return (dicomFile, exportRequestDataMessage); + } + + private async Task GetRemoteAppExecution(ExportRequestDataMessage request, DicomTag[] tags) + { + var remoteAppExecution = new RemoteAppExecution + { + CorrelationId = request.CorrelationId, + WorkflowInstanceId = request.WorkflowInstanceId, + ExportTaskId = request.ExportTaskId, + Files = new System.Collections.Generic.List { request.Filename }, + Status = request.ExportStatus + }; + + + var outgoingStudyUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + remoteAppExecution.OutgoingStudyUid = outgoingStudyUid; + + + foreach (var destination in request.Destinations) + { + remoteAppExecution.ExportDetails.Add(await LookupDestinationAsync(destination, new CancellationToken())); + } + + return remoteAppExecution; + } + + private async Task LookupDestinationAsync(string destinationName, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(destinationName)) + { + throw new ConfigurationException("Export task does not have destination set."); + } + + using var scope = _serviceScopeFactory.CreateScope(); + var repository = scope.ServiceProvider.GetRequiredService(); + var destination = await repository.FindByNameAsync(destinationName, cancellationToken).ConfigureAwait(false); + + if (destination is null) + { + throw new ConfigurationException($"Specified destination '{destinationName}' does not exist."); + } + + return destination; + } + } +} diff --git a/src/InformaticsGateway/ExecutionPlugins/Log.1000.cs b/src/InformaticsGateway/ExecutionPlugins/Log.1000.cs new file mode 100755 index 000000000..b9174d653 --- /dev/null +++ b/src/InformaticsGateway/ExecutionPlugins/Log.1000.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Logging; + +namespace Monai.Deploy.InformaticsGateway.ExecutionPlugins +{ + public static partial class Log + { + [LoggerMessage(EventId = 1000, Level = LogLevel.Debug, Message = "Changed the StudyUid from {OriginalStudyUid} to {NewStudyUid}")] + public static partial void LogStudyUidChanged(this ILogger logger, string OriginalStudyUid, string NewStudyUid); + + [LoggerMessage(EventId = 1001, Level = LogLevel.Error, Message = "Cannot find entry for OriginalStudyUid {OriginalStudyUid} ")] + public static partial void LogOriginalStudyUidNotFound(this ILogger logger, string OriginalStudyUid); + } +} diff --git a/src/InformaticsGateway/Monai - Backup.Deploy.InformaticsGateway.csproj b/src/InformaticsGateway/Monai - Backup.Deploy.InformaticsGateway.csproj new file mode 100644 index 000000000..60a16cbd8 --- /dev/null +++ b/src/InformaticsGateway/Monai - Backup.Deploy.InformaticsGateway.csproj @@ -0,0 +1,114 @@ + + + + + + Monai.Deploy.InformaticsGateway + Exe + net6.0 + Apache-2.0 + true + True + latest + ..\.sonarlint\project-monai_monai-deploy-informatics-gatewaycsharp.ruleset + true + be0fffc8-bebb-4509-a2c0-3c981e5415ab + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_Parameter1>$(AssemblyName).Test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj b/src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj old mode 100644 new mode 100755 diff --git a/src/InformaticsGateway/Program.cs b/src/InformaticsGateway/Program.cs old mode 100644 new mode 100755 diff --git a/src/InformaticsGateway/Test/Services/Common/ExternalAppPluginTest.cs b/src/InformaticsGateway/Test/Services/Common/ExternalAppPluginTest.cs new file mode 100755 index 000000000..2c9f00cd4 --- /dev/null +++ b/src/InformaticsGateway/Test/Services/Common/ExternalAppPluginTest.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using FellowOakDicom; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.ExecutionPlugins; +using Monai.Deploy.InformaticsGateway.Services.Common; +using Moq; +using Xunit; +using System.Threading.Tasks; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using System.Threading; + +namespace Monai.Deploy.InformaticsGateway.Test.Services.Common +{ + public class ExternalAppPluginTest + { + private readonly Mock> _logger; + private readonly Mock> _loggerOut; + private readonly Mock _serviceScopeFactory; + private readonly Mock _serviceScope; + private readonly Mock _repository; + private readonly Mock _destRepo; + private readonly ServiceProvider _serviceProvider; + + public ExternalAppPluginTest() + { + _logger = new Mock>(); + _loggerOut = new Mock>(); + _serviceScopeFactory = new Mock(); + _serviceScope = new Mock(); + _repository = new Mock(); + _destRepo = new Mock(); + _destRepo.Setup(d => d.FindByNameAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new DestinationApplicationEntity())); + + var services = new ServiceCollection(); + services.AddScoped(p => _logger.Object); + services.AddScoped(p => _loggerOut.Object); + services.AddScoped(p => _repository.Object); + services.AddScoped(p => _destRepo.Object); + + _serviceProvider = services.BuildServiceProvider(); + + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public void GivenAnExternalAppIncoming_WhenInitialized_ExpectParametersToBeValidated() + { + Assert.Throws(() => new ExternalAppIncoming(null, null)); + Assert.Throws(() => new ExternalAppIncoming(null, _serviceScopeFactory.Object)); + + _ = new ExternalAppIncoming(_logger.Object, _serviceScopeFactory.Object); + } + + [Fact] + public void GivenAnExternalAppOutgoing_WhenInitialized_ExpectParametersToBeValidated() + { + Assert.Throws(() => new ExternalAppOutgoing(null, null)); + Assert.Throws(() => new ExternalAppOutgoing(null, _serviceScopeFactory.Object)); + + _ = new ExternalAppOutgoing(_loggerOut.Object, _serviceScopeFactory.Object); + } + + [Fact] + public void GivenAnOutputDataPluginEngine_WhenConfigureIsCalledWithAValidAssembly_ExpectNoExceptions() + { + var pluginEngine = new OutputDataPluginEngine( + _serviceProvider, + new Mock>().Object, + new Mock().Object); + + var assemblies = new List() { + typeof(ExternalAppOutgoing).AssemblyQualifiedName}; + + pluginEngine.Configure(assemblies); + } + + [Fact] + public void GivenAnInputDataPluginEngine_WhenConfigureIsCalledWithAValidAssembly_ExpectNoExceptions() + { + var pluginEngine = new InputDataPluginEngine( + _serviceProvider, + new Mock>().Object); + + var assemblies = new List() { + typeof(ExternalAppIncoming).AssemblyQualifiedName}; + + pluginEngine.Configure(assemblies); + } + + [Fact] + public async Task ExternalAppPlugin_Should_Replace_StudyUid_Plus_SaveData() + { + var toolkit = new Mock(); + + RemoteAppExecution localCopy = new RemoteAppExecution(); + + _repository.Setup(r => r.AddAsync(It.IsAny(), It.IsAny())) + .Callback((RemoteAppExecution item, CancellationToken c) => localCopy = item); + var dataset = new DicomDataset + { + { DicomTag.PatientID, "PID" }, + { DicomTag.StudyInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SeriesInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SOPInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SOPClassUID, DicomUID.SecondaryCaptureImageStorage.UID } + }; + var dicomFile = new DicomFile(dataset); + var dicomInfo = new DicomFileStorageMetadata( + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + dicomFile.Dataset.GetString(DicomTag.StudyInstanceUID), + dicomFile.Dataset.GetString(DicomTag.SeriesInstanceUID), + dicomFile.Dataset.GetString(DicomTag.SOPInstanceUID)); + + var originalStudyUid = dataset.GetString(DicomTag.StudyInstanceUID); + + toolkit.Setup(t => t.Load(It.IsAny())).Returns(dicomFile); + + var pluginEngine = new OutputDataPluginEngine( + _serviceProvider, + new Mock>().Object, + toolkit.Object); + pluginEngine.Configure(new List() { typeof(ExternalAppOutgoing).AssemblyQualifiedName }); + + string[] destinations = { "fred" }; + + var exportMessage = new ExportRequestDataMessage(new Messaging.Events.ExportRequestEvent() { Destinations = destinations }, ""); + + var exportRequestDataMessage = await pluginEngine.ExecutePlugins(exportMessage); + + Assert.Equal(originalStudyUid, localCopy.StudyUid); + Assert.Equal(dataset.GetString(DicomTag.StudyInstanceUID), localCopy.OutgoingStudyUid); + Assert.NotEqual(originalStudyUid, dataset.GetString(DicomTag.StudyInstanceUID)); + } + + + [Fact] + public async Task ExternalAppPlugin_Should_Repare_StudyUid() + { + var toolkit = new Mock(); + + var dataset = new DicomDataset + { + { DicomTag.PatientID, "PID" }, + { DicomTag.StudyInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SeriesInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SOPInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SOPClassUID, DicomUID.SecondaryCaptureImageStorage.UID } + }; + var dicomFile = new DicomFile(dataset); + var dicomInfo = new DicomFileStorageMetadata( + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + dicomFile.Dataset.GetString(DicomTag.StudyInstanceUID), + dicomFile.Dataset.GetString(DicomTag.SeriesInstanceUID), + dicomFile.Dataset.GetString(DicomTag.SOPInstanceUID)); + + var outboundStudyUid = dataset.GetString(DicomTag.StudyInstanceUID); + var originalStudyUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + + var remoteAppExecution = new RemoteAppExecution + { + OutgoingStudyUid = outboundStudyUid, + StudyUid = originalStudyUid + }; + + _repository.Setup(r => r.GetAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(remoteAppExecution)); + + var pluginEngine = new InputDataPluginEngine( + _serviceProvider, + new Mock>().Object); + + pluginEngine.Configure(new List() { typeof(ExternalAppIncoming).AssemblyQualifiedName }); + + var (resultDicomFile, resultDicomInfo) = await pluginEngine.ExecutePlugins(dicomFile, dicomInfo); + + Assert.Equal(originalStudyUid, resultDicomFile.Dataset.GetString(DicomTag.StudyInstanceUID)); + Assert.NotEqual(outboundStudyUid, resultDicomFile.Dataset.GetString(DicomTag.StudyInstanceUID)); + } + + [Fact] + public async Task ExternalAppPlugin_Should_Set_WorkflowIds() + { + + var dataset = new DicomDataset + { + { DicomTag.PatientID, "PID" }, + { DicomTag.StudyInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SeriesInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SOPInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SOPClassUID, DicomUID.SecondaryCaptureImageStorage.UID } + }; + var dicomFile = new DicomFile(dataset); + var dicomInfo = new DicomFileStorageMetadata( + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + dicomFile.Dataset.GetString(DicomTag.StudyInstanceUID), + dicomFile.Dataset.GetString(DicomTag.SeriesInstanceUID), + dicomFile.Dataset.GetString(DicomTag.SOPInstanceUID)); + + var workflowInstanceId = "some guid here"; + var workflowTaskId = "some guid here 2"; + + var remoteAppExecution = new RemoteAppExecution + { + OutgoingStudyUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + StudyUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + WorkflowInstanceId = workflowInstanceId, + ExportTaskId = workflowTaskId + }; + + _repository.Setup(r => r.GetAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(remoteAppExecution)); + + var pluginEngine = new InputDataPluginEngine( + _serviceProvider, + new Mock>().Object); + + pluginEngine.Configure(new List() { typeof(ExternalAppIncoming).AssemblyQualifiedName }); + + var (resultDicomFile, resultDicomInfo) = await pluginEngine.ExecutePlugins(dicomFile, dicomInfo); + Assert.Equal(workflowInstanceId, resultDicomInfo.WorkflowInstanceId); + Assert.Equal(workflowTaskId, resultDicomInfo.TaskId); + } + + } +} diff --git a/src/InformaticsGateway/Test/packages.lock.json b/src/InformaticsGateway/Test/packages.lock.json old mode 100644 new mode 100755 diff --git a/src/InformaticsGateway/packages.lock.json b/src/InformaticsGateway/packages.lock.json old mode 100644 new mode 100755 diff --git a/tests/Integration.Test/packages.lock.json b/tests/Integration.Test/packages.lock.json old mode 100644 new mode 100755