From 6fb1527c67483731a75dc6792717fd501f73164f Mon Sep 17 00:00:00 2001 From: Julie Lycklama <8868475+jnlycklama@users.noreply.github.com> Date: Fri, 18 Jun 2021 11:41:57 -0700 Subject: [PATCH] Add ApiVersion v1.0-prerelease (#836) * base functionality for api versioning and swagger docs * add v1 prerelease version to all api endpoints * update health shared package version * add versioned url to e2e tests * keep the original known route names unchanges * add e2e tests for new api paths * update version url in tests * remove ApiController attribute; add VersiontedRoute attribute to simplify versioned routes * remove api version setup code from web project * update formatting * update E2E tests to reuse test parameter data in shared class * consolidate VersionAPIData --- .../Controllers/ChangeFeedController.cs | 3 + .../Controllers/DeleteController.cs | 4 + .../Controllers/ExtendedQueryTagController.cs | 5 ++ .../Controllers/QueryController.cs | 7 ++ .../Controllers/RetrieveController.cs | 8 ++ .../Controllers/StoreController.cs | 2 + .../Features/Routing/KnownRouteNames.cs | 3 + .../Features/Routing/UrlResolver.cs | 6 +- .../Routing/VersionedRouteAttribute.cs | 17 ++++ .../Microsoft.Health.Dicom.Api.csproj | 3 +- .../DicomServerServiceCollectionExtensions.cs | 9 +++ .../Rest/ExtendedQueryTagTests.cs | 38 ++++++--- .../Rest/QueryTransactionTests.cs | 77 +++++++++++-------- ...RetrieveTransactionResourceTests.Common.cs | 13 ++++ .../RetrieveTransactionResourceTests.Frame.cs | 22 ++++-- ...trieveTransactionResourceTests.Instance.cs | 41 +++++++--- ...RetrieveTransactionResourceTests.Series.cs | 8 +- .../RetrieveTransactionResourceTests.Study.cs | 8 +- .../Rest/StoreTransactionTests.cs | 69 ++++++++++------- .../Rest/VersionAPIData.cs | 23 ++++++ 20 files changed, 268 insertions(+), 98 deletions(-) create mode 100644 src/Microsoft.Health.Dicom.Api/Features/Routing/VersionedRouteAttribute.cs create mode 100644 test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/VersionAPIData.cs diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/ChangeFeedController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/ChangeFeedController.cs index c03e79d605..d8711f8179 100644 --- a/src/Microsoft.Health.Dicom.Api/Controllers/ChangeFeedController.cs +++ b/src/Microsoft.Health.Dicom.Api/Controllers/ChangeFeedController.cs @@ -19,6 +19,7 @@ namespace Microsoft.Health.Dicom.Api.Controllers { + [ApiVersion("1.0-prerelease")] [QueryModelStateValidator] [ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))] public class ChangeFeedController : Controller @@ -39,6 +40,7 @@ public ChangeFeedController(IMediator mediator, ILogger lo [ProducesResponseType(typeof(JsonResult), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.Unauthorized)] [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] + [VersionedRoute(KnownRoutes.ChangeFeed)] [Route(KnownRoutes.ChangeFeed)] [AuditEventType(AuditEventSubType.ChangeFeed)] public async Task GetChangeFeed([FromQuery] long offset = 0, [FromQuery] int limit = 10, [FromQuery] bool includeMetadata = true) @@ -58,6 +60,7 @@ public async Task GetChangeFeed([FromQuery] long offset = 0, [Fro [ProducesResponseType(typeof(ChangeFeedEntry), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.Unauthorized)] [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] + [VersionedRoute(KnownRoutes.ChangeFeedLatest)] [Route(KnownRoutes.ChangeFeedLatest)] [AuditEventType(AuditEventSubType.ChangeFeed)] public async Task GetChangeFeedLatest([FromQuery] bool includeMetadata = true) diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/DeleteController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/DeleteController.cs index 35433ba4ea..5046f7c033 100644 --- a/src/Microsoft.Health.Dicom.Api/Controllers/DeleteController.cs +++ b/src/Microsoft.Health.Dicom.Api/Controllers/DeleteController.cs @@ -19,6 +19,7 @@ namespace Microsoft.Health.Dicom.Api.Controllers { + [ApiVersion("1.0-prerelease")] [QueryModelStateValidator] [ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))] public class DeleteController : Controller @@ -39,6 +40,7 @@ public DeleteController(IMediator mediator, ILogger logger) [ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [VersionedRoute(KnownRoutes.StudyRoute)] [Route(KnownRoutes.StudyRoute)] [AuditEventType(AuditEventSubType.Delete)] public async Task DeleteStudyAsync(string studyInstanceUid) @@ -55,6 +57,7 @@ public async Task DeleteStudyAsync(string studyInstanceUid) [ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [VersionedRoute(KnownRoutes.SeriesRoute)] [Route(KnownRoutes.SeriesRoute)] [AuditEventType(AuditEventSubType.Delete)] public async Task DeleteSeriesAsync(string studyInstanceUid, string seriesInstanceUid) @@ -71,6 +74,7 @@ public async Task DeleteSeriesAsync(string studyInstanceUid, stri [ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [VersionedRoute(KnownRoutes.InstanceRoute)] [Route(KnownRoutes.InstanceRoute)] [AuditEventType(AuditEventSubType.Delete)] public async Task DeleteInstanceAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid) diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/ExtendedQueryTagController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/ExtendedQueryTagController.cs index d9712ad417..e0b9db66b4 100644 --- a/src/Microsoft.Health.Dicom.Api/Controllers/ExtendedQueryTagController.cs +++ b/src/Microsoft.Health.Dicom.Api/Controllers/ExtendedQueryTagController.cs @@ -25,6 +25,7 @@ namespace Microsoft.Health.Dicom.Api.Controllers { + [ApiVersion("1.0-prerelease")] [ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))] public class ExtendedQueryTagController : Controller { @@ -46,6 +47,7 @@ public ExtendedQueryTagController(IMediator mediator, ILogger PostAsync([FromBody] IEnumerable extendedQueryTags) @@ -61,6 +63,7 @@ public async Task PostAsync([FromBody] IEnumerable DeleteAsync(string tagPath) @@ -83,6 +86,7 @@ public async Task DeleteAsync(string tagPath) [ProducesResponseType(typeof(JsonResult), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NotFound)] [HttpGet] + [VersionedRoute(KnownRoutes.ExtendedQueryTagRoute)] [Route(KnownRoutes.ExtendedQueryTagRoute)] [AuditEventType(AuditEventSubType.GetAllExtendedQueryTags)] public async Task GetAllTagsAsync() @@ -108,6 +112,7 @@ public async Task GetAllTagsAsync() [ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType((int)HttpStatusCode.NotFound)] [HttpGet] + [VersionedRoute(KnownRoutes.GetExtendedQueryTagRoute)] [Route(KnownRoutes.GetExtendedQueryTagRoute)] [AuditEventType(AuditEventSubType.GetExtendedQueryTag)] public async Task GetTagAsync(string tagPath) diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/QueryController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/QueryController.cs index 6ed327f7a1..b9c082eef9 100644 --- a/src/Microsoft.Health.Dicom.Api/Controllers/QueryController.cs +++ b/src/Microsoft.Health.Dicom.Api/Controllers/QueryController.cs @@ -24,6 +24,7 @@ namespace Microsoft.Health.Dicom.Api.Controllers { + [ApiVersion("1.0-prerelease")] [QueryModelStateValidator] [ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))] public class QueryController : Controller @@ -45,6 +46,7 @@ public QueryController(IMediator mediator, ILogger logger) [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] + [VersionedRoute(KnownRoutes.QueryAllStudiesRoute)] [Route(KnownRoutes.QueryAllStudiesRoute)] [AuditEventType(AuditEventSubType.Query)] public async Task QueryForStudyAsync() @@ -64,6 +66,7 @@ public async Task QueryForStudyAsync() [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] + [VersionedRoute(KnownRoutes.QueryAllSeriesRoute)] [Route(KnownRoutes.QueryAllSeriesRoute)] [AuditEventType(AuditEventSubType.Query)] public async Task QueryForSeriesAsync() @@ -83,6 +86,7 @@ public async Task QueryForSeriesAsync() [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] + [VersionedRoute(KnownRoutes.QuerySeriesInStudyRoute)] [Route(KnownRoutes.QuerySeriesInStudyRoute)] [AuditEventType(AuditEventSubType.Query)] public async Task QueryForSeriesInStudyAsync(string studyInstanceUid) @@ -103,6 +107,7 @@ public async Task QueryForSeriesInStudyAsync(string studyInstance [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] + [VersionedRoute(KnownRoutes.QueryAllInstancesRoute)] [Route(KnownRoutes.QueryAllInstancesRoute)] [AuditEventType(AuditEventSubType.Query)] public async Task QueryForInstancesAsync() @@ -122,6 +127,7 @@ public async Task QueryForInstancesAsync() [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] + [VersionedRoute(KnownRoutes.QueryInstancesInStudyRoute)] [Route(KnownRoutes.QueryInstancesInStudyRoute)] [AuditEventType(AuditEventSubType.Query)] public async Task QueryForInstancesInStudyAsync(string studyInstanceUid) @@ -142,6 +148,7 @@ public async Task QueryForInstancesInStudyAsync(string studyInsta [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] + [VersionedRoute(KnownRoutes.QueryInstancesInSeriesRoute)] [Route(KnownRoutes.QueryInstancesInSeriesRoute)] [AuditEventType(AuditEventSubType.Query)] public async Task QueryForInstancesInSeriesAsync(string studyInstanceUid, string seriesInstanceUid) diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/RetrieveController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/RetrieveController.cs index ebd044233c..1b7a6483f0 100644 --- a/src/Microsoft.Health.Dicom.Api/Controllers/RetrieveController.cs +++ b/src/Microsoft.Health.Dicom.Api/Controllers/RetrieveController.cs @@ -29,6 +29,7 @@ namespace Microsoft.Health.Dicom.Api.Controllers { + [ApiVersion("1.0-prerelease")] [QueryModelStateValidator] [ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))] public class RetrieveController : Controller @@ -51,6 +52,7 @@ public RetrieveController(IMediator mediator, ILogger logger [ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotAcceptable)] [HttpGet] + [VersionedRoute(KnownRoutes.StudyRoute, Name = KnownRouteNames.VersionedRetrieveStudy)] [Route(KnownRoutes.StudyRoute, Name = KnownRouteNames.RetrieveStudy)] [AuditEventType(AuditEventSubType.Retrieve)] public async Task GetStudyAsync(string studyInstanceUid) @@ -69,6 +71,7 @@ public async Task GetStudyAsync(string studyInstanceUid) [ProducesResponseType((int)HttpStatusCode.NotAcceptable)] [ProducesResponseType((int)HttpStatusCode.NotModified)] [HttpGet] + [VersionedRoute(KnownRoutes.StudyMetadataRoute)] [Route(KnownRoutes.StudyMetadataRoute)] [AuditEventType(AuditEventSubType.RetrieveMetadata)] public async Task GetStudyMetadataAsync([FromHeader(Name = IfNoneMatch)] string ifNoneMatch, string studyInstanceUid) @@ -85,6 +88,7 @@ public async Task GetStudyMetadataAsync([FromHeader(Name = IfNone [ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotAcceptable)] [HttpGet] + [VersionedRoute(KnownRoutes.SeriesRoute)] [Route(KnownRoutes.SeriesRoute)] [AuditEventType(AuditEventSubType.Retrieve)] public async Task GetSeriesAsync( @@ -106,6 +110,7 @@ public async Task GetSeriesAsync( [ProducesResponseType((int)HttpStatusCode.NotAcceptable)] [ProducesResponseType((int)HttpStatusCode.NotModified)] [HttpGet] + [VersionedRoute(KnownRoutes.SeriesMetadataRoute)] [Route(KnownRoutes.SeriesMetadataRoute)] [AuditEventType(AuditEventSubType.RetrieveMetadata)] public async Task GetSeriesMetadataAsync([FromHeader(Name = IfNoneMatch)] string ifNoneMatch, string studyInstanceUid, string seriesInstanceUid) @@ -123,6 +128,7 @@ public async Task GetSeriesMetadataAsync([FromHeader(Name = IfNon [ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotAcceptable)] [HttpGet] + [VersionedRoute(KnownRoutes.InstanceRoute, Name = KnownRouteNames.VersionedRetrieveInstance)] [Route(KnownRoutes.InstanceRoute, Name = KnownRouteNames.RetrieveInstance)] [AuditEventType(AuditEventSubType.Retrieve)] public async Task GetInstanceAsync( @@ -148,6 +154,7 @@ public async Task GetInstanceAsync( [ProducesResponseType((int)HttpStatusCode.NotAcceptable)] [ProducesResponseType((int)HttpStatusCode.NotModified)] [HttpGet] + [VersionedRoute(KnownRoutes.InstanceMetadataRoute)] [Route(KnownRoutes.InstanceMetadataRoute)] [AuditEventType(AuditEventSubType.RetrieveMetadata)] public async Task GetInstanceMetadataAsync( @@ -170,6 +177,7 @@ public async Task GetInstanceMetadataAsync( [ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotAcceptable)] [HttpGet] + [VersionedRoute(KnownRoutes.FrameRoute)] [Route(KnownRoutes.FrameRoute)] [AuditEventType(AuditEventSubType.Retrieve)] public async Task GetFramesAsync( diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/StoreController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/StoreController.cs index 5524c4ea6c..5a613a67b2 100644 --- a/src/Microsoft.Health.Dicom.Api/Controllers/StoreController.cs +++ b/src/Microsoft.Health.Dicom.Api/Controllers/StoreController.cs @@ -22,6 +22,7 @@ namespace Microsoft.Health.Dicom.Api.Controllers { + [ApiVersion("1.0-prerelease")] [QueryModelStateValidator] [ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))] public class StoreController : Controller @@ -47,6 +48,7 @@ public StoreController(IMediator mediator, ILogger logger) [ProducesResponseType(typeof(DicomDataset), (int)HttpStatusCode.Conflict)] [ProducesResponseType((int)HttpStatusCode.UnsupportedMediaType)] [HttpPost] + [VersionedRoute(KnownRoutes.StoreRoute)] [Route(KnownRoutes.StoreRoute)] [AuditEventType(AuditEventSubType.Store)] public async Task PostAsync(string studyInstanceUid = null) diff --git a/src/Microsoft.Health.Dicom.Api/Features/Routing/KnownRouteNames.cs b/src/Microsoft.Health.Dicom.Api/Features/Routing/KnownRouteNames.cs index 3c38cab546..e23dbeec71 100644 --- a/src/Microsoft.Health.Dicom.Api/Features/Routing/KnownRouteNames.cs +++ b/src/Microsoft.Health.Dicom.Api/Features/Routing/KnownRouteNames.cs @@ -7,7 +7,10 @@ namespace Microsoft.Health.Dicom.Api.Features.Routing { internal class KnownRouteNames { + internal const string VersionedRetrieveStudy = nameof(VersionedRetrieveStudy); internal const string RetrieveStudy = nameof(RetrieveStudy); + + internal const string VersionedRetrieveInstance = nameof(VersionedRetrieveInstance); internal const string RetrieveInstance = nameof(RetrieveInstance); } } diff --git a/src/Microsoft.Health.Dicom.Api/Features/Routing/UrlResolver.cs b/src/Microsoft.Health.Dicom.Api/Features/Routing/UrlResolver.cs index 9d161c7525..97afab8fb5 100644 --- a/src/Microsoft.Health.Dicom.Api/Features/Routing/UrlResolver.cs +++ b/src/Microsoft.Health.Dicom.Api/Features/Routing/UrlResolver.cs @@ -46,9 +46,10 @@ public UrlResolver( public Uri ResolveRetrieveStudyUri(string studyInstanceUid) { EnsureArg.IsNotNull(studyInstanceUid, nameof(studyInstanceUid)); + var hasVersion = _httpContextAccessor.HttpContext.Request.RouteValues.ContainsKey("version"); return RouteUri( - KnownRouteNames.RetrieveStudy, + hasVersion ? KnownRouteNames.VersionedRetrieveStudy : KnownRouteNames.RetrieveStudy, new RouteValueDictionary() { { KnownActionParameterNames.StudyInstanceUid, studyInstanceUid }, @@ -59,9 +60,10 @@ public Uri ResolveRetrieveStudyUri(string studyInstanceUid) public Uri ResolveRetrieveInstanceUri(InstanceIdentifier instanceIdentifier) { EnsureArg.IsNotNull(instanceIdentifier, nameof(instanceIdentifier)); + var hasVersion = _httpContextAccessor.HttpContext.Request.RouteValues.ContainsKey("version"); return RouteUri( - KnownRouteNames.RetrieveInstance, + hasVersion ? KnownRouteNames.VersionedRetrieveInstance : KnownRouteNames.RetrieveInstance, new RouteValueDictionary() { { KnownActionParameterNames.StudyInstanceUid, instanceIdentifier.StudyInstanceUid }, diff --git a/src/Microsoft.Health.Dicom.Api/Features/Routing/VersionedRouteAttribute.cs b/src/Microsoft.Health.Dicom.Api/Features/Routing/VersionedRouteAttribute.cs new file mode 100644 index 0000000000..b3e636d67f --- /dev/null +++ b/src/Microsoft.Health.Dicom.Api/Features/Routing/VersionedRouteAttribute.cs @@ -0,0 +1,17 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using Microsoft.AspNetCore.Mvc; + +namespace Microsoft.Health.Dicom.Api.Features.Routing +{ + public sealed class VersionedRouteAttribute : RouteAttribute + { + public VersionedRouteAttribute(string template) + : base("v{version:apiVersion}/" + template) + { + } + } +} diff --git a/src/Microsoft.Health.Dicom.Api/Microsoft.Health.Dicom.Api.csproj b/src/Microsoft.Health.Dicom.Api/Microsoft.Health.Dicom.Api.csproj index 6b65b9a471..8b0d135af0 100644 --- a/src/Microsoft.Health.Dicom.Api/Microsoft.Health.Dicom.Api.csproj +++ b/src/Microsoft.Health.Dicom.Api/Microsoft.Health.Dicom.Api.csproj @@ -1,4 +1,4 @@ - + Common components, such as controllers, for Microsoft's DICOMweb APIs using ASP.NET Core. @@ -16,6 +16,7 @@ + diff --git a/src/Microsoft.Health.Dicom.Api/Registration/DicomServerServiceCollectionExtensions.cs b/src/Microsoft.Health.Dicom.Api/Registration/DicomServerServiceCollectionExtensions.cs index f8af84c774..cc85b50223 100644 --- a/src/Microsoft.Health.Dicom.Api/Registration/DicomServerServiceCollectionExtensions.cs +++ b/src/Microsoft.Health.Dicom.Api/Registration/DicomServerServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ using Dicom.Serialization; using EnsureThat; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -94,6 +95,14 @@ public static IDicomServerBuilder AddDicomServer( jsonOptions.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); + services.AddApiVersioning(c => + { + c.AssumeDefaultVersionWhenUnspecified = true; + c.DefaultApiVersion = new ApiVersion(1, 0, "prerelease"); + c.ReportApiVersions = true; + c.UseApiBehavior = false; + }); + services.AddSingleton(); services.RegisterAssemblyModules(typeof(DicomMediatorExtensions).Assembly, dicomServerConfiguration.Features); diff --git a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/ExtendedQueryTagTests.cs b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/ExtendedQueryTagTests.cs index d33b7748b8..8ee5160103 100644 --- a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/ExtendedQueryTagTests.cs +++ b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/ExtendedQueryTagTests.cs @@ -24,6 +24,7 @@ public class ExtendedQueryTagTests : IClassFixture fixture) { EnsureArg.IsNotNull(fixture, nameof(fixture)); @@ -31,8 +32,9 @@ public ExtendedQueryTagTests(HttpIntegrationTestFixture fixture) _client = fixture.Client; } - [Fact] - public async Task GivenValidExtendedQueryTags_WhenGoThroughEndToEndScenario_ThenShouldSucceed() + [Theory] + [ClassData(typeof(VersionAPIData))] + public async Task GivenValidExtendedQueryTags_WhenGoThroughEndToEndScenario_ThenShouldSucceed(string versionPath) { // Prepare 3 extended query tags. // One is private tag on Instance level @@ -101,19 +103,19 @@ public async Task GivenValidExtendedQueryTags_WhenGoThroughEndToEndScenario_Then await _client.StoreAsync(dicomFiles, studyInstanceUid: string.Empty, cancellationToken: default); // Query on instance for private tag - DicomWebAsyncEnumerableResponse queryInstanceResponse = await _client.QueryAsync(new Uri($"/instances?{privateTag.GetPath()}=3", UriKind.Relative), cancellationToken: default); + DicomWebAsyncEnumerableResponse queryInstanceResponse = await _client.QueryAsync(new Uri($"{versionPath}/instances?{privateTag.GetPath()}=3", UriKind.Relative), cancellationToken: default); DicomDataset[] instanceResult = await queryInstanceResponse.ToArrayAsync(); Assert.Single(instanceResult); Assert.Equal(instanceUid3, instanceResult[0].GetSingleValue(DicomTag.SOPInstanceUID)); // Query on series for standardTagSeries - DicomWebAsyncEnumerableResponse querySeriesResponse = await _client.QueryAsync(new Uri($"/series?{standardTagSeries.GetPath()}=ManufacturerModelName2", UriKind.Relative), cancellationToken: default); + DicomWebAsyncEnumerableResponse querySeriesResponse = await _client.QueryAsync(new Uri($"{versionPath}/series?{standardTagSeries.GetPath()}=ManufacturerModelName2", UriKind.Relative), cancellationToken: default); DicomDataset[] seriesResult = await querySeriesResponse.ToArrayAsync(); Assert.Single(seriesResult); Assert.Equal(seriesUid1, seriesResult[0].GetSingleValue(DicomTag.SeriesInstanceUID)); // Query on study for standardTagStudy - DicomWebAsyncEnumerableResponse queryStudyResponse = await _client.QueryAsync(new Uri($"/studies?{standardTagStudy.GetPath()}=1", UriKind.Relative), cancellationToken: default); + DicomWebAsyncEnumerableResponse queryStudyResponse = await _client.QueryAsync(new Uri($"{versionPath}/studies?{standardTagStudy.GetPath()}=1", UriKind.Relative), cancellationToken: default); DicomDataset[] studyResult = await queryStudyResponse.ToArrayAsync(); Assert.Single(studyResult); Assert.Equal(studyUid, seriesResult[0].GetSingleValue(DicomTag.StudyInstanceUID)); @@ -138,11 +140,10 @@ public async Task GivenValidExtendedQueryTags_WhenGoThroughEndToEndScenario_Then } [Theory] - [InlineData("[{\"Path\":\"00100040\"}]", "Level")] - [InlineData("[{\"Path\":\"\",\"Level\":\"Study\"}]", "Path")] - public async Task GivenMissingPropertyInRequestBody_WhenCallingPostAsync_ThenShouldThrowException(string requestBody, string missingProperty) + [MemberData(nameof(GetVersionsAndRequestBodyWithMissingProperty))] + public async Task GivenMissingPropertyInRequestBody_WhenCallingPostAsync_ThenShouldThrowException(string requestBody, string missingProperty, string versionPath) { - using var request = new HttpRequestMessage(HttpMethod.Post, "/extendedquerytags"); + using var request = new HttpRequestMessage(HttpMethod.Post, $"{versionPath}/extendedquerytags"); { request.Content = new StringContent(requestBody); request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(DicomWebConstants.ApplicationJsonMediaType); @@ -154,11 +155,12 @@ public async Task GivenMissingPropertyInRequestBody_WhenCallingPostAsync_ThenSho Assert.Contains(string.Format("The request body is not valid. Details: \r\nThe Dicom Tag Property {0} must be specified and must not be null, empty or whitespace", missingProperty), response.Content.ReadAsStringAsync().Result); } - [Fact] - public async Task GivenInvalidTagLevelInRequestBody_WhenCallingPostAync_ThenShouldThrowException() + [Theory] + [ClassData(typeof(VersionAPIData))] + public async Task GivenInvalidTagLevelInRequestBody_WhenCallingPostAync_ThenShouldThrowException(string versionPath) { string requestBody = "[{\"Path\":\"00100040\",\"Level\":\"Studys\"}]"; - using var request = new HttpRequestMessage(HttpMethod.Post, "/extendedquerytags"); + using var request = new HttpRequestMessage(HttpMethod.Post, $"{versionPath}/extendedquerytags"); { request.Content = new StringContent(requestBody); request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(DicomWebConstants.ApplicationJsonMediaType); @@ -170,6 +172,18 @@ public async Task GivenInvalidTagLevelInRequestBody_WhenCallingPostAync_ThenShou Assert.Equal("The request body is not valid. Details: \r\nInput Dicom Tag Level 'Studys' is invalid. It must have value 'Study', 'Series' or 'Instance'.", response.Content.ReadAsStringAsync().Result); } + public static IEnumerable GetVersionsAndRequestBodyWithMissingProperty + { + get + { + foreach (object[] version in VersionAPIData.VersionSegmentData) + { + yield return new object[] { "[{\"Path\":\"00100040\"}]", "Level", version[0] }; + yield return new object[] { "[{\"Path\":\"\",\"Level\":\"Study\"}]", "Path", version[0] }; + } + } + } + private void CompareExtendedQueryTagEntries(AddExtendedQueryTagEntry addedTag, GetExtendedQueryTagEntry returnedTag) { if (addedTag == null || returnedTag == null) diff --git a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/QueryTransactionTests.cs b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/QueryTransactionTests.cs index e5122245bf..dadcfdf944 100644 --- a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/QueryTransactionTests.cs +++ b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/QueryTransactionTests.cs @@ -32,26 +32,29 @@ public QueryTransactionTests(HttpIntegrationTestFixture fixture) _client = fixture.Client; } - [Fact] - public async Task GivenSearchRequest_WithUnsupportedTag_ReturnBadRequest() + [Theory] + [ClassData(typeof(VersionAPIData))] + public async Task GivenSearchRequest_WithUnsupportedTag_ReturnBadRequest(string versionPath) { DicomWebException exception = await Assert.ThrowsAsync( - () => _client.QueryAsync(new Uri("/studies?Modality=CT", UriKind.Relative))); + () => _client.QueryAsync(new Uri($"{versionPath}/studies?Modality=CT", UriKind.Relative))); Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode); Assert.Equal(exception.ResponseMessage, string.Format(DicomCoreResource.UnsupportedSearchParameter, "Modality", "study")); } - [Fact] - public async Task GivenSearchRequest_WithValidParamsAndNoMatchingResult_ReturnNoContent() + [Theory] + [ClassData(typeof(VersionAPIData))] + public async Task GivenSearchRequest_WithValidParamsAndNoMatchingResult_ReturnNoContent(string versionPath) { - using DicomWebAsyncEnumerableResponse response = await _client.QueryAsync(new Uri("/studies?StudyDate=20200101", UriKind.Relative)); + using DicomWebAsyncEnumerableResponse response = await _client.QueryAsync(new Uri($"{versionPath}/studies?StudyDate=20200101", UriKind.Relative)); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } - [Fact] + [Theory] + [ClassData(typeof(VersionAPIData))] [Trait("Category", "bvt")] - public async Task GivenSearchRequest_AllStudyLevel_MatchResult() + public async Task GivenSearchRequest_AllStudyLevel_MatchResult(string versionPath) { DicomDataset matchInstance = await PostDicomFileAsync(new DicomDataset() { @@ -63,7 +66,7 @@ public async Task GivenSearchRequest_AllStudyLevel_MatchResult() }); var studyId = matchInstance.GetSingleValue(DicomTag.StudyInstanceUID); - using DicomWebAsyncEnumerableResponse response = await _client.QueryAsync(new Uri("/studies?StudyDate=20190101", UriKind.Relative)); + using DicomWebAsyncEnumerableResponse response = await _client.QueryAsync(new Uri($"{versionPath}/studies?StudyDate=20190101", UriKind.Relative)); DicomDataset[] datasets = await response.ToArrayAsync(); @@ -73,8 +76,9 @@ public async Task GivenSearchRequest_AllStudyLevel_MatchResult() ValidateResponseDataset(QueryResource.AllStudies, matchInstance, testDataResponse); } - [Fact] - public async Task GivenSearchRequest_AllStudyLevelOnPatientName_MatchIsCaseIncensitiveAndAccentIncensitive() + [Theory] + [ClassData(typeof(VersionAPIData))] + public async Task GivenSearchRequest_AllStudyLevelOnPatientName_MatchIsCaseIncensitiveAndAccentIncensitive(string versionPath) { string randomNamePart = RandomString(7); string patientName = $"Hall^{randomNamePart}^Tá"; @@ -87,7 +91,7 @@ public async Task GivenSearchRequest_AllStudyLevelOnPatientName_MatchIsCaseIncen }); using DicomWebAsyncEnumerableResponse response = await _client.QueryAsync( - new Uri($"/studies?PatientName={patientNameWithNoAccent}", UriKind.Relative)); + new Uri($"{versionPath}/studies?PatientName={patientNameWithNoAccent}", UriKind.Relative)); DicomDataset[] datasets = await response.ToArrayAsync(); @@ -97,8 +101,9 @@ public async Task GivenSearchRequest_AllStudyLevelOnPatientName_MatchIsCaseIncen Assert.Equal(patientName, testDataResponse.GetString(DicomTag.PatientName)); } - [Fact] - public async Task GivenSearchRequest_StudySeriesLevel_MatchResult() + [Theory] + [ClassData(typeof(VersionAPIData))] + public async Task GivenSearchRequest_StudySeriesLevel_MatchResult(string versionPath) { DicomDataset matchInstance = await PostDicomFileAsync(new DicomDataset() { @@ -113,7 +118,7 @@ await PostDicomFileAsync(new DicomDataset() }); using DicomWebAsyncEnumerableResponse response = await _client.QueryAsync( - new Uri($"/studies/{studyId}/series?Modality=MRI", UriKind.Relative)); + new Uri($"{versionPath}/studies/{studyId}/series?Modality=MRI", UriKind.Relative)); DicomDataset[] datasets = await response.ToArrayAsync(); @@ -121,8 +126,9 @@ await PostDicomFileAsync(new DicomDataset() ValidateResponseDataset(QueryResource.StudySeries, matchInstance, datasets[0]); } - [Fact] - public async Task GivenSearchRequest_AllSeriesLevel_MatchResult() + [Theory] + [ClassData(typeof(VersionAPIData))] + public async Task GivenSearchRequest_AllSeriesLevel_MatchResult(string versionPath) { DicomDataset matchInstance = await PostDicomFileAsync(new DicomDataset() { @@ -131,7 +137,7 @@ public async Task GivenSearchRequest_AllSeriesLevel_MatchResult() var seriesId = matchInstance.GetSingleValue(DicomTag.SeriesInstanceUID); using DicomWebAsyncEnumerableResponse response = await _client.QueryAsync( - new Uri("/series?Modality=MRI", UriKind.Relative)); + new Uri($"{versionPath}/series?Modality=MRI", UriKind.Relative)); DicomDataset[] datasets = await response.ToArrayAsync(); @@ -141,8 +147,9 @@ public async Task GivenSearchRequest_AllSeriesLevel_MatchResult() ValidateResponseDataset(QueryResource.AllSeries, matchInstance, testDataResponse); } - [Fact] - public async Task GivenSearchRequest_StudyInstancesLevel_MatchResult() + [Theory] + [ClassData(typeof(VersionAPIData))] + public async Task GivenSearchRequest_StudyInstancesLevel_MatchResult(string versionPath) { DicomDataset matchInstance = await PostDicomFileAsync(new DicomDataset() { @@ -156,7 +163,7 @@ await PostDicomFileAsync(new DicomDataset() }); using DicomWebAsyncEnumerableResponse response = await _client.QueryAsync( - new Uri($"/studies/{studyId}/instances?Modality=MRI", UriKind.Relative)); + new Uri($"{versionPath}/studies/{studyId}/instances?Modality=MRI", UriKind.Relative)); DicomDataset[] datasets = await response.ToArrayAsync(); @@ -164,8 +171,9 @@ await PostDicomFileAsync(new DicomDataset() ValidateResponseDataset(QueryResource.StudyInstances, matchInstance, datasets[0]); } - [Fact] - public async Task GivenSearchRequest_StudySeriesInstancesLevel_MatchResult() + [Theory] + [ClassData(typeof(VersionAPIData))] + public async Task GivenSearchRequest_StudySeriesInstancesLevel_MatchResult(string versionPath) { DicomDataset matchInstance = await PostDicomFileAsync(); var studyId = matchInstance.GetSingleValue(DicomTag.StudyInstanceUID); @@ -178,7 +186,7 @@ await PostDicomFileAsync(new DicomDataset() }); using DicomWebAsyncEnumerableResponse response = await _client.QueryAsync( - new Uri($"/studies/{studyId}/series/{seriesId}/instances?SOPInstanceUID={instanceId}", UriKind.Relative)); + new Uri($"{versionPath}/studies/{studyId}/series/{seriesId}/instances?SOPInstanceUID={instanceId}", UriKind.Relative)); DicomDataset[] datasets = await response.ToArrayAsync(); @@ -186,8 +194,9 @@ await PostDicomFileAsync(new DicomDataset() ValidateResponseDataset(QueryResource.StudySeriesInstances, matchInstance, datasets[0]); } - [Fact] - public async Task GivenSearchRequest_AllInstancesLevel_MatchResult() + [Theory] + [ClassData(typeof(VersionAPIData))] + public async Task GivenSearchRequest_AllInstancesLevel_MatchResult(string versionPath) { DicomDataset matchInstance = await PostDicomFileAsync(new DicomDataset() { @@ -196,7 +205,7 @@ public async Task GivenSearchRequest_AllInstancesLevel_MatchResult() var studyId = matchInstance.GetSingleValue(DicomTag.StudyInstanceUID); using DicomWebAsyncEnumerableResponse response = await _client.QueryAsync( - new Uri("/instances?Modality=XRAY", UriKind.Relative)); + new Uri($"{versionPath}/instances?Modality=XRAY", UriKind.Relative)); DicomDataset[] datasets = await response.ToArrayAsync(); @@ -206,8 +215,9 @@ public async Task GivenSearchRequest_AllInstancesLevel_MatchResult() ValidateResponseDataset(QueryResource.AllInstances, matchInstance, testDataResponse); } - [Fact] - public async Task GivenSearchRequest_PatientNameFuzzyMatch_MatchResult() + [Theory] + [ClassData(typeof(VersionAPIData))] + public async Task GivenSearchRequest_PatientNameFuzzyMatch_MatchResult(string versionPath) { string randomNamePart = RandomString(7); DicomDataset matchInstance2 = await PostDicomFileAsync(new DicomDataset() @@ -230,7 +240,7 @@ public async Task GivenSearchRequest_PatientNameFuzzyMatch_MatchResult() while (retryCount < 3 || testDataResponse1 == null) { using DicomWebAsyncEnumerableResponse response = await _client.QueryAsync( - new Uri($"/studies?PatientName={randomNamePart}&FuzzyMatching=true", UriKind.Relative)); + new Uri($"{versionPath}/studies?PatientName={randomNamePart}&FuzzyMatching=true", UriKind.Relative)); responseDatasets = await response.ToArrayAsync(); @@ -246,10 +256,11 @@ public async Task GivenSearchRequest_PatientNameFuzzyMatch_MatchResult() ValidateResponseDataset(QueryResource.AllStudies, matchInstance2, testDataResponse2); } - [Fact] - public async Task GivenSearchRequest_OHIFViewerStudyQuery_ReturnsOK() + [Theory] + [ClassData(typeof(VersionAPIData))] + public async Task GivenSearchRequest_OHIFViewerStudyQuery_ReturnsOK(string versionPath) { - var OhifViewerQuery = "/studies?limit=25&offset=0&includefield=00081030%2C00080060&StudyDate=19521125-20210507"; + var OhifViewerQuery = $"{versionPath}/studies?limit=25&offset=0&includefield=00081030%2C00080060&StudyDate=19521125-20210507"; // client is checking the success response and throws exception otherwise using DicomWebAsyncEnumerableResponse response = await _client.QueryAsync(new Uri(OhifViewerQuery, UriKind.Relative)); diff --git a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Common.cs b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Common.cs index 60cd7dea22..9d31b4c46a 100644 --- a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Common.cs +++ b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Common.cs @@ -74,5 +74,18 @@ public async Task DisposeAsync() _studiesToClean.Clear(); } + + public static IEnumerable GetVersionsAndUnsupportedAcceptHeadersForStudiesAndSeries + { + get + { + foreach (object[] version in VersionAPIData.VersionSegmentData) + { + yield return new object[] { true, DicomWebConstants.ApplicationDicomMediaType, DicomWebConstants.OriginalDicomTransferSyntax, version[0] }; // use single part instead of multiple part + yield return new object[] { false, DicomWebConstants.ApplicationOctetStreamMediaType, DicomWebConstants.OriginalDicomTransferSyntax, version[0] }; // unsupported media type image/png + yield return new object[] { false, DicomWebConstants.ApplicationDicomMediaType, "1.2.840.10008.1.2.4.100", version[0] }; // unsupported media type MPEG2 + } + } + } } } diff --git a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Frame.cs b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Frame.cs index 23894537d6..3e927e6db4 100644 --- a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Frame.cs +++ b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Frame.cs @@ -4,6 +4,7 @@ // ------------------------------------------------------------------------------------------------- using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -63,12 +64,10 @@ public async Task GivenSupportedAcceptHeaders_WhenRetrieveFrame_ThenServerShould } [Theory] - [InlineData(true, DicomWebConstants.ApplicationOctetStreamMediaType, DicomWebConstants.OriginalDicomTransferSyntax)] // use single part instead of multiple part - [InlineData(false, DicomWebConstants.ImagePngMediaType, DicomWebConstants.OriginalDicomTransferSyntax)] // unsupported media type image/png - [InlineData(false, DicomWebConstants.ApplicationOctetStreamMediaType, "1.2.840.10008.1.2.4.100")] // unsupported media type MPEG2 - public async Task GivenUnsupportedAcceptHeaders_WhenRetrieveFrame_ThenServerShouldReturnNotAcceptable(bool singlePart, string mediaType, string transferSyntax) + [MemberData(nameof(GetVersionsAndUnsupportedAcceptHeadersForFrames))] + public async Task GivenUnsupportedAcceptHeaders_WhenRetrieveFrame_ThenServerShouldReturnNotAcceptable(bool singlePart, string mediaType, string transferSyntax, string versionPath) { - var requestUri = new Uri(string.Format(DicomWebConstants.BaseRetrieveFramesUriFormat, TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), string.Join("%2C", new int[] { 1 })), UriKind.Relative); + var requestUri = new Uri(versionPath + string.Format(DicomWebConstants.BaseRetrieveFramesUriFormat, TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), string.Join("%2C", new int[] { 1 })), UriKind.Relative); using HttpRequestMessage request = new HttpRequestMessageBuilder().Build(requestUri, singlePart: singlePart, mediaType, transferSyntax); using HttpResponseMessage response = await _client.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); @@ -185,5 +184,18 @@ public async Task GivenInvalidFrames_WhenRetrievingFrame_TheServerShouldReturnBa () => _client.RetrieveFramesAsync(requestUri)); Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode); } + + public static IEnumerable GetVersionsAndUnsupportedAcceptHeadersForFrames + { + get + { + foreach (object[] version in VersionAPIData.VersionSegmentData) + { + yield return new object[] { true, DicomWebConstants.ApplicationOctetStreamMediaType, DicomWebConstants.OriginalDicomTransferSyntax, version[0] }; // use single part instead of multiple part + yield return new object[] { false, DicomWebConstants.ImagePngMediaType, DicomWebConstants.OriginalDicomTransferSyntax, version[0] }; // unsupported media type image/png + yield return new object[] { false, DicomWebConstants.ApplicationOctetStreamMediaType, "1.2.840.10008.1.2.4.100", version[0] }; // unsupported media type MPEG2 + } + } + } } } diff --git a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Instance.cs b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Instance.cs index 6b585410b5..2eb7a0ab55 100644 --- a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Instance.cs +++ b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Instance.cs @@ -4,6 +4,7 @@ // ------------------------------------------------------------------------------------------------- using System; +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -53,10 +54,8 @@ public async Task GivenSinglePartAcceptHeader_WhenRetrieveInstance_ThenServerSho } [Theory] - [InlineData(RequestOriginalContentTestFolder, "*")] - [InlineData(FromJPEG2000LosslessToExplicitVRLittleEndianTestFolder, null)] - [InlineData(FromJPEG2000LosslessToExplicitVRLittleEndianTestFolder, "1.2.840.10008.1.2.1")] - public async Task GivenMultipartAcceptHeader_WhenRetrieveInstance_ThenServerShouldReturnExpectedContent(string testDataFolder, string transferSyntax) + [MemberData(nameof(GetVersionsAndAcceptHeadersForInstances))] + public async Task GivenMultipartAcceptHeader_WhenRetrieveInstance_ThenServerShouldReturnExpectedContent(string testDataFolder, string transferSyntax, string versionPath) { TranscoderTestData transcoderTestData = TranscoderTestDataHelper.GetTestData(testDataFolder); DicomFile inputDicomFile = DicomFile.Open(transcoderTestData.InputDicomFile); @@ -64,7 +63,7 @@ public async Task GivenMultipartAcceptHeader_WhenRetrieveInstance_ThenServerShou await InternalStoreAsync(new[] { inputDicomFile }); - var requestUri = new Uri(string.Format(DicomWebConstants.BaseInstanceUriFormat, instanceId.StudyInstanceUid, instanceId.SeriesInstanceUid, instanceId.SopInstanceUid), UriKind.Relative); + var requestUri = new Uri(versionPath + string.Format(DicomWebConstants.BaseInstanceUriFormat, instanceId.StudyInstanceUid, instanceId.SeriesInstanceUid, instanceId.SopInstanceUid), UriKind.Relative); using HttpRequestMessage request = new HttpRequestMessageBuilder().Build(requestUri, singlePart: false, DicomWebConstants.ApplicationDicomMediaType, transferSyntax); using HttpResponseMessage response = await _client.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); @@ -73,11 +72,10 @@ public async Task GivenMultipartAcceptHeader_WhenRetrieveInstance_ThenServerShou } [Theory] - [InlineData(true, DicomWebConstants.ApplicationOctetStreamMediaType, DicomWebConstants.OriginalDicomTransferSyntax)] // unsupported media type image/png - [InlineData(true, DicomWebConstants.ApplicationDicomMediaType, "1.2.840.10008.1.2.4.100")] // unsupported transfer syntax MPEG2 - public async Task GivenUnsupportedAcceptHeaders_WhenRetrieveInstance_ThenServerShouldReturnNotAcceptable(bool singlePart, string mediaType, string transferSyntax) + [MemberData(nameof(GetVersionsAndUnsupportedAcceptHeadersForInstances))] + public async Task GivenUnsupportedAcceptHeaders_WhenRetrieveInstance_ThenServerShouldReturnNotAcceptable(bool singlePart, string mediaType, string transferSyntax, string versionPath) { - var requestUri = new Uri(string.Format(DicomWebConstants.BaseInstanceUriFormat, TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate()), UriKind.Relative); + var requestUri = new Uri(versionPath + string.Format(DicomWebConstants.BaseInstanceUriFormat, TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate()), UriKind.Relative); using HttpRequestMessage request = new HttpRequestMessageBuilder().Build(requestUri, singlePart: singlePart, mediaType, transferSyntax); using HttpResponseMessage response = await _client.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); @@ -143,5 +141,30 @@ public async Task GivenInstanceWithoutPixelData_WhenRetrieveInstance_ThenServerS using DicomWebResponse instanceRetrieve = await _client.RetrieveInstanceAsync(studyInstanceUid, seriesInstanceUid, sopInstanceUid, dicomTransferSyntax: "*"); Assert.Equal(dicomFile1.ToByteArray(), (await instanceRetrieve.GetValueAsync()).ToByteArray()); } + + public static IEnumerable GetVersionsAndAcceptHeadersForInstances + { + get + { + foreach (object[] version in VersionAPIData.VersionSegmentData) + { + yield return new object[] { RequestOriginalContentTestFolder, "*", version[0] }; + yield return new object[] { FromJPEG2000LosslessToExplicitVRLittleEndianTestFolder, null, version[0] }; + yield return new object[] { FromJPEG2000LosslessToExplicitVRLittleEndianTestFolder, "1.2.840.10008.1.2.1", version[0] }; + } + } + } + + public static IEnumerable GetVersionsAndUnsupportedAcceptHeadersForInstances + { + get + { + foreach (object[] version in VersionAPIData.VersionSegmentData) + { + yield return new object[] { true, DicomWebConstants.ApplicationOctetStreamMediaType, DicomWebConstants.OriginalDicomTransferSyntax, version[0] }; // unsupported media type image/png + yield return new object[] { true, DicomWebConstants.ApplicationDicomMediaType, "1.2.840.10008.1.2.4.100", version[0] }; // unsupported transfer syntax MPEG2 + } + } + } } } diff --git a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Series.cs b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Series.cs index 879624444f..10cd53b957 100644 --- a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Series.cs +++ b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Series.cs @@ -54,12 +54,10 @@ public async Task GivenSupportedAcceptHeaders_WhenRetrieveSeries_ThenServerShoul } [Theory] - [InlineData(true, DicomWebConstants.ApplicationDicomMediaType, DicomWebConstants.OriginalDicomTransferSyntax)] // use single part instead of multiple part - [InlineData(false, DicomWebConstants.ApplicationOctetStreamMediaType, DicomWebConstants.OriginalDicomTransferSyntax)] // unsupported media type image/png - [InlineData(false, DicomWebConstants.ApplicationDicomMediaType, "1.2.840.10008.1.2.4.100")] // unsupported media type MPEG2 - public async Task GivenUnsupportedAcceptHeaders_WhenRetrieveSeries_ThenServerShouldReturnNotAcceptable(bool singlePart, string mediaType, string transferSyntax) + [MemberData(nameof(GetVersionsAndUnsupportedAcceptHeadersForStudiesAndSeries))] + public async Task GivenUnsupportedAcceptHeaders_WhenRetrieveSeries_ThenServerShouldReturnNotAcceptable(bool singlePart, string mediaType, string transferSyntax, string versionPath) { - var requestUri = new Uri(string.Format(DicomWebConstants.BaseSeriesUriFormat, TestUidGenerator.Generate(), TestUidGenerator.Generate()), UriKind.Relative); + var requestUri = new Uri(versionPath + string.Format(DicomWebConstants.BaseSeriesUriFormat, TestUidGenerator.Generate(), TestUidGenerator.Generate()), UriKind.Relative); using HttpRequestMessage request = new HttpRequestMessageBuilder().Build(requestUri, singlePart: singlePart, mediaType, transferSyntax); using HttpResponseMessage response = await _client.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); diff --git a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Study.cs b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Study.cs index 17d68cbd9b..cebd72d7c4 100644 --- a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Study.cs +++ b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Study.cs @@ -55,12 +55,10 @@ public async Task GivenSupportedAcceptHeaders_WhenRetrieveStudy_ThenServerShould } [Theory] - [InlineData(true, DicomWebConstants.ApplicationDicomMediaType, DicomWebConstants.OriginalDicomTransferSyntax)] // use single part instead of multiple part - [InlineData(false, DicomWebConstants.ApplicationOctetStreamMediaType, DicomWebConstants.OriginalDicomTransferSyntax)] // unsupported media type image/png - [InlineData(false, DicomWebConstants.ApplicationDicomMediaType, "1.2.840.10008.1.2.4.100")] // unsupported media type MPEG2 - public async Task GivenUnsupportedAcceptHeaders_WhenRetrieveStudy_ThenServerShouldReturnNotAcceptable(bool singlePart, string mediaType, string transferSyntax) + [MemberData(nameof(GetVersionsAndUnsupportedAcceptHeadersForStudiesAndSeries))] + public async Task GivenUnsupportedAcceptHeaders_WhenRetrieveStudy_ThenServerShouldReturnNotAcceptable(bool singlePart, string mediaType, string transferSyntax, string versionPath) { - var requestUri = new Uri(string.Format(DicomWebConstants.BaseStudyUriFormat, TestUidGenerator.Generate()), UriKind.Relative); + var requestUri = new Uri(versionPath + string.Format(DicomWebConstants.BaseStudyUriFormat, TestUidGenerator.Generate()), UriKind.Relative); using HttpRequestMessage request = new HttpRequestMessageBuilder().Build(requestUri, singlePart: singlePart, mediaType, transferSyntax); using HttpResponseMessage response = await _client.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); diff --git a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/StoreTransactionTests.cs b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/StoreTransactionTests.cs index 570af63f77..d16104adac 100644 --- a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/StoreTransactionTests.cs +++ b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/StoreTransactionTests.cs @@ -4,6 +4,7 @@ // ------------------------------------------------------------------------------------------------- using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -25,7 +26,7 @@ public class StoreTransactionTests : IClassFixture( - () => _client.StoreAsync(StudiesUri, multiContent)); + () => _client.StoreAsync(new Uri(version + "/" + StudiesUriString, UriKind.Relative), multiContent)); Assert.Equal(HttpStatusCode.Conflict, exception.StatusCode); } - [Fact] - public async Task GivenAMultipartRequestWithAnInvalidMultipartSection_WhenStoring_TheServerShouldReturnAccepted() + [Theory] + [ClassData(typeof(VersionAPIData))] + public async Task GivenAMultipartRequestWithAnInvalidMultipartSection_WhenStoring_TheServerShouldReturnAccepted(string version) { var multiContent = new MultipartContent("related"); multiContent.Headers.ContentType.Parameters.Add(new System.Net.Http.Headers.NameValueHeaderValue("type", $"\"{DicomWebConstants.MediaTypeApplicationDicom.MediaType}\"")); @@ -137,13 +141,13 @@ public async Task GivenAMultipartRequestWithAnInvalidMultipartSection_WhenStorin multiContent.Add(validByteContent); } - using DicomWebResponse response = await _client.StoreAsync(StudiesUri, multiContent); + using DicomWebResponse response = await _client.StoreAsync(new Uri(version + "/" + StudiesUriString, UriKind.Relative), multiContent); Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); ValidationHelpers.ValidateReferencedSopSequence( await response.GetValueAsync(), - ConvertToReferencedSopSequenceEntry(validFile.Dataset)); + ConvertToReferencedSopSequenceEntry(validFile.Dataset, version)); } finally { @@ -151,8 +155,9 @@ await response.GetValueAsync(), } } - [Fact] - public async Task GivenAMultipartRequestWithTypeParameterAndFirstSectionWithoutContentType_WhenStoring_TheServerShouldReturnOK() + [Theory] + [ClassData(typeof(VersionAPIData))] + public async Task GivenAMultipartRequestWithTypeParameterAndFirstSectionWithoutContentType_WhenStoring_TheServerShouldReturnOK(string version) { var multiContent = new MultipartContent("related"); multiContent.Headers.ContentType.Parameters.Add(new System.Net.Http.Headers.NameValueHeaderValue("type", $"\"{DicomWebConstants.MediaTypeApplicationDicom.MediaType}\"")); @@ -171,13 +176,13 @@ public async Task GivenAMultipartRequestWithTypeParameterAndFirstSectionWithoutC multiContent.Add(byteContent); } - using DicomWebResponse response = await _client.StoreAsync(StudiesUri, multiContent); + using DicomWebResponse response = await _client.StoreAsync(new Uri(version + "/" + StudiesUriString, UriKind.Relative), multiContent); Assert.Equal(HttpStatusCode.OK, response.StatusCode); ValidationHelpers.ValidateReferencedSopSequence( await response.GetValueAsync(), - ConvertToReferencedSopSequenceEntry(dicomFile.Dataset)); + ConvertToReferencedSopSequenceEntry(dicomFile.Dataset, version)); } finally { @@ -234,7 +239,7 @@ public async Task GivenOneDifferentStudyInstanceUID_WhenStoringWithProvidedStudy ValidationHelpers.ValidateReferencedSopSequence( dataset, - ConvertToReferencedSopSequenceEntry(dicomFile1.Dataset)); + ConvertToReferencedSopSequenceEntry(dicomFile1.Dataset, null)); ValidationHelpers.ValidateFailedSopSequence( dataset, @@ -282,7 +287,7 @@ public async Task GivenExistingDataset_WhenStoring_TheServerShouldReturnConflict ValidationHelpers.ValidateReferencedSopSequence( dataset, - ConvertToReferencedSopSequenceEntry(dicomFile1.Dataset)); + ConvertToReferencedSopSequenceEntry(dicomFile1.Dataset, null)); Assert.False(dataset.TryGetSequence(DicomTag.FailedSOPSequence, out DicomSequence _)); @@ -359,13 +364,25 @@ public async Task StoreSinglepartWithStudyUID_ServerShouldReturnOK() Assert.Equal(HttpStatusCode.OK, response.StatusCode); } - private (string SopInstanceUid, string RetrieveUri, string SopClassUid) ConvertToReferencedSopSequenceEntry(DicomDataset dicomDataset) + public static IEnumerable GetVersionsAndIncorrectAcceptHeaders + { + get + { + foreach (object[] version in VersionAPIData.VersionSegmentData) + { + yield return new object[] { version[0], "application/dicom" }; + yield return new object[] { version[0], "application/data" }; + } + } + } + + private (string SopInstanceUid, string RetrieveUri, string SopClassUid) ConvertToReferencedSopSequenceEntry(DicomDataset dicomDataset, string version) { string studyInstanceUid = dicomDataset.GetSingleValue(DicomTag.StudyInstanceUID); string seriesInstanceUid = dicomDataset.GetSingleValue(DicomTag.SeriesInstanceUID); string sopInstanceUid = dicomDataset.GetSingleValue(DicomTag.SOPInstanceUID); - string relativeUri = $"/studies/{studyInstanceUid}/series/{seriesInstanceUid}/instances/{sopInstanceUid}"; + string relativeUri = $"{version}/studies/{studyInstanceUid}/series/{seriesInstanceUid}/instances/{sopInstanceUid}"; return (dicomDataset.GetSingleValue(DicomTag.SOPInstanceUID), new Uri(_client.HttpClient.BaseAddress, relativeUri).ToString(), diff --git a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/VersionAPIData.cs b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/VersionAPIData.cs new file mode 100644 index 0000000000..81a9cb4254 --- /dev/null +++ b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/VersionAPIData.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Health.Dicom.Web.Tests.E2E.Rest +{ + public class VersionAPIData : IEnumerable + { + public static IReadOnlyList VersionSegmentData { get; } = new List + { + new object[] { "" }, + new object[] { "v1.0-prerelease" } + }; + + public IEnumerator GetEnumerator() => VersionSegmentData.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +}