diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b512a8d9e8..8d8ef31ec8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -416,6 +416,24 @@ jobs: inputs: command: ci workingDir: sample/CustomHandlerRetry + - task: PowerShell@2 + # Temporary script to install required nuget packages for the dotnet isolated sample project + # that is being used for E2E tests. This script should be deleted and removed from the AzDO + # pipeline when these packages are published to nuget. + # Issue tracking removal of this task: https://github.com/Azure/azure-functions-host/issues/8910 + displayName: 'Install MyGet nuget packages' + inputs: + targetType: 'inline' + script: | + dotnet add package Microsoft.Azure.WebJobs --version 3.0.36 --source https://www.myget.org/F/azure-appservice-staging/api/v3/index.json + + dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Storage --version 5.0.2-preview1-20221104.9 --source https://www.myget.org/F/azure-appservice/api/v3/index.json + - task: DotNetCoreCLI@2 + displayName: 'Build dotnet isolated sample app' + inputs: + command: 'build' + projects: | + **\DotNetIsolated.csproj - task: AzureKeyVault@1 inputs: # Note: This is actually a Service Connection in DevOps, not an Azure subscription name @@ -526,6 +544,15 @@ jobs: arguments: '--filter "Group=SamplesEndToEndTests" --no-build' projects: | **\WebJobs.Script.Tests.Integration.csproj + - task: DotNetCoreCLI@2 + displayName: "SDK-type binding end to end tests" + condition: succeededOrFailed() + inputs: + command: 'test' + testRunTitle: "SDK-type binding end to end tests" + arguments: '--filter "Group=SdkTypeBindingEndToEnd" --no-build' + projects: | + **\WebJobs.Script.Tests.Integration.csproj - task: DotNetCoreCLI@2 displayName: "Drain mode end to end tests" condition: succeededOrFailed() diff --git a/sample/DotNetIsolated/Blob/BlobInputBindingSamples.cs b/sample/DotNetIsolated/Blob/BlobInputBindingSamples.cs new file mode 100644 index 0000000000..08187dfbcc --- /dev/null +++ b/sample/DotNetIsolated/Blob/BlobInputBindingSamples.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Azure.Storage.Blobs; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; + +namespace SampleApp +{ + public class BlobInputBindingSamples + { + private readonly ILogger _logger; + + public BlobInputBindingSamples(ILogger logger) + { + _logger = logger; + } + + [Function(nameof(BlobInputClientFunction))] + public async Task BlobInputClientFunction( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + [BlobInput("test-input-sample/sample1.txt", Connection = "AzureWebJobsStorage")] BlobClient client) + { + var response = req.CreateResponse(HttpStatusCode.OK); + var downloadResult = await client.DownloadContentAsync(); + await response.Body.WriteAsync(downloadResult.Value.Content); + return response; + } + + [Function(nameof(BlobInputStreamFunction))] + public async Task BlobInputStreamFunction( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + [BlobInput("test-input-sample/sample1.txt", Connection = "AzureWebJobsStorage")] Stream stream) + { + var response = req.CreateResponse(HttpStatusCode.OK); + using var blobStreamReader = new StreamReader(stream); + await response.WriteStringAsync(blobStreamReader.ReadToEnd()); + return response; + } + + [Function(nameof(BlobInputByteArrayFunction))] + public async Task BlobInputByteArrayFunction( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + [BlobInput("test-input-sample/sample1.txt", Connection = "AzureWebJobsStorage")] Byte[] data) + { + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteStringAsync(Encoding.Default.GetString(data)); + return response; + } + + [Function(nameof(BlobInputStringFunction))] + public async Task BlobInputStringFunction( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + [BlobInput("test-input-sample/sample1.txt", Connection = "AzureWebJobsStorage")] string data) + { + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteStringAsync(data); + return response; + } + + [Function(nameof(BlobInputBookFunction))] + public async Task BlobInputBookFunction( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + [BlobInput("test-input-sample/book.json", Connection = "AzureWebJobsStorage")] Book data) + { + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteStringAsync(data.Name); + return response; + } + } +} diff --git a/sample/DotNetIsolated/Blob/BlobTriggerBindingSamples.cs b/sample/DotNetIsolated/Blob/BlobTriggerBindingSamples.cs new file mode 100644 index 0000000000..caff1ad7c5 --- /dev/null +++ b/sample/DotNetIsolated/Blob/BlobTriggerBindingSamples.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Azure.Storage.Blobs; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; + +namespace SampleApp +{ + public static class BlobTriggerBindingSamples + { + [Function(nameof(BlobClientFunction))] + public static async Task BlobClientFunction( + [BlobTrigger("test-input-client/{name}", Connection = "AzureWebJobsStorage")] BlobClient client, + FunctionContext context) + { + var logger = context.GetLogger(nameof(BlobClientFunction)); + var downloadResult = await client.DownloadContentAsync(); + var content = downloadResult.Value.Content.ToString(); + logger.LogInformation(content); + } + + [Function(nameof(BlobStreamFunction))] + public static void BlobStreamFunction( + [BlobTrigger("test-input-stream/{name}", Connection = "AzureWebJobsStorage")] Stream stream, + FunctionContext context) + { + var logger = context.GetLogger(nameof(BlobStreamFunction)); + using var blobStreamReader = new StreamReader(stream); + logger.LogInformation(blobStreamReader.ReadToEnd()); + } + + [Function(nameof(BlobByteArrayFunction))] + public static void BlobByteArrayFunction( + [BlobTrigger("test-input-byte/{name}", Connection = "AzureWebJobsStorage")] Byte[] data, + FunctionContext context) + { + var logger = context.GetLogger(nameof(BlobByteArrayFunction)); + logger.LogInformation(Encoding.Default.GetString(data)); + } + + [Function(nameof(BlobStringFunction))] + public static void BlobStringFunction( + [BlobTrigger("test-input-string/{name}", Connection = "AzureWebJobsStorage")] string data, + FunctionContext context) + { + var logger = context.GetLogger(nameof(BlobStringFunction)); + logger.LogInformation(data); + } + + [Function(nameof(BlobBookFunction))] + public static void BlobBookFunction( + [BlobTrigger("test-input-book/{name}", Connection = "AzureWebJobsStorage")] Book data, + FunctionContext context) + { + var logger = context.GetLogger(nameof(BlobBookFunction)); + logger.LogInformation($"{data.Id} - {data.Name}"); + } + } +} diff --git a/sample/DotNetIsolated/Blob/ExpressionFunction.cs b/sample/DotNetIsolated/Blob/ExpressionFunction.cs new file mode 100644 index 0000000000..8103a5c558 --- /dev/null +++ b/sample/DotNetIsolated/Blob/ExpressionFunction.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; + +namespace SampleApp +{ + public static class ExpressionFunction + { + [Function(nameof(ExpressionFunction))] + public static void Run( + [QueueTrigger("test-input-sample", Connection = "AzureWebJobsStorage")] Book book, + [BlobInput("test-input-sample/{id}.txt", Connection = "AzureWebJobsStorage")] string myBlob, + FunctionContext context) + { + var logger = context.GetLogger(nameof(ExpressionFunction)); + logger.LogInformation(myBlob); + } + } +} diff --git a/sample/DotNetIsolated/Book.cs b/sample/DotNetIsolated/Book.cs new file mode 100644 index 0000000000..824e883364 --- /dev/null +++ b/sample/DotNetIsolated/Book.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace SampleApp +{ + public class Book + { + public string Id { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/sample/DotNetIsolated/DotNetIsolated.csproj b/sample/DotNetIsolated/DotNetIsolated.csproj new file mode 100644 index 0000000000..9a52f28dbe --- /dev/null +++ b/sample/DotNetIsolated/DotNetIsolated.csproj @@ -0,0 +1,29 @@ + + + net6.0 + preview + v4 + Exe + false + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + \ No newline at end of file diff --git a/sample/DotNetIsolated/Program.cs b/sample/DotNetIsolated/Program.cs new file mode 100644 index 0000000000..f9c235fa77 --- /dev/null +++ b/sample/DotNetIsolated/Program.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Extensions.Hosting; + +namespace SampleApp +{ + public class Program + { + public static void Main() + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .Build(); + + host.Run(); + } + } +} diff --git a/sample/DotNetIsolated/host.json b/sample/DotNetIsolated/host.json new file mode 100644 index 0000000000..beb2e4020b --- /dev/null +++ b/sample/DotNetIsolated/host.json @@ -0,0 +1,11 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + } +} \ No newline at end of file diff --git a/test/WebJobs.Script.Tests.Analyzers/WebJobs.Script.Tests.Analyzers.csproj b/test/WebJobs.Script.Tests.Analyzers/WebJobs.Script.Tests.Analyzers.csproj index 6848ee3b90..9b1935b94d 100644 --- a/test/WebJobs.Script.Tests.Analyzers/WebJobs.Script.Tests.Analyzers.csproj +++ b/test/WebJobs.Script.Tests.Analyzers/WebJobs.Script.Tests.Analyzers.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs b/test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs index 23ebb11ded..af74c2c2c8 100644 --- a/test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs +++ b/test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs @@ -28,6 +28,7 @@ using Microsoft.Azure.WebJobs.Script.Workers.Rpc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.EnvironmentVariables; +using Microsoft.Extensions.Configuration.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; @@ -76,7 +77,8 @@ public TestFunctionHost(string scriptPath, string logPath, Action configureScriptHostLogging = null, Action configureScriptHostServices = null, Action configureWebHostAppConfiguration = null, - bool addTestSettings = true) + bool addTestSettings = true, + bool setStorageEnvironmentVariable = false) { _appRoot = scriptPath; @@ -134,7 +136,7 @@ public TestFunctionHost(string scriptPath, string logPath, { if (addTestSettings) { - scriptHostConfigurationBuilder.AddTestSettings(); + scriptHostConfigurationBuilder.AddTestSettings(setStorageEnvironmentVariable); } configureScriptHostAppConfiguration?.Invoke(scriptHostConfigurationBuilder); }) diff --git a/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/EndToEndTestFixture.cs b/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/EndToEndTestFixture.cs index f31764b81c..b442ed7745 100644 --- a/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/EndToEndTestFixture.cs +++ b/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/EndToEndTestFixture.cs @@ -37,12 +37,14 @@ public abstract class EndToEndTestFixture : IAsyncLifetime private int _workerProcessCount; private string _functionsWorkerRuntimeVersion; private bool _addTestSettings; + private bool _setStorageEnvironmentVariable; - protected EndToEndTestFixture(string rootPath, string testId, - string functionsWorkerRuntime, - int workerProcessesCount = 1, + protected EndToEndTestFixture(string rootPath, string testId, + string functionsWorkerRuntime, + int workerProcessesCount = 1, string functionsWorkerRuntimeVersion = null, - bool addTestSettings = true) + bool addTestSettings = true, + bool setStorageEnvironmentVariable = false) { FixtureId = testId; @@ -51,6 +53,7 @@ protected EndToEndTestFixture(string rootPath, string testId, _workerProcessCount = workerProcessesCount; _functionsWorkerRuntimeVersion = functionsWorkerRuntimeVersion; _addTestSettings = addTestSettings; + _setStorageEnvironmentVariable = setStorageEnvironmentVariable; } public CloudBlobContainer TestInputContainer { get; private set; } @@ -133,7 +136,7 @@ string GetDestPath(int counter) FunctionsSyncManagerMock = new Mock(MockBehavior.Strict); FunctionsSyncManagerMock.Setup(p => p.TrySyncTriggersAsync(It.IsAny())).ReturnsAsync(new SyncTriggersResult { Success = true }); - Host = new TestFunctionHost(_copiedRootPath, logPath, addTestSettings: _addTestSettings, + Host = new TestFunctionHost(_copiedRootPath, logPath, addTestSettings: _addTestSettings, setStorageEnvironmentVariable: _setStorageEnvironmentVariable, configureScriptHostWebJobsBuilder: webJobsBuilder => { ConfigureScriptHost(webJobsBuilder); diff --git a/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/EndToEndTestsBase.cs b/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/EndToEndTestsBase.cs index bcfa945257..31aa9cf9f5 100644 --- a/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/EndToEndTestsBase.cs +++ b/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/EndToEndTestsBase.cs @@ -405,15 +405,15 @@ protected async Task GetFunctionTestResult(string functionName) string logEntry = null; await TestHelpers.Await(() => - { - // search the logs for token "TestResult:" and parse the following JSON - var logs = Fixture.Host.GetScriptHostLogMessages(LogCategories.CreateFunctionUserCategory(functionName)); - if (logs != null) - { - logEntry = logs.Select(p => p.FormattedMessage).SingleOrDefault(p => p != null && p.Contains("TestResult:")); - } - return logEntry != null; - }); + { + // search the logs for token "TestResult:" and parse the following JSON + var logs = Fixture.Host.GetScriptHostLogMessages(LogCategories.CreateFunctionUserCategory(functionName)); + if (logs != null) + { + logEntry = logs.Select(p => p.FormattedMessage).SingleOrDefault(p => p != null && p.Contains("TestResult:")); + } + return logEntry != null; + }); int idx = logEntry.IndexOf("{"); logEntry = logEntry.Substring(idx); @@ -421,6 +421,19 @@ await TestHelpers.Await(() => return JObject.Parse(logEntry); } + protected async Task> GetFunctionLogs(string functionName) + { + IEnumerable logs = null; + + await TestHelpers.Await(() => + { + logs = Fixture.Host.GetScriptHostLogMessages(LogCategories.CreateFunctionUserCategory(functionName)); + return logs != null; + }, timeout: 30000, pollingInterval: 500); + + return logs; + } + public class ScenarioInput { [JsonProperty("scenario")] diff --git a/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SdkTypeBindings/DotNetIsolated_BlobStorage.cs b/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SdkTypeBindings/DotNetIsolated_BlobStorage.cs new file mode 100644 index 0000000000..9911a0d363 --- /dev/null +++ b/test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SdkTypeBindings/DotNetIsolated_BlobStorage.cs @@ -0,0 +1,243 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Azure.Storage.Blob; +using Microsoft.Azure.Storage.Queue; +using Microsoft.Azure.WebJobs.Script.Workers.Rpc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.WebJobs.Script.Tests; +using Xunit; + +namespace Microsoft.Azure.WebJobs.Script.Tests.EndToEnd +{ + [Trait(TestTraits.Category, TestTraits.EndToEnd)] + [Trait(TestTraits.Group, TestTraits.SdkTypeBindingEndToEnd)] + [Collection("Sequential")] + public class DotNetIsolated_BlobStorage : EndToEndTestsBase, IAsyncDisposable + { + public DotNetIsolated_BlobStorage(TestFixture fixture) : base(fixture) + { + } + + [Fact] + public async Task QueueTrigger_BlobInput_ExpressionFunction_Invoke_Succeeds() + { + var inputText = "This book is titled 'Golden Compass'"; + var inputBook = @"{ ""id"": ""1"", ""name"": ""Golden Compass"" }"; + + CloudBlockBlob testBlob = Fixture.TestInputContainer.GetBlockBlobReference("1.txt"); + await testBlob.UploadTextAsync(inputText); + + var message = new CloudQueueMessage(inputBook); + await Fixture.TestQueue.AddMessageAsync(message); + + var logs = await GetFunctionLogs("ExpressionFunction"); + + Assert.True(logs.Any(p => p.FormattedMessage.Contains(inputText))); + } + + [Fact] + public async Task HttpTrigger_BlobInputClientFunction_Invoke_Succeeds() + { + var inputText = "BlobInputClientFunction - hello world"; + + CloudBlockBlob testBlob = Fixture.TestInputContainer.GetBlockBlobReference("sample1.txt"); + await testBlob.UploadTextAsync(inputText); + + var response = await SamplesTestHelpers.InvokeHttpTrigger(Fixture, "BlobInputClientFunction"); + var responseContent = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(inputText, responseContent); + } + + [Fact] + public async Task HttpTrigger_BlobInputStreamFunction_Invoke_Succeeds() + { + var inputText = "BlobInputStreamFunction - hello world"; + + CloudBlockBlob testBlob = Fixture.TestInputContainer.GetBlockBlobReference("sample1.txt"); + await testBlob.UploadTextAsync(inputText); + + var response = await SamplesTestHelpers.InvokeHttpTrigger(Fixture, "BlobInputStreamFunction"); + var responseContent = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(inputText, responseContent); + } + + [Fact] + public async Task HttpTrigger_BlobInputByteArrayFunction_Invoke_Succeeds() + { + var inputText = "BlobInputByteArrayFunction - hello world"; + + CloudBlockBlob testBlob = Fixture.TestInputContainer.GetBlockBlobReference("sample1.txt"); + await testBlob.UploadTextAsync(inputText); + + var response = await SamplesTestHelpers.InvokeHttpTrigger(Fixture, "BlobInputByteArrayFunction"); + var responseContent = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(inputText, responseContent); + } + + [Fact] + public async Task HttpTrigger_BlobInputStringFunction_Invoke_Succeeds() + { + var inputText = "BlobInputStringFunction - hello world"; + + CloudBlockBlob testBlob = Fixture.TestInputContainer.GetBlockBlobReference("sample1.txt"); + await testBlob.UploadTextAsync(inputText); + + var response = await SamplesTestHelpers.InvokeHttpTrigger(Fixture, "BlobInputStringFunction"); + var responseContent = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(inputText, responseContent); + } + + [Fact] + public async Task HttpTrigger_BlobInputBookFunction_Invoke_Succeeds() + { + var inputBook = @"{ ""id"": ""1"", ""name"": ""Golden Compass"" }"; + + CloudBlockBlob testBlob = Fixture.TestInputContainer.GetBlockBlobReference("book.json"); + await testBlob.UploadTextAsync(inputBook); + + var response = await SamplesTestHelpers.InvokeHttpTrigger(Fixture, "BlobInputBookFunction"); + var responseContent = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Golden Compass", responseContent); + } + + [Fact] + public async Task BlobTrigger_BlobClientFunction_Invoke_Succeeds() + { + var inputText = "BlobClientFunction - hello world"; + + var inputContainer = Fixture.BlobClient.GetContainerReference("test-input-client"); + await inputContainer.CreateIfNotExistsAsync(); + + CloudBlockBlob testBlob = inputContainer.GetBlockBlobReference("sample1.txt"); + await testBlob.UploadTextAsync(inputText); + + var logs = await GetFunctionLogs("BlobClientFunction"); + Assert.True(logs.Any(p => p.FormattedMessage.Contains(inputText))); + + await TestHelpers.ClearContainerAsync(inputContainer); + } + + [Fact] + public async Task BlobTrigger_BlobStreamFunction_Invoke_Succeeds() + { + var inputText = "BlobStreamFunction - hello world"; + + var inputContainer = Fixture.BlobClient.GetContainerReference("test-input-stream"); + await inputContainer.CreateIfNotExistsAsync(); + + CloudBlockBlob testBlob = inputContainer.GetBlockBlobReference("sample1.txt"); + await testBlob.UploadTextAsync(inputText); + + var logs = await GetFunctionLogs("BlobStreamFunction"); + Assert.True(logs.Any(p => p.FormattedMessage.Contains(inputText))); + + await TestHelpers.ClearContainerAsync(inputContainer); + } + + [Fact] + public async Task BlobTrigger_BlobByteArrayFunction_Invoke_Succeeds() + { + var inputText = "BlobByteArrayFunction - hello world"; + + var inputContainer = Fixture.BlobClient.GetContainerReference("test-input-byte"); + await inputContainer.CreateIfNotExistsAsync(); + + CloudBlockBlob testBlob = inputContainer.GetBlockBlobReference("sample1.txt"); + await testBlob.UploadTextAsync(inputText); + + var logs = await GetFunctionLogs("BlobByteArrayFunction"); + Assert.True(logs.Any(p => p.FormattedMessage.Contains(inputText))); + + await TestHelpers.ClearContainerAsync(inputContainer); + } + + [Fact] + public async Task BlobTrigger_BlobStringFunction_Invoke_Succeeds() + { + var inputText = "BlobStringFunction - hello world"; + + var inputContainer = Fixture.BlobClient.GetContainerReference("test-input-string"); + await inputContainer.CreateIfNotExistsAsync(); + + CloudBlockBlob testBlob = inputContainer.GetBlockBlobReference("sample1.txt"); + await testBlob.UploadTextAsync(inputText); + + var logs = await GetFunctionLogs("BlobStringFunction"); + Assert.True(logs.Any(p => p.FormattedMessage.Contains(inputText))); + + await TestHelpers.ClearContainerAsync(inputContainer); + } + + [Fact] + public async Task BlobTrigger_BlobBookFunction_Invoke_Succeeds() + { + var inputBook = @"{ ""id"": ""1"", ""name"": ""Golden Compass"" }"; + + var inputContainer = Fixture.BlobClient.GetContainerReference("test-input-book"); + await inputContainer.CreateIfNotExistsAsync(); + + CloudBlockBlob testBlob = inputContainer.GetBlockBlobReference("book.json"); + await testBlob.UploadTextAsync(inputBook); + + var logs = await GetFunctionLogs("BlobBookFunction"); + Assert.True(logs.Any(p => p.FormattedMessage.Contains("1 - Golden Compass"))); + + await TestHelpers.ClearContainerAsync(inputContainer); + } + + public async ValueTask DisposeAsync() + { + await TestHelpers.ClearContainerAsync(Fixture.TestInputContainer); + } + + public class TestFixture : EndToEndTestFixture + { + private static string rootPath = Path.Combine(Environment.CurrentDirectory, "..", "..", "..", "..", "..", "sample", "DotNetIsolated", "bin", "Debug", "net6.0"); + + public TestFixture() : base(rootPath, "sample", RpcWorkerConstants.DotNetIsolatedLanguageWorkerName, setStorageEnvironmentVariable: true) + { + } + + public override void ConfigureScriptHost(IWebJobsBuilder webJobsBuilder) + { + base.ConfigureScriptHost(webJobsBuilder); + + webJobsBuilder.AddAzureStorage() + .Services.Configure(o => + { + o.Functions = new[] + { + "BlobInputClientFunction", + "BlobInputStreamFunction", + "BlobInputByteArrayFunction", + "BlobInputStringFunction", + "BlobInputBookFunction", + "BlobClientFunction", + "BlobStreamFunction", + "BlobByteArrayFunction", + "BlobStringFunction", + "BlobBookFunction", + "ExpressionFunction" + }; + }); + } + } + } +} \ No newline at end of file diff --git a/test/WebJobs.Script.Tests.Shared/TestConfigurationBuilderExtensions.cs b/test/WebJobs.Script.Tests.Shared/TestConfigurationBuilderExtensions.cs index 5d669ad893..5e4f1986d2 100644 --- a/test/WebJobs.Script.Tests.Shared/TestConfigurationBuilderExtensions.cs +++ b/test/WebJobs.Script.Tests.Shared/TestConfigurationBuilderExtensions.cs @@ -3,8 +3,8 @@ using System; using System.IO; -using System.Linq; using Microsoft.Extensions.Configuration; +using Newtonsoft.Json.Linq; namespace Microsoft.Azure.WebJobs.Script.Tests { @@ -14,5 +14,17 @@ public static class TestConfigurationBuilderExtensions private static string configPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".azurefunctions", ConfigFile); public static IConfigurationBuilder AddTestSettings(this IConfigurationBuilder builder) => builder.AddJsonFile(configPath, true); + + public static IConfigurationBuilder AddTestSettings(this IConfigurationBuilder builder, bool setStorageEnvironmentVariable) + { + if (setStorageEnvironmentVariable) + { + JObject config = JObject.Parse(File.ReadAllText(configPath)); + var storageConnection = config["AzureWebJobsStorage"].ToString(); + Environment.SetEnvironmentVariable("AzureWebJobsStorage", storageConnection); + } + + return builder.AddTestSettings(); + } } } diff --git a/test/WebJobs.Script.Tests.Shared/TestTraits.cs b/test/WebJobs.Script.Tests.Shared/TestTraits.cs index 8a29f85c78..31c364956b 100644 --- a/test/WebJobs.Script.Tests.Shared/TestTraits.cs +++ b/test/WebJobs.Script.Tests.Shared/TestTraits.cs @@ -29,6 +29,11 @@ public static class TestTraits /// public const string DrainModeEndToEnd = "DrainModeEndToEndTests"; + /// + /// Tests for the worker-side SDK binding feature. + /// + public const string SdkTypeBindingEndToEnd = "SdkTypeBindingEndToEnd"; + /// /// Standby mode tests are special in that they set uni-directional /// static state, and benefit from test isolation. diff --git a/test/WebJobs.Script.Tests/Binding/FunctionBindingTests.cs b/test/WebJobs.Script.Tests/Binding/FunctionBindingTests.cs index 6b318c43a6..0ece04fd07 100644 --- a/test/WebJobs.Script.Tests/Binding/FunctionBindingTests.cs +++ b/test/WebJobs.Script.Tests/Binding/FunctionBindingTests.cs @@ -173,5 +173,26 @@ public void ReadAsCollection_StringArray_WithBOM() Assert.Equal("Value2", (string)collection[1]); Assert.Equal("Value3", (string)collection[2]); } + + [Fact] + public async Task BindParameterBindingDataAsync() + { + string contentString = "hello world"; + ParameterBindingData bindingData = new ("1.0.0", "AzureStorageBlob", BinaryData.FromString(contentString), "application/json"); + + var binderMock = new Mock(MockBehavior.Strict); + var attributes = new Attribute[] { new BlobAttribute("test") }; + binderMock.Setup(p => p.BindAsync(attributes, CancellationToken.None)).ReturnsAsync(bindingData); + + BindingContext bindingContext = new BindingContext + { + Attributes = attributes, + Binder = binderMock.Object + }; + + await FunctionBinding.BindParameterBindingDataAsync(bindingContext); + + Assert.Equal(bindingData, bindingContext.Value); + } } } diff --git a/test/WebJobs.Script.Tests/Binding/GeneralScriptBindingProviderTests.cs b/test/WebJobs.Script.Tests/Binding/GeneralScriptBindingProviderTests.cs index a73a7077fe..17b0c726c1 100644 --- a/test/WebJobs.Script.Tests/Binding/GeneralScriptBindingProviderTests.cs +++ b/test/WebJobs.Script.Tests/Binding/GeneralScriptBindingProviderTests.cs @@ -21,27 +21,33 @@ public class GeneralScriptBindingProviderTests [InlineData(null, null, typeof(object))] [InlineData(null, "many", typeof(object[]))] [InlineData("string", null, typeof(string))] - [InlineData("StRing", null, typeof(string))] // case insenstive - [InlineData("string", "mANy", typeof(string[]))] // case insensitve + [InlineData("StRing", null, typeof(string))] // case insensitive + [InlineData("string", "mANy", typeof(string[]))] // case insensitive [InlineData("binary", null, typeof(byte[]))] [InlineData("binary", "many", typeof(byte[][]))] [InlineData("stream", null, typeof(Stream))] [InlineData("stream", "many", typeof(Stream[]))] // nonsense? - public void Validate(string dataType, string cardinality, Type expectedType) + [InlineData("string", null, typeof(ParameterBindingData), true)] + [InlineData("string", "many", typeof(ParameterBindingData[]), true)] + [InlineData(null, null, typeof(ParameterBindingData), true)] + [InlineData(null, "many", typeof(ParameterBindingData[]), true)] + public void Validate(string dataType, string cardinality, Type expectedType, bool supportsDeferredBinding = false) { - var ctx = New(dataType, cardinality); + var ctx = New(dataType, cardinality, supportsDeferredBinding); var type = GeneralScriptBindingProvider.GetRequestedType(ctx); Assert.Equal(expectedType, type); } - private static ScriptBindingContext New(string dataType, string cardinality) + private static ScriptBindingContext New(string dataType, string cardinality, bool supportsDeferredBinding) { - var jobj = new JObject(); - jobj["type"] = "test"; - jobj["direction"] = "in"; - jobj["datatype"] = dataType; - jobj["cardinality"] = cardinality; - return new ScriptBindingContext(jobj); + var bindingMetadataJObject = new JObject(); + bindingMetadataJObject["type"] = "test"; + bindingMetadataJObject["direction"] = "in"; + bindingMetadataJObject["datatype"] = dataType; + bindingMetadataJObject["cardinality"] = cardinality; + bindingMetadataJObject["properties"] = new JObject { { "supportsDeferredBinding", supportsDeferredBinding } }; + + return new ScriptBindingContext(bindingMetadataJObject); } [Fact] diff --git a/test/WebJobs.Script.Tests/Description/Worker/WorkerFunctionDescriptorProviderTests.cs b/test/WebJobs.Script.Tests/Description/Worker/WorkerFunctionDescriptorProviderTests.cs index 094b827ba8..0dce7ca151 100644 --- a/test/WebJobs.Script.Tests/Description/Worker/WorkerFunctionDescriptorProviderTests.cs +++ b/test/WebJobs.Script.Tests/Description/Worker/WorkerFunctionDescriptorProviderTests.cs @@ -3,13 +3,17 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Script.Binding; using Microsoft.Azure.WebJobs.Script.Description; using Microsoft.Azure.WebJobs.Script.Extensibility; using Microsoft.Azure.WebJobs.Script.Workers; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Microsoft.WebJobs.Script.Tests; using Moq; using Newtonsoft.Json.Linq; @@ -17,26 +21,36 @@ namespace Microsoft.Azure.WebJobs.Script.Tests { - public class WorkerFunctionDescriptorProviderTests + public class WorkerFunctionDescriptorProviderTests : IDisposable { private IHost _host; private TestWorkerDescriptorProvider _provider; public WorkerFunctionDescriptorProviderTests() { - var scriptHostOptions = new ScriptJobHostOptions(); - var bindingProviders = new Mock>(); var mockApplicationLifetime = new Mock(); var mockFunctionInvocationDispatcher = new Mock(); + string rootPath = Path.Combine(Environment.CurrentDirectory, @"TestScripts\Node"); + _host = new HostBuilder().ConfigureDefaultTestWebScriptHost(b => { b.AddAzureStorage(); - }).Build(); + }, + o => + { + o.ScriptPath = rootPath; + o.LogPath = TestHelpers.GetHostLogFileDirectory().Parent.FullName; + }) + .Build(); var scriptHost = _host.GetScriptHost(); + scriptHost.InitializeAsync().GetAwaiter().GetResult(); + + var config = _host.Services.GetService>().Value; + var providers = _host.Services.GetService>(); - _provider = new TestWorkerDescriptorProvider(scriptHost, null, bindingProviders.Object, mockFunctionInvocationDispatcher.Object, + _provider = new TestWorkerDescriptorProvider(scriptHost, config, providers, mockFunctionInvocationDispatcher.Object, NullLoggerFactory.Instance, mockApplicationLifetime.Object, TimeSpan.FromSeconds(5)); } @@ -97,6 +111,46 @@ public void BindingAttributeContainsExpression_DoesNotFindRegexMatch_ReturnsFals Assert.False(result); } + [Theory] + [InlineData(true, true, typeof(byte[]))] + [InlineData(false, true, typeof(byte[]))] + // [InlineData(true, false, typeof(ParameterBindingData))] // This will work once the new Blob extension is released with ParameterBindingData support + [InlineData(false, false, typeof(byte[]))] + public async Task CreateTriggerParameter_DeferredBindingFlags_SetsTriggerType(bool supportsDeferredBinding, bool skipDeferredBinding, Type expectedType) + { + string bindingJson = $@"{{""name"":""book"",""direction"":""In"",""type"":""blobTrigger"",""blobPath"":""expression-trigger"",""connection"":""AzureWebJobsStorage"",""properties"":{{""SupportsDeferredBinding"":""{supportsDeferredBinding}""}}}}"; + + BindingMetadata metadata = BindingMetadata.Create(JObject.Parse(bindingJson)); + metadata.Properties.Add("SkipDeferredBinding", skipDeferredBinding); + + FunctionMetadata functionMetadata = new FunctionMetadata(); + functionMetadata.Bindings.Add(metadata); + + try + { + var (created, descriptor) = await _provider.TryCreate(functionMetadata); + Assert.Equal(expectedType, descriptor.TriggerParameter.Type); + } + catch (Exception ex) + { + Assert.True(false, "Exception not expected:" + ex.Message); + throw; + } + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _host?.Dispose(); + } + } + + public void Dispose() + { + Dispose(true); + } + private class TestWorkerDescriptorProvider : WorkerFunctionDescriptorProvider { public TestWorkerDescriptorProvider(ScriptHost host, ScriptJobHostOptions config, ICollection bindingProviders, diff --git a/tools/ExtensionsMetadataGenerator/test/ExtensionsMetadataGeneratorTests/ExtensionsMetadataGeneratorTests.csproj b/tools/ExtensionsMetadataGenerator/test/ExtensionsMetadataGeneratorTests/ExtensionsMetadataGeneratorTests.csproj index c9dcde4c60..68f803a83a 100644 --- a/tools/ExtensionsMetadataGenerator/test/ExtensionsMetadataGeneratorTests/ExtensionsMetadataGeneratorTests.csproj +++ b/tools/ExtensionsMetadataGenerator/test/ExtensionsMetadataGeneratorTests/ExtensionsMetadataGeneratorTests.csproj @@ -11,7 +11,7 @@ - +