From e62584e2797771dd9100d193e06a36608a51e761 Mon Sep 17 00:00:00 2001 From: Isao Yamauchi Date: Wed, 17 Dec 2025 10:28:23 -0800 Subject: [PATCH 1/5] Code coverage improvement for R4 API part 2. --- .../Controllers/BulkDeleteControllerTests.cs | 375 ++++++++++++++++++ .../FhirJsonOutputFormatterTests.cs | 218 ++++++++++ .../ImproperBehaviorHealthCheckTests.cs | 54 +++ ...Health.Fhir.Shared.Api.UnitTests.projitems | 2 + 4 files changed, 649 insertions(+) create mode 100644 src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/BulkDeleteControllerTests.cs create mode 100644 src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Health/ImproperBehaviorHealthCheckTests.cs diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/BulkDeleteControllerTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/BulkDeleteControllerTests.cs new file mode 100644 index 0000000000..45b36b746d --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/BulkDeleteControllerTests.cs @@ -0,0 +1,375 @@ +// ------------------------------------------------------------------------------------------------- +// 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.Net; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using Hl7.Fhir.Model; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; +using Microsoft.Health.Fhir.Api.Controllers; +using Microsoft.Health.Fhir.Api.Features.ActionResults; +using Microsoft.Health.Fhir.Api.Models; +using Microsoft.Health.Fhir.Core.Exceptions; +using Microsoft.Health.Fhir.Core.Features; +using Microsoft.Health.Fhir.Core.Features.Operations.BulkDelete.Messages; +using Microsoft.Health.Fhir.Core.Features.Routing; +using Microsoft.Health.Fhir.Core.Messages.Delete; +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.Api.UnitTests.Controllers +{ + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.BulkDelete)] + public class BulkDeleteControllerTests + { + private const string OperationResultUrl = "https://fhir/_operations/bulk-delete/0"; + + private readonly BulkDeleteController _controller; + private readonly HttpRequest _httpRequest; + private readonly IMediator _mediator; + + public BulkDeleteControllerTests() + { + _mediator = Substitute.For(); + _mediator.Send( + Arg.Any(), + Arg.Any()) + .Returns(new CreateBulkDeleteResponse(0)); + _mediator.Send( + Arg.Any(), + Arg.Any()) + .Returns(new GetBulkDeleteResponse( + new List(), + new List(), + HttpStatusCode.Accepted)); + _mediator.Send( + Arg.Any(), + Arg.Any()) + .Returns(new CancelBulkDeleteResponse(HttpStatusCode.OK)); + + _httpRequest = Substitute.For(); + _httpRequest.QueryString.Returns(new QueryString(null)); + + var httpContext = Substitute.For(); + httpContext.Request.Returns(_httpRequest); + + var urlResolver = Substitute.For(); + urlResolver.ResolveOperationResultUrl( + Arg.Any(), + Arg.Any()) + .Returns(new Uri(OperationResultUrl)); + + _controller = new BulkDeleteController( + _mediator, + urlResolver); + _controller.ControllerContext = new ControllerContext( + new ActionContext( + httpContext, + new RouteData(), + new ControllerActionDescriptor())); + } + + [Theory] + [InlineData(DeleteOperation.SoftDelete, false, null, null, true)] + [InlineData(DeleteOperation.HardDelete, false, null, null, true)] + [InlineData(DeleteOperation.PurgeHistory, false, null, null, true)] + [InlineData(DeleteOperation.SoftDelete, true, null, null, true)] + [InlineData(DeleteOperation.SoftDelete, false, "t", null, true)] + [InlineData(DeleteOperation.SoftDelete, false, "t1,t2,t3", null, true)] + [InlineData(DeleteOperation.SoftDelete, false, "t1,t2,t3", "?p1=v1&p2=v2&p3=v3", true)] + public async Task GivenParameters_WhenBulkDeleting_ThenBulkDeleteShouldSucceed( + DeleteOperation operation, + bool removeReferences, + string excludedResourceTypes, + string query, + bool valid) + { + await Run( + null, + operation == DeleteOperation.HardDelete, + operation == DeleteOperation.PurgeHistory, + false, + excludedResourceTypes, + removeReferences, + query, + valid, + (model) => _controller.BulkDelete( + model, + operation == DeleteOperation.PurgeHistory, + removeReferences, + excludedResourceTypes)); + } + + [Theory] + [InlineData(KnownResourceTypes.CareTeam, DeleteOperation.SoftDelete, false, "t1,t2,t3", "?p1=v1&p2=v2&p3=v3", true)] + public async Task GivenParameters_WhenBulkDeleting_ThenBulkDeleteByResourceTypeShouldSucceed( + string resoureType, + DeleteOperation operation, + bool removeReferences, + string excludedResourceTypes, + string query, + bool valid) + { + await Run( + resoureType, + operation == DeleteOperation.HardDelete, + operation == DeleteOperation.PurgeHistory, + false, + excludedResourceTypes, + removeReferences, + query, + valid, + (model) => _controller.BulkDeleteByResourceType( + resoureType, + model, + operation == DeleteOperation.PurgeHistory, + removeReferences, + excludedResourceTypes)); + } + + [Theory] + [InlineData(true, null, true)] + [InlineData(false, null, true)] + [InlineData(true, "?_lastUpdated=lt2021-12-12", true)] + [InlineData(true, "?_lastUpdated=lt2021-12-12&_include=DiagnosticReport:based-on:ServiceRequest&_include:iterate=ServiceRequest:encounter", false)] + public async Task GivenParameters_WhenBulkDeleting_ThenBulkDeleteSoftDeletedShouldSucceed( + bool purgeHistory, + string query, + bool valid) + { + await Run( + null, + true, + purgeHistory, + true, + null, + false, + query, + valid, + _ => _controller.BulkDeleteSoftDeleted(purgeHistory)); + } + + [Theory] + [InlineData(KnownResourceTypes.Patient, true, "?_lastUpdated=lt2021-12-12", true)] + public async Task GivenParameters_WhenBulkDeleting_ThenBulkDeleteSoftDeletedByResourceTypeShouldSucceed( + string resourceType, + bool purgeHistory, + string query, + bool valid) + { + await Run( + resourceType, + true, + purgeHistory, + true, + null, + false, + query, + valid, + _ => _controller.BulkDeleteSoftDeletedByResourceType( + resourceType, + purgeHistory)); + } + + [Theory] + [InlineData(0, HttpStatusCode.Accepted)] + [InlineData(1, HttpStatusCode.OK)] + public async Task GivenParameters_WhenGettingStatus_ThenGetBulkDeleteStatusShouldSucceed( + long id, + HttpStatusCode statusCode) + { + var result = new GetBulkDeleteResponse( + null, + null, + statusCode); + _mediator.Send( + Arg.Any(), + Arg.Any()) + .Returns(result); + + var request = default(GetBulkDeleteRequest); + _mediator + .When(x => x.Send(Arg.Any(), Arg.Any())) + .Do(x => request = x.Arg()); + + var response = await _controller.GetBulkDeleteStatusById(id); + Assert.NotNull(request); + Assert.Equal(id, request.JobId); + + Assert.NotNull(response); + Assert.IsType(response); + + var jobResult = (JobResult)response; + if (statusCode == HttpStatusCode.Accepted) + { + Assert.Contains( + jobResult.Headers, + x => + { + return string.Equals(x.Key, KnownHeaders.Progress, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Value.ToString(), Resources.InProgress, StringComparison.OrdinalIgnoreCase); + }); + } + else + { + Assert.DoesNotContain( + jobResult.Headers, + x => + { + return string.Equals(x.Key, KnownHeaders.Progress, StringComparison.OrdinalIgnoreCase); + }); + } + + await _mediator.Received(1).Send( + Arg.Any(), + Arg.Any()); + } + + private async Task Run( + string typeParameter, + bool hardDelete, + bool purgeHistory, + bool softDeleteCleanup, + string excludedResourceTypes, + bool removeReferences, + string query, + bool valid, + Func> func) + { + _httpRequest.QueryString.Returns(new QueryString(query)); + var hardDeleteModel = new HardDeleteModel() + { + HardDelete = hardDelete, + }; + + var request = default(CreateBulkDeleteRequest); + _mediator + .When(x => x.Send(Arg.Any(), Arg.Any())) + .Do(x => request = x.Arg()); + + try + { + var response = await func(hardDeleteModel); + + Assert.True(valid); + Validate( + typeParameter, + hardDelete, + softDeleteCleanup, + purgeHistory, + removeReferences, + excludedResourceTypes, + query, + request); + Validate(response); + } + catch (RequestNotValidException) + { + Assert.False(valid); + } + + await _mediator.Received(valid ? 1 : 0).Send( + Arg.Any(), + Arg.Any()); + } + + private static List> ParseQuery(string query) + { + var result = new List>(); + if (!string.IsNullOrEmpty(query)) + { + var parameters = HttpUtility.ParseQueryString(query); + foreach (var k in parameters.AllKeys) + { + foreach (var p in parameters.GetValues(k)) + { + result.Add(Tuple.Create(k, p)); + } + } + } + + return result; + } + + private static void Validate( + string typeParameter, + bool hardDelete, + bool softDeleteCleanup, + bool purgeHistory, + bool removeReferences, + string excludedResourceTypes, + string query, + CreateBulkDeleteRequest request) + { + Assert.NotNull(request); + + var operation = DeleteOperation.SoftDelete; + if (hardDelete) + { + operation = DeleteOperation.HardDelete; + } + else if (purgeHistory) + { + operation = DeleteOperation.PurgeHistory; + } + + Assert.Equal(operation, request.DeleteOperation); + Assert.Equal(typeParameter, request.ResourceType, StringComparer.OrdinalIgnoreCase); + Assert.Equal(softDeleteCleanup, request.IncludeSoftDeleted); + Assert.Equal(removeReferences, request.RemoveReferences); + + var conditionalParameters = ParseQuery(query); + Assert.Equal(conditionalParameters.Count, request.ConditionalParameters.Count); + Assert.All( + conditionalParameters, + x => + { + Assert.Contains( + request.ConditionalParameters, + y => + { + return string.Equals(x.Item1, y.Item1, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Item2, y.Item2, StringComparison.OrdinalIgnoreCase); + }); + }); + + var resourceTypesExcluded = excludedResourceTypes?.Split(',').ToList() ?? new List(); + Assert.Equal(resourceTypesExcluded.Count, request.ExcludedResourceTypes.Count); + Assert.All( + resourceTypesExcluded, + x => + { + Assert.Contains( + request.ExcludedResourceTypes, + y => + { + return string.Equals(x, y, StringComparison.OrdinalIgnoreCase); + }); + }); + } + + private static void Validate(IActionResult response) + { + Assert.NotNull(response); + Assert.IsType(response); + + var result = (JobResult)response; + Assert.Equal(OperationResultUrl, result.Headers.ContentLocation.ToString()); + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirJsonOutputFormatterTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirJsonOutputFormatterTests.cs index fbaa5eaa85..80f7d70ff4 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirJsonOutputFormatterTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirJsonOutputFormatterTests.cs @@ -5,21 +5,34 @@ using System; using System.Buffers; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Net; using System.Text; +using System.Threading.Tasks; +using System.Web; +using AngleSharp.Dom; using Hl7.Fhir.Model; using Hl7.Fhir.Rest; using Hl7.Fhir.Serialization; +using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Health.Fhir.Api.Features.Formatters; using Microsoft.Health.Fhir.Api.Features.Resources.Bundle; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features; +using Microsoft.Health.Fhir.Core.Features.Persistence; using Microsoft.Health.Fhir.Core.Models; +using Microsoft.Health.Fhir.Shared.Core.Features.Search; using Microsoft.Health.Fhir.Tests.Common; using Microsoft.Health.Test.Utilities; using Newtonsoft.Json.Linq; using NSubstitute; using Xunit; +using static Hl7.Fhir.Model.Bundle; +using Task = System.Threading.Tasks.Task; namespace Microsoft.Health.Fhir.Api.UnitTests.Features.Formatters { @@ -27,6 +40,20 @@ namespace Microsoft.Health.Fhir.Api.UnitTests.Features.Formatters [Trait(Traits.Category, Categories.Web)] public class FhirJsonOutputFormatterTests { + private static readonly Observation[] Resources = new[] + { + new Observation() + { + Id = Guid.NewGuid().ToString(), + }, + new Observation() + { + Id = Guid.NewGuid().ToString(), + }, + }; + + private static readonly FhirJsonParser Parser = new FhirJsonParser(); + [Fact] public void GivenAJObjectAndJsonContentType_WhenCheckingCanWrite_ThenFalseShouldBeReturned() { @@ -51,6 +78,91 @@ public void GivenAResourceWrapperJsonContentType_WhenCheckingCanWrite_ThenTrueSh Assert.True(result); } + [Theory] + [InlineData(false, null)] + [InlineData(false, "?_elements=identifier,status")] + [InlineData(false, "?_summary=text")] + [InlineData(true, null)] + [InlineData(true, "?_elements=identifier,status")] + [InlineData(true, "?_summary=text")] + public async Task GivenContext_WhenWritingResponseBody_ThenBundleShouldBeWrittenCorrectly( + bool raw, + string query) + { + await Run(true, raw, query); + } + + [Theory] + [InlineData(false, null)] + [InlineData(false, "?_elements=identifier,status")] + [InlineData(false, "?_summary=text")] + [InlineData(true, null)] + [InlineData(true, "?_elements=identifier,status")] + [InlineData(true, "?_summary=text")] + public async Task GivenContext_WhenWritingResponseBody_ThenResourceShouldBeWrittenCorrectly( + bool raw, + string query) + { + await Run(false, raw, query); + } + + private static async Task Run( + bool bundle, + bool raw, + string query) + { + using var writer = new StringWriter(new StringBuilder()); + using var body = new MemoryStream(); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.QueryString = new QueryString(query); + httpContext.Response.StatusCode = (int)HttpStatusCode.OK; + httpContext.Response.Body = body; + + var parameters = ParseQuery(query); + var elements = parameters + .Where(x => string.Equals(x.Item1, KnownQueryParameterNames.Elements, StringComparison.OrdinalIgnoreCase)) + .ToList(); + var summary = httpContext.GetSummaryTypeOrDefault(); + var @object = CreateObject( + bundle, + raw); + var objectType = bundle ? typeof(Hl7.Fhir.Model.Bundle) : (raw ? typeof(RawResourceElement) : typeof(Resource)); + var writeContext = new OutputFormatterWriteContext( + httpContext, + (_, _) => writer, + objectType, + @object); + var formatter = new FhirJsonOutputFormatter( + new FhirJsonSerializer(), + Deserializers.ResourceDeserializer, + ArrayPool.Shared, + new BundleSerializer(), + ModelInfoProvider.Instance); + await formatter.WriteResponseBodyAsync(writeContext, Encoding.UTF8); + + var content = default(string); + if (elements.Any() || summary != SummaryType.False || (!bundle && !raw) || (bundle && !raw)) + { + content = writer.ToString(); + } + else + { + body.Seek(0, SeekOrigin.Begin); + using (var reader = new StreamReader(body)) + { + content = await reader.ReadToEndAsync(); + } + } + + Validate( + bundle, + raw, + elements.Any(), + summary, + content); + } + private bool CanRead(Type modelType, string contentType) { var formatter = new FhirJsonOutputFormatter( @@ -72,5 +184,111 @@ private bool CanRead(Type modelType, string contentType) return result; } + + private static object CreateObject(bool bundle, bool raw) + { + var wrapper = new ResourceWrapper( + Resources[0].ToResourceElement(), + new RawResource(Resources[0].ToJson(), FhirResourceFormat.Json, false), + null, + false, + null, + null, + null); + if (bundle) + { + var r = new Hl7.Fhir.Model.Bundle() + { + Type = BundleType.Batch, + }; + + r.Entry.Add(new RawBundleEntryComponent(wrapper)); + if (!raw) + { + r.Entry.Add(new EntryComponent() { Resource = Resources[1] }); + } + + return r; + } + else if (raw) + { + return new RawResourceElement(wrapper); + } + + return Resources[0]; + } + + private static List> ParseQuery(string query) + { + var result = new List>(); + if (!string.IsNullOrEmpty(query)) + { + var parameters = HttpUtility.ParseQueryString(query); + foreach (var k in parameters.AllKeys) + { + foreach (var p in parameters.GetValues(k)) + { + result.Add(Tuple.Create(k, p)); + } + } + } + + return result; + } + + private static void Validate( + bool bundle, + bool raw, + bool hasElements, + SummaryType summaryType, + string content) + { + Assert.False(string.IsNullOrEmpty(content)); + + var r = Parser.Parse(content); + Assert.NotNull(r); + if (bundle) + { + Assert.IsType(r); + var b = (Hl7.Fhir.Model.Bundle)r; + + var expected = new List(); + if (!raw) + { + expected.AddRange(Resources); + } + else + { + expected.Add(Resources[0]); + } + + Assert.Equal(expected.Count, b.Entry.Count); + foreach (var o in b.Entry.Select(x => x.Resource)) + { + if (hasElements || summaryType != SummaryType.False) + { + Assert.NotEmpty(o.Meta.Tag); + } + else + { + Assert.Equal(0, o.Meta?.Tag.Count ?? 0); + } + } + } + else + { + Assert.IsType(Resources[0].GetType(), r); + + var o = (Observation)r; + if (hasElements || summaryType != SummaryType.False) + { + Assert.NotEmpty(o.Meta.Tag); + } + else + { + Assert.Equal(0, o.Meta?.Tag.Count ?? 0); + } + } + } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Health/ImproperBehaviorHealthCheckTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Health/ImproperBehaviorHealthCheckTests.cs new file mode 100644 index 0000000000..5ad66034e4 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Health/ImproperBehaviorHealthCheckTests.cs @@ -0,0 +1,54 @@ +// ------------------------------------------------------------------------------------------------- +// 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.Extensions.Diagnostics.HealthChecks; +using Microsoft.Health.Fhir.Api.Features.Health; +using Microsoft.Health.Fhir.Core.Features.Health; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Test.Utilities; +using Xunit; + +namespace Microsoft.Health.Fhir.Api.UnitTests.Features.Health +{ + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.Web)] + public class ImproperBehaviorHealthCheckTests + { + private readonly ImproperBehaviorHealthCheck _healthCheck; + + public ImproperBehaviorHealthCheckTests() + { + _healthCheck = new ImproperBehaviorHealthCheck(); + } + + [Theory] + [InlineData(true, "Healthy")] + [InlineData(false, "Unhealthy")] + public async Task GivenNotification_WhenCheckingHealth_ThenCorrectResultShouldBeReturned( + bool healthy, + string message) + { + if (!healthy) + { + var notification = new ImproperBehaviorNotification(message); + await _healthCheck.Handle( + notification, + CancellationToken.None); + } + + var result = await _healthCheck.CheckHealthAsync( + null, + CancellationToken.None); + + Assert.Equal(healthy ? HealthStatus.Healthy : HealthStatus.Unhealthy, result.Status); + if (!healthy) + { + Assert.Contains(message, result.Description); + } + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems index ad21a31ce1..dd1208a858 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems @@ -13,6 +13,7 @@ + @@ -68,6 +69,7 @@ + From 87a2b5696123ea6afe84952c0a0baa5aece4fb99 Mon Sep 17 00:00:00 2001 From: Isao Yamauchi Date: Thu, 18 Dec 2025 09:14:11 -0800 Subject: [PATCH 2/5] Adding NonFhirResourceXmlOutputFormatterTests --- .../NonFhirResourceXmlOutputFormatterTests.cs | 38 +++++++++++++++++++ ...Health.Fhir.Shared.Api.UnitTests.projitems | 1 + 2 files changed, 39 insertions(+) create mode 100644 src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/NonFhirResourceXmlOutputFormatterTests.cs diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/NonFhirResourceXmlOutputFormatterTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/NonFhirResourceXmlOutputFormatterTests.cs new file mode 100644 index 0000000000..ab365814a5 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/NonFhirResourceXmlOutputFormatterTests.cs @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------------------------------- +// 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.Reflection; +using System.Threading.Tasks; +using Hl7.Fhir.Model; +using Microsoft.Health.Fhir.Api.Features.Formatters; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Test.Utilities; +using Xunit; + +namespace Microsoft.Health.Fhir.Api.UnitTests.Features.Formatters +{ + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.Web)] + public class NonFhirResourceXmlOutputFormatterTests + { + [Theory] + [InlineData(typeof(Patient), false)] + [InlineData(typeof(NonFhirResourceXmlOutputFormatter), true)] + public void GivenType_WhenCheckingCanWriteType_ThenFormatterShouldReturnCorrectValue( + Type type, + bool expected) + { + var formatter = new NonFhirResourceXmlOutputFormatter(); + var methodInfo = typeof(NonFhirResourceXmlOutputFormatter).GetMethod( + "CanWriteType", + BindingFlags.NonPublic | BindingFlags.Instance); + var actual = methodInfo.Invoke( + formatter, + new object[] { type }); + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems index dd1208a858..fb27018d4f 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems @@ -65,6 +65,7 @@ + From 116a629193b44bda975dd2dafd79897607d03122 Mon Sep 17 00:00:00 2001 From: Isao Yamauchi Date: Fri, 19 Dec 2025 08:07:01 -0800 Subject: [PATCH 3/5] Adding more UTs for code coverage --- .../Controllers/FhirControllerTests.cs | 97 ++++++++++++++++++- .../NonFhirResourceXmlOutputFormatterTests.cs | 1 - 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/FhirControllerTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/FhirControllerTests.cs index d059d088b5..afe9269a85 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/FhirControllerTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/FhirControllerTests.cs @@ -4,34 +4,44 @@ // ------------------------------------------------------------------------------------------------- using System; +using System.Collections.Generic; using System.Linq; +using System.Net; using System.Reflection; using System.Threading; -using System.Threading.Tasks; +using Hl7.Fhir.Model; +using Hl7.Fhir.Serialization; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Options; using Microsoft.Health.Api.Features.Audit; using Microsoft.Health.Core.Features.Context; using Microsoft.Health.Fhir.Api.Configs; using Microsoft.Health.Fhir.Api.Controllers; +using Microsoft.Health.Fhir.Api.Features.ActionResults; using Microsoft.Health.Fhir.Api.Features.Filters; using Microsoft.Health.Fhir.Api.Features.Filters.Metrics; +using Microsoft.Health.Fhir.Api.Features.Resources; using Microsoft.Health.Fhir.Api.Models; +using Microsoft.Health.Fhir.Core.Extensions; using Microsoft.Health.Fhir.Core.Features; using Microsoft.Health.Fhir.Core.Features.Context; using Microsoft.Health.Fhir.Core.Features.Persistence; using Microsoft.Health.Fhir.Core.Features.Routing; +using Microsoft.Health.Fhir.Core.Messages.Create; using Microsoft.Health.Fhir.Core.Messages.Delete; +using Microsoft.Health.Fhir.Core.Messages.Upsert; 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.Api.UnitTests.Controllers { @@ -75,6 +85,10 @@ public FhirControllerTests() Substitute.For(), new RouteData(), new ControllerActionDescriptor())); + _urlResolver.ResolveResourceUrl( + Arg.Any(), + Arg.Any()) + .Returns(new Uri("https://localhost/location")); } [Fact] @@ -177,6 +191,87 @@ await _mediator.Received(1).Send( Arg.Any()); } + [Theory] + [InlineData("p=v", null, SaveOutcomeType.Created)] + [InlineData("p=v", null, null)] + [InlineData("p=v", ConditionalQueryProcessingLogic.Sequential, SaveOutcomeType.MatchFound)] + [InlineData("p=v", ConditionalQueryProcessingLogic.Parallel, SaveOutcomeType.Created)] + [InlineData("p0=v0&p1=v1&p2=v2", ConditionalQueryProcessingLogic.Parallel, SaveOutcomeType.Created)] + public async Task GivenConditionalCreateRequest_WhenVariousHeadersAreSpecified_ThenRequestShouldBeHandledCorrectly( + string ifNotExist, + ConditionalQueryProcessingLogic? queryProcessingLogic, + SaveOutcomeType? saveOutcome) + { + var resource = new Patient() + { + Id = Guid.NewGuid().ToString(), + VersionId = Guid.NewGuid().ToString(), + }; + + var wrapper = new ResourceWrapper( + resource.ToResourceElement(), + new RawResource(resource.ToJson(), FhirResourceFormat.Json, false), + null, + false, + null, + null, + null); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers[KnownHeaders.IfNoneExist] = ifNotExist; + if (queryProcessingLogic.HasValue) + { + httpContext.Request.Headers[KnownHeaders.ConditionalQueryProcessingLogic] = queryProcessingLogic.Value.ToString(); + } + + _fhirController.ControllerContext.HttpContext = httpContext; + _mediator.Send( + Arg.Any(), + Arg.Any()) + .Returns(saveOutcome == null ? null : new UpsertResourceResponse(new SaveOutcome(new RawResourceElement(wrapper), saveOutcome.Value))); + + var request = default(ConditionalCreateResourceRequest); + _mediator.When( + x => x.Send( + Arg.Any(), + Arg.Any())) + .Do(x => request = x.Arg()); + + var response = await _fhirController.ConditionalCreate(resource); + if (saveOutcome.HasValue) + { + Assert.IsType(response); + Assert.Equal( + saveOutcome.Value == SaveOutcomeType.Created ? HttpStatusCode.Created : HttpStatusCode.OK, + ((FhirResult)response).StatusCode); + } + else + { + Assert.IsType(response); + } + + var expectedHeaders = QueryHelpers.ParseQuery(ifNotExist); + Assert.Equal(expectedHeaders.Count, request?.ConditionalParameters.Count ?? 0); + Assert.All( + expectedHeaders, + x => + { + Assert.Contains( + request?.ConditionalParameters, + y => + { + return string.Equals(x.Key, y.Item1, StringComparison.Ordinal) + && string.Equals(x.Value.ToString(), y.Item2, StringComparison.Ordinal); + }); + }); + + await _mediator.Received(1).Send( + Arg.Any(), + Arg.Any()); + _requestContextAccessor.RequestContext.Properties + .Received(queryProcessingLogic.HasValue && queryProcessingLogic.Value == ConditionalQueryProcessingLogic.Parallel ? 1 : 0) + .TryAdd(KnownQueryParameterNames.OptimizeConcurrency, true); + } + private static void TestIfTargetMethodContainsCustomAttribute(Type expectedCustomAttributeType, string methodName, Type targetClassType) { MethodInfo bundleMethodInfo = targetClassType.GetMethod(methodName); diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/NonFhirResourceXmlOutputFormatterTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/NonFhirResourceXmlOutputFormatterTests.cs index ab365814a5..289ae889b7 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/NonFhirResourceXmlOutputFormatterTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/NonFhirResourceXmlOutputFormatterTests.cs @@ -5,7 +5,6 @@ using System; using System.Reflection; -using System.Threading.Tasks; using Hl7.Fhir.Model; using Microsoft.Health.Fhir.Api.Features.Formatters; using Microsoft.Health.Fhir.Tests.Common; From 97f87272f634eeb6f2e32c47cc9273b283ea4bdb Mon Sep 17 00:00:00 2001 From: Isao Yamauchi Date: Wed, 31 Dec 2025 10:10:27 -0800 Subject: [PATCH 4/5] Addressing Mikael's comment. --- .../Features/Formatters/FhirJsonOutputFormatterTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirJsonOutputFormatterTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirJsonOutputFormatterTests.cs index 80f7d70ff4..b1b9889ff8 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirJsonOutputFormatterTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirJsonOutputFormatterTests.cs @@ -10,13 +10,10 @@ using System.Linq; using System.Net; using System.Text; -using System.Threading.Tasks; using System.Web; -using AngleSharp.Dom; using Hl7.Fhir.Model; using Hl7.Fhir.Rest; using Hl7.Fhir.Serialization; -using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Health.Fhir.Api.Features.Formatters; From 456d0b79ce07a3e9b8e99612729c96b9c3dff863 Mon Sep 17 00:00:00 2001 From: Isao Yamauchi Date: Wed, 31 Dec 2025 10:14:39 -0800 Subject: [PATCH 5/5] Addressing Mikael's comment 2. --- .../Controllers/BulkDeleteControllerTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/BulkDeleteControllerTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/BulkDeleteControllerTests.cs index 45b36b746d..3ba3187b32 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/BulkDeleteControllerTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/BulkDeleteControllerTests.cs @@ -118,7 +118,7 @@ await Run( [Theory] [InlineData(KnownResourceTypes.CareTeam, DeleteOperation.SoftDelete, false, "t1,t2,t3", "?p1=v1&p2=v2&p3=v3", true)] public async Task GivenParameters_WhenBulkDeleting_ThenBulkDeleteByResourceTypeShouldSucceed( - string resoureType, + string resourceType, DeleteOperation operation, bool removeReferences, string excludedResourceTypes, @@ -126,7 +126,7 @@ public async Task GivenParameters_WhenBulkDeleting_ThenBulkDeleteByResourceTypeS bool valid) { await Run( - resoureType, + resourceType, operation == DeleteOperation.HardDelete, operation == DeleteOperation.PurgeHistory, false, @@ -135,7 +135,7 @@ await Run( query, valid, (model) => _controller.BulkDeleteByResourceType( - resoureType, + resourceType, model, operation == DeleteOperation.PurgeHistory, removeReferences,