Skip to content

Commit

Permalink
gh-165 Unit test for C-ECHO API
Browse files Browse the repository at this point in the history
Signed-off-by: Victor Chang <vicchang@nvidia.com>
  • Loading branch information
mocsharp committed Sep 29, 2022
1 parent 15978d0 commit d4b85ad
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 24 deletions.
3 changes: 3 additions & 0 deletions src/InformaticsGateway/Logging/Log.8000.HttpServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ public static partial class Log
[LoggerMessage(EventId = 8014, Level = LogLevel.Error, Message = "Error deleting DICOM destination.")]
public static partial void ErrorDeletingDestinationApplicationEntity(this ILogger logger, Exception ex);

[LoggerMessage(EventId = 8015, Level = LogLevel.Error, Message = "Error C-ECHO to DICOM destination {name}.")]
public static partial void ErrorCEechoDestinationApplicationEntity(this ILogger logger, string name, Exception ex);

// Source AE Title Controller
[LoggerMessage(EventId = 8020, Level = LogLevel.Information, Message = "DICOM source added AE Title={aeTitle}, Host/IP={hostIp}.")]
public static partial void SourceApplicationEntityAdded(this ILogger logger, string aeTitle, string hostIp);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public async Task<ActionResult<IEnumerable<DestinationApplicationEntity>>> Get()
catch (Exception ex)
{
_logger.ErrorQueryingDatabase(ex);
return Problem(title: "Error querying database.", statusCode: (int)System.Net.HttpStatusCode.InternalServerError, detail: ex.Message);
return Problem(title: "Error querying database.", statusCode: StatusCodes.Status500InternalServerError, detail: ex.Message);
}
}

Expand All @@ -86,7 +86,7 @@ public async Task<ActionResult<DestinationApplicationEntity>> GetAeTitle(string
catch (Exception ex)
{
_logger.ErrorListingDestinationApplicationEntities(ex);
return Problem(title: "Error querying DICOM destinations.", statusCode: (int)System.Net.HttpStatusCode.InternalServerError, detail: ex.Message);
return Problem(title: "Error querying DICOM destinations.", statusCode: StatusCodes.Status500InternalServerError, detail: ex.Message);
}
}

Expand All @@ -101,6 +101,11 @@ public async Task<IActionResult> CEcho(string name)
var traceId = HttpContext?.TraceIdentifier ?? Guid.NewGuid().ToString();
try
{
if (string.IsNullOrWhiteSpace(name))
{
return NotFound();
}

var destinationApplicationEntity = await _repository.FindAsync(name).ConfigureAwait(false);

if (destinationApplicationEntity is null)
Expand All @@ -111,7 +116,7 @@ public async Task<IActionResult> CEcho(string name)
var request = new ScuRequest(traceId, RequestType.CEcho, destinationApplicationEntity.HostIp, destinationApplicationEntity.Port, destinationApplicationEntity.AeTitle);
var response = await _scuQueue.Queue(request, HttpContext.RequestAborted).ConfigureAwait(false);

if (response.Status == ResponseStatus.Failure)
if (response.Status != ResponseStatus.Success)
{
return Problem(
title: "C-ECHO Failure",
Expand All @@ -124,11 +129,11 @@ public async Task<IActionResult> CEcho(string name)
}
catch (Exception ex)
{
_logger.ErrorListingDestinationApplicationEntities(ex);
_logger.ErrorCEechoDestinationApplicationEntity(name, ex);
return Problem(
title: "Error querying DICOM destinations.",
title: $"Error performing C-ECHO",
instance: traceId,
statusCode: (int)System.Net.HttpStatusCode.InternalServerError,
statusCode: StatusCodes.Status500InternalServerError,
detail: ex.Message);
}
}
Expand Down Expand Up @@ -159,7 +164,7 @@ public async Task<ActionResult<string>> Create(DestinationApplicationEntity item
catch (Exception ex)
{
_logger.ErrorAddingDestinationApplicationEntity(ex);
return Problem(title: "Error adding new DICOM destination.", statusCode: (int)System.Net.HttpStatusCode.InternalServerError, detail: ex.Message);
return Problem(title: "Error adding new DICOM destination.", statusCode: StatusCodes.Status500InternalServerError, detail: ex.Message);
}
}

Expand Down Expand Up @@ -187,7 +192,7 @@ public async Task<ActionResult<DestinationApplicationEntity>> Delete(string name
catch (Exception ex)
{
_logger.ErrorDeletingDestinationApplicationEntity(ex);
return Problem(title: "Error deleting DICOM destination.", statusCode: (int)System.Net.HttpStatusCode.InternalServerError, detail: ex.Message);
return Problem(title: "Error deleting DICOM destination.", statusCode: StatusCodes.Status500InternalServerError, detail: ex.Message);
}
}

Expand Down
12 changes: 9 additions & 3 deletions src/InformaticsGateway/Services/Scp/ScpService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using FellowOakDicom;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
Expand Down Expand Up @@ -53,14 +54,19 @@ internal sealed class ScpService : IHostedService, IDisposable, IMonaiService
IHostApplicationLifetime appLifetime,
IOptions<InformaticsGatewayConfiguration> configuration)
{
Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory));
Guard.Against.Null(applicationEntityManager, nameof(applicationEntityManager));
Guard.Against.Null(appLifetime, nameof(appLifetime));
Guard.Against.Null(configuration, nameof(configuration));

