diff --git a/src/Microsoft.Health.Fhir.Core/Messages/Patch/ConditionalPatchResourceRequest.cs b/src/Microsoft.Health.Fhir.Core/Messages/Patch/ConditionalPatchResourceRequest.cs index 4d9284076b..6ac0b6a6b2 100644 --- a/src/Microsoft.Health.Fhir.Core/Messages/Patch/ConditionalPatchResourceRequest.cs +++ b/src/Microsoft.Health.Fhir.Core/Messages/Patch/ConditionalPatchResourceRequest.cs @@ -22,19 +22,23 @@ public ConditionalPatchResourceRequest( PatchPayload payload, IReadOnlyList> conditionalParameters, BundleResourceContext bundleResourceContext = null, - WeakETag weakETag = null) + WeakETag weakETag = null, + bool metaHistory = true) : base(resourceType, conditionalParameters, bundleResourceContext) { EnsureArg.IsNotNull(payload, nameof(payload)); Payload = payload; WeakETag = weakETag; + MetaHistory = metaHistory; } public PatchPayload Payload { get; } public WeakETag WeakETag { get; } + public bool MetaHistory { get; } + protected override IEnumerable GetCapabilities() => Capabilities; } } diff --git a/src/Microsoft.Health.Fhir.Core/Messages/Patch/PatchResourceRequest.cs b/src/Microsoft.Health.Fhir.Core/Messages/Patch/PatchResourceRequest.cs index 113872528d..3629a747b7 100644 --- a/src/Microsoft.Health.Fhir.Core/Messages/Patch/PatchResourceRequest.cs +++ b/src/Microsoft.Health.Fhir.Core/Messages/Patch/PatchResourceRequest.cs @@ -17,7 +17,7 @@ namespace Microsoft.Health.Fhir.Core.Messages.Patch { public sealed class PatchResourceRequest : BaseBundleInnerRequest, IRequest, IRequireCapability { - public PatchResourceRequest(ResourceKey resourceKey, PatchPayload payload, BundleResourceContext bundleResourceContext, WeakETag weakETag = null) + public PatchResourceRequest(ResourceKey resourceKey, PatchPayload payload, BundleResourceContext bundleResourceContext, WeakETag weakETag = null, bool metaHistory = true) : base(bundleResourceContext) { EnsureArg.IsNotNull(resourceKey, nameof(resourceKey)); @@ -26,6 +26,7 @@ public PatchResourceRequest(ResourceKey resourceKey, PatchPayload payload, Bundl ResourceKey = resourceKey; Payload = payload; WeakETag = weakETag; + MetaHistory = metaHistory; } public PatchPayload Payload { get; } @@ -34,6 +35,8 @@ public PatchResourceRequest(ResourceKey resourceKey, PatchPayload payload, Bundl public WeakETag WeakETag { get; } + public bool MetaHistory { get; } + public IEnumerable RequiredCapabilities() { yield return new CapabilityQuery($"CapabilityStatement.rest.resource.where(type = '{ResourceKey.ResourceType}').interaction.where(code = 'patch').exists()"); diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs index cda799fdff..d6a62153c2 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs @@ -492,13 +492,14 @@ public async Task ConditionalDelete(string typeParameter, HardDel /// The identifier. /// The JSON patch document. /// Optional If-Match header. + /// Optional flag indicating if a historical version should be created if the changes are only to metadata. [HttpPatch] [ValidateIdSegmentAttribute] [Route(KnownRoutes.ResourceTypeById)] [AuditEventType(AuditEventSubType.Patch)] [ServiceFilter(typeof(SearchParameterFilterAttribute))] [Consumes("application/json-patch+json")] - public async Task PatchJson(string typeParameter, string idParameter, [FromBody] JsonPatchDocument patchDocument, [ModelBinder(typeof(WeakETagBinder))] WeakETag ifMatchHeader) + public async Task PatchJson(string typeParameter, string idParameter, [FromBody] JsonPatchDocument patchDocument, [ModelBinder(typeof(WeakETagBinder))] WeakETag ifMatchHeader, [FromQuery(Name = KnownQueryParameterNames.MetaHistory)] bool metaHistory = true) { var payload = new JsonPatchPayload(patchDocument); @@ -507,7 +508,8 @@ public async Task PatchJson(string typeParameter, string idParame new ResourceKey(typeParameter, idParameter), payload, GetBundleResourceContext(), - ifMatchHeader), + ifMatchHeader, + metaHistory), HttpContext.RequestAborted); return ToSaveOutcomeResult(response.Outcome); @@ -519,12 +521,13 @@ public async Task PatchJson(string typeParameter, string idParame /// Type of resource to patch. /// The JSON patch document. /// Optional If-Match header. + /// Optional flag indicating if a historical version should be created if the changes are only to metadata. [HttpPatch] [Route(KnownRoutes.ResourceType)] [AuditEventType(AuditEventSubType.ConditionalPatch)] [ServiceFilter(typeof(SearchParameterFilterAttribute))] [Consumes("application/json-patch+json")] - public async Task ConditionalPatchJson(string typeParameter, [FromBody] JsonPatchDocument patchDocument, [ModelBinder(typeof(WeakETagBinder))] WeakETag ifMatchHeader) + public async Task ConditionalPatchJson(string typeParameter, [FromBody] JsonPatchDocument patchDocument, [ModelBinder(typeof(WeakETagBinder))] WeakETag ifMatchHeader, [FromQuery(Name = KnownQueryParameterNames.MetaHistory)] bool metaHistory = true) { IReadOnlyList> conditionalParameters = GetQueriesForSearch(); var payload = new JsonPatchPayload(patchDocument); @@ -532,7 +535,7 @@ public async Task ConditionalPatchJson(string typeParameter, [Fro SetupConditionalRequestWithQueryOptimizeConcurrency(); UpsertResourceResponse response = await _mediator.ConditionalPatchResourceAsync( - new ConditionalPatchResourceRequest(typeParameter, payload, conditionalParameters, GetBundleResourceContext(), ifMatchHeader), + new ConditionalPatchResourceRequest(typeParameter, payload, conditionalParameters, GetBundleResourceContext(), ifMatchHeader, metaHistory), HttpContext.RequestAborted); return ToSaveOutcomeResult(response.Outcome); } @@ -544,18 +547,19 @@ public async Task ConditionalPatchJson(string typeParameter, [Fro /// The identifier. /// The JSON FHIR Parameters Resource. /// Optional If-Match header. + /// Optional flag indicating if a historical version should be created if the changes are only to metadata. [HttpPatch] [ValidateIdSegmentAttribute] [Route(KnownRoutes.ResourceTypeById)] [AuditEventType(AuditEventSubType.Patch)] [ServiceFilter(typeof(SearchParameterFilterAttribute))] [Consumes("application/fhir+json")] - public async Task PatchFhir(string typeParameter, string idParameter, [FromBody] Parameters paramsResource, [ModelBinder(typeof(WeakETagBinder))] WeakETag ifMatchHeader) + public async Task PatchFhir(string typeParameter, string idParameter, [FromBody] Parameters paramsResource, [ModelBinder(typeof(WeakETagBinder))] WeakETag ifMatchHeader, [FromQuery(Name = KnownQueryParameterNames.MetaHistory)] bool metaHistory = true) { var payload = new FhirPathPatchPayload(paramsResource); UpsertResourceResponse response = await _mediator.PatchResourceAsync( - new PatchResourceRequest(new ResourceKey(typeParameter, idParameter), payload, GetBundleResourceContext(), ifMatchHeader), + new PatchResourceRequest(new ResourceKey(typeParameter, idParameter), payload, GetBundleResourceContext(), ifMatchHeader, metaHistory), HttpContext.RequestAborted); return ToSaveOutcomeResult(response.Outcome); } @@ -566,12 +570,13 @@ public async Task PatchFhir(string typeParameter, string idParame /// Type of resource to patch. /// The JSON FHIR Parameters Resource. /// Optional If-Match header. + /// Optional flag indicating if a historical version should be created if the changes are only to metadata. [HttpPatch] [Route(KnownRoutes.ResourceType)] [AuditEventType(AuditEventSubType.ConditionalPatch)] [ServiceFilter(typeof(SearchParameterFilterAttribute))] [Consumes("application/fhir+json")] - public async Task ConditionalPatchFhir(string typeParameter, [FromBody] Parameters paramsResource, [ModelBinder(typeof(WeakETagBinder))] WeakETag ifMatchHeader) + public async Task ConditionalPatchFhir(string typeParameter, [FromBody] Parameters paramsResource, [ModelBinder(typeof(WeakETagBinder))] WeakETag ifMatchHeader, [FromQuery(Name = KnownQueryParameterNames.MetaHistory)] bool metaHistory = true) { IReadOnlyList> conditionalParameters = GetQueriesForSearch(); var payload = new FhirPathPatchPayload(paramsResource); @@ -579,7 +584,7 @@ public async Task ConditionalPatchFhir(string typeParameter, [Fro SetupConditionalRequestWithQueryOptimizeConcurrency(); UpsertResourceResponse response = await _mediator.ConditionalPatchResourceAsync( - new ConditionalPatchResourceRequest(typeParameter, payload, conditionalParameters, GetBundleResourceContext(), ifMatchHeader), + new ConditionalPatchResourceRequest(typeParameter, payload, conditionalParameters, GetBundleResourceContext(), ifMatchHeader, metaHistory), HttpContext.RequestAborted); return ToSaveOutcomeResult(response.Outcome); } diff --git a/src/Microsoft.Health.Fhir.Shared.Client/FhirClient.cs b/src/Microsoft.Health.Fhir.Shared.Client/FhirClient.cs index b511e60bec..540015001e 100644 --- a/src/Microsoft.Health.Fhir.Shared.Client/FhirClient.cs +++ b/src/Microsoft.Health.Fhir.Shared.Client/FhirClient.cs @@ -240,16 +240,16 @@ public Task HardDeleteAsync(T resource, CancellationToken cance return DeleteAsync($"{resource.TypeName}/{resource.Id}?hardDelete=true", cancellationToken); } - public async Task> JsonPatchAsync(T resource, string content, string ifMatchVersion = null, CancellationToken cancellationToken = default) + public async Task> JsonPatchAsync(T resource, string content, string ifMatchVersion = null, bool metaHistory = true, CancellationToken cancellationToken = default) where T : Resource { - return await JsonPatchAsync($"{resource.TypeName}/{resource.Id}", content, ifMatchVersion, cancellationToken); + return await JsonPatchAsync($"{resource.TypeName}/{resource.Id}?_meta-history={metaHistory}", content, ifMatchVersion, cancellationToken); } - public async Task> ConditionalJsonPatchAsync(string resourceType, string searchCriteria, string content, string ifMatchVersion = null, CancellationToken cancellationToken = default) + public async Task> ConditionalJsonPatchAsync(string resourceType, string searchCriteria, string content, string ifMatchVersion = null, bool metaHistory = true, CancellationToken cancellationToken = default) where T : Resource { - return await JsonPatchAsync($"{resourceType}?{searchCriteria}", content, ifMatchVersion, cancellationToken); + return await JsonPatchAsync($"{resourceType}?{searchCriteria}&_meta-history={metaHistory}", content, ifMatchVersion, cancellationToken); } private async Task> JsonPatchAsync(string uri, string content, string ifMatchVersion = null, CancellationToken cancellationToken = default) @@ -276,16 +276,16 @@ private async Task> JsonPatchAsync(string uri, string content return await CreateResponseAsync(response); } - public async Task> FhirPatchAsync(T resource, Parameters patchRequest, string ifMatchVersion = null, CancellationToken cancellationToken = default) + public async Task> FhirPatchAsync(T resource, Parameters patchRequest, string ifMatchVersion = null, bool metaHistory = true, CancellationToken cancellationToken = default) where T : Resource { - return await FhirPatchAsync($"{resource.TypeName}/{resource.Id}", patchRequest, ifMatchVersion, cancellationToken); + return await FhirPatchAsync($"{resource.TypeName}/{resource.Id}?_meta-history={metaHistory}", patchRequest, ifMatchVersion, cancellationToken); } - public async Task> ConditionalFhirPatchAsync(string resourceType, string searchCriteria, Parameters patchRequest, string ifMatchVersion = null, CancellationToken cancellationToken = default) + public async Task> ConditionalFhirPatchAsync(string resourceType, string searchCriteria, Parameters patchRequest, string ifMatchVersion = null, bool metaHistory = true, CancellationToken cancellationToken = default) where T : Resource { - return await FhirPatchAsync($"{resourceType}?{searchCriteria}", patchRequest, ifMatchVersion, cancellationToken); + return await FhirPatchAsync($"{resourceType}?{searchCriteria}&_meta-history={metaHistory}", patchRequest, ifMatchVersion, cancellationToken); } private async Task> FhirPatchAsync(string uri, Parameters patchRequest, string ifMatchVersion = null, CancellationToken cancellationToken = default) diff --git a/src/Microsoft.Health.Fhir.Shared.Client/IFhirClient.cs b/src/Microsoft.Health.Fhir.Shared.Client/IFhirClient.cs index 27c230a77b..71d1feae41 100644 --- a/src/Microsoft.Health.Fhir.Shared.Client/IFhirClient.cs +++ b/src/Microsoft.Health.Fhir.Shared.Client/IFhirClient.cs @@ -40,16 +40,16 @@ Task DeleteAsync(T resource, CancellationToken cancellationToke Task HardDeleteAsync(T resource, CancellationToken cancellationToken = default) where T : Resource; - Task> JsonPatchAsync(T resource, string content, string ifMatchVersion = null, CancellationToken cancellationToken = default) + Task> JsonPatchAsync(T resource, string content, string ifMatchVersion = null, bool metaHistory = true, CancellationToken cancellationToken = default) where T : Resource; - Task> ConditionalJsonPatchAsync(string resourceType, string searchCriteria, string content, string ifMatchVersion = null, CancellationToken cancellationToken = default) + Task> ConditionalJsonPatchAsync(string resourceType, string searchCriteria, string content, string ifMatchVersion = null, bool metaHistory = true, CancellationToken cancellationToken = default) where T : Resource; - Task> FhirPatchAsync(T resource, Parameters patchRequest, string ifMatchVersion = null, CancellationToken cancellationToken = default) + Task> FhirPatchAsync(T resource, Parameters patchRequest, string ifMatchVersion = null, bool metaHistory = true, CancellationToken cancellationToken = default) where T : Resource; - Task> ConditionalFhirPatchAsync(string resourceType, string searchCriteria, Parameters patchRequest, string ifMatchVersion = null, CancellationToken cancellationToken = default) + Task> ConditionalFhirPatchAsync(string resourceType, string searchCriteria, Parameters patchRequest, string ifMatchVersion = null, bool metaHistory = true, CancellationToken cancellationToken = default) where T : Resource; Task BulkUpdateAsync(string uri, Parameters patchRequest, CancellationToken cancellationToken = default); diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Resources/Patch/ConditionalPatchResourceHandler.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Resources/Patch/ConditionalPatchResourceHandler.cs index 4b8b761bc2..ad5038d878 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Resources/Patch/ConditionalPatchResourceHandler.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Resources/Patch/ConditionalPatchResourceHandler.cs @@ -64,7 +64,7 @@ public override async Task HandleSingleMatch(Conditional } var patchedResource = request.Payload.Patch(match.Resource); - return await _mediator.Send(new UpsertResourceRequest(patchedResource, bundleResourceContext: null), cancellationToken); + return await _mediator.Send(new UpsertResourceRequest(patchedResource, bundleResourceContext: null, metaHistory: request.MetaHistory), cancellationToken); } public override Task CheckAccess(CancellationToken cancellationToken) diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Resources/Patch/PatchResourceHandler.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Resources/Patch/PatchResourceHandler.cs index 8cc4cb8744..f8431e24a2 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Resources/Patch/PatchResourceHandler.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Resources/Patch/PatchResourceHandler.cs @@ -74,7 +74,7 @@ public async Task Handle(PatchResourceRequest request, C } ResourceElement patchedResource = request.Payload.Patch(currentDoc); - return await _mediator.Send(new UpsertResourceRequest(patchedResource, request.BundleResourceContext, request.WeakETag), cancellationToken); + return await _mediator.Send(new UpsertResourceRequest(patchedResource, request.BundleResourceContext, request.WeakETag, request.MetaHistory), cancellationToken); } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/BundleFactory.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/BundleFactory.cs index ad095bcf39..6603cd9b7d 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/BundleFactory.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/BundleFactory.cs @@ -65,7 +65,8 @@ public ResourceElement CreateHistoryBundle(SearchResult result) var hasVerb = Enum.TryParse(r.Resource.Request?.Method, true, out Bundle.HTTPVerb httpVerb); #if Stu3 // STU3 doesn't have PATCH verb, so let's map it to PUT. - if (!hasVerb && string.Equals("PATCH", r.Resource.Request?.Method, StringComparison.OrdinalIgnoreCase)) + if ((!hasVerb && string.Equals("PATCH", r.Resource.Request?.Method, StringComparison.OrdinalIgnoreCase)) + || httpVerb == Bundle.HTTPVerb.PATCH) { hasVerb = true; httpVerb = Bundle.HTTPVerb.PUT; diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/ConditionalPatchTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/ConditionalPatchTests.cs index 63d43d3f57..e837378f3a 100644 --- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/ConditionalPatchTests.cs +++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/ConditionalPatchTests.cs @@ -4,11 +4,15 @@ // ------------------------------------------------------------------------------------------------- using System; +using System.Collections.Generic; +using System.Linq; using System.Net; +using Hl7.Fhir.ElementModel; using Hl7.Fhir.Model; using Hl7.Fhir.Rest; using Microsoft.Health.Fhir.Client; using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Resources.Patch.FhirPathPatch.Helpers; using Microsoft.Health.Fhir.Tests.Common; using Microsoft.Health.Fhir.Tests.Common.FixtureParameters; using Microsoft.Health.Fhir.Tests.E2E.Common; @@ -255,5 +259,159 @@ public async Task GivenSecondVersionOfResource_WhenPatchingConditionallyWithOneM Assert.Equal(HttpStatusCode.PreconditionFailed, exceptionJson.Response.StatusCode); Assert.Equal(HttpStatusCode.PreconditionFailed, exceptionFhir.Response.StatusCode); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [Trait(Traits.Priority, Priority.One)] + [HttpIntegrationFixtureArgumentSets(DataStore.SqlServer)] + public async Task GivenAResource_WhenFhirPatchingConditionallyWithMetaHistoryFlag_TheServerShouldRespectTheFlag(bool metaHistory) + { + // Create initial patient resource + var poco = Samples.GetDefaultPatient().ToPoco(); + FhirResponse createResponse = await _client.CreateAsync(poco); + + Assert.Equal("1", createResponse.Resource.Meta.VersionId); + Assert.NotNull(createResponse.Resource.Id); + + string patientId = createResponse.Resource.Id; + string initialVersionId = createResponse.Resource.Meta.VersionId; + + // Create patch request to add a tag to meta.tag array + var patchRequest = new Parameters().AddPatchParameter( + "add", + "Patient.meta", + "tag", + new List() + { + new Parameters.ParameterComponent { Name = "system", Value = new FhirUri("ORGANIZATION_ID") }, + new Parameters.ParameterComponent { Name = "code", Value = new Code("fhirLegalEntityId") }, + new Parameters.ParameterComponent { Name = "display", Value = new FhirString("fhirLegalEntityId") }, + }); + + // Apply patch with If-Match header + using FhirResponse patchResponse = await _client.ConditionalFhirPatchAsync( + "Patient", + $"_id={patientId}", + patchRequest, + ifMatchVersion: initialVersionId, + metaHistory: metaHistory); + + // Verify patch was successful + Assert.Equal(HttpStatusCode.OK, patchResponse.Response.StatusCode); + Assert.Equal("2", patchResponse.Resource.Meta.VersionId); + Assert.Equal(patientId, patchResponse.Resource.Id); + + // Verify the tag was added + Assert.NotNull(patchResponse.Resource.Meta.Tag); + var addedTag = patchResponse.Resource.Meta.Tag.FirstOrDefault(t => + t.System == "ORGANIZATION_ID" && + t.Code == "fhirLegalEntityId"); + Assert.NotNull(addedTag); + Assert.Equal("fhirLegalEntityId", addedTag.Display); + + // Verify history is preserved - both versions should exist + FhirResponse historyResponse = await _client.ReadHistoryAsync( + ResourceType.Patient, + patientId); + + Assert.NotNull(historyResponse.Resource); + if (metaHistory) + { + Assert.True( + historyResponse.Resource.Entry.Count >= 2, + $"Expected at least 2 history entries, but found {historyResponse.Resource.Entry.Count}"); + } + else + { + Assert.True( + historyResponse.Resource.Entry.Count == 1, + $"Expected at 1 history entry, but found {historyResponse.Resource.Entry.Count}"); + } + + // Verify version 1's state in history + var version1Entry = historyResponse.Resource.Entry.FirstOrDefault(e => + e.Resource is Patient p && p.Meta.VersionId == "1"); + + if (metaHistory) + { + Assert.NotNull(version1Entry); + + // Verify version 1 doesn't have the tag + var version1Patient = version1Entry.Resource as Patient; + var version1Tag = version1Patient?.Meta.Tag?.FirstOrDefault(t => + t.System == "ORGANIZATION_ID" && + t.Code == "fhirLegalEntityId"); + Assert.Null(version1Tag); + } + else + { + Assert.Null(version1Entry); + } + + // Verify version 2 exists in history + var version2Entry = historyResponse.Resource.Entry.FirstOrDefault(e => + e.Resource is Patient p && p.Meta.VersionId == "2"); + Assert.NotNull(version2Entry); + + // Verify version 2 has the tag + var version2Patient = version2Entry.Resource as Patient; + var version2Tag = version2Patient?.Meta.Tag?.FirstOrDefault(t => + t.System == "ORGANIZATION_ID" && + t.Code == "fhirLegalEntityId"); + Assert.NotNull(version2Tag); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [Trait(Traits.Priority, Priority.One)] + [HttpIntegrationFixtureArgumentSets(DataStore.SqlServer)] + public async Task GivenAResource_WhenJsonPatchingConditionallyWithMetaHistoryFlag_TheServerShouldRespectTheFlag(bool metaHistory) + { + var parser = new Hl7.Fhir.Serialization.FhirJsonParser(); + string adJson = "{\"resourceType\":\"ActivityDefinition\",\"status\":\"active\"}"; + var poco = parser. + Parse(adJson). + ToTypedElement(). + ToResourceElement(). + ToPoco(); + FhirResponse response = await _client.CreateAsync(poco); + + string patchDocument = + "[{\"op\":\"add\",\"path\":\"/meta/tag\",\"value\":{\"system\":\"http://example.org/fhir/tags\",\"code\":\"example-tag\"}}]"; + FhirResponse patchResponse = await _client.ConditionalJsonPatchAsync( + "ActivityDefinition", + $"_id={response.Resource.Id}", + patchDocument, + metaHistory: metaHistory); + + ActivityDefinition ad = patchResponse.Resource; + Assert.Contains( + ad.Meta.Tag, + tag => tag.System == "http://example.org/fhir/tags" && tag.Code == "example-tag"); + + FhirResponse getResponse = await _client.ReadAsync( + ResourceType.ActivityDefinition, + ad.Id); + ActivityDefinition adFromGet = getResponse.Resource; + Assert.Contains( + adFromGet.Meta.Tag, + tag => tag.System == "http://example.org/fhir/tags" && tag.Code == "example-tag"); + + FhirResponse historyResponse = await _client.ReadHistoryAsync( + ResourceType.ActivityDefinition, + ad.Id); + if (metaHistory) + { + // There should be two entries in the history: one for the create, one for the patch + Assert.Equal(2, historyResponse.Resource.Entry.Count); + } + else + { + // There should be only one entry in the history: the create + Assert.Single(historyResponse.Resource.Entry); + } + } } } diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/FhirPathPatchTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/FhirPathPatchTests.cs index 4120a610ae..848d19aeb4 100644 --- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/FhirPathPatchTests.cs +++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/FhirPathPatchTests.cs @@ -81,7 +81,7 @@ public async Task GivenAServerThatSupportsIt_WhenSubmittingInvalidFhirPatch_Then Assert.Equal(OperationOutcome.IssueType.Invalid, responseObject.Issue[0].Code); } - [SkippableFact(Skip = "This test is skipped for STU3.")] + [SkippableFact] [Trait(Traits.Priority, Priority.One)] public async Task GivenAPatchDocument_WhenSubmittingAParallelBundleWithDuplicatedPatch_ThenServerShouldReturnAnError() { @@ -109,10 +109,9 @@ public async Task GivenAPatchDocument_WhenSubmittingAParallelBundleWithDuplicate } } - [SkippableTheory(Skip = "This test is skipped for STU3.")] + [SkippableTheory] [Trait(Traits.Priority, Priority.One)] - [InlineData(FhirBundleProcessingLogic.Parallel)] - [InlineData(FhirBundleProcessingLogic.Sequential)] + [InlineData(FhirBundleProcessingLogic.Sequential)] // Parallel logic will execute in a random order, and thus is not suitable for this test public async Task GivenAPatchDocument_WhenSubmittingABundleWithFhirPatch_ThenServerShouldPatchCorrectly(FhirBundleProcessingLogic processingLogic) { Skip.If(ModelInfoProvider.Version == FhirSpecification.Stu3, "Patch isn't supported in Bundles by STU3"); @@ -476,12 +475,13 @@ public async Task GivenAPatchDocumentWithDate_WhenPassingDateTime_ThenExceptionS await Assert.ThrowsAsync(() => _client.FhirPatchAsync(response.Resource, patchRequest)); } - [SkippableFact(Skip = "This test is skipped for STU3.")] + [Theory] [Trait(Traits.Priority, Priority.One)] - public async Task GivenAServerThatSupportsIt_WhenPatchingOnlyMetaTag_ThenServerShouldCreateNewVersionAndPreserveHistory() + [InlineData(true)] + [InlineData(false)] + [HttpIntegrationFixtureArgumentSets(DataStore.SqlServer)] + public async Task GivenAServerThatSupportsIt_WhenPatchingOnlyMetaTag_ThenServerHonorsMetaHistoryParameter(bool metaHistory) { - Skip.If(ModelInfoProvider.Version == FhirSpecification.Stu3, "Patch isn't supported in Bundles by STU3"); - // Create initial patient resource var poco = Samples.GetDefaultPatient().ToPoco(); FhirResponse createResponse = await _client.CreateAsync(poco); @@ -508,7 +508,8 @@ public async Task GivenAServerThatSupportsIt_WhenPatchingOnlyMetaTag_ThenServerS using FhirResponse patchResponse = await _client.FhirPatchAsync( createResponse.Resource, patchRequest, - ifMatchVersion: initialVersionId); + ifMatchVersion: initialVersionId, + metaHistory: metaHistory); // Verify patch was successful Assert.Equal(HttpStatusCode.OK, patchResponse.Response.StatusCode); @@ -529,27 +530,44 @@ public async Task GivenAServerThatSupportsIt_WhenPatchingOnlyMetaTag_ThenServerS patientId); Assert.NotNull(historyResponse.Resource); - Assert.True( + if (metaHistory) + { + Assert.True( historyResponse.Resource.Entry.Count >= 2, $"Expected at least 2 history entries, but found {historyResponse.Resource.Entry.Count}"); + } + else + { + Assert.True( + historyResponse.Resource.Entry.Count == 1, + $"Expected at 1 history entry, but found {historyResponse.Resource.Entry.Count}"); + } - // Verify version 1 exists in history + // Verify version 1's state in history var version1Entry = historyResponse.Resource.Entry.FirstOrDefault(e => - e.Resource is Patient p && p.Meta.VersionId == "1"); - Assert.NotNull(version1Entry); + e.Resource is Patient p && p.Meta.VersionId == "1"); + + if (metaHistory) + { + Assert.NotNull(version1Entry); + + // Verify version 1 doesn't have the tag + var version1Patient = version1Entry.Resource as Patient; + var version1Tag = version1Patient?.Meta.Tag?.FirstOrDefault(t => + t.System == "ORGANIZATION_ID" && + t.Code == "fhirLegalEntityId"); + Assert.Null(version1Tag); + } + else + { + Assert.Null(version1Entry); + } // Verify version 2 exists in history var version2Entry = historyResponse.Resource.Entry.FirstOrDefault(e => - e.Resource is Patient p && p.Meta.VersionId == "2"); + e.Resource is Patient p && p.Meta.VersionId == "2"); Assert.NotNull(version2Entry); - // Verify version 1 doesn't have the tag - var version1Patient = version1Entry.Resource as Patient; - var version1Tag = version1Patient?.Meta.Tag?.FirstOrDefault(t => - t.System == "ORGANIZATION_ID" && - t.Code == "fhirLegalEntityId"); - Assert.Null(version1Tag); - // Verify version 2 has the tag var version2Patient = version2Entry.Resource as Patient; var version2Tag = version2Patient?.Meta.Tag?.FirstOrDefault(t => @@ -591,7 +609,7 @@ public async Task GivenAServerThatSupportsIt_WhenPatchingMetaTagWithWrongVersion Assert.Equal(HttpStatusCode.PreconditionFailed, exception.Response.StatusCode); } - [SkippableFact(Skip = "This test is skipped for STU3.")] + [SkippableFact] [Trait(Traits.Priority, Priority.One)] public async Task GivenAServerThatSupportsIt_WhenPatchingMetaTagMultipleTimes_ThenAllVersionsShouldBeInHistory() { diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/JsonPatchTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/JsonPatchTests.cs index 925af94f18..78e36a1512 100644 --- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/JsonPatchTests.cs +++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/JsonPatchTests.cs @@ -5,6 +5,7 @@ using System; using System.Net; +using System.Threading.Tasks; using Hl7.Fhir.ElementModel; using Hl7.Fhir.Model; using Microsoft.Health.Fhir.Client; @@ -391,5 +392,56 @@ public async Task GivenAPatchDocumentWithDate_WhenPassingDateTime_ThenExceptionS // DateTime with offset await Assert.ThrowsAsync(() => _client.JsonPatchAsync(response.Resource, patchDocument)); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [Trait(Traits.Priority, Priority.One)] + [HttpIntegrationFixtureArgumentSets(DataStore.SqlServer)] + public async Task GivenAResource_WhenJsonPatchingConditionallyWithMetaHistoryFlag_TheServerShouldRespectTheFlag(bool metaHistory) + { + var parser = new Hl7.Fhir.Serialization.FhirJsonParser(); + string adJson = "{\"resourceType\":\"ActivityDefinition\",\"status\":\"active\"}"; + var poco = parser. + Parse(adJson). + ToTypedElement(). + ToResourceElement(). + ToPoco(); + FhirResponse response = await _client.CreateAsync(poco); + + string patchDocument = + "[{\"op\":\"add\",\"path\":\"/meta/tag\",\"value\":{\"system\":\"http://example.org/fhir/tags\",\"code\":\"example-tag\"}}]"; + FhirResponse patchResponse = await _client.JsonPatchAsync( + response.Resource, + patchDocument, + metaHistory: metaHistory); + + ActivityDefinition ad = patchResponse.Resource; + Assert.Contains( + ad.Meta.Tag, + tag => tag.System == "http://example.org/fhir/tags" && tag.Code == "example-tag"); + + FhirResponse getResponse = await _client.ReadAsync( + ResourceType.ActivityDefinition, + ad.Id); + ActivityDefinition adFromGet = getResponse.Resource; + Assert.Contains( + adFromGet.Meta.Tag, + tag => tag.System == "http://example.org/fhir/tags" && tag.Code == "example-tag"); + + FhirResponse historyResponse = await _client.ReadHistoryAsync( + ResourceType.ActivityDefinition, + ad.Id); + if (metaHistory) + { + // There should be two entries in the history: one for the create, one for the patch + Assert.Equal(2, historyResponse.Resource.Entry.Count); + } + else + { + // There should be only one entry in the history: the create + Assert.Single(historyResponse.Resource.Entry); + } + } } }