Skip to content

Commit

Permalink
Removed UID validation when any of the study, series or instance UIDs…
Browse files Browse the repository at this point in the history
… are same (#3257)

* Removed UID validation when any of the UIDs are same

* Fix failed tests

* Changes from review comment, nits

* Changes from review comment, nits

* Added test for V2

* Remove substiture for IElementMinimumValidator, skipping failed tests
  • Loading branch information
arunmk-ms authored Jan 8, 2024
1 parent cad3b8f commit 448cce7
Show file tree
Hide file tree
Showing 10 changed files with 61 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -119,19 +119,19 @@ public async Task GivenARequestWithInvalidSopInstanceIdentifier_WhenRetrievingIn
[InlineData("1", "1", "2")]
[InlineData("1", "2", "1")]
[InlineData("1", "2", "2")]
public async Task GivenRepeatedIdentifiers_WhenRetrievingInstanceMetadata_ThenDicomBadRequestExceptionIsThrownAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid)
public async Task GivenRepeatedIdentifiers_WhenRetrievingInstanceMetadata_ThenResponseMetadataIsReturnedSuccessfully(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid)
{
string ifNoneMatch = null;
const string expectedErrorMessage = "The values for StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID must be unique.";
var request = new RetrieveMetadataRequest(
studyInstanceUid: studyInstanceUid,
seriesInstanceUid: seriesInstanceUid,
sopInstanceUid: sopInstanceUid,
ifNoneMatch: ifNoneMatch);

var ex = await Assert.ThrowsAsync<BadRequestException>(() => _retrieveMetadataHandler.Handle(request, CancellationToken.None));
RetrieveMetadataResponse response = SetupRetrieveMetadataResponse();
_retrieveMetadataService.RetrieveSopInstanceMetadataAsync(studyInstanceUid, seriesInstanceUid, sopInstanceUid).Returns(response);

Assert.Equal(expectedErrorMessage, ex.Message);
await ValidateRetrieveMetadataResponse(response, request);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// -------------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -142,20 +142,20 @@ public async Task GivenNoFrames_WhenRetrievingFrames_ThenDicomBadRequestExceptio
[InlineData("1", "1", "2")]
[InlineData("1", "2", "1")]
[InlineData("1", "2", "2")]
public async Task GivenRepeatedIdentifiers_WhenRetrievingFrames_ThenDicomBadRequestExceptionIsThrownAsync(
public async Task GivenRepeatedIdentifiers_WhenRetrievingFrames_ThenNoExceptionIsThrown(
string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid)
{
const string expectedErrorMessage = "The values for StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID must be unique.";
RetrieveResourceResponse expectedResponse = new RetrieveResourceResponse(Substitute.For<IAsyncEnumerable<RetrieveResourceInstance>>(), KnownContentTypes.ApplicationOctetStream);
var request = new RetrieveResourceRequest(
studyInstanceUid: studyInstanceUid,
seriesInstanceUid: seriesInstanceUid,
sopInstanceUid: sopInstanceUid,
frames: new int[] { 1 },
acceptHeaders: new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() });
_retrieveResourceService.GetInstanceResourceAsync(request, CancellationToken.None).Returns(expectedResponse);

var ex = await Assert.ThrowsAsync<BadRequestException>(() => _retrieveResourceHandler.Handle(request, CancellationToken.None));

Assert.Equal(expectedErrorMessage, ex.Message);
RetrieveResourceResponse response = await _retrieveResourceHandler.Handle(request, CancellationToken.None);
Assert.Same(expectedResponse, response);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,15 +338,20 @@ public async Task GivenNonRequiredTagNull_ExpectTagValidatedAndNoErrorProduced()

[Theory]
[MemberData(nameof(GetDuplicatedDicomIdentifierValues))]
public async Task GivenDuplicatedIdentifiers_WhenValidated_ThenDatasetValidationExceptionShouldBeThrown(string firstDicomTagInString, string secondDicomTagInString)
public async Task GivenDuplicatedIdentifiers_WhenValidated_ThenValidationPasses(string firstDicomTagInString, string secondDicomTagInString)
{
DicomTag firstDicomTag = DicomTag.Parse(firstDicomTagInString);
DicomTag secondDicomTag = DicomTag.Parse(secondDicomTagInString);

string value = _dicomDataset.GetSingleValue<string>(firstDicomTag);
_dicomDataset.AddOrUpdate(secondDicomTag, value);

await ExecuteAndValidateInvalidTagEntries(secondDicomTag);
var result = await _dicomDatasetValidator.ValidateAsync(
_dicomDataset,
null,
new CancellationToken());

Assert.Empty(result.InvalidTagErrors);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class StoreDatasetValidatorTestsV2
private readonly StoreMeter _storeMeter;
private readonly IDicomRequestContextAccessor _dicomRequestContextAccessorV2 = Substitute.For<IDicomRequestContextAccessor>();
private readonly IDicomRequestContext _dicomRequestContextV2 = Substitute.For<IDicomRequestContext>();
private readonly IElementMinimumValidator _minimumValidator = Substitute.For<IElementMinimumValidator>();
private readonly IElementMinimumValidator _minimumValidator = new ElementMinimumValidator();

public StoreDatasetValidatorTestsV2()
{
Expand All @@ -47,7 +47,7 @@ public StoreDatasetValidatorTestsV2()
_dicomDatasetValidator = new StoreDatasetValidator(featureConfiguration, _minimumValidator, _queryTagService, _storeMeter, _dicomRequestContextAccessorV2, NullLogger<StoreDatasetValidator>.Instance);
}

[Fact]
[Fact(Skip = "Issue with minimum validation implementation, Fixing as part of https://github.com/microsoft/dicom-server/pull/3283")]
public async Task GivenFullValidation_WhenPatientIDInvalid_ExpectErrorProduced()
{
// Even when V2 api is requested, if full validation is enabled, we will validate and generate warnings on invalid tags
Expand Down Expand Up @@ -249,7 +249,7 @@ public async Task GivenV2Enabled_WhenNonRequiredTagNull_ExpectTagValidatedAndNoE
Assert.Empty(result.InvalidTagErrors);
}

[Fact]
[Fact(Skip = "Issue with minimum validation implementation, Fixing as part of https://github.com/microsoft/dicom-server/pull/3283")]
public async Task GivenV2Enabled_WhenCoreTagUidWithLeadingZeroes_ExpectTagValidatedAndNoOnlyWarningProduced()
{
// For Core Tag validation like studyInstanceUid, we expect to use minimum validator which is more lenient
Expand Down Expand Up @@ -378,4 +378,32 @@ public async Task GivenAValidDicomDataset_WhenValidated_ThenItShouldSucceed()
Assert.Empty(result.InvalidTagErrors);
Assert.Equal(ValidationWarnings.None, result.WarningCodes);
}

[Theory]
[MemberData(nameof(GetDuplicatedDicomIdentifierValues))]
public async Task GivenDuplicatedIdentifiers_WhenValidated_ThenValidationPasses(string firstDicomTagInString, string secondDicomTagInString)
{
DicomTag firstDicomTag = DicomTag.Parse(firstDicomTagInString);
DicomTag secondDicomTag = DicomTag.Parse(secondDicomTagInString);

string value = _dicomDataset.GetSingleValue<string>(firstDicomTag);
_dicomDataset.AddOrUpdate(secondDicomTag, value);

var result = await _dicomDatasetValidator.ValidateAsync(
_dicomDataset,
null,
new CancellationToken());

Assert.Empty(result.InvalidTagErrors);
}

public static IEnumerable<object[]> GetDuplicatedDicomIdentifierValues()
{
return new List<object[]>
{
new[] { DicomTag.StudyInstanceUID.ToString(), DicomTag.SeriesInstanceUID.ToString() },
new[] { DicomTag.StudyInstanceUID.ToString(), DicomTag.SOPInstanceUID.ToString() },
new[] { DicomTag.SeriesInstanceUID.ToString(), DicomTag.SOPInstanceUID.ToString() },
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// -------------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -51,20 +51,18 @@ public void GivenAnInvalidInstanceIdentifier_WhenValidatedForRequestedResourceTy
[InlineData("1", "1", "2")]
[InlineData("1", "2", "1")]
[InlineData("1", "2", "2")]
public void GivenARequestWithRepeatedIdentifiers_WhenValidatedForRequestedResourceTypeInstance_ThenBadRequestExceptionIsThrown(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid)
public void GivenARequestWithRepeatedIdentifiers_WhenValidatedForRequestedResourceTypeInstance_ThenNoExceptionIsThrown(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid)
{
var ex = Assert.Throws<BadRequestException>(() => RetrieveRequestValidator.ValidateInstanceIdentifiers(ResourceType.Instance, studyInstanceUid, seriesInstanceUid, sopInstanceUid));
Assert.Equal("The values for StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID must be unique.", ex.Message);
RetrieveRequestValidator.ValidateInstanceIdentifiers(ResourceType.Instance, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
}

[Fact]
public void GivenARequestWithRepeatedStudyAndSeriesInstanceIdentifiers_WhenValidatedForRequestedResourceTypeSeries_ThenBadRequestExceptionIsThrown()
public void GivenARequestWithRepeatedStudyAndSeriesInstanceIdentifiers_WhenValidatedForRequestedResourceTypeSeries_ThenNoExceptionIsThrown()
{
string studyInstanceUid = TestUidGenerator.Generate();

// Use same identifier as studyInstanceUid and seriesInstanceUid.
var ex = Assert.Throws<BadRequestException>(() => RetrieveRequestValidator.ValidateInstanceIdentifiers(ResourceType.Series, studyInstanceUid, studyInstanceUid));
Assert.Equal("The values for StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID must be unique.", ex.Message);
RetrieveRequestValidator.ValidateInstanceIdentifiers(ResourceType.Series, studyInstanceUid, studyInstanceUid);
}

[Theory]
Expand Down
14 changes: 3 additions & 11 deletions src/Microsoft.Health.Dicom.Core/DicomCoreResource.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions src/Microsoft.Health.Dicom.Core/DicomCoreResource.resx
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,6 @@
<value>Dicom element '{0}' failed validation for VR '{1}': {2}</value>
<comment>Dicom element {0} of VR {1}. {2} has more detailed message on why.</comment>
</data>
<data name="DuplicatedUidsNotAllowed" xml:space="preserve">
<value>The values for StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID must be unique.</value>
<comment>StudyInstanceUID, SeriesInstanceUID, and SOPInstanceUID are defined by the spec and does not need to be translated.</comment>
</data>
<data name="DuplicateAttribute" xml:space="preserve">
<value>Invalid query: attribute '{0}' has been specified more than once using different ID formats. Each attribute is only allowed to be specified once.</value>
<comment>{0} attribute-id</comment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,23 +140,8 @@ private static void ValidateRequiredCoreTags(DicomDataset dicomDataset, string r

// The format of the identifiers will be validated by fo-dicom.
string studyInstanceUid = EnsureRequiredTagIsPresentWithValue(dicomDataset, DicomTag.StudyInstanceUID);
string seriesInstanceUid = EnsureRequiredTagIsPresentWithValue(dicomDataset, DicomTag.SeriesInstanceUID);
string sopInstanceUid = EnsureRequiredTagIsPresentWithValue(dicomDataset, DicomTag.SOPInstanceUID);

// Ensure the StudyInstanceUid != SeriesInstanceUid != sopInstanceUid
if (studyInstanceUid == seriesInstanceUid ||
studyInstanceUid == sopInstanceUid ||
seriesInstanceUid == sopInstanceUid)
{
var tag = studyInstanceUid == seriesInstanceUid ? DicomTag.SeriesInstanceUID :
studyInstanceUid == sopInstanceUid ? DicomTag.SOPInstanceUID :
seriesInstanceUid == sopInstanceUid ? DicomTag.SOPInstanceUID : null;

throw new DatasetValidationException(
FailureReasonCodes.ValidationFailure,
DicomCoreResource.DuplicatedUidsNotAllowed,
tag);
}
EnsureRequiredTagIsPresentWithValue(dicomDataset, DicomTag.SeriesInstanceUID);
EnsureRequiredTagIsPresentWithValue(dicomDataset, DicomTag.SOPInstanceUID);

// If the requestedStudyInstanceUid is specified, then the StudyInstanceUid must match, ignoring whitespace.
if (requiredStudyInstanceUid != null &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public static void ValidateInstanceIdentifiers(ResourceType resourceType, string
EnsureArg.IsNotNullOrWhiteSpace(studyInstanceUid, nameof(studyInstanceUid));

ValidateInstanceIdentifiersAreValid(resourceType, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
ValidateInstanceIdentifiersAreNotDuplicate(resourceType, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
}

private static void ValidateInstanceIdentifiersAreValid(ResourceType resourceType, string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid)
Expand All @@ -44,30 +43,6 @@ private static void ValidateInstanceIdentifiersAreValid(ResourceType resourceTyp
}
}

private static void ValidateInstanceIdentifiersAreNotDuplicate(ResourceType resourceType, string studyInstanceUid, string seriesInstanceUid = null, string sopInstanceUid = null)
{
switch (resourceType)
{
case ResourceType.Series:
if (studyInstanceUid == seriesInstanceUid)
{
throw new BadRequestException(DicomCoreResource.DuplicatedUidsNotAllowed);
}

break;
case ResourceType.Frames:
case ResourceType.Instance:
if ((studyInstanceUid == seriesInstanceUid) ||
(studyInstanceUid == sopInstanceUid) ||
(seriesInstanceUid == sopInstanceUid))
{
throw new BadRequestException(DicomCoreResource.DuplicatedUidsNotAllowed);
}

break;
}
}

public static void ValidateFrames(IReadOnlyCollection<int> frames)
{
if (frames == null || frames.Count == 0 || frames.Any(x => x < 0))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,21 +243,13 @@ await ResponseHelper.ValidateReferencedSopSequenceAsync(
}

[Fact]
public async Task GivenDatasetWithDuplicateIdentifiers_WhenStoring_TheServerShouldReturnConflict()
public async Task GivenDatasetWithDuplicateIdentifiers_WhenStoring_TheServerShouldReturnAccepted()
{
var studyInstanceUID = TestUidGenerator.Generate();
DicomFile dicomFile1 = Samples.CreateRandomDicomFile(studyInstanceUID, studyInstanceUID);

DicomWebException exception = await Assert.ThrowsAsync<DicomWebException>(() => _client.StoreAsync(dicomFile1));
Assert.Equal(HttpStatusCode.Conflict, exception.StatusCode);

DicomDataset dataset = exception.ResponseDataset;

Assert.False(dataset.TryGetSequence(DicomTag.ReferencedSOPSequence, out DicomSequence _));

ValidationHelpers.ValidateFailedSopSequence(
dataset,
ResponseHelper.ConvertToFailedSopSequenceEntry(dicomFile1.Dataset, ValidationHelpers.ValidationFailedFailureCode));
using DicomWebResponse<DicomDataset> response = await _client.StoreAsync(new[] { dicomFile1, dicomFile1 }, studyInstanceUID);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
}

[Fact]
Expand Down

0 comments on commit 448cce7

Please sign in to comment.