diff --git a/sdk/Sdk/Targets/Microsoft.Azure.Functions.Worker.Sdk.Publish.ZipDeploy.targets b/sdk/Sdk/Targets/Microsoft.Azure.Functions.Worker.Sdk.Publish.ZipDeploy.targets index cfcf56a08..5dfd90445 100644 --- a/sdk/Sdk/Targets/Microsoft.Azure.Functions.Worker.Sdk.Publish.ZipDeploy.targets +++ b/sdk/Sdk/Targets/Microsoft.Azure.Functions.Worker.Sdk.Publish.ZipDeploy.targets @@ -50,7 +50,8 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and DeploymentPassword="$(Password)" SiteName="$(DeployIisAppPath)" PublishUrl="$(PublishUrl)" - UserAgentVersion="$(ZipDeployUserAgent)"/> + UserAgentVersion="$(ZipDeployUserAgent)" + UseBlobContainerDeploy="$(UseBlobContainerDeploy)"/> \ No newline at end of file diff --git a/sdk/Sdk/Tasks/ZipDeploy/DeployStatus.cs b/sdk/Sdk/Tasks/ZipDeploy/DeployStatus.cs index d48420ec9..43c989743 100644 --- a/sdk/Sdk/Tasks/ZipDeploy/DeployStatus.cs +++ b/sdk/Sdk/Tasks/ZipDeploy/DeployStatus.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. // IMPORTANT: Do not modify this file directly with major changes @@ -14,6 +14,8 @@ public enum DeployStatus Building = 1, Deploying = 2, Failed = 3, - Success = 4 + Success = 4, + Conflict = 5, + PartialSuccess = 6 } } diff --git a/sdk/Sdk/Tasks/ZipDeploy/StringMessages.cs b/sdk/Sdk/Tasks/ZipDeploy/StringMessages.cs index a167048d0..38f981e11 100644 --- a/sdk/Sdk/Tasks/ZipDeploy/StringMessages.cs +++ b/sdk/Sdk/Tasks/ZipDeploy/StringMessages.cs @@ -6,6 +6,7 @@ public static class StringMessages { public const string DeploymentStatus = "Deployment status is {0}."; + public const string DeploymentStatusWithText = "Deployment status is {0}: {1}"; public const string DeploymentStatusPolling = "Polling for deployment status..."; public const string NeitherSiteNameNorPublishUrlGivenError = "Neither SiteName nor PublishUrl was given a value."; public const string PublishingZipViaZipDeploy = "Publishing {0} to {1}..."; diff --git a/sdk/Sdk/Tasks/ZipDeploy/ZipDeployTask.cs b/sdk/Sdk/Tasks/ZipDeploy/ZipDeployTask.cs index 0c1654f9e..28df601f7 100644 --- a/sdk/Sdk/Tasks/ZipDeploy/ZipDeployTask.cs +++ b/sdk/Sdk/Tasks/ZipDeploy/ZipDeployTask.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System; -using System.Diagnostics; using System.IO; using System.Linq; using System.Net; @@ -41,6 +40,7 @@ public class ZipDeployTask : Task public string? PublishUrl { get; set; } + public bool UseBlobContainerDeploy { get; set; } /// /// Our fallback if PublishUrl is not given, which is the case for ZIP Deploy profiles created prior to 15.8 Preview 4. @@ -49,16 +49,21 @@ public class ZipDeployTask : Task public string? SiteName { get; set; } public override bool Execute() - { + { using (DefaultHttpClient client = new DefaultHttpClient()) { - System.Threading.Tasks.Task t = ZipDeployAsync(ZipToPublishPath!, DeploymentUsername!, DeploymentPassword!, PublishUrl, SiteName!, UserAgentVersion!, client, true); + System.Threading.Tasks.Task t = ZipDeployAsync(ZipToPublishPath!, DeploymentUsername!, DeploymentPassword!, PublishUrl, SiteName!, UserAgentVersion!, UseBlobContainerDeploy, client, true); t.Wait(); return t.Result; } } - internal async System.Threading.Tasks.Task ZipDeployAsync(string zipToPublishPath, string userName, string password, string? publishUrl, string siteName, string userAgentVersion, IHttpClient client, bool logMessages) + internal System.Threading.Tasks.Task ZipDeployAsync(string zipToPublishPath, string userName, string password, string publishUrl, string siteName, string userAgentVersion, IHttpClient client, bool logMessages) + { + return ZipDeployAsync(zipToPublishPath, userName, password, publishUrl, siteName, userAgentVersion, useBlobContainerDeploy: false, client, logMessages); + } + + internal async System.Threading.Tasks.Task ZipDeployAsync(string zipToPublishPath, string userName, string password, string? publishUrl, string siteName, string userAgentVersion, bool useBlobContainerDeploy, IHttpClient client, bool logMessages) { if (!File.Exists(zipToPublishPath) || client == null) { @@ -73,11 +78,11 @@ internal async System.Threading.Tasks.Task ZipDeployAsync(string zipToPubl publishUrl += "/"; } - zipDeployPublishUrl = publishUrl + "api/zipdeploy"; + zipDeployPublishUrl = publishUrl + "api"; } else if (!string.IsNullOrEmpty(siteName)) { - zipDeployPublishUrl = $"https://{siteName}.scm.azurewebsites.net/api/zipdeploy"; + zipDeployPublishUrl = $"https://{siteName}.scm.azurewebsites.net/api"; } else { @@ -89,13 +94,18 @@ internal async System.Threading.Tasks.Task ZipDeployAsync(string zipToPubl return false; } + // publish endpoint differs when using a blob storage container + var publishUriPath = useBlobContainerDeploy ? "publish?RemoteBuild=false" : "zipdeploy?isAsync=true"; + + // "/api/zipdeploy?isAsync=true" or "/api/publish?RemoteBuild=false" + zipDeployPublishUrl = $"{zipDeployPublishUrl}/{publishUriPath}"; + if (logMessages) { Log.LogMessage(MessageImportance.High, String.Format(StringMessages.PublishingZipViaZipDeploy, zipToPublishPath, zipDeployPublishUrl)); } - // use the async version of the api - Uri uri = new Uri($"{zipDeployPublishUrl}?isAsync=true", UriKind.Absolute); + Uri uri = new Uri($"{zipDeployPublishUrl}", UriKind.Absolute); string userAgent = $"{UserAgentName}/{userAgentVersion}"; FileStream stream = File.OpenRead(zipToPublishPath); IHttpResponse response = await client.PostRequestAsync(uri, userName, password, "application/zip", userAgent, Encoding.UTF8, stream); @@ -120,12 +130,12 @@ internal async System.Threading.Tasks.Task ZipDeployAsync(string zipToPubl { ZipDeploymentStatus deploymentStatus = new ZipDeploymentStatus(client, userAgent, Log, logMessages); DeployStatus status = await deploymentStatus.PollDeploymentStatusAsync(deploymentUrl, userName, password); - if (status == DeployStatus.Success) + if (status == DeployStatus.Success || status == DeployStatus.PartialSuccess) { Log.LogMessage(MessageImportance.High, StringMessages.ZipDeploymentSucceeded); return true; } - else if (status == DeployStatus.Failed || status == DeployStatus.Unknown) + else if (status == DeployStatus.Failed || status == DeployStatus.Conflict || status == DeployStatus.Unknown) { Log.LogError(String.Format(StringMessages.ZipDeployFailureErrorMessage, zipDeployPublishUrl, status)); return false; diff --git a/sdk/Sdk/Tasks/ZipDeploy/ZipDeploymentStatus.cs b/sdk/Sdk/Tasks/ZipDeploy/ZipDeploymentStatus.cs index 1bb7bf4c5..b96138457 100644 --- a/sdk/Sdk/Tasks/ZipDeploy/ZipDeploymentStatus.cs +++ b/sdk/Sdk/Tasks/ZipDeploy/ZipDeploymentStatus.cs @@ -3,10 +3,8 @@ using System; using System.Collections.Generic; -using System.IO; using System.Net; using System.Net.Http; -using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -43,21 +41,33 @@ public ZipDeploymentStatus(IHttpClient client, string userAgent, TaskLoggingHelp public async Task PollDeploymentStatusAsync(string deploymentUrl, string userName, string password) { - DeployStatus deployStatus = DeployStatus.Pending; + var deployStatus = DeployStatus.Pending; + var deployStatusText = string.Empty; var tokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(MaxMinutesToWait)); if (_logMessages) { _log.LogMessage(StringMessages.DeploymentStatusPolling); } - while (!tokenSource.IsCancellationRequested && deployStatus != DeployStatus.Success && deployStatus != DeployStatus.Failed && deployStatus != DeployStatus.Unknown) + while (!tokenSource.IsCancellationRequested + && deployStatus != DeployStatus.Success + && deployStatus != DeployStatus.PartialSuccess + && deployStatus != DeployStatus.Failed + && deployStatus != DeployStatus.Conflict + && deployStatus != DeployStatus.Unknown) { try { - deployStatus = await GetDeploymentStatusAsync(deploymentUrl, userName, password, RetryCount, TimeSpan.FromSeconds(RetryDelaySeconds), tokenSource); + (deployStatus, deployStatusText) = await GetDeploymentStatusAsync(deploymentUrl, userName, password, RetryCount, TimeSpan.FromSeconds(RetryDelaySeconds), tokenSource); if (_logMessages) { - _log.LogMessage(String.Format(StringMessages.DeploymentStatus, Enum.GetName(typeof(DeployStatus), deployStatus))); + var deployStatusName = Enum.GetName(typeof(DeployStatus), deployStatus); + + var message = string.IsNullOrEmpty(deployStatusText) + ? string.Format(StringMessages.DeploymentStatus, deployStatusName) + : string.Format(StringMessages.DeploymentStatusWithText, deployStatusName, deployStatusText); + + _log.LogMessage(message); } } catch (HttpRequestException) @@ -71,16 +81,29 @@ public async Task PollDeploymentStatusAsync(string deploymentUrl, return deployStatus; } - private async Task GetDeploymentStatusAsync(string deploymentUrl, string userName, string password, int retryCount, TimeSpan retryDelay, CancellationTokenSource cts) + private async Task<(DeployStatus, string)> GetDeploymentStatusAsync(string deploymentUrl, string userName, string password, int retryCount, TimeSpan retryDelay, CancellationTokenSource cts) { + var status = DeployStatus.Unknown; + var statusText = string.Empty; + IDictionary? json = await InvokeGetRequestWithRetryAsync>(deploymentUrl, userName, password, retryCount, retryDelay, cts); - if (json != null && TryParseDeploymentStatus(json, out DeployStatus result)) + if (json is not null) { - return result; + // status + if (TryParseDeploymentStatus(json, out DeployStatus result)) + { + status = result; + } + + // status text message + if (TryParseDeploymentStatusText(json, out string text)) + { + statusText = text; + } } - return DeployStatus.Unknown; + return (status, statusText); } private static bool TryParseDeploymentStatus(IDictionary json, out DeployStatus status) @@ -97,6 +120,20 @@ private static bool TryParseDeploymentStatus(IDictionary json, o return false; } + private static bool TryParseDeploymentStatusText(IDictionary json, out string statusText) + { + statusText = string.Empty; + + if (json.TryGetValue("status_text", out var textObj) + && textObj is not null) + { + statusText = textObj.ToString(); + return true; + } + + return false; + } + private async Task InvokeGetRequestWithRetryAsync(string url, string userName, string password, int retryCount, TimeSpan retryDelay, CancellationTokenSource cts) { IHttpResponse? response = null; diff --git a/sdk/release_notes.md b/sdk/release_notes.md index 848d79253..8b2c498cc 100644 --- a/sdk/release_notes.md +++ b/sdk/release_notes.md @@ -8,6 +8,9 @@ - Fix incorrect function version in build message (#2606) - Fix inner build failures when central package management is enabled (#2689) +- Add support to publish a Function App (Flex Consumption) with `ZipDeploy` (#2712) + - Add `'UseBlobContainerDeploy'` property to identify when to use `OneDeploy` publish API endpoint (`"/api/publish"`) + - Enhance `ZipDeploy` deployment status logging by appending the `'status_message'` (when defined) to the output messages ### Microsoft.Azure.Functions.Worker.Sdk.Generators diff --git a/test/FunctionMetadataGeneratorTests/Resources/TestPublishContents.zip b/test/FunctionMetadataGeneratorTests/Resources/TestPublishContents.zip new file mode 100644 index 000000000..904e149b1 Binary files /dev/null and b/test/FunctionMetadataGeneratorTests/Resources/TestPublishContents.zip differ diff --git a/test/FunctionMetadataGeneratorTests/SdkTests.csproj b/test/FunctionMetadataGeneratorTests/SdkTests.csproj index fd40ace00..da0bc2af8 100644 --- a/test/FunctionMetadataGeneratorTests/SdkTests.csproj +++ b/test/FunctionMetadataGeneratorTests/SdkTests.csproj @@ -44,4 +44,9 @@ + + + Always + + diff --git a/test/FunctionMetadataGeneratorTests/ZipDeployTaskTests.cs b/test/FunctionMetadataGeneratorTests/ZipDeployTaskTests.cs new file mode 100644 index 000000000..e4343e9d7 --- /dev/null +++ b/test/FunctionMetadataGeneratorTests/ZipDeployTaskTests.cs @@ -0,0 +1,170 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.NET.Sdk.Functions.Http; +using Microsoft.NET.Sdk.Functions.Tasks; +using Moq; +using Xunit; + +namespace Microsoft.Azure.Functions.SdkTests +{ + public class ZipDeployTaskTests + { + private static string _testZippedPublishContentsPath; + private const string TestAssemblyToTestZipPath = @"Resources\TestPublishContents.zip"; + private const string UserAgentName = "functions-core-tools"; + private const string UserAgentVersion = "1.0"; + + public static string TestZippedPublishContentsPath + { + get + { + if (_testZippedPublishContentsPath == null) + { + string codebase = typeof(ZipDeployTaskTests).Assembly.Location; + string assemblyPath = new Uri(codebase, UriKind.Absolute).LocalPath; + string baseDirectory = Path.GetDirectoryName(assemblyPath); + _testZippedPublishContentsPath = Path.Combine(baseDirectory, TestAssemblyToTestZipPath); + } + + return _testZippedPublishContentsPath; + } + } + + [Fact] + public async Task ExecuteZipDeploy_InvalidZipFilePath() + { + Mock client = new Mock(); + ZipDeployTask zipDeployer = new ZipDeployTask(); + + bool result = await zipDeployer.ZipDeployAsync(string.Empty, "username", "password", "publishUrl", null, "Foo", false, client.Object, false); + + client.Verify(c => c.PostAsync(It.IsAny(), It.IsAny()), Times.Never); + Assert.False(result); + } + + /// + /// ZipDeploy should use PublishUrl if not null or empty, else use SiteName. + /// + [Theory] + [InlineData("https://sitename.scm.azurewebsites.net", null, false, "https://sitename.scm.azurewebsites.net/api/zipdeploy?isAsync=true")] + [InlineData("https://sitename.scm.azurewebsites.net", null, true, "https://sitename.scm.azurewebsites.net/api/publish?RemoteBuild=false")] + [InlineData("https://sitename.scm.azurewebsites.net", "", false, "https://sitename.scm.azurewebsites.net/api/zipdeploy?isAsync=true")] + [InlineData("https://sitename.scm.azurewebsites.net", "", true, "https://sitename.scm.azurewebsites.net/api/publish?RemoteBuild=false")] + [InlineData("https://sitename.scm.azurewebsites.net", "shouldNotBeUsed", false, "https://sitename.scm.azurewebsites.net/api/zipdeploy?isAsync=true")] + [InlineData("https://sitename.scm.azurewebsites.net", "shouldNotBeUsed", true, "https://sitename.scm.azurewebsites.net/api/publish?RemoteBuild=false")] + [InlineData(null, "sitename", false, "https://sitename.scm.azurewebsites.net/api/zipdeploy?isAsync=true")] + [InlineData(null, "sitename", true, "https://sitename.scm.azurewebsites.net/api/publish?RemoteBuild=false")] + [InlineData("", "sitename", false, "https://sitename.scm.azurewebsites.net/api/zipdeploy?isAsync=true")] + [InlineData("", "sitename", true, "https://sitename.scm.azurewebsites.net/api/publish?RemoteBuild=false")] + public async Task ExecuteZipDeploy_PublishUrlOrSiteNameGiven(string publishUrl, string siteName, bool useBlobContainerDeploy, string expectedZipDeployEndpoint) + { + Action, bool> verifyStep = (client, result) => + { + client.Verify(c => c.PostAsync( + It.Is(uri => string.Equals(uri.AbsoluteUri, expectedZipDeployEndpoint, StringComparison.Ordinal)), + It.Is(streamContent => IsStreamContentEqualToFileContent(streamContent, TestZippedPublishContentsPath))), + Times.Once); + Assert.Equal($"{UserAgentName}/{UserAgentVersion}", client.Object.DefaultRequestHeaders.GetValues("User-Agent").FirstOrDefault()); + Assert.True(result); + }; + + await RunZipDeployAsyncTest(publishUrl, siteName, UserAgentVersion, useBlobContainerDeploy, HttpStatusCode.OK, verifyStep); + } + + [Theory] + [InlineData(null, null)] + [InlineData("", "")] + [InlineData("", null)] + [InlineData(null, "")] + public async Task ExecuteZipDeploy_NeitherPublishUrlNorSiteNameGiven(string publishUrl, string siteName) + { + Action, bool> verifyStep = (client, result) => + { + client.Verify(c => c.PostAsync( + It.IsAny(), + It.IsAny()), + Times.Never); + Assert.False(client.Object.DefaultRequestHeaders.TryGetValues("User-Agent", out _)); + Assert.False(result); + }; + + await RunZipDeployAsyncTest(publishUrl, siteName, UserAgentVersion, useBlobContainerDeploy: false, HttpStatusCode.OK, verifyStep); + } + + [Theory] + [InlineData(HttpStatusCode.OK, false, true)] + [InlineData(HttpStatusCode.OK, true, true)] + [InlineData(HttpStatusCode.Accepted, false, true)] + [InlineData(HttpStatusCode.Accepted, true, true)] + [InlineData(HttpStatusCode.Forbidden, false, false)] + [InlineData(HttpStatusCode.Forbidden, true, false)] + [InlineData(HttpStatusCode.NotFound, false, false)] + [InlineData(HttpStatusCode.NotFound, true, false)] + [InlineData(HttpStatusCode.RequestTimeout, false, false)] + [InlineData(HttpStatusCode.RequestTimeout, true, false)] + [InlineData(HttpStatusCode.InternalServerError, false, false)] + [InlineData(HttpStatusCode.InternalServerError, true, false)] + public async Task ExecuteZipDeploy_VaryingHttpResponseStatuses( + HttpStatusCode responseStatusCode, bool useBlobContainerDeploy, bool expectedResult) + { + var zipDeployPublishUrl = useBlobContainerDeploy + ? "https://sitename.scm.azurewebsites.net/api/publish?RemoteBuild=false" + : "https://sitename.scm.azurewebsites.net/api/zipdeploy?isAsync=true"; + + Action, bool> verifyStep = (client, result) => + { + client.Verify(c => c.PostAsync( + It.Is(uri => string.Equals(uri.AbsoluteUri, zipDeployPublishUrl, StringComparison.Ordinal)), + It.Is(streamContent => IsStreamContentEqualToFileContent(streamContent, TestZippedPublishContentsPath))), + Times.Once); + Assert.Equal($"{UserAgentName}/{UserAgentVersion}", client.Object.DefaultRequestHeaders.GetValues("User-Agent").FirstOrDefault()); + Assert.Equal(expectedResult, result); + }; + + await RunZipDeployAsyncTest("https://sitename.scm.azurewebsites.net", null, UserAgentVersion, useBlobContainerDeploy, responseStatusCode, verifyStep); + } + + private async Task RunZipDeployAsyncTest(string publishUrl, string siteName, string userAgentVersion, bool useBlobContainerDeploy, HttpStatusCode responseStatusCode, Action, bool> verifyStep) + { + Mock client = new Mock(); + + //constructing HttpRequestMessage to get HttpRequestHeaders as HttpRequestHeaders contains no public constructors + HttpRequestMessage requestMessage = new HttpRequestMessage(); + client.Setup(x => x.DefaultRequestHeaders).Returns(requestMessage.Headers); + client.Setup(c => c.PostAsync(It.IsAny(), It.IsAny())).Returns((Uri uri, StreamContent streamContent) => + { + byte[] plainAuthBytes = Encoding.ASCII.GetBytes("username:password"); + string base64AuthParam = Convert.ToBase64String(plainAuthBytes); + + Assert.Equal(base64AuthParam, client.Object.DefaultRequestHeaders.Authorization.Parameter); + Assert.Equal("Basic", client.Object.DefaultRequestHeaders.Authorization.Scheme); + + return Task.FromResult(new HttpResponseMessage(responseStatusCode)); + }); + + Func> runPostAsync = (uri, streamContent) => + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + }; + + ZipDeployTask zipDeployer = new ZipDeployTask(); + + bool result = await zipDeployer.ZipDeployAsync(TestZippedPublishContentsPath, "username", "password", publishUrl, siteName, userAgentVersion, useBlobContainerDeploy, client.Object, false); + + verifyStep(client, result); + } + + private bool IsStreamContentEqualToFileContent(StreamContent streamContent, string filePath) + { + byte[] expectedZipByteArr = File.ReadAllBytes(filePath); + Task t = streamContent.ReadAsByteArrayAsync(); + t.Wait(); + return expectedZipByteArr.SequenceEqual(t.Result); + } + } +} diff --git a/test/FunctionMetadataGeneratorTests/ZipDeploymentStatusTests.cs b/test/FunctionMetadataGeneratorTests/ZipDeploymentStatusTests.cs new file mode 100644 index 000000000..0e0e92cc1 --- /dev/null +++ b/test/FunctionMetadataGeneratorTests/ZipDeploymentStatusTests.cs @@ -0,0 +1,166 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.NET.Sdk.Functions.Http; +using Microsoft.NET.Sdk.Functions.MSBuild.Tasks; +using Moq; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.Azure.Functions.SdkTests +{ + public class ZipDeploymentStatusTests + { + private const string UserAgentName = "functions-core-tools"; + private const string UserAgentVersion = "1.0"; + private const string userName = "deploymentUser"; + private const string password = "deploymentPassword"; + private const string DeploymentResponse = @"{ + ""id"": ""7010fa61-d5df-46b5-a22e-98cfc81f1637"", + ""status"": 3, + ""status_text"": """", + ""author_email"": ""N/A"", + ""author"": ""N/A"", + ""deployer"": ""Push-Deployer"", + ""message"": ""Created via a push deployment"", + ""progress"": """", + ""received_time"": ""2024-09-10T04:40:36.0994691Z"", + ""start_time"": ""2024-09-10T04:40:37.1272389Z"", + ""end_time"": ""2024-09-10T04:40:39.4733696Z"", + ""last_success_end_time"": null, + ""complete"": true, + ""active"": false, + ""is_temp"": false, + ""is_readonly"": true, + ""url"": ""https://testFunctionApp.scm.azurewebsites.net/api/deployments/latest"", + ""log_url"": ""https://testFuncitonApp.scm.azurewebsites.net/api/deployments/latest/log"", + ""site_name"": ""testFunctionApp"", + ""build_summary"": { + ""errors"": [], + ""warnings"": [] + } +}"; + + [Theory] + [InlineData(HttpStatusCode.Forbidden, DeployStatus.Unknown)] + [InlineData(HttpStatusCode.NotFound, DeployStatus.Unknown)] + [InlineData(HttpStatusCode.RequestTimeout, DeployStatus.Unknown)] + [InlineData(HttpStatusCode.InternalServerError, DeployStatus.Unknown)] + public async Task PollDeploymentStatusTest_ForErrorResponses(HttpStatusCode responseStatusCode, DeployStatus expectedDeployStatus) + { + // Arrange + string deployUrl = "https://sitename.scm.azurewebsites.net/DeploymentStatus?Id=knownId"; + Action, bool> verifyStep = (client, result) => + { + client.Verify(c => c.GetAsync( + It.Is(uri => string.Equals(uri.AbsoluteUri, deployUrl, StringComparison.Ordinal)), It.IsAny())); + Assert.Equal($"{UserAgentName}/{UserAgentVersion}", client.Object.DefaultRequestHeaders.GetValues("User-Agent").FirstOrDefault()); + Assert.True(result); + }; + + Mock client = new Mock(); + HttpRequestMessage requestMessage = new HttpRequestMessage(); + client.Setup(x => x.DefaultRequestHeaders).Returns(requestMessage.Headers); + client.Setup(c => c.GetAsync(new Uri(deployUrl, UriKind.RelativeOrAbsolute), It.IsAny())).Returns(() => + { + return Task.FromResult(new HttpResponseMessage(responseStatusCode)); + }); + ZipDeploymentStatus deploymentStatus = new ZipDeploymentStatus(client.Object, $"{UserAgentName}/{UserAgentVersion}", null, false); + + // Act + var actualdeployStatus = await deploymentStatus.PollDeploymentStatusAsync(deployUrl, userName, password); + + // Assert + verifyStep(client, expectedDeployStatus == actualdeployStatus); + } + + [Theory] + [InlineData(HttpStatusCode.OK, "", DeployStatus.Success)] + [InlineData(HttpStatusCode.Accepted, null, DeployStatus.Success)] + [InlineData(HttpStatusCode.OK, "", DeployStatus.PartialSuccess)] + [InlineData(HttpStatusCode.Accepted, "Operation succeeded partially", DeployStatus.PartialSuccess)] + [InlineData(HttpStatusCode.OK, "Instance configuration is not valid", DeployStatus.Failed)] + [InlineData(HttpStatusCode.Accepted, "", DeployStatus.Failed)] + [InlineData(HttpStatusCode.OK, "Conflicting changes exist", DeployStatus.Conflict)] + [InlineData(HttpStatusCode.Accepted, "", DeployStatus.Conflict)] + [InlineData(HttpStatusCode.OK, null, DeployStatus.Unknown)] + [InlineData(HttpStatusCode.Accepted, null, DeployStatus.Unknown)] + public async Task PollDeploymentStatusTest_ForValidResponses(HttpStatusCode responseStatusCode, string statusMessage, DeployStatus expectedDeployStatus) + { + // Arrange + string deployUrl = "https://sitename.scm.azurewebsites.net/DeploymentStatus?Id=knownId"; + Action, bool> verifyStep = (client, result) => + { + client.Verify(c => c.GetAsync( + It.Is(uri => string.Equals(uri.AbsoluteUri, deployUrl, StringComparison.Ordinal)), It.IsAny())); + Assert.Equal($"{UserAgentName}/{UserAgentVersion}", client.Object.DefaultRequestHeaders.GetValues("User-Agent").FirstOrDefault()); + Assert.True(result); + }; + + Mock client = new Mock(); + HttpRequestMessage requestMessage = new HttpRequestMessage(); + client.Setup(x => x.DefaultRequestHeaders).Returns(requestMessage.Headers); + client.Setup(c => c.GetAsync(new Uri(deployUrl, UriKind.RelativeOrAbsolute), It.IsAny())).Returns(() => + { + string statusJson = JsonConvert.SerializeObject(new + { + status = expectedDeployStatus, + status_text = statusMessage + }, Formatting.Indented); + + HttpContent httpContent = new StringContent(statusJson, Encoding.UTF8, "application/json"); + HttpResponseMessage responseMessage = new HttpResponseMessage(responseStatusCode) + { + Content = httpContent + }; + return Task.FromResult(responseMessage); + }); + ZipDeploymentStatus deploymentStatus = new ZipDeploymentStatus(client.Object, $"{UserAgentName}/{UserAgentVersion}", null, false); + + // Act + var actualdeployStatus = await deploymentStatus.PollDeploymentStatusAsync(deployUrl, userName, password); + + // Assert + verifyStep(client, expectedDeployStatus == actualdeployStatus); + } + + [Fact] + public async Task PollDeploymentStatusTest_WithDeploymentSummary_Succeeds() + { + // Arrange + string deployUrl = "https://sitename.scm.azurewebsites.net/DeploymentStatus?Id=knownId"; + Action, DeployStatus> verifyStep = (client, status) => + { + client.Verify(c => c.GetAsync( + It.Is(uri => string.Equals(uri.AbsoluteUri, deployUrl, StringComparison.Ordinal)), It.IsAny())); + Assert.Equal($"{UserAgentName}/{UserAgentVersion}", client.Object.DefaultRequestHeaders.GetValues("User-Agent").FirstOrDefault()); + Assert.Equal(DeployStatus.Failed, status); + }; + + Mock client = new Mock(); + HttpRequestMessage requestMessage = new HttpRequestMessage(); + client.Setup(x => x.DefaultRequestHeaders).Returns(requestMessage.Headers); + client.Setup(c => c.GetAsync(new Uri(deployUrl, UriKind.RelativeOrAbsolute), It.IsAny())).Returns(() => + { + HttpContent httpContent = new StringContent(DeploymentResponse, Encoding.UTF8, "application/json"); + HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = httpContent + }; + return Task.FromResult(responseMessage); + }); + + ZipDeploymentStatus deploymentStatus = new ZipDeploymentStatus(client.Object, $"{UserAgentName}/{UserAgentVersion}", null, false); + + // Act + var actualdeployStatus = await deploymentStatus.PollDeploymentStatusAsync(deployUrl, userName, password); + + // Assert + verifyStep(client, actualdeployStatus); + } + } +}