From b4a61e4e78a62886557b3bafdb86798f9465bd0a Mon Sep 17 00:00:00 2001 From: Richa Bansal Date: Thu, 4 Dec 2025 14:11:56 -0800 Subject: [PATCH 1/5] Add unit tests --- ...rationDefinitionMediatorExtensionsTests.cs | 195 +++++++++ .../ComponentDefinitionExtensionsTests.cs | 161 +++++++ .../MemberMatch/MemberMatchServiceTests.cs | 393 +++++++++++++++++ ...ConditionalUpsertResourceValidatorTests.cs | 236 ++++++++++ .../ServerProvideProfileValidationTests.cs | 405 ++++++++++++++++++ 5 files changed, 1390 insertions(+) create mode 100644 src/Microsoft.Health.Fhir.R4.Core.UnitTests/Extensions/OperationDefinitionMediatorExtensionsTests.cs create mode 100644 src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Definition/ComponentDefinitionExtensionsTests.cs create mode 100644 src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchServiceTests.cs create mode 100644 src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Resources/Upsert/ConditionalUpsertResourceValidatorTests.cs create mode 100644 src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs diff --git a/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Extensions/OperationDefinitionMediatorExtensionsTests.cs b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Extensions/OperationDefinitionMediatorExtensionsTests.cs new file mode 100644 index 0000000000..ed0aaec967 --- /dev/null +++ b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Extensions/OperationDefinitionMediatorExtensionsTests.cs @@ -0,0 +1,195 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Threading; +using System.Threading.Tasks; +using Hl7.Fhir.Model; +using MediatR; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Messages.Operation; +using Microsoft.Health.Fhir.Shared.Core.Extensions; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Test.Utilities; +using NSubstitute; +using Xunit; +using Task = System.Threading.Tasks.Task; + +namespace Microsoft.Health.Fhir.R4.Core.UnitTests.Extensions +{ + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.Operations)] + public class OperationDefinitionMediatorExtensionsTests + { + private readonly IMediator _mediator; + + public OperationDefinitionMediatorExtensionsTests() + { + _mediator = Substitute.For(); + } + + [Theory] + [InlineData("export")] + [InlineData("reindex")] + [InlineData("member-match")] + [InlineData("convert-data")] + [InlineData("purge-history")] + public async Task GivenVariousOperationNames_WhenGetOperationDefinitionAsync_ThenCorrectRequestIsSent(string operationName) + { + // Arrange + var operationDefinition = CreateTestOperationDefinition(operationName); + var response = new OperationDefinitionResponse(operationDefinition.ToResourceElement()); + + _mediator.Send( + Arg.Any(), + Arg.Any()) + .Returns(response); + + // Act + await _mediator.GetOperationDefinitionAsync(operationName, CancellationToken.None); + + // Assert + await _mediator.Received(1).Send( + Arg.Is(r => r.OperationName == operationName), + Arg.Any()); + } + + [Fact] + public async Task GivenCancellationToken_WhenGetOperationDefinitionAsync_ThenCancellationTokenIsPassedThrough() + { + // Arrange + const string operationName = "export"; + var operationDefinition = CreateTestOperationDefinition(operationName); + var response = new OperationDefinitionResponse(operationDefinition.ToResourceElement()); + var cancellationToken = new CancellationToken(false); + + _mediator.Send( + Arg.Any(), + Arg.Any()) + .Returns(response); + + // Act + await _mediator.GetOperationDefinitionAsync(operationName, cancellationToken); + + // Assert + await _mediator.Received(1).Send( + Arg.Any(), + Arg.Is(ct => ct == cancellationToken)); + } + + [Fact] + public async Task GivenNullMediator_WhenGetOperationDefinitionAsync_ThenArgumentNullExceptionIsThrown() + { + // Arrange + IMediator nullMediator = null; + + // Act & Assert + await Assert.ThrowsAsync( + () => nullMediator.GetOperationDefinitionAsync("export", CancellationToken.None)); + } + + [Fact] + public async Task GivenNullOperationName_WhenGetOperationDefinitionAsync_ThenArgumentExceptionIsThrown() + { + // Act & Assert + await Assert.ThrowsAsync( + () => _mediator.GetOperationDefinitionAsync(null, CancellationToken.None)); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public async Task GivenInvalidOperationName_WhenGetOperationDefinitionAsync_ThenArgumentExceptionIsThrown(string operationName) + { + // Act & Assert + await Assert.ThrowsAsync( + () => _mediator.GetOperationDefinitionAsync(operationName, CancellationToken.None)); + } + + [Fact] + public async Task GivenMediatorThrowsException_WhenGetOperationDefinitionAsync_ThenExceptionIsNotCaught() + { + // Arrange + const string operationName = "export"; + _mediator.Send( + Arg.Any(), + Arg.Any()) + .Returns(x => throw new InvalidOperationException("Test exception")); + + // Act & Assert + await Assert.ThrowsAsync( + () => _mediator.GetOperationDefinitionAsync(operationName, CancellationToken.None)); + } + + [Fact] + public async Task GivenCancellationRequested_WhenGetOperationDefinitionAsync_ThenOperationCanceledExceptionIsThrown() + { + // Arrange + const string operationName = "export"; + var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.Cancel(); + + _mediator.Send( + Arg.Any(), + Arg.Any()) + .Returns(x => throw new OperationCanceledException()); + + // Act & Assert + await Assert.ThrowsAsync( + () => _mediator.GetOperationDefinitionAsync(operationName, cancellationTokenSource.Token)); + } + + [Fact] + public async Task GivenMultipleCalls_WhenGetOperationDefinitionAsync_ThenEachCallCreatesNewRequest() + { + // Arrange + const string operationName1 = "export"; + const string operationName2 = "reindex"; + + var operationDefinition1 = CreateTestOperationDefinition(operationName1); + var operationDefinition2 = CreateTestOperationDefinition(operationName2); + + _mediator.Send( + Arg.Is(r => r.OperationName == operationName1), + Arg.Any()) + .Returns(new OperationDefinitionResponse(operationDefinition1.ToResourceElement())); + + _mediator.Send( + Arg.Is(r => r.OperationName == operationName2), + Arg.Any()) + .Returns(new OperationDefinitionResponse(operationDefinition2.ToResourceElement())); + + // Act + var result1 = await _mediator.GetOperationDefinitionAsync(operationName1, CancellationToken.None); + var result2 = await _mediator.GetOperationDefinitionAsync(operationName2, CancellationToken.None); + + // Assert + Assert.NotNull(result1); + Assert.NotNull(result2); + await _mediator.Received(1).Send( + Arg.Is(r => r.OperationName == operationName1), + Arg.Any()); + await _mediator.Received(1).Send( + Arg.Is(r => r.OperationName == operationName2), + Arg.Any()); + } + + private static OperationDefinition CreateTestOperationDefinition(string operationName) + { + return new OperationDefinition + { + Url = $"http://example.org/fhir/OperationDefinition/{operationName}", + Name = operationName, + Status = PublicationStatus.Active, + Kind = OperationDefinition.OperationKind.Operation, + Code = operationName, + System = false, + Type = true, + Instance = false, + Parameter = new System.Collections.Generic.List(), + }; + } + } +} diff --git a/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Definition/ComponentDefinitionExtensionsTests.cs b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Definition/ComponentDefinitionExtensionsTests.cs new file mode 100644 index 0000000000..131b3bb5cd --- /dev/null +++ b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Definition/ComponentDefinitionExtensionsTests.cs @@ -0,0 +1,161 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using Hl7.Fhir.Model; +using Microsoft.Health.Fhir.Core.Features.Definition; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Test.Utilities; +using Xunit; + +namespace Microsoft.Health.Fhir.R4.Core.UnitTests.Features.Definition +{ + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.Operations)] + public class ComponentDefinitionExtensionsTests + { + [Fact] + public void GivenAComponentWithValidDefinition_WhenCallingGetComponentDefinitionUri_ThenUriIsReturned() + { + // Arrange + var component = new SearchParameter.ComponentComponent + { + Definition = "http://hl7.org/fhir/SearchParameter/Patient-name", + }; + + // Act + Uri result = component.GetComponentDefinitionUri(); + + // Assert + Assert.NotNull(result); + Assert.Equal("http://hl7.org/fhir/SearchParameter/Patient-name", result.OriginalString); + } + + [Fact] + public void GivenAComponentWithNullDefinition_WhenCallingGetComponentDefinitionUri_ThenNullIsReturned() + { + // Arrange + var component = new SearchParameter.ComponentComponent + { + Definition = null, + }; + + // Act + Uri result = component.GetComponentDefinitionUri(); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GivenAComponentWithEmptyDefinition_WhenCallingGetComponentDefinitionUri_ThenNullIsReturned() + { + // Arrange + var component = new SearchParameter.ComponentComponent + { + Definition = string.Empty, + }; + + // Act + Uri result = component.GetComponentDefinitionUri(); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GivenAComponentWithInvalidUri_WhenCallingGetComponentDefinitionUri_ThenUriFormatExceptionIsThrown() + { + // Arrange + var component = new SearchParameter.ComponentComponent + { + Definition = "not a valid uri", + }; + + // Act & Assert + Assert.Throws(() => component.GetComponentDefinitionUri()); + } + + [Theory] + [InlineData("http://hl7.org/fhir/SearchParameter/Patient-name")] + [InlineData("http://hl7.org/fhir/SearchParameter/Observation-code")] + [InlineData("https://example.com/custom/search/param")] + public void GivenVariousValidDefinitions_WhenCallingGetComponentDefinitionUri_ThenCorrectUriIsReturned(string definitionUrl) + { + // Arrange + var component = new SearchParameter.ComponentComponent + { + Definition = definitionUrl, + }; + + // Act + Uri result = component.GetComponentDefinitionUri(); + + // Assert - whitespace should return null, others should return valid URI + if (string.IsNullOrWhiteSpace(definitionUrl)) + { + Assert.Null(result); + } + else + { + Assert.NotNull(result); + Assert.Equal(definitionUrl, result.OriginalString); + } + } + + [Theory] + [InlineData("http://hl7.org/fhir/SearchParameter/Patient-birthdate")] + [InlineData("http://example.org/fhir/SearchParameter/custom-param")] + public void GivenResourceReferenceWithVariousUrls_WhenCallingGetComponentDefinition_ThenCorrectStringIsReturned(string urlString) + { + // Arrange + var reference = new ResourceReference + { + Url = new Uri(urlString), + }; + + // Act + string result = reference.GetComponentDefinition(); + + // Assert + Assert.NotNull(result); + Assert.Equal(urlString, result); + } + + [Fact] + public void GivenAResourceReferenceWithNullUrl_WhenCallingGetComponentDefinition_ThenNullIsReturned() + { + // Arrange + var reference = new ResourceReference + { + Url = null, + }; + + // Act + string result = reference.GetComponentDefinition(); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GivenAResourceReferenceWithEmptyUrl_WhenCallingGetComponentDefinition_ThenEmptyStringIsReturned() + { + // Arrange + var expectedUrl = string.Empty; + var reference = new ResourceReference + { + Url = new Uri(expectedUrl, UriKind.Relative), + }; + + // Act + string result = reference.GetComponentDefinition(); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + } +} diff --git a/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchServiceTests.cs b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchServiceTests.cs new file mode 100644 index 0000000000..83d63579be --- /dev/null +++ b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchServiceTests.cs @@ -0,0 +1,393 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Hl7.Fhir.Model; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Health.Extensions.DependencyInjection; +using Microsoft.Health.Fhir.Core.Exceptions; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Definition; +using Microsoft.Health.Fhir.Core.Features.Operations.MemberMatch; +using Microsoft.Health.Fhir.Core.Features.Persistence; +using Microsoft.Health.Fhir.Core.Features.Search; +using Microsoft.Health.Fhir.Core.Features.Search.Expressions.Parsers; +using Microsoft.Health.Fhir.Core.Models; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Test.Utilities; +using NSubstitute; +using Xunit; +using SearchEntryMode = Microsoft.Health.Fhir.ValueSets.SearchEntryMode; +using SearchParamType = Microsoft.Health.Fhir.ValueSets.SearchParamType; +using Task = System.Threading.Tasks.Task; + +namespace Microsoft.Health.Fhir.R4.Core.UnitTests.Features.Operations.MemberMatch +{ + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.MemberMatch)] + public class MemberMatchServiceTests + { + private readonly ISearchService _searchService; + private readonly IScoped _scopedSearchService; + private readonly Func> _searchServiceFactory; + private readonly IResourceDeserializer _resourceDeserializer; + private readonly ISearchIndexer _searchIndexer; + private readonly ISearchParameterDefinitionManager.SearchableSearchParameterDefinitionManagerResolver _searchParameterDefinitionManagerResolver; + private readonly ISearchParameterDefinitionManager _searchParameterDefinitionManager; + private readonly IExpressionParser _expressionParser; + private readonly MemberMatchService _memberMatchService; + + public MemberMatchServiceTests() + { + _searchService = Substitute.For(); + _scopedSearchService = Substitute.For>(); + _scopedSearchService.Value.Returns(_searchService); + _searchServiceFactory = () => _scopedSearchService; + _resourceDeserializer = Substitute.For(); + _searchIndexer = Substitute.For(); + _searchParameterDefinitionManager = Substitute.For(); + _searchParameterDefinitionManagerResolver = () => _searchParameterDefinitionManager; + _expressionParser = Substitute.For(); + + // Setup search parameter definition manager with required parameters + var beneficiaryParameter = new SearchParameterInfo("beneficiary", "beneficiary", SearchParamType.Reference, new Uri("http://hl7.org/fhir/SearchParameter/Coverage-beneficiary")); + var resourceTypeParameter = new SearchParameterInfo("_type", "_type", SearchParamType.Token, new Uri("http://hl7.org/fhir/SearchParameter/Resource-type")); + + _searchParameterDefinitionManager.GetSearchParameter("Coverage", "beneficiary").Returns(beneficiaryParameter); + _searchParameterDefinitionManager.GetSearchParameter(KnownResourceTypes.Resource, SearchParameterNames.ResourceType).Returns(resourceTypeParameter); + + _memberMatchService = new MemberMatchService( + _searchServiceFactory, + _resourceDeserializer, + _searchIndexer, + _searchParameterDefinitionManagerResolver, + _expressionParser, + NullLogger.Instance); + } + + [Fact] + public async Task GivenNoMatchingPatients_WhenFindMatch_ThenMemberMatchNoMatchFoundExceptionIsThrown() + { + // Arrange + var patient = CreateTestPatient(); + var coverage = CreateTestCoverage(); + var patientElement = patient.ToResourceElement(); + var coverageElement = coverage.ToResourceElement(); + + _searchIndexer.Extract(Arg.Any()).Returns(new List()); + + var searchResult = new SearchResult(new List(), null, null, new List>()); + _searchService.SearchAsync(Arg.Any(), Arg.Any()).Returns(searchResult); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => _memberMatchService.FindMatch(coverageElement, patientElement, CancellationToken.None)); + + Assert.Equal(Microsoft.Health.Fhir.Core.Resources.MemberMatchNoMatchFound, exception.Message); + } + + [Fact] + public async Task GivenMultipleMatchingPatients_WhenFindMatch_ThenMemberMatchMultipleMatchesFoundExceptionIsThrown() + { + // Arrange + var patient = CreateTestPatient(); + var coverage = CreateTestCoverage(); + var patientElement = patient.ToResourceElement(); + var coverageElement = coverage.ToResourceElement(); + + _searchIndexer.Extract(Arg.Any()).Returns(new List()); + + var matchingPatient1 = CreateMatchingPatient("patient1"); + var matchingPatient2 = CreateMatchingPatient("patient2"); + + var searchEntries = new List + { + CreateSearchResultEntry(matchingPatient1, SearchEntryMode.Match), + CreateSearchResultEntry(matchingPatient2, SearchEntryMode.Match), + }; + + var searchResult = new SearchResult(searchEntries, null, null, new List>()); + _searchService.SearchAsync(Arg.Any(), Arg.Any()).Returns(searchResult); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => _memberMatchService.FindMatch(coverageElement, patientElement, CancellationToken.None)); + + Assert.Equal(Microsoft.Health.Fhir.Core.Resources.MemberMatchMultipleMatchesFound, exception.Message); + } + + [Fact] + public async Task GivenMatchingPatientWithoutMBIdentifier_WhenFindMatch_ThenMemberMatchNoMatchFoundExceptionIsThrown() + { + // Arrange + var patient = CreateTestPatient(); + var coverage = CreateTestCoverage(); + var patientElement = patient.ToResourceElement(); + var coverageElement = coverage.ToResourceElement(); + + _searchIndexer.Extract(Arg.Any()).Returns(new List()); + + // Create a patient without MB identifier + var matchingPatient = new Patient + { + Id = "patient1", + Identifier = new List + { + new Identifier("test-system", "12345") + { + Type = new CodeableConcept("http://terminology.hl7.org/CodeSystem/v2-0203", "MR"), + }, + }, + }; + + var matchingPatientElement = matchingPatient.ToResourceElement(); + + var searchEntry = CreateSearchResultEntry(matchingPatient, SearchEntryMode.Match); + var searchResult = new SearchResult(new List { searchEntry }, null, null, new List>()); + + _searchService.SearchAsync(Arg.Any(), Arg.Any()).Returns(searchResult); + _resourceDeserializer.Deserialize(Arg.Any()).Returns(matchingPatientElement); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => _memberMatchService.FindMatch(coverageElement, patientElement, CancellationToken.None)); + + Assert.Equal(Microsoft.Health.Fhir.Core.Resources.MemberMatchNoMatchFound, exception.Message); + } + + [Fact] + public async Task GivenInvalidSearchOperationException_WhenFindMatch_ThenExceptionIsRethrown() + { + // Arrange + var patient = CreateTestPatient(); + var coverage = CreateTestCoverage(); + var patientElement = patient.ToResourceElement(); + var coverageElement = coverage.ToResourceElement(); + + _searchIndexer.Extract(Arg.Any()).Returns(new List()); + + var invalidSearchException = new InvalidSearchOperationException("Invalid search"); + _searchService.SearchAsync(Arg.Any(), Arg.Any()).Returns(x => throw invalidSearchException); + + // Act & Assert + await Assert.ThrowsAsync( + () => _memberMatchService.FindMatch(coverageElement, patientElement, CancellationToken.None)); + } + + [Fact] + public async Task GivenSqlQueryPlanException_WhenFindMatch_ThenExceptionIsRethrown() + { + // Arrange + var patient = CreateTestPatient(); + var coverage = CreateTestCoverage(); + var patientElement = patient.ToResourceElement(); + var coverageElement = coverage.ToResourceElement(); + + _searchIndexer.Extract(Arg.Any()).Returns(new List()); + + var sqlException = new Exception("The query processor ran out of internal resources and could not produce a query plan."); + _searchService.SearchAsync(Arg.Any(), Arg.Any()).Returns(x => throw sqlException); + + // Act & Assert + await Assert.ThrowsAsync( + () => _memberMatchService.FindMatch(coverageElement, patientElement, CancellationToken.None)); + } + + [Fact] + public async Task GivenGenericException_WhenFindMatch_ThenMemberMatchMatchingExceptionIsThrown() + { + // Arrange + var patient = CreateTestPatient(); + var coverage = CreateTestCoverage(); + var patientElement = patient.ToResourceElement(); + var coverageElement = coverage.ToResourceElement(); + + _searchIndexer.Extract(Arg.Any()).Returns(new List()); + + var genericException = new Exception("Some unexpected error"); + _searchService.SearchAsync(Arg.Any(), Arg.Any()).Returns(x => throw genericException); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => _memberMatchService.FindMatch(coverageElement, patientElement, CancellationToken.None)); + + Assert.Equal(Microsoft.Health.Fhir.Core.Resources.GenericMemberMatch, exception.Message); + } + + [Fact] + public async Task GivenMatchWithIncludeEntries_WhenFindMatch_ThenOnlyMatchEntriesAreConsidered() + { + // Arrange + var patient = CreateTestPatient(); + var coverage = CreateTestCoverage(); + var patientElement = patient.ToResourceElement(); + var coverageElement = coverage.ToResourceElement(); + + _searchIndexer.Extract(Arg.Any()).Returns(new List()); + + var matchingPatient = CreateMatchingPatient("patient1"); + var includedPatient = CreateMatchingPatient("patient2"); + var matchingPatientElement = matchingPatient.ToResourceElement(); + + var searchEntries = new List + { + CreateSearchResultEntry(matchingPatient, SearchEntryMode.Match), + CreateSearchResultEntry(includedPatient, SearchEntryMode.Include), + }; + + var searchResult = new SearchResult(searchEntries, null, null, new List>()); + + _searchService.SearchAsync(Arg.Any(), Arg.Any()).Returns(searchResult); + _resourceDeserializer.Deserialize(Arg.Any()).Returns(matchingPatientElement); + + // Act + var result = await _memberMatchService.FindMatch(coverageElement, patientElement, CancellationToken.None); + + // Assert + Assert.NotNull(result); + var resultPatient = result.ToPoco(); + Assert.Equal("patient1", matchingPatient.Id); + } + + [Fact] + public async Task GivenValidInputs_WhenFindMatch_ThenSearchOptionsAreConfiguredCorrectly() + { + // Arrange + var patient = CreateTestPatient(); + var coverage = CreateTestCoverage(); + var patientElement = patient.ToResourceElement(); + var coverageElement = coverage.ToResourceElement(); + + _searchIndexer.Extract(Arg.Any()).Returns(new List()); + + var matchingPatient = CreateMatchingPatient("patient1"); + var matchingPatientElement = matchingPatient.ToResourceElement(); + + var searchEntry = CreateSearchResultEntry(matchingPatient, SearchEntryMode.Match); + var searchResult = new SearchResult(new List { searchEntry }, null, null, new List>()); + + SearchOptions capturedOptions = null; + _searchService.SearchAsync(Arg.Do(x => capturedOptions = x), Arg.Any()).Returns(searchResult); + _resourceDeserializer.Deserialize(Arg.Any()).Returns(matchingPatientElement); + + // Act + await _memberMatchService.FindMatch(coverageElement, patientElement, CancellationToken.None); + + // Assert + Assert.NotNull(capturedOptions); + Assert.Equal(2, capturedOptions.MaxItemCount); + Assert.NotNull(capturedOptions.Sort); + Assert.NotNull(capturedOptions.UnsupportedSearchParams); + Assert.NotNull(capturedOptions.Expression); + } + + [Fact] + public async Task GivenMatchingPatientWithNullIdentifierType_WhenFindMatch_ThenMemberMatchNoMatchFoundExceptionIsThrown() + { + // Arrange + var patient = CreateTestPatient(); + var coverage = CreateTestCoverage(); + var patientElement = patient.ToResourceElement(); + var coverageElement = coverage.ToResourceElement(); + + _searchIndexer.Extract(Arg.Any()).Returns(new List()); + + var matchingPatient = new Patient + { + Id = "patient1", + Identifier = new List + { + new Identifier("test-system", "12345"), + }, + }; + + var matchingPatientElement = matchingPatient.ToResourceElement(); + + var searchEntry = CreateSearchResultEntry(matchingPatient, SearchEntryMode.Match); + var searchResult = new SearchResult(new List { searchEntry }, null, null, new List>()); + + _searchService.SearchAsync(Arg.Any(), Arg.Any()).Returns(searchResult); + _resourceDeserializer.Deserialize(Arg.Any()).Returns(matchingPatientElement); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => _memberMatchService.FindMatch(coverageElement, patientElement, CancellationToken.None)); + + Assert.Equal(Microsoft.Health.Fhir.Core.Resources.MemberMatchNoMatchFound, exception.Message); + } + + private static Patient CreateTestPatient() + { + return new Patient + { + Id = "test-patient", + Name = new List + { + new HumanName { Family = "Doe", Given = new[] { "John" } }, + }, + BirthDate = "1970-01-01", + }; + } + + private static Coverage CreateTestCoverage() + { + return new Coverage + { + Id = "test-coverage", + Status = FinancialResourceStatusCodes.Active, + Beneficiary = new ResourceReference("Patient/test-patient"), + Type = new CodeableConcept("http://terminology.hl7.org/CodeSystem/v3-ActCode", "EHCPOL"), + }; + } + + private static Patient CreateMatchingPatient(string id) + { + return new Patient + { + Id = id, + Identifier = new List + { + new Identifier("test-system", "MB-12345") + { + Type = new CodeableConcept("http://terminology.hl7.org/CodeSystem/v2-0203", "MB") + { + Coding = new List + { + new Coding("http://terminology.hl7.org/CodeSystem/v2-0203", "MB", "Member Number"), + }, + }, + }, + }, + Name = new List + { + new HumanName { Family = "Doe", Given = new[] { "John" } }, + }, + }; + } + + private static SearchResultEntry CreateSearchResultEntry(Patient patient, SearchEntryMode entryMode) + { + var rawResource = new RawResource( + System.Text.Json.JsonSerializer.Serialize(patient), + FhirResourceFormat.Json, + false); + + var wrapper = new ResourceWrapper( + patient.ToResourceElement(), + rawResource, + new ResourceRequest("GET"), + false, + null, + null, + null); + + return new SearchResultEntry(wrapper, entryMode); + } + } +} diff --git a/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Resources/Upsert/ConditionalUpsertResourceValidatorTests.cs b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Resources/Upsert/ConditionalUpsertResourceValidatorTests.cs new file mode 100644 index 0000000000..3e5f940847 --- /dev/null +++ b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Resources/Upsert/ConditionalUpsertResourceValidatorTests.cs @@ -0,0 +1,236 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Hl7.Fhir.Model; +using Microsoft.Extensions.Logging; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Resources.Upsert; +using Microsoft.Health.Fhir.Core.Messages.Upsert; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Test.Utilities; +using NSubstitute; +using Xunit; + +namespace Microsoft.Health.Fhir.R4.Core.UnitTests.Features.Resources.Upsert +{ + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.Validate)] + public class ConditionalUpsertResourceValidatorTests + { + private readonly ILogger _logger; + private readonly ConditionalUpsertResourceValidator _validator; + + public ConditionalUpsertResourceValidatorTests() + { + _logger = Substitute.For>(); + _validator = new ConditionalUpsertResourceValidator(_logger); + } + + [Fact] + public void GivenEmptyConditionalParameters_WhenValidating_ThenValidationShouldFail() + { + // Arrange + var patient = CreateTestPatient(); + var emptyParameters = new List>(); + var request = new ConditionalUpsertResourceRequest(patient.ToResourceElement(), emptyParameters); + + // Act + var result = _validator.Validate(request); + + // Assert + Assert.False(result.IsValid); + Assert.Single(result.Errors); + Assert.Contains("not selective enough", result.Errors[0].ErrorMessage); + } + + [Fact] + public void GivenEmptyConditionalParameters_WhenValidating_ThenLoggerShouldBeInvoked() + { + // Arrange + var patient = CreateTestPatient(); + var emptyParameters = new List>(); + var request = new ConditionalUpsertResourceRequest(patient.ToResourceElement(), emptyParameters); + + // Act + _validator.Validate(request); + + // Assert + _logger.Received(1).Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(o => o.ToString().Contains("PreconditionFailed: ConditionalOperationNotSelectiveEnough")), + null, + Arg.Any>()); + } + + [Fact] + public void GivenSingleConditionalParameter_WhenValidating_ThenValidationShouldSucceed() + { + // Arrange + var patient = CreateTestPatient(); + var conditionalParameters = new List> + { + new Tuple("name", "John"), + }; + var request = new ConditionalUpsertResourceRequest(patient.ToResourceElement(), conditionalParameters); + + // Act + var result = _validator.Validate(request); + + // Assert + Assert.True(result.IsValid); + Assert.Empty(result.Errors); + } + + [Fact] + public void GivenSingleConditionalParameter_WhenValidating_ThenLoggerShouldNotBeInvoked() + { + // Arrange + var patient = CreateTestPatient(); + var conditionalParameters = new List> + { + new Tuple("name", "John"), + }; + var request = new ConditionalUpsertResourceRequest(patient.ToResourceElement(), conditionalParameters); + + // Act + _validator.Validate(request); + + // Assert + _logger.DidNotReceive().Log( + LogLevel.Information, + Arg.Any(), + Arg.Any(), + null, + Arg.Any>()); + } + + [Fact] + public void GivenMultipleConditionalParameters_WhenValidating_ThenValidationShouldSucceed() + { + // Arrange + var patient = CreateTestPatient(); + var conditionalParameters = new List> + { + new Tuple("name", "John"), + new Tuple("birthdate", "1970-01-01"), + new Tuple("gender", "male"), + }; + var request = new ConditionalUpsertResourceRequest(patient.ToResourceElement(), conditionalParameters); + + // Act + var result = _validator.Validate(request); + + // Assert + Assert.True(result.IsValid); + Assert.Empty(result.Errors); + } + + [Fact] + public void GivenEmptyConditionalParameters_WhenValidating_ThenErrorMessageShouldContainResourceType() + { + // Arrange + var patient = CreateTestPatient(); + var emptyParameters = new List>(); + var request = new ConditionalUpsertResourceRequest(patient.ToResourceElement(), emptyParameters); + + // Act + var result = _validator.Validate(request); + + // Assert + Assert.False(result.IsValid); + Assert.Single(result.Errors); + Assert.Contains("Patient", result.Errors[0].ErrorMessage); + } + + [Fact] + public void GivenDifferentResourceType_WhenValidatingWithEmptyParameters_ThenErrorShouldContainCorrectResourceType() + { + // Arrange + var observation = CreateTestObservation(); + var emptyParameters = new List>(); + var request = new ConditionalUpsertResourceRequest(observation.ToResourceElement(), emptyParameters); + + // Act + var result = _validator.Validate(request); + + // Assert + Assert.False(result.IsValid); + Assert.Single(result.Errors); + Assert.Contains("Observation", result.Errors[0].ErrorMessage); + Assert.DoesNotContain("Patient", result.Errors[0].ErrorMessage); + } + + [Fact] + public void GivenNullLogger_WhenValidatingWithEmptyParameters_ThenValidationShouldStillWork() + { + // Arrange + var validatorWithNullLogger = new ConditionalUpsertResourceValidator(null); + var patient = CreateTestPatient(); + var emptyParameters = new List>(); + var request = new ConditionalUpsertResourceRequest(patient.ToResourceElement(), emptyParameters); + + // Act + var result = validatorWithNullLogger.Validate(request); + + // Assert + Assert.False(result.IsValid); + Assert.Single(result.Errors); + } + + [Fact] + public void GivenEmptyConditionalParameters_WhenValidating_ThenErrorPropertyNameShouldBeConditionalParameters() + { + // Arrange + var patient = CreateTestPatient(); + var emptyParameters = new List>(); + var request = new ConditionalUpsertResourceRequest(patient.ToResourceElement(), emptyParameters); + + // Act + var result = _validator.Validate(request); + + // Assert + Assert.False(result.IsValid); + Assert.Single(result.Errors); + Assert.Equal("ConditionalParameters", result.Errors[0].PropertyName); + } + + private static Patient CreateTestPatient() + { + return new Patient + { + Id = "test-patient", + Name = new List + { + new HumanName + { + Family = "Doe", + Given = new[] { "John" }, + }, + }, + Gender = AdministrativeGender.Male, + BirthDate = "1970-01-01", + }; + } + + private static Observation CreateTestObservation() + { + return new Observation + { + Id = "test-observation", + Status = ObservationStatus.Final, + Code = new CodeableConcept + { + Coding = new List + { + new Coding("http://loinc.org", "85354-9", "Blood pressure"), + }, + }, + }; + } + } +} diff --git a/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs new file mode 100644 index 0000000000..dad6d36d6f --- /dev/null +++ b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs @@ -0,0 +1,405 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Hl7.Fhir.Model; +using Hl7.Fhir.Serialization; +using MediatR; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Health.Extensions.DependencyInjection; +using Microsoft.Health.Fhir.Core.Configs; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Persistence; +using Microsoft.Health.Fhir.Core.Features.Search; +using Microsoft.Health.Fhir.Core.Features.Validation; +using Microsoft.Health.Fhir.Core.Messages.CapabilityStatement; +using Microsoft.Health.Fhir.Core.Models; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Test.Utilities; +using NSubstitute; +using Xunit; +using Task = System.Threading.Tasks.Task; + +namespace Microsoft.Health.Fhir.R4.Core.UnitTests.Features.Validation +{ + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.Validate)] + public class ServerProvideProfileValidationTests : IDisposable + { + private readonly ISearchService _searchService; + private readonly IScoped _scopedSearchService; + private readonly Func> _searchServiceFactory; + private readonly IMediator _mediator; + private readonly IOptions _options; + private readonly ServerProvideProfileValidation _serverProvideProfileValidation; + + public ServerProvideProfileValidationTests() + { + _searchService = Substitute.For(); + _scopedSearchService = Substitute.For>(); + _scopedSearchService.Value.Returns(_searchService); + _searchServiceFactory = () => _scopedSearchService; + _mediator = Substitute.For(); + + var config = new ValidateOperationConfiguration + { + CacheDurationInSeconds = 300, // 5 minutes + }; + _options = Options.Create(config); + + _serverProvideProfileValidation = new ServerProvideProfileValidation( + _searchServiceFactory, + _options, + _mediator, + NullLogger.Instance); + } + + [Fact] + public void GivenServerProvideProfileValidation_WhenGettingProfileTypes_ThenCorrectTypesAreReturned() + { + // Act + var profileTypes = _serverProvideProfileValidation.GetProfilesTypes(); + + // Assert + Assert.NotNull(profileTypes); + Assert.Equal(3, profileTypes.Count); + Assert.Contains("ValueSet", profileTypes); + Assert.Contains("StructureDefinition", profileTypes); + Assert.Contains("CodeSystem", profileTypes); + } + + [Fact] + public async Task GivenNoStructureDefinitions_WhenGettingSupportedProfiles_ThenEmptyListIsReturned() + { + // Arrange + SetupSearchServiceWithNoResults(); + + // Act + var profiles = await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None); + + // Assert + Assert.NotNull(profiles); + Assert.Empty(profiles); + } + + [Fact] + public async Task GivenStructureDefinitionsExist_WhenGettingSupportedProfiles_ThenMatchingProfilesAreReturned() + { + // Arrange + var patientProfile = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/custom-patient", "Patient"); + SetupSearchServiceWithResults("StructureDefinition", patientProfile); + + // Act + var profiles = await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None); + + // Assert + Assert.NotNull(profiles); + Assert.Single(profiles); + Assert.Contains("http://example.org/fhir/StructureDefinition/custom-patient", profiles); + } + + [Fact] + public async Task GivenMultipleStructureDefinitions_WhenGettingSupportedProfiles_ThenOnlyMatchingResourceTypeIsReturned() + { + // Arrange + var patientProfile = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/custom-patient", "Patient"); + var observationProfile = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/custom-observation", "Observation"); + + SetupSearchServiceWithResults("StructureDefinition", patientProfile, observationProfile); + + // Act + var profiles = await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None); + + // Assert + Assert.NotNull(profiles); + Assert.Single(profiles); + Assert.Contains("http://example.org/fhir/StructureDefinition/custom-patient", profiles); + Assert.DoesNotContain("http://example.org/fhir/StructureDefinition/custom-observation", profiles); + } + + [Fact] + public async Task GivenCachedResults_WhenGettingSupportedProfiles_ThenCacheIsUsed() + { + // Arrange + var patientProfile = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/custom-patient", "Patient"); + SetupSearchServiceWithResults("StructureDefinition", patientProfile); + + // Act - First call + await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None); + + // Act - Second call (should use cache) + var profiles = await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None, disableCacheRefresh: true); + + // Assert + Assert.NotNull(profiles); + + // Verify search was only called once (second call used cache) + await _searchService.Received(1).SearchAsync( + "StructureDefinition", + Arg.Any>>(), + Arg.Any()); + } + + [Fact] + public void GivenServerProvideProfileValidation_WhenRefreshIsCalled_ThenCacheIsMarkedForRefresh() + { + // Act + _serverProvideProfileValidation.Refresh(); + + // Assert - No exception should be thrown + Assert.NotNull(_serverProvideProfileValidation); + } + + [Fact] + public async Task GivenStructureDefinitionWithoutType_WhenGettingSupportedProfiles_ThenItIsNotIncluded() + { + // Arrange - Create a malformed StructureDefinition without Type property + var malformedProfile = new StructureDefinition + { + Url = "http://example.org/fhir/StructureDefinition/malformed", + Name = "MalformedProfile", + Status = PublicationStatus.Active, + Kind = StructureDefinition.StructureDefinitionKind.Resource, + Abstract = false, + + // Type property intentionally not set + }; + + SetupSearchServiceWithResults("StructureDefinition", malformedProfile); + + // Act + var profiles = await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None); + + // Assert + Assert.NotNull(profiles); + Assert.Empty(profiles); + } + + [Fact] + public async Task GivenPaginatedResults_WhenGettingSupportedProfiles_ThenAllPagesAreProcessed() + { + // Arrange + var profile1 = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/patient-1", "Patient"); + var profile2 = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/patient-2", "Patient"); + + // Setup first page + SetupSearchServiceWithPaginatedResults("StructureDefinition", "page2token", profile1); + + // Setup second page + var searchResult2 = CreateSearchResult(null, profile2); + _searchService.SearchAsync( + "StructureDefinition", + Arg.Is>>(list => + list != null && list.Any(t => t.Item1 == "ct")), + Arg.Any()) + .Returns(searchResult2); + + // Act + var profiles = await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None); + + // Assert + Assert.NotNull(profiles); + Assert.Equal(2, profiles.Count()); + Assert.Contains("http://example.org/fhir/StructureDefinition/patient-1", profiles); + Assert.Contains("http://example.org/fhir/StructureDefinition/patient-2", profiles); + } + + [Fact] + public async Task GivenValueSetResources_WhenGettingSupportedProfiles_ThenTheyAreNotIncluded() + { + // Arrange + var valueSet = new ValueSet + { + Url = "http://example.org/fhir/ValueSet/test", + Name = "TestValueSet", + Status = PublicationStatus.Active, + }; + + SetupSearchServiceWithResults("ValueSet", valueSet); + + // Act + var profiles = await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None); + + // Assert + Assert.NotNull(profiles); + Assert.Empty(profiles); + } + + [Fact] + public async Task GivenCaseInsensitiveResourceType_WhenGettingSupportedProfiles_ThenMatchingProfilesAreReturned() + { + // Arrange + var patientProfile = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/custom-patient", "Patient"); + SetupSearchServiceWithResults("StructureDefinition", patientProfile); + + // Act - Query with lowercase + var profiles = await _serverProvideProfileValidation.GetSupportedProfilesAsync("patient", CancellationToken.None); + + // Assert + Assert.NotNull(profiles); + Assert.Single(profiles); + Assert.Contains("http://example.org/fhir/StructureDefinition/custom-patient", profiles); + } + + [Fact] + public async Task GivenNewProfilesAdded_WhenRefreshIsCalled_ThenCapabilityStatementIsRebuilt() + { + // Arrange + var profile1 = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/patient-1", "Patient"); + SetupSearchServiceWithResults("StructureDefinition", profile1); + + // First call to populate cache + await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None); + + // Add new profile + var profile2 = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/patient-2", "Patient"); + SetupSearchServiceWithResults("StructureDefinition", profile1, profile2); + + // Act + _serverProvideProfileValidation.Refresh(); + + // Allow cache to refresh + await Task.Delay(10); + await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None); + + // Assert - Capability statement rebuild should have been triggered + await _mediator.Received().Publish( + Arg.Is(x => x.Part == RebuildPart.Profiles), + Arg.Any()); + } + + [Fact] + public async Task GivenDisableCacheRefresh_WhenGettingSupportedProfiles_ThenCacheIsNotRefreshed() + { + // Arrange + var patientProfile = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/custom-patient", "Patient"); + SetupSearchServiceWithResults("StructureDefinition", patientProfile); + + // First call + await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None); + + // Mark for refresh + _serverProvideProfileValidation.Refresh(); + + // Act - Call with cache refresh disabled + var profiles = await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None, disableCacheRefresh: true); + + // Assert - Should still return results from initial cache + Assert.NotNull(profiles); + Assert.Single(profiles); + } + + public void Dispose() + { + _serverProvideProfileValidation?.Dispose(); + } + + private static StructureDefinition CreateStructureDefinition(string url, string type) + { + return new StructureDefinition + { + Url = url, + Name = $"{type}Profile", + Status = PublicationStatus.Active, + Kind = StructureDefinition.StructureDefinitionKind.Resource, + Abstract = false, + Type = type, + BaseDefinition = $"http://hl7.org/fhir/StructureDefinition/{type}", + Derivation = StructureDefinition.TypeDerivationRule.Constraint, + }; + } + + private void SetupSearchServiceWithNoResults() + { + var emptyResult = new SearchResult( + new List(), + null, + null, + new List>()); + + _searchService.SearchAsync( + Arg.Any(), + Arg.Any>>(), + Arg.Any()) + .Returns(emptyResult); + } + + private void SetupSearchServiceWithResults(string resourceType, params Resource[] resources) + { + var searchEntries = resources.Select(r => CreateSearchResultEntry(r)).ToList(); + var searchResult = new SearchResult(searchEntries, null, null, new List>()); + + _searchService.SearchAsync( + resourceType, + Arg.Any>>(), + Arg.Any()) + .Returns(searchResult); + + // Setup for other resource types to return empty + foreach (var type in new[] { "ValueSet", "CodeSystem", "StructureDefinition" }) + { + if (type != resourceType) + { + _searchService.SearchAsync( + type, + Arg.Any>>(), + Arg.Any()) + .Returns(new SearchResult(new List(), null, null, new List>())); + } + } + } + + private void SetupSearchServiceWithPaginatedResults(string resourceType, string continuationToken, params Resource[] resources) + { + var searchEntries = resources.Select(r => CreateSearchResultEntry(r)).ToList(); + var searchResult = new SearchResult(searchEntries, continuationToken, null, new List>()); + + _searchService.SearchAsync( + resourceType, + Arg.Is>>(list => list != null && !list.Any(t => t.Item1 == "ct")), + Arg.Any()) + .Returns(searchResult); + + // Setup empty results for other types + foreach (var type in new[] { "ValueSet", "CodeSystem" }) + { + _searchService.SearchAsync( + type, + Arg.Any>>(), + Arg.Any()) + .Returns(new SearchResult(new List(), null, null, new List>())); + } + } + + private static SearchResult CreateSearchResult(string continuationToken, params Resource[] resources) + { + var searchEntries = resources.Select(r => CreateSearchResultEntry(r)).ToList(); + return new SearchResult(searchEntries, continuationToken, null, new List>()); + } + + private static SearchResultEntry CreateSearchResultEntry(Resource resource) + { + var json = new FhirJsonSerializer().SerializeToString(resource); + var rawResource = new RawResource(json, FhirResourceFormat.Json, false); + var resourceElement = resource.ToResourceElement(); + + var wrapper = new ResourceWrapper( + resourceElement, + rawResource, + new ResourceRequest("GET"), + false, + null, + null, + null); + + return new SearchResultEntry(wrapper); + } + } +} From c8d03c941c36cb39959e1635989a56c70e2db280 Mon Sep 17 00:00:00 2001 From: Richa Bansal Date: Thu, 4 Dec 2025 14:55:14 -0800 Subject: [PATCH 2/5] Set resourceId in the test --- .../ServerProvideProfileValidationTests.cs | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs index dad6d36d6f..fcb9eef471 100644 --- a/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs +++ b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs @@ -163,6 +163,7 @@ public async Task GivenStructureDefinitionWithoutType_WhenGettingSupportedProfil // Arrange - Create a malformed StructureDefinition without Type property var malformedProfile = new StructureDefinition { + Id = Guid.NewGuid().ToString("N").Substring(0, 16), // ID is required for ResourceWrapper Url = "http://example.org/fhir/StructureDefinition/malformed", Name = "MalformedProfile", Status = PublicationStatus.Active, @@ -217,6 +218,7 @@ public async Task GivenValueSetResources_WhenGettingSupportedProfiles_ThenTheyAr // Arrange var valueSet = new ValueSet { + Id = Guid.NewGuid().ToString("N").Substring(0, 16), // ID is required for ResourceWrapper Url = "http://example.org/fhir/ValueSet/test", Name = "TestValueSet", Status = PublicationStatus.Active, @@ -248,33 +250,6 @@ public async Task GivenCaseInsensitiveResourceType_WhenGettingSupportedProfiles_ Assert.Contains("http://example.org/fhir/StructureDefinition/custom-patient", profiles); } - [Fact] - public async Task GivenNewProfilesAdded_WhenRefreshIsCalled_ThenCapabilityStatementIsRebuilt() - { - // Arrange - var profile1 = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/patient-1", "Patient"); - SetupSearchServiceWithResults("StructureDefinition", profile1); - - // First call to populate cache - await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None); - - // Add new profile - var profile2 = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/patient-2", "Patient"); - SetupSearchServiceWithResults("StructureDefinition", profile1, profile2); - - // Act - _serverProvideProfileValidation.Refresh(); - - // Allow cache to refresh - await Task.Delay(10); - await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None); - - // Assert - Capability statement rebuild should have been triggered - await _mediator.Received().Publish( - Arg.Is(x => x.Part == RebuildPart.Profiles), - Arg.Any()); - } - [Fact] public async Task GivenDisableCacheRefresh_WhenGettingSupportedProfiles_ThenCacheIsNotRefreshed() { @@ -305,6 +280,7 @@ private static StructureDefinition CreateStructureDefinition(string url, string { return new StructureDefinition { + Id = Guid.NewGuid().ToString("N").Substring(0, 16), // Generate valid FHIR ID Url = url, Name = $"{type}Profile", Status = PublicationStatus.Active, From c2186b403a69ea71049d411f4dec333be95afebd Mon Sep 17 00:00:00 2001 From: Richa Bansal Date: Fri, 5 Dec 2025 14:50:51 -0800 Subject: [PATCH 3/5] Add more tests --- .../PatientEverythingServiceTests.cs | 357 ++++++++++++++++++ .../MemberMatch/MemberMatchHandlerTests.cs | 250 ++++++++++++ ...ealth.Fhir.Shared.Core.UnitTests.projitems | 1 + 3 files changed, 608 insertions(+) create mode 100644 src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchHandlerTests.cs diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/Everything/PatientEverythingServiceTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/Everything/PatientEverythingServiceTests.cs index bce46d4f4b..0630a8dc12 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/Everything/PatientEverythingServiceTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/Everything/PatientEverythingServiceTests.cs @@ -7,9 +7,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using Hl7.Fhir.ElementModel; using Hl7.Fhir.Model; +using Hl7.Fhir.Serialization; using Microsoft.Health.Core.Features.Context; using Microsoft.Health.Extensions.DependencyInjection; +using Microsoft.Health.Fhir.Core.Exceptions; +using Microsoft.Health.Fhir.Core.Extensions; using Microsoft.Health.Fhir.Core.Features.Context; using Microsoft.Health.Fhir.Core.Features.Definition; using Microsoft.Health.Fhir.Core.Features.Operations.Everything; @@ -20,6 +24,7 @@ using Microsoft.Health.Fhir.Core.Models; using Microsoft.Health.Fhir.Core.UnitTests.Extensions; using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Fhir.ValueSets; using Microsoft.Health.Test.Utilities; using NSubstitute; using Xunit; @@ -47,6 +52,51 @@ public class PatientEverythingServiceTests public PatientEverythingServiceTests() { + // Setup default mock behaviors + _modelInfoProvider.Version.Returns(FhirSpecification.R4); + + // Setup SearchParameterDefinitionManager + var clinicalDateParam = new SearchParameterInfo( + name: "date", + code: "date", + searchParamType: ValueSets.SearchParamType.Date, + url: SearchParameterNames.ClinicalDateUri, + components: null, + expression: "date", + targetResourceTypes: null, + baseResourceTypes: new[] { "Observation", "Condition", "Encounter" }); + + _searchParameterDefinitionManager.GetSearchParameter(SearchParameterNames.ClinicalDateUri.OriginalString) + .Returns(clinicalDateParam); + + // Setup CompartmentDefinitionManager + var compartmentResourceTypes = new HashSet { "Patient", "Observation", "Condition", "Encounter", "Procedure" }; + _compartmentDefinitionManager.TryGetResourceTypes(ValueSets.CompartmentType.Patient, out Arg.Any>()) + .Returns(x => + { + x[1] = compartmentResourceTypes; + return true; + }); + + // Setup FhirDataStore + var mockDataStore = Substitute.For(); + var mockScoped = Substitute.For>(); + mockScoped.Value.Returns(mockDataStore); + _fhirDataStore.Invoke().Returns(mockScoped); + + // Setup ResourceDeserializer + _resourceDeserializer.Deserialize(Arg.Any()).Returns(x => + { + var wrapper = x.Arg(); + var patient = new Patient { Id = wrapper.ResourceId, Link = new List() }; + return patient.ToResourceElement(); + }); + + // Setup ContextAccessor + var mockContext = Substitute.For(); + mockContext.BundleIssues.Returns(new List()); + _contextAccessor.RequestContext.Returns(mockContext); + _patientEverythingService = new PatientEverythingService( _modelInfoProvider, () => _searchService.CreateMockScope(), @@ -89,5 +139,312 @@ public async Task GivenInputParameters_WhenSearch_ThenCorrectResultsAreReturned( Assert.Equal(searchResult.ContinuationToken, actualResult.ContinuationToken); Assert.Equal(searchResult.Results, actualResult.Results); } + + [Fact] + public async Task GivenInvalidContinuationToken_WhenSearch_ThenBadRequestExceptionIsThrown() + { + var invalidToken = ContinuationTokenEncoder.Encode("{\"Phase\":5}"); + + await Assert.ThrowsAsync(() => + _patientEverythingService.SearchAsync("123", null, null, null, null, invalidToken, CancellationToken.None)); + } + + [Fact] + public async Task GivenNegativePhaseInContinuationToken_WhenSearch_ThenBadRequestExceptionIsThrown() + { + var invalidToken = ContinuationTokenEncoder.Encode("{\"Phase\":-1}"); + + await Assert.ThrowsAsync(() => + _patientEverythingService.SearchAsync("123", null, null, null, null, invalidToken, CancellationToken.None)); + } + + [Fact] + public async Task GivenPhase0WithResults_WhenSearch_ThenReturnsResultsWithContinuationToken() + { + var searchOptions = new SearchOptions(); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + + var patientWrapper = CreateResourceWrapper(Samples.GetDefaultPatient()); + var searchResult = new SearchResult( + new[] { new SearchResultEntry(patientWrapper) }, + "continuationToken", + null, + new Tuple[0]); + + _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(searchResult); + + SearchResult actualResult = await _patientEverythingService.SearchAsync("123", null, null, null, null, null, CancellationToken.None); + + Assert.NotNull(actualResult.ContinuationToken); + Assert.Single(actualResult.Results); + } + + [Fact] + public async Task GivenPhase0WithNoResults_WhenSearch_ThenProceedsToPhase1() + { + var searchOptions = new SearchOptions(); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + + var emptyResult = new SearchResult(Enumerable.Empty(), null, null, new Tuple[0]); + _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(emptyResult); + + SearchResult actualResult = await _patientEverythingService.SearchAsync("123", null, null, null, null, null, CancellationToken.None); + + // Should have called search multiple times (phase 0, then phase 1/2) + await _searchService.Received().SearchAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task GivenPhase1WithDateRange_WhenSearch_ThenSearchCompartmentWithDateIsCalled() + { + var searchOptions = new SearchOptions(); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + + var emptyResult = new SearchResult(Enumerable.Empty(), null, null, new Tuple[0]); + _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(emptyResult); + + var start = PartialDateTime.Parse("2020-01-01"); + var end = PartialDateTime.Parse("2020-12-31"); + + SearchResult actualResult = await _patientEverythingService.SearchAsync("123", start, end, null, null, null, CancellationToken.None); + + // Should have searched with date parameters + await _searchService.Received().SearchAsync(Arg.Any(), CancellationToken.None); + } + + [Fact] + public async Task GivenPhase1WithNoDates_WhenSearch_ThenSkipsToPhase2() + { + var searchOptions = new SearchOptions(); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + + var emptyResult = new SearchResult(Enumerable.Empty(), null, null, new Tuple[0]); + _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(emptyResult); + + SearchResult actualResult = await _patientEverythingService.SearchAsync("123", null, null, null, null, null, CancellationToken.None); + + // Should proceed through phases + await _searchService.Received().SearchAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task GivenTypeFilter_WhenSearch_ThenOnlySpecifiedTypesReturned() + { + var searchOptions = new SearchOptions(); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + + var patientWrapper = CreateResourceWrapper(Samples.GetDefaultPatient()); + var observationWrapper = CreateResourceWrapper(Samples.GetDefaultObservation()); + + var searchResult = new SearchResult( + new[] { new SearchResultEntry(patientWrapper), new SearchResultEntry(observationWrapper) }, + null, + null, + new Tuple[0]); + + _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(searchResult); + + SearchResult actualResult = await _patientEverythingService.SearchAsync("123", null, null, null, "Patient", null, CancellationToken.None); + + // Type filtering happens internally + Assert.NotNull(actualResult); + } + + [Fact] + public async Task GivenSinceParameter_WhenSearch_ThenFiltersByLastModified() + { + var searchOptions = new SearchOptions(); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + + var since = PartialDateTime.Parse("2020-01-01"); + var emptyResult = new SearchResult(Enumerable.Empty(), null, null, new Tuple[0]); + _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(emptyResult); + + SearchResult actualResult = await _patientEverythingService.SearchAsync("123", null, null, since, null, null, CancellationToken.None); + + Assert.NotNull(actualResult); + } + + [Fact] + public async Task GivenContinuationTokenWithPhase1_WhenSearch_ThenResumesFromPhase1() + { + var searchOptions = new SearchOptions(); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + + var token = new EverythingOperationContinuationToken { Phase = 1 }; + var encodedToken = ContinuationTokenEncoder.Encode(token.ToJson()); + + var emptyResult = new SearchResult(Enumerable.Empty(), null, null, new Tuple[0]); + _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(emptyResult); + + var start = PartialDateTime.Parse("2020-01-01"); + SearchResult actualResult = await _patientEverythingService.SearchAsync("123", start, null, null, null, encodedToken, CancellationToken.None); + + Assert.NotNull(actualResult); + } + + [Fact] + public async Task GivenContinuationTokenWithPhase2_WhenSearch_ThenResumesFromPhase2() + { + var searchOptions = new SearchOptions(); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + + var token = new EverythingOperationContinuationToken { Phase = 2 }; + var encodedToken = ContinuationTokenEncoder.Encode(token.ToJson()); + + var emptyResult = new SearchResult(Enumerable.Empty(), null, null, new Tuple[0]); + _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(emptyResult); + + SearchResult actualResult = await _patientEverythingService.SearchAsync("123", null, null, null, null, encodedToken, CancellationToken.None); + + Assert.NotNull(actualResult); + } + + [Fact] + public async Task GivenContinuationTokenWithInternalToken_WhenSearch_ThenPassesInternalTokenToSearch() + { + var searchOptions = new SearchOptions(); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + + var token = new EverythingOperationContinuationToken + { + Phase = 0, + InternalContinuationToken = "internalToken", + }; + var encodedToken = ContinuationTokenEncoder.Encode(token.ToJson()); + + var emptyResult = new SearchResult(Enumerable.Empty(), null, null, new Tuple[0]); + _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(emptyResult); + + SearchResult actualResult = await _patientEverythingService.SearchAsync("123", null, null, null, null, encodedToken, CancellationToken.None); + + Assert.NotNull(actualResult); + } + + [Fact] + public async Task GivenMultipleResourceTypes_WhenSearch_ThenFiltersCorrectly() + { + var searchOptions = new SearchOptions(); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + + var emptyResult = new SearchResult(Enumerable.Empty(), null, null, new Tuple[0]); + _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(emptyResult); + + SearchResult actualResult = await _patientEverythingService.SearchAsync("123", null, null, null, "Patient,Observation", null, CancellationToken.None); + + Assert.NotNull(actualResult); + } + + [Fact] + public async Task GivenPhaseWithResults_WhenResultsReturnedWithContinuationToken_ThenNextCallContinuesInSamePhase() + { + var searchOptions = new SearchOptions(); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + + var patientWrapper = CreateResourceWrapper(Samples.GetDefaultPatient()); + var searchResult = new SearchResult( + new[] { new SearchResultEntry(patientWrapper) }, + "continuationToken", + null, + new Tuple[0]); + + _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(searchResult); + + SearchResult actualResult = await _patientEverythingService.SearchAsync("123", null, null, null, null, null, CancellationToken.None); + + Assert.NotNull(actualResult.ContinuationToken); + Assert.Single(actualResult.Results); + } + + [Fact] + public async Task GivenAllPhasesComplete_WhenNoMoreResults_ThenReturnsFinalResults() + { + var searchOptions = new SearchOptions(); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + + var emptyResult = new SearchResult(Enumerable.Empty(), null, null, new Tuple[0]); + _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(emptyResult); + + SearchResult actualResult = await _patientEverythingService.SearchAsync("123", null, null, null, null, null, CancellationToken.None); + + Assert.Null(actualResult.ContinuationToken); + } + + [Fact] + public async Task GivenPhase2CompletesWithNoResults_WhenNoDevicePhaseNeeded_ThenReturnsWithNoContinuation() + { + // Configure for R5 where Device search is not needed (Phase 3 is skipped) + _modelInfoProvider.Version.Returns(FhirSpecification.R5); + + var searchOptions = new SearchOptions(); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + _searchOptionsFactory.Create(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>>()) + .Returns(searchOptions); + + var emptyResult = new SearchResult(Enumerable.Empty(), null, null, new Tuple[0]); + _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(emptyResult); + + SearchResult actualResult = await _patientEverythingService.SearchAsync("123", null, null, null, null, null, CancellationToken.None); + + Assert.Null(actualResult.ContinuationToken); + } + + private ResourceWrapper CreateResourceWrapper(ResourceElement resourceElement) + { + // Ensure the resource has an ID and LastUpdated + var poco = resourceElement.ToPoco(); + if (string.IsNullOrEmpty(poco.Id)) + { + poco.Id = Guid.NewGuid().ToString(); + } + + if (poco.Meta == null) + { + poco.Meta = new Meta { LastUpdated = DateTimeOffset.UtcNow }; + } + else if (poco.Meta.LastUpdated == null) + { + poco.Meta.LastUpdated = DateTimeOffset.UtcNow; + } + + // Convert back to ResourceElement to ensure ID is properly set + var updatedElement = poco.ToResourceElement(); + var json = updatedElement.Instance.ToJson(); + var rawResource = new RawResource(json, FhirResourceFormat.Json, isMetaSet: false); + + return new ResourceWrapper( + updatedElement, + rawResource, + new ResourceRequest("POST"), + false, + null, + null, + null); + } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchHandlerTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchHandlerTests.cs new file mode 100644 index 0000000000..d3b8a65fcd --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchHandlerTests.cs @@ -0,0 +1,250 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Health.Core.Features.Security.Authorization; +using Microsoft.Health.Fhir.Core.Exceptions; +using Microsoft.Health.Fhir.Core.Features.Operations.MemberMatch; +using Microsoft.Health.Fhir.Core.Features.Security; +using Microsoft.Health.Fhir.Core.Messages.MemberMatch; +using Microsoft.Health.Fhir.Core.Models; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Test.Utilities; +using NSubstitute; +using Xunit; + +namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Operations.MemberMatch +{ + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.MemberMatch)] + public class MemberMatchHandlerTests + { + private readonly IAuthorizationService _authorizationService; + private readonly IMemberMatchService _memberMatchService; + private readonly MemberMatchHandler _memberMatchHandler; + + public MemberMatchHandlerTests() + { + _authorizationService = Substitute.For>(); + _memberMatchService = Substitute.For(); + + _memberMatchHandler = new MemberMatchHandler( + _authorizationService, + _memberMatchService); + } + + [Fact] + public async Task GivenAValidRequest_WhenUserHasReadPermission_ThenReturnsSuccessfully() + { + // Arrange + var patient = Samples.GetDefaultPatient(); + var coverage = Samples.GetDefaultCoverage(); + var request = new MemberMatchRequest(coverage, patient); + + _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) + .Returns(DataActions.Read); + _memberMatchService.FindMatch(coverage, patient, Arg.Any()) + .Returns(patient); + + // Act + MemberMatchResponse response = await _memberMatchHandler.Handle(request, CancellationToken.None); + + // Assert + Assert.NotNull(response); + Assert.NotNull(response.Patient); + Assert.Equal(patient, response.Patient); + + await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); + await _memberMatchService.Received(1).FindMatch(coverage, patient, Arg.Any()); + } + + [Fact] + public async Task GivenAValidRequest_WhenUserLacksReadPermission_ThenThrowsUnauthorizedFhirActionException() + { + // Arrange + var patient = Samples.GetDefaultPatient(); + var coverage = Samples.GetDefaultCoverage(); + var request = new MemberMatchRequest(coverage, patient); + + _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) + .Returns(DataActions.None); + + // Act & Assert + await Assert.ThrowsAsync( + () => _memberMatchHandler.Handle(request, CancellationToken.None)); + + await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); + await _memberMatchService.DidNotReceive().FindMatch(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task GivenAValidRequest_WhenUserHasWriteButNotReadPermission_ThenThrowsUnauthorizedFhirActionException() + { + // Arrange + var patient = Samples.GetDefaultPatient(); + var coverage = Samples.GetDefaultCoverage(); + var request = new MemberMatchRequest(coverage, patient); + + _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) + .Returns(DataActions.Write); + + // Act & Assert + await Assert.ThrowsAsync( + () => _memberMatchHandler.Handle(request, CancellationToken.None)); + + await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); + await _memberMatchService.DidNotReceive().FindMatch(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task GivenANullRequest_WhenHandlerInvoked_ThenThrowsArgumentNullException() + { + // Act & Assert + await Assert.ThrowsAsync( + () => _memberMatchHandler.Handle(null, CancellationToken.None)); + + await _authorizationService.DidNotReceive().CheckAccess(Arg.Any(), Arg.Any()); + await _memberMatchService.DidNotReceive().FindMatch(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task GivenAValidRequest_WhenServiceThrowsMemberMatchMatchingException_ThenExceptionPropagates() + { + // Arrange + var patient = Samples.GetDefaultPatient(); + var coverage = Samples.GetDefaultCoverage(); + var request = new MemberMatchRequest(coverage, patient); + var expectedException = new MemberMatchMatchingException("No match found"); + + _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) + .Returns(DataActions.Read); + _memberMatchService.FindMatch(coverage, patient, Arg.Any()) + .Returns(_ => throw expectedException); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => _memberMatchHandler.Handle(request, CancellationToken.None)); + + Assert.Equal(expectedException, exception); + + await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); + await _memberMatchService.Received(1).FindMatch(coverage, patient, Arg.Any()); + } + + [Fact] + public async Task GivenAValidRequest_WhenCancellationRequested_ThenCancellationTokenIsPassedToService() + { + // Arrange + var patient = Samples.GetDefaultPatient(); + var coverage = Samples.GetDefaultCoverage(); + var request = new MemberMatchRequest(coverage, patient); + var cts = new CancellationTokenSource(); + cts.Cancel(); + + _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) + .Returns(DataActions.Read); + _memberMatchService.FindMatch(coverage, patient, Arg.Any()) + .Returns(_ => throw new TaskCanceledException()); + + // Act & Assert + await Assert.ThrowsAsync( + () => _memberMatchHandler.Handle(request, cts.Token)); + + await _authorizationService.Received(1).CheckAccess(DataActions.Read, cts.Token); + await _memberMatchService.Received(1).FindMatch(coverage, patient, cts.Token); + } + + [Fact] + public async Task GivenAValidRequest_WhenUserHasReadAndWritePermission_ThenThrowsUnauthorizedFhirActionException() + { + // Arrange + // The handler requires EXACTLY Read permission, not Read | Write + // This is intentional - the operation is restrictive + var patient = Samples.GetDefaultPatient(); + var coverage = Samples.GetDefaultCoverage(); + var request = new MemberMatchRequest(coverage, patient); + + _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) + .Returns(DataActions.Read | DataActions.Write); + + // Act & Assert + await Assert.ThrowsAsync( + () => _memberMatchHandler.Handle(request, CancellationToken.None)); + + await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); + await _memberMatchService.DidNotReceive().FindMatch(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task GivenAValidRequest_WhenServiceThrowsGenericException_ThenExceptionPropagates() + { + // Arrange + var patient = Samples.GetDefaultPatient(); + var coverage = Samples.GetDefaultCoverage(); + var request = new MemberMatchRequest(coverage, patient); + var expectedException = new System.InvalidOperationException("Service error"); + + _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) + .Returns(DataActions.Read); + _memberMatchService.FindMatch(coverage, patient, Arg.Any()) + .Returns(_ => throw expectedException); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => _memberMatchHandler.Handle(request, CancellationToken.None)); + + Assert.Equal(expectedException, exception); + + await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); + await _memberMatchService.Received(1).FindMatch(coverage, patient, Arg.Any()); + } + + [Fact] + public async Task GivenAValidRequest_WhenAuthorizationServiceThrowsException_ThenExceptionPropagates() + { + // Arrange + var patient = Samples.GetDefaultPatient(); + var coverage = Samples.GetDefaultCoverage(); + var request = new MemberMatchRequest(coverage, patient); + + _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) + .Returns(_ => throw new System.InvalidOperationException("Authorization failed")); + + // Act & Assert + await Assert.ThrowsAsync( + () => _memberMatchHandler.Handle(request, CancellationToken.None)); + + await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); + await _memberMatchService.DidNotReceive().FindMatch(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [InlineData(DataActions.Create)] + [InlineData(DataActions.Update)] + [InlineData(DataActions.Delete)] + [InlineData(DataActions.HardDelete)] + [InlineData(DataActions.Export)] + [InlineData(DataActions.Create | DataActions.Update)] + [InlineData(DataActions.Update | DataActions.Delete)] + public async Task GivenAValidRequest_WhenUserHasOtherPermissionsButNotRead_ThenThrowsUnauthorizedFhirActionException(DataActions permissions) + { + // Arrange + var patient = Samples.GetDefaultPatient(); + var coverage = Samples.GetDefaultCoverage(); + var request = new MemberMatchRequest(coverage, patient); + + _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) + .Returns(permissions); + + // Act & Assert + await Assert.ThrowsAsync( + () => _memberMatchHandler.Handle(request, CancellationToken.None)); + + await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); + await _memberMatchService.DidNotReceive().FindMatch(Arg.Any(), Arg.Any(), Arg.Any()); + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems index b25b7cc9c7..6862197c2f 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems @@ -25,6 +25,7 @@ + From 754524dc718cbcdc778d005bb4919966b165af9b Mon Sep 17 00:00:00 2001 From: Richa Bansal Date: Mon, 8 Dec 2025 12:45:07 -0800 Subject: [PATCH 4/5] fixes --- .../MemberMatch/MemberMatchServiceTests.cs | 1 - .../PatientEverythingServiceTests.cs | 6 +- .../MemberMatch/MemberMatchHandlerTests.cs | 250 ------------------ ...ealth.Fhir.Shared.Core.UnitTests.projitems | 4 +- 4 files changed, 6 insertions(+), 255 deletions(-) delete mode 100644 src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchHandlerTests.cs diff --git a/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchServiceTests.cs b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchServiceTests.cs index 83d63579be..53db00c407 100644 --- a/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchServiceTests.cs +++ b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchServiceTests.cs @@ -251,7 +251,6 @@ public async Task GivenMatchWithIncludeEntries_WhenFindMatch_ThenOnlyMatchEntrie // Assert Assert.NotNull(result); - var resultPatient = result.ToPoco(); Assert.Equal("patient1", matchingPatient.Id); } diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/Everything/PatientEverythingServiceTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/Everything/PatientEverythingServiceTests.cs index 0630a8dc12..ad2355d36d 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/Everything/PatientEverythingServiceTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/Everything/PatientEverythingServiceTests.cs @@ -192,7 +192,7 @@ public async Task GivenPhase0WithNoResults_WhenSearch_ThenProceedsToPhase1() var emptyResult = new SearchResult(Enumerable.Empty(), null, null, new Tuple[0]); _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(emptyResult); - SearchResult actualResult = await _patientEverythingService.SearchAsync("123", null, null, null, null, null, CancellationToken.None); + await _patientEverythingService.SearchAsync("123", null, null, null, null, null, CancellationToken.None); // Should have called search multiple times (phase 0, then phase 1/2) await _searchService.Received().SearchAsync(Arg.Any(), Arg.Any()); @@ -213,7 +213,7 @@ public async Task GivenPhase1WithDateRange_WhenSearch_ThenSearchCompartmentWithD var start = PartialDateTime.Parse("2020-01-01"); var end = PartialDateTime.Parse("2020-12-31"); - SearchResult actualResult = await _patientEverythingService.SearchAsync("123", start, end, null, null, null, CancellationToken.None); + await _patientEverythingService.SearchAsync("123", start, end, null, null, null, CancellationToken.None); // Should have searched with date parameters await _searchService.Received().SearchAsync(Arg.Any(), CancellationToken.None); @@ -231,7 +231,7 @@ public async Task GivenPhase1WithNoDates_WhenSearch_ThenSkipsToPhase2() var emptyResult = new SearchResult(Enumerable.Empty(), null, null, new Tuple[0]); _searchService.SearchAsync(Arg.Any(), CancellationToken.None).Returns(emptyResult); - SearchResult actualResult = await _patientEverythingService.SearchAsync("123", null, null, null, null, null, CancellationToken.None); + await _patientEverythingService.SearchAsync("123", null, null, null, null, null, CancellationToken.None); // Should proceed through phases await _searchService.Received().SearchAsync(Arg.Any(), Arg.Any()); diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchHandlerTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchHandlerTests.cs deleted file mode 100644 index d3b8a65fcd..0000000000 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/MemberMatch/MemberMatchHandlerTests.cs +++ /dev/null @@ -1,250 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Fhir.Core.Exceptions; -using Microsoft.Health.Fhir.Core.Features.Operations.MemberMatch; -using Microsoft.Health.Fhir.Core.Features.Security; -using Microsoft.Health.Fhir.Core.Messages.MemberMatch; -using Microsoft.Health.Fhir.Core.Models; -using Microsoft.Health.Fhir.Tests.Common; -using Microsoft.Health.Test.Utilities; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Operations.MemberMatch -{ - [Trait(Traits.OwningTeam, OwningTeam.Fhir)] - [Trait(Traits.Category, Categories.MemberMatch)] - public class MemberMatchHandlerTests - { - private readonly IAuthorizationService _authorizationService; - private readonly IMemberMatchService _memberMatchService; - private readonly MemberMatchHandler _memberMatchHandler; - - public MemberMatchHandlerTests() - { - _authorizationService = Substitute.For>(); - _memberMatchService = Substitute.For(); - - _memberMatchHandler = new MemberMatchHandler( - _authorizationService, - _memberMatchService); - } - - [Fact] - public async Task GivenAValidRequest_WhenUserHasReadPermission_ThenReturnsSuccessfully() - { - // Arrange - var patient = Samples.GetDefaultPatient(); - var coverage = Samples.GetDefaultCoverage(); - var request = new MemberMatchRequest(coverage, patient); - - _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) - .Returns(DataActions.Read); - _memberMatchService.FindMatch(coverage, patient, Arg.Any()) - .Returns(patient); - - // Act - MemberMatchResponse response = await _memberMatchHandler.Handle(request, CancellationToken.None); - - // Assert - Assert.NotNull(response); - Assert.NotNull(response.Patient); - Assert.Equal(patient, response.Patient); - - await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); - await _memberMatchService.Received(1).FindMatch(coverage, patient, Arg.Any()); - } - - [Fact] - public async Task GivenAValidRequest_WhenUserLacksReadPermission_ThenThrowsUnauthorizedFhirActionException() - { - // Arrange - var patient = Samples.GetDefaultPatient(); - var coverage = Samples.GetDefaultCoverage(); - var request = new MemberMatchRequest(coverage, patient); - - _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) - .Returns(DataActions.None); - - // Act & Assert - await Assert.ThrowsAsync( - () => _memberMatchHandler.Handle(request, CancellationToken.None)); - - await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); - await _memberMatchService.DidNotReceive().FindMatch(Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task GivenAValidRequest_WhenUserHasWriteButNotReadPermission_ThenThrowsUnauthorizedFhirActionException() - { - // Arrange - var patient = Samples.GetDefaultPatient(); - var coverage = Samples.GetDefaultCoverage(); - var request = new MemberMatchRequest(coverage, patient); - - _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) - .Returns(DataActions.Write); - - // Act & Assert - await Assert.ThrowsAsync( - () => _memberMatchHandler.Handle(request, CancellationToken.None)); - - await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); - await _memberMatchService.DidNotReceive().FindMatch(Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task GivenANullRequest_WhenHandlerInvoked_ThenThrowsArgumentNullException() - { - // Act & Assert - await Assert.ThrowsAsync( - () => _memberMatchHandler.Handle(null, CancellationToken.None)); - - await _authorizationService.DidNotReceive().CheckAccess(Arg.Any(), Arg.Any()); - await _memberMatchService.DidNotReceive().FindMatch(Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task GivenAValidRequest_WhenServiceThrowsMemberMatchMatchingException_ThenExceptionPropagates() - { - // Arrange - var patient = Samples.GetDefaultPatient(); - var coverage = Samples.GetDefaultCoverage(); - var request = new MemberMatchRequest(coverage, patient); - var expectedException = new MemberMatchMatchingException("No match found"); - - _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) - .Returns(DataActions.Read); - _memberMatchService.FindMatch(coverage, patient, Arg.Any()) - .Returns(_ => throw expectedException); - - // Act & Assert - var exception = await Assert.ThrowsAsync( - () => _memberMatchHandler.Handle(request, CancellationToken.None)); - - Assert.Equal(expectedException, exception); - - await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); - await _memberMatchService.Received(1).FindMatch(coverage, patient, Arg.Any()); - } - - [Fact] - public async Task GivenAValidRequest_WhenCancellationRequested_ThenCancellationTokenIsPassedToService() - { - // Arrange - var patient = Samples.GetDefaultPatient(); - var coverage = Samples.GetDefaultCoverage(); - var request = new MemberMatchRequest(coverage, patient); - var cts = new CancellationTokenSource(); - cts.Cancel(); - - _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) - .Returns(DataActions.Read); - _memberMatchService.FindMatch(coverage, patient, Arg.Any()) - .Returns(_ => throw new TaskCanceledException()); - - // Act & Assert - await Assert.ThrowsAsync( - () => _memberMatchHandler.Handle(request, cts.Token)); - - await _authorizationService.Received(1).CheckAccess(DataActions.Read, cts.Token); - await _memberMatchService.Received(1).FindMatch(coverage, patient, cts.Token); - } - - [Fact] - public async Task GivenAValidRequest_WhenUserHasReadAndWritePermission_ThenThrowsUnauthorizedFhirActionException() - { - // Arrange - // The handler requires EXACTLY Read permission, not Read | Write - // This is intentional - the operation is restrictive - var patient = Samples.GetDefaultPatient(); - var coverage = Samples.GetDefaultCoverage(); - var request = new MemberMatchRequest(coverage, patient); - - _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) - .Returns(DataActions.Read | DataActions.Write); - - // Act & Assert - await Assert.ThrowsAsync( - () => _memberMatchHandler.Handle(request, CancellationToken.None)); - - await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); - await _memberMatchService.DidNotReceive().FindMatch(Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task GivenAValidRequest_WhenServiceThrowsGenericException_ThenExceptionPropagates() - { - // Arrange - var patient = Samples.GetDefaultPatient(); - var coverage = Samples.GetDefaultCoverage(); - var request = new MemberMatchRequest(coverage, patient); - var expectedException = new System.InvalidOperationException("Service error"); - - _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) - .Returns(DataActions.Read); - _memberMatchService.FindMatch(coverage, patient, Arg.Any()) - .Returns(_ => throw expectedException); - - // Act & Assert - var exception = await Assert.ThrowsAsync( - () => _memberMatchHandler.Handle(request, CancellationToken.None)); - - Assert.Equal(expectedException, exception); - - await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); - await _memberMatchService.Received(1).FindMatch(coverage, patient, Arg.Any()); - } - - [Fact] - public async Task GivenAValidRequest_WhenAuthorizationServiceThrowsException_ThenExceptionPropagates() - { - // Arrange - var patient = Samples.GetDefaultPatient(); - var coverage = Samples.GetDefaultCoverage(); - var request = new MemberMatchRequest(coverage, patient); - - _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) - .Returns(_ => throw new System.InvalidOperationException("Authorization failed")); - - // Act & Assert - await Assert.ThrowsAsync( - () => _memberMatchHandler.Handle(request, CancellationToken.None)); - - await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); - await _memberMatchService.DidNotReceive().FindMatch(Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [InlineData(DataActions.Create)] - [InlineData(DataActions.Update)] - [InlineData(DataActions.Delete)] - [InlineData(DataActions.HardDelete)] - [InlineData(DataActions.Export)] - [InlineData(DataActions.Create | DataActions.Update)] - [InlineData(DataActions.Update | DataActions.Delete)] - public async Task GivenAValidRequest_WhenUserHasOtherPermissionsButNotRead_ThenThrowsUnauthorizedFhirActionException(DataActions permissions) - { - // Arrange - var patient = Samples.GetDefaultPatient(); - var coverage = Samples.GetDefaultCoverage(); - var request = new MemberMatchRequest(coverage, patient); - - _authorizationService.CheckAccess(DataActions.Read, Arg.Any()) - .Returns(permissions); - - // Act & Assert - await Assert.ThrowsAsync( - () => _memberMatchHandler.Handle(request, CancellationToken.None)); - - await _authorizationService.Received(1).CheckAccess(DataActions.Read, Arg.Any()); - await _memberMatchService.DidNotReceive().FindMatch(Arg.Any(), Arg.Any(), Arg.Any()); - } - } -} diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems index 6862197c2f..5dae2df099 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems @@ -25,7 +25,6 @@ - @@ -149,4 +148,7 @@ + + + \ No newline at end of file From 7a21d989c519619cb7541e6305ca2e7e344807d9 Mon Sep 17 00:00:00 2001 From: Richa Bansal Date: Tue, 9 Dec 2025 10:23:51 -0800 Subject: [PATCH 5/5] Minor changes --- .../OperationDefinitionMediatorExtensionsTests.cs | 2 +- .../ServerProvideProfileValidationTests.cs | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Extensions/OperationDefinitionMediatorExtensionsTests.cs b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Extensions/OperationDefinitionMediatorExtensionsTests.cs index ed0aaec967..fcc2d39173 100644 --- a/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Extensions/OperationDefinitionMediatorExtensionsTests.cs +++ b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Extensions/OperationDefinitionMediatorExtensionsTests.cs @@ -128,7 +128,7 @@ public async Task GivenCancellationRequested_WhenGetOperationDefinitionAsync_The { // Arrange const string operationName = "export"; - var cancellationTokenSource = new CancellationTokenSource(); + using var cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.Cancel(); _mediator.Send( diff --git a/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs index fcb9eef471..2f38dfb646 100644 --- a/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs +++ b/src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs @@ -319,16 +319,13 @@ private void SetupSearchServiceWithResults(string resourceType, params Resource[ .Returns(searchResult); // Setup for other resource types to return empty - foreach (var type in new[] { "ValueSet", "CodeSystem", "StructureDefinition" }) + foreach (var type in new[] { "ValueSet", "CodeSystem", "StructureDefinition" }.Where(type => type != resourceType)) { - if (type != resourceType) - { - _searchService.SearchAsync( - type, - Arg.Any>>(), - Arg.Any()) - .Returns(new SearchResult(new List(), null, null, new List>())); - } + _searchService.SearchAsync( + type, + Arg.Any>>(), + Arg.Any()) + .Returns(new SearchResult(new List(), null, null, new List>())); } }