diff --git a/src/Spark.Engine.Test/FhirResponseFactory/ConditionalHeaderFhirResponseInterceptorTests.cs b/src/Spark.Engine.Test/FhirResponseFactory/ConditionalHeaderFhirResponseInterceptorTests.cs new file mode 100644 index 000000000..221f60133 --- /dev/null +++ b/src/Spark.Engine.Test/FhirResponseFactory/ConditionalHeaderFhirResponseInterceptorTests.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Http; +using Spark.Engine.Core; +using Spark.Engine.Extensions; +using Spark.Engine.FhirResponseFactory; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Spark.Engine.Test.FhirResponseFactory; + +public class ConditionalHeaderFhirResponseInterceptorTests +{ + [Fact] + public void TestNotModified() + { + var versionId = "1"; + var etag = ETag.Create(versionId); + var lastUpdated = DateTimeOffset.UtcNow.AddMinutes(-5); + var context = new DefaultHttpContext(); + context.Request.Headers["If-None-Match"] = etag; + context.Request.Headers["If-Modified-Since"] = lastUpdated.ToString("R"); + var parameters = new ConditionalHeaderParameters(context.Request); + + Assert.Equal(parameters.IfModifiedSince.Value.ToString("R"), lastUpdated.ToString("R")); + Assert.Contains(parameters.IfNoneMatchTags, i => i == etag); + + var patient = new Hl7.Fhir.Model.Patient + { + Id = "1", + Meta = new Hl7.Fhir.Model.Meta + { + VersionId = versionId, + LastUpdated = lastUpdated + } + }; + + var interceptor = new ConditionalHeaderFhirResponseInterceptor(); + var response = interceptor.GetFhirResponse(Entry.PUT(Key.Create(patient.TypeName, patient.Id, patient.Meta.VersionId), patient), parameters); + Assert.NotNull(response); + Assert.True(response.StatusCode == System.Net.HttpStatusCode.NotModified); + } + + + [Fact] + public void TestModified() + { + var versionId = "1"; + var etag = ETag.Create(versionId); + var lastUpdated = DateTimeOffset.UtcNow.AddMinutes(-5); + var context = new DefaultHttpContext(); + context.Request.Headers["If-None-Match"] = etag; + context.Request.Headers["If-Modified-Since"] = lastUpdated.ToString("R"); + var parameters = new ConditionalHeaderParameters(context.Request); + + Assert.Equal(parameters.IfModifiedSince.Value.ToString("R"), lastUpdated.ToString("R")); + Assert.Contains(parameters.IfNoneMatchTags, i => i == etag); + + var patient = new Hl7.Fhir.Model.Patient + { + Id = "1", + Meta = new Hl7.Fhir.Model.Meta + { + VersionId = "2", + LastUpdated = DateTimeOffset.UtcNow + } + }; + + var interceptor = new ConditionalHeaderFhirResponseInterceptor(); + var response = interceptor.GetFhirResponse(Entry.PUT(Key.Create(patient.TypeName, patient.Id, patient.Meta.VersionId), patient), parameters); + Assert.Null(response); + } +} diff --git a/src/Spark.Engine/Extensions/ETag.cs b/src/Spark.Engine/Extensions/ETag.cs index cd84c5823..2cbfbdc01 100644 --- a/src/Spark.Engine/Extensions/ETag.cs +++ b/src/Spark.Engine/Extensions/ETag.cs @@ -10,9 +10,9 @@ namespace Spark.Engine.Extensions; public static class ETag { - public static EntityTagHeaderValue Create(string value) + public static string Create(string value) { string tag = "\"" + value + "\""; - return new EntityTagHeaderValue(tag, true); + return new EntityTagHeaderValue(tag, true).ToString(); } } diff --git a/src/Spark.Engine/Extensions/HttpRequestFhirExtensions.cs b/src/Spark.Engine/Extensions/HttpRequestFhirExtensions.cs index 1729065b9..4bf5a92e6 100644 --- a/src/Spark.Engine/Extensions/HttpRequestFhirExtensions.cs +++ b/src/Spark.Engine/Extensions/HttpRequestFhirExtensions.cs @@ -165,12 +165,12 @@ internal static void AcquireHeaders(this HttpResponse response, FhirResponse fhi { if (fhirResponse.Key != null) { - response.Headers.Append(HttpHeaderName.ETAG, ETag.Create(fhirResponse.Key.VersionId)?.ToString()); + response.Headers.Append(HttpHeaderName.ETAG, ETag.Create(fhirResponse.Key.VersionId)); Uri location = fhirResponse.Key.ToUri(); response.Headers.Append(HttpHeaderName.LOCATION, location.OriginalString); - if (response.ContentLength > 0) + if (fhirResponse.HasBody) { response.Headers.Append(HttpHeaderName.CONTENT_LOCATION, location.OriginalString); if (fhirResponse.Resource?.Meta?.LastUpdated != null) diff --git a/src/Spark.Engine/Extensions/InteractionExtensions.cs b/src/Spark.Engine/Extensions/InteractionExtensions.cs index ec9d5c016..7b8c599f1 100644 --- a/src/Spark.Engine/Extensions/InteractionExtensions.cs +++ b/src/Spark.Engine/Extensions/InteractionExtensions.cs @@ -79,7 +79,7 @@ public static Bundle.EntryComponent TranslateToSparseEntry(this Entry entry, Fhi { Status = string.Format("{0} {1}", (int) response.StatusCode, response.StatusCode), Location = response.Key?.ToString(), - Etag = response.Key != null ? ETag.Create(response.Key.VersionId).ToString() : null, + Etag = response.Key != null ? ETag.Create(response.Key.VersionId) : null, LastModified = (entry != null && entry.Resource != null && entry.Resource.Meta != null) ? entry.Resource.Meta.LastUpdated diff --git a/src/Spark.Engine/FhirResponseFactory/ConditionalHeaderFhirResponseInterceptor.cs b/src/Spark.Engine/FhirResponseFactory/ConditionalHeaderFhirResponseInterceptor.cs index a1d54a148..8df528db1 100644 --- a/src/Spark.Engine/FhirResponseFactory/ConditionalHeaderFhirResponseInterceptor.cs +++ b/src/Spark.Engine/FhirResponseFactory/ConditionalHeaderFhirResponseInterceptor.cs @@ -29,7 +29,7 @@ public FhirResponse GetFhirResponse(Entry entry, object input) ConditionalHeaderParameters parameters = ConvertInput(input); if (parameters == null) return null; - bool? matchTags = parameters.IfNoneMatchTags.Any() ? parameters.IfNoneMatchTags.Any(t => t == ETag.Create(entry.Key.VersionId).Tag) : (bool?)null; + bool? matchTags = parameters.IfNoneMatchTags.Any() ? parameters.IfNoneMatchTags.Any(t => t == ETag.Create(entry.Key.VersionId)) : (bool?)null; bool? matchModifiedDate = parameters.IfModifiedSince.HasValue ? parameters.IfModifiedSince.Value < entry.Resource.Meta.LastUpdated : (bool?) null; diff --git a/src/Spark.Web/Controllers/FhirController.cs b/src/Spark.Web/Controllers/FhirController.cs index a7d8d98f9..f110bd7e0 100644 --- a/src/Spark.Web/Controllers/FhirController.cs +++ b/src/Spark.Web/Controllers/FhirController.cs @@ -55,7 +55,10 @@ public async Task VRead(string type, string id, string vid) [HttpPut("{type}/{id?}")] public async Task> Update(string type, Resource resource, string id = null) { - string versionId = Request.GetTypedHeaders().IfMatch?.FirstOrDefault()?.Tag.Buffer; + string versionId = null; + var ifMatch = Request.GetTypedHeaders().IfMatch.FirstOrDefault(); + if (ifMatch is { Tag.Value: not null }) versionId = ifMatch.Tag.Value.Trim('"'); + Key key = Key.Create(type, id, versionId); if (key.HasResourceId()) { @@ -77,12 +80,14 @@ public async Task Create(string type, Resource resource) if (Request.Headers.ContainsKey(FhirHttpHeaders.IfNoneExist)) { - NameValueCollection searchQueryString = HttpUtility.ParseQueryString(Request.GetTypedHeaders().IfNoneExist()); + NameValueCollection searchQueryString = + HttpUtility.ParseQueryString(Request.GetTypedHeaders().IfNoneExist()); IEnumerable> searchValues = searchQueryString.Keys.Cast() .Select(k => new Tuple(k, searchQueryString[k])); - return await _fhirService.ConditionalCreateAsync(key, resource, SearchParams.FromUriParamList(searchValues)).ConfigureAwait(false); + return await _fhirService.ConditionalCreateAsync(key, resource, SearchParams.FromUriParamList(searchValues)) + .ConfigureAwait(false); } return await _fhirService.CreateAsync(key, resource).ConfigureAwait(false); @@ -243,4 +248,4 @@ public async Task Document(string id) Key key = Key.Create("Composition", id); return await _fhirService.DocumentAsync(key).ConfigureAwait(false); } -} \ No newline at end of file +}