Skip to content

Commit

Permalink
Added query param support and validation for export request. (#438)
Browse files Browse the repository at this point in the history
Added query param support and validation for export request. We support (and require) _destinationType and _destinationConnectionSettings. Added a new ISecretStore and corresponding Azure KeyVault based implementation to store this information.
  • Loading branch information
namadabu authored and jackliums committed May 14, 2019
1 parent 868d1c5 commit 5b343c4
Show file tree
Hide file tree
Showing 36 changed files with 795 additions and 177 deletions.
7 changes: 7 additions & 0 deletions Microsoft.Health.Fhir.sln
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Extensions
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Fhir.SqlServer.UnitTests", "src\Microsoft.Health.Fhir.SqlServer.UnitTests\Microsoft.Health.Fhir.SqlServer.UnitTests.csproj", "{E02E5224-32CD-490F-B1E5-8509AD669334}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Fhir.KeyVault", "src\Microsoft.Health.Fhir.KeyVault\Microsoft.Health.Fhir.KeyVault.csproj", "{5A7E7D1B-B307-45FD-8230-C4E331EAB2CD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -168,6 +170,10 @@ Global
{E02E5224-32CD-490F-B1E5-8509AD669334}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E02E5224-32CD-490F-B1E5-8509AD669334}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E02E5224-32CD-490F-B1E5-8509AD669334}.Release|Any CPU.Build.0 = Release|Any CPU
{5A7E7D1B-B307-45FD-8230-C4E331EAB2CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5A7E7D1B-B307-45FD-8230-C4E331EAB2CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A7E7D1B-B307-45FD-8230-C4E331EAB2CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A7E7D1B-B307-45FD-8230-C4E331EAB2CD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -198,6 +204,7 @@ Global
{87849B3F-5D12-41CA-A082-FAC065EF9FD8} = {8AD2A324-DAB5-4380-94A5-31F7D817C384}
{4DDF9576-E22C-460A-937F-0EE0FEA6DB87} = {8AD2A324-DAB5-4380-94A5-31F7D817C384}
{E02E5224-32CD-490F-B1E5-8509AD669334} = {8AD2A324-DAB5-4380-94A5-31F7D817C384}
{5A7E7D1B-B307-45FD-8230-C4E331EAB2CD} = {8AD2A324-DAB5-4380-94A5-31F7D817C384}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
RESX_SortFileContentOnSave = True
Expand Down
3 changes: 2 additions & 1 deletion samples/templates/default-azuredeploy.json
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,8 @@
"permissions": {
"secrets": [
"get",
"list"
"list",
"set"
]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public class ExportControllerTests
private IFhirRequestContextAccessor _fhirRequestContextAccessor = Substitute.For<IFhirRequestContextAccessor>();
private IUrlResolver _urlResolver = Substitute.For<IUrlResolver>();

private const string DestinationType = "destinationType";
private const string DestinationConnection = "destinationConnection";

public ExportControllerTests()
{
_exportEnabledController = GetController(new ExportJobConfiguration() { Enabled = true });
Expand All @@ -36,7 +39,7 @@ public async Task GivenAnExportRequest_WhenDisabled_ThenRequestNotValidException
{
var exportController = GetController(new ExportJobConfiguration() { Enabled = false });

await Assert.ThrowsAsync<RequestNotValidException>(() => exportController.Export());
await Assert.ThrowsAsync<RequestNotValidException>(() => exportController.Export(DestinationType, DestinationConnection));
}

[Fact]
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System.Collections.Generic;
using Hl7.Fhir.Rest;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Health.Fhir.Api.Features.Filters;
using Microsoft.Health.Fhir.Core.Configs;
using Microsoft.Health.Fhir.Core.Exceptions;
using Microsoft.Health.Fhir.Core.Features;
using Microsoft.Net.Http.Headers;
using NSubstitute;
using Xunit;

namespace Microsoft.Health.Fhir.Api.UnitTests.Features.Filters
{
public class ValidateExportRequestFilterAttributeTests
{
private const string CorrectAcceptHeaderValue = ContentType.JSON_CONTENT_HEADER;
private const string CorrectPreferHeaderValue = "respond-async";
private const string PreferHeaderName = "Prefer";
private const string SupportedDestinationType = "AnySupportedDestinationType";

[Theory]
[InlineData("application/fhir+xml")]
[InlineData("application/xml")]
[InlineData("text/xml")]
[InlineData("application/json")]
[InlineData("*/*")]
public void GiveARequestWithInvalidAcceptHeader_WhenGettingAnExportOperationRequest_ThenARequestNotValidExceptionShouldBeThrown(string acceptHeader)
{
var filter = GetFilter();
var context = CreateContextWithParams();

context.HttpContext.Request.Headers.Add(HeaderNames.Accept, acceptHeader);

Assert.Throws<RequestNotValidException>(() => filter.OnActionExecuting(context));
}

[Fact]
public void GiveARequestWithNoAcceptHeader_WhenGettingAnExportOperationRequest_ThenARequestNotValidExceptionShouldBeThrown()
{
var filter = GetFilter();
var context = CreateContextWithParams();

context.HttpContext.Request.Headers.Add(PreferHeaderName, CorrectPreferHeaderValue);

Assert.Throws<RequestNotValidException>(() => filter.OnActionExecuting(context));
}

[Theory]
[InlineData("respond-async, wait = 10")]
[InlineData("return-content")]
[InlineData("*")]
public void GiveARequestWithInvalidPreferHeader_WhenGettingAnExportOperationRequest_ThenARequestNotValidExceptionShouldBeThrown(string preferHeader)
{
var filter = GetFilter();
var context = CreateContextWithParams();

context.HttpContext.Request.Headers.Add(PreferHeaderName, preferHeader);

Assert.Throws<RequestNotValidException>(() => filter.OnActionExecuting(context));
}

[Fact]
public void GiveARequestWithNoPreferHeader_WhenGettingAnExportOperationRequest_ThenARequestNotValidExceptionShouldBeThrown()
{
var filter = GetFilter();
var context = CreateContextWithParams();

context.HttpContext.Request.Headers.Add(HeaderNames.Accept, CorrectAcceptHeaderValue);

Assert.Throws<RequestNotValidException>(() => filter.OnActionExecuting(context));
}

[Fact]
public void GiveARequestWithValidAcceptAndPreferHeader_WhenGettingAnExportOperationRequest_ThenTheResultIsSuccessful()
{
var filter = GetFilter();
var context = CreateContextWithParams();

context.HttpContext.Request.Headers.Add(HeaderNames.Accept, CorrectAcceptHeaderValue);
context.HttpContext.Request.Headers.Add(PreferHeaderName, CorrectPreferHeaderValue);

filter.OnActionExecuting(context);
}

[Fact]
public void GivenARequestWithCorrectHeadersAndMissingDestinationTypeParam_WhenGettingAnExportOperationRequest_ThenARequestNotValidExceptionShouldBeThrown()
{
var filter = GetFilter();
var queryParams = new Dictionary<string, StringValues>()
{
{ KnownQueryParameterNames.DestinationConnectionSettings, "destination" },
};

var context = CreateContextWithParams(queryParams);

context.HttpContext.Request.Headers.Add(HeaderNames.Accept, CorrectAcceptHeaderValue);
context.HttpContext.Request.Headers.Add(PreferHeaderName, CorrectPreferHeaderValue);

Assert.Throws<RequestNotValidException>(() => filter.OnActionExecuting(context));
}

[Fact]
public void GivenARequestWithCorrectHeadersAndMissingDestinationConnectionParam_WhenGettingAnExportOperationRequest_ThenARequestNotValidExceptionShouldBeThrown()
{
var filter = GetFilter();
var queryParams = new Dictionary<string, StringValues>()
{
{ KnownQueryParameterNames.DestinationType, SupportedDestinationType },
};

var context = CreateContextWithParams(queryParams);

context.HttpContext.Request.Headers.Add(HeaderNames.Accept, CorrectAcceptHeaderValue);
context.HttpContext.Request.Headers.Add(PreferHeaderName, CorrectPreferHeaderValue);

Assert.Throws<RequestNotValidException>(() => filter.OnActionExecuting(context));
}

[Fact]
public void GivenARequestWithCorrectHeadersAndDestinationTypeThatIsNotSupported_WhenGettingAnExportOperationRequest_ThenARequestNotValidExceptionShouldBeThrown()
{
var filter = GetFilter();
var queryParams = new Dictionary<string, StringValues>()
{
{ KnownQueryParameterNames.DestinationType, "Azure" },
{ KnownQueryParameterNames.DestinationConnectionSettings, "destination" },
};

var context = CreateContextWithParams(queryParams);

context.HttpContext.Request.Headers.Add(HeaderNames.Accept, CorrectAcceptHeaderValue);
context.HttpContext.Request.Headers.Add(PreferHeaderName, CorrectPreferHeaderValue);

Assert.Throws<RequestNotValidException>(() => filter.OnActionExecuting(context));
}

[Fact]
public void GivenARequestWithCorrectHeadersAndUnsupportedQueryParameter_WhenGettingAnExportOperationRequest_ThenARequestNotValidExceptionShouldBeThrown()
{
var filter = GetFilter();
var queryParams = new Dictionary<string, StringValues>()
{
{ KnownQueryParameterNames.DestinationType, SupportedDestinationType },
{ KnownQueryParameterNames.DestinationConnectionSettings, "destination" },
{ KnownQueryParameterNames.Since, "forever" },
};

var context = CreateContextWithParams(queryParams);

context.HttpContext.Request.Headers.Add(HeaderNames.Accept, CorrectAcceptHeaderValue);
context.HttpContext.Request.Headers.Add(PreferHeaderName, CorrectPreferHeaderValue);

Assert.Throws<RequestNotValidException>(() => filter.OnActionExecuting(context));
}

private static ActionExecutingContext CreateContextWithParams(Dictionary<string, StringValues> queryParams = null)
{
var context = new ActionExecutingContext(
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()),
new List<IFilterMetadata>(),
new Dictionary<string, object>(),
FilterTestsHelper.CreateMockExportController());

if (queryParams == null)
{
queryParams = new Dictionary<string, StringValues>();
queryParams.Add(KnownQueryParameterNames.DestinationType, SupportedDestinationType);
queryParams.Add(KnownQueryParameterNames.DestinationConnectionSettings, "connectionString");
}

context.HttpContext.Request.Query = new QueryCollection(queryParams);
return context;
}

private static ValidateExportRequestFilterAttribute GetFilter(ExportJobConfiguration exportJobConfig = null)
{
if (exportJobConfig == null)
{
exportJobConfig = new ExportJobConfiguration();
exportJobConfig.Enabled = true;
exportJobConfig.SupportedDestinations.Add(SupportedDestinationType);
}

var opConfig = new OperationsConfiguration()
{
Export = exportJobConfig,
};

IOptions<OperationsConfiguration> options = Substitute.For<IOptions<OperationsConfiguration>>();
options.Value.Returns(opConfig);

return new ValidateExportRequestFilterAttribute(options);
}
}
}
Loading

0 comments on commit 5b343c4

Please sign in to comment.