From b1653b25e470a066d30800b1bb78bc47ec8c134e Mon Sep 17 00:00:00 2001 From: Kim Santiago <31145923+kisantia@users.noreply.github.com> Date: Wed, 4 Aug 2021 13:02:52 -0700 Subject: [PATCH] Insert sql bindings into Azure Functions (#1224) * getting table name from a script * add InsertSqlInputBindingOperation * cleanup * move azure functions stuff out of dacfx service * cleanup * add tests * add another test * cleanup * add comments and connection string setting * addressing comments * change name to use add instead of insert --- Packages.props | 3 + .../AzureFunctions/AddSqlBindingOperation.cs | 143 ++++++++++++++++++ .../AzureFunctions/AzureFunctionsService.cs | 56 +++++++ .../Contracts/AddSqlBindingRequest.cs | 60 ++++++++ .../HostLoader.cs | 4 + .../Localization/sr.cs | 16 ++ .../Localization/sr.resx | 10 ++ .../Localization/sr.strings | 7 +- .../Localization/sr.xlf | 10 ++ .../Microsoft.SqlTools.ServiceLayer.csproj | 3 + .../AzureFunctionsInputBinding.ts | 59 ++++++++ .../AzureFunctionsMultipleSameFunction.ts | 59 ++++++++ .../AzureFunctionsNoBindings.ts | 59 ++++++++ .../AzureFunctionsOutputBinding.ts | 59 ++++++++ .../AzureFunctionsServiceTests.cs | 135 +++++++++++++++++ 15 files changed, 682 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/AddSqlBindingOperation.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/AzureFunctionsService.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/Contracts/AddSqlBindingRequest.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsInputBinding.ts create mode 100644 test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsMultipleSameFunction.ts create mode 100644 test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsNoBindings.ts create mode 100644 test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsOutputBinding.ts create mode 100644 test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionsServiceTests.cs diff --git a/Packages.props b/Packages.props index 012b9981..a144a0f1 100644 --- a/Packages.props +++ b/Packages.props @@ -26,6 +26,9 @@ + + + diff --git a/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/AddSqlBindingOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/AddSqlBindingOperation.cs new file mode 100644 index 00000000..b3b25b81 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/AddSqlBindingOperation.cs @@ -0,0 +1,143 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.Utility; +using Microsoft.CodeAnalysis.CSharp; + +namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions +{ + /// + /// Class to represent inserting a sql binding into an Azure Function + /// + class AddSqlBindingOperation + { + const string functionAttributeText = "FunctionName"; + + public AddSqlBindingParams Parameters { get; } + + public AddSqlBindingOperation(AddSqlBindingParams parameters) + { + Validate.IsNotNull("parameters", parameters); + this.Parameters = parameters; + } + + public ResultStatus AddBinding() + { + try + { + string text = File.ReadAllText(Parameters.filePath); + + SyntaxTree tree = CSharpSyntaxTree.ParseText(text); + CompilationUnitSyntax root = tree.GetCompilationUnitRoot(); + + // look for Azure Function to update + IEnumerable azureFunctionMethods = from methodDeclaration in root.DescendantNodes().OfType() + where methodDeclaration.AttributeLists.Count > 0 + where methodDeclaration.AttributeLists.Where(a => a.Attributes.Where(attr => attr.Name.ToString().Contains(functionAttributeText) && attr.ArgumentList.Arguments.First().ToString().Equals($"\"{Parameters.functionName}\"")).Count() == 1).Count() == 1 + select methodDeclaration; + + if (azureFunctionMethods.Count() == 0) + { + return new ResultStatus() + { + Success = false, + ErrorMessage = SR.CouldntFindAzureFunction(Parameters.functionName, Parameters.filePath) + }; + } + else if (azureFunctionMethods.Count() > 1) + { + return new ResultStatus() + { + Success = false, + ErrorMessage = SR.MoreThanOneAzureFunctionWithName(Parameters.functionName, Parameters.filePath) + }; + } + + MethodDeclarationSyntax azureFunction = azureFunctionMethods.First(); + var newParam = this.Parameters.bindingType == BindingType.input ? this.GenerateInputBinding() : this.GenerateOutputBinding(); + + // Generate updated method with the new parameter + // normalizewhitespace gets rid of any newline whitespace in the leading trivia, so we add that back + var updatedMethod = azureFunction.AddParameterListParameters(newParam).NormalizeWhitespace().WithLeadingTrivia(azureFunction.GetLeadingTrivia()).WithTrailingTrivia(azureFunction.GetTrailingTrivia()); + + // Replace the node in the tree + root = root.ReplaceNode(azureFunction, updatedMethod); + + // write updated tree to file + var workspace = new AdhocWorkspace(); + var syntaxTree = CSharpSyntaxTree.ParseText(root.ToString()); + var formattedNode = CodeAnalysis.Formatting.Formatter.Format(syntaxTree.GetRoot(), workspace); + StringBuilder sb = new StringBuilder(formattedNode.ToString()); + string content = sb.ToString(); + File.WriteAllText(Parameters.filePath, content); + + return new ResultStatus() + { + Success = true + }; + } + catch (Exception e) + { + return new ResultStatus() + { + Success = false, + ErrorMessage = e.ToString() + }; + } + } + + /// + /// Generates a parameter for the sql input binding that looks like + /// [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable result + /// + private ParameterSyntax GenerateInputBinding() + { + // Create arguments for the Sql Input Binding attribute + var argumentList = SyntaxFactory.AttributeArgumentList(); + argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.IdentifierName($"\"select * from {Parameters.objectName}\""))); + argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName("CommandType"), SyntaxFactory.IdentifierName("System.Data.CommandType.Text")))); + argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName("ConnectionStringSetting"), SyntaxFactory.IdentifierName($"\"{Parameters.connectionStringSetting}\"")))); + + // Create Sql Binding attribute + SyntaxList attributesList = new SyntaxList(); + attributesList = attributesList.Add(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Sql")).WithArgumentList(argumentList)))); + + // Create new parameter + ParameterSyntax newParam = SyntaxFactory.Parameter(attributesList, new SyntaxTokenList(), SyntaxFactory.ParseTypeName("IEnumerable"), SyntaxFactory.Identifier("result"), null); + return newParam; + } + + /// + /// Generates a parameter for the sql output binding that looks like + /// [Sql("[dbo].[table1]", ConnectionStringSetting = "SqlConnectionString")] out Object output + /// + private ParameterSyntax GenerateOutputBinding() + { + // Create arguments for the Sql Output Binding attribute + var argumentList = SyntaxFactory.AttributeArgumentList(); + argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.IdentifierName($"\"{Parameters.objectName}\""))); + argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName("ConnectionStringSetting"), SyntaxFactory.IdentifierName($"\"{Parameters.connectionStringSetting}\"")))); + + SyntaxList attributesList = new SyntaxList(); + attributesList = attributesList.Add(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Sql")).WithArgumentList(argumentList)))); + + var syntaxTokenList = new SyntaxTokenList(); + syntaxTokenList = syntaxTokenList.Add(SyntaxFactory.Token(SyntaxKind.OutKeyword)); + + ParameterSyntax newParam = SyntaxFactory.Parameter(attributesList, syntaxTokenList, SyntaxFactory.ParseTypeName(typeof(Object).Name), SyntaxFactory.Identifier("output"), null); + return newParam; + } + } +} + diff --git a/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/AzureFunctionsService.cs b/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/AzureFunctionsService.cs new file mode 100644 index 00000000..41aeec1d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/AzureFunctionsService.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using System; +using System.Threading.Tasks; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts; +using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions +{ + /// + /// Main class for Azure Functions service + /// + class AzureFunctionsService + { + private static readonly Lazy instance = new Lazy(() => new AzureFunctionsService()); + + /// + /// Gets the singleton instance object + /// + public static AzureFunctionsService Instance + { + get { return instance.Value; } + } + + /// + /// Initializes the service instance + /// + /// + public void InitializeService(ServiceHost serviceHost) + { + serviceHost.SetRequestHandler(AddSqlBindingRequest.Type, this.HandleAddSqlBindingRequest); + } + + /// + /// Handles request to add sql binding into Azure Functions + /// + public async Task HandleAddSqlBindingRequest(AddSqlBindingParams parameters, RequestContext requestContext) + { + try + { + AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters); + ResultStatus result = operation.AddBinding(); + + await requestContext.SendResult(result); + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/Contracts/AddSqlBindingRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/Contracts/AddSqlBindingRequest.cs new file mode 100644 index 00000000..88171e66 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/Contracts/AddSqlBindingRequest.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using System.Collections.Generic; +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts +{ + /// + /// Binding types for sql bindings for Azure Functions + /// + public enum BindingType + { + input, + output + } + + /// + /// Parameters for adding a sql binding + /// + public class AddSqlBindingParams + { + /// + /// Gets or sets the filePath + /// + public string filePath { get; set; } + + /// + /// Gets or sets the binding type + /// + public BindingType bindingType { get; set; } + + /// + /// Gets or sets the function name + /// + public string functionName { get; set; } + + /// + /// Gets or sets the object name + /// + public string objectName { get; set; } + + /// + /// Gets or sets the connection string setting + /// + public string connectionStringSetting { get; set; } + } + + /// + /// Defines the Add Sql Binding request + /// + class AddSqlBindingRequest + { + public static readonly RequestType Type = + RequestType.Create("azureFunctions/sqlBinding"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs index 8f11ca05..44f92347 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs @@ -10,6 +10,7 @@ using Microsoft.SqlTools.Hosting; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Admin; using Microsoft.SqlTools.ServiceLayer.Agent; +using Microsoft.SqlTools.ServiceLayer.AzureFunctions; using Microsoft.SqlTools.ServiceLayer.Cms; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.DacFx; @@ -133,6 +134,9 @@ namespace Microsoft.SqlTools.ServiceLayer SchemaCompare.SchemaCompareService.Instance.InitializeService(serviceHost); serviceProvider.RegisterSingleService(SchemaCompareService.Instance); + AzureFunctions.AzureFunctionsService.Instance.InitializeService(serviceHost); + serviceProvider.RegisterSingleService(AzureFunctionsService.Instance); + ServerConfigService.Instance.InitializeService(serviceHost); serviceProvider.RegisterSingleService(ServerConfigService.Instance); diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index 4b7523d8..49808a5f 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -3278,6 +3278,16 @@ namespace Microsoft.SqlTools.ServiceLayer return Keys.GetString(Keys.SqlAssessmentUnsuppoertedEdition, editionCode); } + public static string CouldntFindAzureFunction(string functionName, string fileName) + { + return Keys.GetString(Keys.CouldntFindAzureFunction, functionName, fileName); + } + + public static string MoreThanOneAzureFunctionWithName(string functionName, string fileName) + { + return Keys.GetString(Keys.MoreThanOneAzureFunctionWithName, functionName, fileName); + } + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Keys { @@ -4555,6 +4565,12 @@ namespace Microsoft.SqlTools.ServiceLayer public const string SqlAssessmentUnsuppoertedEdition = "SqlAssessmentUnsuppoertedEdition"; + public const string CouldntFindAzureFunction = "CouldntFindAzureFunction"; + + + public const string MoreThanOneAzureFunctionWithName = "MoreThanOneAzureFunctionWithName"; + + private Keys() { } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx index 3380c3b7..549a0bb9 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx @@ -1854,4 +1854,14 @@ . Parameters: 0 - editionCode (int) + + Couldn't find Azure function with FunctionName {0} in {1} + . + Parameters: 0 - functionName (string), 1 - fileName (string) + + + More than one Azure function found with the FunctionName {0} in {1} + . + Parameters: 0 - functionName (string), 1 - fileName (string) + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index 91dbb106..41728c6f 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -848,4 +848,9 @@ SchemaCompareSessionNotFound = Could not find the schema compare session to canc SqlAssessmentGenerateScriptTaskName = Generate SQL Assessment script SqlAssessmentQueryInvalidOwnerUri = Not connected to a server SqlAssessmentConnectingError = Cannot connect to the server -SqlAssessmentUnsuppoertedEdition(int editionCode) = Unsupported engine edition {0} \ No newline at end of file +SqlAssessmentUnsuppoertedEdition(int editionCode) = Unsupported engine edition {0} + +############################################################################ +# Azure Functions +CouldntFindAzureFunction(string functionName, string fileName) = Couldn't find Azure function with FunctionName '{0}' in {1} +MoreThanOneAzureFunctionWithName(string functionName, string fileName) = More than one Azure function found with the FunctionName '{0}' in {1} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf index 357afdc9..8cf74345 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf @@ -2161,6 +2161,16 @@ No External Streaming Job creation TSQL found (EXEC sp_create_streaming_job statement). + + Couldn't find Azure function with FunctionName {0} in {1} + Couldn't find Azure function with FunctionName {0} in {1} + + + + More than one Azure function found with the FunctionName {0} in {1} + More than one Azure function found with the FunctionName {0} in {1} + + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj index f905ac94..cc652e76 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj +++ b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj @@ -21,6 +21,9 @@ + + + diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsInputBinding.ts b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsInputBinding.ts new file mode 100644 index 00000000..4cc775b1 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsInputBinding.ts @@ -0,0 +1,59 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Company.Namespace.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; + +namespace Company.Namespace +{ + public class ArtistsApi + { + private ILogger _logger; + + /// Initializes a new instance of ArtistsApi. + /// Class logger. + /// is null. + public ArtistsApi(ILogger logger) + { + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _logger = logger; + } + + /// Returns a list of artists. + /// Raw HTTP Request. + /// The cancellation token provided on Function shutdown. + [FunctionName("GetArtists_get")] + public IActionResult GetArtists([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "artists")] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable result) + { + _logger.LogInformation("HTTP trigger function processed a request."); + // TODO: Handle Documented Responses. + // Spec Defines: HTTP 200 + // Spec Defines: HTTP 400 + throw new NotImplementedException(); + } + + /// Lets a user post a new artist. + /// The Artist to use. + /// Raw HTTP Request. + /// The cancellation token provided on Function shutdown. + /// is null. + [FunctionName("NewArtist_post")] + public IActionResult NewArtist([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "artists")] Artist body, HttpRequest req) + { + _logger.LogInformation("HTTP trigger function processed a request."); + // TODO: Handle Documented Responses. + // Spec Defines: HTTP 200 + // Spec Defines: HTTP 400 + throw new NotImplementedException(); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsMultipleSameFunction.ts b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsMultipleSameFunction.ts new file mode 100644 index 00000000..571a7938 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsMultipleSameFunction.ts @@ -0,0 +1,59 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Company.Namespace.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; + +namespace Company.Namespace +{ + public class ArtistsApi + { + private ILogger _logger; + + /// Initializes a new instance of ArtistsApi. + /// Class logger. + /// is null. + public ArtistsApi(ILogger logger) + { + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _logger = logger; + } + + /// Returns a list of artists. + /// Raw HTTP Request. + /// The cancellation token provided on Function shutdown. + [FunctionName("GetArtists_get")] + public IActionResult GetArtists([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "artists")] HttpRequest req) + { + _logger.LogInformation("HTTP trigger function processed a request."); + // TODO: Handle Documented Responses. + // Spec Defines: HTTP 200 + // Spec Defines: HTTP 400 + throw new NotImplementedException(); + } + + /// Lets a user post a new artist. + /// The Artist to use. + /// Raw HTTP Request. + /// The cancellation token provided on Function shutdown. + /// is null. + [FunctionName("GetArtists_get")] + public IActionResult NewArtist([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "artists")] Artist body, HttpRequest req) + { + _logger.LogInformation("HTTP trigger function processed a request."); + // TODO: Handle Documented Responses. + // Spec Defines: HTTP 200 + // Spec Defines: HTTP 400 + throw new NotImplementedException(); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsNoBindings.ts b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsNoBindings.ts new file mode 100644 index 00000000..3bdb537a --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsNoBindings.ts @@ -0,0 +1,59 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Company.Namespace.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; + +namespace Company.Namespace +{ + public class ArtistsApi + { + private ILogger _logger; + + /// Initializes a new instance of ArtistsApi. + /// Class logger. + /// is null. + public ArtistsApi(ILogger logger) + { + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _logger = logger; + } + + /// Returns a list of artists. + /// Raw HTTP Request. + /// The cancellation token provided on Function shutdown. + [FunctionName("GetArtists_get")] + public IActionResult GetArtists([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "artists")] HttpRequest req) + { + _logger.LogInformation("HTTP trigger function processed a request."); + // TODO: Handle Documented Responses. + // Spec Defines: HTTP 200 + // Spec Defines: HTTP 400 + throw new NotImplementedException(); + } + + /// Lets a user post a new artist. + /// The Artist to use. + /// Raw HTTP Request. + /// The cancellation token provided on Function shutdown. + /// is null. + [FunctionName("NewArtist_post")] + public IActionResult NewArtist([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "artists")] Artist body, HttpRequest req) + { + _logger.LogInformation("HTTP trigger function processed a request."); + // TODO: Handle Documented Responses. + // Spec Defines: HTTP 200 + // Spec Defines: HTTP 400 + throw new NotImplementedException(); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsOutputBinding.ts b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsOutputBinding.ts new file mode 100644 index 00000000..7d74a4a3 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsOutputBinding.ts @@ -0,0 +1,59 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Company.Namespace.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; + +namespace Company.Namespace +{ + public class ArtistsApi + { + private ILogger _logger; + + /// Initializes a new instance of ArtistsApi. + /// Class logger. + /// is null. + public ArtistsApi(ILogger logger) + { + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _logger = logger; + } + + /// Returns a list of artists. + /// Raw HTTP Request. + /// The cancellation token provided on Function shutdown. + [FunctionName("GetArtists_get")] + public IActionResult GetArtists([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "artists")] HttpRequest req) + { + _logger.LogInformation("HTTP trigger function processed a request."); + // TODO: Handle Documented Responses. + // Spec Defines: HTTP 200 + // Spec Defines: HTTP 400 + throw new NotImplementedException(); + } + + /// Lets a user post a new artist. + /// The Artist to use. + /// Raw HTTP Request. + /// The cancellation token provided on Function shutdown. + /// is null. + [FunctionName("NewArtist_post")] + public IActionResult NewArtist([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "artists")] Artist body, HttpRequest req, [Sql("[dbo].[table1]", ConnectionStringSetting = "SqlConnectionString")] out Object output) + { + _logger.LogInformation("HTTP trigger function processed a request."); + // TODO: Handle Documented Responses. + // Spec Defines: HTTP 200 + // Spec Defines: HTTP 400 + throw new NotImplementedException(); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionsServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionsServiceTests.cs new file mode 100644 index 00000000..aaf86c1d --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionsServiceTests.cs @@ -0,0 +1,135 @@ +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.AzureFunctions; +using Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Moq; +using NUnit.Framework; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.AzureFunctions +{ + class AzureFunctionsServiceTests + { + private string testAzureFunctionsFolder = Path.Combine("..", "..", "..", "AzureFunctions", "AzureFunctionTestFiles"); + + /// + /// Verify input binding gets added + /// + [Test] + public void AddSqlInputBinding() + { + // copy the original file because the input binding will be inserted into the file + string originalFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsNoBindings.ts"); + string testFile = Path.Join(Path.GetTempPath(), string.Format("InsertSqlInputBinding-{0}.ts", DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss"))); + File.Copy(originalFile, testFile, true); + + AddSqlBindingParams parameters = new AddSqlBindingParams + { + bindingType = BindingType.input, + filePath = testFile, + functionName = "GetArtists_get", + objectName = "[dbo].[table1]", + connectionStringSetting = "SqlConnectionString" + }; + + AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters); + ResultStatus result = operation.AddBinding(); + + Assert.True(result.Success); + Assert.IsNull(result.ErrorMessage); + + string expectedFileText = File.ReadAllText(Path.Join(testAzureFunctionsFolder, "AzureFunctionsInputBinding.ts")); + string actualFileText = File.ReadAllText(testFile); + Assert.AreEqual(expectedFileText, actualFileText); + } + + /// + /// Verify output binding gets added + /// + [Test] + public void AddSqlOutputBinding() + { + // copy the original file because the output binding will be inserted into the file + string originalFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsNoBindings.ts"); + string testFile = Path.Join(Path.GetTempPath(), string.Format("InsertSqlOutputBinding-{0}.ts", DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss"))); + File.Copy(originalFile, testFile, true); + + AddSqlBindingParams parameters = new AddSqlBindingParams + { + bindingType = BindingType.output, + filePath = testFile, + functionName = "NewArtist_post", + objectName = "[dbo].[table1]", + connectionStringSetting = "SqlConnectionString" + }; + + AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters); + ResultStatus result = operation.AddBinding(); + + Assert.True(result.Success); + Assert.IsNull(result.ErrorMessage); + + string expectedFileText = File.ReadAllText(Path.Join(testAzureFunctionsFolder, "AzureFunctionsOutputBinding.ts")); + string actualFileText = File.ReadAllText(testFile); + Assert.AreEqual(expectedFileText, actualFileText); + } + + /// + /// Verify what happens when specified azure function isn't found + /// + [Test] + public void NoAzureFunctionForSqlBinding() + { + // copy the original file because the input binding will be inserted into the file + string originalFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsNoBindings.ts"); + string testFile = Path.Join(Path.GetTempPath(), string.Format("NoAzureFunctionForSqlBinding-{0}.ts", DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss"))); + File.Copy(originalFile, testFile, true); + + AddSqlBindingParams parameters = new AddSqlBindingParams + { + bindingType = BindingType.input, + filePath = testFile, + functionName = "noExistingFunction", + objectName = "[dbo].[table1]", + connectionStringSetting = "SqlConnectionString" + }; + + AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters); + ResultStatus result = operation.AddBinding(); + + Assert.False(result.Success); + Assert.NotNull(result.ErrorMessage); + Assert.True(result.ErrorMessage.Equals(SR.CouldntFindAzureFunction("noExistingFunction", testFile))); + } + + /// + /// Verify what happens when there's more than one Azure function with the specified name in the file + /// + [Test] + public void MoreThanOneAzureFunctionWithSpecifiedName() + { + // copy the original file because the input binding will be inserted into the file + string originalFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsMultipleSameFunction.ts"); + string testFile = Path.Join(Path.GetTempPath(), string.Format("MoreThanOneAzureFunctionWithSpecifiedName-{0}.ts", DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss"))); + File.Copy(originalFile, testFile, true); + + AddSqlBindingParams parameters = new AddSqlBindingParams + { + bindingType = BindingType.input, + filePath = testFile, + functionName = "GetArtists_get", + objectName = "[dbo].[table1]", + connectionStringSetting = "SqlConnectionString" + }; + + AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters); + ResultStatus result = operation.AddBinding(); + + Assert.False(result.Success); + Assert.NotNull(result.ErrorMessage); + Assert.True(result.ErrorMessage.Equals(SR.MoreThanOneAzureFunctionWithName("GetArtists_get", testFile))); + } + } +}