diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs index 6453762017..b459789974 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs @@ -177,6 +177,7 @@ public async Task Create([FromBody] Resource resource) [ConditionalConstraint] [Route(KnownRoutes.ResourceType)] [AuditEventType(AuditEventSubType.ConditionalCreate)] + [ServiceFilter(typeof(SearchParameterFilterAttribute))] public async Task ConditionalCreate([FromBody] Resource resource) { StringValues conditionalCreateHeader = HttpContext.Request.Headers[KnownHeaders.IfNoneExist]; @@ -211,6 +212,7 @@ public async Task ConditionalCreate([FromBody] Resource resource) [ValidateResourceIdFilter] [Route(KnownRoutes.ResourceTypeById)] [AuditEventType(AuditEventSubType.Update)] + [ServiceFilter(typeof(SearchParameterFilterAttribute))] public async Task Update([FromBody] Resource resource, [ModelBinder(typeof(WeakETagBinder))] WeakETag ifMatchHeader) { BundleResourceContext bundleResourceContext = GetBundleResourceContext(); @@ -228,6 +230,7 @@ public async Task Update([FromBody] Resource resource, [ModelBind [HttpPut] [Route(KnownRoutes.ResourceType)] [AuditEventType(AuditEventSubType.ConditionalUpdate)] + [ServiceFilter(typeof(SearchParameterFilterAttribute))] public async Task ConditionalUpdate([FromBody] Resource resource) { IReadOnlyList> conditionalParameters = GetQueriesForSearch(); diff --git a/src/Microsoft.Health.Fhir.Tests.Common/Microsoft.Health.Fhir.Tests.Common.csproj b/src/Microsoft.Health.Fhir.Tests.Common/Microsoft.Health.Fhir.Tests.Common.csproj index 2a5ed37d84..e443645ffd 100644 --- a/src/Microsoft.Health.Fhir.Tests.Common/Microsoft.Health.Fhir.Tests.Common.csproj +++ b/src/Microsoft.Health.Fhir.Tests.Common/Microsoft.Health.Fhir.Tests.Common.csproj @@ -85,6 +85,9 @@ + + + diff --git a/src/Microsoft.Health.Fhir.Tests.Common/TestFiles/Normative/SearchParameterDuplicatedConditionalCreate.json b/src/Microsoft.Health.Fhir.Tests.Common/TestFiles/Normative/SearchParameterDuplicatedConditionalCreate.json new file mode 100644 index 0000000000..901ad08de4 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Tests.Common/TestFiles/Normative/SearchParameterDuplicatedConditionalCreate.json @@ -0,0 +1,17 @@ +{ + "resourceType": "SearchParameter", + "meta": { + "versionId": "3", + "lastUpdated": "2023-04-04T19:31:09.483+00:00" + }, + "version": "1.0.0", + "name": "code", + "status": "active", + "description": "Observation filtering based on code", + "code": "code", + "base": [ + "Observation" + ], + "type": "token", + "expression": "Observation.code.coding.where(system = 'http://loinc.org/' or system = 'http://loinc.org').code" +} diff --git a/src/Microsoft.Health.Fhir.Tests.Common/TestFiles/Normative/SearchParameterDuplicatedConditionalUpdate.json b/src/Microsoft.Health.Fhir.Tests.Common/TestFiles/Normative/SearchParameterDuplicatedConditionalUpdate.json new file mode 100644 index 0000000000..68f4512c3a --- /dev/null +++ b/src/Microsoft.Health.Fhir.Tests.Common/TestFiles/Normative/SearchParameterDuplicatedConditionalUpdate.json @@ -0,0 +1,13 @@ +{ + "resourceType": "SearchParameter", + "version": "4.0.1", + "name": "subject", + "status": "active", + "description": "Who this goal is intended for", + "code": "subject", + "base": [ + "Goal" + ], + "type": "reference", + "expression": "Goal.subject" +} diff --git a/src/Microsoft.Health.Fhir.Tests.Common/TestFiles/Normative/SearchParameterDuplicatedUpdate.json b/src/Microsoft.Health.Fhir.Tests.Common/TestFiles/Normative/SearchParameterDuplicatedUpdate.json new file mode 100644 index 0000000000..3bff0b7e39 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Tests.Common/TestFiles/Normative/SearchParameterDuplicatedUpdate.json @@ -0,0 +1,13 @@ +{ + "resourceType": "SearchParameter", + "version": "4.0.1", + "name": "relation", + "status": "draft", + "description": "replaces | transforms | signs | appends", + "code": "relation", + "base": [ + "DocumentReference" + ], + "type": "token", + "expression": "DocumentReference.relatesTo.code" +} diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/ConditionalCreateTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/ConditionalCreateTests.cs index a9c77014b6..04ac6ae921 100644 --- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/ConditionalCreateTests.cs +++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/ConditionalCreateTests.cs @@ -140,5 +140,81 @@ public async Task GivenAResource_WhenCreatingConditionallyWithEmptyIfNoneHeader_ Assert.Single(exception.OperationOutcome.Issue); Assert.Equal(exception.Response.Resource.Issue[0].Diagnostics, string.Format(Core.Resources.ConditionalOperationNotSelectiveEnough, "Observation")); } + + [Fact] + [Trait(Traits.Priority, Priority.One)] + public async Task GivenAResource_WhenCreatingConditionallyANewDuplicatedSearchParameterResourceWithSameUrl_TheServerShouldFail() + { + /* When the server starts, search-parameters.json files are loaded and the default search parameters + * are created. The search parameter with the code 'code' and base 'Observation' already exists with + * the url http://hl7.org/fhir/SearchParameter/clinical-code */ + + var resourceToCreate = Samples.GetJsonSample("SearchParameterDuplicatedConditionalCreate"); + resourceToCreate.Id = null; + resourceToCreate.Url = "http://hl7.org/fhir/SearchParameter/clinical-code"; // Same Url than the default one + + // For calling a Conditional Create we do need to send a conditionalCreateCriteria which in this case is the url. + using FhirClientException ex = await Assert.ThrowsAsync(() => _client.CreateAsync( + resourceToCreate, + $"url={resourceToCreate.Url}")); + + var expectedSubstring = "A search parameter with the same definition URL 'http://hl7.org/fhir/SearchParameter/clinical-code' already exists."; + Assert.Equal(HttpStatusCode.BadRequest, ex.StatusCode); + Assert.Contains(expectedSubstring, ex.Message); + } + + [Fact] + [Trait(Traits.Priority, Priority.One)] + public async Task GivenAResource_WhenCreatingConditionallyANewDuplicatedSearchParameterResourceWithUrl_TheServerShouldFail() + { + /* When the server starts, search-parameters.json files are loaded and the default search parameters + * are created. The search parameter with the code 'code' and base 'Observation' already exists with + * the url http://hl7.org/fhir/SearchParameter/clinical-code */ + + var resourceToCreate = Samples.GetJsonSample("SearchParameterDuplicatedConditionalCreate"); + resourceToCreate.Id = null; + resourceToCreate.Url = "http://fhir.medlix.org/SearchParameter/code-observation-test-conditional-create-url"; + + // For calling a Conditional Create we do need to send a conditionalCreateCriteria which in this case is the url. + using FhirClientException ex = await Assert.ThrowsAsync(() => _client.CreateAsync( + resourceToCreate, + $"url={resourceToCreate.Url}")); + + var expectedSubstring = "A search parameter with the same code value 'code' already exists for base type 'Observation'"; + Assert.Equal(HttpStatusCode.BadRequest, ex.StatusCode); + Assert.Contains(expectedSubstring, ex.Message); + + /* If there is a Search parameter alredy defined with the same url, this test will fail because the ex.Message is different, + * in that case we should received (example, URL may change): + * "A search parameter with the same definition URL 'http://fhir.medlix.org/SearchParameter/code-observation-test-conditional-create-url' already exists. + */ + } + + [Fact] + [Trait(Traits.Priority, Priority.One)] + public async Task GivenAResource_WhenCreatingConditionallyANewDuplicatedSearchParameterResourceWithCode_TheServerShouldFail() + { + /* When the server starts, search-parameters.json files are loaded and the default search parameters + * are created. The search parameter with the code 'code' and base 'Observation' already exists with + * the url http://hl7.org/fhir/SearchParameter/clinical-code */ + + var resourceToCreate = Samples.GetJsonSample("SearchParameterDuplicatedConditionalCreate"); + resourceToCreate.Id = null; + resourceToCreate.Url = "http://hl7.org/fhir/SearchParameter/code-observation-test-conditional-create-code"; + + // For calling a Conditional Create we do need to send a conditionalCreateCriteria which in this case is the base. + using FhirClientException ex = await Assert.ThrowsAsync(() => _client.CreateAsync( + resourceToCreate, + $"code=code")); + + var expectedSubstring = "A search parameter with the same code value 'code' already exists for base type 'Observation'"; + Assert.Equal(HttpStatusCode.BadRequest, ex.StatusCode); + Assert.Contains(expectedSubstring, ex.Message); + + /* If there is a Search parameter alredy defined with the same url, this test will fail because the ex.Message is different, + * in that case we should received (example, URL may change): + * "A search parameter with the same definition URL 'http://fhir.medlix.org/SearchParameter/code-observation-test-conditional-create-code' already exists. + */ + } } } diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/ConditionalUpdateTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/ConditionalUpdateTests.cs index 4c115e1409..b6af785d4d 100644 --- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/ConditionalUpdateTests.cs +++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/ConditionalUpdateTests.cs @@ -230,5 +230,97 @@ public async Task GivenAResource_WhenUpsertingConditionallyWithMultipleMatches_T Assert.Equal(HttpStatusCode.PreconditionFailed, exception.Response.StatusCode); } + + [Fact] + [Trait(Traits.Priority, Priority.One)] + public async Task GivenAResource_WhenUpdatingConditionallyANewDuplicatedSearchParameterResourceWithSameUrl_TheServerShouldFail() + { + /* When the server starts, search-parameters.json files are loaded and the default search parameters + * are created. The search parameter with the code 'subject' and base 'Goal' already exists with + * the url http://hl7.org/fhir/SearchParameter/Goal-subject */ + + var resourceToCreate = Samples.GetJsonSample("SearchParameterDuplicatedConditionalUpdate"); + resourceToCreate.Id = null; + resourceToCreate.Url = "http://hl7.org/fhir/SearchParameter/Goal-subject"; // Same url than the default one + + using FhirClientException ex = await Assert.ThrowsAsync(() => _client.ConditionalUpdateAsync( + resourceToCreate, + $"url={resourceToCreate.Url}")); + + var operationOutcome = ex.OperationOutcome; + Assert.Equal(HttpStatusCode.BadRequest, ex.StatusCode); + Assert.NotNull(operationOutcome); + Assert.NotEmpty(operationOutcome.Issue); + Assert.Single(operationOutcome.Issue); + + var expectedError = "A search parameter with the same code value 'subject' already exists for base type 'Goal'."; + Assert.Contains(expectedError, operationOutcome.Issue[0].Diagnostics); + } + + [Fact] + [Trait(Traits.Priority, Priority.One)] + public async Task GivenAResource_WhenUpdatingConditionallyANewDuplicatedSearchParameterResourceWithUrl_TheServerShouldFail() + { + /* When the server starts, search-parameters.json files are loaded and the default search parameters + * are created. The search parameter with the code 'subject' and base 'Goal' already exists with + * the url http://hl7.org/fhir/SearchParameter/Goal-subject */ + + var resourceToCreate = Samples.GetJsonSample("SearchParameterDuplicatedConditionalUpdate"); + resourceToCreate.Id = null; + resourceToCreate.Url = "http://hl7.org/fhir/SearchParameter/subject-goal-test-conditional-update-url"; + + using FhirClientException ex = await Assert.ThrowsAsync(() => _client.ConditionalUpdateAsync( + resourceToCreate, + $"url={resourceToCreate.Url}")); + + var operationOutcome = ex.OperationOutcome; + Assert.Equal(HttpStatusCode.BadRequest, ex.StatusCode); + Assert.NotNull(operationOutcome); + Assert.NotEmpty(operationOutcome.Issue); + Assert.Equal(2, operationOutcome.Issue.Count); + + var firstIssue = "A search parameter with Uri 'http://hl7.org/fhir/SearchParameter/subject-goal-test-conditional-update-url' was not found."; + var secondIssue = "A search parameter with the same code value 'subject' already exists for base type 'Goal'."; + + Assert.Contains(firstIssue, operationOutcome.Issue[0].Diagnostics); + Assert.Contains(secondIssue, operationOutcome.Issue[1].Diagnostics); + + /* If a search parameter with the url http://hl7.org/fhir/SearchParameter/subject-goal-test-conditional-update-url already exists + * this test will fail because the first Issue will not be shown in the OperationOutcome. + */ + } + + [Fact] + [Trait(Traits.Priority, Priority.One)] + public async Task GivenAResource_WhenUpdatingConditionallyANewDuplicatedSearchParameterResourceWithCode_TheServerShouldFail() + { + /* When the server starts, search-parameters.json files are loaded and the default search parameters + * are created. The search parameter with the code 'subject' and base 'Goal' already exists with + * the url http://hl7.org/fhir/SearchParameter/Goal-subject */ + + var resourceToCreate = Samples.GetJsonSample("SearchParameterDuplicatedConditionalUpdate"); + resourceToCreate.Id = null; + resourceToCreate.Url = "http://hl7.org/fhir/SearchParameter/subject-goal-test-conditional-update-code"; + + using FhirClientException ex = await Assert.ThrowsAsync(() => _client.ConditionalUpdateAsync( + resourceToCreate, + $"code=subject")); + + var operationOutcome = ex.OperationOutcome; + Assert.Equal(HttpStatusCode.BadRequest, ex.StatusCode); + Assert.NotNull(operationOutcome); + Assert.NotEmpty(operationOutcome.Issue); + Assert.Equal(2, operationOutcome.Issue.Count); + + var firstIssue = "A search parameter with Uri 'http://hl7.org/fhir/SearchParameter/subject-goal-test-conditional-update-code' was not found."; + var secondIssue = "A search parameter with the same code value 'subject' already exists for base type 'Goal'."; + + Assert.Contains(firstIssue, operationOutcome.Issue[0].Diagnostics); + Assert.Contains(secondIssue, operationOutcome.Issue[1].Diagnostics); + + /* If a search parameter with the url http://hl7.org/fhir/SearchParameter/subject-goal-test-conditional-update-code already exists + * this test will fail because the first Issue will not be shown in the OperationOutcome. + */ + } } } diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/UpdateTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/UpdateTests.cs index 3503c27e46..f2386120d7 100644 --- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/UpdateTests.cs +++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/UpdateTests.cs @@ -261,6 +261,67 @@ public async Task GivenTheResource_WhenUpdatingAnExistingResourceWithNoDataChang Assert.Contains(updateResponseAfterMetaUpdated.Resource.Meta.Tag, t => t.Code == "TestCode2"); } + [Fact] + [Trait(Traits.Priority, Priority.One)] + public async Task GivenTheResource_WhenUpdatingANewDuplicatedSearchParameterResourceWithSameUrl_TheServerShouldFail() + { + /* When the server starts, search-parameters.json files are loaded and the default search parameters + * are created. The search parameter with the code 'description' and base 'ResearchDefinition' already exists with + * the url http://hl7.org/fhir/SearchParameter/DocumentReference-relation */ + + var id = Guid.NewGuid(); + var resourceToCreate = Samples.GetJsonSample("SearchParameterDuplicatedUpdate"); + resourceToCreate.Id = id.ToString(); + resourceToCreate.Url = "http://hl7.org/fhir/SearchParameter/DocumentReference-relation"; + + using FhirClientException ex = await Assert.ThrowsAsync(() => _client.UpdateAsync( + $"SearchParameter/{id}", + resourceToCreate)); + + var operationOutcome = ex.OperationOutcome; + Assert.Equal(HttpStatusCode.BadRequest, ex.StatusCode); + Assert.NotNull(operationOutcome); + Assert.NotEmpty(operationOutcome.Issue); + Assert.Single(operationOutcome.Issue); + + var expectedError = "A search parameter with the same code value 'relation' already exists for base type 'DocumentReference'."; + Assert.Contains(expectedError, operationOutcome.Issue[0].Diagnostics); + } + + [Fact] + [Trait(Traits.Priority, Priority.One)] + public async Task GivenTheResource_WhenUpdatingANewDuplicatedSearchParameterResourceWithUrl_TheServerShouldFail() + { + /* When the server starts, search-parameters.json files are loaded and the default search parameters + * are created. The search parameter with the code 'description' and base 'ResearchDefinition' already exists with + * the url http://hl7.org/fhir/SearchParameter/DocumentReference-relation */ + + var id = Guid.NewGuid(); + var resourceToCreate = Samples.GetJsonSample("SearchParameterDuplicatedUpdate"); + resourceToCreate.Id = id.ToString(); + resourceToCreate.Url = "http://hl7.org/fhir/SearchParameter/relation-DocumentReference-test-update-url"; + + using FhirClientException ex = await Assert.ThrowsAsync(() => _client.UpdateAsync( + $"SearchParameter/{id}", + resourceToCreate)); + + var operationOutcome = ex.OperationOutcome; + Assert.Equal(HttpStatusCode.BadRequest, ex.StatusCode); + Assert.NotNull(operationOutcome); + Assert.NotEmpty(operationOutcome.Issue); + Assert.Equal(2, operationOutcome.Issue.Count); + + var firstIssue = "A search parameter with Uri 'http://hl7.org/fhir/SearchParameter/relation-DocumentReference-test-update-url' was not found."; + var secondIssue = "A search parameter with the same code value 'relation' already exists for base type 'DocumentReference'."; + + Assert.Contains(firstIssue, operationOutcome.Issue[0].Diagnostics); + Assert.Contains(secondIssue, operationOutcome.Issue[1].Diagnostics); + + /* If a search parameter with the url http://hl7.org/fhir/SearchParameter/relation-DocumentReference-test-update-url already exists + * this test will fail because the first Issue will not be shown in the OperationOutcome. + */ + } + private static void ValidateUpdateResponse(Observation oldResource, FhirResponse newResponse, bool same, HttpStatusCode expectedStatusCode) { Assert.Equal(expectedStatusCode, newResponse.StatusCode);