From 3153cbc13f89c3308677f32ed604bb96f8d13d5e Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Thu, 21 Nov 2024 18:09:09 +0000 Subject: [PATCH] test: add pact verification todo: shutdown app cleanly --- .github/workflows/ProviderPactVerify.yml | 18 ++++ .../PactVerificationTest.cs | 90 +++++++++++++++++++ .../TemporaryAzureFunctionsApplication.cs | 67 ++++++++++++++ .../provider_azure_function_tests.csproj | 33 +++++++ .../src/api.pact.spec.ts | 8 +- 5 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ProviderPactVerify.yml create mode 100644 provider_azure_function_tests/PactVerificationTest.cs create mode 100644 provider_azure_function_tests/TemporaryAzureFunctionsApplication.cs create mode 100644 provider_azure_function_tests/provider_azure_function_tests.csproj diff --git a/.github/workflows/ProviderPactVerify.yml b/.github/workflows/ProviderPactVerify.yml new file mode 100644 index 0000000..11fbbad --- /dev/null +++ b/.github/workflows/ProviderPactVerify.yml @@ -0,0 +1,18 @@ +name: Provider-Pact-Verification +on: + push: + +jobs: + test: + runs-on: ubuntu-latest + env: + application_folder_consumer: smartbearcoin-payments-ui + application_folder_provider_tests: smartbearcoin-payments-ui + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + - run: cd ${{ env.application_folder_consumer }} && npm ci + - run: cd ${{ env.application_folder_consumer }} && npm test + - run: cd ${{ env.application_folder_provider_tests }} && dotnet test + env: + PACT_URL: ../../../../smartbearcoin-payments-ui/pacts/SmartBearCoin-Payments-UI-SmartBearCoin-Payee-Provider.json \ No newline at end of file diff --git a/provider_azure_function_tests/PactVerificationTest.cs b/provider_azure_function_tests/PactVerificationTest.cs new file mode 100644 index 0000000..46d71de --- /dev/null +++ b/provider_azure_function_tests/PactVerificationTest.cs @@ -0,0 +1,90 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using PactNet.Infrastructure.Outputters; +using PactNet.Output.Xunit; +using PactNet.Verifier; +using PactNet; +using Xunit.Abstractions; + +namespace provider_azure_function_tests; + +public class ProviderApiTests : IDisposable +{ + private string _providerUri { get; } + private ITestOutputHelper _outputHelper { get; } + private System.Threading.Tasks.Task _app { get; } + + public ProviderApiTests(ITestOutputHelper output) + { + _outputHelper = output; + _providerUri = "http://localhost:7071/api"; + _app = TemporaryAzureFunctionsApplication.StartNewAsync(new DirectoryInfo("../../../../provider_azure_function")); + } + + [Fact] + public void EnsureProviderApiHonoursPactWithConsumer() + { + + // Wait for the Azure Functions application to start + _app.Wait(15000); + + // Arrange + var config = new PactVerifierConfig + { + + // NOTE: We default to using a ConsoleOutput, + // however xUnit 2 does not capture the console output, + // so a custom outputter is required. + Outputters = new List + { + new XunitOutput(_outputHelper), + new ConsoleOutput() + }, + + // Output verbose verification logs to the test output + LogLevel = PactLogLevel.Information, + }; + + string providerName = "SmartBearCoin-Payee-Provider"; + IPactVerifier pactVerifier = new PactVerifier(providerName, config); + string pactUrl = Environment.GetEnvironmentVariable("PACT_URL"); + + pactVerifier.WithHttpEndpoint(new Uri(_providerUri)) + .WithFileSource(new FileInfo(pactUrl)) + .Verify(); + +// _app.Dispose(); + + } + + #region IDisposable Support + + private bool _disposed = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + Console.WriteLine("SAF IS A BOSS"); + _app.Dispose(); + } + + _disposed = true; + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + + GC.SuppressFinalize(this); + } + + #endregion +} \ No newline at end of file diff --git a/provider_azure_function_tests/TemporaryAzureFunctionsApplication.cs b/provider_azure_function_tests/TemporaryAzureFunctionsApplication.cs new file mode 100644 index 0000000..4b93276 --- /dev/null +++ b/provider_azure_function_tests/TemporaryAzureFunctionsApplication.cs @@ -0,0 +1,67 @@ +using System.Diagnostics; +using Polly; +using Polly.Retry; + +public class TemporaryAzureFunctionsApplication : IAsyncDisposable +{ + private readonly Process _application; + private static readonly HttpClient HttpClient = new HttpClient(); + + private TemporaryAzureFunctionsApplication(Process application) + { + _application = application; + } + + public static async Task StartNewAsync(DirectoryInfo projectDirectory) + { + int port = 7071; + Process app = StartApplication(port, projectDirectory); + await WaitUntilTriggerIsAvailableAsync($"http://localhost:{port}/"); + + return new TemporaryAzureFunctionsApplication(app); + } + + private static Process StartApplication(int port, DirectoryInfo projectDirectory) + { + var appInfo = new ProcessStartInfo("func", $"start --port {port}") + { + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = projectDirectory.FullName + }; + + var app = new Process { StartInfo = appInfo }; + app.Start(); + return app; + } + + private static async Task WaitUntilTriggerIsAvailableAsync(string endpoint) + { + AsyncRetryPolicy retryPolicy = + Policy.Handle() + .WaitAndRetryForeverAsync(index => TimeSpan.FromMilliseconds(500)); + + PolicyResult result = + await Policy.TimeoutAsync(TimeSpan.FromSeconds(30)) + .WrapAsync(retryPolicy) + .ExecuteAndCaptureAsync(() => HttpClient.GetAsync(endpoint)); + + if (result.Outcome == OutcomeType.Failure) + { + throw new InvalidOperationException( + "The Azure Functions project doesn't seem to be running, " + + "please check any build or runtime errors that could occur during startup"); + } + } + + public ValueTask DisposeAsync() + { + if (!_application.HasExited) + { + _application.Kill(entireProcessTree: true); + } + + _application.Dispose(); + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/provider_azure_function_tests/provider_azure_function_tests.csproj b/provider_azure_function_tests/provider_azure_function_tests.csproj new file mode 100644 index 0000000..2bb4c6b --- /dev/null +++ b/provider_azure_function_tests/provider_azure_function_tests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/smartbearcoin-payments-ui/src/api.pact.spec.ts b/smartbearcoin-payments-ui/src/api.pact.spec.ts index 56a8239..4549a00 100644 --- a/smartbearcoin-payments-ui/src/api.pact.spec.ts +++ b/smartbearcoin-payments-ui/src/api.pact.spec.ts @@ -31,7 +31,7 @@ describe('test with pact', () => { .withRequest({ method: 'GET', path: '/payees', - query: { country_of_registration: like('DE'), name: like('test') }, + query: { country_of_registration: like('IE'), name: like('LTD') }, headers: { 'x-Authorization': like('Bearer 1234') } @@ -43,13 +43,13 @@ describe('test with pact', () => { }); return providerWithConsumerA.executeTest((mockserver) => { const client = new API(mockserver.url); - return client.getPayees('DE', 'foo').then((res) => { + return client.getPayees('IE', 'LTD').then((res) => { expect(res).toEqual(expectedPayees); }); }); }); it('should return a particular payee', () => { - const id = '592b4ece-c7a2-46ff-b380-96fd1638852a'; + const id = '1e331a0f-29bd-4b6b-8b21-8b87ed653c6b'; const expectedPayee = { account_name: 'account_name', any_bic: 'VHO7ZKQT', @@ -57,7 +57,7 @@ describe('test with pact', () => { bank_code: 'bank_code', bank_name: 'bank_name', iban: 'IE01AIBK935955939393', - id: '592b4ece-c7a2-46ff-b380-96fd1638852a', + id, name: 'name' }; providerWithConsumerA