diff --git a/.editorconfig b/.editorconfig index 1f2ad763f..8ee28425b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -296,11 +296,17 @@ dotnet_diagnostic.IDE1006.severity = warning # Predefined type alias should not # We allow usage of "var" inside tests as it reduces churn as we remove/rename types csharp_style_var_for_built_in_types = true:none csharp_style_var_elsewhere = true:none +dotnet_diagnostic.CA1861.severity = none +dotnet_diagnostic.CA2208.severity = none +dotnet_diagnostic.xUnit1000.severity = none [**/test/**/*.cs] # We allow usage of "var" inside tests as it reduces churn as we remove/rename types csharp_style_var_for_built_in_types = true:none csharp_style_var_elsewhere = true:none +dotnet_diagnostic.CA1861.severity = none +dotnet_diagnostic.CA2208.severity = none +dotnet_diagnostic.xUnit1000.severity = none [**/generated/**/*.cs] dotnet_analyzer_diagnostic.severity = none diff --git a/Azure.Functions.Cli.sln b/Azure.Functions.Cli.sln index 7c7f354a4..5b1cfa0bd 100644 --- a/Azure.Functions.Cli.sln +++ b/Azure.Functions.Cli.sln @@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreToolsHost", "src\CoreTo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Functions.Cli.TestFramework", "test\Cli\TestFramework\Azure.Functions.Cli.TestFramework.csproj", "{3A8E1907-E3A2-1CE0-BA8B-805B655FAF09}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Functions.Cli.E2E.Tests", "test\Cli\Func.E2E.Tests\Azure.Functions.Cli.E2E.Tests.csproj", "{D61226F6-3472-32C7-16F5-F07705F779CE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +59,10 @@ Global {3A8E1907-E3A2-1CE0-BA8B-805B655FAF09}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A8E1907-E3A2-1CE0-BA8B-805B655FAF09}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A8E1907-E3A2-1CE0-BA8B-805B655FAF09}.Release|Any CPU.Build.0 = Release|Any CPU + {D61226F6-3472-32C7-16F5-F07705F779CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D61226F6-3472-32C7-16F5-F07705F779CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D61226F6-3472-32C7-16F5-F07705F779CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D61226F6-3472-32C7-16F5-F07705F779CE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -70,6 +76,7 @@ Global {BC78165E-CE5B-4303-BB8E-BC172E5B86E0} = {154FDAF2-0E86-450E-BE57-4E3D410B0FAC} {0333D5B6-B628-4605-A51E-D0AEE4C3F1FC} = {5F51C958-39C0-4E0C-9165-71D0BCE647BC} {3A8E1907-E3A2-1CE0-BA8B-805B655FAF09} = {6EE1D011-2334-44F2-9D41-608B969DAE6D} + {D61226F6-3472-32C7-16F5-F07705F779CE} = {6EE1D011-2334-44F2-9D41-608B969DAE6D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FA1E01D6-A57B-4061-A333-EDC511D283C0} diff --git a/Directory.Build.props b/Directory.Build.props index 5fea93dfa..58bc65151 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,6 +4,7 @@ $(MSBuildThisFileDirectory) $(RepoRoot)eng/ $(RepoRoot)src/ + $(RepoRoot)test/ diff --git a/build/BuildSteps.cs b/build/BuildSteps.cs index cdd3ab6a6..7f66ecf17 100644 --- a/build/BuildSteps.cs +++ b/build/BuildSteps.cs @@ -332,6 +332,42 @@ public static void Test() Shell.Run("dotnet", $"test {Settings.TestProjectFile} -f net8.0 --logger trx"); } + public static void TestNewE2EProject() + { + var funcPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Path.Combine(Settings.OutputDir, "win-x86", "func.exe") + : Path.Combine(Settings.OutputDir, "linux-x64", "func"); + Environment.SetEnvironmentVariable("FUNC_PATH", funcPath); + + string durableStorageConnectionVar = "DURABLE_STORAGE_CONNECTION"; + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(durableStorageConnectionVar))) + { + Environment.SetEnvironmentVariable(durableStorageConnectionVar, "UseDevelopmentStorage=true"); + } + + Environment.SetEnvironmentVariable("DURABLE_FUNCTION_PATH", Settings.DurableFolder); + + Shell.Run("dotnet", $"test {Settings.NewTestProjectFile} -f net8.0 --blame-hang-timeout 10m --logger \"console;verbosity=detailed\""); + } + + public static void TestNewE2EProjectDotnetInProc() + { + var funcPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Path.Combine(Settings.OutputDir, "win-x86", "func.exe") + : Path.Combine(Settings.OutputDir, "linux-x64", "func"); + Environment.SetEnvironmentVariable("FUNC_PATH", funcPath); + + string durableStorageConnectionVar = "DURABLE_STORAGE_CONNECTION"; + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(durableStorageConnectionVar))) + { + Environment.SetEnvironmentVariable(durableStorageConnectionVar, "UseDevelopmentStorage=true"); + } + + Environment.SetEnvironmentVariable("DURABLE_FUNCTION_PATH", Settings.DurableFolder); + + Shell.Run("dotnet", $"test {Settings.NewTestProjectFile} -f net8.0 --logger trx --settings {Settings.RuntimeSettings} --blame-hang-timeout 10m"); + } + public static void CopyBinariesToSign() { string toSignDirPath = Path.Combine(Settings.OutputDir, Settings.SignInfo.ToSignDir); diff --git a/build/Program.cs b/build/Program.cs index 13c374fd4..115c7bc55 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -27,6 +27,8 @@ static void Main(string[] args) .Then(AddGoZip) .Then(TestPreSignedArtifacts, skip: !args.Contains("--ci")) .Then(CopyBinariesToSign, skip: !args.Contains("--ci")) + .Then(TestNewE2EProject) + .Then(TestNewE2EProjectDotnetInProc) .Then(Test) .Then(Zip) .Then(DotnetPublishForNupkg) diff --git a/build/Settings.cs b/build/Settings.cs index f52bb838e..6d647d9f6 100644 --- a/build/Settings.cs +++ b/build/Settings.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; @@ -39,6 +39,10 @@ private static string config(string @default = null, [CallerMemberName] string k public static readonly string DurableFolder = Path.Combine(TestProjectPath, "Resources", "DurableTestFolder"); + public static readonly string NewTestProjectFile = Path.Combine("..", "test", "Cli", "Func.E2E.Tests", "Azure.Functions.Cli.E2E.Tests.csproj"); + + public static readonly string RuntimeSettings = Path.Combine("..", "test", "Cli", "Func.E2E.Tests", ".runsettings", "start_tests", "ci_pipeline", "dotnet_inproc.runsettings"); + public static readonly string[] TargetRuntimes = new[] { "min.win-arm64", "min.win-x86", diff --git a/eng/ci/templates/official/jobs/build-test.yml b/eng/ci/templates/official/jobs/build-test.yml index 717cdb0cf..0463da773 100644 --- a/eng/ci/templates/official/jobs/build-test.yml +++ b/eng/ci/templates/official/jobs/build-test.yml @@ -108,8 +108,16 @@ jobs: DURABLE_STORAGE_CONNECTION: $(DURABLE_STORAGE_CONNECTION) TELEMETRY_INSTRUMENTATION_KEY: $(TELEMETRY_INSTRUMENTATION_KEY) IntegrationBuildNumber: $(INTEGRATIONBUILDNUMBER) + DirectoryToLogTo: $(Build.SourcesDirectory)/TestLogs displayName: 'Executing build script' + - task: 1ES.PublishPipelineArtifact@1 + condition: succeededOrFailed() + inputs: + targetPath: '$(Build.SourcesDirectory)/TestLogs' + artifactName: 'TestLogs' + artifactType: 'pipeline' + - template: ci/sign-files.yml@eng parameters: displayName: 'Authenticode signing (dll)' diff --git a/eng/ci/templates/public/jobs/build-test-public.yml b/eng/ci/templates/public/jobs/build-test-public.yml index e3aa08160..e283115b9 100644 --- a/eng/ci/templates/public/jobs/build-test-public.yml +++ b/eng/ci/templates/public/jobs/build-test-public.yml @@ -59,8 +59,16 @@ jobs: IsReleaseBuild: false IsPublicBuild: true IsCodeqlBuild: false + DirectoryToLogTo: $(Build.SourcesDirectory)/TestLogs displayName: 'Executing build script' + - task: 1ES.PublishPipelineArtifact@1 + condition: succeededOrFailed() + inputs: + targetPath: '$(Build.SourcesDirectory)/TestLogs' + artifactName: 'TestLogs' + artifactType: 'pipeline' + - task: PublishTestResults@2 inputs: testResultsFormat: 'VSTest' diff --git a/eng/scripts/ArtifactAssemblerHelpers/testArtifacts.ps1 b/eng/scripts/ArtifactAssemblerHelpers/testArtifacts.ps1 index aef9bfc35..1ed99eac5 100644 --- a/eng/scripts/ArtifactAssemblerHelpers/testArtifacts.ps1 +++ b/eng/scripts/ArtifactAssemblerHelpers/testArtifacts.ps1 @@ -8,8 +8,9 @@ Write-Host "$rootDir" ls $rootDir # Set the path to test project (.csproj) and runtime settings -$testProjectPath = ".\test\Azure.Functions.Cli.Tests\Azure.Functions.Cli.Tests.csproj" -$runtimeSettings = ".\test\Azure.Functions.Cli.Tests\E2E\StartTests_artifact_consolidation.runsettings" +$testProjectPath = ".\test\Cli\Func.E2E.Tests\Azure.Functions.Cli.E2E.Tests.csproj" +$defaultRuntimeSettings = ".\test\Cli\Func.E2E.Tests\.runsettings\start_tests\artifact_consolidation_pipeline\default.runsettings" +$inProcRuntimeSettings = ".\test\Cli\Func.E2E.Tests\.runsettings\start_tests\artifact_consolidation_pipeline\dotnet_inproc.runsettings" dotnet build $testProjectPath @@ -29,8 +30,11 @@ Get-ChildItem -Path $StagingDirectory -Directory | ForEach-Object { [System.Environment]::SetEnvironmentVariable("FUNC_PATH", $funcExePath.FullName, "Process") # Run dotnet test with the environment variable set - Write-Host "Running 'dotnet test' on test project: $testProjectPath" - dotnet test $testProjectPath --no-build --settings $runtimeSettings --logger "console;verbosity=detailed" + Write-Host "Running 'dotnet test' on test project: $testProjectPath with default artifacts" + dotnet test $testProjectPath --no-build --settings $defaultRuntimeSettings --logger "console;verbosity=detailed" + + Write-Host "Running 'dotnet test' on test project: $testProjectPath with inproc artifacts" + dotnet test $testProjectPath --no-build --settings $inProcRuntimeSettings --logger "console;verbosity=detailed" if ($LASTEXITCODE -ne 0) { # If the exit code is non-zero, throw an error diff --git a/eng/scripts/ArtifactAssemblerHelpers/testVsArtifacts.ps1 b/eng/scripts/ArtifactAssemblerHelpers/testVsArtifacts.ps1 index e14c36452..264c3cc4b 100644 --- a/eng/scripts/ArtifactAssemblerHelpers/testVsArtifacts.ps1 +++ b/eng/scripts/ArtifactAssemblerHelpers/testVsArtifacts.ps1 @@ -8,11 +8,33 @@ Write-Host "Root directory: $rootDir" ls $rootDir # Set the path to test project (.csproj) and runtime settings -$testProjectPath = ".\test\Azure.Functions.Cli.Tests\Azure.Functions.Cli.Tests.csproj" -$runtimeSettings = ".\test\Azure.Functions.Cli.Tests\E2E\StartTests_artifact_consolidation_visualstudio.runsettings" +$testProjectPath = ".\test\Cli\Func.E2E.Tests\Azure.Functions.Cli.E2E.Tests.csproj" +$runtimeSettings = ".\test\Cli\Func.E2E.Tests\.runsettings\start_tests\artifact_consolidation_pipeline\visualstudio.runsettings" [System.Environment]::SetEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME", "dotnet", "Process") +# Path for Visual Studio test projects (convert to absolute paths) +$net8VsProjectPath = ".\test\TestFunctionApps\VisualStudioTestProjects\TestNet8InProcProject" +$net6VsProjectPath = ".\test\TestFunctionApps\VisualStudioTestProjects\TestNet6InProcProject" + +# Resolve paths to absolute paths +$absoluteNet8VsProjectPath = (Resolve-Path -Path $net8VsProjectPath -ErrorAction SilentlyContinue).Path +if (-not $absoluteNet8VsProjectPath) { + $absoluteNet8VsProjectPath = (Join-Path -Path (Get-Location) -ChildPath $net8VsProjectPath) + Write-Host "Absolute NET8 VS project path (constructed): $absoluteNet8VsProjectPath" +} else { + Write-Host "Absolute NET8 VS project path (resolved): $absoluteNet8VsProjectPath" +} + +$absoluteNet6VsProjectPath = (Resolve-Path -Path $net6VsProjectPath -ErrorAction SilentlyContinue).Path +if (-not $absoluteNet6VsProjectPath) { + $absoluteNet6VsProjectPath = (Join-Path -Path (Get-Location) -ChildPath $net6VsProjectPath) + Write-Host "Absolute NET6 VS project path (constructed): $absoluteNet6VsProjectPath" +} else { + Write-Host "Absolute NET6 VS project path (resolved): $absoluteNet6VsProjectPath" +} + +# Build the test project dotnet build $testProjectPath # Loop through each subdirectory within the parent directory @@ -30,6 +52,10 @@ Get-ChildItem -Path $StagingDirectory -Directory | ForEach-Object { # Set the environment variable FUNC_PATH to the func.exe or func path [System.Environment]::SetEnvironmentVariable("FUNC_PATH", $funcExePath.FullName, "Process") + + # Set the environment variables for test projects - use the absolute paths + [System.Environment]::SetEnvironmentVariable("NET8_VS_PROJECT_PATH", $absoluteNet8VsProjectPath, "Process") + [System.Environment]::SetEnvironmentVariable("NET6_VS_PROJECT_PATH", $absoluteNet6VsProjectPath, "Process") # Run dotnet test with the environment variable set Write-Host "Running 'dotnet test' on test project: $testProjectPath" diff --git a/src/Cli/func/Common/Constants.cs b/src/Cli/func/Common/Constants.cs index 83f80bee3..1240c328d 100644 --- a/src/Cli/func/Common/Constants.cs +++ b/src/Cli/func/Common/Constants.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reflection; using Azure.Functions.Cli.Helpers; diff --git a/src/Cli/func/Properties/AssemblyInfo.cs b/src/Cli/func/Properties/AssemblyInfo.cs index 8c49d8e82..fb8a0528f 100644 --- a/src/Cli/func/Properties/AssemblyInfo.cs +++ b/src/Cli/func/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleToAttribute("Azure.Functions.Cli.Tests")] -[assembly: InternalsVisibleToAttribute("DynamicProxyGenAssembly2")] \ No newline at end of file +[assembly: InternalsVisibleToAttribute("DynamicProxyGenAssembly2")] +[assembly: InternalsVisibleToAttribute("Azure.Functions.Cli.E2E.Tests")] diff --git a/test/Azure.Functions.Cli.Tests/Azure.Functions.Cli.Tests.csproj b/test/Azure.Functions.Cli.Tests/Azure.Functions.Cli.Tests.csproj index 3f36857ab..06bad1b96 100644 --- a/test/Azure.Functions.Cli.Tests/Azure.Functions.Cli.Tests.csproj +++ b/test/Azure.Functions.Cli.Tests/Azure.Functions.Cli.Tests.csproj @@ -48,8 +48,5 @@ - - - $(MSBuildProjectDirectory)\E2E\StartTests_default.runsettings - + diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs deleted file mode 100644 index fb972c1db..000000000 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ /dev/null @@ -1,1968 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Sockets; -using System.Text; -using System.Threading.Tasks; -using Azure.Functions.Cli.Common; -using Azure.Functions.Cli.Tests.E2E.Helpers; -using FluentAssertions; -using Xunit; -using Xunit.Abstractions; - -namespace Azure.Functions.Cli.Tests.E2E -{ - public class StartTests : BaseE2ETest, IAsyncLifetime - { - private int _funcHostPort; - private const string _serverNotReady = "Host was not ready after 10 seconds"; - - public StartTests(ITestOutputHelper output) : base(output) { } - - public async Task InitializeAsync() - { - try - { - _funcHostPort = ProcessHelper.GetAvailablePort(); - } - catch - { - // Just use default func host port if we encounter any issues - _funcHostPort = 7071; - } - - await Task.CompletedTask; - } - - [Fact] - public async Task Start_PowershellApp_SuccessfulFunctionExecution() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime powershell --managed-dependencies false", - "new --template \"Http trigger\" --name HttpTrigger" - }, - CommandTimeout = TimeSpan.FromMinutes(300), - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort}" - }, - ExpectExit = false, - CommandTimeout = TimeSpan.FromMinutes(300), - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}/") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - await Task.Delay(TimeSpan.FromSeconds(2)); - result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); - } - }, - } - }, _output); - } - - [Fact] - public async Task Start_NodeJsApp_SuccessfulFunctionExecution_WithoutSpecifyingDefaultHost() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime node", - "new --template \"Http trigger\" --name HttpTrigger" - }, - CommandTimeout = TimeSpan.FromSeconds(300) - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose" - }, - ExpectExit = false, - OutputContains = new[] - { - "Functions:", - $"HttpTrigger: [GET,POST] http://localhost:{_funcHostPort}/api/HttpTrigger" - }, - OutputDoesntContain = new string[] - { - "Content root path:" // ASPNETCORE_SUPPRESSSTATUSMESSAGES is set to true by default - }, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}/") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - result.Should().Be("Hello, Test!", because: "response from default function should be 'Hello, {name}!'"); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain("4.10"); - testOutputHelper.Output.Should().Contain("Selected out-of-process host."); - } - } - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Fact] - public async Task Start_NodeJsApp_SuccessfulFunctionExecution_WithSpecifyingDefaultHost() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime node", - "new --template \"Http trigger\" --name HttpTrigger" - }, - CommandTimeout = TimeSpan.FromSeconds(300) - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose --runtime default" - }, - ExpectExit = false, - OutputContains = new[] - { - "Functions:", - $"HttpTrigger: [GET,POST] http://localhost:{_funcHostPort}/api/HttpTrigger" - }, - OutputDoesntContain = new string[] - { - "Content root path:" // ASPNETCORE_SUPPRESSSTATUSMESSAGES is set to true by default - }, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}/") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - result.Should().Be("Hello, Test!", because: "response from default function should be 'Hello, {name}!'"); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain("4.10"); - testOutputHelper.Output.Should().Contain("Selected out-of-process host."); - } - } - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Fact] - public async Task Start_NodeJsApp_V3_SuccessfulFunctionExecution() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime node -m v3", - "new --template \"Http trigger\" --name HttpTrigger" - }, - CommandTimeout = TimeSpan.FromSeconds(300) - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort}" - }, - ExpectExit = false, - OutputContains = new[] - { - "Functions:", - $"HttpTrigger: [GET,POST] http://localhost:{_funcHostPort}/api/HttpTrigger" - }, - OutputDoesntContain = new string[] - { - "Initializing function HTTP routes", - "Content root path:" // ASPNETCORE_SUPPRESSSTATUSMESSAGES is set to true by default - }, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}/") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); - } - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] - public async Task Start_InProc_SuccessfulFunctionExecution() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet", - "new --template Httptrigger --name HttpTrigger" - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --build --port {_funcHostPort}" - }, - ExpectExit = false, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - await Task.Delay(TimeSpan.FromSeconds(2)); - result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain($".NET 6 is no longer supported. Please consider migrating to a supported version. For more information, see https://aka.ms/azure-functions/dotnet/net8-in-process. If you intend to target .NET 8 on the in-process model, make sure that '{Constants.InProcDotNet8EnabledSetting}' is set to '1' in {Constants.LocalSettingsJsonFileName}."); - } - } - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.UseInConsolidatedArtifactGeneration)] - public async Task Start_InProc_Net8_SuccessfulFunctionExecution_WithoutSpecifyingRuntime() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet --target-framework net8.0", - "new --template Httptrigger --name HttpTrigger", - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose" - }, - ExpectExit = false, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain("Starting child process for inproc8 model host."); - testOutputHelper.Output.Should().Contain("Selected inproc8 host."); - } - } - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.UseInConsolidatedArtifactGeneration)] - public async Task Start_InProc_Net8_SuccessfulFunctionExecution_WithSpecifyingRuntime() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet --target-framework net8.0", - "new --template Httptrigger --name HttpTrigger", - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --runtime inproc8 --verbose" - }, - ExpectExit = false, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain("Starting child process for inproc8 model host."); - testOutputHelper.Output.Should().Contain("Selected inproc8 host."); - } - } - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.UseInVisualStudioConsolidatedArtifactGeneration)] - public async Task Start_InProc_Net8_VisualStudio_SuccessfulFunctionExecution() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose" - }, - ExpectExit = false, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/Function1?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain("Loading .NET 8 host"); - } - } - }, - CommandTimeout = TimeSpan.FromSeconds(300), - } - }, _output, "../../../../test/Azure.Functions.Cli.Tests/E2E/TestProject/TestNet8InProcProject"); - - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.UseInVisualStudioConsolidatedArtifactGeneration)] - public async Task Start_InProc_Net6_VisualStudio_SuccessfulFunctionExecution() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose" - }, - ExpectExit = false, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/Function2?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain("Loading .NET 6 host"); - } - } - }, - CommandTimeout = TimeSpan.FromSeconds(300), - } - }, _output, "../../../../test/Azure.Functions.Cli.Tests/E2E/TestProject/TestNet6InProcProject"); - - } - - [Fact] - public async Task Start_DotnetIsolated_Net9_SuccessfulFunctionExecution() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet-isolated --target-framework net9.0", - "new --template Httptrigger --name HttpTrigger" - }, - }, - new RunConfiguration - { - Commands = new[] - { - $"start --build --port {_funcHostPort} --verbose" - }, - ExpectExit = false, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - result.Should().Be("Welcome to Azure Functions!", because: "response from default function should be 'Welcome to Azure Functions!'"); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain("4.10"); - testOutputHelper.Output.Should().Contain("Selected out-of-process host."); - } - } - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Theory] - [InlineData("function", false, "Welcome to Azure Functions!", "response from default function should be 'Welcome to Azure Functions!'", "Selected out-of-process host.")] - [InlineData("anonymous", true, "Welcome to Azure Functions!", "response from default function should be 'Welcome to Azure Functions!'", "Selected out-of-process host.")] - [InlineData("anonymous", true, "", "the call to the function is unauthorized", "\"status\": \"401\"")] - public async Task Start_DotnetIsolated_Test_EnableAuthFeature(string authLevel, bool enableAuth, string resultOfFunctionCall, string becauseResult, string testOutputHelperValue) - { - string templateCommand = $"new --template Httptrigger --name HttpTrigger --authlevel ${authLevel}"; - string startCommand = enableAuth ? $"start --build --port {_funcHostPort} --verbose --enableAuth" : $"start --build --port {_funcHostPort} --verbose"; - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet-isolated", - templateCommand, - }, - }, - new RunConfiguration - { - Commands = new[] - { - startCommand, - }, - ExpectExit = false, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - result.Should().Be(resultOfFunctionCall, because: becauseResult); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain(testOutputHelperValue); - } - } - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Fact] - public async Task Start_WithInspect_DebuggerIsStarted() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime node", - "new --template \"Http trigger\" --name HttpTrigger", - }, - CommandTimeout = TimeSpan.FromSeconds(300) - }, - new RunConfiguration - { - WaitForRunningHostState = true, - HostProcessPort = _funcHostPort, - Commands = new[] - { - $"start --port {_funcHostPort} --verbose --language-worker -- \"--inspect=5050\"" - }, - ExpectExit = false, - OutputContains = new[] - { - "Debugger listening on ws://127.0.0.1:5050" - }, - Test = async (_, p, stdout) => - { - await LogWatcher.WaitForLogOutput(stdout, "Debugger listening on", TimeSpan.FromSeconds(5)); - p.Kill(); - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Fact] - public async Task Start_PortInUse_FailsWithExpectedError() - { - var tcpListner = new TcpListener(IPAddress.Any, _funcHostPort); - try - { - tcpListner.Start(); - - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime node", - "new --template \"Http Trigger\" --name HttpTrigger" - }, - CommandTimeout = TimeSpan.FromSeconds(300) - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort}" - }, - ExpectExit = true, - ExitInError = true, - ErrorContains = new[] { $"Port {_funcHostPort} is unavailable" }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - finally - { - tcpListner.Stop(); - } - } - - [Fact] - public async Task Start_EmptyEnvVars_HandledAsExpected() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime node", - "new --template \"Http trigger\" --name HttpTrigger", - "settings add emptySetting EMPTY_VALUE", - }, - Test = async (workingDir, p, _) => - { - var settingsFile = Path.Combine(workingDir, "local.settings.json"); - var content = File.ReadAllText(settingsFile); - content = content.Replace("EMPTY_VALUE", ""); - File.WriteAllText(settingsFile, content); - await Task.CompletedTask; - }, - CommandTimeout = TimeSpan.FromSeconds(300), - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort}" - }, - ExpectExit = false, - Test = async (w, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}/") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - response.EnsureSuccessStatusCode(); - p.Kill(); - } - }, - OutputDoesntContain = new string[] - { - "Skipping 'emptySetting' from local settings as it's already defined in current environment variables." - }, - CommandTimeout = TimeSpan.FromSeconds(300), - } - }, _output); - } - - [Fact] - public async Task Start_FunctionsStartArgument_OnlySelectedFunctionsRun() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime javascript", - "new --template \"Http trigger\" --name http1", - "new --template \"Http trigger\" --name http2", - "new --template \"Http trigger\" --name http3" - }, - CommandTimeout = TimeSpan.FromSeconds(300) - }, - new RunConfiguration - { - Commands = new[] - { - $"start --functions http2 http1 --port {_funcHostPort}" - }, - ExpectExit = false, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}/") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/http1?name=Test"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - - response = await client.GetAsync("/api/http2?name=Test"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - - response = await client.GetAsync("/api/http3?name=Test"); - response.StatusCode.Should().Be(HttpStatusCode.NotFound); - p.Kill(); - } - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Fact] - public async Task Start_LanguageWorker_LogLevelOverridenViaSettings_LogLevelSetToExpectedValue() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime node", - "settings add AzureFunctionsJobHost__logging__logLevel__Default Debug", - "new --template \"Http trigger\" --name HttpTrigger", - }, - CommandTimeout = TimeSpan.FromSeconds(300) - }, - new RunConfiguration - { - WaitForRunningHostState = true, - HostProcessPort = _funcHostPort, - Commands = new[] - { - $"start --port {_funcHostPort}" - }, - ExpectExit = false, - OutputContains = new[] - { - "Workers Directory set to" - }, - Test = async (_, p, stdout) => - { - await LogWatcher.WaitForLogOutput(stdout, "Workers Directory set to", TimeSpan.FromSeconds(5)); - p.Kill(); - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Fact] - public async Task Start_Net8InProc_ExpectedToFail_WithSpecifyingRuntime() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet --target-framework net8.0", - "new --template Httptrigger --name HttpTrigger", - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose --runtime inproc8" - }, - ExpectExit = true, - ExitInError = true, - ErrorContains = ["Failed to locate the inproc8 model host"], - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - await Task.Delay(TimeSpan.FromSeconds(2)); - } - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Fact] - public async Task Start_Net8InProc_ExpectedToFail_WithoutSpecifyingRuntime() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet --target-framework net8.0", - "new --template Httptrigger --name HttpTrigger", - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose" - }, - ExpectExit = true, - ExitInError = true, - ErrorContains = ["Failed to locate the inproc8 model host"], - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - await Task.Delay(TimeSpan.FromSeconds(2)); - } - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.UseInConsolidatedArtifactGeneration)] - public async Task Start_InProc_Net6_SuccessfulFunctionExecution_WithSpecifyingRuntime() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet --target-framework net6.0", - "new --template Httptrigger --name HttpTrigger", - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose --runtime inproc6" - }, - ExpectExit = false, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain("Starting child process for inproc6 model host."); - testOutputHelper.Output.Should().Contain("Selected inproc6 host."); - } - } - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.UseInConsolidatedArtifactGeneration)] - public async Task Start_InProc_Net6_SuccessfulFunctionExecution_WithoutSpecifyingRuntime() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet --target-framework net6.0", - "new --template Httptrigger --name HttpTrigger", - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose" - }, - ExpectExit = false, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - result.Should().Be("Hello, Test. This HTTP triggered function executed successfully.", because: "response from default function should be 'Hello, {name}. This HTTP triggered function executed successfully.'"); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain("Starting child process for inproc6 model host."); - testOutputHelper.Output.Should().Contain("Selected inproc6 host."); - } - } - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Fact] - public async Task Start_InProc_Dotnet6_WithoutSpecifyingRuntime_ExpectedToFail() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet --target-framework net6.0", - "new --template Httptrigger --name HttpTrigger", - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose" - }, - ExpectExit = false, - ExitInError = true, - ErrorContains = ["Failed to locate the inproc6 model host at"], - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - - } - await Task.CompletedTask; - }, - CommandTimeout = TimeSpan.FromSeconds(100) - } - }, _output); - } - - [Fact] - public async Task Start_InProc_Dotnet6_WithSpecifyingRuntime_ExpectedToFail() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet --target-framework net6.0", - "new --template Httptrigger --name HttpTrigger", - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose --runtime inproc6" - }, - ExpectExit = false, - ExitInError = true, - ErrorContains = ["Failed to locate the inproc6 model host at"], - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - - } - await Task.CompletedTask; - }, - CommandTimeout = TimeSpan.FromSeconds(100) - } - }, _output); - } - - [Fact] - public async Task Start_LanguageWorker_LogLevelOverridenViaHostJson_LogLevelSetToExpectedValue() - { - var functionName = "HttpTrigger"; - - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime node", - $"new --template Httptrigger --name {functionName}", - }, - Test = async (workingDir, p, _) => - { - var filePath = Path.Combine(workingDir, "host.json"); - string hostJsonContent = "{\"version\": \"2.0\",\"logging\": {\"logLevel\": {\"Default\": \"None\"}}}"; - await File.WriteAllTextAsync(filePath, hostJsonContent); - }, - CommandTimeout = TimeSpan.FromSeconds(300) - }, - new RunConfiguration - { - WaitForRunningHostState = true, - HostProcessPort = _funcHostPort, - Commands = new[] - { - $"start --port {_funcHostPort}" - }, - ExpectExit = false, - OutputContains = new [] - { - "Worker process started and initialized" - }, - OutputDoesntContain = new string[] - { - "Initializing function HTTP routes" - }, - Test = async (_, p, stdout) => - { - await LogWatcher.WaitForLogOutput(stdout, "Worker process started and initialized", TimeSpan.FromSeconds(5)); - p.Kill(); - }, - CommandTimeout = TimeSpan.FromSeconds(300) - }, - }, _output); - } - - [Fact] - public async Task DontStart_InProc6_SpecifiedRuntime_ForDotnetIsolated() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet-isolated", - "new --template Httptrigger --name HttpTrigger", - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose --runtime inproc6" - }, - ExpectExit = true, - ExitInError = true, - ErrorContains = ["The runtime argument value provided, 'inproc6', is invalid. The provided value is only valid for the worker runtime 'dotnet'."], - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - await Task.Delay(TimeSpan.FromSeconds(2)); - } - }, - CommandTimeout = TimeSpan.FromSeconds(100), - }, - }, _output); - } - - [Fact] - public async Task DontStart_InProc8_SpecifiedRuntime_ForDotnetIsolated() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet-isolated", - "new --template Httptrigger --name HttpTrigger", - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose --runtime inproc8" - }, - ExpectExit = false, - ExitInError = true, - ErrorContains = ["The runtime argument value provided, 'inproc8', is invalid. The provided value is only valid for the worker runtime 'dotnet'."], - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - await Task.Delay(TimeSpan.FromSeconds(2)); - } - }, - CommandTimeout = TimeSpan.FromSeconds(100), - }, - }, _output); - } - - [Fact] - public async Task DontStart_InProc8_SpecifiedRuntime_ForDotnet6InProc() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet --target-framework net6.0", - "new --template Httptrigger --name HttpTrigger", - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose --runtime inproc8" - }, - ExpectExit = false, - ExitInError = true, - ErrorContains = [$"The runtime argument value provided, 'inproc8', is invalid. For the .NET 8 runtime on the in-process model, you must set the '{Constants.InProcDotNet8EnabledSetting}' environment variable to '1'. For more information, see https://aka.ms/azure-functions/dotnet/net8-in-process."], - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - await Task.Delay(TimeSpan.FromSeconds(2)); - } - }, - CommandTimeout = TimeSpan.FromSeconds(100), - }, - }, _output); - } - - [Fact] - public async Task DontStart_DefaultRuntime_SpecifiedRuntime_ForDotnet6InProc() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet --target-framework net6.0", - "new --template Httptrigger --name HttpTrigger", - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose --runtime default" - }, - ExpectExit = false, - ExitInError = true, - ErrorContains = ["The runtime argument value provided, 'default', is invalid. The provided value is only valid for the worker runtime 'dotnetIsolated'."], - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - await Task.Delay(TimeSpan.FromSeconds(2)); - } - }, - CommandTimeout = TimeSpan.FromSeconds(100), - }, - }, _output); - } - - [Fact] - public async Task DontStart_DefaultRuntime_SpecifiedRuntime_ForDotnet8InProc() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet --target-framework net8.0", - "new --template Httptrigger --name HttpTrigger", - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose --runtime default" - }, - ExpectExit = false, - ExitInError = true, - ErrorContains = ["The runtime argument value provided, 'default', is invalid. The provided value is only valid for the worker runtime 'dotnetIsolated'."], - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - await Task.Delay(TimeSpan.FromSeconds(2)); - } - }, - CommandTimeout = TimeSpan.FromSeconds(100), - }, - }, _output); - } - - [Fact] - public async Task DontStart_InProc6_SpecifiedRuntime_ForDotnet8InProc() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet --target-framework net8.0", - "new --template Httptrigger --name HttpTrigger", - }, - CommandTimeout = TimeSpan.FromSeconds(300) - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose --runtime inproc6" - }, - ExpectExit = false, - ExitInError = true, - ErrorContains = ["The runtime argument value provided, 'inproc6', is invalid. For the 'inproc6' runtime, the 'FUNCTIONS_INPROC_NET8_ENABLED' environment variable cannot be be set. See https://aka.ms/azure-functions/dotnet/net8-in-process."], - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - await Task.Delay(TimeSpan.FromSeconds(2)); - } - }, - CommandTimeout = TimeSpan.FromSeconds(100), - }, - }, _output); - } - - [Fact] - public async Task DontStart_InProc6_SpecifiedRuntime_ForNonDotnetApp() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime node", - "new --template \"Httptrigger\" --name HttpTrigger", - }, - CommandTimeout = TimeSpan.FromSeconds(300) - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose --runtime inproc6" - }, - ExpectExit = false, - ExitInError = true, - ErrorContains = ["The runtime argument value provided, 'inproc6', is invalid. The provided value is only valid for the worker runtime 'dotnet'."], - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - await Task.Delay(TimeSpan.FromSeconds(2)); - } - }, - CommandTimeout = TimeSpan.FromSeconds(100), - }, - }, _output); - } - - [Fact] - public async Task DontStart_InProc8_SpecifiedRuntime_ForNonDotnetApp() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime node", - "new --template \"Httptrigger\" --name HttpTrigger", - }, - CommandTimeout = TimeSpan.FromSeconds(300) - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose --runtime inproc8" - }, - ExpectExit = false, - ExitInError = true, - ErrorContains = ["The runtime argument value provided, 'inproc8', is invalid. The provided value is only valid for the worker runtime 'dotnet'."], - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - await Task.Delay(TimeSpan.FromSeconds(2)); - } - }, - CommandTimeout = TimeSpan.FromSeconds(100), - }, - }, _output); - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.UseInConsolidatedArtifactGeneration)] - public async Task Start_DotnetIsolated_WithRuntimeSpecified() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet-isolated", - "new --template Httptrigger --name HttpTrigger", - }, - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose --runtime default" - }, - ExpectExit = false, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - await Task.Delay(TimeSpan.FromSeconds(2)); - result.Should().Be("Welcome to Azure Functions!", because: "response from default function should be 'Welcome to Azure Functions!'"); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain("4.10"); - testOutputHelper.Output.Should().Contain("Selected default host."); - } - } - }, - CommandTimeout = TimeSpan.FromSeconds(300), - } - }, _output); - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.UseInConsolidatedArtifactGeneration)] - public async Task Start_DotnetIsolated_WithoutRuntimeSpecified() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet-isolated", - "new --template Httptrigger --name HttpTrigger", - }, - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --verbose" - }, - ExpectExit = false, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - await Task.Delay(TimeSpan.FromSeconds(2)); - result.Should().Be("Welcome to Azure Functions!", because: "response from default function should be 'Welcome to Azure Functions!'"); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain("4.10"); - testOutputHelper.Output.Should().Contain("Selected default host."); - } - } - }, - CommandTimeout = TimeSpan.FromSeconds(300), - } - }, _output); - } - - [Fact] - public async Task Start_LanguageWorker_InvalidFunctionJson_FailsWithExpectedError() - { - var functionName = "HttpTriggerJS"; - - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime node -m v3", - $"new --template \"Http Trigger\" --name {functionName}", - }, - Test = async (workingDir, _, _) => - { - var filePath = Path.Combine(workingDir, functionName, "function.json"); - var functionJson = await File.ReadAllTextAsync(filePath); - functionJson = functionJson.Replace("\"type\": \"http\"", "\"type\": \"http2\""); - await File.WriteAllTextAsync(filePath, functionJson); - }, - CommandTimeout = TimeSpan.FromSeconds(300) - }, - new RunConfiguration - { - WaitForRunningHostState = true, - HostProcessPort = _funcHostPort, - Commands = new[] - { - $"start --port {_funcHostPort}" - }, - ExpectExit = false, - OutputContains = new [] - { - "The binding type(s) 'http2' were not found in the configured extension bundle. Please ensure the type is correct and the correct version of extension bundle is configured." - }, - Test = async (_, p, stdout) => - { - await LogWatcher.WaitForLogOutput(stdout, "The binding type(s) 'http2' were not found", TimeSpan.FromSeconds(5)); - p.Kill(); - } - } - }, _output); - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] - public async Task Start_InProc_LogLevelOverridenViaHostJson_LogLevelSetToExpectedValue() - { - var functionName = "HttpTriggerCSharp"; - - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet", - $"new --template Httptrigger --name {functionName}", - }, - Test = async (workingDir, p, _) => - { - var filePath = Path.Combine(workingDir, "host.json"); - string hostJsonContent = "{\"version\": \"2.0\",\"logging\": {\"logLevel\": {\"Default\": \"Debug\"}}}"; - await File.WriteAllTextAsync(filePath, hostJsonContent); - }, - }, - new RunConfiguration - { - WaitForRunningHostState = true, - HostProcessPort = _funcHostPort, - Commands = new[] - { - $"start --port {_funcHostPort}" - }, - ExpectExit = false, - OutputContains = new [] - { - "Host configuration applied." - }, - Test = async (_, p, stdout) => - { - await LogWatcher.WaitForLogOutput(stdout, "Host configuration applied", TimeSpan.FromSeconds(5)); - p.Kill(); - } - }, - }, _output); - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] - public async Task Start_InProc_LogLevelOverridenWithFilter_LogLevelSetToExpectedValue() - { - var functionName = "HttpTriggerCSharp"; - - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet", - $"new --template Httptrigger --name {functionName}", - }, - Test = async (workingDir, p, _) => - { - var filePath = Path.Combine(workingDir, "host.json"); - string hostJsonContent = "{\"version\": \"2.0\",\"logging\": {\"logLevel\": {\"Default\": \"None\", \"Host.Startup\": \"Information\"}}}"; - await File.WriteAllTextAsync(filePath, hostJsonContent); - }, - }, - new RunConfiguration - { - WaitForRunningHostState = true, - HostProcessPort = _funcHostPort, - Commands = new[] - { - $"start --port {_funcHostPort}" - }, - ExpectExit = false, - OutputContains = new [] - { - "Found the following functions:" - }, - OutputDoesntContain = new string[] - { - "Reading host configuration file" - }, - Test = async (_, p, stdout) => - { - await LogWatcher.WaitForLogOutput(stdout, "Reading host configuration file", TimeSpan.FromSeconds(5)); - p.Kill(); - } - }, - }, _output); - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] - public async Task Start_InProc_InvalidHostJson_FailsWithExpectedError() - { - var functionName = "HttpTriggerCSharp"; - - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet", - $"new --template Httptrigger --name {functionName}", - - }, - Test = async (workingDir, p, _) => - { - var filePath = Path.Combine(workingDir, "host.json"); - string hostJsonContent = "{ \"version\": \"2.0\", \"extensionBundle\": { \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\", \"version\": \"[2.*, 3.0.0)\" }}"; - await File.WriteAllTextAsync(filePath, hostJsonContent); - }, - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort}" - }, - ExpectExit = true, - OutputContains = new[] { "Extension bundle configuration should not be present" }, - }, - }, _output); - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] - public async Task Start_InProc_MissingHostJson_FailsWithExpectedError() - { - var functionName = "HttpTriggerCSharp"; - - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet", - $"new --template Httptrigger --name {functionName}", - }, - Test = async (workingDir, p, _) => - { - var hostJsonPath = Path.Combine(workingDir, "host.json"); - File.Delete(hostJsonPath); - await Task.CompletedTask; - }, - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort}" - }, - ExpectExit = true, - OutputContains = new[] { "Host.json file in missing" }, - }, - }, _output); - } - - [Theory(Skip = "Test is flakey")] - [InlineData("dotnet")] - [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] - // [InlineData("dotnet-isolated")] Skip due to dotnet error on x86: https://github.com/Azure/azure-functions-core-tools/issues/3873 - public async Task Start_Dotnet_WithUserSecrets_SuccessfulFunctionExecution(string language) - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - $"init . --worker-runtime {language}", - "new --template \"Http trigger\" --name http1", - "new --template \"Queue trigger\" --name queue1" - }, - }, - new RunConfiguration - { - PreTest = (workingDir) => - { - // add connection string setting to queue code - var queueCodePath = Path.Combine(workingDir, "queue1.cs"); - Assert.True(File.Exists(queueCodePath)); - _output.WriteLine($"Writing to file {queueCodePath}"); - StringBuilder queueCodeStringBuilder = new StringBuilder(); - using (StreamReader sr = File.OpenText(queueCodePath)) - { - string s = ""; - while ((s = sr.ReadLine()) != null) - { - queueCodeStringBuilder.Append(s); - } - } - var queueCodeString = queueCodeStringBuilder.ToString(); - _output.WriteLine($"Old Queue File: {queueCodeString}"); - var replacedText = queueCodeString.Replace("Connection = \"\"", "Connection = \"ConnectionStrings:MyQueueConn\""); - _output.WriteLine($"New Queue File: {replacedText}"); - File.WriteAllText(queueCodePath, replacedText); - - // clear local.settings.json - var localSettingsPath = Path.Combine(workingDir, "local.settings.json"); - Assert.True(File.Exists(localSettingsPath)); - _output.WriteLine($"Writing to file {localSettingsPath}"); - File.WriteAllText(localSettingsPath, "{ \"IsEncrypted\": false, \"Values\": {\""+ Constants.FunctionsWorkerRuntime + "\": \"" + language + "\", \"AzureWebJobsSecretStorageType\": \"files\"} }"); - - // init and set user secrets - Dictionary userSecrets = new Dictionary() - { - { Constants.AzureWebJobsStorage, "UseDevelopmentStorage=true" }, - { "ConnectionStrings:MyQueueConn", "UseDevelopmentStorage=true" }, - }; - SetUserSecrets(workingDir, userSecrets); - }, - WaitForRunningHostState = true, - HostProcessPort = _funcHostPort, - Commands = new[] - { - $"start --build --port {_funcHostPort}", - }, - ExpectExit = false, - OutputContains = new string[] - { - "Using for user secrets file configuration." - }, - CommandTimeout = TimeSpan.FromSeconds(300), - Test = async (_, p, stdout) => - { - await QueueStorageHelper.InsertIntoQueue("myqueue-items", "hello world"); - - await LogWatcher.WaitForLogOutput(stdout, "C# Queue trigger function processed: hello world", TimeSpan.FromSeconds(10)); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain("C# Queue trigger function processed: hello world"); - } - - p.Kill(); - } - } - }, _output); - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] - public async Task Start_Dotnet_WithUserSecrets_MissingStorageConnString_FailsWithExpectedError() - { - string AzureWebJobsStorageConnectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage"); - Skip.If(!string.IsNullOrEmpty(AzureWebJobsStorageConnectionString), - reason: "AzureWebJobsStorage should be not set to verify this test."); - - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet", - "new --template \"Http trigger\" --name http1", - "new --template \"Queue trigger\" --name queue1" - }, - }, - new RunConfiguration - { - PreTest = (workingDir) => - { - // add connection string setting to queue code - var queueCodePath = Path.Combine(workingDir, "queue1.cs"); - Assert.True(File.Exists(queueCodePath)); - _output.WriteLine($"Writing to file {queueCodePath}"); - StringBuilder queueCodeStringBuilder = new StringBuilder(); - using (StreamReader sr = File.OpenText(queueCodePath)) - { - string s = ""; - while ((s = sr.ReadLine()) != null) - { - queueCodeStringBuilder.Append(s); - } - } - var queueCodeString = queueCodeStringBuilder.ToString(); - _output.WriteLine($"Old Queue File: {queueCodeString}"); - var replacedText = queueCodeString.Replace("Connection = \"\"", "Connection = \"ConnectionStrings:MyQueueConn\""); - _output.WriteLine($"New Queue File: {replacedText}"); - File.WriteAllText(queueCodePath, replacedText); - - // clear local.settings.json - var localSettingsPath = Path.Combine(workingDir, "local.settings.json"); - Assert.True(File.Exists(queueCodePath)); - _output.WriteLine($"Writing to file {localSettingsPath}"); - File.WriteAllText(localSettingsPath, "{ \"IsEncrypted\": false, \"Values\": {\""+ Constants.FunctionsWorkerRuntime + "\": \"dotnet\"} }"); - - // init and set user secrets - Dictionary userSecrets = new Dictionary() - { - { "ConnectionStrings:MyQueueConn", "UseDevelopmentStorage=true" }, - }; - SetUserSecrets(workingDir, userSecrets); - }, - Commands = new[] - { - $"start --functions http1 --csharp --port {_funcHostPort}", - }, - CommandTimeout = TimeSpan.FromSeconds(300), - ExpectExit = true, - OutputContains = new[] - { - "Missing value for AzureWebJobsStorage in local.settings.json. This is required for all triggers other than httptrigger, kafkatrigger, orchestrationTrigger, activityTrigger, entityTrigger", - "A host error has occurred during startup operation" - } - } - }, _output); - } - - [Fact(Skip = "blob storage repository check fails")] - public async Task Start_Dotnet_WithUserSecrets_MissingBindingSetting_FailsWithExpectedError() - { - string AzureWebJobsStorageConnectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage"); - Skip.If(!string.IsNullOrEmpty(AzureWebJobsStorageConnectionString), - reason: "AzureWebJobsStorage should be not set to verify this test."); - - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet", - "new --template \"Http trigger\" --name http1", - "new --template \"Queue trigger\" --name queue1" - }, - }, - new RunConfiguration - { - PreTest = (workingDir) => - { - // add connection string setting to queue code - var queueCodePath = Path.Combine(workingDir, "queue1.cs"); - Assert.True(File.Exists(queueCodePath)); - _output.WriteLine($"Writing to file {queueCodePath}"); - StringBuilder queueCodeStringBuilder = new StringBuilder(); - using (StreamReader sr = File.OpenText(queueCodePath)) - { - string s = ""; - while ((s = sr.ReadLine()) != null) - { - queueCodeStringBuilder.Append(s); - } - } - var queueCodeString = queueCodeStringBuilder.ToString(); - _output.WriteLine($"Old Queue File: {queueCodeString}"); - var replacedText = queueCodeString.Replace("Connection = \"\"", "Connection = \"ConnectionStrings:MyQueueConn\""); - _output.WriteLine($"New Queue File: {replacedText}"); - File.WriteAllText(queueCodePath, replacedText); - - // clear local.settings.json - var localSettingsPath = Path.Combine(workingDir, "local.settings.json"); - Assert.True(File.Exists(queueCodePath)); - _output.WriteLine($"Writing to file {localSettingsPath}"); - File.WriteAllText(localSettingsPath, "{ \"IsEncrypted\": false, \"Values\": {\""+ Constants.FunctionsWorkerRuntime + "\": \"dotnet\"} }"); - - // init and set user secrets - Dictionary userSecrets = new Dictionary() - { - { Constants.AzureWebJobsStorage, "UseDevelopmentStorage=true" }, - }; - SetUserSecrets(workingDir, userSecrets); - }, - WaitForRunningHostState = true, - HostProcessPort = _funcHostPort, - Commands = new[] - { - $"start --functions http1 --csharp --port {_funcHostPort}", - }, - ExpectExit = false, - OutputContains = new[] - { - "Warning: Cannot find value named 'ConnectionStrings:MyQueueConn' in local.settings.json that matches 'connection' property set on 'queueTrigger' in", - "You can run 'func azure functionapp fetch-app-settings ' or specify a connection string in local.settings.json." - }, - Test = async (_, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}/") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/http1?name=Test"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - p.Kill(); - } - }, - CommandTimeout = TimeSpan.FromSeconds(300) - } - }, _output); - } - - [Theory] - [InlineData("dotnet-isolated", "--dotnet-isolated", "HttpTriggerFunc: [GET,POST] http://localhost:", true, false)] // Runtime parameter set (dni), successful startup & invocation - [InlineData("node", "--node", "HttpTriggerFunc: [GET,POST] http://localhost:", true, false)] // Runtime parameter set (node), successful startup & invocation - [InlineData("dotnet", "--worker-runtime None", $"Use the up/down arrow keys to select a worker runtime:", false, false)] // Runtime parameter set to None, worker runtime prompt displayed - [InlineData("dotnet", "", $"Use the up/down arrow keys to select a worker runtime:", false, false)] // Runtime parameter not provided, worker runtime prompt displayed - [InlineData("dotnet-isolated", "", "HttpTriggerFunc: [GET,POST] http://localhost:", true, true)] // Runtime value is set via environment variable, successful startup & invocation - public async Task Start_MissingLocalSettingsJson_BehavesAsExpected(string language, string runtimeParameter, string expectedOutput, bool invokeFunction, bool setRuntimeViaEnvironment) - { - try - { - if (setRuntimeViaEnvironment) - { - Environment.SetEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME", "dotnet-isolated"); - } - - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - $"init . --worker-runtime {language}", - $"new --template Httptrigger --name HttpTriggerFunc", - }, - CommandTimeout = TimeSpan.FromSeconds(300), - }, - new RunConfiguration - { - PreTest = (workingDir) => - { - var localSettingsJson = Path.Combine(workingDir, "local.settings.json"); - File.Delete(localSettingsJson); - }, - Commands = new[] - { - $"start {runtimeParameter} --port {_funcHostPort}", - }, - ExpectExit = false, - OutputContains = new[] - { - expectedOutput - }, - Test = async (_, p,_) => - { - if (invokeFunction) - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}/") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTriggerFunc?name=Test"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await Task.Delay(TimeSpan.FromSeconds(2)); - p.Kill(); - } - } - else - { - await Task.Delay(TimeSpan.FromSeconds(2)); - p.Kill(); - } - - } - } - }, _output); - } - finally - { - Environment.SetEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME", null); - } - } - - [Fact] - [Trait(TestTraits.Group, TestTraits.UseInConsolidatedArtifactGeneration)] - public async Task Start_InProc6_SpecifiedRuntime_Show_Migration_Warning() - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet --target-framework net6.0", - "new --template Httptrigger --name HttpTrigger", - }, - CommandTimeout = TimeSpan.FromSeconds(300) - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --runtime inproc6" - }, - ExpectExit = false, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTrigger?name=Test"); - var result = await response.Content.ReadAsStringAsync(); - p.Kill(); - - if (_output is Xunit.Sdk.TestOutputHelper testOutputHelper) - { - testOutputHelper.Output.Should().Contain($".NET 6 is no longer supported. Please consider migrating to a supported version. For more information, see https://aka.ms/azure-functions/dotnet/net8-in-process. If you intend to target .NET 8 on the in-process model, make sure that '{Constants.InProcDotNet8EnabledSetting}' is set to '1' in {Constants.LocalSettingsJsonFileName}."); - } - } - }, - CommandTimeout = TimeSpan.FromSeconds(100), - }, - }, _output); - } - - private async Task WaitUntilReady(HttpClient client) - { - for (var limit = 0; limit < 10; limit++) - { - try - { - var response = await client.GetAsync("/admin/host/ping"); - if (response.IsSuccessStatusCode) - { - return true; - } - await Task.Delay(1000); - } - catch - { - await Task.Delay(1000); - } - } - return false; - } - - private void SetUserSecrets(string workingDir, Dictionary userSecrets) - { - // init and set user secrets - string procOutput; - Process proc = new Process() - { - StartInfo = new ProcessStartInfo() - { - WindowStyle = ProcessWindowStyle.Hidden, - RedirectStandardOutput = true, - FileName = "cmd.exe", - Arguments = "/C dotnet user-secrets init", - WorkingDirectory = workingDir - } - }; - proc.Start(); - procOutput = proc.StandardOutput.ReadToEnd(); - proc.WaitForExit(); - _output.WriteLine(procOutput); - - foreach (KeyValuePair pair in userSecrets) - { - proc.StartInfo.Arguments = $"/C dotnet user-secrets set \"{pair.Key}\" \"{pair.Value}\""; - proc.Start(); - procOutput = proc.StandardOutput.ReadToEnd(); - proc.WaitForExit(); - _output.WriteLine(procOutput); - } - } - - public async Task DisposeAsync() - { - ProcessHelper.TryKillProcessForPort(_funcHostPort); - await Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests_artifact_consolidation.runsettings b/test/Azure.Functions.Cli.Tests/E2E/StartTests_artifact_consolidation.runsettings deleted file mode 100644 index 256cfd64e..000000000 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests_artifact_consolidation.runsettings +++ /dev/null @@ -1,6 +0,0 @@ - - - - (Group = RequiresNestedInProcArtifacts | Group = UseInConsolidatedArtifactGeneration) - - \ No newline at end of file diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests_default.runsettings b/test/Azure.Functions.Cli.Tests/E2E/StartTests_default.runsettings deleted file mode 100644 index 36bea3ac3..000000000 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests_default.runsettings +++ /dev/null @@ -1,6 +0,0 @@ - - - - (Group != RequiresNestedInProcArtifacts) & (Group != UseInVisualStudioConsolidatedArtifactGeneration) & (Group != UseInConsolidatedArtifactGeneration) - - \ No newline at end of file diff --git a/test/Cli/Func.E2E.Tests/.runsettings/start_tests/artifact_consolidation_pipeline/default.runsettings b/test/Cli/Func.E2E.Tests/.runsettings/start_tests/artifact_consolidation_pipeline/default.runsettings new file mode 100644 index 000000000..414e75401 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/.runsettings/start_tests/artifact_consolidation_pipeline/default.runsettings @@ -0,0 +1,6 @@ + + + + Group = UseInConsolidatedArtifactGeneration + + \ No newline at end of file diff --git a/test/Cli/Func.E2E.Tests/.runsettings/start_tests/artifact_consolidation_pipeline/dotnet_inproc.runsettings b/test/Cli/Func.E2E.Tests/.runsettings/start_tests/artifact_consolidation_pipeline/dotnet_inproc.runsettings new file mode 100644 index 000000000..d8c797b19 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/.runsettings/start_tests/artifact_consolidation_pipeline/dotnet_inproc.runsettings @@ -0,0 +1,6 @@ + + + + Group = RequiresNestedInProcArtifacts + + \ No newline at end of file diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests_artifact_consolidation_visualstudio.runsettings b/test/Cli/Func.E2E.Tests/.runsettings/start_tests/artifact_consolidation_pipeline/visualstudio.runsettings similarity index 100% rename from test/Azure.Functions.Cli.Tests/E2E/StartTests_artifact_consolidation_visualstudio.runsettings rename to test/Cli/Func.E2E.Tests/.runsettings/start_tests/artifact_consolidation_pipeline/visualstudio.runsettings diff --git a/test/Cli/Func.E2E.Tests/.runsettings/start_tests/ci_pipeline/default.runsettings b/test/Cli/Func.E2E.Tests/.runsettings/start_tests/ci_pipeline/default.runsettings new file mode 100644 index 000000000..bd95a4759 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/.runsettings/start_tests/ci_pipeline/default.runsettings @@ -0,0 +1,6 @@ + + + + (Group != InProc) & (Group != RequiresNestedInProcArtifacts) & (Group != UseInVisualStudioConsolidatedArtifactGeneration) & (Group != UseInConsolidatedArtifactGeneration) + + \ No newline at end of file diff --git a/test/Cli/Func.E2E.Tests/.runsettings/start_tests/ci_pipeline/dotnet_inproc.runsettings b/test/Cli/Func.E2E.Tests/.runsettings/start_tests/ci_pipeline/dotnet_inproc.runsettings new file mode 100644 index 000000000..8924efa30 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/.runsettings/start_tests/ci_pipeline/dotnet_inproc.runsettings @@ -0,0 +1,6 @@ + + + + (Group = InProc) & (Group != RequiresNestedInProcArtifacts) & (Group != UseInVisualStudioConsolidatedArtifactGeneration) & (Group != UseInConsolidatedArtifactGeneration) + + \ No newline at end of file diff --git a/test/Cli/Func.E2E.Tests/Azure.Functions.Cli.E2E.Tests.csproj b/test/Cli/Func.E2E.Tests/Azure.Functions.Cli.E2E.Tests.csproj new file mode 100644 index 000000000..bef98d123 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Azure.Functions.Cli.E2E.Tests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + true + + + + + + + + + + + + + + + + + + + + + + + + + + $(MSBuildProjectDirectory)\.runsettings\start_tests\ci_pipeline\default.runsettings + + + \ No newline at end of file diff --git a/test/Cli/Func.E2E.Tests/BaseE2ETests.cs b/test/Cli/Func.E2E.Tests/BaseE2ETests.cs new file mode 100644 index 000000000..8401e48c8 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/BaseE2ETests.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Runtime.InteropServices; +using Azure.Functions.Cli.TestFramework.Helpers; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests +{ + public abstract class BaseE2ETests(ITestOutputHelper log) : IAsyncLifetime + { + protected ITestOutputHelper Log { get; } = log; + + protected string FuncPath { get; set; } = Environment.GetEnvironmentVariable(Constants.FuncPath) ?? string.Empty; + + protected string WorkingDirectory { get; set; } = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + public Task InitializeAsync() + { + if (string.IsNullOrEmpty(FuncPath)) + { + // Fallback for local testing in Visual Studio, etc. + FuncPath = Path.Combine(Environment.CurrentDirectory, "func"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + FuncPath += ".exe"; + } + + if (!File.Exists(FuncPath)) + { + throw new ApplicationException("Could not locate the 'func' executable to use for testing. Make sure the FUNC_PATH environment variable is set to the full path of the func executable."); + } + } + + Directory.CreateDirectory(WorkingDirectory); + return Task.CompletedTask; + } + + public Task DisposeAsync() + { + try + { + Directory.Delete(WorkingDirectory, true); + } + catch + { + // Cleanup failed but we shouldn't crash on this + } + + return Task.CompletedTask; + } + + public async Task FuncInitWithRetryAsync(string testName, IEnumerable args) + { + await FunctionAppSetupHelper.FuncInitWithRetryAsync(FuncPath, testName, WorkingDirectory, Log, args); + } + + public async Task FuncNewWithRetryAsync(string testName, IEnumerable args, string? workerRuntime = null) + { + await FunctionAppSetupHelper.FuncNewWithRetryAsync(FuncPath, testName, WorkingDirectory, Log, args, workerRuntime); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/AuthTests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/AuthTests.cs new file mode 100644 index 000000000..fd9eded71 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/AuthTests.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart +{ + public class AuthTests(ITestOutputHelper log) : BaseE2ETests(log) + { + [Theory] + [InlineData("function", false, "Welcome to Azure Functions!")] + [InlineData("function", true, "")] + [InlineData("anonymous", true, "Welcome to Azure Functions!")] + public async Task Start_DotnetIsolated_EnableAuthFeature( + string authLevel, + bool enableAuth, + string expectedResult) + { + var port = ProcessHelper.GetAvailablePort(); + + var methodName = nameof(Start_DotnetIsolated_EnableAuthFeature); + var uniqueTestName = $"{methodName}_{authLevel}_{enableAuth}"; + + // Call func init and func new + await FuncInitWithRetryAsync(uniqueTestName, [".", "--worker-runtime", "dotnet-isolated"]); + await FuncNewWithRetryAsync(uniqueTestName, [".", "--template", "Httptrigger", "--name", "HttpTrigger", "--authlevel", authLevel]); + + // Call func start + var funcStartCommand = new FuncStartCommand(FuncPath, methodName, Log); + funcStartCommand.ProcessStartedHandler = async (process) => + { + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand)), "HttpTrigger"); + }; + + // Build command arguments based on enableAuth parameter + var commandArgs = new List { "start", "--verbose", "--port", port.ToString() }; + if (enableAuth) + { + commandArgs.Add("--enableAuth"); + } + + var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .Execute([.. commandArgs]); + + // Validate expected output content + if (string.IsNullOrEmpty(expectedResult)) + { + result.Should().HaveStdOutContaining("\"status\": \"401\""); + } + else + { + result.Should().StartOutOfProcessHost(); + } + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/Core/BaseLogLevelTests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/Core/BaseLogLevelTests.cs new file mode 100644 index 000000000..23ed1d0d0 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/Core/BaseLogLevelTests.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.Core +{ + public class BaseLogLevelTests(ITestOutputHelper log) : BaseE2ETests(log) + { + public async Task RunLogLevelOverridenViaHostJsonTest(string language, string testName) + { + int port = ProcessHelper.GetAvailablePort(); + + // Initialize function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", language]); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "HttpTrigger", "--name", "HttpTriggerCSharp"]); + + // Modify host.json to set log level to Debug + string hostJsonPath = Path.Combine(WorkingDirectory, "host.json"); + string hostJsonContent = "{\"version\": \"2.0\",\"logging\": {\"logLevel\": {\"Default\": \"Debug\"}}}"; + File.WriteAllText(hostJsonPath, hostJsonContent); + + var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log))); + + funcStartCommand.ProcessStartedHandler = async (process) => + { + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter))); + }; + + var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .Execute(["start", "--port", port.ToString()]); + + // Validate host configuration was applied + result.Should().HaveStdOutContaining("Host configuration applied."); + } + + public async Task RunLogLevelOverridenWithFilterTest(string language, string testName) + { + int port = ProcessHelper.GetAvailablePort(); + + // Initialize function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", language]); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "HttpTrigger", "--name", "HttpTriggerCSharp"]); + + // Modify host.json to set log level with filter + string hostJsonPath = Path.Combine(WorkingDirectory, "host.json"); + string hostJsonContent = "{\"version\": \"2.0\",\"logging\": {\"logLevel\": {\"Default\": \"None\", \"Host.Startup\": \"Information\"}}}"; + File.WriteAllText(hostJsonPath, hostJsonContent); + + // Call func start + var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log))); + + funcStartCommand.ProcessStartedHandler = async (process) => + { + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), shouldDelayForLogs: true); + }; + + var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .Execute(["--port", port.ToString()]); + + // Validate we see some logs but not others due to filters + result.Should().HaveStdOutContaining("Found the following functions:"); + result.Should().NotHaveStdOutContaining("Reading host configuration file"); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/Core/BaseMissingConfigTests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/Core/BaseMissingConfigTests.cs new file mode 100644 index 000000000..71809d2c0 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/Core/BaseMissingConfigTests.cs @@ -0,0 +1,130 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.Core +{ + public class BaseMissingConfigTests(ITestOutputHelper log) : BaseE2ETests(log) + { + public async Task RunInvalidHostJsonTest(string language, string testName) + { + int port = ProcessHelper.GetAvailablePort(); + + // Initialize function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", language]); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "HttpTrigger", "--name", "HttpTriggerCSharp"]); + + // Create invalid host.json + var hostJsonPath = Path.Combine(WorkingDirectory, "host.json"); + var hostJsonContent = "{ \"version\": \"2.0\", \"extensionBundle\": { \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\", \"version\": \"[2.*, 3.0.0)\" }}"; + File.WriteAllText(hostJsonPath, hostJsonContent); + + // Call func start + var result = new FuncStartCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log))) + .WithWorkingDirectory(WorkingDirectory) + .Execute(["--port", port.ToString()]); + + // Validate error message + result.Should().HaveStdOutContaining("Extension bundle configuration should not be present"); + } + + public async Task RunMissingHostJsonTest(string language, string testName) + { + int port = ProcessHelper.GetAvailablePort(); + + // Initialize function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", language]); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "HttpTrigger", "--name", "HttpTriggerCSharp"]); + + // Delete host.json + var hostJsonPath = Path.Combine(WorkingDirectory, "host.json"); + File.Delete(hostJsonPath); + + // Call func start + var result = new FuncStartCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log))) + .WithWorkingDirectory(WorkingDirectory) + .Execute(["--port", port.ToString()]); + + // Validate error message + result.Should().HaveStdOutContaining("Host.json file in missing"); + } + + public async Task RunMissingLocalSettingsJsonTest(string language, string runtimeParameter, string expectedOutput, bool invokeFunction, bool setRuntimeViaEnvironment, string testName) + { + try + { + var logFileName = $"{testName}_{language}_{runtimeParameter}"; + + var port = ProcessHelper.GetAvailablePort(); + + // Initialize function app using retry helper + await FuncInitWithRetryAsync(logFileName, [".", "--worker-runtime", language]); + + var funcNewArgs = new[] { ".", "--template", "HttpTrigger", "--name", "HttpTriggerFunc" } + .Concat(!language.Contains("dotnet") ? ["--language", language] : Array.Empty()) + .ToArray(); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(logFileName, funcNewArgs); + + // Delete local.settings.json + var localSettingsJson = Path.Combine(WorkingDirectory, "local.settings.json"); + File.Delete(localSettingsJson); + + // Call func start + var funcStartCommand = new FuncStartCommand(FuncPath, logFileName, Log ?? throw new ArgumentNullException(nameof(Log))); + + funcStartCommand.ProcessStartedHandler = async (process) => + { + // Wait for host to start up if param is set, otherwise just wait 10 seconds for logs and kill the process + if (invokeFunction) + { + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTriggerFunc"); + } + else + { + await Task.Delay(10000); + process.Kill(true); + } + }; + + var startCommand = new List { "--port", port.ToString(), "--verbose" }; + if (!string.IsNullOrEmpty(runtimeParameter)) + { + startCommand.Add(runtimeParameter); + } + + // Configure the command execution + var commandSetup = funcStartCommand + .WithWorkingDirectory(WorkingDirectory); + + // Conditionally set environment variable only if required + if (setRuntimeViaEnvironment) + { + commandSetup = commandSetup.WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, language); + } + + var result = commandSetup.Execute(startCommand); + + result.Should().HaveStdOutContaining(expectedOutput); + } + finally + { + // Clean up environment variable + if (setRuntimeViaEnvironment) + { + Environment.SetEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME", null); + } + } + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/Core/BaseUserSecretsTests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/Core/BaseUserSecretsTests.cs new file mode 100644 index 000000000..0f8201b71 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/Core/BaseUserSecretsTests.cs @@ -0,0 +1,358 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Diagnostics; +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.Core +{ + public class BaseUserSecretsTests(ITestOutputHelper log) : BaseE2ETests(log) + { + public async Task RunUserSecretsTest(string language, string testName) + { + var port = ProcessHelper.GetAvailablePort(); + + // Initialize dotnet function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", language]); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "Httptrigger", "--name", "http1"]); + + // Add Queue trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "QueueTrigger", "--name", "queue1"]); + + // Modify queue code to use connection string + var queueCodePath = Path.Combine(WorkingDirectory, "queue1.cs"); + var queueCode = File.ReadAllText(queueCodePath); + queueCode = queueCode.Replace("Connection = \"\"", "Connection = \"ConnectionStrings:MyQueueConn\""); + File.WriteAllText(queueCodePath, queueCode); + + // Clear local.settings.json + var settingsPath = Path.Combine(WorkingDirectory, "local.settings.json"); + var settingsContent = "{ \"IsEncrypted\": false, \"Values\": { \"FUNCTIONS_WORKER_RUNTIME\": \"" + language + "\", \"AzureWebJobsSecretStorageType\": \"files\"} }"; + File.WriteAllText(settingsPath, settingsContent); + + // Set up user secrets + var userSecrets = new Dictionary + { + { "AzureWebJobsStorage", "UseDevelopmentStorage=true" }, + { "ConnectionStrings:MyQueueConn", "UseDevelopmentStorage=true" } + }; + + SetupUserSecrets(userSecrets); + + // Call func start + var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log))); + + funcStartCommand.ProcessStartedHandler = async (process) => + { + try + { + await ProcessHelper.WaitForFunctionHostToStart(process, port, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter))); + + // Insert message into queue + await QueueStorageHelper.InsertIntoQueue("myqueue-items", "hello world"); + } + finally + { + process.Kill(true); + } + }; + + var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .Execute(["start", "--build", "--port", port.ToString()]); + + // Validate user secrets are used + result.Should().HaveStdOutContaining("Using for user secrets file configuration."); + } + + public async Task RunMissingStorageConnString_FailsWithExpectedError(string languageWorker, string testName) + { + var azureWebJobsStorage = Environment.GetEnvironmentVariable("AzureWebJobsStorage"); + if (!string.IsNullOrEmpty(azureWebJobsStorage)) + { + Log?.WriteLine("Skipping test as AzureWebJobsStorage is set"); + return; + } + + int port = ProcessHelper.GetAvailablePort(); + + // Initialize dotnet function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", languageWorker]); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "Httptrigger", "--name", "http1"]); + + // Add Queue trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "QueueTrigger", "--name", "queue1"]); + + // Modify queue code to use connection string + var queueCodePath = Path.Combine(WorkingDirectory, "queue1.cs"); + var queueCode = File.ReadAllText(queueCodePath); + queueCode = queueCode.Replace("Connection = \"\"", "Connection = \"ConnectionStrings:MyQueueConn\""); + File.WriteAllText(queueCodePath, queueCode); + + // Clear local.settings.json + var settingsPath = Path.Combine(WorkingDirectory, "local.settings.json"); + var settingsContent = "{ \"IsEncrypted\": false, \"Values\": { \"FUNCTIONS_WORKER_RUNTIME\": \"" + languageWorker + "\"} }"; + File.WriteAllText(settingsPath, settingsContent); + + // Set up user secrets with missing AzureWebJobsStorage + var userSecrets = new Dictionary + { + { "ConnectionStrings:MyQueueConn", "UseDevelopmentStorage=true" } + }; + + SetupUserSecrets(userSecrets); + + // Call func start for HTTP function only + var result = new FuncStartCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log))) + .WithWorkingDirectory(WorkingDirectory) + .Execute(["start", "--functions", "http1", "--port", port.ToString()]); + + // Validate error message + result.Should().HaveStdOutContaining("Missing value for AzureWebJobsStorage in local.settings.json"); + result.Should().HaveStdOutContaining("A host error has occurred during startup operation"); + } + + public async Task RunWithUserSecrets_MissingBindingSetting_FailsWithExpectedError(string languageWorker, string testName) + { + var azureWebJobsStorage = Environment.GetEnvironmentVariable("AzureWebJobsStorage"); + if (!string.IsNullOrEmpty(azureWebJobsStorage)) + { + Log?.WriteLine("Skipping test as AzureWebJobsStorage is set"); + return; + } + + var port = ProcessHelper.GetAvailablePort(); + + // Initialize dotnet function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", languageWorker]); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "Httptrigger", "--name", "http1"]); + + // Add Queue trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "QueueTrigger", "--name", "queue1"]); + + // Modify queue code to use connection string + var queueCodePath = Path.Combine(WorkingDirectory, "queue1.cs"); + var queueCode = File.ReadAllText(queueCodePath); + queueCode = queueCode.Replace("Connection = \"\"", "Connection = \"ConnectionStrings:MyQueueConn\""); + File.WriteAllText(queueCodePath, queueCode); + + // Clear local.settings.json + var settingsPath = Path.Combine(WorkingDirectory, "local.settings.json"); + var settingsContent = "{ \"IsEncrypted\": false, \"Values\": { \"FUNCTIONS_WORKER_RUNTIME\": \"" + languageWorker + "\"} }"; + File.WriteAllText(settingsPath, settingsContent); + + // Set up user secrets with AzureWebJobsStorage but missing MyQueueConn + var userSecrets = new Dictionary + { + { "AzureWebJobsStorage", "UseDevelopmentStorage=true" } + }; + + SetupUserSecrets(userSecrets); + + // Call func start + var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log))); + + funcStartCommand.ProcessStartedHandler = async (process) => + { + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter))); + }; + + var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .Execute(["--port", port.ToString()]); + + // Validate warning message about missing connection string + result.Should().HaveStdOutContaining("Warning: Cannot find value named 'ConnectionStrings:MyQueueConn' in local.settings.json"); + result.Should().HaveStdOutContaining("You can run 'func azure functionapp fetch-app-settings ' or specify a connection string in local.settings.json."); + } + + private void SetupUserSecrets(Dictionary secrets) + { + // Initialize user secrets + var initProcess = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "user-secrets init", + WorkingDirectory = WorkingDirectory, + RedirectStandardOutput = true, + UseShellExecute = false + }; + + using (var process = Process.Start(initProcess)) + { + process?.WaitForExit(); + } + + // Set each secret + foreach (var secret in secrets) + { + var setProcess = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"user-secrets set \"{secret.Key}\" \"{secret.Value}\"", + WorkingDirectory = WorkingDirectory, + RedirectStandardOutput = true, + UseShellExecute = false + }; + + using var process = Process.Start(setProcess); + process?.WaitForExit(); + } + } + + [Theory] + [InlineData("dotnet-isolated", "--dotnet-isolated", true, false)] + [InlineData("node", "--node", true, false)] + [InlineData("dotnet-isolated", "", true, true)] + public async Task Start_MissingLocalSettingsJson_BehavesAsExpected(string language, string runtimeParameter, bool invokeFunction, bool setRuntimeViaEnvironment) + { + try + { + var methodName = nameof(Start_MissingLocalSettingsJson_BehavesAsExpected); + var logFileName = $"{methodName}_{language}_{runtimeParameter}"; + if (setRuntimeViaEnvironment) + { + Environment.SetEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet-isolated"); + } + + var port = ProcessHelper.GetAvailablePort(); + + // Initialize function app using retry helper + await FuncInitWithRetryAsync(logFileName, [".", "--worker-runtime", language]); + + var funcNewArgs = new[] { ".", "--template", "HttpTrigger", "--name", "HttpTriggerFunc" } + .Concat(!language.Contains("dotnet") ? ["--language", language] : Array.Empty()) + .ToArray(); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(logFileName, funcNewArgs); + + // Delete local.settings.json + var localSettingsJson = Path.Combine(WorkingDirectory, "local.settings.json"); + File.Delete(localSettingsJson); + + // Call func start + var funcStartCommand = new FuncStartCommand(FuncPath, logFileName, Log ?? throw new ArgumentNullException(nameof(Log))); + + funcStartCommand.ProcessStartedHandler = async (process) => + { + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTriggerFunc"); + }; + + var startCommand = new List { "--port", port.ToString(), "--verbose" }; + if (!string.IsNullOrEmpty(runtimeParameter)) + { + startCommand.Add(runtimeParameter); + } + + var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .Execute([.. startCommand]); + + // Validate output contains expected function URL + if (invokeFunction) + { + result.Should().HaveStdOutContaining("HttpTriggerFunc: [GET,POST] http://localhost:"); + } + + result.Should().HaveStdOutContaining("Executed 'Functions.HttpTriggerFunc' (Succeeded"); + } + finally + { + // Clean up environment variable + if (setRuntimeViaEnvironment) + { + Environment.SetEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, null); + } + } + } + + [Fact] + public async Task Start_LanguageWorker_InvalidFunctionJson_FailsWithExpectedError() + { + var port = ProcessHelper.GetAvailablePort(); + var functionName = "HttpTriggerJS"; + var testName = nameof(Start_LanguageWorker_InvalidFunctionJson_FailsWithExpectedError); + + // Initialize Node.js function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", "node", "-m", "v3"]); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "Httptrigger", "--name", functionName, "--language", "node"], workerRuntime: "node"); + + // Modify function.json to include an invalid binding type + var filePath = Path.Combine(WorkingDirectory, functionName, "function.json"); + var functionJson = await File.ReadAllTextAsync(filePath); + functionJson = functionJson.Replace("\"type\": \"http\"", "\"type\": \"http2\""); + await File.WriteAllTextAsync(filePath, functionJson); + + // Call func start + var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log))); + + funcStartCommand.ProcessStartedHandler = async (process) => + { + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter))); + }; + + var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "node") + .Execute(["--port", port.ToString(), "--verbose"]); + + // Validate error message + result.Should().HaveStdOutContaining("The binding type(s) 'http2' were not found in the configured extension bundle. Please ensure the type is correct and the correct version of extension bundle is configured."); + } + + [Fact] + public async Task Start_EmptyEnvVars_HandledAsExpected() + { + var port = ProcessHelper.GetAvailablePort(); + var testName = nameof(Start_EmptyEnvVars_HandledAsExpected); + + // Initialize Node.js function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", "node", "-m", "v4"]); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "Httptrigger", "--name", "HttpTrigger", "--language", "node"], workerRuntime: "node"); + + // Add empty setting + var funcSettingsResult = new FuncSettingsCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log))) + .WithWorkingDirectory(WorkingDirectory) + .Execute(["add", "emptySetting", "EMPTY_VALUE"]); + funcSettingsResult.Should().ExitWith(0); + + // Modify settings file to have empty value + var settingsPath = Path.Combine(WorkingDirectory, "local.settings.json"); + var settingsContent = File.ReadAllText(settingsPath); + settingsContent = settingsContent.Replace("EMPTY_VALUE", string.Empty); + File.WriteAllText(settingsPath, settingsContent); + + // Call func start + var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log))); + + funcStartCommand.ProcessStartedHandler = async (process) => + { + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter))); + }; + + var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "node") + .Execute(["--port", port.ToString(), "--verbose"]); + + // Validate function works and doesn't show skipping message + result.Should().NotHaveStdOutContaining("Skipping 'emptySetting' from local settings as it's already defined in current environment variables."); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/InProcTestWithoutTargetFramework.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/InProcTestWithoutTargetFramework.cs new file mode 100644 index 000000000..65247f7af --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/InProcTestWithoutTargetFramework.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.E2E.Tests.Traits; +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.InProcTests +{ + public class InProcTestWithoutTargetFramework(ITestOutputHelper log) : BaseE2ETests(log) + { + [Fact] + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] + public async Task Start_InProc_SuccessfulFunctionExecution() + { + var port = ProcessHelper.GetAvailablePort(); + var testName = nameof(Start_InProc_SuccessfulFunctionExecution); + + // Initialize dotnet function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", "dotnet"]); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "HttpTrigger", "--name", "HttpTrigger"]); + + // Call func start + var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log ?? throw new ArgumentNullException(nameof(Log))); + string? capturedContent = null; + + funcStartCommand.ProcessStartedHandler = async (process) => + { + capturedContent = await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger?name=Test"); + }; + + var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .Execute(["--port", port.ToString()]); + + capturedContent.Should().Be("Hello, Test. This HTTP triggered function executed successfully."); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/LogLevelTests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/LogLevelTests.cs new file mode 100644 index 000000000..8758c03ad --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/LogLevelTests.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.Core; +using Azure.Functions.Cli.E2E.Tests.Traits; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.InProcTests +{ + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] + public class LogLevelTests(ITestOutputHelper log) : BaseLogLevelTests(log) + { + [Fact] + public async Task Start_InProc_LogLevelOverridenViaHostJson_LogLevelSetToExpectedValue() + { + await RunLogLevelOverridenViaHostJsonTest("dotnet-isolated", nameof(Start_InProc_LogLevelOverridenViaHostJson_LogLevelSetToExpectedValue)); + } + + [Fact] + public async Task Start_InProc_LogLevelOverridenWithFilter_LogLevelSetToExpectedValue() + { + await RunLogLevelOverridenWithFilterTest("dotnet-isolated", nameof(Start_InProc_LogLevelOverridenWithFilter_LogLevelSetToExpectedValue)); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/MissingConfigTests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/MissingConfigTests.cs new file mode 100644 index 000000000..f42ef3781 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/MissingConfigTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.Core; +using Azure.Functions.Cli.E2E.Tests.Traits; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.InProcTests +{ + public class MissingConfigTests(ITestOutputHelper log) : BaseMissingConfigTests(log) + { + [Fact] + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] + public async Task Start_InProc_InvalidHostJson_FailsWithExpectedError() + { + await RunInvalidHostJsonTest("dotnet", nameof(Start_InProc_InvalidHostJson_FailsWithExpectedError)); + } + + [Fact] + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] + public async Task Start_InProc_MissingHostJson_FailsWithExpectedError() + { + await RunMissingHostJsonTest("dotnet", nameof(Start_InProc_MissingHostJson_FailsWithExpectedError)); + } + + [Theory] + [Trait(TestTraits.Group, TestTraits.InProc)] + [InlineData("dotnet", "--worker-runtime None", "Use the up/down arrow keys to select a worker runtime:", false, false)] // Runtime parameter set to None, worker runtime prompt displayed + [InlineData("dotnet", "", $"Use the up/down arrow keys to select a worker runtime:", false, false)] // Runtime parameter not provided, worker runtime prompt displayed + public async Task Start_InProc_MissingLocalSettingsJson_BehavesAsExpected(string language, string runtimeParameter, string expectedOutput, bool invokeFunction, bool setRuntimeViaEnvironment) + { + await RunMissingLocalSettingsJsonTest(language, runtimeParameter, expectedOutput, invokeFunction, setRuntimeViaEnvironment, nameof(Start_InProc_MissingLocalSettingsJson_BehavesAsExpected)); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/UserSecretsTests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/UserSecretsTests.cs new file mode 100644 index 000000000..f1c81e31b --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/UserSecretsTests.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.Core; +using Azure.Functions.Cli.E2E.Tests.Traits; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.InProcTests +{ + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] + public class UserSecretsTests(ITestOutputHelper log) : BaseUserSecretsTests(log) + { + [Fact] + public async Task Start_InProc_WithUserSecrets_SuccessfulFunctionExecution() + { + await RunUserSecretsTest("dotnet", nameof(Start_InProc_WithUserSecrets_SuccessfulFunctionExecution)); + } + + [Fact] + public async Task Start_InProc_WithUserSecrets_MissingStorageConnString_FailsWithExpectedError() + { + await RunMissingStorageConnString_FailsWithExpectedError("dotnet", nameof(Start_InProc_WithUserSecrets_MissingStorageConnString_FailsWithExpectedError)); + } + + [Fact] + public async Task Start_InProc_WithUserSecrets_MissingBindingSetting_FailsWithExpectedError() + { + await RunWithUserSecrets_MissingBindingSetting_FailsWithExpectedError("dotnet", nameof(Start_InProc_WithUserSecrets_MissingBindingSetting_FailsWithExpectedError)); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/VisualStudioInProcTests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/VisualStudioInProcTests.cs new file mode 100644 index 000000000..07abcf584 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/InProcTests/VisualStudioInProcTests.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.E2E.Tests.Traits; +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.InProcTests +{ + [Trait(TestTraits.Group, TestTraits.UseInVisualStudioConsolidatedArtifactGeneration)] + public class VisualStudioInProcTests(ITestOutputHelper log) : BaseE2ETests(log) + { + private readonly string _vsNet8ProjectPath = Environment.GetEnvironmentVariable(Constants.VisualStudioNet8ProjectPath) ?? Path.Combine("..", "..", "..", "..", "..", "TestFunctionApps", "VisualStudioTestProjects", "TestNet8InProcProject"); + private readonly string _vsNet6ProjectPath = Environment.GetEnvironmentVariable(Constants.VisualStudioNet6ProjectPath) ?? Path.Combine("..", "..", "..", "..", "..", "TestFunctionApps", "VisualStudioTestProjects", "TestNet6InProcProject"); + + [Fact] + public void Start_InProc_Net8_VisualStudio_SuccessfulFunctionExecution() + { + var port = ProcessHelper.GetAvailablePort(); + var testName = nameof(Start_InProc_Net8_VisualStudio_SuccessfulFunctionExecution); + + // Call func start (on existing VS project) + var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log); + string? capturedOutput = null; + + funcStartCommand.ProcessStartedHandler = async (process) => + { + capturedOutput = await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "Dotnet8InProc?name=Test"); + }; + + var result = funcStartCommand + .WithWorkingDirectory(_vsNet8ProjectPath) + .Execute(["--verbose", "--port", port.ToString()]); + capturedOutput.Should().Be("Hello, Test. This HTTP triggered function executed successfully."); + + // Validate .NET 8 host was loaded + result.Should().LoadNet8HostVisualStudio(); + } + + [Fact] + public void Start_InProc_Net6_VisualStudio_SuccessfulFunctionExecution() + { + var port = ProcessHelper.GetAvailablePort(); + var testName = nameof(Start_InProc_Net6_VisualStudio_SuccessfulFunctionExecution); + + // Call func start (on existing VS project) + var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log); + string? capturedOutput = null; + + funcStartCommand.ProcessStartedHandler = async (process) => + { + capturedOutput = await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "Dotnet6InProc?name=Test"); + }; + + var result = funcStartCommand + .WithWorkingDirectory(_vsNet6ProjectPath) + .Execute(["--verbose", "--port", port.ToString()]); + capturedOutput.Should().Be("Hello, Test. This HTTP triggered function executed successfully."); + + // Validate .NET 6 host was loaded + result.Should().LoadNet6HostVisualStudio(); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/LogLevelTests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/LogLevelTests.cs new file mode 100644 index 000000000..aba87dd4f --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/LogLevelTests.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.Core; +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart +{ + public class LogLevelTests(ITestOutputHelper log) : BaseLogLevelTests(log) + { + [Fact] + public async Task Start_Dotnet_Isolated_LogLevelOverridenViaHostJson_LogLevelSetToExpectedValue() + { + await RunLogLevelOverridenViaHostJsonTest("dotnet-isolated", nameof(Start_Dotnet_Isolated_LogLevelOverridenViaHostJson_LogLevelSetToExpectedValue)); + } + + [Fact] + public async Task Start_Dotnet_Isolated_LogLevelOverridenWithFilter_LogLevelSetToExpectedValue() + { + await RunLogLevelOverridenWithFilterTest("dotnet-isolated", nameof(Start_Dotnet_Isolated_LogLevelOverridenWithFilter_LogLevelSetToExpectedValue)); + } + + [Fact] + public async Task Start_LanguageWorker_LogLevelOverridenViaSettings_LogLevelSetToExpectedValue() + { + var port = ProcessHelper.GetAvailablePort(); + var testName = nameof(Start_LanguageWorker_LogLevelOverridenViaSettings_LogLevelSetToExpectedValue); + + // Initialize Node.js function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", "node", "-m", "v4"]); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "HttpTrigger", "--name", "HttpTrigger", "--language", "node"], workerRuntime: "node"); + + // Add debug log level setting + var funcSettingsResult = new FuncSettingsCommand(FuncPath, testName, Log) + .WithWorkingDirectory(WorkingDirectory) + .Execute(["add", "AzureFunctionsJobHost__logging__logLevel__Default", "Debug"]); + funcSettingsResult.Should().ExitWith(0); + + // Call func start + var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log); + funcStartCommand.ProcessStartedHandler = async (process) => + { + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger?name=Test"); + }; + var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "node") + .Execute(["--port", port.ToString(), "--verbose"]); + + // Validate we see detailed worker logs + result.Should().HaveStdOutContaining("Workers Directory set to"); + } + + [Fact] + public async Task Start_LanguageWorker_LogLevelOverridenViaHostJson_LogLevelSetToExpectedValue() + { + var port = ProcessHelper.GetAvailablePort(); + var testName = nameof(Start_LanguageWorker_LogLevelOverridenViaHostJson_LogLevelSetToExpectedValue); + + // Initialize Node.js function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", "node", "-m", "v4"]); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "HttpTrigger", "--name", "HttpTrigger", "--language", "node"], workerRuntime: "node"); + + // Modify host.json to set log level + var hostJsonPath = Path.Combine(WorkingDirectory, "host.json"); + var hostJsonContent = "{\"version\": \"2.0\",\"logging\": {\"logLevel\": {\"Default\": \"None\"}}}"; + File.WriteAllText(hostJsonPath, hostJsonContent); + + // Call func start + var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log); + funcStartCommand.ProcessStartedHandler = async (process) => + { + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger?name=Test"); + }; + var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "node") + .Execute(["--port", port.ToString()]); + + // Validate minimal worker logs due to "None" log level + result.Should().HaveStdOutContaining("Worker process started and initialized"); + result.Should().NotHaveStdOutContaining("Initializing function HTTP routes"); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/MissingConfigTests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/MissingConfigTests.cs new file mode 100644 index 000000000..6aa799eb3 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/MissingConfigTests.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.Core; +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart +{ + public class MissingConfigTests(ITestOutputHelper log) : BaseMissingConfigTests(log) + { + [Fact(Skip="Test fails and needs to be investiagted on why it does.")] + public async Task Start_DotnetIsolated_InvalidHostJson_FailsWithExpectedError() + { + await RunInvalidHostJsonTest("dotnet-isolated", nameof(Start_DotnetIsolated_InvalidHostJson_FailsWithExpectedError)); + } + + [Fact(Skip = "Test fails and needs to be investiagted on why it does.")] + public async Task Start_DotnetIsolated_MissingHostJson_FailsWithExpectedError() + { + await RunMissingHostJsonTest("dotnet-isolated", nameof(Start_DotnetIsolated_MissingHostJson_FailsWithExpectedError)); + } + + [Theory] + [InlineData("dotnet-isolated", "--dotnet-isolated", "HttpTriggerFunc: [GET,POST] http://localhost:", true, false)] // Runtime parameter set (dni), successful startup & invocation + [InlineData("node", "--node", "HttpTriggerFunc: [GET,POST] http://localhost:", true, false)] // Runtime parameter set (node), successful startup & invocation + [InlineData("dotnet-isolated", "", "HttpTriggerFunc: [GET,POST] http://localhost:", true, true)] // Runtime value is set via environment variable, successful startup & invocation + public async Task Start_MissingLocalSettingsJson_BehavesAsExpected(string language, string runtimeParameter, string expectedOutput, bool invokeFunction, bool setRuntimeViaEnvironment) + { + await RunMissingLocalSettingsJsonTest(language, runtimeParameter, expectedOutput, invokeFunction, setRuntimeViaEnvironment, nameof(Start_MissingLocalSettingsJson_BehavesAsExpected)); + } + + [Fact] + public async Task Start_LanguageWorker_InvalidFunctionJson_FailsWithExpectedError() + { + var port = ProcessHelper.GetAvailablePort(); + var functionName = "HttpTriggerJS"; + var testName = nameof(Start_LanguageWorker_InvalidFunctionJson_FailsWithExpectedError); + + // Initialize Node.js function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", "node", "-m", "v3"]); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "Httptrigger", "--name", functionName, "--language", "node"], workerRuntime: "node"); + + // Modify function.json to include an invalid binding type + var filePath = Path.Combine(WorkingDirectory, functionName, "function.json"); + var functionJson = await File.ReadAllTextAsync(filePath); + functionJson = functionJson.Replace("\"type\": \"http\"", "\"type\": \"http2\""); + await File.WriteAllTextAsync(filePath, functionJson); + + // Call func start + var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log); + + funcStartCommand.ProcessStartedHandler = async (process) => + { + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter))); + }; + + var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "node") + .Execute(["--port", port.ToString(), "--verbose"]); + + // Validate error message + result.Should().HaveStdOutContaining("The binding type(s) 'http2' were not found in the configured extension bundle. Please ensure the type is correct and the correct version of extension bundle is configured."); + } + + [Fact] + public async Task Start_EmptyEnvVars_HandledAsExpected() + { + var port = ProcessHelper.GetAvailablePort(); + var testName = nameof(Start_EmptyEnvVars_HandledAsExpected); + + // Initialize Node.js function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", "node", "-m", "v4"]); + + // Add HTTP trigger using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "Httptrigger", "--name", "HttpTrigger", "--language", "node"], workerRuntime: "node"); + + // Add empty setting + var funcSettingsResult = new FuncSettingsCommand(FuncPath, testName, Log) + .WithWorkingDirectory(WorkingDirectory) + .Execute(["add", "emptySetting", "EMPTY_VALUE"]); + funcSettingsResult.Should().ExitWith(0); + + // Modify settings file to have empty value + var settingsPath = Path.Combine(WorkingDirectory, "local.settings.json"); + var settingsContent = File.ReadAllText(settingsPath); + settingsContent = settingsContent.Replace("EMPTY_VALUE", string.Empty); + File.WriteAllText(settingsPath, settingsContent); + + // Call func start + var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log); + + funcStartCommand.ProcessStartedHandler = async (process) => + { + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter))); + }; + + var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "node") + .Execute(["--port", port.ToString(), "--verbose"]); + + // Validate function works and doesn't show skipping message + result.Should().NotHaveStdOutContaining("Skipping 'emptySetting' from local settings as it's already defined in current environment variables."); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/MultipleFunctionsTests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/MultipleFunctionsTests.cs new file mode 100644 index 000000000..25e8daca5 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/MultipleFunctionsTests.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Net; +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart +{ + public class MultipleFunctionsTests(ITestOutputHelper log) : BaseE2ETests(log) + { + [Fact] + public async Task Start_FunctionsStartArgument_OnlySelectedFunctionsRun() + { + var port = ProcessHelper.GetAvailablePort(); + var testName = nameof(Start_FunctionsStartArgument_OnlySelectedFunctionsRun); + + // Initialize JavaScript function app using retry helper + await FuncInitWithRetryAsync(testName, [".", "--worker-runtime", "javascript"]); + + // Add multiple HTTP triggers using retry helper + await FuncNewWithRetryAsync(testName, [".", "--template", "Httptrigger", "--name", "http1"]); + await FuncNewWithRetryAsync(testName, [".", "--template", "Httptrigger", "--name", "http2"]); + await FuncNewWithRetryAsync(testName, [".", "--template", "Httptrigger", "--name", "http3"]); + + // Call func start with specific functions + var funcStartCommand = new FuncStartCommand(FuncPath, testName, Log); + funcStartCommand.ProcessStartedHandler = async (process) => + { + try + { + await ProcessHelper.WaitForFunctionHostToStart(process, port, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter))); + + using var client = new HttpClient(); + + // http1 should be available + var response1 = await client.GetAsync($"http://localhost:{port}/api/http1?name=Test"); + response1.StatusCode.Should().Be(HttpStatusCode.OK); + + // http2 should be available + var response2 = await client.GetAsync($"http://localhost:{port}/api/http2?name=Test"); + response2.StatusCode.Should().Be(HttpStatusCode.OK); + + // http3 should not be available + var response3 = await client.GetAsync($"http://localhost:{port}/api/http3?name=Test"); + response3.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + finally + { + process.Kill(true); + } + }; + + funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .Execute(["--functions", "http2", "http1", "--port", port.ToString()]); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/DotnetInProc6Tests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/DotnetInProc6Tests.cs new file mode 100644 index 000000000..f8e32462b --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/DotnetInProc6Tests.cs @@ -0,0 +1,203 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.E2E.Tests.Fixtures; +using Azure.Functions.Cli.E2E.Tests.Traits; +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.TestsWithFixtures +{ + [Collection("DotnetInProc6")] + [Trait(TestTraits.Group, TestTraits.InProc)] + public class DotnetInProc6Tests : IClassFixture + { + private readonly Dotnet6InProcFunctionAppFixture _fixture; + + public DotnetInProc6Tests(Dotnet6InProcFunctionAppFixture fixture, ITestOutputHelper log) + { + _fixture = fixture; + _fixture.Log = log; + } + + [Fact] + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] + public void Start_InProc_Net6_WithSpecifyingRuntime_SuccessfulFunctionExecution() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_InProc_Net6_WithSpecifyingRuntime_SuccessfulFunctionExecution), _fixture.Log); + string? capturedContent = null; + + funcStartCommand.ProcessStartedHandler = async (process) => + { + capturedContent = await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger?name=Test"); + }; + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet") + .Execute(new[] { "--verbose", "--runtime", "inproc6", "--port", port.ToString() }); + + capturedContent.Should().Be("Hello, Test. This HTTP triggered function executed successfully."); + + // Validate inproc6 host was started + result.Should().StartInProc6Host(); + } + + [Fact] + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] + public void Start_InProc_Net6_SuccessfulFunctionExecution_WithoutSpecifyingRuntime() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_InProc_Net6_SuccessfulFunctionExecution_WithoutSpecifyingRuntime), _fixture.Log); + + string? capturedContent = null; + + funcStartCommand.ProcessStartedHandler = async (process) => + { + capturedContent = await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger?name=Test"); + }; + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet") + .Execute(new[] { "start", "--verbose", "--port", port.ToString() }); + + capturedContent.Should().Be("Hello, Test. This HTTP triggered function executed successfully."); + + // Validate inproc6 host was started + result.Should().StartInProc6Host(); + } + + [Fact] + public void Start_InProc_Dotnet6_WithoutSpecifyingRuntime_ExpectedToFail() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start (expected to fail) + var result = new FuncStartCommand(_fixture.FuncPath, nameof(Start_InProc_Dotnet6_WithoutSpecifyingRuntime_ExpectedToFail), _fixture.Log) + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet") + .Execute(new[] { "start", "--verbose", "--port", port.ToString() }); + + // Validate failure message + result.Should().ExitWith(1); + result.Should().HaveStdErrContaining("Failed to locate the inproc6 model host at"); + } + + [Fact] + public void Start_InProc_Dotnet6_WithSpecifyingRuntime_ExpectedToFail() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start (expected to fail) + var result = new FuncStartCommand(_fixture.FuncPath, nameof(Start_InProc_Dotnet6_WithSpecifyingRuntime_ExpectedToFail), _fixture.Log) + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet") + .Execute(new[] { "start", "--verbose", "--runtime", "inproc6", "--port", port.ToString() }); + + // Validate failure message + result.Should().ExitWith(1); + result.Should().HaveStdErrContaining("Failed to locate the inproc6 model host at"); + } + + [Fact] + public void Start_Dotnet6InProcApp_With_InProc8AsRuntime_ShouldFail() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start (expected to fail) + var result = new FuncStartCommand(_fixture.FuncPath, nameof(Start_Dotnet6InProcApp_With_InProc8AsRuntime_ShouldFail), _fixture.Log) + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet") + .Execute(new[] { "start", "--verbose", "--runtime", "inproc8", "--port", port.ToString() }); + + // Validate failure message + result.Should().ExitWith(1); + result.Should().HaveStdErrContaining("The runtime argument value provided, 'inproc8', is invalid. For the .NET 8 runtime on the in-process model, you must set the 'FUNCTIONS_INPROC_NET8_ENABLED' environment variable to '1'. For more information, see https://aka.ms/azure-functions/dotnet/net8-in-process."); + } + + [Fact] + public void Start_Dotnet6InProcApp_With_DefaultAsRuntime_ShouldFail() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start (expected to fail) + var result = new FuncStartCommand(_fixture.FuncPath, nameof(Start_Dotnet6InProcApp_With_DefaultAsRuntime_ShouldFail), _fixture.Log) + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet") + .Execute(new[] { "start", "--verbose", "--runtime", "default", "--port", port.ToString() }); + + // Validate failure message + result.Should().ExitWith(1); + result.Should().HaveStdErrContaining("The runtime argument value provided, 'default', is invalid. The provided value is only valid for the worker runtime 'dotnetIsolated'."); + } + + [Fact] + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] + public void Start_InProc_InvalidHostJson_FailsWithExpectedError() + { + int port = ProcessHelper.GetAvailablePort(); + + // Create a temporary working directory with invalid host.json + var tempDir = Path.Combine(_fixture.WorkingDirectory, "temp_invalid_host"); + Directory.CreateDirectory(tempDir); + CopyDirectoryHelpers.CopyDirectory(_fixture.WorkingDirectory, tempDir); + + // Create invalid host.json + var hostJsonPath = Path.Combine(tempDir, "host.json"); + var hostJsonContent = "{ \"version\": \"2.0\", \"extensionBundle\": { \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\", \"version\": \"[2.*, 3.0.0)\" }}"; + File.WriteAllText(hostJsonPath, hostJsonContent); + + // Call func start + var result = new FuncStartCommand(_fixture.FuncPath, nameof(Start_InProc_InvalidHostJson_FailsWithExpectedError), _fixture.Log) + .WithWorkingDirectory(tempDir) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet") + .Execute(new[] { "--port", port.ToString() }); + + // Validate error message + // We are expecting an exit to happen gracefully here; + // if func start were to succeed, the user would have to manually kill the process and exit with -1 + result.Should().ExitWith(0); + result.Should().HaveStdOutContaining("Extension bundle configuration should not be present"); + + // Clean up temporary directory + Directory.Delete(tempDir, true); + } + + [Fact] + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] + public void Start_InProc_MissingHostJson_FailsWithExpectedError() + { + int port = ProcessHelper.GetAvailablePort(); + + // Create a temporary working directory without host.json + var tempDir = Path.Combine(_fixture.WorkingDirectory, "temp_missing_host"); + Directory.CreateDirectory(tempDir); + CopyDirectoryHelpers.CopyDirectoryWithout(_fixture.WorkingDirectory, tempDir, "host.json"); + + // Call func start + var result = new FuncStartCommand(_fixture.FuncPath, nameof(Start_InProc_MissingHostJson_FailsWithExpectedError), _fixture.Log) + .WithWorkingDirectory(tempDir) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet") + .Execute(new[] { "--port", port.ToString() }); + + // Validate error message + // We are expecting an exit to happen gracefully here; + // if func start were to succeed, the user would have to manually kill the process and exit with -1 + result.Should().ExitWith(0); + result.Should().HaveStdOutContaining("Host.json file in missing"); + + // Clean up temporary directory + Directory.Delete(tempDir, true); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/DotnetInProc8Tests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/DotnetInProc8Tests.cs new file mode 100644 index 000000000..2bd652888 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/DotnetInProc8Tests.cs @@ -0,0 +1,153 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.E2E.Tests.Fixtures; +using Azure.Functions.Cli.E2E.Tests.Traits; +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.TestsWithFixtures +{ + [Collection("DotnetInProc8")] + [Trait(TestTraits.Group, TestTraits.InProc)] + public class DotnetInProc8Tests : IClassFixture + { + private readonly Dotnet8InProcFunctionAppFixture _fixture; + + public DotnetInProc8Tests(Dotnet8InProcFunctionAppFixture fixture, ITestOutputHelper log) + { + _fixture = fixture; + _fixture.Log = log; + } + + [Fact] + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] + public void Start_InProc_Net8_WithoutSpecifyingRuntime_SuccessfulFunctionExecution() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_InProc_Net8_WithoutSpecifyingRuntime_SuccessfulFunctionExecution), _fixture.Log); + + string? capturedContent = null; + + funcStartCommand.ProcessStartedHandler = async (process) => + { + capturedContent = await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger?name=Test"); + }; + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet") + .Execute(["--verbose", "--port", port.ToString()]); + + capturedContent.Should().Be("Hello, Test. This HTTP triggered function executed successfully."); + + // Validate inproc8 host was started + result.Should().StartInProc8Host(); + } + + [Fact] + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] + public void Start_InProc_Net8_WithSpecifyingRuntime_SuccessfulFunctionExecution() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_InProc_Net8_WithSpecifyingRuntime_SuccessfulFunctionExecution), _fixture.Log); + + string? capturedContent = null; + + funcStartCommand.ProcessStartedHandler = async (process) => + { + capturedContent = await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger?name=Test"); + }; + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet") + .Execute(["--verbose", "--runtime", "inproc8", "--port", port.ToString()]); + + capturedContent.Should().Be("Hello, Test. This HTTP triggered function executed successfully."); + + // Validate inproc8 host was started + result.Should().StartInProc8Host(); + } + + [Fact] + public void Start_Net8InProc_WithSpecifyingRuntime_ExpectedToFail() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start (expected to fail) + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_Net8InProc_WithSpecifyingRuntime_ExpectedToFail), _fixture.Log); + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet") + .Execute(["start", "--verbose", "--runtime", "inproc8", "--port", port.ToString()]); + + // Validate failure message + result.Should().ExitWith(1); + result.Should().HaveStdErrContaining("Failed to locate the inproc8 model host"); + } + + [Fact] + public void Start_Net8InProc_WithoutSpecifyingRuntime_ExpectedToFail() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start (expected to fail) + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_Net8InProc_WithoutSpecifyingRuntime_ExpectedToFail), _fixture.Log); + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet") + .Execute(["start", "--verbose", "--port", port.ToString()]); + + // Validate failure message + result.Should().ExitWith(1); + result.Should().HaveStdErrContaining("Failed to locate the inproc8 model host"); + } + + [Fact] + public void Start_Dotnet8InProcApp_With_InProc6Runtime_ShouldFail() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start (expected to fail) + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_Dotnet8InProcApp_With_InProc6Runtime_ShouldFail), _fixture.Log); + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet") + .Execute(["start", "--verbose", "--runtime", "inproc6", "--port", port.ToString()]); + + // Validate failure message + result.Should().ExitWith(1); + result.Should().HaveStdErrContaining("The runtime argument value provided, 'inproc6', is invalid. For the 'inproc6' runtime, the 'FUNCTIONS_INPROC_NET8_ENABLED' environment variable cannot be be set. See https://aka.ms/azure-functions/dotnet/net8-in-process."); + } + + [Fact] + public void Start_Dotnet8InProcApp_With_DefaultRuntime_ShouldFail() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start (expected to fail) + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_Dotnet8InProcApp_With_DefaultRuntime_ShouldFail), _fixture.Log); + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet") + .Execute(["start", "--verbose", "--runtime", "default", "--port", port.ToString()]); + + // Validate failure message + result.Should().ExitWith(1); + result.Should().HaveStdErrContaining("The runtime argument value provided, 'default', is invalid. The provided value is only valid for the worker runtime 'dotnetIsolated'."); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/DotnetIsolatedTests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/DotnetIsolatedTests.cs new file mode 100644 index 000000000..c1010798d --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/DotnetIsolatedTests.cs @@ -0,0 +1,142 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.E2E.Tests.Fixtures; +using Azure.Functions.Cli.E2E.Tests.Traits; +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.TestsWithFixtures +{ + public class DotnetIsolatedTests : IClassFixture + { + private readonly DotnetIsolatedFunctionAppFixture _fixture; + + public DotnetIsolatedTests(DotnetIsolatedFunctionAppFixture fixture, ITestOutputHelper log) + { + _fixture = fixture; + _fixture.Log = log; + } + + [Fact] + public void Start_Net9_SuccessfulFunctionExecution() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_Net9_SuccessfulFunctionExecution), _fixture.Log); + + string? capturedContent = null; + + funcStartCommand.ProcessStartedHandler = async (process) => + { + capturedContent = await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger"); + }; + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet-isolated") + .Execute(["--verbose", "--port", port.ToString()]); + + // Validate that getting http endpoint works + capturedContent.Should().Be("Welcome to Azure Functions!"); + + // Validate out-of-process host was started + result.Should().StartOutOfProcessHost(); + } + + [Fact] + [Trait(TestTraits.Group, TestTraits.UseInConsolidatedArtifactGeneration)] + public void Start_DotnetIsolated_WithRuntimeSpecified_SuccessfulFunctionExecution() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_DotnetIsolated_WithRuntimeSpecified_SuccessfulFunctionExecution), _fixture.Log); + string? capturedContent = null; + + funcStartCommand.ProcessStartedHandler = async (process) => + { + capturedContent = await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger"); + }; + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet-isolated") + .Execute(["start", "--verbose", "--runtime", "default", "--port", port.ToString()]); + + // Validate that getting http endpoint works + capturedContent.Should().Be( + "Welcome to Azure Functions!", + because: "response from default function should be 'Welcome to Azure Functions!'"); + + // Validate default host was started + result.Should().StartDefaultHost(); + } + + [Fact] + [Trait(TestTraits.Group, TestTraits.UseInConsolidatedArtifactGeneration)] + public void Start_DotnetIsolated_WithoutRuntimeSpecified_SuccessfulFunctionExecution() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_DotnetIsolated_WithoutRuntimeSpecified_SuccessfulFunctionExecution), _fixture.Log); + string? capturedContent = null; + + funcStartCommand.ProcessStartedHandler = async (process) => + { + capturedContent = await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger?name=Test"); + }; + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet-isolated") + .Execute(["--verbose", "--port", port.ToString()]); + + // Validate that getting http endpoint works + capturedContent.Should().Be( + "Welcome to Azure Functions!", + because: "response from default function should be 'Welcome to Azure Functions!'"); + + // Validate default host was started + result.Should().StartOutOfProcessHost(); + } + + [Fact] + public void Start_DotnetIsolatedApp_With_InProc8AsRuntime_ShouldFail() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start with invalid runtime (expected to fail) + var result = new FuncStartCommand(_fixture.FuncPath, nameof(Start_DotnetIsolatedApp_With_InProc8AsRuntime_ShouldFail), _fixture.Log) + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet-isolated") + .Execute(["--verbose", "--runtime", "inproc6", "--port", port.ToString()]); + + // Validate error message + result.Should().ExitWith(1); + result.Should().HaveStdErrContaining("The runtime argument value provided, 'inproc6', is invalid. The provided value is only valid for the worker runtime 'dotnet'."); + } + + [Fact] + public void Start_DotnetIsolatedApp_With_InProc6AsRuntime_ShouldFail() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start with invalid runtime (expected to fail) + var result = new FuncStartCommand(_fixture.FuncPath, nameof(Start_DotnetIsolatedApp_With_InProc6AsRuntime_ShouldFail), _fixture.Log) + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "dotnet-isolated") + .Execute(["--verbose", "--runtime", "inproc8", "--port", port.ToString()]); + + // Validate error message + result.Should().ExitWith(1); + result.Should().HaveStdErrContaining("The runtime argument value provided, 'inproc8', is invalid. The provided value is only valid for the worker runtime 'dotnet'."); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/NodeV3Tests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/NodeV3Tests.cs new file mode 100644 index 000000000..931ca972e --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/NodeV3Tests.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.E2E.Tests.Fixtures; +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.TestsWithFixtures +{ + [Collection("NodeV3")] + public class NodeV3Tests : IClassFixture + { + private readonly NodeV3FunctionAppFixture _fixture; + + public NodeV3Tests(NodeV3FunctionAppFixture fixture, ITestOutputHelper log) + { + _fixture = fixture; + _fixture.Log = log; + } + + [Fact] + public void Start_NodeJsApp_V3_SuccessfulFunctionExecution() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_NodeJsApp_V3_SuccessfulFunctionExecution), _fixture.Log); + + string? capturedContent = null; + + funcStartCommand.ProcessStartedHandler = async (process) => + { + capturedContent = await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger"); + }; + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "node") + .Execute(["--verbose", "--port", port.ToString()]); + + capturedContent.Should().Be("This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/NodeV4Tests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/NodeV4Tests.cs new file mode 100644 index 000000000..b27e38dc9 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/NodeV4Tests.cs @@ -0,0 +1,178 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Net; +using System.Net.Sockets; +using Azure.Functions.Cli.E2E.Tests.Fixtures; +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.TestsWithFixtures +{ + [Collection("NodeV4")] + public class NodeV4Tests : IClassFixture + { + private readonly NodeV4FunctionAppFixture _fixture; + + public NodeV4Tests(NodeV4FunctionAppFixture fixture, ITestOutputHelper log) + { + _fixture = fixture; + _fixture.Log = log; + } + + [Fact] + public void Start_WithoutSpecifyingDefaultHost_SuccessfulFunctionExecution() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_WithoutSpecifyingDefaultHost_SuccessfulFunctionExecution), _fixture.Log); + + string? capturedContent = null; + + funcStartCommand.ProcessStartedHandler = async (process) => + { + capturedContent = await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger?name=Test"); + }; + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "node") + .Execute(["--verbose", "--port", port.ToString()]); + + capturedContent.Should().Be("Hello, Test!"); + + result.Should().NotHaveStdOutContaining("Content root path:"); + + // Validate out-of-process host was started + result.Should().StartOutOfProcessHost(); + } + + [Fact] + public void Start_WithSpecifyingDefaultHost_SuccessfulFunctionExecution() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_WithSpecifyingDefaultHost_SuccessfulFunctionExecution), _fixture.Log); + + string? capturedContent = null; + + funcStartCommand.ProcessStartedHandler = async (process) => + { + capturedContent = await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger?name=Test"); + }; + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "node") + .Execute(["--verbose", "--port", port.ToString(), "--runtime", "default"]); + + capturedContent.Should().Be("Hello, Test!"); + + result.Should().NotHaveStdOutContaining("Content root path:"); + + // Validate out-of-process host was started + result.Should().StartDefaultHost(); + } + + [Fact] + public void Start_WithInspect_DebuggerIsStarted() + { + int port = ProcessHelper.GetAvailablePort(); + int debugPort = ProcessHelper.GetAvailablePort(); + + // Call func start with inspect flag + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_WithInspect_DebuggerIsStarted), _fixture.Log); + + funcStartCommand.ProcessStartedHandler = async (process) => + { + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger?name=Test"); + }; + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "node") + .Execute(["--port", port.ToString(), "--verbose", "--language-worker", "--", $"\"--inspect={debugPort}\""]); + + // Validate debugger started + result.Should().HaveStdOutContaining($"Debugger listening on ws://127.0.0.1:{debugPort}"); + } + + [Fact] + public void Start_PortInUse_FailsWithExpectedError() + { + int port = ProcessHelper.GetAvailablePort(); + + // Start a listener on the port to simulate it being in use + var tcpListener = new TcpListener(IPAddress.Any, port); + + try + { + tcpListener.Start(); + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_PortInUse_FailsWithExpectedError), _fixture.Log); + + // Call func start + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "node") + .Execute(["--port", port.ToString()]); + + funcStartCommand.ProcessStartedHandler = async (process) => + { + // Wait for debugger message + await Task.Delay(5000); + process.Kill(true); + }; + + // Validate error message + result.Should().HaveStdErrContaining($"Port {port} is unavailable"); + } + finally + { + // Clean up listener + tcpListener.Stop(); + } + } + + [Fact] + public void Start_NonDotnetApp_With_InProc6Runtime_ShouldFail() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start (expected to fail) + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_NonDotnetApp_With_InProc6Runtime_ShouldFail), _fixture.Log); + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "node") + .Execute(["--verbose", "--runtime", "inproc6", "--port", port.ToString()]); + + // Validate failure message + result.Should().ExitWith(1); + result.Should().HaveStdErrContaining("The runtime argument value provided, 'inproc6', is invalid. The provided value is only valid for the worker runtime 'dotnet'."); + } + + [Fact] + public void Start_NonDotnetApp_With_InProc8Runtime_ShouldFail() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start (expected to fail) + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_NonDotnetApp_With_InProc8Runtime_ShouldFail), _fixture.Log); + + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "node") + .Execute(["--verbose", "--runtime", "inproc8", "--port", port.ToString()]); + + // Validate failure message + result.Should().ExitWith(1); + result.Should().HaveStdErrContaining("The runtime argument value provided, 'inproc8', is invalid. The provided value is only valid for the worker runtime 'dotnet'."); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/PowershellTests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/PowershellTests.cs new file mode 100644 index 000000000..b78087190 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/TestsWithFixtures/PowershellTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.E2E.Tests.Fixtures; +using Azure.Functions.Cli.TestFramework.Assertions; +using Azure.Functions.Cli.TestFramework.Commands; +using Azure.Functions.Cli.TestFramework.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.TestsWithFixtures +{ + [Collection("Powershell")] + public class PowershellTests : IClassFixture + { + private readonly PowershellFunctionAppFixture _fixture; + + public PowershellTests(PowershellFunctionAppFixture fixture, ITestOutputHelper log) + { + _fixture = fixture; + _fixture.Log = log; + } + + [Fact] + public void Start_PowershellApp_SuccessfulFunctionExecution() + { + int port = ProcessHelper.GetAvailablePort(); + + // Call func start + var funcStartCommand = new FuncStartCommand(_fixture.FuncPath, nameof(Start_PowershellApp_SuccessfulFunctionExecution), _fixture.Log); + string? capturedContent = null; + + funcStartCommand.ProcessStartedHandler = async (process) => + { + capturedContent = await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter ?? throw new ArgumentNullException(nameof(funcStartCommand.FileWriter)), "HttpTrigger?name=Test"); + }; + var result = funcStartCommand + .WithWorkingDirectory(_fixture.WorkingDirectory) + .WithEnvironmentVariable(Common.Constants.FunctionsWorkerRuntime, "powershell") + .Execute(["--verbose", "--port", port.ToString()]); + + capturedContent.Should().Be("Hello, Test. This HTTP triggered function executed successfully."); + + // Validate out-of-process host was started + result.Should().StartOutOfProcessHost(); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Commands/FuncStart/UserSecretsTests.cs b/test/Cli/Func.E2E.Tests/Commands/FuncStart/UserSecretsTests.cs new file mode 100644 index 000000000..39d5185af --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Commands/FuncStart/UserSecretsTests.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.Core; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Commands.FuncStart +{ + public class UserSecretsTests(ITestOutputHelper log) : BaseUserSecretsTests(log) + { + [Fact] + public async Task Start_Dotnet_Isolated_WithUserSecrets_SuccessfulFunctionExecution() + { + await RunUserSecretsTest("dotnet-isolated", nameof(Start_Dotnet_Isolated_WithUserSecrets_SuccessfulFunctionExecution)); + } + + [Fact(Skip = "Test is not working as expected for dotnet-isolated. Need to further investigate why.")] + public async Task Start_Dotnet_Isolated_WithUserSecrets_MissingStorageConnString_FailsWithExpectedError() + { + await RunMissingStorageConnString_FailsWithExpectedError("dotnet-isolated", nameof(Start_Dotnet_Isolated_WithUserSecrets_MissingStorageConnString_FailsWithExpectedError)); + } + + [Fact(Skip = "Test is not working as expected for dotnet-isolated. Need to further investigate why.")] + public async Task Start_Dotnet_Isolated_WithUserSecrets_MissingBindingSetting_FailsWithExpectedError() + { + await RunWithUserSecrets_MissingBindingSetting_FailsWithExpectedError("dotnet-isolated", nameof(Start_Dotnet_Isolated_WithUserSecrets_MissingBindingSetting_FailsWithExpectedError)); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Constants.cs b/test/Cli/Func.E2E.Tests/Constants.cs new file mode 100644 index 000000000..85293b634 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Constants.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Azure.Functions.Cli.E2E.Tests +{ + internal static class Constants + { + internal const string FuncPath = "FUNC_PATH"; + internal const string VisualStudioNet8ProjectPath = "NET8_VS_PROJECT_PATH"; + internal const string VisualStudioNet6ProjectPath = "NET6_VS_PROJECT_PATH"; + } +} diff --git a/test/Cli/Func.E2E.Tests/Fixtures/BaseFunctionAppFixture.cs b/test/Cli/Func.E2E.Tests/Fixtures/BaseFunctionAppFixture.cs new file mode 100644 index 000000000..282d7e538 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Fixtures/BaseFunctionAppFixture.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Runtime.InteropServices; +using Azure.Functions.Cli.Helpers; +using Azure.Functions.Cli.TestFramework.Helpers; +using Moq; +using Xunit; +using Xunit.Abstractions; + +namespace Azure.Functions.Cli.E2E.Tests.Fixtures +{ + public abstract class BaseFunctionAppFixture : IAsyncLifetime + { + public BaseFunctionAppFixture(WorkerRuntime workerRuntime, string? targetFramework = null, string? version = null) + { + WorkerRuntime = workerRuntime; + TargetFramework = targetFramework; + Version = version; + + Log = new Mock().Object; + + FuncPath = Environment.GetEnvironmentVariable(Constants.FuncPath) ?? string.Empty; + + if (string.IsNullOrEmpty(FuncPath)) + { + // Fallback for local testing in Visual Studio, etc. + FuncPath = Path.Combine(Environment.CurrentDirectory, "func"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + FuncPath += ".exe"; + } + + if (!File.Exists(FuncPath)) + { + throw new ApplicationException("Could not locate the 'func' executable to use for testing. Make sure the FUNC_PATH environment variable is set to the full path of the func executable."); + } + } + + Directory.CreateDirectory(WorkingDirectory); + } + + public ITestOutputHelper Log { get; set; } + + public string FuncPath { get; set; } + + public string WorkingDirectory { get; set; } = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + public bool CleanupWorkingDirectory { get; set; } = true; + + public WorkerRuntime WorkerRuntime { get; set; } + + public string? TargetFramework { get; set; } + + public string? Version { get; set; } + + public Task DisposeAsync() + { + try + { + Directory.Delete(WorkingDirectory, true); + } + catch + { + // Ignore any errors when cleaning up + } + + return Task.CompletedTask; + } + + public async Task InitializeAsync() + { + var workerRuntime = WorkerRuntimeLanguageHelper.GetRuntimeMoniker(WorkerRuntime); + var initArgs = new List { ".", "--worker-runtime", workerRuntime } + .Concat(TargetFramework != null + ? new[] { "--target-framework", TargetFramework } + : []) + .Concat(Version != null + ? new[] { "-m", Version } + : []) + .ToList(); + + string nameOfFixture = WorkerRuntime + (TargetFramework ?? string.Empty) + (Version ?? string.Empty); + + await FunctionAppSetupHelper.FuncInitWithRetryAsync(FuncPath, nameOfFixture, WorkingDirectory, Log, initArgs); + + var funcNewArgs = new[] { ".", "--template", "HttpTrigger", "--name", "HttpTrigger" } + .Concat((WorkerRuntime != WorkerRuntime.dotnet && WorkerRuntime != WorkerRuntime.dotnetIsolated) ? ["--language", workerRuntime] : Array.Empty()) + .ToArray(); + await FunctionAppSetupHelper.FuncNewWithRetryAsync(FuncPath, nameOfFixture, WorkingDirectory, Log, funcNewArgs, workerRuntime); + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Fixtures/Dotnet6InProcFunctionAppFixture.cs b/test/Cli/Func.E2E.Tests/Fixtures/Dotnet6InProcFunctionAppFixture.cs new file mode 100644 index 000000000..c3a0e5a38 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Fixtures/Dotnet6InProcFunctionAppFixture.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.Helpers; + +namespace Azure.Functions.Cli.E2E.Tests.Fixtures +{ + public class Dotnet6InProcFunctionAppFixture : BaseFunctionAppFixture + { + public Dotnet6InProcFunctionAppFixture() + : base(WorkerRuntime.dotnet, targetFramework: "net6.0") + { + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Fixtures/Dotnet8InProcFunctionAppFixture.cs b/test/Cli/Func.E2E.Tests/Fixtures/Dotnet8InProcFunctionAppFixture.cs new file mode 100644 index 000000000..1efb02844 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Fixtures/Dotnet8InProcFunctionAppFixture.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.Helpers; + +namespace Azure.Functions.Cli.E2E.Tests.Fixtures +{ + public class Dotnet8InProcFunctionAppFixture : BaseFunctionAppFixture + { + public Dotnet8InProcFunctionAppFixture() + : base(WorkerRuntime.dotnet, targetFramework: "net8.0") + { + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Fixtures/DotnetIsolatedFunctionAppFixture.cs b/test/Cli/Func.E2E.Tests/Fixtures/DotnetIsolatedFunctionAppFixture.cs new file mode 100644 index 000000000..5571ab35a --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Fixtures/DotnetIsolatedFunctionAppFixture.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.Helpers; + +namespace Azure.Functions.Cli.E2E.Tests.Fixtures +{ + public class DotnetIsolatedFunctionAppFixture : BaseFunctionAppFixture + { + public DotnetIsolatedFunctionAppFixture() + : base(WorkerRuntime.dotnetIsolated) + { + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Fixtures/NodeV3FunctionAppFixture.cs b/test/Cli/Func.E2E.Tests/Fixtures/NodeV3FunctionAppFixture.cs new file mode 100644 index 000000000..dd7c6ce10 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Fixtures/NodeV3FunctionAppFixture.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.Helpers; + +namespace Azure.Functions.Cli.E2E.Tests.Fixtures +{ + public class NodeV3FunctionAppFixture : BaseFunctionAppFixture + { + public NodeV3FunctionAppFixture() + : base(WorkerRuntime.node, version: "v3") + { + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Fixtures/NodeV4FunctionAppFixture.cs b/test/Cli/Func.E2E.Tests/Fixtures/NodeV4FunctionAppFixture.cs new file mode 100644 index 000000000..42aa1a089 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Fixtures/NodeV4FunctionAppFixture.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.Helpers; + +namespace Azure.Functions.Cli.E2E.Tests.Fixtures +{ + public class NodeV4FunctionAppFixture : BaseFunctionAppFixture + { + public NodeV4FunctionAppFixture() + : base(WorkerRuntime.node, version: "v4") + { + } + } +} diff --git a/test/Cli/Func.E2E.Tests/Fixtures/PowershellFunctionAppFixture.cs b/test/Cli/Func.E2E.Tests/Fixtures/PowershellFunctionAppFixture.cs new file mode 100644 index 000000000..5e007e4f6 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Fixtures/PowershellFunctionAppFixture.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Azure.Functions.Cli.Helpers; + +namespace Azure.Functions.Cli.E2E.Tests.Fixtures +{ + public class PowershellFunctionAppFixture : BaseFunctionAppFixture + { + public PowershellFunctionAppFixture() + : base(WorkerRuntime.powershell) + { + } + } +} diff --git a/test/Cli/Func.E2E.Tests/README.md b/test/Cli/Func.E2E.Tests/README.md new file mode 100644 index 000000000..1f868cd05 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/README.md @@ -0,0 +1,231 @@ +# Azure Functions Core Tools E2E Testing Guide + +## Overview + +This project contains the E2E tests for Azure Functions Core Tools. When adding a new E2E test, please follow these guidelines to ensure consistency and efficient test execution. + +### Test Organization + +- Create tests within `Commands/Func[COMMAND_NAME]` directories +- Separate tests into categories by organizing them in files with similar tests + - Examples: `AuthTests`, `LogLevelTests`, etc. +- This organization allows tests in different files to run in parallel, improving execution speed + +### Test Types for `func start` + +There are two main types of tests for the `func start` command: + +#### 1. Tests with Fixtures + +- Based on `BaseFunctionAppFixture` +- The fixture handles initialization (`func init` and `func new`) +- Best for tests that require a simple `HttpTrigger` with no additional configuration +- Add your test to the appropriate fixture to avoid setup overhead + +#### 2. Tests without Fixtures + +- Based on `BaseE2ETests` +- Sets up variables like `WorkingDirectory` and `FuncPath` +- You must handle application setup in each test +- Use the provided helper methods `FuncInitWithRetryAsync` and `FuncNewWithRetryAsync` for setup + +### Important Notes + +### Test Parallelization + +❗ **Dotnet isolated templates and dotnet in-proc templates CANNOT be run in parallel** + +- Use traits to distinguish between dotnet-isolated and dotnet-inproc tests +- Refer to TestTraits.cs for trait names and explanations on when they should be used +- This ensures they run sequentially rather than in parallel + +### Node.js Tests + +⚠️ **Node.js tests require explicit environment variable configuration** + +- Due to flakiness in `func init` and `func new` not properly initializing environment variables +- Always append `.WithEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME", "node")` to the `funcStartCommand` like this: + +```csharp +var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .WithEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME", "node") + .Execute(commandArgs.ToArray()); +``` + +## Step-by-Step Guide: Creating a `func start` Test without a Fixture + +### 1. Get an Available Port + +First, get an available port using `ProcessHelper.GetAvailablePort()` to ensure your test doesn't conflict with other processes: + +```csharp +int port = ProcessHelper.GetAvailablePort(); +``` + +### 2. Define Test Names + +Create descriptive names for your test. For parameterized tests, include the parameters in the test name for better traceability: + +```csharp +string methodName = "Start_DotnetIsolated_Test_EnableAuthFeature"; +string uniqueTestName = $"{methodName}_{parameterValue1}_{parameterValue2}"; +``` + +### 3. Initialize a Function App + +Use `FuncInitWithRetryAsync` to create a new function app with the appropriate runtime: + +```csharp +await FuncInitWithRetryAsync(uniqueTestName, new[] { ".", "--worker-runtime", "dotnet-isolated" }); +``` + +### 4. Add a Trigger Function + +Use `FuncNewWithRetryAsync` to add a trigger function with specific configuration: + +```csharp +await FuncNewWithRetryAsync(uniqueTestName, new[] { + ".", + "--template", "HttpTrigger", + "--name", "HttpTrigger", + "--authlevel", authLevel +}); +``` + +### 5. Create a FuncStartCommand + +Initialize a `FuncStartCommand` with the path to the func executable, test name, and logger: + +```csharp +var funcStartCommand = new FuncStartCommand(FuncPath, methodName, Log); +``` + +### 6. Add a Process Started Handler (Optional) + +If you need to wait for the host to start or check logs from a different process, add a `ProcessStartedHandler`: + +```csharp +funcStartCommand.ProcessStartedHandler = async (process) => +{ + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter, "HttpTrigger"); +}; +``` + +The `ProcessStartedHandlerHelper` method: +- Waits for the host to start +- Makes an HTTP request to the function +- Captures logs from the process +- Returns when the function has processed the request + +### 7. Build Command Arguments + +Build your command arguments based on test parameters: + +```csharp +var commandArgs = new List { "start", "--verbose", "--port", port.ToString() }; +if (enableSomeFeature) +{ + commandArgs.Add("--featureFlag"); +} +``` + +### 8. Execute the Command + +Execute the command with the working directory and arguments: + +```csharp +var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .Execute(commandArgs.ToArray()); +``` + +### 9. Validate the Results + +Validate the command output contains the expected results based on test parameters: + +```csharp +if (someCondition) +{ + result.Should().HaveStdOutContaining("expected output 1"); +} +else +{ + result.Should().HaveStdOutContaining("expected output 2"); +} +``` + +You may also create a custom condition and then call the method like so: + +```csharp +// Validate inproc6 host was started +result.Should().StartInProc6Host(); + +``` + +## Testing with Fixtures + +The steps are similar for creating a `func start` test with a fixture, except you can skip the setup logic that calls `func init` and `func new` as the fixture handles this for you. Please ensure that the test that are being added to the fixture DO NOT change the environment or add any extra variables or config, as that may cause problems with the existing tests. + +## Complete Example + +```csharp +public async Task Start_DotnetIsolated_Test_EnableAuthFeature( + string authLevel, + bool enableAuth, + string expectedResult) +{ + int port = ProcessHelper.GetAvailablePort(); + string methodName = "Start_DotnetIsolated_Test_EnableAuthFeature"; + string uniqueTestName = $"{methodName}_{authLevel}_{enableAuth}"; + + // Setup the function app + await FuncInitWithRetryAsync(uniqueTestName, new[] { ".", "--worker-runtime", "dotnet-isolated" }); + await FuncNewWithRetryAsync(uniqueTestName, new[] { ".", "--template", "HttpTrigger", "--name", "HttpTrigger", "--authlevel", authLevel }); + + // Create and configure the start command + var funcStartCommand = new FuncStartCommand(FuncPath, methodName, Log); + funcStartCommand.ProcessStartedHandler = async (process) => + { + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter, "HttpTrigger"); + }; + + // Build command arguments + var commandArgs = new List { "start", "--verbose", "--port", port.ToString() }; + if (enableAuth) + { + commandArgs.Add("--enableAuth"); + } + + // Execute and validate + var result = funcStartCommand + .WithWorkingDirectory(WorkingDirectory) + .Execute(commandArgs.ToArray()); + + if (string.IsNullOrEmpty(expectedResult)) + { + result.Should().HaveStdOutContaining("\"status\": \"401\""); + } + else + { + result.Should().HaveStdOutContaining("Selected out-of-process host."); + } +} +``` + +## How to Build and Run Tests Locally + +1. Run `dotnet build` on `Azure.Functions.Cli.E2E.Tests.csproj`. + +2. Navigate to `CORE_TOOLS_REPO_ROOT\out\bin\Azure.Functions.Cli.E2E.Tests\debug` and add a `templates` file if it doesn't already exist. There are two options for how to obtain the contents of the templates folder: + - Copy the existing templates file from the core tools installation on your local machine to the debug directory. + - Run the `Build.csproj` project to generate the `templates` folder within the debug directory of the build project. Copy that file over to the debug directory for the E2E tests. + +3. To test in-proc artifacts, copy the `inproc6` and `inproc8` directories by either: + - Building the `inproc` branch locally and manually copying the folders into the debug folder, or + - Copying these folders from the core tools installation on your local machine to the debug folder. + +4. Execute the tests using the `dotnet test` command or Visual Studio Test Explorer. Note that only tests requiring default artifacts (not in-proc artifacts) will run by default. + - To run individual tests, use the `--filter` option with the test name. For example: `dotnet test --filter FullyQualifiedName~Azure.Functions.Cli.E2E.Tests.Commands.FuncStart.Start_MissingLocalSettingsJson_BehavesAsExpected`. + +5. To run a specific test with runtime settings, use: `dotnet test {PATH_TO_E2E_TEST_PROJECT} --filter "TestCategory=InProc"`.` \ No newline at end of file diff --git a/test/Cli/Func.E2E.Tests/Traits/TestTraits.cs b/test/Cli/Func.E2E.Tests/Traits/TestTraits.cs new file mode 100644 index 000000000..5bf2d52b4 --- /dev/null +++ b/test/Cli/Func.E2E.Tests/Traits/TestTraits.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Azure.Functions.Cli.E2E.Tests.Traits +{ + internal static class TestTraits + { + /// + /// Defines a group of tests to be run together. Useful for test isolation. + /// + public const string Group = "Group"; + + /// + /// Tests with RequiresNestedInProcArtifacts label will not be run in the default scenario and only in the artifact consolidation pipeline + /// Otherwise tests with this label will fail in the PR/ official core tools pipelines since the nested inproc artifacts are not present. + /// + public const string RequiresNestedInProcArtifacts = "RequiresNestedInProcArtifacts"; + + /// + /// Tests with UseInConsolidatedArtifactGeneration label will be used in the default scenario and in the artifact consolidation pipeline + /// We still want to run these tests in the PR/ official core tools pipelines and in the artifact consolidation pipeline for a sanity check before publishing the artifacts. + /// + public const string UseInConsolidatedArtifactGeneration = "UseInConsolidatedArtifactGeneration"; + + /// + /// Tests with UseInVisualStudioConsolidatedArtifactGeneration label will not be run in the default scenario and only in the artifact consolidation pipeline + /// Otherwise tests with this label will fail in the PR/ official core tools pipelines since the nested inproc artifacts are not present. + /// + public const string UseInVisualStudioConsolidatedArtifactGeneration = "UseInVisualStudioConsolidatedArtifactGeneration"; + + /// + /// Tests with InProc label are used to distinguish dotnet isolated tests from dotnet inproc tests that are not involved in the artifact consolidation pipeline and do not require nested inproc artifacts. + /// This is done since when the dotnet isolated tests are run with dotnet inproc, we run into templating conflict errors. + /// + public const string InProc = "InProc"; + } +} diff --git a/test/Cli/Func.E2ETests/placeholder b/test/Cli/Func.E2ETests/placeholder deleted file mode 100644 index 9239a1421..000000000 --- a/test/Cli/Func.E2ETests/placeholder +++ /dev/null @@ -1 +0,0 @@ -e2e tests \ No newline at end of file diff --git a/test/Cli/TestFramework/Assertions/CommandResultAssertions.cs b/test/Cli/TestFramework/Assertions/CommandResultAssertions.cs index ec0693284..a9b08363b 100644 --- a/test/Cli/TestFramework/Assertions/CommandResultAssertions.cs +++ b/test/Cli/TestFramework/Assertions/CommandResultAssertions.cs @@ -39,5 +39,47 @@ public AndConstraint NotHaveStdOutContaining(string pat .FailWith($"The command output did contain expected result: {pattern}{Environment.NewLine}"); return new AndConstraint(this); } + + public AndConstraint StartInProc6Host() + { + Execute.Assertion.ForCondition(_commandResult.StdOut is not null && _commandResult.StdOut.Contains("Starting child process for inproc6 model host.") && _commandResult.StdOut.Contains("Selected inproc6 host.")) + .FailWith($"The command output did not contain expected result for inproc6 host.{Environment.NewLine}"); + return new AndConstraint(this); + } + + public AndConstraint StartInProc8Host() + { + Execute.Assertion.ForCondition(_commandResult.StdOut is not null && _commandResult.StdOut.Contains("Starting child process for inproc8 model host.") && _commandResult.StdOut.Contains("Selected inproc8 host.")) + .FailWith($"The command output did not contain expected result for inproc8 host.{Environment.NewLine}"); + return new AndConstraint(this); + } + + public AndConstraint StartOutOfProcessHost() + { + Execute.Assertion.ForCondition(_commandResult.StdOut is not null && _commandResult.StdOut.Contains("4.10") && _commandResult.StdOut.Contains("Selected out-of-process host.")) + .FailWith($"The command output did not contain expected result for out of process host.{Environment.NewLine}"); + return new AndConstraint(this); + } + + public AndConstraint StartDefaultHost() + { + Execute.Assertion.ForCondition(_commandResult.StdOut is not null && _commandResult.StdOut.Contains("Selected default host.")) + .FailWith($"The command output did not contain expected result for default host.{Environment.NewLine}"); + return new AndConstraint(this); + } + + public AndConstraint LoadNet6HostVisualStudio() + { + Execute.Assertion.ForCondition(_commandResult.StdOut is not null && _commandResult.StdOut.Contains("Loading .NET 6 host")) + .FailWith($"The command output did not contain expected result for .NET 6 host.{Environment.NewLine}"); + return new AndConstraint(this); + } + + public AndConstraint LoadNet8HostVisualStudio() + { + Execute.Assertion.ForCondition(_commandResult.StdOut is not null && _commandResult.StdOut.Contains("Loading .NET 8 host")) + .FailWith($"The command output did not contain expected result for .NET 8 host.{Environment.NewLine}"); + return new AndConstraint(this); + } } } diff --git a/test/Cli/TestFramework/Helpers/ProcessHelper.cs b/test/Cli/TestFramework/Helpers/ProcessHelper.cs index f96d34bb4..77b303f95 100644 --- a/test/Cli/TestFramework/Helpers/ProcessHelper.cs +++ b/test/Cli/TestFramework/Helpers/ProcessHelper.cs @@ -44,21 +44,16 @@ await RetryHelper.RetryAsync(async () => return true; } - LogMessage($"Trying to get ping response"); - // Try ping endpoint var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); HttpResponseMessage pingResponse = await httpClient.GetAsync($"{url}/admin/host/ping", cts.Token); - LogMessage($"Got ping response"); - if (pingResponse.IsSuccessStatusCode) { LogMessage("Host responded to ping - assuming it's running"); return true; } - LogMessage($"Returning false"); return false; } catch (Exception ex) diff --git a/test/TestFunctionApps/VisualStudioTestProjects/TestNet6InProcProject/Dotnet6InProc.cs b/test/TestFunctionApps/VisualStudioTestProjects/TestNet6InProcProject/Dotnet6InProc.cs new file mode 100644 index 000000000..6e81b3d7b --- /dev/null +++ b/test/TestFunctionApps/VisualStudioTestProjects/TestNet6InProcProject/Dotnet6InProc.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace TestNet8InProcProject +{ + public static class Dotnet6InProc + { + [FunctionName("Dotnet6InProc")] + public static async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, + ILogger log) + { + log.LogInformation("C# HTTP trigger function processed a request."); + + string? name = req.Query["name"]; + + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + dynamic? data = JsonConvert.DeserializeObject(requestBody); + name = name ?? data?.name; + + string responseMessage = string.IsNullOrEmpty(name) + ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response." + : $"Hello, {name}. This HTTP triggered function executed successfully."; + + return new OkObjectResult(responseMessage); + } + } +} diff --git a/test/TestFunctionApps/VisualStudioTestProjects/TestNet6InProcProject/TestNet6InProcProject.csproj b/test/TestFunctionApps/VisualStudioTestProjects/TestNet6InProcProject/TestNet6InProcProject.csproj new file mode 100644 index 000000000..085e962d2 --- /dev/null +++ b/test/TestFunctionApps/VisualStudioTestProjects/TestNet6InProcProject/TestNet6InProcProject.csproj @@ -0,0 +1,18 @@ + + + net6.0 + v4 + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + diff --git a/test/TestFunctionApps/VisualStudioTestProjects/TestNet6InProcProject/host.json b/test/TestFunctionApps/VisualStudioTestProjects/TestNet6InProcProject/host.json new file mode 100644 index 000000000..ee5cf5f83 --- /dev/null +++ b/test/TestFunctionApps/VisualStudioTestProjects/TestNet6InProcProject/host.json @@ -0,0 +1,12 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + } +} \ No newline at end of file diff --git a/test/TestFunctionApps/VisualStudioTestProjects/TestNet6InProcProject/local.settings.json b/test/TestFunctionApps/VisualStudioTestProjects/TestNet6InProcProject/local.settings.json new file mode 100644 index 000000000..4fce9ff39 --- /dev/null +++ b/test/TestFunctionApps/VisualStudioTestProjects/TestNet6InProcProject/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet" + } +} \ No newline at end of file diff --git a/test/TestFunctionApps/VisualStudioTestProjects/TestNet8InProcProject/Dotnet8InProc.cs b/test/TestFunctionApps/VisualStudioTestProjects/TestNet8InProcProject/Dotnet8InProc.cs new file mode 100644 index 000000000..fc044d153 --- /dev/null +++ b/test/TestFunctionApps/VisualStudioTestProjects/TestNet8InProcProject/Dotnet8InProc.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace TestNet8InProcProject +{ + public static class Dotnet8InProc + { + [FunctionName("Dotnet8InProc")] + public static async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, + ILogger log) + { + log.LogInformation("C# HTTP trigger function processed a request."); + + string? name = req.Query["name"]; + + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + dynamic? data = JsonConvert.DeserializeObject(requestBody); + name = name ?? data?.name; + + string responseMessage = string.IsNullOrEmpty(name) + ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response." + : $"Hello, {name}. This HTTP triggered function executed successfully."; + + return new OkObjectResult(responseMessage); + } + } +} diff --git a/test/TestFunctionApps/VisualStudioTestProjects/TestNet8InProcProject/TestNet8InProcProject.csproj b/test/TestFunctionApps/VisualStudioTestProjects/TestNet8InProcProject/TestNet8InProcProject.csproj new file mode 100644 index 000000000..e09adb47e --- /dev/null +++ b/test/TestFunctionApps/VisualStudioTestProjects/TestNet8InProcProject/TestNet8InProcProject.csproj @@ -0,0 +1,18 @@ + + + net8.0 + v4 + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + diff --git a/test/TestFunctionApps/VisualStudioTestProjects/TestNet8InProcProject/host.json b/test/TestFunctionApps/VisualStudioTestProjects/TestNet8InProcProject/host.json new file mode 100644 index 000000000..ee5cf5f83 --- /dev/null +++ b/test/TestFunctionApps/VisualStudioTestProjects/TestNet8InProcProject/host.json @@ -0,0 +1,12 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + } +} \ No newline at end of file diff --git a/test/TestFunctionApps/VisualStudioTestProjects/TestNet8InProcProject/local.settings.json b/test/TestFunctionApps/VisualStudioTestProjects/TestNet8InProcProject/local.settings.json new file mode 100644 index 000000000..3cbda74c1 --- /dev/null +++ b/test/TestFunctionApps/VisualStudioTestProjects/TestNet8InProcProject/local.settings.json @@ -0,0 +1,8 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_INPROC_NET8_ENABLED": "1", // test comment + "FUNCTIONS_WORKER_RUNTIME": "dotnet", + } +} \ No newline at end of file