diff --git a/source/Octopus.Tentacle.Tests.Integration/ClientFileTransferRetriesTimeout.cs b/source/Octopus.Tentacle.Tests.Integration/ClientFileTransferRetriesTimeout.cs index 7349aaf1b..0c894e989 100644 --- a/source/Octopus.Tentacle.Tests.Integration/ClientFileTransferRetriesTimeout.cs +++ b/source/Octopus.Tentacle.Tests.Integration/ClientFileTransferRetriesTimeout.cs @@ -77,9 +77,10 @@ public async Task WhenRpcRetriesTimeOut_DuringUploadFile_TheRpcCallIsCancelled(T // Start the script which will wait for a file to exist var duration = Stopwatch.StartNew(); var executeScriptTask = clientAndTentacle.TentacleClient.UploadFile(remotePath, dataStream, CancellationToken, inMemoryLog); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); - Func action = async () => await executeScriptTask; - await action.Should().ThrowAsync(); + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); duration.Stop(); methodUsages.For(nameof(IAsyncClientFileTransferService.UploadFileAsync)).Started.Should().BeGreaterOrEqualTo(2); @@ -123,8 +124,9 @@ public async Task WhenUploadFileFails_AndTakesLongerThanTheRetryDuration_TheCall var dataStream = DataStream.FromString("The Stream"); var executeScriptTask = clientAndTentacle.TentacleClient.UploadFile(remotePath, dataStream, CancellationToken, inMemoryLog); - Func action = async () => await executeScriptTask; - await action.Should().ThrowAsync(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); methodUsages.For(nameof(IAsyncClientFileTransferService.UploadFileAsync)).Started.Should().Be(1); methodUsages.For(nameof(IAsyncClientFileTransferService.DownloadFileAsync)).Started.Should().Be(0); @@ -183,8 +185,10 @@ public async Task WhenRpcRetriesTimeOut_DuringDownloadFile_TheRpcCallIsCancelled var duration = Stopwatch.StartNew(); var executeScriptTask = clientAndTentacle.TentacleClient.DownloadFile(tempFile.File.FullName, CancellationToken, inMemoryLog); - Func> action = async () => await executeScriptTask; - await action.Should().ThrowAsync(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); + duration.Stop(); recordedUsages.For(nameof(IAsyncClientFileTransferService.DownloadFileAsync)).Started.Should().BeGreaterOrEqualTo(2); diff --git a/source/Octopus.Tentacle.Tests.Integration/ClientFileTransfersAreNotRetriedWhenRetriesAreDisabled.cs b/source/Octopus.Tentacle.Tests.Integration/ClientFileTransfersAreNotRetriedWhenRetriesAreDisabled.cs index 0d6e8068b..44f8f8a14 100644 --- a/source/Octopus.Tentacle.Tests.Integration/ClientFileTransfersAreNotRetriedWhenRetriesAreDisabled.cs +++ b/source/Octopus.Tentacle.Tests.Integration/ClientFileTransfersAreNotRetriedWhenRetriesAreDisabled.cs @@ -43,11 +43,11 @@ public async Task FailedUploadsAreNotRetriedAndFail(TentacleConfigurationTestCas .Build(CancellationToken); var remotePath = Path.Combine(clientTentacle.TemporaryDirectory.DirectoryPath, "UploadFile.txt"); - var uploadFileTask = clientTentacle.TentacleClient.UploadFile(remotePath, DataStream.FromString("Hello"), CancellationToken); - Func action = async () => await uploadFileTask; - await action.Should().ThrowAsync(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientTentacle).Build(); + + await AssertionExtensions.Should(async () => await uploadFileTask).ThrowExceptionContractAsync(expectedException); recordedUsages.For(nameof(IAsyncClientFileTransferService.UploadFileAsync)).LastException.Should().NotBeNull(); recordedUsages.For(nameof(IAsyncClientFileTransferService.UploadFileAsync)).Started.Should().Be(1); @@ -84,9 +84,10 @@ public async Task FailedDownloadsAreNotRetriedAndFail(TentacleConfigurationTestC await clientTentacle.TentacleClient.UploadFile(remotePath, DataStream.FromString("Hello"), CancellationToken); var downloadFileTask = clientTentacle.TentacleClient.DownloadFile(remotePath, CancellationToken); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientTentacle).Build(); - Func action = async () => await downloadFileTask; - await action.Should().ThrowAsync(); + await AssertionExtensions.Should(async () => await downloadFileTask).ThrowExceptionContractAsync(expectedException); recordedUsages.For(nameof(IAsyncClientFileTransferService.DownloadFileAsync)).LastException.Should().NotBeNull(); recordedUsages.For(nameof(IAsyncClientFileTransferService.DownloadFileAsync)).Started.Should().Be(1); diff --git a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreDisabled.cs b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreDisabled.cs index 42a1a7231..7a368a3fa 100644 --- a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreDisabled.cs +++ b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreDisabled.cs @@ -85,8 +85,9 @@ public async Task DuringGetCapabilities_ScriptExecutionCanBeCancelled(TentacleCo var (_, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, ensureCancellationOccursDuringAnRpcCall); // ASSERT - // The ExecuteScript operation threw an OperationCancelledException - actualException.Should().BeTaskOrOperationCancelledException(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + actualException.ShouldMatchExceptionContract(expectedException); // If the rpc call could be cancelled then the correct error was recorded var latestException = capabilitiesMethodUsages.For(nameof(IAsyncClientCapabilitiesServiceV2.GetCapabilitiesAsync)).LastException; @@ -185,8 +186,9 @@ public async Task DuringStartScript_ScriptExecutionCanBeCancelled(TentacleConfig var (_, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, ensureCancellationOccursDuringAnRpcCall); // ASSERT - // The ExecuteScript operation threw an OperationCancelledException - actualException.Should().BeTaskOrOperationCancelledException(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + actualException.ShouldMatchExceptionContract(expectedException); // If the rpc call could be cancelled then the correct error was recorded var latestException = recordedUsages.For(nameof(IAsyncClientScriptServiceV2.StartScriptAsync)).LastException; @@ -303,8 +305,9 @@ public async Task DuringGetStatus_ScriptExecutionCanBeCancelled(TentacleConfigur var (_, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, ensureCancellationOccursDuringAnRpcCall); // ASSERT - // The ExecuteScript operation threw an OperationCancelledException - actualException.Should().BeTaskOrOperationCancelledException(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + actualException.ShouldMatchExceptionContract(expectedException); // If the rpc call could be cancelled then the correct error was recorded var latestException = recordedUsages.For(nameof(IAsyncClientScriptServiceV2.GetStatusAsync)).LastException; @@ -384,9 +387,16 @@ public async Task DuringCompleteScript_ScriptExecutionCanBeCancelled(TentacleCon .Build(); // ACT - var (responseAndLogs, _, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, new SemaphoreSlim(Int32.MaxValue, Int32.MaxValue)); + var (_, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, new SemaphoreSlim(int.MaxValue, int.MaxValue)); // ASSERT + // The actual exception may be null if the script completed before the cancellation was observed + if (actualException != null) + { + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + actualException.ShouldMatchExceptionContract(expectedException); + } + // Halibut Errors were recorded on CompleteScript recordedUsages.For(nameof(IAsyncClientScriptServiceV2.CompleteScriptAsync)).LastException?.Should().Match(x => x is HalibutClientException || x is OperationCanceledException || x is TaskCanceledException); // Complete Script was cancelled quickly cancellationDuration.Should().BeLessOrEqualTo(TimeSpan.FromSeconds(30)); diff --git a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreEnabled.cs b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreEnabled.cs index 93a5b3775..be4e4e898 100644 --- a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreEnabled.cs +++ b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreEnabled.cs @@ -98,8 +98,9 @@ public async Task DuringGetCapabilities_ScriptExecutionCanBeCancelled(TentacleCo var (_, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, ensureCancellationOccursDuringAnRpcCall); // ASSERT - // The ExecuteScript operation threw an OperationCancelledException - actualException.Should().BeTaskOrOperationCancelledException(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + actualException.ShouldMatchExceptionContract(expectedException); // If the rpc call could be cancelled then the correct error was recorded var latestException = capabilitiesMethodUsages.For(nameof(IAsyncClientCapabilitiesServiceV2.GetCapabilitiesAsync)).LastException; @@ -235,8 +236,9 @@ public async Task DuringStartScript_ScriptExecutionCanBeCancelled(TentacleConfig var (_, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, ensureCancellationOccursDuringAnRpcCall); // ASSERT - // The ExecuteScript operation threw an OperationCancelledException - actualException.Should().BeTaskOrOperationCancelledException(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + actualException.ShouldMatchExceptionContract(expectedException); // If the rpc call could be cancelled then the correct error was recorded var latestException = recordedUsages.For(nameof(IAsyncClientScriptServiceV2.StartScriptAsync)).LastException; @@ -390,8 +392,9 @@ public async Task DuringGetStatus_ScriptExecutionCanBeCancelled(TentacleConfigur var (_, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, ensureCancellationOccursDuringAnRpcCall); // ASSERT - // The ExecuteScript operation threw an OperationCancelledException - actualException.Should().BeTaskOrOperationCancelledException(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + actualException.ShouldMatchExceptionContract(expectedException); // If the rpc call could be cancelled then the correct error was recorded var latestException = recordedUsages.For(nameof(IAsyncClientScriptServiceV2.GetStatusAsync)).LastException; @@ -483,9 +486,16 @@ public async Task DuringCompleteScript_ScriptExecutionCanBeCancelled(TentacleCon .Build(); // ACT - var (responseAndLogs, _, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, new SemaphoreSlim(int.MaxValue, int.MaxValue)); + var (responseAndLogs, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, new SemaphoreSlim(int.MaxValue, int.MaxValue)); // ASSERT + // The actual exception may be null if the script completed before the cancellation was observed + if (actualException != null) + { + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + actualException.ShouldMatchExceptionContract(expectedException); + } + // Halibut Errors were recorded on CompleteScript recordedUsages.For(nameof(IAsyncClientScriptServiceV2.CompleteScriptAsync)).LastException?.Should().Match(x => x is HalibutClientException || x is OperationCanceledException || x is TaskCanceledException); diff --git a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanRecoverFromNetworkIssues.cs b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanRecoverFromNetworkIssues.cs index 747b10fb2..b71502a31 100644 --- a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanRecoverFromNetworkIssues.cs +++ b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanRecoverFromNetworkIssues.cs @@ -239,7 +239,9 @@ await clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, actualException = ex; } - actualException.Should().NotBeNull().And.BeOfType().And.Match(x => x.Message == "Script execution was cancelled"); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase.TentacleType, clientTentacle).Build(); + + actualException!.ShouldMatchExceptionContract(expectedException); var allLogs = logs.JoinLogs(); allLogs.Should().Contain("hello"); diff --git a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionRetriesTimeout.cs b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionRetriesTimeout.cs index fedabd9a8..60aec7724 100644 --- a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionRetriesTimeout.cs +++ b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionRetriesTimeout.cs @@ -76,10 +76,12 @@ public async Task WhenRpcRetriesTimeOut_DuringGetCapabilities_TheRpcCallIsCancel .Build(); var duration = Stopwatch.StartNew(); - var executeScriptTask = clientAndTentacle.TentacleClient.ExecuteScript(startScriptCommand, CancellationToken, null, inMemoryLog); - Func action = async () => await executeScriptTask; - await action.Should().ThrowAsync(); + var executeScriptTask = clientAndTentacle.TentacleClient.ExecuteScript(startScriptCommand, CancellationToken, null, inMemoryLog); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); + duration.Stop(); capabilitiesMethodUsages.For(nameof(IAsyncClientCapabilitiesServiceV2.GetCapabilitiesAsync)).Started.Should().BeGreaterOrEqualTo(2); @@ -127,8 +129,10 @@ public async Task WhenGetCapabilitiesFails_AndTakesLongerThanTheRetryDuration_Th .Build(); var executeScriptTask = clientAndTentacle.TentacleClient.ExecuteScript(startScriptCommand, CancellationToken, null, inMemoryLog); - Func action = async () => await executeScriptTask; - await action.Should().ThrowAsync(); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); capabilitiesMethodUsages.For(nameof(IAsyncClientCapabilitiesServiceV2.GetCapabilitiesAsync)).Started.Should().Be(1); scriptMethodUsages.For(nameof(IAsyncClientScriptServiceV2.StartScriptAsync)).Started.Should().Be(0, "Test should not have not proceeded past GetCapabilities"); @@ -189,8 +193,11 @@ public async Task WhenRpcRetriesTimeOut_DuringStartScript_TheRpcCallIsCancelled( var duration = Stopwatch.StartNew(); var executeScriptTask = clientAndTentacle.TentacleClient.ExecuteScript(startScriptCommand, CancellationToken, null, inMemoryLog); - Func action = async () => await executeScriptTask; - await action.Should().ThrowAsync(); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); + duration.Stop(); recordedUsages.For(nameof(IAsyncClientScriptServiceV2.StartScriptAsync)).Started.Should().BeGreaterOrEqualTo(2); @@ -238,8 +245,10 @@ public async Task WhenStartScriptFails_AndTakesLongerThanTheRetryDuration_TheCal .Build(); var executeScriptTask = clientAndTentacle.TentacleClient.ExecuteScript(startScriptCommand, CancellationToken, null, inMemoryLog); - Func action = async () => await executeScriptTask; - await action.Should().ThrowAsync(); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); recordedUsages.For(nameof(IAsyncClientScriptServiceV2.StartScriptAsync)).Started.Should().Be(1); recordedUsages.For(nameof(IAsyncClientScriptServiceV2.GetStatusAsync)).Started.Should().Be(0); @@ -304,8 +313,11 @@ public async Task WhenRpcRetriesTimeOut_DuringGetStatus_TheRpcCallIsCancelled(Te var duration = Stopwatch.StartNew(); var executeScriptTask = clientAndTentacle.TentacleClient.ExecuteScript(startScriptCommand, CancellationToken, null, inMemoryLog); - Func action = async () => await executeScriptTask; - await action.Should().ThrowAsync(); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); + duration.Stop(); @@ -356,8 +368,10 @@ public async Task WhenGetStatusFails_AndTakesLongerThanTheRetryDuration_TheCallI .Build(); var executeScriptTask = clientAndTentacle.TentacleClient.ExecuteScript(startScriptCommand, CancellationToken, null, inMemoryLog); - Func action = async () => await executeScriptTask; - await action.Should().ThrowAsync(); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); recordedUsages.For(nameof(IAsyncClientScriptServiceV2.StartScriptAsync)).Started.Should().Be(1); recordedUsages.For(nameof(IAsyncClientScriptServiceV2.GetStatusAsync)).Started.Should().Be(1); @@ -437,7 +451,10 @@ public async Task WhenRpcRetriesTimeOut_DuringCancelScript_TheRpcCallIsCancelled // We cancel script execution via the cancellation token. This should trigger the CancelScript RPC call to be made testCancellationTokenSource.Cancel(); - await action.Should().ThrowAsync(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); + duration.Stop(); recordedUsages.For(nameof(IAsyncClientScriptServiceV2.StartScriptAsync)).Started.Should().Be(1); @@ -501,7 +518,9 @@ public async Task WhenCancelScriptFails_AndTakesLongerThanTheRetryDuration_TheCa // We cancel script execution via the cancellation token. This should trigger the CancelScript RPC call to be made testCancellationTokenSource.Cancel(); - await action.Should().ThrowAsync(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientAndTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); recordedUsages.For(nameof(IAsyncClientScriptServiceV2.StartScriptAsync)).Started.Should().Be(1); recordedUsages.For(nameof(IAsyncClientScriptServiceV2.GetStatusAsync)).Started.Should().BeGreaterOrEqualTo(1); diff --git a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionScriptServiceNonV1IsNotRetriedWhenRetriesAreDisabled.cs b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionScriptServiceNonV1IsNotRetriedWhenRetriesAreDisabled.cs index 92b10cf89..e4dab66e2 100644 --- a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionScriptServiceNonV1IsNotRetriedWhenRetriesAreDisabled.cs +++ b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionScriptServiceNonV1IsNotRetriedWhenRetriesAreDisabled.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using FluentAssertions; -using Halibut; using NUnit.Framework; using Octopus.Tentacle.CommonTestUtils.Builders; using Octopus.Tentacle.Contracts; @@ -54,7 +53,11 @@ public async Task WhenNetworkFailureOccurs_DuringGetCapabilities_TheCallIsNotRet .WithScriptBody(new ScriptBuilder().Print("hello")).Build(); var logs = new List(); - Assert.ThrowsAsync(async () => await clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, CancellationToken)); + var executeScriptTask = clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, CancellationToken); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); var allLogs = logs.JoinLogs(); @@ -104,7 +107,11 @@ public async Task WhenANetworkFailureOccurs_DuringStartScript_TheCallIsNotRetrie .Build(); var logs = new List(); - Assert.ThrowsAsync(async () => await clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, CancellationToken)); + var executeScriptTask = clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, CancellationToken); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); var allLogs = logs.JoinLogs(); @@ -155,10 +162,13 @@ public async Task WhenANetworkFailureOccurs_DuringGetStatus_TheCallIsNotRetried( .Print("AllDone")) .Build(); - List logs = new List(); - Logger.Information("Starting and waiting for script exec"); - Assert.ThrowsAsync(async () => await clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, CancellationToken)); - Logger.Information("Exception thrown."); + var logs = new List(); + + var executeScriptTask = clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, CancellationToken); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); var allLogs = logs.JoinLogs(); @@ -219,8 +229,12 @@ public async Task WhenANetworkFailureOccurs_DuringCancelScript_TheCallIsNotRetri .Print("AllDone")) .Build(); - List logs = new List(); - Assert.ThrowsAsync(async () => await clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, cts.Token)); + var logs = new List(); + var executeScriptTask = clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, cts.Token); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); var allLogs = logs.JoinLogs(); @@ -268,7 +282,7 @@ public async Task WhenANetworkFailureOccurs_DuringCompleteScript_TheCallIsNotRet .Print("AllDone")) .Build(); - List logs = new List(); + var logs = new List(); await clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, CancellationToken); var allLogs = logs.JoinLogs(); diff --git a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionScriptServiceV1IsNotRetried.cs b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionScriptServiceV1IsNotRetried.cs index e0ca00975..21887017b 100644 --- a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionScriptServiceV1IsNotRetried.cs +++ b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionScriptServiceV1IsNotRetried.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using FluentAssertions; -using Halibut; using NUnit.Framework; using Octopus.Tentacle.CommonTestUtils.Builders; using Octopus.Tentacle.Contracts; @@ -55,8 +54,12 @@ public async Task WhenANetworkFailureOccurs_DuringStartScript_WithATentacleThatO .Print("AllDone")) .Build(); - List logs = new List(); - Assert.ThrowsAsync(async () => await clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, CancellationToken)); + var logs = new List(); + var executeScriptTask = clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, CancellationToken); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); // Let the script finish. File.WriteAllText(waitForFile, ""); @@ -109,10 +112,12 @@ public async Task WhenANetworkFailureOccurs_DuringGetStatus_WithATentacleThatOnl .Print("AllDone")) .Build(); - List logs = new List(); - Logger.Information("Starting and waiting for script exec"); - Assert.ThrowsAsync(async () => await clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, CancellationToken)); - Logger.Information("Exception thrown."); + var logs = new List(); + var executeScriptTask = clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, CancellationToken); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); // Let the script finish. File.WriteAllText(waitForFile, ""); @@ -176,7 +181,11 @@ public async Task WhenANetworkFailureOccurs_DuringCancelScript_WithATentacleThat var logs = new List(); - Assert.ThrowsAsync(async () => await clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, cts.Token)); + var executeScriptTask = clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, cts.Token); + + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientTentacle).Build(); + + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); var allLogs = logs.JoinLogs(); allLogs.Should().NotContain("AllDone"); @@ -219,13 +228,15 @@ public async Task WhenANetworkFailureOccurs_DuringCompleteScript_WithATentacleTh var startScriptCommand = new LatestStartScriptCommandBuilder().WithScriptBody(new ScriptBuilder().Print("hello")).Build(); - List logs = new List(); - Assert.ThrowsAsync(async () => await clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, CancellationToken)); + var logs = new List(); + var executeScriptTask = clientTentacle.TentacleClient.ExecuteScript(startScriptCommand, logs, CancellationToken); - // We Can not verify what will be in the logs because of race conditions in tentacle. - // The last complete script which we fail might come back with the logs. + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ConnectionFaulted, tentacleConfigurationTestCase.TentacleType, clientTentacle).Build(); + await AssertionExtensions.Should(async () => await executeScriptTask).ThrowExceptionContractAsync(expectedException); + // We Can not verify what will be in the logs because of race conditions in tentacle. + // The last complete script which we fail might come back with the logs. recordedUsages.For(nameof(IAsyncClientScriptService.CompleteScriptAsync)).LastException.Should().NotBeNull(); recordedUsages.For(nameof(IAsyncClientScriptService.StartScriptAsync)).Started.Should().Be(1); recordedUsages.For(nameof(IAsyncClientScriptService.CompleteScriptAsync)).Started.Should().Be(1); diff --git a/source/Octopus.Tentacle.Tests.Integration/Support/ExceptionContract.cs b/source/Octopus.Tentacle.Tests.Integration/Support/ExceptionContract.cs new file mode 100644 index 000000000..8e8789ce4 --- /dev/null +++ b/source/Octopus.Tentacle.Tests.Integration/Support/ExceptionContract.cs @@ -0,0 +1,22 @@ +using System; + +namespace Octopus.Tentacle.Tests.Integration.Support +{ + public class ExceptionContract + { + public Type[] ExceptionTypes { get; } + public string[] ExceptionMessageShouldContainAny { get; } + + public ExceptionContract(Type exceptionTypes, string[] exceptionMessageShouldContainAny) + { + ExceptionTypes = new[] { exceptionTypes }; + ExceptionMessageShouldContainAny = exceptionMessageShouldContainAny; + } + + public ExceptionContract(Type[] exceptionTypes, string[] exceptionMessageShouldContainAny) + { + ExceptionTypes = exceptionTypes; + ExceptionMessageShouldContainAny = exceptionMessageShouldContainAny; + } + } +} \ No newline at end of file diff --git a/source/Octopus.Tentacle.Tests.Integration/Support/ExceptionContractAssertionBuilder.cs b/source/Octopus.Tentacle.Tests.Integration/Support/ExceptionContractAssertionBuilder.cs new file mode 100644 index 000000000..8e28cc03a --- /dev/null +++ b/source/Octopus.Tentacle.Tests.Integration/Support/ExceptionContractAssertionBuilder.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading.Tasks; +using Halibut; + +namespace Octopus.Tentacle.Tests.Integration.Support +{ + public class ExceptionContractAssertionBuilder + { + readonly FailureScenario failureScenario; + readonly TentacleType tentacleType; + readonly ClientAndTentacle clientAndTentacle; + + public ExceptionContractAssertionBuilder(FailureScenario failureScenario, TentacleType tentacleType, ClientAndTentacle clientAndTentacle) + { + this.failureScenario = failureScenario; + this.tentacleType = tentacleType; + this.clientAndTentacle = clientAndTentacle; + } + + public ExceptionContract Build() + { + if (failureScenario == FailureScenario.ConnectionFaulted) + { + switch (tentacleType) + { + case TentacleType.Listening: + return new ExceptionContract(typeof(HalibutClientException), new[] + { + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', before the request could begin: Transport endpoint is not connected", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', before the request could begin: Connection refused", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', before the request could begin: Broken pipe", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', before the request could begin: Received an unexpected EOF or 0 bytes from the transport stream", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', before the request could begin: Unable to write data to the transport connection: Broken pipe", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', before the request could begin: Unable to read data from the transport connection: Broken pipe", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', before the request could begin: Attempted to read past the end of the stream.", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', before the request could begin: An established connection was aborted by the software in your host machine", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', before the request could begin: Unable to write data to the transport connection: An established connection was aborted by the software in your host machine", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', before the request could begin: Unable to read data from the transport connection: An established connection was aborted by the software in your host machine", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', before the request could begin: An existing connection was forcibly closed by the remote host", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', before the request could begin: Unable to write data to the transport connection: An existing connection was forcibly closed by the remote host", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', before the request could begin: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host", + + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: Broken pipe", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: Received an unexpected EOF or 0 bytes from the transport stream", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: Unable to write data to the transport connection: Broken pipe", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: Unable to read data from the transport connection: Broken pipe", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: Attempted to read past the end of the stream.", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: An established connection was aborted by the software in your host machine", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: Unable to write data to the transport connection: An established connection was aborted by the software in your host machine", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: Unable to read data from the transport connection: An established connection was aborted by the software in your host machine", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: An existing connection was forcibly closed by the remote host", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: Unable to write data to the transport connection: An existing connection was forcibly closed by the remote host", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host" + }); + case TentacleType.Polling: + return new ExceptionContract(typeof(HalibutClientException), new[] + { + "Transport endpoint is not connected", + "Connection refused", + "Broken pipe", + "Received an unexpected EOF or 0 bytes from the transport stream", + "Unable to write data to the transport connection: Broken pipe", + "Unable to read data from the transport connection: Broken pipe", + "Attempted to read past the end of the stream.", + "Unable to write data to the transport connection: An established connection was aborted by the software in your host machine", + "Unable to read data from the transport connection: An established connection was aborted by the software in your host machine", + "Unable to write data to the transport connection: An existing connection was forcibly closed by the remote host", + "Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host" + }); + default: + throw new ArgumentOutOfRangeException(); + } + } + + if (failureScenario == FailureScenario.ScriptExecutionCancelled) + { + return new ExceptionContract( + new[]{ + typeof(OperationCanceledException), + typeof(TaskCanceledException) + }, + new[] + { + "Script execution was cancelled", + "A task was canceled." // Cancellation during StartScript while connecting throws the wrong error + }); + } + + throw new NotSupportedException("Scenario not supported"); + } + } +} \ No newline at end of file diff --git a/source/Octopus.Tentacle.Tests.Integration/Support/ExceptionContractAssertionBuilderExtensionMethods.cs b/source/Octopus.Tentacle.Tests.Integration/Support/ExceptionContractAssertionBuilderExtensionMethods.cs new file mode 100644 index 000000000..b314e5783 --- /dev/null +++ b/source/Octopus.Tentacle.Tests.Integration/Support/ExceptionContractAssertionBuilderExtensionMethods.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Execution; +using FluentAssertions.Specialized; +using Serilog; + +namespace Octopus.Tentacle.Tests.Integration.Support +{ + public static class ExceptionContractAssertionBuilderExtensionMethods + { + public static async Task> ThrowExceptionContractAsync( + this NonGenericAsyncFunctionAssertions should, + ExceptionContract expected, + string because = "", + params object[] becauseArgs) + { + var exceptionAssertions = await should.ThrowAsync(because, becauseArgs); + var exception = exceptionAssertions.And; + + exception.ShouldMatchExceptionContract(expected, because, becauseArgs); + + return exceptionAssertions; + } + + public static async Task> ThrowExceptionContractAsync( + this GenericAsyncFunctionAssertions should, + ExceptionContract expected, + string because = "", + params object[] becauseArgs) + { + var exceptionAssertions = await should.ThrowAsync(because, becauseArgs); + var exception = exceptionAssertions.And; + + exception.ShouldMatchExceptionContract(expected, because, becauseArgs); + + return exceptionAssertions; + } + + public static void ShouldMatchExceptionContract( + this Exception exception, + ExceptionContract expected, + string because = "", + params object[] becauseArgs) + { + exception.Should().NotBeNull(because, becauseArgs); + + using var scope = new AssertionScope(); + + Execute.Assertion + .ForCondition(expected.ExceptionTypes.Any(v => v == exception.GetType())) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context} to be {0}{reason}, but found {1}.", expected.ExceptionTypes.Select(x => x.Name), exception.GetType().Name); + + exception.Message.Should().ContainAny(expected.ExceptionMessageShouldContainAny, because, becauseArgs); + } + } +} diff --git a/source/Octopus.Tentacle.Tests.Integration/Support/FailureScenario.cs b/source/Octopus.Tentacle.Tests.Integration/Support/FailureScenario.cs new file mode 100644 index 000000000..7f3a48494 --- /dev/null +++ b/source/Octopus.Tentacle.Tests.Integration/Support/FailureScenario.cs @@ -0,0 +1,10 @@ +using System; + +namespace Octopus.Tentacle.Tests.Integration.Support +{ + public enum FailureScenario + { + ConnectionFaulted, + ScriptExecutionCancelled + } +} \ No newline at end of file