_serviceScope = serviceScopeFactory.CreateScope();
_associationDataProvider = applicationEntityManager ?? throw new ServiceNotFoundException(nameof(applicationEntityManager));
_associationDataProvider = applicationEntityManager;

var logginFactory = _serviceScope.ServiceProvider.GetService<ILoggerFactory>();

_logger = logginFactory.CreateLogger<ScpService>();
_appLifetime = appLifetime ?? throw new ServiceNotFoundException(nameof(appLifetime));
_configuration = configuration ?? throw new ServiceNotFoundException(nameof(configuration));
_appLifetime = appLifetime;
_configuration = configuration;
_ = DicomDictionary.Default;
}

Expand Down
5 changes: 3 additions & 2 deletions src/InformaticsGateway/Services/Scu/ScuResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@ public enum ResponseError
CEchoError,
Unhandled,
UnsupportedRequestType,
Unknown
Unknown,
AssociationAborted
}

public class ScuResponse
{
internal static readonly ScuResponse NullResponse = new ScuResponse { Status = ResponseStatus.Unknown, Error = ResponseError.Unknown };

public ResponseStatus Status { get; set; }
public string Message { get; set; }
public string Message { get; set; } = string.Empty;
public ResponseError Error { get; internal set; }
}
}
28 changes: 20 additions & 8 deletions src/InformaticsGateway/Services/Scu/ScuService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,14 @@ internal sealed class ScuService : IHostedService, IDisposable, IMonaiService
public string ServiceName => "DICOM SCU Service";

public ScuService(IServiceScopeFactory serviceScopeFactory,
IOptions<InformaticsGatewayConfiguration> configuration,
ILogger<ScuService> logger)
ILogger<ScuService> logger,
IOptions<InformaticsGatewayConfiguration> configuration)
{
Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory));

_scope = serviceScopeFactory.CreateScope();
_logger = logger ?? throw new ServiceNotFoundException(nameof(logger));
_configuration = configuration ?? throw new ServiceNotFoundException(nameof(configuration));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));

