From 678278154dd00bc8bb1a3f5bdf8d5c445fd34798 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 13 Sep 2024 14:35:32 +0100 Subject: [PATCH 01/11] test: add pact-net to project /initial Pact test --- .../Explore.Cli.Tests.csproj | 2 + test/Explore.Cli.Tests/PactConsumerTest.cs | 133 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 test/Explore.Cli.Tests/PactConsumerTest.cs diff --git a/test/Explore.Cli.Tests/Explore.Cli.Tests.csproj b/test/Explore.Cli.Tests/Explore.Cli.Tests.csproj index 360aaee..c6058f1 100644 --- a/test/Explore.Cli.Tests/Explore.Cli.Tests.csproj +++ b/test/Explore.Cli.Tests/Explore.Cli.Tests.csproj @@ -20,6 +20,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + diff --git a/test/Explore.Cli.Tests/PactConsumerTest.cs b/test/Explore.Cli.Tests/PactConsumerTest.cs new file mode 100644 index 0000000..468f811 --- /dev/null +++ b/test/Explore.Cli.Tests/PactConsumerTest.cs @@ -0,0 +1,133 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Explore.Cli.Models.Explore; +using Explore.Cli.ExploreHttpClient; +using PactNet; +using PactNet.Output.Xunit; +using Xunit.Abstractions; + +namespace Consumer.Tests +{ + public class ExploreCliTests + { + private readonly IPactBuilderV4 pact; + + public ExploreCliTests(ITestOutputHelper output) + { + var config = new PactConfig + { + PactDir = "../../../pacts/", + Outputters = new[] + { + new XunitOutput(output) + }, + DefaultJsonSettings = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } + }, + LogLevel = PactLogLevel.Debug + }; + + this.pact = Pact.V4("explore-cli", "api-explore-connectionservice", config).WithHttpInteractions(); + } + + [Fact] + public async Task ChecksExploreSpaceExists() + { + var expectedStatusCode = HttpStatusCode.OK; + var spaceName = "new space"; + var spaceContent = new StringContent(JsonSerializer.Serialize(new SpaceRequest() { Name = spaceName }), Encoding.UTF8, "application/json"); + var spaceId = new Guid(); + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + pact + .UponReceiving("a request to check if a space exists") + .Given("a space with name {name} does not exist", new Dictionary { ["name"] = spaceName }) + .WithRequest(HttpMethod.Get, $"/spaces/{spaceId}") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WillRespond() + .WithHeader("Content-Type", "application/hal+json") + .WithStatus(expectedStatusCode); + + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var response = await client.CheckSpaceExists(exploreCookie, spaceId.ToString(), false); + + Assert.True(response); + }); + } + [Fact] + public async Task CreatesANewExploreSpace() + { + var expectedStatusCode = HttpStatusCode.Created; + var expectedId = new Guid(); + var spaceName = "new space"; + var spaceContent = new SpaceRequest() { Name = spaceName }; + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + pact + .UponReceiving("a request to create a new space") + .Given("a space with name {name} does not exist", new Dictionary { ["name"] = spaceName }) + .WithRequest(HttpMethod.Post, "/spaces") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WithJsonBody(spaceContent) + .WillRespond() + .WithStatus(expectedStatusCode) + .WithJsonBody(new SpaceResponse() + { + Id = expectedId, + Name = spaceName, + Links = new Links{ + Self = new(){ + Href = new Uri("http://test"), + }} + }); + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var spaceResponse = await client.CreateSpace(exploreCookie,spaceName); + + Assert.Equal(expectedId, spaceResponse.Id); + }); + } + [Fact] + public async Task HandlesUnknownFailuresWhenCreatingANewExploreSpace() + { + var expectedStatusCode = HttpStatusCode.Conflict; + var spaceName = "new space"; + var spaceContent = new SpaceRequest() { Name = spaceName }; + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + pact + .UponReceiving("a request to create a new space") + .Given("the request will not be processed") + .WithRequest(HttpMethod.Post, "/spaces") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WithJsonBody(spaceContent) + .WillRespond() + .WithStatus(expectedStatusCode); + + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var spaceResponse = await client.CreateSpace(exploreCookie,spaceName); + + Assert.Equal(expectedStatusCode, spaceResponse.StatusCode); + }); + } + } +} \ No newline at end of file From 2895c8768d210fbfa07f4fb6effcc960612ea14f Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 13 Sep 2024 14:36:07 +0100 Subject: [PATCH 02/11] refactor: create ExploreHttpClient.cs --- src/Explore.Cli/ExploreHttpClient.cs | 488 +++++++++++++++++++++++++++ 1 file changed, 488 insertions(+) create mode 100644 src/Explore.Cli/ExploreHttpClient.cs diff --git a/src/Explore.Cli/ExploreHttpClient.cs b/src/Explore.Cli/ExploreHttpClient.cs new file mode 100644 index 0000000..c6f5a05 --- /dev/null +++ b/src/Explore.Cli/ExploreHttpClient.cs @@ -0,0 +1,488 @@ +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using Explore.Cli.Models.Explore; +using System.Threading.Tasks; +using System.CommandLine; +using System.Net; +using System.Net.Http.Json; +using Spectre.Console; +namespace Explore.Cli.ExploreHttpClient; +public class ExploreHttpClient +{ + private readonly HttpClient _httpClient; + + public ExploreHttpClient(string baseAddress = "https://api.explore.swaggerhub.com/spaces-api/v1") + { + _httpClient = new HttpClient { BaseAddress = new Uri(baseAddress) }; + } + + public async Task CheckSpaceExists(string exploreCookie, string? id, bool? verboseOutput) + { + if (string.IsNullOrEmpty(id)) + { + return false; + } + + _httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + _httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + + var spacesResponse = await _httpClient.GetAsync($"/spaces/{id}"); + + if (spacesResponse.StatusCode == HttpStatusCode.OK) + { + if (!UtilityHelper.IsContentTypeExpected(spacesResponse.Content.Headers, "application/hal+json")) + { + Console.WriteLine(spacesResponse); + AnsiConsole.MarkupLine($"[red]Please review your credentials, Unexpected response GET spaces endpoint[/]"); + throw new HttpRequestException("Please review your credentials, Unexpected response GET spaces endpoint"); + } + + return true; + } + + if (verboseOutput != null && verboseOutput == true) + { + AnsiConsole.MarkupLine($"[orange3]StatusCode {spacesResponse.StatusCode} returned from the GetSpaceById API. New space will be created[/]"); + } + + return false; + } + + public async Task CheckApiExists(string exploreCookie, string spaceId, string? id, bool? verboseOutput) + { + if (string.IsNullOrEmpty(id)) + { + return false; + } + + _httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + _httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + + var spacesResponse = await _httpClient.GetAsync($"/spaces/{spaceId}/apis/{id}"); + + if (spacesResponse.StatusCode == HttpStatusCode.OK) + { + return true; + } + + if (verboseOutput != null && verboseOutput == true) + { + AnsiConsole.MarkupLine($"[orange3]StatusCode {spacesResponse.StatusCode} returned from the GetApiById API. New API will be created in the space.[/]"); + } + + return false; + } + + public async Task CheckConnectionExists(string exploreCookie, string spaceId, string apiId, string? id, bool? verboseOutput) + { + if (string.IsNullOrEmpty(id)) + { + return false; + } + + + + _httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + _httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + + var response = await _httpClient.GetAsync($"/spaces/{spaceId}/apis/{apiId}/connections/{id}"); + + if (response.StatusCode == HttpStatusCode.OK) + { + return true; + } + + if (verboseOutput != null && verboseOutput == true) + { + AnsiConsole.MarkupLine($"[orange3]StatusCode {response.StatusCode} returned from the GetConnectionById API. New connection within API will be created.[/]"); + } + + return false; + } + + public async Task UpsertSpace(string exploreCookie, bool spaceExists, string? name, string? id) + { + + + var spaceContent = new StringContent(JsonSerializer.Serialize( + new SpaceRequest() + { + Name = name + } + ), Encoding.UTF8, "application/json"); + + _httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + _httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + + HttpResponseMessage? spacesResponse; + + if (string.IsNullOrEmpty(id) || !spaceExists) + { + spacesResponse = await _httpClient.PostAsync("/spaces", spaceContent); + } + else + { + spacesResponse = await _httpClient.PutAsync($"/spaces/{id}", spaceContent); + + if (spacesResponse.StatusCode == HttpStatusCode.Conflict) + { + // swallow 409 as server is being overly strict + return new SpaceResponse() { Id = Guid.Parse(id), Name = name }; + } + } + + if (spacesResponse.IsSuccessStatusCode) + { + return await spacesResponse.Content.ReadFromJsonAsync() ?? new SpaceResponse(); + } + + if (!UtilityHelper.IsContentTypeExpected(spacesResponse.Content.Headers, "application/hal+json") && !UtilityHelper.IsContentTypeExpected(spacesResponse.Content.Headers, "application/json")) + { + Console.WriteLine(spacesResponse); + AnsiConsole.MarkupLine($"[red]Please review your credentials, Unexpected response from POST/PUT spaces API for name: {name}, id:{id}[/]"); + } + else + { + AnsiConsole.MarkupLine($"[red]StatusCode {spacesResponse.StatusCode} returned from the POST/PUT spaces API for name: {name}, id:{id}[/]"); + } + + return new SpaceResponse(); + } + + public async Task UpsertApi(string exploreCookie, bool spaceExists, string spaceId, string? id, string? name, string? type, bool? verboseOutput) + { + + + var apiContent = new StringContent(JsonSerializer.Serialize( + new ApiRequest() + { + Name = name, + Type = type + } + ), Encoding.UTF8, "application/json"); + + _httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + _httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + + HttpResponseMessage? apiResponse; + + if (spaceExists && await CheckApiExists(exploreCookie, spaceId, id, verboseOutput)) + { + // update the api + apiResponse = await _httpClient.PutAsync($"/spaces/{spaceId}/apis/{id}", apiContent); + + if (apiResponse.StatusCode == HttpStatusCode.Conflict) + { + // swallow 409 as server is being overly strict + return new ApiResponse() { Id = Guid.Parse(id ?? string.Empty), Name = name, Type = type }; + } + } + else + { + //create the api + apiResponse = await _httpClient.PostAsync($"/spaces/{spaceId}/apis", apiContent); + } + + if (apiResponse.IsSuccessStatusCode) + { + return await apiResponse.Content.ReadFromJsonAsync() ?? new ApiResponse(); + } + + if (!UtilityHelper.IsContentTypeExpected(apiResponse.Content.Headers, "application/hal+json") && !UtilityHelper.IsContentTypeExpected(apiResponse.Content.Headers, "application/json")) + { + AnsiConsole.MarkupLine($"[red]Please review your credentials, Unexpected response from POST/PUT spaces API for name: {name}, id:{id}[/]"); + } + else + { + AnsiConsole.WriteLine($"[red]StatusCode {apiResponse.StatusCode} returned from the POST spaces/{{id}}/apis for {name}[/]"); + } + + return new ApiResponse(); + } + + public async Task UpsertConnection(string exploreCookie, bool spaceExists, string spaceId, string apiId, string? connectionId, Connection? connection, bool? verboseOutput) + { + + + _httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + _httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + + var connectionContent = new StringContent(JsonSerializer.Serialize(MappingHelper.MassageConnectionExportForImport(connection)), Encoding.UTF8, "application/json"); + + HttpResponseMessage? connectionResponse; + + if (spaceExists && await CheckConnectionExists(exploreCookie, spaceId, apiId, connectionId, verboseOutput)) + { + connectionResponse = await _httpClient.PutAsync($"/spaces/{spaceId}/apis/{apiId}/connections/{connectionId}", connectionContent); + } + else + { + connectionResponse = await _httpClient.PostAsync($"/spaces/{spaceId}/apis/{apiId}/connections", connectionContent); + } + + if (connectionResponse.IsSuccessStatusCode) + { + return true; + } + else + { + if (!UtilityHelper.IsContentTypeExpected(connectionResponse.Content.Headers, "application/hal+json") && !UtilityHelper.IsContentTypeExpected(connectionResponse.Content.Headers, "application/json")) + { + AnsiConsole.MarkupLine($"[red]Please review your credentials, Unexpected response from the connections API for api: {apiId} and {connection?.Name}[/]"); + } + else + { + AnsiConsole.WriteLine($"[red]StatusCode {connectionResponse.StatusCode} returned from the connections API for api: {apiId} and {connection?.Name}[/]"); + + var message = await connectionResponse.Content.ReadAsStringAsync(); + AnsiConsole.WriteLine($"error: {message}"); + } + } + return false; + } + + public class CreateSpaceResult + { + public bool Result { get; set; } + public Guid? Id { get; set; } + public string? Reason { get; set; } + public System.Net.HttpStatusCode StatusCode { get; set; } + } + + public async Task CreateSpace(string exploreCookie, string spaceName) + { + var cleanedCollectionName = UtilityHelper.CleanString(spaceName); + var spaceContent = new StringContent(JsonSerializer.Serialize(new SpaceRequest() { Name = cleanedCollectionName }), Encoding.UTF8, "application/json"); + + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + _httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + var spacesResponse = await _httpClient.PostAsync("/spaces", spaceContent); + var spaceResponse = spacesResponse.Content.ReadFromJsonAsync(); + switch (spacesResponse.StatusCode) + { + case HttpStatusCode.Created: + var spaceId = spaceResponse.Result?.Id; + return new CreateSpaceResult + { + Id = new Guid(), + Result = true, + StatusCode = spacesResponse.StatusCode + }; + case HttpStatusCode.OK: + return new CreateSpaceResult + { + Reason = "AUTH_REQUIRED", + Result = false, + StatusCode = spacesResponse.StatusCode + }; + case HttpStatusCode.Conflict: + return new CreateSpaceResult + { + Reason = "SPACE_CONFLICT", + Result = false, + StatusCode = spacesResponse.StatusCode + }; + + default: + return new CreateSpaceResult + { + Reason = spacesResponse.ReasonPhrase, + Result = false, + StatusCode = spacesResponse.StatusCode + }; + } + } + + public class CreateApiEntryResult + { + public bool Result { get; set; } + public Guid? Id { get; set; } + public string? Reason { get; set; } + public System.Net.HttpStatusCode StatusCode { get; set; } + } + + public async Task CreateApiEntry(string exploreCookie, Guid? spaceId, string apiName, string importer, string? description) + { + + var cleanedAPIName = UtilityHelper.CleanString(apiName); + var apiContent = new StringContent(JsonSerializer.Serialize(new ApiRequest() + { + Name = cleanedAPIName, + Type = "REST", + Description = $"imported from {importer} on {DateTime.UtcNow.ToShortDateString()}\n{description}" + }), Encoding.UTF8, "application/json"); + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + _httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + var apiResponse = await _httpClient.PostAsync($"/spaces/{spaceId}/apis", apiContent); + switch (apiResponse.StatusCode) + { + case HttpStatusCode.Created: + var createdApiResponse = apiResponse.Content.ReadFromJsonAsync(); + var createdApiResponseId = createdApiResponse.Result?.Id; + return new CreateApiEntryResult + { + Result = true, + Id = createdApiResponseId, + StatusCode = apiResponse.StatusCode + }; + + default: + return new CreateApiEntryResult + { + Reason = apiResponse.StatusCode.ToString(), + Result = false, + StatusCode = apiResponse.StatusCode + }; + } + } + public class CreateApiConnectionResult + { + public bool Result { get; set; } + public Guid? Id { get; set; } + public string? Reason { get; set; } + public System.Net.HttpStatusCode StatusCode { get; set; } + } + + public async Task CreateApiConnection(string exploreCookie, Guid? spaceId, Guid? apiId, string connectionRequestBody) + { + var connectionContent = new StringContent(connectionRequestBody, Encoding.UTF8, "application/json"); + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + _httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + var connectionResponse = await _httpClient.PostAsync($"/spaces/{spaceId}/apis/{apiId}/connections", connectionContent); + + switch (connectionResponse.StatusCode) + { + case HttpStatusCode.Created: + return new CreateApiConnectionResult + { + Result = true, + StatusCode = connectionResponse.StatusCode + }; + + default: + return new CreateApiConnectionResult + { + Reason = "Connection NOT created", + Result = false, + StatusCode = connectionResponse.StatusCode + }; + } + } + + public class GetSpacesResult + { + public bool Result { get; set; } + public PagedSpaces? Spaces { get; set; } + public string? Reason { get; set; } + public System.Net.HttpStatusCode StatusCode { get; set; } + + } + + public async Task GetSpaces(string exploreCookie) + { + + var spacesResponse = await _httpClient.GetAsync("/spaces?page=0&size=2000"); + if (spacesResponse.StatusCode == HttpStatusCode.OK) + { + if (!UtilityHelper.IsContentTypeExpected(spacesResponse.Content.Headers, "application/hal+json")) + { + Console.WriteLine(spacesResponse); + AnsiConsole.MarkupLine($"[red]Please review your credentials, Unexpected response GET spaces endpoint[/]"); + return new GetSpacesResult + { + Result = false, + Reason = "AUTH_REQUIRED", + StatusCode = spacesResponse.StatusCode + }; + } + var spaces = await spacesResponse.Content.ReadFromJsonAsync(); + return new GetSpacesResult + { + Result = true, + Spaces = spaces, + StatusCode = spacesResponse.StatusCode + }; + } + else + { + return new GetSpacesResult + { + Result = false, + Reason = spacesResponse.ReasonPhrase, + StatusCode = spacesResponse.StatusCode + }; + } + } + public class GetSpaceApisResult + { + public bool Result { get; set; } + public PagedApis? Apis { get; set; } + public string? Reason { get; set; } + public System.Net.HttpStatusCode StatusCode { get; set; } + + } + + public async Task GetSpaceApis(string exploreCookie, Guid? spaceId) + { + + var apisResponse = await _httpClient.GetAsync($"/spaces/{spaceId}/apis?page=0&size=2000"); + if (apisResponse.StatusCode == HttpStatusCode.OK) + { + var apis = await apisResponse.Content.ReadFromJsonAsync(); + return new GetSpaceApisResult + { + Result = true, + Apis = apis, + StatusCode = apisResponse.StatusCode + }; + } + else + { + return new GetSpaceApisResult + { + Result = false, + Reason = apisResponse.ReasonPhrase, + StatusCode = apisResponse.StatusCode + }; + } + } + public class GetApiConnectionsForSpaceResult + { + public bool Result { get; set; } + public PagedConnections? Connections { get; set; } + public string? Reason { get; set; } + public System.Net.HttpStatusCode StatusCode { get; set; } + + } + + public async Task GetApiConnectionsForSpace(string exploreCookie, Guid? spaceId, Guid? apiId) + { + + var connectionsResponse = await _httpClient.GetAsync($"/spaces/{spaceId}/apis/{apiId}/connections?page=0&size=2000"); + if (connectionsResponse.StatusCode == HttpStatusCode.OK) + { + var connections = await connectionsResponse.Content.ReadFromJsonAsync(); + return new GetApiConnectionsForSpaceResult + { + Result = true, + Connections = connections, + StatusCode = connectionsResponse.StatusCode + }; + } + else + { + return new GetApiConnectionsForSpaceResult + { + Result = false, + Reason = connectionsResponse.ReasonPhrase, + StatusCode = connectionsResponse.StatusCode + }; + } + } + +} From 3cb38f517e1b65f616acdf05c11630fbed8976df Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 13 Sep 2024 14:36:29 +0100 Subject: [PATCH 03/11] chore(test): .gitignore generated pact files --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4799469..bf01888 100644 --- a/.gitignore +++ b/.gitignore @@ -398,4 +398,5 @@ FodyWeavers.xsd *.sln.iml # distribution artifacts -/dist \ No newline at end of file +/dist +pacts/ \ No newline at end of file From 18a7e233c4a356dba6ddc820513fcb52bfb135d6 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 13 Sep 2024 14:37:29 +0100 Subject: [PATCH 04/11] fix: remove links.apis field - undefined in OAD --- src/Explore.Cli/Models/Explore/ExploreContracts.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Explore.Cli/Models/Explore/ExploreContracts.cs b/src/Explore.Cli/Models/Explore/ExploreContracts.cs index 90e4493..1727ec3 100644 --- a/src/Explore.Cli/Models/Explore/ExploreContracts.cs +++ b/src/Explore.Cli/Models/Explore/ExploreContracts.cs @@ -252,8 +252,6 @@ public partial class Links [JsonPropertyName("self")] public Apis? Self { get; set; } - [JsonPropertyName("apis")] - public Apis? Apis { get; set; } } public partial class Apis From 5264fbe6787ccebfb2959fb75930a49f61562e7f Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 13 Sep 2024 14:46:17 +0100 Subject: [PATCH 05/11] ci: add pact to ci pipeline --- .github/workflows/build-test-cross.yml | 16 +++++++++++++++- .github/workflows/build-test-package.yml | 13 +++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-test-cross.yml b/.github/workflows/build-test-cross.yml index 8f7591f..873bc8d 100644 --- a/.github/workflows/build-test-cross.yml +++ b/.github/workflows/build-test-cross.yml @@ -122,6 +122,13 @@ jobs: with: useConfigFile: true configFilePath: ./.github/gitversion.yml + + - uses: pactflow/actions/can-i-deploy@auto_detect_version_branch + with: + to_environment: production + application_name: explore-cli + broker_url: ${{ secrets.PACT_BROKER_BASE_URL }} + token: ${{ secrets.PACT_BROKER_TOKEN }} - name: Download artifacts uses: actions/download-artifact@v4 @@ -131,4 +138,11 @@ jobs: repo_token: ${{ secrets.GITHUB_TOKEN }} file: artifact/* file_glob: true - tag: ${{ steps.gitversion.outputs.MajorMinorPatch }} \ No newline at end of file + tag: ${{ steps.gitversion.outputs.MajorMinorPatch }} + + - uses: pactflow/actions/record-release@auto_detect_version_branch + with: + environment: production + application_name: explore-cli + PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }} + PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/build-test-package.yml b/.github/workflows/build-test-package.yml index 43bddbd..011ff3e 100644 --- a/.github/workflows/build-test-package.yml +++ b/.github/workflows/build-test-package.yml @@ -65,6 +65,19 @@ jobs: dotnet test working-directory: test/Explore.Cli.Tests + - uses: pactflow/actions/publish-pact-files@auto_detect_version_branch + with: + pactfiles: test/Explore.Cli.Tests/pacts + broker_url: ${{ secrets.PACT_BROKER_BASE_URL }} + token: ${{ secrets.PACT_BROKER_TOKEN }} + + - uses: pactflow/actions/can-i-deploy@auto_detect_version_branch + with: + to_environment: production + application_name: explore-cli + broker_url: ${{ secrets.PACT_BROKER_BASE_URL }} + token: ${{ secrets.PACT_BROKER_TOKEN }} + - name: Create Package run: dotnet pack --configuration $BUILD_CONFIG -o:package /p:PackageVersion=${{ steps.gitVersion.outputs.assemblySemVer }} working-directory: src/Explore.Cli From 73e7bc4a15e04384d4719135427cc05cf4e6903f Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 13 Sep 2024 15:16:28 +0100 Subject: [PATCH 06/11] deps(test): update pact-net to 5.0.0-beta.3 --- test/Explore.Cli.Tests/Explore.Cli.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Explore.Cli.Tests/Explore.Cli.Tests.csproj b/test/Explore.Cli.Tests/Explore.Cli.Tests.csproj index c6058f1..8cd6103 100644 --- a/test/Explore.Cli.Tests/Explore.Cli.Tests.csproj +++ b/test/Explore.Cli.Tests/Explore.Cli.Tests.csproj @@ -20,7 +20,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + From 56f9e47c469e74fa0b0b6a8e9788a91ceb0c79e0 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 13 Sep 2024 15:27:20 +0100 Subject: [PATCH 07/11] docs: add pact can-i-deploy readme badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 618694b..2ff0051 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # explore-cli + Simple utility CLI for importing data into SwaggerHub Explore. +![Can I Deploy](https://smartbear.pactflow.io/pacticipants/explore-cli/branches/main/latest-version/can-i-deploy/to-environment/production/badge) ``` _____ _ ____ _ _ From a185bae8c895b313d068214a6c8fe4c1843c8658 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 13 Sep 2024 17:22:42 +0100 Subject: [PATCH 08/11] chore: use updated pactflow v2 actions --- .github/workflows/build-test-cross.yml | 8 ++++---- .github/workflows/build-test-package.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test-cross.yml b/.github/workflows/build-test-cross.yml index 873bc8d..6bb2ddb 100644 --- a/.github/workflows/build-test-cross.yml +++ b/.github/workflows/build-test-cross.yml @@ -123,7 +123,7 @@ jobs: useConfigFile: true configFilePath: ./.github/gitversion.yml - - uses: pactflow/actions/can-i-deploy@auto_detect_version_branch + - uses: pactflow/actions/can-i-deploy@v2 with: to_environment: production application_name: explore-cli @@ -140,9 +140,9 @@ jobs: file_glob: true tag: ${{ steps.gitversion.outputs.MajorMinorPatch }} - - uses: pactflow/actions/record-release@auto_detect_version_branch + - uses: pactflow/actions/record-release@v2 with: environment: production application_name: explore-cli - PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }} - PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }} \ No newline at end of file + broker_url: ${{ secrets.PACT_BROKER_BASE_URL }} + token: ${{ secrets.PACT_BROKER_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/build-test-package.yml b/.github/workflows/build-test-package.yml index 011ff3e..8b55b0d 100644 --- a/.github/workflows/build-test-package.yml +++ b/.github/workflows/build-test-package.yml @@ -65,13 +65,13 @@ jobs: dotnet test working-directory: test/Explore.Cli.Tests - - uses: pactflow/actions/publish-pact-files@auto_detect_version_branch + - uses: pactflow/actions/publish-pact-files@v2 with: pactfiles: test/Explore.Cli.Tests/pacts broker_url: ${{ secrets.PACT_BROKER_BASE_URL }} token: ${{ secrets.PACT_BROKER_TOKEN }} - - uses: pactflow/actions/can-i-deploy@auto_detect_version_branch + - uses: pactflow/actions/can-i-deploy@v2 with: to_environment: production application_name: explore-cli From e5705096ede6343943126d0796dfe45d1109d3e0 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Tue, 17 Sep 2024 19:10:33 +0100 Subject: [PATCH 09/11] test: cover mover ExploreHttpClient cases note: remaining - UpsertSpace / UpsertApi / UpsertConnection modified OAD for BDCT comparison. Updated included --- src/Explore.Cli/ExploreHttpClient.cs | 11 +- .../Models/Explore/ExploreContracts.cs | 19 +- test/Explore.Cli.Tests/PactConsumerTest.cs | 441 +++++- test/Explore.Cli.Tests/explore.yaml | 1385 +++++++++++++++++ 4 files changed, 1841 insertions(+), 15 deletions(-) create mode 100644 test/Explore.Cli.Tests/explore.yaml diff --git a/src/Explore.Cli/ExploreHttpClient.cs b/src/Explore.Cli/ExploreHttpClient.cs index c6f5a05..a3b4d31 100644 --- a/src/Explore.Cli/ExploreHttpClient.cs +++ b/src/Explore.Cli/ExploreHttpClient.cs @@ -386,6 +386,9 @@ public class GetSpacesResult public async Task GetSpaces(string exploreCookie) { + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + _httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); var spacesResponse = await _httpClient.GetAsync("/spaces?page=0&size=2000"); if (spacesResponse.StatusCode == HttpStatusCode.OK) { @@ -429,7 +432,9 @@ public class GetSpaceApisResult public async Task GetSpaceApis(string exploreCookie, Guid? spaceId) { - + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + _httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); var apisResponse = await _httpClient.GetAsync($"/spaces/{spaceId}/apis?page=0&size=2000"); if (apisResponse.StatusCode == HttpStatusCode.OK) { @@ -462,7 +467,9 @@ public class GetApiConnectionsForSpaceResult public async Task GetApiConnectionsForSpace(string exploreCookie, Guid? spaceId, Guid? apiId) { - + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + _httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); var connectionsResponse = await _httpClient.GetAsync($"/spaces/{spaceId}/apis/{apiId}/connections?page=0&size=2000"); if (connectionsResponse.StatusCode == HttpStatusCode.OK) { diff --git a/src/Explore.Cli/Models/Explore/ExploreContracts.cs b/src/Explore.Cli/Models/Explore/ExploreContracts.cs index 1727ec3..7df2801 100644 --- a/src/Explore.Cli/Models/Explore/ExploreContracts.cs +++ b/src/Explore.Cli/Models/Explore/ExploreContracts.cs @@ -13,12 +13,15 @@ public partial class Transaction public partial class Connection { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("id")] public string? Id { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("type")] public string? Type { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("name")] public string? Name { get; set; } @@ -31,12 +34,17 @@ public partial class Connection [JsonPropertyName("connectionDefinition")] public ConnectionDefinition? ConnectionDefinition { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("paths")] public Dictionary? Paths {get; set;} + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("settings")] public Settings? Settings { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("credentials")] public Credentials? Credentials { get; set; } } @@ -143,12 +151,15 @@ public partial class Credentials public partial class ConnectionDefinition { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("openapi")] public string? OpenApi { get; set; } - + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("servers")] public List? Servers { get; set; } - + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("paths")] public Dictionary? Paths { get; set; } } @@ -252,6 +263,10 @@ public partial class Links [JsonPropertyName("self")] public Apis? Self { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("apis")] + public Apis? Apis { get; set; } + } public partial class Apis diff --git a/test/Explore.Cli.Tests/PactConsumerTest.cs b/test/Explore.Cli.Tests/PactConsumerTest.cs index 468f811..5e326a6 100644 --- a/test/Explore.Cli.Tests/PactConsumerTest.cs +++ b/test/Explore.Cli.Tests/PactConsumerTest.cs @@ -7,6 +7,8 @@ using PactNet; using PactNet.Output.Xunit; using Xunit.Abstractions; +using static Explore.Cli.ExploreHttpClient.ExploreHttpClient; +using System.Text.Json.Nodes; namespace Consumer.Tests { @@ -65,7 +67,7 @@ await pact.VerifyAsync(async ctx => }); } [Fact] - public async Task CreatesANewExploreSpace() + public async Task CreateSpace() { var expectedStatusCode = HttpStatusCode.Created; var expectedId = new Guid(); @@ -82,21 +84,24 @@ public async Task CreatesANewExploreSpace() .WithJsonBody(spaceContent) .WillRespond() .WithStatus(expectedStatusCode) - .WithJsonBody(new SpaceResponse() - { - Id = expectedId, + .WithJsonBody(new SpaceResponse() + { + Id = expectedId, Name = spaceName, - Links = new Links{ - Self = new(){ - Href = new Uri("http://test"), - }} - }); + Links = new Links + { + Self = new() + { + Href = new Uri("http://test"), + } + } + }); await pact.VerifyAsync(async ctx => { var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); - var spaceResponse = await client.CreateSpace(exploreCookie,spaceName); + var spaceResponse = await client.CreateSpace(exploreCookie, spaceName); Assert.Equal(expectedId, spaceResponse.Id); }); @@ -124,10 +129,424 @@ await pact.VerifyAsync(async ctx => { var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); - var spaceResponse = await client.CreateSpace(exploreCookie,spaceName); + var spaceResponse = await client.CreateSpace(exploreCookie, spaceName); Assert.Equal(expectedStatusCode, spaceResponse.StatusCode); }); } + + [Fact] + public async Task CreateApiEntry() + { + var expectedStatusCode = HttpStatusCode.Created; + var expectedId = new Guid(); + var spaceId = new Guid(); + var apiName = "new space"; + var importer = "xunit"; + var description = "sample desc"; + var apiContent = new ApiRequest() + { + Name = apiName, + Type = "REST", + Description = $"imported from {importer} on {DateTime.UtcNow.ToShortDateString()}\n{description}" + }; + var expectedResponse = new CreateApiEntryResult + { + Result = true, + Id = new Guid(), + StatusCode = expectedStatusCode + }; + var expectedApiResponse = new ApiResponse + { + Id = new Guid(), + Name = "foo", + Type = "TEST", + Servers = new List + { + + }, + Description = "foo" + }; + + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + pact + .UponReceiving("a request to create a new api entry") + .Given("a space {spaceId} exists", new Dictionary { ["spaceId"] = spaceId.ToString() }) + .WithRequest(HttpMethod.Post, $"/spaces/{spaceId}/apis") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WithJsonBody(apiContent) + .WillRespond() + .WithStatus(expectedStatusCode) + .WithJsonBody(expectedApiResponse); + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var spaceResponse = await client.CreateApiEntry(exploreCookie, spaceId, apiName, importer, description); + + Assert.Equal(expectedId, spaceResponse.Id); + }); + } + + [Fact] + public async Task CreateApiConnection() + { + var expectedStatusCode = HttpStatusCode.Created; + var spaceId = new Guid(); + var apiId = new Guid(); + var apiConnectionContent = new JsonObject + { + + }; + var expectedResponse = new CreateApiConnectionResult + { + Result = true, + Id = new Guid(), + StatusCode = expectedStatusCode + }; + + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + pact + .UponReceiving("a request to create a new api connection") + .Given("a space {spaceId} with api {apiId} exists", new Dictionary { ["spaceId"] = spaceId.ToString(), ["apiId"] = apiId.ToString() }) + .WithRequest(HttpMethod.Post, $"/spaces/{spaceId}/apis/{apiId}/connections") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WithJsonBody(apiConnectionContent) + .WillRespond() + .WithStatus(expectedStatusCode); + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var spaceResponse = await client.CreateApiConnection(exploreCookie, spaceId, apiId, apiConnectionContent.ToString()); + + Assert.True(spaceResponse.Result); + }); + } + + [Fact] + public async Task CheckApiExists() + { + var expectedStatusCode = HttpStatusCode.OK; + var spaceId = new Guid(); + var apiId = new Guid(); + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + pact + .UponReceiving("a request to check an api exists") + .Given("a space {spaceId} with api {apiId} exists", new Dictionary { ["spaceId"] = spaceId.ToString(), ["apiId"] = apiId.ToString() }) + .WithRequest(HttpMethod.Get, $"/spaces/{spaceId}/apis/{apiId}") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WillRespond() + .WithStatus(expectedStatusCode); + + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var spaceResponse = await client.CheckApiExists(exploreCookie, spaceId.ToString(), apiId.ToString(), false); + + Assert.True(spaceResponse); + }); + } + [Fact] + public async Task CheckApiDoesNotExist() + { + var expectedStatusCode = HttpStatusCode.NotFound; + var spaceId = new Guid(); + var apiId = new Guid(); + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + pact + .UponReceiving("a request to check an api exists") + .Given("a space {spaceId} with api {apiId} does not exist", new Dictionary { ["spaceId"] = spaceId.ToString(), ["apiId"] = apiId.ToString() }) + .WithRequest(HttpMethod.Get, $"/spaces/{spaceId}/apis/{apiId}") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WillRespond() + .WithStatus(expectedStatusCode); + + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var spaceResponse = await client.CheckApiExists(exploreCookie, spaceId.ToString(), apiId.ToString(), false); + + Assert.False(spaceResponse); + }); + } + + [Fact] + public async Task CheckConnectionExists() + { + var expectedStatusCode = HttpStatusCode.OK; + var spaceId = new Guid(); + var apiId = new Guid(); + var connectionId = new Guid(); + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + pact + .UponReceiving("a request to check an api connection exists") + .Given("a space {spaceId} with api {apiId} and connection {connectionId} exists", new Dictionary { ["spaceId"] = spaceId.ToString(), ["apiId"] = apiId.ToString(), ["connectionId"] = connectionId.ToString() }) + .WithRequest(HttpMethod.Get, $"/spaces/{spaceId}/apis/{apiId}/connections/{connectionId}") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WillRespond() + .WithStatus(expectedStatusCode); + + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var spaceResponse = await client.CheckConnectionExists(exploreCookie, spaceId.ToString(), apiId.ToString(), connectionId.ToString(), false); + + Assert.True(spaceResponse); + }); + } + + [Fact] + public async Task CheckConnectionDoesNotExist() + { + var expectedStatusCode = HttpStatusCode.NotFound; + var spaceId = new Guid(); + var apiId = new Guid(); + var connectionId = new Guid(); + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + pact + .UponReceiving("a request to check an api connection exists") + .Given("a space {spaceId} with api {apiId} and connection {connectionId} does not exist", new Dictionary { ["spaceId"] = spaceId.ToString(), ["apiId"] = apiId.ToString(), ["connectionId"] = connectionId.ToString() }) + .WithRequest(HttpMethod.Get, $"/spaces/{spaceId}/apis/{apiId}/connections/{connectionId}") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WillRespond() + .WithStatus(expectedStatusCode); + + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var spaceResponse = await client.CheckConnectionExists(exploreCookie, spaceId.ToString(), apiId.ToString(), connectionId.ToString(), false); + + Assert.False(spaceResponse); + }); + } + + [Fact] + public async Task GetSpaces() + { + var expectedStatusCode = HttpStatusCode.OK; + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + var expectedResult = new GetSpacesResult + { + Result = true, + Spaces = new PagedSpaces + { + Embedded = new EmbeddedSpaces + { + Spaces = new List{ + new() { + Id = new Guid(), + Name = "foo", + Links = new Links + { + Self = new() + { + Href = new Uri("http://test"), + } + } + }, + new() { + Id = new Guid(), + Name = "foo2", + Links = new Links + { + Self = new() + { + Href = new Uri("http://test"), + } + } + } + } + } + }, + StatusCode = expectedStatusCode + }; + pact + .UponReceiving("a request to get spaces") + .Given("some spaces exist") + .WithRequest(HttpMethod.Get, $"/spaces") + .WithQuery("page", "0") + .WithQuery("size", "2000") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WillRespond() + .WithHeader("Content-Type", "application/hal+json") + .WithStatus(expectedStatusCode) + .WithJsonBody(expectedResult.Spaces); + + + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var response = await client.GetSpaces(exploreCookie); + Assert.Equal(expectedResult.Result, response.Result); + Assert.Equal(expectedResult.StatusCode, response.StatusCode); + Assert.Equal(expectedResult.Spaces.Embedded.Spaces[0].Id, response?.Spaces?.Embedded?.Spaces?[0].Id); + Assert.Equal(expectedResult.Spaces.Embedded.Spaces[0].Name, response?.Spaces?.Embedded?.Spaces?[0].Name); + Assert.Equal(expectedResult.Spaces.Embedded.Spaces[0].Links?.Self?.Href, response?.Spaces?.Embedded?.Spaces?[0].Links?.Self?.Href); + }); + } + + [Fact] + public async Task GetSpaceApis() + { + var expectedStatusCode = HttpStatusCode.OK; + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + + var spaceId = new Guid(); + var apiId = new Guid(); + var expectedResult = new GetSpaceApisResult + { + Result = true, + Apis = new PagedApis + { + Embedded = new EmbeddedApis + { + Apis = new List{ + new() { + Id = new Guid(), + Name = "foo", + Type = "TEST", + Servers = new List{ + + }, + Description = "foo" + } + } + } + }, + StatusCode = expectedStatusCode + }; + pact + .UponReceiving("a request to get apis for a space") + .Given("a space {spaceId} with api {apiId} exists", new Dictionary { ["spaceId"] = spaceId.ToString(), ["apiId"] = apiId.ToString() }) + .WithRequest(HttpMethod.Get, $"/spaces/{spaceId}/apis") + .WithQuery("page", "0") + .WithQuery("size", "2000") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WillRespond() + .WithHeader("Content-Type", "application/hal+json") + .WithStatus(expectedStatusCode) + .WithJsonBody(expectedResult.Apis); + + + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var response = await client.GetSpaceApis(exploreCookie, spaceId); + Assert.Equal(expectedResult.Result, response.Result); + Assert.Equal(expectedResult.StatusCode, response.StatusCode); + Assert.Equal(expectedResult.Apis.Embedded.Apis[0].Id, response?.Apis?.Embedded?.Apis?[0].Id); + Assert.Equal(expectedResult.Apis.Embedded.Apis[0].Name, response?.Apis?.Embedded?.Apis?[0].Name); + }); + } + [Fact] + public async Task GetApiConnectionsForSpace() + { + var expectedStatusCode = HttpStatusCode.OK; + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + + var spaceId = new Guid(); + var apiId = new Guid(); + var connectionId = new Guid(); + var expectedResult = new GetApiConnectionsForSpaceResult + { + Result = true, + Connections = new PagedConnections + { + Embedded = new EmbeddedConnections + { + Connections = new List{ + // we get a fair bit back in the response that isn't shown in the + // openapi document, so adding them in fails BDCT verificatiom + // excluding properties and making them nullable resolves cross-compat checks + new() { + Id = new Guid().ToString(), + Name = "REST", + Schema = "OpenAPI", + SchemaVersion = "3.0.1", + // Type = "foo", + // ConnectionDefinition = new ConnectionDefinition{ + // Paths = new Dictionary{{"/me",new object{}}}, + // Servers = new List{ + // new() { + // Url = "http://test" + // } + + // } + // }, + ConnectionDefinition = new ConnectionDefinition{ + }, + // Settings = new Settings{ + // Type = "RestConnectionSettings", + // EncodeUrl = true, + // ConnectTimeout = 30, + // FollowRedirects = true, + // }, + // Credentials = new Credentials{ + + // }, + } + } + } + }, + StatusCode = expectedStatusCode + }; + pact + .UponReceiving("a request to get connections for an api") + .Given("a space {spaceId} with api {apiId} and connection {connectionId} exists", new Dictionary { ["spaceId"] = spaceId.ToString(), ["apiId"] = apiId.ToString(), ["connectionId"] = connectionId.ToString() }) + .WithRequest(HttpMethod.Get, $"/spaces/{spaceId}/apis/{apiId}/connections") + .WithQuery("page", "0") + .WithQuery("size", "2000") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WillRespond() + .WithHeader("Content-Type", "application/hal+json") + .WithStatus(expectedStatusCode) + .WithJsonBody(expectedResult.Connections); + + + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var response = await client.GetApiConnectionsForSpace(exploreCookie, spaceId, apiId); + Assert.Equal(expectedResult.Result, response.Result); + Assert.Equal(expectedResult.StatusCode, response.StatusCode); + Assert.Equal(expectedResult.Connections.Embedded.Connections[0].Id, response?.Connections?.Embedded?.Connections?[0].Id); + Assert.Equal(expectedResult.Connections.Embedded.Connections[0].Name, response?.Connections?.Embedded?.Connections?[0].Name); + }); + } } } \ No newline at end of file diff --git a/test/Explore.Cli.Tests/explore.yaml b/test/Explore.Cli.Tests/explore.yaml new file mode 100644 index 0000000..45a0200 --- /dev/null +++ b/test/Explore.Cli.Tests/explore.yaml @@ -0,0 +1,1385 @@ +openapi: 3.0.0 +info: + title: Explore Spaces API + description: | + ## REST API for Managing Spaces and related resources for Explore + This API is intended for managing resources in Explore. Spaces, APIs, Connections. + termsOfService: http://swagger.io/terms/ + contact: + email: apiexploration@smartbear.com + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.0 +servers: +- url: https://virtserver.swaggerhub.com/smartbear/explore-spaces-service/1.0.0 + description: SwaggerHub API Auto Mocking +- url: "https://{environment}/spaces-api/v1" + variables: + environment: + default: api.explore.swaggerhub.com + enum: + - api.explore.swaggerhub.com + - api.dev.explore.swaggerhub.com + - api.int.explore.swaggerhub.com +security: +- SbTokenDataAuth: [] +tags: +- name: space + description: Operations related to connections spaces +- name: api + description: Operations related to connections APIs +- name: connection + description: Operations related to connections +- name: export + description: Operations related to exports +paths: + /spaces: + get: + tags: + - space + summary: Return a list of spaces for the current user + description: List all spaces of the current user + operationId: findSpaces + parameters: + - name: page + in: query + schema: + type: integer + format: int64 + - name: size + in: query + schema: + type: integer + format: int64 + responses: + "200": + description: a list of spaces + content: + application/json: + schema: + $ref: '#/components/schemas/PagedSpaces' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + post: + tags: + - space + summary: create a new Space + description: Create a new Space for organising APIs. + operationId: createSpace + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceRequest' + responses: + "201": + description: Space created + content: + application/json: + schema: + $ref: '#/components/schemas/Space' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + /spaces/{spaceId}: + get: + tags: + - space + summary: Get specific space + description: Get specific space by ID + operationId: getSpace + parameters: + - name: spaceId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + responses: + "200": + description: Space + content: + application/json: + schema: + $ref: '#/components/schemas/Space' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + put: + tags: + - space + summary: Update a specific space + description: Update a specific space + operationId: updateSpace + parameters: + - name: spaceId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceRequest' + responses: + "200": + description: Space + content: + application/json: + schema: + $ref: '#/components/schemas/Space' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + delete: + tags: + - space + summary: Delete specific space + description: Delete specific space by ID + operationId: deleteSpace + parameters: + - name: spaceId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: removeSnapshots + in: query + description: | + If set to true, delete associated snapshots. + required: false + style: form + explode: true + schema: + type: boolean + responses: + "204": + description: Space deleted + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + /spaces/snapshot/{snapshotId}: + post: + tags: + - space + summary: Create a new space from a snapshot + description: Create a new space using the provided snapshot ID + operationId: createSpaceFromSnapshot + parameters: + - name: snapshotId + in: path + description: Snapshot ID + required: true + style: simple + explode: false + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceFromSnapshotRequest' + responses: + "200": + description: Space created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Space' + "201": + description: Space created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Space' + "404": + description: The requested snapshot was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/SnapshotNotFoundResponse' + "409": + description: The requested spase name already exists. + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceNameAlreadyExistsResponse' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + /spaces/{spaceId}/apis: + get: + tags: + - api + summary: Return a list of apis contained in a space + description: List all apis of a specific space + operationId: findApis + parameters: + - name: spaceId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: page + in: query + schema: + type: integer + format: int64 + - name: size + in: query + schema: + type: integer + format: int64 + responses: + "200": + description: a list of spaces + content: + application/json: + schema: + $ref: '#/components/schemas/PagedApis' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + post: + tags: + - api + summary: create a new API + description: Create a new API for organising connections. + operationId: createApi + parameters: + - name: spaceId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ApiRequest' + responses: + "201": + description: API created + content: + application/json: + schema: + $ref: '#/components/schemas/Api' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + /spaces/{spaceId}/apis/{apiId}: + get: + tags: + - api + summary: Get API + description: Get API by ID + operationId: getApi + parameters: + - name: spaceId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: apiId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + responses: + "200": + description: API + content: + application/json: + schema: + $ref: '#/components/schemas/Api' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + put: + tags: + - api + summary: Update API + description: Update the name of an API by ID + operationId: updateApi + parameters: + - name: spaceId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: apiId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateApiRequest' + responses: + "200": + description: API + content: + application/json: + schema: + $ref: '#/components/schemas/Api' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + delete: + tags: + - api + summary: Delete API + description: Delete API by ID + operationId: deleteApi + parameters: + - name: spaceId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: apiId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + responses: + "204": + description: API deleted + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + /spaces/{spaceId}/apis/{apiId}/connections: + get: + tags: + - connection + summary: Return connections for an API + description: Returns connections for the referenced API in paged format + operationId: getConnectionsForApi + parameters: + - name: spaceId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: apiId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: page + in: query + schema: + type: integer + format: int64 + - name: size + in: query + schema: + type: integer + format: int64 + responses: + "200": + description: A list of all the connections of the referenced API + content: + application/json: + schema: + $ref: '#/components/schemas/PagedConnections' + post: + tags: + - connection + summary: Create a new Connection + description: create a connection from an existing transaction + operationId: createConnection + parameters: + - name: spaceId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: apiId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateConnectionRequest' + responses: + "201": + description: Connection created + content: + application/json: + schema: + $ref: '#/components/schemas/LinkConnection' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + /spaces/{spaceId}/apis/{apiId}/connections/{connectionId}: + get: + tags: + - connection + summary: Get connection + description: Get connection by ID + operationId: getConnection + parameters: + - name: spaceId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: apiId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: connectionId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + responses: + "200": + description: Connection + content: + application/json: + schema: + $ref: '#/components/schemas/LinkConnection' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + put: + tags: + - connection + summary: Update connection + description: Update connection by ID + operationId: updateConnection + parameters: + - name: spaceId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: apiId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: connectionId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ConnectionRequest' + responses: + "200": + description: Connection + content: + application/json: + schema: + $ref: '#/components/schemas/LinkConnection' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + delete: + tags: + - connection + summary: Delete connection + description: Delete connection by ID + operationId: deleteConnection + parameters: + - name: spaceId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: apiId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: connectionId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + responses: + "204": + description: Connection deleted + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + /spaces/snapshot: + post: + tags: + - space + summary: create a space snapshot + description: Create a new Snapshot for selected space + operationId: createSpaceSnapshot + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceSnapshotRequest' + responses: + "201": + description: Snapshot created + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceSnapshot' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + /exports: + get: + tags: + - export + summary: get all/search for exports + description: get all exports + parameters: + - name: apiId + in: query + description: Id of the API to filter by + required: true + style: form + explode: true + schema: + type: string + format: uuid + - name: page + in: query + schema: + type: integer + format: int64 + - name: size + in: query + schema: + type: integer + format: int64 + responses: + "200": + description: A list of all the exports for the requested API + content: + application/json: + schema: + $ref: '#/components/schemas/PagedExports' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + post: + tags: + - export + summary: Export the given API in the specified definition format + description: Returns an API definition for the given API + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ExportRequest' + responses: + "201": + description: API definition successfully exported + content: + application/json: + schema: + $ref: '#/components/schemas/Export' + default: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' +components: + schemas: + SnapshotNotFoundResponse: + type: object + properties: + type: + type: string + example: https://api.explore.swaggerhub.com/spaces-api/v1/errors/SpaceSnapshotNotFound + title: + type: string + example: Not Found + detail: + type: string + example: Space snapshot with id 'a5782f8e-1c14-4f38-9cb8-27258e73cc53' not found + status: + type: integer + example: 404 + snapshotId: + type: string + format: uuid + SpaceNameAlreadyExistsResponse: + type: object + properties: + type: + type: string + example: https://api.explore.swaggerhub.com/spaces-api/v1/errors/SpaceNameAlreadyExists + title: + type: string + example: Not Found + detail: + type: string + example: Space snapshot named 'Default Space' already exists. + status: + type: integer + example: 409 + spaceName: + type: string + example: Default Space Name + snapshotId: + type: string + format: uuid + CreateConnectionRequest: + type: object + # discriminator: + # propertyName: type + # required: + # - type + CreateConnectionFromTransactionRequest: + allOf: + - $ref: '#/components/schemas/CreateConnectionRequest' + - type: object + properties: + transactionId: + type: string + description: the id of the transaction to create a connection from + format: uuid + ConnectionRequest: + allOf: + - $ref: '#/components/schemas/CreateConnectionRequest' + - $ref: '#/components/schemas/Connection' + ApiRequest: + type: object + properties: + name: + type: string + description: Name of API to create + type: + type: string + enum: + - REST + - KAFKA + description: + type: string + description: Description of API + nullable: true + UpdateApiRequest: + type: object + properties: + name: + type: string + description: New name of API + nullable: false + description: + type: string + description: Description of API + nullable: true + SpaceRequest: + type: object + properties: + name: + type: string + description: Name of Space to create + SpaceSnapshotRequest: + required: + - spaceId + type: object + properties: + spaceId: + type: string + description: Id of Space to create snapshot + format: uuid + shareAuth: + type: boolean + description: Enable sharing of Auth details in snapshot + default: false + oneTimeLink: + type: boolean + description: Create snapshot and link for one time use + default: true + SpaceFromSnapshotRequest: + type: object + properties: + mode: + type: string + description: space create mode + enum: + - CREATE + - OVERRIDE + - DUPLICATE + PagedSpaces: + properties: + _embedded: + $ref: '#/components/schemas/PagedSpaces__embedded' + _links: + $ref: '#/components/schemas/PagedLink' + page: + $ref: '#/components/schemas/Page' + PagedApis: + properties: + _embedded: + $ref: '#/components/schemas/PagedApis__embedded' + _links: + $ref: '#/components/schemas/PagedLink' + page: + $ref: '#/components/schemas/Page' + PagedConnections: + properties: + _embedded: + $ref: '#/components/schemas/PagedConnections__embedded' + _links: + $ref: '#/components/schemas/PagedLink' + page: + $ref: '#/components/schemas/Page' + Space: + type: object + properties: + name: + type: string + id: + type: string + format: uuid + _links: + $ref: '#/components/schemas/Link' + Api: + type: object + properties: + name: + type: string + id: + type: string + format: uuid + type: + type: string + description: the type of the API + example: REST + description: + type: string + servers: + type: array + items: + $ref: '#/components/schemas/Server' + _links: + $ref: '#/components/schemas/Link' + Server: + type: object + properties: + url: + type: string + LinkConnection: + allOf: + - $ref: '#/components/schemas/Connection' + - type: object + properties: + _links: + $ref: '#/components/schemas/Link' + Transaction: + type: object + properties: + id: + type: string + format: uuid + example: cd357463-613b-4fe6-81d8-4f716fe2dc42 + date: + type: string + format: date-time + readOnly: true + example: 2017-07-21T17:32:28Z + operationData: + type: object + example: + type: REST + connection: + name: My Connection + schema: OpenAPI + schemaVersion: 3.0.1 + connectionDefinition: + openapi: 3.0.1 + servers: + - url: http://example.com + paths: + /somePath: + get: {} + settings: + type: RestConnectionSettings + connectTimeout: 10 + followRedirects: true + encodeUrl: false + enableHttp2: false + status: + type: string + example: SUCCESS + statusDetails: + type: string + _links: + $ref: '#/components/schemas/Link' + ExportRequest: + required: + - apiId + - format + - title + - version + type: object + properties: + apiId: + type: string + description: The id of the API to export. Will add all endpoints of the API to the exported API definition. + format: uuid + version: + type: string + description: "The version of the exported API, will be added in the info section of the exported API definition." + example: 1.0.0 + title: + type: string + description: "The title of the exported API, will be added in the info section of the exported API definition." + example: My exported API + format: + type: string + description: The specification format of the exported API + enum: + - OPENAPI_30 + Export: + type: object + properties: + id: + type: string + description: "The ID of the export, can be used to retrieve metadata about the export" + format: uuid + createdAt: + type: string + description: The date and time when the export was created + format: date-time + definition: + type: object + example: | + { + "openapi":"3.0.1", + "servers":[ + { + "url":"http://example.com" + } + ], + "paths":{ + "/somePath":{ + "get":{} + } + } + } + _links: + $ref: '#/components/schemas/Link' + PagedExports: + properties: + _embedded: + $ref: '#/components/schemas/PagedExports__embedded' + _links: + $ref: '#/components/schemas/PagedLink' + page: + $ref: '#/components/schemas/Page' + SpaceSnapshot: + type: object + properties: + id: + type: string + format: uuid + spaceId: + type: string + description: The id of the space for which this snapshot was created + format: uuid + isOneTime: + type: boolean + description: Flag to check if this snapshot can be used only once + shareAuth: + type: boolean + description: Flag to check if this snapshot includes auth settings + createdAt: + type: string + description: The date and time when the snapshot was created + format: date-time + ProblemDetails: + type: object + properties: + type: + type: string + description: A URI reference that identifies the problem type + format: uri + example: https://api.smartbear.com/problems/missing-body-property + title: + type: string + description: A short summary of the problem type + example: Your request is missing a required request body property + detail: + type: string + description: An explanation specific to this occurrence of the problem + example: "Your request does not contain the required property {propertyName}" + status: + type: integer + description: The status code for this occurrence of the problem + example: 400 + Connection: + required: + - connectionDefinition + - schema + - schemaVersion + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: example.com REST Connection + schema: + type: string + description: unique identifier of the schema for this connection + enum: + - OpenAPI + - AsyncAPI + - Internal + schemaVersion: + type: string + example: 3.0.3 + description: + type: string + connectionDefinition: + type: object + description: The actual connection object. Must conform to the referenced schema. + createdBy: + type: string + readOnly: true + createdTime: + type: string + format: date-time + readOnly: true + example: 2021-01-01T00:00:00Z + modifiedBy: + type: string + readOnly: true + modifiedTime: + type: string + format: date-time + readOnly: true + example: 2021-01-01T00:00:00Z + description: Describes a connection + BasicAuthCredentials: + allOf: + - $ref: '#/components/schemas/UsernamePasswordCredentials' + UsernamePasswordCredentials: + allOf: + - $ref: '#/components/schemas/Credentials' + - type: object + properties: + username: + type: string + description: The username used to authenticate the request + example: admin + password: + type: string + description: The password used to authenticate the request + format: password + example: admin + Credentials: + type: object + # discriminator: + # propertyName: type + # required: + # - type + SaslPlainSslCredentials: + allOf: + - $ref: '#/components/schemas/UsernamePasswordCredentials' + SaslPlainCredentials: + allOf: + - $ref: '#/components/schemas/UsernamePasswordCredentials' + TokenCredentials: + allOf: + - $ref: '#/components/schemas/Credentials' + - type: object + properties: + token: + type: string + description: The token used to authenticate the request + format: password + example: bearerToken + ApiKeyCredentials: + allOf: + - $ref: '#/components/schemas/Credentials' + - type: object + properties: + apiKey: + type: string + description: The API Key used to authenticate the request + format: password + example: apiKey + RestConnectionSettings: + allOf: + - $ref: '#/components/schemas/Settings' + - type: object + properties: + encodeUrl: + type: boolean + description: "Encode the server, path and query parameters" + default: true + followRedirects: + type: boolean + description: Follow HTTP 3xx responses as redirects + default: false + enableHttp2: + type: boolean + description: Enable HTTP/2 support + default: false + followOriginalHttpMethod: + type: boolean + description: Redirect with the original HTTP method instead of the default behavior of redirecting with GET + example: false + connectTimeout: + minimum: 1 + type: integer + description: Sets the connect timeout duration for this request in seconds + example: 10 + Settings: + type: object + # discriminator: + # propertyName: type + # required: + # - type + KafkaConnectionSettings: + allOf: + - $ref: '#/components/schemas/Settings' + - type: object + properties: + closeSubscriptionSettings: + $ref: '#/components/schemas/CloseSubscriptionSettings' + serializationSettings: + $ref: '#/components/schemas/SerializationSettings' + consumerConfigurations: + type: object + additionalProperties: true + description: "Consumer Configuration Parameters - described [here](https://kafka.apache.org/documentation/#consumerconfigs)" + producerConfigurations: + type: object + additionalProperties: true + description: "Producer Configuration Parameters - described [here](https://kafka.apache.org/documentation/#producerconfigs)" + CloseSubscriptionSettings: + type: object + properties: + closeSubscriptionOn: + type: string + description: Will close subscription when either one of or all of the parameters is met + default: ONE_OF + enum: + - ONE_OF + - ALL_OF + idleTime: + maximum: 3600 + minimum: 1 + type: integer + description: The amount of time the subscription has been open without messages (seconds) + default: 60 + runTime: + maximum: 3600 + minimum: 1 + type: integer + description: The amount of time the subscription will stay open (seconds) + default: 60 + messagesReceived: + maximum: 500 + minimum: 1 + type: integer + description: The number of messages to receive before closing the subscription + default: 50 + SerializationSettings: + required: + - type + type: object + properties: + type: + type: string + description: The type of serializer/deserializer to use + enum: + - string + - avro + - protobuf + schemaRegistryUrl: + type: string + description: The url of the schema registry if needed + format: uri + example: http://example.com:8081 + PagedLink: + type: object + properties: + first: + $ref: '#/components/schemas/PagedLink_first' + prev: + $ref: '#/components/schemas/PagedLink_first' + self: + $ref: '#/components/schemas/PagedLink_first' + next: + $ref: '#/components/schemas/PagedLink_first' + last: + $ref: '#/components/schemas/PagedLink_first' + Page: + type: object + properties: + totalElements: + type: integer + format: int64 + totalPages: + type: integer + format: int64 + size: + type: integer + format: int64 + number: + type: integer + format: int64 + Link: + type: object + properties: + self: + $ref: '#/components/schemas/Link_self' + PagedSpaces__embedded: + type: object + properties: + spaces: + type: array + items: + $ref: '#/components/schemas/Space' + PagedApis__embedded: + type: object + properties: + apis: + type: array + items: + $ref: '#/components/schemas/Api' + PagedConnections__embedded: + type: object + properties: + connections: + type: array + items: + $ref: '#/components/schemas/LinkConnection' + PagedExports__embedded_exports: + type: object + properties: + id: + type: string + format: uuid + createdAt: + type: string + description: The date and time when the export was created + format: date-time + PagedExports__embedded: + type: object + properties: + exports: + type: array + items: + $ref: '#/components/schemas/PagedExports__embedded_exports' + PagedLink_first: + type: object + properties: + href: + type: string + format: uri + Link_self: + type: object + properties: + href: + type: string + responses: + ErrorResponse: + description: The request failed. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + SnapshotNotFoundErrorResponse: + description: The requested snapshot was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/SnapshotNotFoundResponse' + SpaceNameAlreadyExistsErrorResponse: + description: The requested spase name already exists. + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceNameAlreadyExistsResponse' + parameters: + SpaceId: + name: spaceId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + ApiId: + name: apiId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + ConnectionId: + name: connectionId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + Page: + name: page + in: query + schema: + type: integer + format: int64 + Size: + name: size + in: query + schema: + type: integer + format: int64 + securitySchemes: + SbTokenDataAuth: + type: apiKey + description: A JWT token that contains email and other basic information about the logged in user. + name: x-xsrf-token + in: header From 033a8dc9a72628e52ed88ba906fc3216fa43ff1f Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 20 Sep 2024 12:55:33 +0100 Subject: [PATCH 10/11] test: add Upsert Method Pact tests --- test/Explore.Cli.Tests/PactConsumerTest.cs | 137 +++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/test/Explore.Cli.Tests/PactConsumerTest.cs b/test/Explore.Cli.Tests/PactConsumerTest.cs index 5e326a6..76378ef 100644 --- a/test/Explore.Cli.Tests/PactConsumerTest.cs +++ b/test/Explore.Cli.Tests/PactConsumerTest.cs @@ -548,5 +548,142 @@ await pact.VerifyAsync(async ctx => Assert.Equal(expectedResult.Connections.Embedded.Connections[0].Name, response?.Connections?.Embedded?.Connections?[0].Name); }); } + + [Fact] + public async Task UpsertSpaceWithExistingSpaceId() + { + var expectedStatusCode = HttpStatusCode.OK; + var expectedId = new Guid(); + var spaceName = "new space"; + var spaceId = new Guid(); + var spaceExists = true; + var spaceContent = new SpaceRequest() { Name = spaceName }; + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + pact + .UponReceiving("a request to update a space") + .Given("a space with name {name} and spaceId {spaceId} exists", new Dictionary { ["name"] = spaceName, ["spaceId"] = spaceId.ToString() }) + .WithRequest(HttpMethod.Put, $"/spaces/{spaceId}") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WithJsonBody(spaceContent) + .WillRespond() + .WithStatus(expectedStatusCode) + .WithJsonBody(new SpaceResponse() + { + Id = expectedId, + Name = spaceName, + Links = new Links + { + Self = new() + { + Href = new Uri("http://test"), + } + } + }); + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var spaceResponse = await client.UpsertSpace(exploreCookie, spaceExists, spaceName, spaceId.ToString()); + + Assert.Equal(expectedId, spaceResponse.Id); + }); + } + [Fact] + public async Task UpsertApiWithoutExistingApiId() + { + // NOTE, if space exists we need to mock out + // CheckApiExists as well. For another test. + var expectedStatusCode = HttpStatusCode.Created; + var expectedId = new Guid(); + var spaceName = "new space"; + var apiName = "new api"; + var apiType = new Connection + { + Type = "REST" + }; + var spaceId = new Guid(); + var apiId = new Guid(); + var apiContent = new ApiRequest() { Name = apiName, Type = "REST" }; + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + pact + .UponReceiving("a request to update a api that does not exist, creates a new api") + .Given("a space with apiId {apiId} does not exist", new Dictionary { ["apiId"] = apiId.ToString() }) + .WithRequest(HttpMethod.Post, $"/spaces/{spaceId}/apis") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WithJsonBody(apiContent) + .WillRespond() + .WithStatus(expectedStatusCode) + .WithJsonBody(new ApiResponse() + { + Id = expectedId, + Name = spaceName, + Type = apiType.Type.ToString(), + Description = "foo", + Servers = new List + { + + }, + }); + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var spaceResponse = await client.UpsertApi(exploreCookie, false, spaceId.ToString(), null, apiName, apiType.Type.ToString(), false); + + Assert.Equal(expectedId, spaceResponse.Id); + }); + } + [Fact] + public async Task UpsertConnectionWithoutExistingConnectionId() + { + // NOTE, if space exists we need to mock out + // CheckConnectionExists as well. For another test. + var expectedStatusCode = HttpStatusCode.Created; + var apiType = new Connection + { + Type = "ConnectionRequest" + }; + var spaceId = new Guid(); + var apiId = new Guid(); + var apiContent = + new Connection() + { + Id = new Guid().ToString(), + Name = "REST", + Schema = "OpenAPI", + SchemaVersion = "3.0.1", + Type = "ConnectionRequest", + ConnectionDefinition = new ConnectionDefinition + { + }, + }; + + var exploreXsrfToken = "bar"; + var exploreCookie = $"foo;XSRF-TOKEN={exploreXsrfToken}"; + pact + .UponReceiving("a request to update an api connection that does not exist, creates a new api connection") + .Given("a space with apiId {apiId} exists", new Dictionary { ["apiId"] = apiId.ToString() }) + .WithRequest(HttpMethod.Post, $"/spaces/{spaceId}/apis/{apiId}/connections") + .WithHeader("Cookie", exploreCookie) + .WithHeader("X-Xsrf-Token", exploreXsrfToken) + .WithJsonBody(apiContent) + .WillRespond() + .WithStatus(expectedStatusCode); + + await pact.VerifyAsync(async ctx => + { + var client = new ExploreHttpClient(ctx.MockServerUri.ToString()); + + var connectionResponse = await client.UpsertConnection(exploreCookie, false, spaceId.ToString(), apiId.ToString(), null, apiContent, false); + + Assert.True(connectionResponse); + }); + } } } \ No newline at end of file From 5e27aeaa15b1eec2f55852419d81bb14452e0ce8 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Wed, 25 Sep 2024 17:07:03 +0100 Subject: [PATCH 11/11] ci: add can-i-deploy retry if unknown --- .github/workflows/build-test-cross.yml | 2 ++ .github/workflows/build-test-package.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/build-test-cross.yml b/.github/workflows/build-test-cross.yml index 6bb2ddb..1e58846 100644 --- a/.github/workflows/build-test-cross.yml +++ b/.github/workflows/build-test-cross.yml @@ -129,6 +129,8 @@ jobs: application_name: explore-cli broker_url: ${{ secrets.PACT_BROKER_BASE_URL }} token: ${{ secrets.PACT_BROKER_TOKEN }} + retry_while_unknown: 5 + retry_interval: 10 - name: Download artifacts uses: actions/download-artifact@v4 diff --git a/.github/workflows/build-test-package.yml b/.github/workflows/build-test-package.yml index 8b55b0d..1611f06 100644 --- a/.github/workflows/build-test-package.yml +++ b/.github/workflows/build-test-package.yml @@ -77,6 +77,8 @@ jobs: application_name: explore-cli broker_url: ${{ secrets.PACT_BROKER_BASE_URL }} token: ${{ secrets.PACT_BROKER_TOKEN }} + retry_while_unknown: 5 + retry_interval: 10 - name: Create Package run: dotnet pack --configuration $BUILD_CONFIG -o:package /p:PackageVersion=${{ steps.gitVersion.outputs.assemblySemVer }}