diff --git a/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs b/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs index 01bbe6665..fc218e203 100644 --- a/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs +++ b/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs @@ -209,6 +209,39 @@ public override async Task RunAsync() // We do not change the default targetFramework if no .csproj file is found } + // Show warning message for other worker runtimes (Node, Python, Powershell, Java) + if (workerRuntime != WorkerRuntime.Dotnet && workerRuntime != WorkerRuntime.DotnetIsolated) + { + string workerRuntimeStr = Convert.ToString(workerRuntime); + string runtimeVersion = GetWorkerRuntimeVersion(workerRuntime, functionAppRoot); + + if (!string.IsNullOrEmpty(runtimeVersion)) + { + // Get runtime stacks + var stacks = await AzureHelper.GetFunctionsStacks(AccessToken, ManagementURL); + DateTime currentDate = DateTime.Now; + + object runtimeSettings = (workerRuntime == WorkerRuntime.Python) ? stacks.GetOtherRuntimeSettings(workerRuntimeStr, runtimeVersion, s => s.LinuxRuntimeSettings) : stacks.GetOtherRuntimeSettings(workerRuntimeStr, runtimeVersion, s => s.WindowsRuntimeSettings); + + DateTime? eolDate = runtimeSettings switch + { + LinuxRuntimeSettings linux => linux.EndOfLifeDate, + WindowsRuntimeSettings windows => windows.EndOfLifeDate, + _ => null + }; + + if (eolDate.HasValue) + { + DateTime warningThresholdDate = eolDate.Value.AddMonths(-6); + if (currentDate > eolDate || currentDate >= warningThresholdDate) + { + // Show EOL warning message + ShowEolMessageForOtherStack(stacks, eolDate.Value, workerRuntimeStr, runtimeVersion); + } + } + } + } + // Check for any additional conditions or app settings that need to change // before starting any of the publish activity. var additionalAppSettings = await ValidateFunctionAppPublish(functionApp, workerRuntime, functionAppRoot); @@ -1453,7 +1486,7 @@ private void ShowEolMessage(FunctionsStacks stacks, WindowsRuntimeSettings curre var nextDotnetVersion = stacks.GetNextDotnetVersion(majorDotnetVersion.Value); if (nextDotnetVersion != null) { - var warningMessage = EolMessages.GetAfterEolUpdateMessageDotNet(majorDotnetVersion.ToString(), nextDotnetVersion.ToString(), currentRuntimeSettings.EndOfLifeDate.Value); + var warningMessage = EolMessages.GetAfterEolUpdateMessageDotNet(majorDotnetVersion.ToString(), nextDotnetVersion.ToString(), currentRuntimeSettings.EndOfLifeDate.Value, Constants.FunctionsStackUpgrade); ColoredConsole.WriteLine(WarningColor(warningMessage)); } } @@ -1462,7 +1495,7 @@ private void ShowEolMessage(FunctionsStacks stacks, WindowsRuntimeSettings curre var nextDotnetVersion = stacks.GetNextDotnetVersion(majorDotnetVersion.Value); if (nextDotnetVersion != null) { - var warningMessage = EolMessages.GetEarlyEolUpdateMessageDotNet(majorDotnetVersion.ToString(), nextDotnetVersion.ToString(), currentRuntimeSettings.EndOfLifeDate.Value); + var warningMessage = EolMessages.GetEarlyEolUpdateMessageDotNet(majorDotnetVersion.ToString(), nextDotnetVersion.ToString(), currentRuntimeSettings.EndOfLifeDate.Value, Constants.FunctionsStackUpgrade); ColoredConsole.WriteLine(WarningColor(warningMessage)); } } @@ -1473,6 +1506,69 @@ private void ShowEolMessage(FunctionsStacks stacks, WindowsRuntimeSettings curre } } + /// + /// Determines the version of the specified worker runtime. + /// + private string GetWorkerRuntimeVersion(WorkerRuntime workerRuntime, string functionAppRoot) + { + switch (workerRuntime) + { + case WorkerRuntime.Node: + return NodeJSHelpers.GetNodeVersion(functionAppRoot); + case WorkerRuntime.Python: + return PythonHelpers.GetPythonVersion(functionAppRoot).GetAwaiter().GetResult(); + case WorkerRuntime.Powershell: + return PowerShellHelper.GetPowerShellVersion(functionAppRoot); + case WorkerRuntime.Java: + return JavaHelper.GetJavaVersion(functionAppRoot); + default: + return null; + } + } + + private void ShowEolMessageForOtherStack(FunctionsStacks stacks, DateTime eolDate, string workerRuntime, string runtimeVersion) + { + try + { + string nextVersion, displayName, warningMessage = string.Empty; + (nextVersion, displayName) = workerRuntime switch + { + var wr when wr.Equals(WorkerRuntime.Python.ToString(), StringComparison.OrdinalIgnoreCase) + || wr.Equals(WorkerRuntime.Powershell.ToString(), StringComparison.OrdinalIgnoreCase) + || wr.Equals(WorkerRuntime.Java.ToString(), StringComparison.OrdinalIgnoreCase) + => stacks.GetNextRuntimeVersion( + workerRuntime, + runtimeVersion, + properties => properties.MajorVersions + .SelectMany(mv => mv.MinorVersions, (major, minor) => minor.Value), + isNumericVersion: false), + + var wr when wr.Equals(WorkerRuntime.Node.ToString(), StringComparison.OrdinalIgnoreCase) + => stacks.GetNextRuntimeVersion( + workerRuntime, + runtimeVersion, + properties => properties.MajorVersions + .Select(mv => mv.Value), + isNumericVersion: true), + _ => (null, workerRuntime) // Default case: No next version available + }; + if (StacksApiHelper.ExpiresInNextSixMonths(eolDate)) + { + warningMessage = EolMessages.GetEarlyEolUpdateMessage(displayName, runtimeVersion, nextVersion, eolDate, Constants.FunctionsStackUpgrade); + ColoredConsole.WriteLine(WarningColor(warningMessage)); + } + else + { + warningMessage = EolMessages.GetAfterEolUpdateMessage(displayName, runtimeVersion, nextVersion, eolDate, Constants.FunctionsStackUpgrade); + ColoredConsole.WriteLine(WarningColor(warningMessage)); + } + } + catch (Exception) + { + // ignore. Failure to show the EOL message should not fail the deployment. + } + } + // For testing internal class AzureHelperService { diff --git a/src/Cli/func/Common/Constants.cs b/src/Cli/func/Common/Constants.cs index 0ae7ea7b6..3ef6faa60 100644 --- a/src/Cli/func/Common/Constants.cs +++ b/src/Cli/func/Common/Constants.cs @@ -97,6 +97,7 @@ internal static partial class Constants public const string AzureDevSessionsRemoteHostName = "AzureDevSessionsRemoteHostName"; public const string AzureDevSessionsPortSuffixPlaceholder = ""; public const string GitHubReleaseApiUrl = "https://api.github.com/repos/Azure/azure-functions-core-tools/releases/latest"; + public const string FunctionsStackUpgrade = "https://aka.ms/FunctionsStackUpgrade"; // Sample format https://n12abc3t-.asse.devtunnels.ms/ public static readonly Dictionary> WorkerRuntimeImages = new Dictionary> diff --git a/src/Cli/func/Helpers/EolMessages.cs b/src/Cli/func/Helpers/EolMessages.cs index 2ea3b1644..8cff18af1 100644 --- a/src/Cli/func/Helpers/EolMessages.cs +++ b/src/Cli/func/Helpers/EolMessages.cs @@ -19,17 +19,27 @@ public static string GetAfterEolCreateMessageDotNet(string stackVersion, DateTim public static string GetEarlyEolUpdateMessageDotNet(string currentStackVersion, string nextStackVersion, DateTime eol, string link = "") { - return $"Upgrade your app to .NET {nextStackVersion} as .NET {currentStackVersion} will reach EOL on {FormatDate(eol)} and will no longer be supported. {link}"; + return $"Upgrade to .NET {nextStackVersion} as .NET {currentStackVersion} will reach end-of-life on {FormatDate(eol)} and will no longer be supported. Learn more: {link}"; } public static string GetAfterEolUpdateMessageDotNet(string currentStackVersion, string nextStackVersion, DateTime eol, string link = "") { - return $"Upgrade your app to .NET {nextStackVersion} as .NET {currentStackVersion} has reached EOL on {FormatDate(eol)} and is no longer supported. {link}"; + return $"Upgrade to .NET {nextStackVersion} as .NET {currentStackVersion} has reached end-of-life on {FormatDate(eol)} and is no longer supported. Learn more: {link}"; + } + + public static string GetEarlyEolUpdateMessage(string displayName, string currentStackVersion, string nextStackVersion, DateTime eol, string link = "") + { + return $"Upgrade to {displayName} {nextStackVersion} as {displayName} {currentStackVersion} will reach end-of-life on {FormatDate(eol)} and will no longer be supported. Learn more: {link}"; + } + + public static string GetAfterEolUpdateMessage(string displayName, string currentStackVersion, string nextStackVersion, DateTime eol, string link = "") + { + return $"Upgrade to {displayName} {nextStackVersion} as {displayName} {currentStackVersion} has reached end-of-life on {FormatDate(eol)} and is no longer supported. Learn more: {link}"; } private static string FormatDate(DateTime dateTime) { - return dateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + return dateTime.ToString(CultureInfo.CurrentCulture); } } } diff --git a/src/Cli/func/Helpers/JavaHelper.cs b/src/Cli/func/Helpers/JavaHelper.cs new file mode 100644 index 000000000..9c0752c5f --- /dev/null +++ b/src/Cli/func/Helpers/JavaHelper.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Xml.Linq; + +namespace Azure.Functions.Cli.Helpers +{ + public static class JavaHelper + { + public static string GetJavaVersion(string functionAppRoot) + { + string pomXmlPath = Path.Combine(functionAppRoot, "pom.xml"); + if (File.Exists(pomXmlPath)) + { + var xmlDoc = XDocument.Load(pomXmlPath); + var versionElement = xmlDoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "java.version"); + return versionElement?.Value; + } + + return null; + } + } +} diff --git a/src/Cli/func/Helpers/NodeJSHelpers.cs b/src/Cli/func/Helpers/NodeJSHelpers.cs index 81b581e78..587e4231e 100644 --- a/src/Cli/func/Helpers/NodeJSHelpers.cs +++ b/src/Cli/func/Helpers/NodeJSHelpers.cs @@ -3,6 +3,7 @@ using Azure.Functions.Cli.Common; using Colors.Net; +using Newtonsoft.Json.Linq; using static Azure.Functions.Cli.Common.OutputTheme; namespace Azure.Functions.Cli.Helpers @@ -61,5 +62,25 @@ public static async Task SetupProject(ProgrammingModel programmingModel, string await FileSystemHelpers.WriteFileIfNotExists("tsconfig.json", await StaticResources.TsConfig); } } + + public static string GetNodeVersion(string functionAppRoot) + { + string packageJsonPath = Path.Combine(functionAppRoot, "package.json"); + if (!File.Exists(packageJsonPath)) + { + return null; + } + + var packageJson = JObject.Parse(File.ReadAllText(packageJsonPath)); + + // Check if "engines" field specifies Node.js version + string nodeVersion = packageJson["engines"]?["node"]?.ToString(); + if (!string.IsNullOrEmpty(nodeVersion)) + { + return nodeVersion; + } + + return null; + } } } diff --git a/src/Cli/func/Helpers/PowerShellHelper.cs b/src/Cli/func/Helpers/PowerShellHelper.cs index 612608b0a..97e7d1615 100644 --- a/src/Cli/func/Helpers/PowerShellHelper.cs +++ b/src/Cli/func/Helpers/PowerShellHelper.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Text.Json; using System.Xml; using Azure.Functions.Cli.Common; @@ -95,5 +96,32 @@ await RetryHelper.Retry( return latestMajorVersion; } + + public static string GetPowerShellVersion(string functionAppRoot) + { + // Check environment variable (for Azure) + string runtimeVersion = Environment.GetEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME_VERSION"); + if (!string.IsNullOrEmpty(runtimeVersion)) + { + return runtimeVersion; + } + + // Fallback: Check local.settings.json (for local development) + string settingsPath = Path.Combine(functionAppRoot, "local.settings.json"); + if (File.Exists(settingsPath)) + { + var jsonText = File.ReadAllText(settingsPath); + using (JsonDocument doc = JsonDocument.Parse(jsonText)) + { + if (doc.RootElement.TryGetProperty("Values", out JsonElement values) && + values.TryGetProperty("FUNCTIONS_WORKER_RUNTIME_VERSION", out JsonElement versionElement)) + { + return versionElement.GetString(); + } + } + } + + return null; + } } } diff --git a/src/Cli/func/Helpers/PythonHelpers.cs b/src/Cli/func/Helpers/PythonHelpers.cs index 2afb69abe..790a28c51 100644 --- a/src/Cli/func/Helpers/PythonHelpers.cs +++ b/src/Cli/func/Helpers/PythonHelpers.cs @@ -692,5 +692,11 @@ public static bool HasPySteinFile() { return FileSystemHelpers.FileExists(Path.Combine(Environment.CurrentDirectory, Constants.PySteinFunctionAppPy)); } + + public static async Task GetPythonVersion(string functionAppRoot) + { + var versionInfo = await PythonHelpers.GetVersion(); + return versionInfo?.Version; + } } } diff --git a/src/Cli/func/Helpers/StacksApiHelper.cs b/src/Cli/func/Helpers/StacksApiHelper.cs index 2bb44bcfc..c699704b1 100644 --- a/src/Cli/func/Helpers/StacksApiHelper.cs +++ b/src/Cli/func/Helpers/StacksApiHelper.cs @@ -60,5 +60,100 @@ public static bool IsInNextSixMonths(this DateTime? date) return date < DateTime.Now.AddMonths(6); } } + + public static T GetOtherRuntimeSettings(this FunctionsStacks stacks, string workerRuntime, string runtimeVersion, Func settingsSelector) + { + if (WorkerRuntime.Java.ToString() == workerRuntime) + { + if (runtimeVersion.StartsWith("1.")) + { + runtimeVersion = runtimeVersion.Substring(2); // Removes "1." + } + } + + var languageStack = stacks?.Languages + .FirstOrDefault(x => x.Name.Equals(workerRuntime, StringComparison.InvariantCultureIgnoreCase)); + + var majorVersion = languageStack?.Properties.MajorVersions? + .FirstOrDefault(mv => runtimeVersion.StartsWith(mv.Value, StringComparison.InvariantCultureIgnoreCase)); + + var minorVersion = majorVersion?.MinorVersions? + .FirstOrDefault(mv => runtimeVersion.StartsWith(mv.Value, StringComparison.InvariantCultureIgnoreCase)) + ?? majorVersion?.MinorVersions?.LastOrDefault(); + + return settingsSelector(minorVersion?.StackSettings); + } + + public static (string NextVersion, string DisplayText) GetNextRuntimeVersion( + this FunctionsStacks stacks, + string workerRuntime, + string currentRuntimeVersion, + Func> versionSelector, + bool isNumericVersion = false) // Handle Node.js separately as it has integer versions + { + var runtimeStack = stacks?.Languages + .FirstOrDefault(x => x.Name.Equals(workerRuntime, StringComparison.InvariantCultureIgnoreCase)); + if (runtimeStack?.Properties == null) + { + return (null, null); // No matching runtime found + } + + string displayName = runtimeStack.Properties.DisplayText; + + // Extract and sort supported versions using the provided selector function + var supportedVersions = versionSelector(runtimeStack.Properties)? + .Where(v => !string.IsNullOrEmpty(v)) + .ToList(); + if (supportedVersions == null || supportedVersions.Count == 0) + { + return (null, displayName); // No valid versions found + } + + if (isNumericVersion) + { + // Special case for Node.js: Versions are integers + var numericVersions = supportedVersions + .Select(v => int.TryParse(v, out int version) ? version : (int?)null) + .Where(v => v.HasValue) + .OrderByDescending(v => v) + .ToList(); + if (!int.TryParse(currentRuntimeVersion, out int currentMajorVersion)) + { + return (null, displayName); // Invalid current version + } + + var nextVersion = numericVersions.FirstOrDefault(v => v > currentMajorVersion); + return ((nextVersion ?? numericVersions.First()).ToString(), displayName); + } + else + { + // Standard versioning (Python, Java, PowerShell) + var parsedVersions = supportedVersions + .Where(v => Version.TryParse(v, out _)) + .Select(v => Version.Parse(v)) + .OrderByDescending(v => v) + .ToList(); + if (!Version.TryParse(currentRuntimeVersion, out Version currentVersion)) + { + return (null, displayName); // Invalid current version + } + + var nextVersion = parsedVersions.FirstOrDefault(v => v > currentVersion); + return ((nextVersion ?? parsedVersions.First()).ToString(), displayName); + } + } + + public static bool ExpiresInNextSixMonths(this DateTime? date) + { + if (!date.HasValue) + { + return false; // Null check + } + + DateTime currentDate = DateTime.UtcNow; + DateTime sixMonthsFromNow = currentDate.AddMonths(6); + + return currentDate <= date.Value && date.Value <= sixMonthsFromNow; + } } } diff --git a/test/Azure.Functions.Cli.Tests/TestsToMigrate/PublishActionTests.cs b/test/Azure.Functions.Cli.Tests/TestsToMigrate/PublishActionTests.cs index dfdcce2db..496f991c1 100644 --- a/test/Azure.Functions.Cli.Tests/TestsToMigrate/PublishActionTests.cs +++ b/test/Azure.Functions.Cli.Tests/TestsToMigrate/PublishActionTests.cs @@ -8,6 +8,7 @@ using Azure.Functions.Cli.Arm.Models; using Azure.Functions.Cli.Common; using Azure.Functions.Cli.Helpers; +using Azure.Functions.Cli.StacksApi; using Xunit; using static Azure.Functions.Cli.Actions.AzureActions.PublishFunctionAppAction; @@ -189,6 +190,227 @@ public void ValidateFunctionAppPublish_ThrowException_WhenWorkerRuntimeIsNone() Assert.Equal($"Worker runtime cannot be '{WorkerRuntime.None}'. Please set a valid runtime.", ex.Message); } + private FunctionsStacks GetMockFunctionStacks() + { + return new FunctionsStacks + { + Languages = new List + { + new Language + { + Name = "python", + Properties = new Properties + { + DisplayText = "Python", + MajorVersions = new List + { + new MajorVersion + { + Value = "3", + MinorVersions = new List + { + new MinorVersion + { + Value = "3.8", + StackSettings = new StackSettings + { + LinuxRuntimeSettings = new LinuxRuntimeSettings + { + RuntimeVersion = "Python|3.8" + } + } + }, + new MinorVersion + { + Value = "3.12", + StackSettings = new StackSettings + { + LinuxRuntimeSettings = new LinuxRuntimeSettings + { + RuntimeVersion = "Python|3.12" + } + } + } + } + } + } + } + }, + new Language + { + Name = "node", +Properties = new Properties +{ + DisplayText = "Node.js", + MajorVersions = new List + { + new MajorVersion + { + Value = "14", + MinorVersions = new List + { + new MinorVersion + { + Value = "14.17", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|14.17" + } + } + }, + new MinorVersion + { + Value = "14.20 LTS", // Ensure an LTS version exists + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|14.20 LTS" + } + } + } + } + }, + new MajorVersion + { + Value = "22", + MinorVersions = new List + { + new MinorVersion + { + Value = "22.0", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|22.0" + } + } + }, + new MinorVersion + { + Value = "22.0 LTS", // Ensure an LTS version exists + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|22.0 LTS" + } + } + } + } + } + } +} + }, + new Language +{ + Name = "powershell", + Properties = new Properties + { + DisplayText = "PowerShell", + MajorVersions = new List + { + new MajorVersion + { + Value = "7", + MinorVersions = new List + { + new MinorVersion + { + Value = "7", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "PowerShell|7" + } + } + }, + new MinorVersion + { + Value = "7.2 LTS", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "PowerShell|7.2 LTS" + } + } + } + } + } + } + } +} + } + }; + } + + [Theory] + [InlineData("node", "14", "22")] // Node.js 14 should return next supported 22 + public void GetNextRuntimeNodeVersion_ShouldReturnCorrectVersion(string runtime, string currentVersion, string expectedNextVersion) + { + // Arrange + var stacks = GetMockFunctionStacks(); + // Act + var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, p => p.MajorVersions.Select(mv => mv.Value), isNumericVersion: true); + // Assert + Assert.Equal(expectedNextVersion, nextVersion); + } + + [Theory] + [InlineData("python", "3.8", "3.12")] // Python 3.8 should return next supported 3.12 + public void GetNextRuntimePythonVersion_ShouldReturnCorrectVersion(string runtime, string currentVersion, string expectedNextVersion) + { + // Arrange + var stacks = GetMockFunctionStacks(); + // Act + var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, p => p.MajorVersions.SelectMany(mv => mv.MinorVersions, (major, minor) => minor.Value)); + // Assert + Assert.Equal(expectedNextVersion, nextVersion); + } + + [Theory] + [InlineData("node", "14.17", true)] // Test for a known valid version + [InlineData("node", "14.20 LTS", true)] // Test for an LTS version + public void GetRuntimeSettingsForNode_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull) + { + // Arrange + var stacks = GetMockFunctionStacks(); + // Act + var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.WindowsRuntimeSettings); + // Assert + Assert.Equal(expectedNotNull, settings != null); + } + + [Theory] + [InlineData("python", "3.8", true)] // Python 3.8 should return runtime settings + public void GetRuntimeSettingsForPython_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull) + { + // Arrange + var stacks = GetMockFunctionStacks(); + // Act + var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.LinuxRuntimeSettings); + // Assert + Assert.Equal(expectedNotNull, settings != null); + } + + [Theory] + [InlineData("powershell", "7", true)] // PowerShell 7 should return runtime settings + [InlineData("powershell", "7.2 LTS", true)] // PowerShell 7.2 LTS should return runtime settings + public void GetRuntimeSettingsForPowerShell_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull) + { + // Arrange + var stacks = GetMockFunctionStacks(); + // Act + var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.WindowsRuntimeSettings); + // Assert + Assert.Equal(expectedNotNull, settings != null); + } + private class TestAzureHelperService : AzureHelperService { public Dictionary UpdatedSettings { get; private set; } @@ -204,5 +426,49 @@ public override Task> UpdateWebSettings(Site function return Task.FromResult(new HttpResult(string.Empty)); } } + + [Theory] + [InlineData("node", "22", "22")] // Node.js 22 is highest → should return itself + [InlineData("python", "3.12", "3.12")] // Python 3.12 is highest → should return itself + public void GetNextRuntimeVersion_ShouldReturnCurrentIfNoNewerExists(string runtime, string currentVersion, string expectedVersion) + { + // Arrange + var stacks = GetMockFunctionStacks(); + var selector = runtime == "node" + ? (Func>)(p => p.MajorVersions.Select(mv => mv.Value)) + : p => p.MajorVersions.SelectMany(mv => mv.MinorVersions.Select(minor => minor.Value)); + bool isNumeric = runtime == "node"; + // Act + var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, selector, isNumeric); + // Assert + Assert.Equal(expectedVersion, nextVersion); + } + + [Fact] + public void GetNextRuntimeVersion_ShouldReturnNull_WhenNoVersionsAvailable() + { + // Arrange + var stacks = new FunctionsStacks + { + Languages = new List + { + new Language + { + Name = "java", + Properties = new Properties + { + DisplayText = "Java", + MajorVersions = new List() // No versions + } + } + } + }; + // Act + var (nextVersion, displayName) = stacks.GetNextRuntimeVersion( + "java", "11", p => p.MajorVersions.Select(mv => mv.Value), isNumericVersion: true); + // Assert + Assert.Null(nextVersion); + Assert.Equal("Java", displayName); + } } } diff --git a/test/Cli/Func.Unit.Tests/ActionsTests/PublishActionTests.cs b/test/Cli/Func.Unit.Tests/ActionsTests/PublishActionTests.cs new file mode 100644 index 000000000..fdc38a48a --- /dev/null +++ b/test/Cli/Func.Unit.Tests/ActionsTests/PublishActionTests.cs @@ -0,0 +1,316 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Azure.Functions.Cli.Actions.AzureActions; +using Azure.Functions.Cli.Arm.Models; +using Azure.Functions.Cli.Common; +using Azure.Functions.Cli.Helpers; +using Azure.Functions.Cli.StacksApi; +using Xunit; +using static Azure.Functions.Cli.Actions.AzureActions.PublishFunctionAppAction; + +namespace Azure.Functions.Cli.Unit.Test.ActionsTests +{ + public class PublishActionTests + { + private FunctionsStacks GetMockFunctionStacks() + { + return new FunctionsStacks + { + Languages = new List + { + new Language + { + Name = "python", + Properties = new Properties + { + DisplayText = "Python", + MajorVersions = new List + { + new MajorVersion + { + Value = "3", + MinorVersions = new List + { + new MinorVersion + { + Value = "3.8", + StackSettings = new StackSettings + { + LinuxRuntimeSettings = new LinuxRuntimeSettings + { + RuntimeVersion = "Python|3.8" + } + } + }, + new MinorVersion + { + Value = "3.12", + StackSettings = new StackSettings + { + LinuxRuntimeSettings = new LinuxRuntimeSettings + { + RuntimeVersion = "Python|3.12" + } + } + } + } + } + } + } + }, + new Language + { + Name = "node", + Properties = new Properties +{ + DisplayText = "Node.js", + MajorVersions = new List + { + new MajorVersion + { + Value = "14", + MinorVersions = new List + { + new MinorVersion + { + Value = "14.17", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|14.17" + } + } + }, + new MinorVersion + { + Value = "14.20 LTS", // Ensure an LTS version exists + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|14.20 LTS" + } + } + } + } + }, + new MajorVersion + { + Value = "22", + MinorVersions = new List + { + new MinorVersion + { + Value = "22.0", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|22.0" + } + } + }, + new MinorVersion + { + Value = "22.0 LTS", // Ensure an LTS version exists + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|22.0 LTS" + } + } + } + } + } + } +} + }, + new Language +{ + Name = "powershell", + Properties = new Properties + { + DisplayText = "PowerShell", + MajorVersions = new List + { + new MajorVersion + { + Value = "7", + MinorVersions = new List + { + new MinorVersion + { + Value = "7", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "PowerShell|7" + } + } + }, + new MinorVersion + { + Value = "7.2 LTS", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "PowerShell|7.2 LTS" + } + } + } + } + } + } + } +} + } + }; + } + + [Theory] + [InlineData("node", "14", "22")] // Node.js 14 should return next supported 22 + public void GetNextRuntimeNodeVersion_ShouldReturnCorrectVersion(string runtime, string currentVersion, string expectedNextVersion) + { + // Arrange + var stacks = GetMockFunctionStacks(); + + // Act + var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, p => p.MajorVersions.Select(mv => mv.Value), isNumericVersion: true); + + // Assert + Assert.Equal(expectedNextVersion, nextVersion); + } + + [Theory] + [InlineData("python", "3.8", "3.12")] // Python 3.8 should return next supported 3.12 + public void GetNextRuntimePythonVersion_ShouldReturnCorrectVersion(string runtime, string currentVersion, string expectedNextVersion) + { + // Arrange + var stacks = GetMockFunctionStacks(); + + // Act + var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, p => p.MajorVersions.SelectMany(mv => mv.MinorVersions, (major, minor) => minor.Value)); + + // Assert + Assert.Equal(expectedNextVersion, nextVersion); + } + + [Theory] + [InlineData("node", "14.17", true)] // Test for a known valid version + [InlineData("node", "14.20 LTS", true)] // Test for an LTS version + public void GetRuntimeSettingsForNode_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull) + { + // Arrange + var stacks = GetMockFunctionStacks(); + + // Act + var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.WindowsRuntimeSettings); + + // Assert + Assert.Equal(expectedNotNull, settings != null); + } + + [Theory] + [InlineData("python", "3.8", true)] // Python 3.8 should return runtime settings + public void GetRuntimeSettingsForPython_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull) + { + // Arrange + var stacks = GetMockFunctionStacks(); + + // Act + var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.LinuxRuntimeSettings); + + // Assert + Assert.Equal(expectedNotNull, settings != null); + } + + [Theory] + [InlineData("powershell", "7", true)] // PowerShell 7 should return runtime settings + [InlineData("powershell", "7.2 LTS", true)] // PowerShell 7.2 LTS should return runtime settings + public void GetRuntimeSettingsForPowerShell_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull) + { + // Arrange + var stacks = GetMockFunctionStacks(); + + // Act + var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.WindowsRuntimeSettings); + + // Assert + Assert.Equal(expectedNotNull, settings != null); + } + + [Theory] + [InlineData("node", "22", "22")] // Node.js 22 is highest → should return itself + [InlineData("python", "3.12", "3.12")] // Python 3.12 is highest → should return itself + public void GetNextRuntimeVersion_ShouldReturnCurrentIfNoNewerExists(string runtime, string currentVersion, string expectedVersion) + { + // Arrange + var stacks = GetMockFunctionStacks(); + var selector = runtime == "node" + ? (Func>)(p => p.MajorVersions.Select(mv => mv.Value)) + : p => p.MajorVersions.SelectMany(mv => mv.MinorVersions.Select(minor => minor.Value)); + bool isNumeric = runtime == "node"; + + // Act + var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, selector, isNumeric); + + // Assert + Assert.Equal(expectedVersion, nextVersion); + } + + [Fact] + public void GetNextRuntimeVersion_ShouldReturnNull_WhenNoVersionsAvailable() + { + // Arrange + var stacks = new FunctionsStacks + { + Languages = new List + { + new Language + { + Name = "java", + Properties = new Properties + { + DisplayText = "Java", + MajorVersions = new List() // No versions + } + } + } + }; + + // Act + var (nextVersion, displayName) = stacks.GetNextRuntimeVersion( + "java", "11", p => p.MajorVersions.Select(mv => mv.Value), isNumericVersion: true); + + // Assert + Assert.Null(nextVersion); + Assert.Equal("Java", displayName); + } + + private class TestAzureHelperService : AzureHelperService + { + public TestAzureHelperService() + : base(null, null) + { + UpdatedSettings = new Dictionary(); + } + + public Dictionary UpdatedSettings { get; private set; } + + public override Task> UpdateWebSettings(Site functionApp, Dictionary updatedSettings) + { + UpdatedSettings = updatedSettings; + return Task.FromResult(new HttpResult(string.Empty)); + } + } + } +}