_workQueue = _scope.ServiceProvider.GetService<IScuQueue>() ?? throw new ServiceNotFoundException(nameof(IScuQueue));
}
Expand Down Expand Up @@ -130,15 +132,11 @@ private async Task<ScuResponse> HandleCEchoRequest(ScuRequest request, Cancellat
};
client.AssociationRejected += (sender, args) =>
{
scuResponse.Status = ResponseStatus.Failure;
scuResponse.Error = ResponseError.AssociationRejected;
_logger.ScuAssociationRejected();
manualResetEvent.Set();
};
client.AssociationReleased += (sender, args) =>
{
_logger.ScuAssociationReleased();
manualResetEvent.Set();
};
client.ServiceOptions.LogDataPDUs = _configuration.Value.Dicom.Scu.LogDataPdus;
client.ServiceOptions.LogDimseDatasets = _configuration.Value.Dicom.Scu.LogDimseDatasets;
Expand All @@ -163,6 +161,20 @@ private async Task<ScuResponse> HandleCEchoRequest(ScuRequest request, Cancellat
await client.AddRequestAsync(cechoRequest).ConfigureAwait(false);
await client.SendAsync(cancellationToken).ConfigureAwait(false);
}
catch (DicomAssociationRejectedException ex)
{
scuResponse.Status = ResponseStatus.Failure;
scuResponse.Error = ResponseError.AssociationRejected;
scuResponse.Message = ex.Message;
_logger.CEchoFailure(ex.Message);
}
catch (DicomAssociationAbortedException ex)
{
scuResponse.Status = ResponseStatus.Failure;
scuResponse.Error = ResponseError.AssociationAborted;
scuResponse.Message = ex.Message;
_logger.CEchoFailure(ex.Message);
}
catch (Exception ex)
{
scuResponse.Status = ResponseStatus.Failure;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public void GetMonaiServices()
Assert.Collection(result,
items => items.ServiceName.Equals("DataRetrievalService"),
items => items.ServiceName.Equals("ScpService"),
items => items.ServiceName.Equals("ScuService"),
items => items.ServiceName.Equals("SpaceReclaimerService"),
items => items.ServiceName.Equals("DicomWebExportService"),
items => items.ServiceName.Equals("ScuExportService"),
Expand All @@ -61,7 +62,7 @@ public void GetServiceStatus()
var serviceLocator = new MonaiServiceLocator(_serviceProvider.Object);
var result = serviceLocator.GetServiceStatus();

Assert.Equal(7, result.Count);
Assert.Equal(8, result.Count);
foreach (var svc in result.Keys)
{
Assert.Equal(ServiceStatus.Running, result[svc]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,14 @@ public DestinationAeTitleControllerTest()

_repository = new Mock<IInformaticsGatewayRepository<DestinationApplicationEntity>>();

var controllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() };
_controller = new DestinationAeTitleController(
_logger.Object,
_repository.Object,
_scuQueue.Object)
{
ProblemDetailsFactory = _problemDetailsFactory.Object
ProblemDetailsFactory = _problemDetailsFactory.Object,
ControllerContext = controllerContext
};
}

Expand Down Expand Up @@ -176,6 +178,104 @@ public async Task GetAeTitle_ShallReturnProblemOnFailure()

#endregion GetAeTitle

#region C-Echo

[RetryFact(5, 250)]
public async Task GivenAnEmptyString_WhenCEchoIsCalled_Returns404()
{
var result = await _controller.CEcho(string.Empty);
var notFoundResult = result as NotFoundResult;
Assert.NotNull(notFoundResult);
Assert.Equal(StatusCodes.Status404NotFound, notFoundResult.StatusCode);
}

[RetryFact(5, 250)]
public async Task GivenADestinationName_WhenCEchoIsCalledAndEntityCannotBeFound_Returns404()
{
_repository.Setup(p => p.FindAsync(It.IsAny<string>())).ReturnsAsync(default(DestinationApplicationEntity));
var result = await _controller.CEcho("AET");
var notFoundResult = result as NotFoundResult;
Assert.NotNull(notFoundResult);
Assert.Equal(StatusCodes.Status404NotFound, notFoundResult.StatusCode);
}

[RetryFact(5, 250)]
public async Task GivenADestinationName_WhenCEchoIsCalledWithAnError_Returns502()
{
_repository.Setup(p => p.FindAsync(It.IsAny<string>()))
.ReturnsAsync(new DestinationApplicationEntity
{
AeTitle = "AET",
HostIp = "1.2.3.4",
Port = 104,
Name = "AET"
});
_scuQueue.Setup(p => p.Queue(It.IsAny<ScuRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ScuResponse
{
Status = ResponseStatus.Failure,
Error = ResponseError.AssociationRejected,
Message = "error"
});
var result = await _controller.CEcho("AET");
var objectResult = result as ObjectResult;
Assert.NotNull(objectResult);
var problemDetails = objectResult.Value as ProblemDetails;
Assert.NotNull(problemDetails);
Assert.Equal(StatusCodes.Status502BadGateway, problemDetails.Status);
Assert.Equal("C-ECHO Failure", problemDetails.Title);
Assert.Equal("error", problemDetails.Detail);
}

[RetryFact(5, 250)]
public async Task GivenADestinationName_WhenCEchoIsCalledWithUnhandledError_Returns500()
{
_repository.Setup(p => p.FindAsync(It.IsAny<string>()))
.ReturnsAsync(new DestinationApplicationEntity
{
AeTitle = "AET",
HostIp = "1.2.3.4",
Port = 104,
Name = "AET"
});
_scuQueue.Setup(p => p.Queue(It.IsAny<ScuRequest>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception("error"));
var result = await _controller.CEcho("AET");
var objectResult = result as ObjectResult;
Assert.NotNull(objectResult);
var problemDetails = objectResult.Value as ProblemDetails;
Assert.NotNull(problemDetails);
Assert.Equal(StatusCodes.Status500InternalServerError, problemDetails.Status);
Assert.Equal("Error performing C-ECHO", problemDetails.Title);
Assert.Equal("error", problemDetails.Detail);
}

[RetryFact(5, 250)]
public async Task GivenADestinationName_WhenCEchoIsCalledSuccessfully_Returns200()
{
_repository.Setup(p => p.FindAsync(It.IsAny<string>()))
.ReturnsAsync(new DestinationApplicationEntity
{
AeTitle = "AET",
HostIp = "1.2.3.4",
Port = 104,
Name = "AET"
});
_scuQueue.Setup(p => p.Queue(It.IsAny<ScuRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ScuResponse
{
Status = ResponseStatus.Success,
Error = ResponseError.None,
Message = ""
});
var result = await _controller.CEcho("AET");
var okResult = result as OkResult;
Assert.NotNull(okResult);
Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode);
}

#endregion C-Echo

#region Create

[RetryFact(5, 250, DisplayName = "GetAeTitle - Shall return problem on validation failure")]
Expand Down
Loading

0 comments on commit d4b85ad

Please sign in to comment.