From 31bbd1489a9b103e70e59d6cd4ac662cf02df5a8 Mon Sep 17 00:00:00 2001 From: Frank Kilcommins Date: Fri, 10 Jan 2025 12:38:44 +0000 Subject: [PATCH] Improve support for nested postman collection items --- .github/gitversion.yml | 2 +- README.md | 4 +- src/Explore.Cli/Explore.Cli.csproj | 2 +- .../Postman/PostmanCollectionMappingHelper.cs | 88 +++++++ src/Explore.Cli/Models/ExploreCliModels.cs | 9 + src/Explore.Cli/Program.cs | 58 +++-- .../PostmanCollectionMappingHelperTests.cs | 41 +++ ...ees_API_mixed_urls.postman_collection.json | 235 ++++++++++++++++++ .../API_.Payees_API.postman_collection.json | 195 +++++++++++++++ 9 files changed, 606 insertions(+), 28 deletions(-) create mode 100644 test/Explore.Cli.Tests/fixtures/API.Payees_API_mixed_urls.postman_collection.json create mode 100644 test/Explore.Cli.Tests/fixtures/API_.Payees_API.postman_collection.json diff --git a/.github/gitversion.yml b/.github/gitversion.yml index 366133f..1cb5b23 100644 --- a/.github/gitversion.yml +++ b/.github/gitversion.yml @@ -1,4 +1,4 @@ -next-version: 0.7.0 +next-version: 0.8.0 assembly-versioning-scheme: MajorMinorPatch assembly-file-versioning-scheme: MajorMinorPatchTag assembly-informational-format: '{InformationalVersion}' diff --git a/README.md b/README.md index 2ff0051..e2a0a1d 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,8 @@ From SwaggerHub Explore, navigate to your browser development tools, locate the > **Notes:** > - Compatible with Postman Collections v2.1 -> - Nested collections get flattened into a single Explore space +> - Root level request get bundled into API folder with same name as collection +> - Nested collections get added to an API folder with naming format (`parent folder - nested folder`) > - GraphQL collections/requests not supported > - Environments, Authorization data (not including explicit headers), Pre-request Scripts, Tests are not included in import @@ -298,7 +299,6 @@ From SwaggerHub Explore, navigate to your browser development tools, locate the > - Environments variables are inlined and set within the Explore Space > - Authorization - only Basic and Bearer Token variants are supported - ### Running the `import-pact-file` command **Command Options** diff --git a/src/Explore.Cli/Explore.Cli.csproj b/src/Explore.Cli/Explore.Cli.csproj index 6e6ab7f..04805ff 100644 --- a/src/Explore.Cli/Explore.Cli.csproj +++ b/src/Explore.Cli/Explore.Cli.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/Explore.Cli/MappingHelpers/Postman/PostmanCollectionMappingHelper.cs b/src/Explore.Cli/MappingHelpers/Postman/PostmanCollectionMappingHelper.cs index 033bd83..048037b 100644 --- a/src/Explore.Cli/MappingHelpers/Postman/PostmanCollectionMappingHelper.cs +++ b/src/Explore.Cli/MappingHelpers/Postman/PostmanCollectionMappingHelper.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Explore.Cli.Models.Explore; using Explore.Cli.Models.Postman; +using Explore.Cli.Models; public static class PostmanCollectionMappingHelper { @@ -289,4 +290,91 @@ public static bool IsCollectionVersion2_1(string json) return false; } + + public static List MapPostmanCollectionItemsToExploreConnections(Item collectionItem) + { + var connections = new List(); + + if(collectionItem.Request != null && IsItemRequestModeSupported(collectionItem.Request)) + { + connections.Add(MapPostmanCollectionItemToExploreConnection(collectionItem)); + } + + // if nested item exists, then add it to the connections list if it has request data + if (collectionItem.ItemList != null) + { + foreach(var item in collectionItem.ItemList) + { + if(item.Request != null && IsItemRequestModeSupported(item.Request)) + { + connections.Add(MapPostmanCollectionItemToExploreConnection(item)); + } + } + } + + return connections; + } + + public static List MapPostmanCollectionToStagedAPI(PostmanCollection postmanCollection, string rootName) + { + var stagedAPIs = new List(); + + stagedAPIs.Add(new StagedAPI() + { + APIName = rootName, + }); + + + if(postmanCollection.Item != null) + { + foreach(var item in postmanCollection.Item) + { + + if(item.Request != null && IsItemRequestModeSupported(item.Request)) + { + StagedAPI api = new StagedAPI() + { + APIName = item.Name ?? string.Empty, + APIUrl = GetServerUrlFromItemRequest(item.Request), + Connections = MapPostmanCollectionItemsToExploreConnections(item) + }; + + //if an API with same name already exists, add the connection to the existing API + var existingAPI = stagedAPIs.FirstOrDefault(x => x.APIName == rootName); + if(existingAPI != null) + { + existingAPI.Connections.AddRange(api.Connections); + } + else + { + stagedAPIs.Add(new StagedAPI() + { + APIUrl = api.APIUrl, + APIName = api.APIName, + Connections = api.Connections + }); + } + } + + // if nested item exists, then add it to the staged API list + if (item.ItemList != null) + { + PostmanCollection tempCollection = new PostmanCollection() + { + Item = item.ItemList, + Info = postmanCollection.Info + }; + + if (tempCollection.Info != null) + { + tempCollection.Info.Name = item.Name; + } + + stagedAPIs.AddRange(MapPostmanCollectionToStagedAPI(tempCollection, $"{rootName} - {item.Name}")); + } + } + } + + return stagedAPIs; + } } \ No newline at end of file diff --git a/src/Explore.Cli/Models/ExploreCliModels.cs b/src/Explore.Cli/Models/ExploreCliModels.cs index 683b934..0d810a4 100644 --- a/src/Explore.Cli/Models/ExploreCliModels.cs +++ b/src/Explore.Cli/Models/ExploreCliModels.cs @@ -1,6 +1,15 @@ +using Explore.Cli.Models.Explore; + namespace Explore.Cli.Models; + public partial class SchemaValidationResult { public bool isValid { get; set; } = false; public string? Message { get; set; } +} + +public partial class StagedAPI { + public string APIName { get; set; } = string.Empty; + public string APIUrl { get; set; } = string.Empty; + public List Connections { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Explore.Cli/Program.cs b/src/Explore.Cli/Program.cs index bc207ca..3370d88 100644 --- a/src/Explore.Cli/Program.cs +++ b/src/Explore.Cli/Program.cs @@ -3,6 +3,7 @@ using System.Net.Http.Json; using System.Text; using System.Text.Json; +using System.Linq; using Spectre.Console; using Explore.Cli.Models.Explore; using Explore.Cli.Models.Postman; @@ -132,7 +133,7 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string if (!validationResult.isValid) { - Console.WriteLine($"The provide json does not conform to the expected schema. Errors: {validationResult.Message}"); + Console.WriteLine($"The provided json does not conform to the expected schema. Errors: {validationResult.Message}"); return; } @@ -156,6 +157,7 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string var cleanedCollectionName = UtilityHelper.CleanString(postmanCollection.Info?.Name); var createSpacesResult = await exploreHttpClient.CreateSpace(exploreCookie, cleanedCollectionName); + if (createSpacesResult.Result) { var apiImportResults = new Table() { Title = new TableTitle(text: $"SPACE [green]{cleanedCollectionName}[/] CREATED"), Width = 75, UseSafeBorder = true }; @@ -163,30 +165,38 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string apiImportResults.AddColumn("API Imported"); apiImportResults.AddColumn("Connection Imported"); - //Postman Items cant contain nested items, so we can flatten the list - var flattenedItems = PostmanCollectionMappingHelper.FlattenItems(postmanCollection.Item); + var apisToImport = PostmanCollectionMappingHelper.MapPostmanCollectionToStagedAPI(postmanCollection, cleanedCollectionName); - foreach (var item in flattenedItems) + foreach (var item in apisToImport) { - if (item.Request != null) + if (item.APIName == null || item.Connections == null) { - //check if request format is supported - if (!PostmanCollectionMappingHelper.IsItemRequestModeSupported(item.Request)) - { - apiImportResults.AddRow("[orange3]skipped[/]", $"Item '{item.Name}' skipped", $"Request method not supported"); - continue; - } + apiImportResults.AddRow("[orange3]skipped[/]", $"API '{item.APIName ?? "Unknown"}' skipped", $"No supported request found in collection"); + continue; + } - //now let's create an API entry in the space - var cleanedAPIName = UtilityHelper.CleanString(item.Name); - var createApiEntryResult = await exploreHttpClient.CreateApiEntry(exploreCookie, createSpacesResult.Id, cleanedAPIName, "postman", item.Request.Description?.Content); + AnsiConsole.MarkupLine($"Processing API: {item.APIName} with {item.Connections.Count} connections"); - if (createApiEntryResult.Result) + if(item.Connections == null || item.Connections.Count == 0) + { + apiImportResults.AddRow("[orange3]skipped[/]", $"API '{item.APIName}' skipped", $"No supported request found in collection"); + continue; + } + + //now let's create an API entry in the space + var cleanedAPIName = UtilityHelper.CleanString(item.APIName); + //var description = item.Connections.FirstOrDefault(c => c.Description != null)?.Description?.Content; + + var createApiEntryResult = await exploreHttpClient.CreateApiEntry(exploreCookie, createSpacesResult.Id, cleanedAPIName, "postman", null); + + if(createApiEntryResult.Result) + { + foreach(var connection in item.Connections) { - var connectionRequestBody = JsonSerializer.Serialize(PostmanCollectionMappingHelper.MapPostmanCollectionItemToExploreConnection(item)); + var connectionRequestBody = JsonSerializer.Serialize(connection); //now let's do the work and import the connection var createConnectionResponse = await exploreHttpClient.CreateApiConnection(exploreCookie, createSpacesResult.Id, createApiEntryResult.Id, connectionRequestBody); - + if (createConnectionResponse.Result) { apiImportResults.AddRow("[green]OK[/]", $"API '{cleanedAPIName}' created", "Connection created"); @@ -196,13 +206,13 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string apiImportResults.AddRow("[orange3]OK[/]", $"API '{cleanedAPIName}' created", "[orange3]Connection NOT created[/]"); } } - else - { - apiImportResults.AddRow("[red]NOK[/]", $"API creation failed. StatusCode {createApiEntryResult.StatusCode}", ""); - } } - } + else + { + apiImportResults.AddRow("[red]NOK[/]", $"API creation failed. StatusCode {createApiEntryResult.StatusCode}", ""); + } + } resultTable.AddRow(new Markup("[green]success[/]"), apiImportResults); @@ -255,9 +265,9 @@ internal static async Task ImportPostmanCollection(string exploreCookie, string //ToDo - deal with scenario of item-groups } - catch (FileNotFoundException) + catch (FileNotFoundException ex) { - Console.WriteLine("File not found."); + Console.WriteLine($"File not found: {ex.Message}"); } catch (Exception ex) { diff --git a/test/Explore.Cli.Tests/PostmanCollectionMappingHelperTests.cs b/test/Explore.Cli.Tests/PostmanCollectionMappingHelperTests.cs index bc2f06e..ed48a8a 100644 --- a/test/Explore.Cli.Tests/PostmanCollectionMappingHelperTests.cs +++ b/test/Explore.Cli.Tests/PostmanCollectionMappingHelperTests.cs @@ -1,5 +1,6 @@ using Explore.Cli.Models.Explore; using Explore.Cli.Models.Postman; +using Explore.Cli.Models; using System.Text.Json; public class PostmanCollectionMappingHelperTests @@ -121,4 +122,44 @@ public void ProcessesDescriptions() Assert.Equal("GET", postmanCollection?.Item?[0].ItemList?[0].Request?.Method?.ToString()); Assert.Equal("Gets information about the authenticated user.", postmanCollection?.Item?[0].ItemList?[0].Request?.Description?.Content?.ToString()); } + + [Fact] + public void ProcessNestedCollections_ShouldReturnTwoStagedAPIs() + { + // Arrange + var filePath = "../../../fixtures/API_.Payees_API.postman_collection.json"; + var mockCollectionAsJson = File.ReadAllText(filePath); + var postmanCollection = JsonSerializer.Deserialize(mockCollectionAsJson); + + // Act + List result = new List(); + if (postmanCollection != null) + { + result = PostmanCollectionMappingHelper.MapPostmanCollectionToStagedAPI(postmanCollection, "Payees API"); + } + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + Assert.Equal(2, result.Count); + } + + [Fact] + public void ProcessNestedCollections_ShouldReturnMultipleStagedAPIs() + { + // Arrange + var filePath = "../../../fixtures/API.Payees_API_mixed_urls.postman_collection.json"; + var mockCollectionAsJson = File.ReadAllText(filePath); + var postmanCollection = JsonSerializer.Deserialize(mockCollectionAsJson); + + // Act + List result = new List(); + if (postmanCollection != null) + { + result = PostmanCollectionMappingHelper.MapPostmanCollectionToStagedAPI(postmanCollection, "Payees API"); + } + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + Assert.Equal(2, result.Count); + } } \ No newline at end of file diff --git a/test/Explore.Cli.Tests/fixtures/API.Payees_API_mixed_urls.postman_collection.json b/test/Explore.Cli.Tests/fixtures/API.Payees_API_mixed_urls.postman_collection.json new file mode 100644 index 0000000..5fbfc04 --- /dev/null +++ b/test/Explore.Cli.Tests/fixtures/API.Payees_API_mixed_urls.postman_collection.json @@ -0,0 +1,235 @@ +{ + "info": { + "_postman_id": "ad365ee6-44ef-4ce1-84e8-86894f4713ff", + "name": "Payees API", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "304136" + }, + "item": [ + { + "name": "Irish Payees", + "item": [ + { + "name": "GET Irish LTD companies", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response time is less than 1000ms\", function () {\r", + " pm.expect(pm.response.responseTime).to.be.below(1000);\r", + "});\r", + "\r", + "pm.test(\"Your test name\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData.data[0].bank_account_currency).to.eql(\"EUR\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "oauth2", + "oauth2": [ + { + "key": "addTokenTo", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net/api/payees?country_of_registration=IE&name=ltd", + "protocol": "https", + "host": [ + "sbdevrel-fua-smartbearcoin-prd", + "azurewebsites", + "net" + ], + "path": [ + "api", + "payees" + ], + "query": [ + { + "key": "country_of_registration", + "value": "IE" + }, + { + "key": "name", + "value": "ltd" + } + ] + } + }, + "response": [] + }, + { + "name": "GET Payee Details", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response time is less than 1000ms\", function () {\r", + " pm.expect(pm.response.responseTime).to.be.below(1000);\r", + "});\r", + "\r", + "pm.test(\"Your test name\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData.data[0].bank_account_currency).to.eql(\"EUR\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "oauth2", + "oauth2": [ + { + "key": "addTokenTo", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net/api/payees/4eb603f3-e7e7-47b1-a5df-557b6204616e", + "protocol": "https", + "host": [ + "sbdevrel-fua-smartbearcoin-prd", + "azurewebsites", + "net" + ], + "path": [ + "api", + "payees", + "4eb603f3-e7e7-47b1-a5df-557b6204616e" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "GET DE companies", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response time is less than 1000ms\", function () {\r", + " pm.expect(pm.response.responseTime).to.be.below(1000);\r", + "});\r", + "\r", + "pm.test(\"Your test name\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData.data[0].bank_account_currency).to.eql(\"EUR\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "oauth2", + "oauth2": [ + { + "key": "addTokenTo", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net/api/payees?country_of_registration=IE&name=ltd", + "protocol": "https", + "host": [ + "sbdevrel-fua-smartbearcoin-prd", + "azurewebsites", + "net" + ], + "path": [ + "api", + "payees" + ], + "query": [ + { + "key": "country_of_registration", + "value": "IE" + }, + { + "key": "name", + "value": "ltd" + } + ] + } + }, + "response": [] + }, + { + "name": "http://www.dneonline.com/calculator.asmx", + "request": { + "method": "POST", + "header": [ + { + "key": "SOAPAction", + "value": "http://tempuri.org/Add", + "type": "text" + }, + { + "key": "Content-Type", + "value": "text/xml", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "\r\n \r\n \r\n \r\n 10\r\n 10\r\n \r\n \r\n", + "options": { + "raw": { + "language": "xml" + } + } + }, + "url": { + "raw": "http://www.dneonline.com/calculator.asmx", + "protocol": "http", + "host": [ + "www", + "dneonline", + "com" + ], + "path": [ + "calculator.asmx" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/test/Explore.Cli.Tests/fixtures/API_.Payees_API.postman_collection.json b/test/Explore.Cli.Tests/fixtures/API_.Payees_API.postman_collection.json new file mode 100644 index 0000000..ac82fbd --- /dev/null +++ b/test/Explore.Cli.Tests/fixtures/API_.Payees_API.postman_collection.json @@ -0,0 +1,195 @@ +{ + "info": { + "_postman_id": "ad365ee6-44ef-4ce1-84e8-86894f4713ff", + "name": "Payees API", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "304136" + }, + "item": [ + { + "name": "Irish Payees", + "item": [ + { + "name": "GET Irish LTD companies", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response time is less than 1000ms\", function () {\r", + " pm.expect(pm.response.responseTime).to.be.below(1000);\r", + "});\r", + "\r", + "pm.test(\"Your test name\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData.data[0].bank_account_currency).to.eql(\"EUR\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "oauth2", + "oauth2": [ + { + "key": "addTokenTo", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net/api/payees?country_of_registration=IE&name=ltd", + "protocol": "https", + "host": [ + "sbdevrel-fua-smartbearcoin-prd", + "azurewebsites", + "net" + ], + "path": [ + "api", + "payees" + ], + "query": [ + { + "key": "country_of_registration", + "value": "IE" + }, + { + "key": "name", + "value": "ltd" + } + ] + } + }, + "response": [] + }, + { + "name": "GET Payee Details", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response time is less than 1000ms\", function () {\r", + " pm.expect(pm.response.responseTime).to.be.below(1000);\r", + "});\r", + "\r", + "pm.test(\"Your test name\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData.data[0].bank_account_currency).to.eql(\"EUR\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "oauth2", + "oauth2": [ + { + "key": "addTokenTo", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net/api/payees/4eb603f3-e7e7-47b1-a5df-557b6204616e", + "protocol": "https", + "host": [ + "sbdevrel-fua-smartbearcoin-prd", + "azurewebsites", + "net" + ], + "path": [ + "api", + "payees", + "4eb603f3-e7e7-47b1-a5df-557b6204616e" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "GET DE companies", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response time is less than 1000ms\", function () {\r", + " pm.expect(pm.response.responseTime).to.be.below(1000);\r", + "});\r", + "\r", + "pm.test(\"Your test name\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData.data[0].bank_account_currency).to.eql(\"EUR\");\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "oauth2", + "oauth2": [ + { + "key": "addTokenTo", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net/api/payees?country_of_registration=IE&name=ltd", + "protocol": "https", + "host": [ + "sbdevrel-fua-smartbearcoin-prd", + "azurewebsites", + "net" + ], + "path": [ + "api", + "payees" + ], + "query": [ + { + "key": "country_of_registration", + "value": "IE" + }, + { + "key": "name", + "value": "ltd" + } + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file