From 5b4354bad1d1fabf6cbe14151d8d0e4cabd1a04e Mon Sep 17 00:00:00 2001 From: Frank Kilcommins Date: Thu, 1 Feb 2024 08:44:19 +0000 Subject: [PATCH] Feat: Add `Import-postman-collection` command * Initial simple REST mapper * Add Basic Collection Importing * Add postman collection import support * Remove Inspector command and code as it's sunset * Merge branch 'main' into import-postman-collection * chore: fix merge issue * Validate collection to ensure v2.1 --- .github/gitversion.yml | 2 +- README.md | 38 + explore-cli.sln | 39 + src/Explore.Cli/ExploreContracts.cs | 10 + src/Explore.Cli/InspectorContracts.cs | 125 -- src/Explore.Cli/MappingHelper.cs | 257 ---- src/Explore.Cli/PostmanCollectionContract.cs | 175 +++ .../PostmanCollectionMappingHelper.cs | 293 ++++ src/Explore.Cli/Program.cs | 259 +++- src/Explore.Cli/UtilityHelper.cs | 1331 ++++++++++++++++- test/Explore.Cli.Tests/MappingHelperTests.cs | 458 ------ .../PostmanCollectionMappingHelperTests.cs | 117 ++ test/Explore.Cli.Tests/UtilityHelperTests.cs | 16 +- 13 files changed, 2194 insertions(+), 926 deletions(-) create mode 100644 explore-cli.sln delete mode 100644 src/Explore.Cli/InspectorContracts.cs create mode 100644 src/Explore.Cli/PostmanCollectionContract.cs create mode 100644 src/Explore.Cli/PostmanCollectionMappingHelper.cs create mode 100644 test/Explore.Cli.Tests/PostmanCollectionMappingHelperTests.cs diff --git a/.github/gitversion.yml b/.github/gitversion.yml index 464592c..cd5e8e2 100644 --- a/.github/gitversion.yml +++ b/.github/gitversion.yml @@ -1,4 +1,4 @@ -next-version: 0.4 +next-version: 0.5 assembly-versioning-scheme: MajorMinorPatch assembly-file-versioning-scheme: MajorMinorPatchTag assembly-informational-format: '{InformationalVersion}' diff --git a/README.md b/README.md index 678eb50..c1daabc 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ Simple utility CLI for importing data into SwaggerHub Explore. > `export-spaces` Export SwaggerHub Explore spaces to filesystem > > `import-spaces` Import SwaggerHub Explore spaces from a file +> +> `import-postman-collection` Import Postman Collection from a file into SwaggerHub Explore ### Prerequisites @@ -139,6 +141,42 @@ From SwaggerHub Explore, navigate to your browser development tools, locate the > Please note - the current `import-spaces` does not support importing KAFKA APIs +### Running the `import-postman-collection` command + +**Command Options** +``` + _____ _ ____ _ _ + | ____| __ __ _ __ | | ___ _ __ ___ / ___| | | (_) + | _| \ \/ / | '_ \ | | / _ \ | '__| / _ \ | | | | | | + | |___ > < | |_) | | | | (_) | | | | __/ _ | |___ | | | | + |_____| /_/\_\ | .__/ |_| \___/ |_| \___| (_) \____| |_| |_| + |_| +``` +**Description:** + > Import SwaggerHub Explore spaces from a file + +**Usage:** + > Explore.CLI import-spaces [options] + +**Options:** + > `-ec`, `--explore-cookie` (REQUIRED) A valid and active SwaggerHub Explore session cookie + + > `-fp`, `--file-path` (REQUIRED) The path to the file used for importing data + + > `-v`, `--verbose` Include verbose output during processing + + > `-?`, `-h`, `--help` Show help and usage information + +**Note** - the format for SwaggerHub Explore cookies is as follows: `"cookie-name=cookie-value; cookie-name=cookie-value"` + +>Example: `"SESSION=5a0a2e2f-97c6-4405-b72a-299fa8ce07c8; XSRF-TOKEN=3310cb20-2ec1-4655-b1e3-4ab76a2ac2c8"` + +> **Notes:** +> - Compatible with Postman Collections v2.1 +> - Nested collections get flattened into a single Explore space +> - GraphQL collections/requests not supported +> - Environments, Authorization data (not including explicit headers), Pre-request Scripts, Tests are not included in import + ## More Information on SwaggerHub Explore - For SwaggerHub Explore info, see - https://swagger.io/tools/swaggerhub-explore/ diff --git a/explore-cli.sln b/explore-cli.sln new file mode 100644 index 0000000..e3b3da9 --- /dev/null +++ b/explore-cli.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{91156892-0E82-463A-9EE0-74C0C52183C9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Explore.Cli", "src\Explore.Cli\Explore.Cli.csproj", "{7620B78C-50A4-4B9D-A2E9-ECFF012CAC8B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{80DA4196-73E3-43FF-BF1F-E4779C246124}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Explore.Cli.Tests", "test\Explore.Cli.Tests\Explore.Cli.Tests.csproj", "{453AAC82-60C9-4282-A301-909D9B7B91B2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7620B78C-50A4-4B9D-A2E9-ECFF012CAC8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7620B78C-50A4-4B9D-A2E9-ECFF012CAC8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7620B78C-50A4-4B9D-A2E9-ECFF012CAC8B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7620B78C-50A4-4B9D-A2E9-ECFF012CAC8B}.Release|Any CPU.Build.0 = Release|Any CPU + {453AAC82-60C9-4282-A301-909D9B7B91B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {453AAC82-60C9-4282-A301-909D9B7B91B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {453AAC82-60C9-4282-A301-909D9B7B91B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {453AAC82-60C9-4282-A301-909D9B7B91B2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {7620B78C-50A4-4B9D-A2E9-ECFF012CAC8B} = {91156892-0E82-463A-9EE0-74C0C52183C9} + {453AAC82-60C9-4282-A301-909D9B7B91B2} = {80DA4196-73E3-43FF-BF1F-E4779C246124} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {46BE36A8-9E60-457F-97F1-F0933F220729} + EndGlobalSection +EndGlobal diff --git a/src/Explore.Cli/ExploreContracts.cs b/src/Explore.Cli/ExploreContracts.cs index 60a4dbd..95805f0 100644 --- a/src/Explore.Cli/ExploreContracts.cs +++ b/src/Explore.Cli/ExploreContracts.cs @@ -180,12 +180,22 @@ public class Parameter [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("schema")] + public Schema? Schema { get; set; } + [JsonPropertyName("examples")] public Examples? Examples { get; set; } } +public class Schema +{ + [JsonPropertyName("type")] + public string? type { get; set; } +} + + public class Examples { [JsonPropertyName("example")] diff --git a/src/Explore.Cli/InspectorContracts.cs b/src/Explore.Cli/InspectorContracts.cs deleted file mode 100644 index 6993854..0000000 --- a/src/Explore.Cli/InspectorContracts.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections.Generic; - -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Globalization; - -public class InspectorCollection -{ - [JsonPropertyName("_id")] - public Id? Id { get; set; } - - [JsonPropertyName("modelClass")] - public string? ModelClass { get; set; } - - [JsonPropertyName("collectionId")] - public Guid CollectionId { get; set; } - - [JsonPropertyName("userId")] - public Guid UserId { get; set; } - - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("collectionEntries")] - public List? CollectionEntries { get; set; } -} -public class CollectionEntry -{ - [JsonPropertyName("_id")] - public Id? Id { get; set; } - - [JsonPropertyName("modelClass")] - public string? ModelClass { get; set; } - - [JsonPropertyName("userId")] - public Guid UserId { get; set; } - - [JsonPropertyName("endpoint")] - public Uri? Endpoint { get; set; } - - [JsonPropertyName("uri")] - public UriClass? Uri { get; set; } - - [JsonPropertyName("method")] - public string? Method { get; set; } - - [JsonPropertyName("body")] - public string? Body { get; set; } - - [JsonPropertyName("authentication")] - public string? Authentication { get; set; } - - [JsonPropertyName("headers")] - public List
? Headers { get; set; } - - [JsonPropertyName("type")] - public string? Type { get; set; } - - [JsonPropertyName("entryId")] - public Guid EntryId { get; set; } - - [JsonPropertyName("timestamp")] - public DateTimeOffset Timestamp { get; set; } - - [JsonPropertyName("ciHost")] - public Uri? CiHost { get; set; } - - [JsonPropertyName("name")] - public string? Name { get; set; } -} - -public class Header -{ - [JsonPropertyName("modelClass")] - public string? ModelClass { get; set; } - - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("value")] - public string? Value { get; set; } -} - -public class Id -{ - [JsonPropertyName("timestamp")] - public long Timestamp { get; set; } - - [JsonPropertyName("counter")] - public long Counter { get; set; } - - [JsonPropertyName("time")] - public long Time { get; set; } - - [JsonPropertyName("date")] - public DateTimeOffset Date { get; set; } - - [JsonPropertyName("machineIdentifier")] - public long MachineIdentifier { get; set; } - - [JsonPropertyName("processIdentifier")] - public long ProcessIdentifier { get; set; } - - [JsonPropertyName("timeSecond")] - public long TimeSecond { get; set; } -} - -public class UriClass -{ - [JsonPropertyName("modelClass")] - public string? ModelClass { get; set; } - - [JsonPropertyName("scheme")] - public string? Scheme { get; set; } - - [JsonPropertyName("host")] - public string? Host { get; set; } - - [JsonPropertyName("path")] - public string? Path { get; set; } - - [JsonPropertyName("query")] - public string? Query { get; set; } -} \ No newline at end of file diff --git a/src/Explore.Cli/MappingHelper.cs b/src/Explore.Cli/MappingHelper.cs index 50ad0db..9bbc4d9 100644 --- a/src/Explore.Cli/MappingHelper.cs +++ b/src/Explore.Cli/MappingHelper.cs @@ -2,257 +2,6 @@ public static class MappingHelper { - public static bool CollectionEntriesNotLimitedToSoap(List? entries) - { - if(entries == null) - { - return false; - } - - return entries.Exists(c => c.Type?.ToUpper() != "SOAP" && c.Type?.ToUpper() != "WSDL") ? true : false; - } - - public static Connection MapInspectorCollectionEntryToExploreConnection(CollectionEntry entry) - { - - return new Connection() - { - Type = "ConnectionRequest", - Name = "REST", - Schema = "OpenAPI", - SchemaVersion = "3.0.1", - ConnectionDefinition = new ConnectionDefinition() - { - Servers = new List() - { - new Server() - { - Url = $"{entry.Uri?.Scheme + "://" + entry.Uri?.Host}" - } - }, - Paths = CreatePathsDictionary(entry) - }, - Settings = new Settings() - { - Type = "RestConnectionSettings", - ConnectTimeout = 30, - FollowRedirects = true, - EncodeUrl = true - }, - Credentials = MapInspectorAuthenticationToCredentials(entry.Authentication) - }; - - } - - public static Credentials? MapInspectorAuthenticationToCredentials(string? authentication) - { - if(!string.IsNullOrEmpty(authentication)) - { - var parsedAuth = string.Empty; - var authenticationKvp = authentication.Split("/"); - - if(authentication.Any()) - { - switch(authenticationKvp[0].ToLowerInvariant()) - { - case ("oauth 2.0"): - return new Credentials() { Type = "TokenCredentials", Token = $"{authenticationKvp[1]}" }; - case ("basic authentication"): - var basicAuth = authenticationKvp[1].Split(":"); - return new Credentials() { Type = "BasicAuthCredentials", Username = $"{basicAuth[0]}", Password = $"{basicAuth[1]}"}; - } - } - } - - return null; - } - - public static string ParseInspectorAuthentication(string authentication) - { - var parsedAuth = string.Empty; - var authenticationKvp = authentication.Split("/"); - - if(authentication.Any()) - { - switch(authenticationKvp[0].ToLowerInvariant()) - { - case ("oauth 2.0"): - parsedAuth = $"Bearer {authenticationKvp[1]}"; - break; - case ("basic authentication"): - parsedAuth = $"Basic {authenticationKvp[1]}"; - break; - } - } - - return parsedAuth; - } - - public static List MapInspectorParamsToExploreParams(CollectionEntry entry) - { - List parameters = new List(); - - if(entry.Headers != null) - { - // map the headers - foreach(var header in entry.Headers) - { - parameters.Add(new Parameter() - { - In = "header", - Name = header.Name, - Examples = new Examples() - { - Example = new Example() - { - Value = header.Value - } - } - }); - } - - // map the authorization property - //if(!string.IsNullOrEmpty(entry.Authentication)) - //{ - // parameters.Add(new Parameter() - // { - // In = "Header", - // Name = "Authorization", - // Examples = new Examples() - // { - // Example = new Example() - // { - // Value = ParseInspectorAuthentication(entry.Authentication) - // } - // } - // - // }); - //} - } - - // parse and map the query string - if(entry.Uri != null) - { - if(!string.IsNullOrEmpty(entry.Uri.Query)) - { - var queryParams = entry.Uri.Query.Split('&'); - - foreach(var param in queryParams) - { - var keyValue = param.Split('='); - - parameters.Add(new Parameter() - { - In = "query", - Name = keyValue[0], - Examples = new Examples() - { - Example = new Example() - { - Value = keyValue[1] ?? string.Empty - } - } - }); - } - } - } - - return parameters; - } - - public static Components? CreateComponentsFromInspectorAuthentication(string authentication) - { - if(!string.IsNullOrEmpty(authentication)) - { - var parsedAuth = string.Empty; - var authenticationKvp = authentication.Split("/"); - - if(authentication.Any()) - { - switch(authenticationKvp[0].ToLowerInvariant()) - { - case ("oauth 2.0"): - return new Components() - { - SecuritySchemes = new SecuritySchemes() - { - TokenCredentials = new SecuritySchemeTokenCredentials() - { - Type = "http", - Scheme = "bearer" - } - } - }; - case ("basic authentication"): - return new Components() - { - SecuritySchemes = new SecuritySchemes() - { - BasicAuthCredentials = new SecuritySchemeBasicAuthCredentials() - { - Type = "http", - Scheme = "basic" - } - } - }; - } - } - } - - return null; - } - public static Dictionary CreatePathsDictionary(CollectionEntry entry) - { - if(entry.Uri != null) - { - if(!string.IsNullOrEmpty(entry.Method) && !string.IsNullOrEmpty(entry.Uri?.Path)) - { - var pathsContent = new PathsContent() - { - Parameters = MapInspectorParamsToExploreParams(entry) - }; - - //add request body - if(!string.IsNullOrEmpty(entry.Body)) - { - var examplesJson = new Dictionary(); - examplesJson.Add("examples", MapEntryBodyToContentExamples(entry.Body)); - - var contentJson = new Dictionary(); - contentJson.Add("*/*", examplesJson); - - pathsContent.RequestBody = new RequestBody() - { - Content = contentJson - }; - } - - // add header and query params - var methodJson = new Dictionary(); - //methodJson.Add(entry.Method, MapInspectorParamsToExploreParams(entry)); - methodJson.Add(entry.Method.ToLowerInvariant(), pathsContent); - - var json = new Dictionary(); - json.Add(entry.Uri.Path, methodJson); - - return json; - } - } - - return new Dictionary(); - } - - public static Examples MapEntryBodyToContentExamples(string body) - { - return new Examples() - { - Example = new Example() - { - Value = body ?? string.Empty - } - }; - } - public static Connection MassageConnectionExportForImport(Connection? exportedConnection) { if(exportedConnection == null) @@ -266,10 +15,4 @@ public static Connection MassageConnectionExportForImport(Connection? exportedCo return exportedConnection; } - public static string? ExtractXSRFTokenFromCookie(string sessionCookie) - { - var cookieComponents = sessionCookie.Split(";"); - var xsrfComponent = cookieComponents.FirstOrDefault(x => x.Trim().ToUpperInvariant().StartsWith("XSRF-TOKEN")); - return xsrfComponent?.Split("=")[1]; - } } \ No newline at end of file diff --git a/src/Explore.Cli/PostmanCollectionContract.cs b/src/Explore.Cli/PostmanCollectionContract.cs new file mode 100644 index 0000000..625520e --- /dev/null +++ b/src/Explore.Cli/PostmanCollectionContract.cs @@ -0,0 +1,175 @@ +#nullable enable + +using System.Text.Json.Serialization; + +public class PostmanCollection +{ + [JsonPropertyName("info")] + public PostmanCollectionInfo? Info { get; set; } + + [JsonPropertyName("item")] + public List? Item { get; set; } +} + +public partial class PostmanCollectionInfo +{ + [JsonPropertyName("_postman_id")] + public string? PostmanId { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("schema")] + public string? Schema { get; set; } + + [JsonPropertyName("version")] + public string? Version { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } +} + +public class Item +{ + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("request")] + public Request? Request { get; set; } + + [JsonPropertyName("item")] + public List? ItemList { get; set; } + +} + +public class Request +{ + [JsonPropertyName("method")] + public string? Method { get; set; } + + [JsonPropertyName("header")] + public List
? Header { get; set; } + + [JsonPropertyName("body")] + public Body? Body { get; set; } + + [JsonPropertyName("url")] + public Url? Url { get; set; } + + [JsonPropertyName("description")] + public Description? Description { get; set; } +} + +public class Header +{ + [JsonPropertyName("key")] + public string? Key { get; set; } + + [JsonPropertyName("value")] + public string? Value { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } +} + +public class Body +{ + [JsonPropertyName("mode")] + public string? Mode { get; set; } + + [JsonPropertyName("raw")] + public string? Raw { get; set; } + + [JsonPropertyName("formdata")] + public List? Formdata { get; set; } + + [JsonPropertyName("urlencoded")] + public List? Urlencoded { get; set; } + + [JsonPropertyName("graphql")] + public Graphql? GraphQL { get; set; } +} + +public class Formdata +{ + [JsonPropertyName("key")] + public string? Key { get; set; } + + [JsonPropertyName("value")] + public string? Value { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } +} + +public class Urlencoded +{ + [JsonPropertyName("key")] + public string? Key { get; set; } + + [JsonPropertyName("value")] + public string? Value { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } +} + +public class Graphql +{ + [JsonPropertyName("query")] + public string? Query { get; set; } + + [JsonPropertyName("variables")] + public string? Variables { get; set; } + + [JsonPropertyName("operationName")] + public string? OperationName { get; set; } +} + +public class Url +{ + [JsonPropertyName("raw")] + public string? Raw { get; set; } + + [JsonPropertyName("protocol")] + public string? Protocol { get; set; } + + [JsonPropertyName("host")] + public List? Host { get; set; } + + [JsonPropertyName("port")] + public string? Port { get; set; } + + [JsonPropertyName("path")] + public List? Path { get; set; } + + [JsonPropertyName("query")] + public List? Query { get; set; } +} + +public class Query +{ + [JsonPropertyName("key")] + public string? Key { get; set; } + + [JsonPropertyName("value")] + public string? Value { get; set; } +} + +public class Description +{ + [JsonPropertyName("content")] + public string? Content { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; set; } +} diff --git a/src/Explore.Cli/PostmanCollectionMappingHelper.cs b/src/Explore.Cli/PostmanCollectionMappingHelper.cs new file mode 100644 index 0000000..9bc11db --- /dev/null +++ b/src/Explore.Cli/PostmanCollectionMappingHelper.cs @@ -0,0 +1,293 @@ +using System.Net.NetworkInformation; +using System.Text.Json; +using Explore.Cli.Models; +using Namotion.Reflection; + +public static class PostmanCollectionMappingHelper +{ + public static Connection MapPostmanCollectionItemToExploreConnection(Item postmanCollectionItem) + { + return new Connection() + { + Type = "ConnectionRequest", + Name = "REST", + Schema = "OpenAPI", + SchemaVersion = "3.0.1", + ConnectionDefinition = new ConnectionDefinition() + { + Servers = new List() + { + new Server() + { + Url = GetServerUrlFromItemRequest(postmanCollectionItem.Request) + } + }, + Paths = CreatePathsDictionary(postmanCollectionItem.Request), + }, + Settings = new Settings() + { + Type = "RestConnectionSettings", + ConnectTimeout = 30, + FollowRedirects = true, + EncodeUrl = true + }, + }; + } + + public static string GetServerUrlFromItemRequest(Request? request) + { + if(request == null || request.Url == null || string.IsNullOrEmpty(request.Url.Raw)) + { + return string.Empty; + } + + var host = string.Join(".", request.Url?.Host ?? Enumerable.Empty()); + var serverUrl = $"{request.Url?.Protocol}://{host}"; + + if(!string.IsNullOrEmpty(request.Url?.Port)) + { + serverUrl += $":{request.Url.Port}"; + } + + return serverUrl; + } + + public static List MapHeaderAndQueryParams(Request? request) + { + List parameters = new List(); + + if(request?.Header != null && request.Header.Any()) + { + // map the headers + foreach(var hdr in request.Header) + { + parameters.Add(new Parameter() + { + In = "header", + Name = hdr.Key, + Examples = new Examples() + { + Example = new Example() + { + Value = hdr.Value + } + } + }); + } + } + + // if we have urlencoded body then force the content type header as plaintext (Explore doesn't support urlencoded natively) + if(request?.Body != null && request.Body.Mode != null && request.Body.Mode.Equals("urlencoded", StringComparison.OrdinalIgnoreCase)) + { + parameters.Add(new Parameter() + { + In = "header", + Name = "Content-Type", + Schema = new Schema() + { + type = "string" + }, + Examples = new Examples() + { + Example = new Example() + { + Value = "application/x-www-form-urlencoded" + } + } + }); + } + + // parse and map the query string + if(request?.Url != null) + { + if(request.Url.Query != null) + { + foreach(var param in request.Url.Query) + { + parameters.Add(new Parameter() + { + In = "query", + Name = param.Key, + Examples = new Examples() + { + Example = new Example() + { + Value = param.Value + } + } + }); + } + } + } + + return parameters; + } + + public static Dictionary CreatePathsDictionary(Request? request) + { + + if(request?.Url != null && request.Url.Path != null) + { + var pathsContent = new PathsContent() + { + Parameters = MapHeaderAndQueryParams(request) + }; + + //add request body + if(request.Body != null) + { + if(request.Body.Raw != null) + { + var examplesJson = new Dictionary + { + { "examples", MapEntryBodyToContentExamples(request.Body.Raw) } + }; + + var contentJson = new Dictionary + { + { "*/*", examplesJson } + }; + + pathsContent.RequestBody = new RequestBody() + { + Content = contentJson + }; + } + else if(request.Body.Urlencoded != null) + { + var examplesJson = new Dictionary + { + { "examples", MapUrlEncodedBodyToContentExamples(request.Body.Urlencoded) } + }; + + var contentJson = new Dictionary + { + { "application/x-www-form-urlencoded", examplesJson } + }; + + pathsContent.RequestBody = new RequestBody() + { + Content = contentJson + }; + } + } + + // add header and query params + if(request.Method != null) + { + var methodJson = new Dictionary + { + { request.Method.ToLowerInvariant(), pathsContent } + }; + + var json = new Dictionary + { + { $"/{string.Join("/", request.Url.Path)}", methodJson } + }; + + return json; + } + } + + return new Dictionary(); + } + + public static Examples MapEntryBodyToContentExamples(string? rawBody) + { + return new Examples() + { + Example = new Example() + { + Value = rawBody + } + }; + } + + public static Examples MapUrlEncodedBodyToContentExamples(List? urlEncodedBody) + { + var rawBody = string.Empty; + + if(urlEncodedBody != null) + { + foreach(var param in urlEncodedBody) + { + rawBody += $"{param.Key}={param.Value}&"; + } + } + + return new Examples() + { + Example = new Example() + { + Value = rawBody + } + }; + } + + public static List FlattenItems(List items) + { + var result = new List(); + + foreach (var item in items) + { + // Add the current item to the list if it has request data + if(item.Request != null) + { + result.Add(item); + } + + // If the item has nested items, flatten each one and add it to the list + if (item.ItemList != null) + { + result.AddRange(FlattenItems(item.ItemList)); + } + } + + return result; + } + + public static bool IsItemRequestModeSupported(Request request) + { + if(request.Body != null && request.Body.Mode != null && request.Url != null && request.Url.Protocol != null) + { + // if the request body mode is not raw or urlencoded and the protocol is not http or https, return false + if(!(request.Body.Mode.Equals("raw", StringComparison.OrdinalIgnoreCase) || request.Body.Mode.Equals("urlencoded", StringComparison.OrdinalIgnoreCase) || request.Body.Mode.Equals("formdata", StringComparison.OrdinalIgnoreCase)) && request.Url.Protocol.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + else + { + return true; + } + } + + return true; + } + + public static bool IsCollectionVersion2_1(string json) + { + var jsonObject = JsonSerializer.Deserialize>(json); + + if(jsonObject != null && jsonObject.ContainsKey("info")) + { + + var info = JsonSerializer.Deserialize>(jsonObject["info"].ToString() ?? string.Empty); + + + if(info != null && info.ContainsKey("schema")) + { + if(info["schema"] != null && info["schema"].ToString() != null) + { + var schema = info["schema"].ToString(); + if(string.Equals(schema, "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + } + } + + return false; + } +} \ No newline at end of file diff --git a/src/Explore.Cli/Program.cs b/src/Explore.Cli/Program.cs index 89cb7c5..84bb9a7 100644 --- a/src/Explore.Cli/Program.cs +++ b/src/Explore.Cli/Program.cs @@ -1,4 +1,4 @@ -using System.CommandLine; +using System.CommandLine; using System.Net; using System.Net.Http.Json; using System.Text; @@ -10,9 +10,11 @@ internal class Program { public static async Task Main(string[] args) { - var rootCommand = new RootCommand(); - rootCommand.Name = "Explore.CLI"; - rootCommand.Description = "Simple utility CLI for importing data into and out of SwaggerHub Explore"; + var rootCommand = new RootCommand + { + Name = "Explore.CLI", + Description = "Simple utility CLI for importing data into and out of SwaggerHub Explore" + }; var exploreCookie = new Option(name: "--explore-cookie", description: "A valid and active SwaggerHub Explore session cookie") { IsRequired = true }; exploreCookie.AddAlias("-ec"); @@ -46,11 +48,227 @@ public static async Task Main(string[] args) importSpacesCommand.SetHandler(async (ec, fp, v, n) => { await ImportSpaces(ec, fp, v, n); }, exploreCookie, importFilePath, names, verbose); + var importPostmanCollectionCommand = new Command("import-postman-collection") { exploreCookie, importFilePath, verbose }; + importPostmanCollectionCommand.Description = "Import a Postman collection v2.1 into SwaggerHub Explore"; + rootCommand.Add(importPostmanCollectionCommand); + + importPostmanCollectionCommand.SetHandler(async (ec, fp, v) => + { await ImportPostmanCollection(ec, fp, v); }, exploreCookie, importFilePath, verbose); + AnsiConsole.Write(new FigletText("Explore.Cli").Color(new Color(133, 234, 45))); return await rootCommand.InvokeAsync(args); } + internal static async Task ImportPostmanCollection(string exploreCookie, string filePath, bool? verboseOutput) + { + //check file existence and read permissions + try + { + using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + { + //let's verify it's a JSON file now + if (!UtilityHelper.IsJsonFile(filePath)) + { + AnsiConsole.MarkupLine($"[red]The file provided is not a JSON file. Please review.[/]"); + return; + } + + // You can read from the file if this point is reached + AnsiConsole.MarkupLine($"processing ..."); + } + } + catch (UnauthorizedAccessException) + { + AnsiConsole.MarkupLine($"[red]Access to {filePath} is denied. Please review file permissions any try again.[/]"); + return; + } + catch (FileNotFoundException) + { + AnsiConsole.MarkupLine($"[red]The file {filePath} does not exist. Please review the provided file path.[/]"); + return; + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]An error occurred accessing the file: {ex.Message}[/]"); + return; + } + + try + { + //validate collection against postman collection schema + string json = File.ReadAllText(filePath); + + if(!PostmanCollectionMappingHelper.IsCollectionVersion2_1(json)) + { + Console.WriteLine($"The provided JSON does not conform to the expected schema. Errors: Only Postman Collection v2.1 are supported at this time."); + return; + } + + var postmanCollection = JsonSerializer.Deserialize(json); + + //validate json against known (high level) schema + var validationResult = await UtilityHelper.ValidateSchema(json, "postman"); + + if (!validationResult.isValid) + { + Console.WriteLine($"The provide json does not conform to the expected schema. Errors: {validationResult.Message}"); + return; + } + + var panel = new Panel($"You have [green]{postmanCollection!.Item!.Count} items[/] to import") + { + Width = 100, + Header = new PanelHeader("Postman Collection Data").Centered() + }; + AnsiConsole.Write(panel); + Console.WriteLine(""); + + var exploreHttpClient = new HttpClient + { + BaseAddress = new Uri("https://api.explore.swaggerhub.com") + }; + + //iterate over the items and import + if(postmanCollection != null && postmanCollection.Item != null) + { + //create an initial space to hold the collection items + var resultTable = new Table() { Title = new TableTitle(text: $"PROCESSING [green]{postmanCollection.Info?.Name}[/]"), Width = 100, UseSafeBorder = true }; + resultTable.AddColumn("Result"); + resultTable.AddColumn(new TableColumn("Details").Centered()); + + var cleanedCollectionName = UtilityHelper.CleanString(postmanCollection.Info?.Name); + var spaceContent = new StringContent(JsonSerializer.Serialize(new SpaceRequest() { Name = cleanedCollectionName }), Encoding.UTF8, "application/json"); + + exploreHttpClient.DefaultRequestHeaders.Clear(); + exploreHttpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + exploreHttpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + var spacesResponse = await exploreHttpClient.PostAsync("/spaces-api/v1/spaces", spaceContent); + + if (spacesResponse.StatusCode == HttpStatusCode.Created) + { + var apiImportResults = new Table() { Title = new TableTitle(text: $"SPACE [green]{cleanedCollectionName}[/] CREATED"), Width = 75, UseSafeBorder = true }; + apiImportResults.AddColumn("Result"); + apiImportResults.AddColumn("API Imported"); + apiImportResults.AddColumn("Connection Imported"); + + //parse the response + var spaceResponse = spacesResponse.Content.ReadFromJsonAsync(); + + //Postman Items cant contain nested items, so we can flatten the list + var flattenedItems = PostmanCollectionMappingHelper.FlattenItems(postmanCollection.Item); + + foreach(var item in flattenedItems) + { + if(item.Request != 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; + } + + //now let's create an API entry in the space + var cleanedAPIName = UtilityHelper.CleanString(item.Name); + var apiContent = new StringContent(JsonSerializer.Serialize(new ApiRequest() { Name = cleanedAPIName, Type = "REST", Description = $"imported from postman on {DateTime.UtcNow.ToShortDateString()}" }), Encoding.UTF8, "application/json"); + + exploreHttpClient.DefaultRequestHeaders.Clear(); + exploreHttpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + exploreHttpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + var apiResponse = await exploreHttpClient.PostAsync($"/spaces-api/v1/spaces/{spaceResponse.Result?.Id}/apis", apiContent); + + if (apiResponse.StatusCode == HttpStatusCode.Created) + { + var createdApiResponse = apiResponse.Content.ReadFromJsonAsync(); + var connectionRequestBody = JsonSerializer.Serialize(PostmanCollectionMappingHelper.MapPostmanCollectionItemToExploreConnection(item)); + var connectionContent = new StringContent(connectionRequestBody, Encoding.UTF8, "application/json"); + + //now let's do the work and import the connection + exploreHttpClient.DefaultRequestHeaders.Clear(); + exploreHttpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); + exploreHttpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + var connectionResponse = await exploreHttpClient.PostAsync($"/spaces-api/v1/spaces/{spaceResponse.Result?.Id}/apis/{createdApiResponse.Result?.Id}/connections", connectionContent); + + if (connectionResponse.StatusCode == HttpStatusCode.Created) + { + apiImportResults.AddRow("[green]OK[/]", $"API '{cleanedAPIName}' created", "Connection created"); + } + else + { + apiImportResults.AddRow("[orange3]OK[/]", $"API '{cleanedAPIName}' created", "[orange3]Connection NOT created[/]"); + } + } + else + { + apiImportResults.AddRow("[red]NOK[/]", $"API creation failed. StatusCode {apiResponse.StatusCode}", ""); + } + } + } + + + resultTable.AddRow(new Markup("[green]success[/]"), apiImportResults); + + if (verboseOutput == null || verboseOutput == false) + { + AnsiConsole.MarkupLine($"[green]\u2713 [/]{cleanedCollectionName}"); + } + + } + else + { + switch (spacesResponse.StatusCode) + { + case HttpStatusCode.OK: + // not expecting a 200 OK here - this would be returned for a failed auth and a redirect to SB ID + resultTable.AddRow(new Markup("[red]failure[/]"), new Markup($"[red] Auth failed connecting to SwaggerHub Explore. Please review provided cookie.[/]")); + AnsiConsole.Write(resultTable); + Console.WriteLine(""); + break; + + case HttpStatusCode.Conflict: + var apiImportResults = new Table() { Title = new TableTitle(text: $"[orange3]SPACE[/] {cleanedCollectionName} [orange3]ALREADY EXISTS[/]") }; + apiImportResults.AddColumn("Result"); + apiImportResults.AddColumn("API Imported"); + apiImportResults.AddColumn("Connection Imported"); + apiImportResults.AddRow("skipped", "", ""); + + resultTable.AddRow(new Markup("[orange3]skipped[/]"), apiImportResults); + AnsiConsole.Write(resultTable); + Console.WriteLine(""); + break; + + default: + resultTable.AddRow(new Markup("[red]failure[/]"), new Markup($"[red] StatusCode: {spacesResponse.StatusCode}, Details: {spacesResponse.ReasonPhrase}[/]")); + AnsiConsole.Write(resultTable); + Console.WriteLine(""); + break; + } + } + + if (verboseOutput != null && verboseOutput == true) + { + AnsiConsole.Write(resultTable); + } + + } + + Console.WriteLine(); + AnsiConsole.MarkupLine($"[green]Import completed[/]"); + + //ToDo - deal with scenario of item-groups + } + catch (FileNotFoundException) + { + Console.WriteLine("File not found."); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + } + + } + internal static async Task ExportSpaces(string exploreCookie, string filePath, string exportFileName, string names, bool? verboseOutput) { var httpClient = new HttpClient @@ -59,7 +277,7 @@ internal static async Task ExportSpaces(string exploreCookie, string filePath, s }; httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); - httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{MappingHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); //get spaces var spacesResponse = await httpClient.GetAsync("/spaces-api/v1/spaces?page=0&size=2000"); @@ -131,7 +349,7 @@ internal static async Task ExportSpaces(string exploreCookie, string filePath, s // get the APIs httpClient.DefaultRequestHeaders.Clear(); httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); - httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{MappingHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); //get space APIs var apisResponse = await httpClient.GetAsync($"/spaces-api/v1/spaces/{space.Id}/apis?page=0&size=2000"); @@ -157,7 +375,7 @@ internal static async Task ExportSpaces(string exploreCookie, string filePath, s // get the API connections httpClient.DefaultRequestHeaders.Clear(); httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); - httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{MappingHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); var connectionsResponse = await httpClient.GetAsync($"/spaces-api/v1/spaces/{space.Id}/apis/{api.Id}/connections?page=0&size=2000"); if (connectionsResponse.StatusCode == HttpStatusCode.OK) @@ -277,7 +495,7 @@ internal static async Task ImportSpaces(string exploreCookie, string filePath, s var exportedSpaces = JsonSerializer.Deserialize(json); //validate json against known (high level) schema - var validationResult = await UtilityHelper.ValidateSchema(json, "ExploreSpaces.schema.json"); + var validationResult = await UtilityHelper.ValidateSchema(json, "explore"); if (!validationResult.isValid) { @@ -285,9 +503,11 @@ internal static async Task ImportSpaces(string exploreCookie, string filePath, s return; } - var panel = new Panel($"You have [green]{exportedSpaces!.ExploreSpaces!.Count} spaces[/] to import"); - panel.Width = 100; - panel.Header = new PanelHeader("SwaggerHub Explore Data").Centered(); + var panel = new Panel($"You have [green]{exportedSpaces!.ExploreSpaces!.Count} spaces[/] to import") + { + Width = 100, + Header = new PanelHeader("SwaggerHub Explore Data").Centered() + }; AnsiConsole.Write(panel); Console.WriteLine(""); @@ -346,7 +566,10 @@ internal static async Task ImportSpaces(string exploreCookie, string filePath, s } } } - apiImportResults.AddRow("[orange3]skipped[/]", $"API '{exportedAPI.Name}' skipped", $"Kafka not yet supported by export"); + else + { + apiImportResults.AddRow("[orange3]skipped[/]", $"API '{exportedAPI.Name}' skipped", $"Kafka not yet supported by export"); + } } } @@ -392,7 +615,7 @@ private static async Task CheckSpaceExists(string exploreCookie, string? i }; httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); - httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{MappingHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); var spacesResponse = await httpClient.GetAsync($"/spaces-api/v1/spaces/{id}"); @@ -428,7 +651,7 @@ private static async Task CheckApiExists(string exploreCookie, string spac }; httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); - httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{MappingHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); var spacesResponse = await httpClient.GetAsync($"/spaces-api/v1/spaces/{spaceId}/apis/{id}"); @@ -458,7 +681,7 @@ private static async Task CheckConnectionExists(string exploreCookie, stri }; httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); - httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{MappingHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); var response = await httpClient.GetAsync($"/spaces-api/v1/spaces/{spaceId}/apis/{apiId}/connections/{id}"); @@ -490,7 +713,7 @@ private static async Task UpsertSpace(string exploreCookie, bool ), Encoding.UTF8, "application/json"); httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); - httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{MappingHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); HttpResponseMessage? spacesResponse; @@ -542,7 +765,7 @@ private static async Task UpsertApi(string exploreCookie, bool spac ), Encoding.UTF8, "application/json"); httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); - httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{MappingHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); HttpResponseMessage? apiResponse; @@ -588,7 +811,7 @@ private static async Task UpsertConnection(string exploreCookie, bool spac }; httpClient.DefaultRequestHeaders.Add("Cookie", exploreCookie); - httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{MappingHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); + httpClient.DefaultRequestHeaders.Add("X-Xsrf-Token", $"{UtilityHelper.ExtractXSRFTokenFromCookie(exploreCookie)}"); var connectionContent = new StringContent(JsonSerializer.Serialize(MappingHelper.MassageConnectionExportForImport(connection)), Encoding.UTF8, "application/json"); diff --git a/src/Explore.Cli/UtilityHelper.cs b/src/Explore.Cli/UtilityHelper.cs index ff3d4cb..9ee37bd 100644 --- a/src/Explore.Cli/UtilityHelper.cs +++ b/src/Explore.Cli/UtilityHelper.cs @@ -68,103 +68,1299 @@ public static bool IsJsonFile(string filePath) return false; } - public static async Task ValidateSchema(string jsonAsString, string schemaName) + private static string GetSchemaByApplicationName(string name) { - var validationResult = new SchemaValidationResult(); - var schemaAsString = @"{ - ""$schema"": ""https://json-schema.org/draft/2019-09/schema"", - ""type"": ""object"", - ""description"": ""an object storing SwaggerHub Explore spaces which have been exported (or crafted to import via the Explore.cli)."", - ""properties"": { - ""info"": { - ""type"": ""object"", - ""properties"": { - ""version"": { - ""type"": ""string"", - ""description"": ""the version of the explore spaces export/import capability"", - ""pattern"": ""^([0-9]+)\\.([0-9]+)\\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+[0-9A-Za-z-]+)?$"", - ""example"": ""0.0.1"" - }, - ""exportedAt"": { - ""type"": ""string"", - ""description"": ""the timestamp when the export was created"" - } - }, - ""required"": [ - ""version"" - ] - }, - ""exploreSpaces"": { - ""type"": ""array"", - ""description"": ""an array of exported SwaggerHub Explore spaces, apis, and connections"", - ""items"": [ - { + return name switch + { + "explore" => @"{ + ""$schema"": ""https://json-schema.org/draft/2019-09/schema"", ""type"": ""object"", - ""description"": ""a SwaggerHub Explore space"", + ""description"": ""an object storing SwaggerHub Explore spaces which have been exported (or crafted to import via the Explore.cli)."", ""properties"": { - ""id"": { - ""type"": ""string"", - ""description"": ""the space identifier"", - ""pattern"": ""^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"" - }, - ""name"": { - ""type"": ""string"", - ""description"": ""the name of the space"" + ""info"": { + ""type"": ""object"", + ""properties"": { + ""version"": { + ""type"": ""string"", + ""description"": ""the version of the explore spaces export/import capability"", + ""pattern"": ""^([0-9]+)\\.([0-9]+)\\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+[0-9A-Za-z-]+)?$"", + ""example"": ""0.0.1"" + }, + ""exportedAt"": { + ""type"": ""string"", + ""description"": ""the timestamp when the export was created"" + } + }, + ""required"": [ + ""version"" + ] }, - ""apis"": - { + ""exploreSpaces"": { ""type"": ""array"", - ""description"": ""apis contained within a space"", + ""description"": ""an array of exported SwaggerHub Explore spaces, apis, and connections"", ""items"": [ { ""type"": ""object"", - ""description"": ""an API contained within a space"", + ""description"": ""a SwaggerHub Explore space"", ""properties"": { ""id"": { ""type"": ""string"", - ""description"": ""the api identifier"", + ""description"": ""the space identifier"", ""pattern"": ""^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"" }, ""name"": { ""type"": ""string"", - ""description"": ""the name of the api"" - }, - ""type"": { - ""type"": ""string"", - ""description"": ""the type of API"", - ""enum"": [""REST"", ""KAFKA"", ""OTHER""] + ""description"": ""the name of the space"" }, - ""connections"": { + ""apis"": + { ""type"": ""array"", - ""description"": ""an array of connections to an API"", + ""description"": ""apis contained within a space"", ""items"": [ { - ""type"": ""object"" + ""type"": ""object"", + ""description"": ""an API contained within a space"", + ""properties"": { + ""id"": { + ""type"": ""string"", + ""description"": ""the api identifier"", + ""pattern"": ""^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"" + }, + ""name"": { + ""type"": ""string"", + ""description"": ""the name of the api"" + }, + ""type"": { + ""type"": ""string"", + ""description"": ""the type of API"", + ""enum"": [""REST"", ""KAFKA"", ""OTHER""] + }, + ""connections"": { + ""type"": ""array"", + ""description"": ""an array of connections to an API"", + ""items"": [ + { + ""type"": ""object"" + } + ] + } + }, + ""required"": [ + ""name"", + ""type"" + ] } ] } }, ""required"": [ ""name"", - ""type"" + ""apis"" ] } ] } }, ""required"": [ - ""name"", - ""apis"" + ""info"", + ""exploreSpaces"" ] - } - ] + }", + "postman" => @"{ + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""https://schema.getpostman.com/json/collection/v2.1.0/"", + ""type"": ""object"", + ""properties"": { + ""info"": { + ""$ref"": ""#/definitions/info"" + }, + ""item"": { + ""type"": ""array"", + ""description"": ""Items are the basic unit for a Postman collection. You can think of them as corresponding to a single API endpoint. Each Item has one request and may have multiple API responses associated with it."", + ""items"": { + ""title"": ""Items"", + ""oneOf"": [ + { + ""$ref"": ""#/definitions/item"" + }, + { + ""$ref"": ""#/definitions/item-group"" + } + ] + } + }, + ""event"": { + ""$ref"": ""#/definitions/event-list"" + }, + ""variable"": { + ""$ref"": ""#/definitions/variable-list"" + }, + ""auth"": { + ""oneOf"": [ + { + ""type"": ""null"" + }, + { + ""$ref"": ""#/definitions/auth"" + } + ] + }, + ""protocolProfileBehavior"": { + ""$ref"": ""#/definitions/protocol-profile-behavior"" + } + }, + ""required"": [ + ""info"", + ""item"" + ], + ""definitions"": { + ""auth-attribute"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""type"": ""object"", + ""title"": ""Auth"", + ""id"": ""#/definitions/auth-attribute"", + ""description"": ""Represents an attribute for any authorization method provided by Postman. For example `username` and `password` are set as auth attributes for Basic Authentication method."", + ""properties"": { + ""key"": { + ""type"": ""string"" + }, + ""value"": {}, + ""type"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""key"" + ] + }, + ""auth"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""type"": ""object"", + ""title"": ""Auth"", + ""id"": ""#/definitions/auth"", + ""description"": ""Represents authentication helpers provided by Postman"", + ""properties"": { + ""type"": { + ""type"": ""string"", + ""enum"": [ + ""apikey"", + ""awsv4"", + ""basic"", + ""bearer"", + ""digest"", + ""edgegrid"", + ""hawk"", + ""noauth"", + ""oauth1"", + ""oauth2"", + ""ntlm"" + ] + }, + ""noauth"": {}, + ""apikey"": { + ""type"": ""array"", + ""title"": ""API Key Authentication"", + ""description"": ""The attributes for API Key Authentication."", + ""items"": { + ""$ref"": ""#/definitions/auth-attribute"" + } + }, + ""awsv4"": { + ""type"": ""array"", + ""title"": ""AWS Signature v4"", + ""description"": ""The attributes for [AWS Auth](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html)."", + ""items"": { + ""$ref"": ""#/definitions/auth-attribute"" + } + }, + ""basic"": { + ""type"": ""array"", + ""title"": ""Basic Authentication"", + ""description"": ""The attributes for [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)."", + ""items"": { + ""$ref"": ""#/definitions/auth-attribute"" + } + }, + ""bearer"": { + ""type"": ""array"", + ""title"": ""Bearer Token Authentication"", + ""description"": ""The helper attributes for [Bearer Token Authentication](https://tools.ietf.org/html/rfc6750)"", + ""items"": { + ""$ref"": ""#/definitions/auth-attribute"" + } + }, + ""digest"": { + ""type"": ""array"", + ""title"": ""Digest Authentication"", + ""description"": ""The attributes for [Digest Authentication](https://en.wikipedia.org/wiki/Digest_access_authentication)."", + ""items"": { + ""$ref"": ""#/definitions/auth-attribute"" + } + }, + ""edgegrid"": { + ""type"": ""array"", + ""title"": ""EdgeGrid Authentication"", + ""description"": ""The attributes for [Akamai EdgeGrid Authentication](https://developer.akamai.com/legacy/introduction/Client_Auth.html)."", + ""items"": { + ""$ref"": ""#/definitions/auth-attribute"" + } + }, + ""hawk"": { + ""type"": ""array"", + ""title"": ""Hawk Authentication"", + ""description"": ""The attributes for [Hawk Authentication](https://github.com/hueniverse/hawk)"", + ""items"": { + ""$ref"": ""#/definitions/auth-attribute"" + } + }, + ""ntlm"": { + ""type"": ""array"", + ""title"": ""NTLM Authentication"", + ""description"": ""The attributes for [NTLM Authentication](https://msdn.microsoft.com/en-us/library/cc237488.aspx)"", + ""items"": { + ""$ref"": ""#/definitions/auth-attribute"" + } + }, + ""oauth1"": { + ""type"": ""array"", + ""title"": ""OAuth1"", + ""description"": ""The attributes for [OAuth2](https://oauth.net/1/)"", + ""items"": { + ""$ref"": ""#/definitions/auth-attribute"" + } + }, + ""oauth2"": { + ""type"": ""array"", + ""title"": ""OAuth2"", + ""description"": ""Helper attributes for [OAuth2](https://oauth.net/2/)"", + ""items"": { + ""$ref"": ""#/definitions/auth-attribute"" + } + } + }, + ""required"": [ + ""type"" + ] + }, + ""certificate-list"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/certificate-list"", + ""title"": ""Certificate List"", + ""description"": ""A representation of a list of ssl certificates"", + ""type"": ""array"", + ""items"": { + ""$ref"": ""#/definitions/certificate"" + } + }, + ""certificate"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/certificate"", + ""title"": ""Certificate"", + ""description"": ""A representation of an ssl certificate"", + ""type"": ""object"", + ""properties"": { + ""name"": { + ""description"": ""A name for the certificate for user reference"", + ""type"": ""string"" + }, + ""matches"": { + ""description"": ""A list of Url match pattern strings, to identify Urls this certificate can be used for."", + ""type"": ""array"", + ""items"": { + ""type"": ""string"", + ""description"": ""An Url match pattern string"" + } + }, + ""key"": { + ""description"": ""An object containing path to file containing private key, on the file system"", + ""type"": ""object"", + ""properties"": { + ""src"": { + ""description"": ""The path to file containing key for certificate, on the file system"" + } + } + }, + ""cert"": { + ""description"": ""An object containing path to file certificate, on the file system"", + ""type"": ""object"", + ""properties"": { + ""src"": { + ""description"": ""The path to file containing key for certificate, on the file system"" + } + } + }, + ""passphrase"": { + ""description"": ""The passphrase for the certificate"", + ""type"": ""string"" + } + } + }, + ""cookie-list"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/cookie-list"", + ""title"": ""Certificate List"", + ""description"": ""A representation of a list of cookies"", + ""type"": ""array"", + ""items"": { + ""$ref"": ""#/definitions/cookie"" + } + }, + ""cookie"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""type"": ""object"", + ""title"": ""Cookie"", + ""id"": ""#/definitions/cookie"", + ""description"": ""A Cookie, that follows the [Google Chrome format](https://developer.chrome.com/extensions/cookies)"", + ""properties"": { + ""domain"": { + ""type"": ""string"", + ""description"": ""The domain for which this cookie is valid."" + }, + ""expires"": { + ""oneOf"": [ + { + ""type"": ""string"" + }, + { + ""type"": ""number"" + } + ], + ""description"": ""When the cookie expires."" + }, + ""maxAge"": { + ""type"": ""string"" + }, + ""hostOnly"": { + ""type"": ""boolean"", + ""description"": ""True if the cookie is a host-only cookie. (i.e. a request's URL domain must exactly match the domain of the cookie)."" + }, + ""httpOnly"": { + ""type"": ""boolean"", + ""description"": ""Indicates if this cookie is HTTP Only. (if True, the cookie is inaccessible to client-side scripts)"" + }, + ""name"": { + ""type"": ""string"", + ""description"": ""This is the name of the Cookie."" + }, + ""path"": { + ""type"": ""string"", + ""description"": ""The path associated with the Cookie."" + }, + ""secure"": { + ""type"": ""boolean"", + ""description"": ""Indicates if the 'secure' flag is set on the Cookie, meaning that it is transmitted over secure connections only. (typically HTTPS)"" + }, + ""session"": { + ""type"": ""boolean"", + ""description"": ""True if the cookie is a session cookie."" + }, + ""value"": { + ""type"": ""string"", + ""description"": ""The value of the Cookie."" + }, + ""extensions"": { + ""type"": ""array"", + ""description"": ""Custom attributes for a cookie go here, such as the [Priority Field](https://code.google.com/p/chromium/issues/detail?id=232693)"" + } + }, + ""required"": [ + ""domain"", + ""path"" + ] + }, + ""description"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/description"", + ""description"": ""A Description can be a raw text, or be an object, which holds the description along with its format."", + ""oneOf"": [ + { + ""type"": ""object"", + ""title"": ""Description"", + ""properties"": { + ""content"": { + ""type"": ""string"", + ""description"": ""The content of the description goes here, as a raw string."" + }, + ""type"": { + ""type"": ""string"", + ""description"": ""Holds the mime type of the raw description content. E.g: 'text/markdown' or 'text/html'.\nThe type is used to correctly render the description when generating documentation, or in the Postman app."" + }, + ""version"": { + ""description"": ""Description can have versions associated with it, which should be put in this property."" + } + } + }, + { + ""type"": ""string"" + }, + { + ""type"": ""null"" + } + ] + }, + ""event-list"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/event-list"", + ""title"": ""Event List"", + ""type"": ""array"", + ""description"": ""Postman allows you to configure scripts to run when specific events occur. These scripts are stored here, and can be referenced in the collection by their ID."", + ""items"": { + ""$ref"": ""#/definitions/event"" + } + }, + ""event"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/event"", + ""title"": ""Event"", + ""description"": ""Defines a script associated with an associated event name"", + ""type"": ""object"", + ""properties"": { + ""id"": { + ""type"": ""string"", + ""description"": ""A unique identifier for the enclosing event."" + }, + ""listen"": { + ""type"": ""string"", + ""description"": ""Can be set to `test` or `prerequest` for test scripts or pre-request scripts respectively."" + }, + ""script"": { + ""$ref"": ""#/definitions/script"" + }, + ""disabled"": { + ""type"": ""boolean"", + ""default"": false, + ""description"": ""Indicates whether the event is disabled. If absent, the event is assumed to be enabled."" + } + }, + ""required"": [ + ""listen"" + ] + }, + ""header-list"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/header-list"", + ""title"": ""Header List"", + ""description"": ""A representation for a list of headers"", + ""type"": ""array"", + ""items"": { + ""$ref"": ""#/definitions/header"" + } + }, + ""header"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""type"": ""object"", + ""title"": ""Header"", + ""id"": ""#/definitions/header"", + ""description"": ""Represents a single HTTP Header"", + ""properties"": { + ""key"": { + ""description"": ""This holds the LHS of the HTTP Header, e.g ``Content-Type`` or ``X-Custom-Header``"", + ""type"": ""string"" + }, + ""value"": { + ""type"": ""string"", + ""description"": ""The value (or the RHS) of the Header is stored in this field."" + }, + ""disabled"": { + ""type"": ""boolean"", + ""default"": false, + ""description"": ""If set to true, the current header will not be sent with requests."" + }, + ""description"": { + ""$ref"": ""#/definitions/description"" + } + }, + ""required"": [ + ""key"", + ""value"" + ] + }, + ""info"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/info"", + ""title"": ""Information"", + ""description"": ""Detailed description of the info block"", + ""type"": ""object"", + ""properties"": { + ""name"": { + ""type"": ""string"", + ""title"": ""Name of the collection"", + ""description"": ""A collection's friendly name is defined by this field. You would want to set this field to a value that would allow you to easily identify this collection among a bunch of other collections, as such outlining its usage or content."" + }, + ""_postman_id"": { + ""type"": ""string"", + ""description"": ""Every collection is identified by the unique value of this field. The value of this field is usually easiest to generate using a UID generator function. If you already have a collection, it is recommended that you maintain the same id since changing the id usually implies that is a different collection than it was originally.\n *Note: This field exists for compatibility reasons with Collection Format V1.*"" + }, + ""description"": { + ""$ref"": ""#/definitions/description"" + }, + ""version"": { + ""$ref"": ""#/definitions/version"" + }, + ""schema"": { + ""description"": ""This should ideally hold a link to the Postman schema that is used to validate this collection. E.g: https://schema.getpostman.com/collection/v1"", + ""type"": ""string"" + } + }, + ""required"": [ + ""name"", + ""schema"" + ] + }, + ""item-group"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""title"": ""Folder"", + ""id"": ""#/definitions/item-group"", + ""description"": ""One of the primary goals of Postman is to organize the development of APIs. To this end, it is necessary to be able to group requests together. This can be achived using 'Folders'. A folder just is an ordered set of requests."", + ""type"": ""object"", + ""properties"": { + ""name"": { + ""type"": ""string"", + ""description"": ""A folder's friendly name is defined by this field. You would want to set this field to a value that would allow you to easily identify this folder."" + }, + ""description"": { + ""$ref"": ""#/definitions/description"" + }, + ""variable"": { + ""$ref"": ""#/definitions/variable-list"" + }, + ""item"": { + ""description"": ""Items are entities which contain an actual HTTP request, and sample responses attached to it. Folders may contain many items."", + ""type"": ""array"", + ""items"": { + ""title"": ""Items"", + ""anyOf"": [ + { + ""$ref"": ""#/definitions/item"" + }, + { + ""$ref"": ""#/definitions/item-group"" + } + ] + } + }, + ""event"": { + ""$ref"": ""#/definitions/event-list"" + }, + ""auth"": { + ""oneOf"": [ + { + ""type"": ""null"" + }, + { + ""$ref"": ""#/definitions/auth"" + } + ] + }, + ""protocolProfileBehavior"": { + ""$ref"": ""#/definitions/protocol-profile-behavior"" + } + }, + ""required"": [ + ""item"" + ] + }, + ""item"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""type"": ""object"", + ""title"": ""Item"", + ""id"": ""#/definitions/item"", + ""description"": ""Items are entities which contain an actual HTTP request, and sample responses attached to it."", + ""properties"": { + ""id"": { + ""type"": ""string"", + ""description"": ""A unique ID that is used to identify collections internally"" + }, + ""name"": { + ""type"": ""string"", + ""description"": ""A human readable identifier for the current item."" + }, + ""description"": { + ""$ref"": ""#/definitions/description"" + }, + ""variable"": { + ""$ref"": ""#/definitions/variable-list"" + }, + ""event"": { + ""$ref"": ""#/definitions/event-list"" + }, + ""request"": { + ""$ref"": ""#/definitions/request"" + }, + ""response"": { + ""type"": ""array"", + ""title"": ""Responses"", + ""items"": { + ""$ref"": ""#/definitions/response"" + } + }, + ""protocolProfileBehavior"": { + ""$ref"": ""#/definitions/protocol-profile-behavior"" + } + }, + ""required"": [ + ""request"" + ] + }, + ""protocol-profile-behavior"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""type"": ""object"", + ""title"": ""Protocol Profile Behavior"", + ""id"": ""#/definitions/protocol-profile-behavior"", + ""description"": ""Set of configurations used to alter the usual behavior of sending the request"" + }, + ""proxy-config"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/proxy-config"", + ""title"": ""Proxy Config"", + ""description"": ""Using the Proxy, you can configure your custom proxy into the postman for particular url match"", + ""type"": ""object"", + ""properties"": { + ""match"": { + ""default"": ""http+https://*/*"", + ""description"": ""The Url match for which the proxy config is defined"", + ""type"": ""string"" + }, + ""host"": { + ""type"": ""string"", + ""description"": ""The proxy server host"" + }, + ""port"": { + ""type"": ""integer"", + ""minimum"": 0, + ""default"": 8080, + ""description"": ""The proxy server port"" + }, + ""tunnel"": { + ""description"": ""The tunneling details for the proxy config"", + ""default"": false, + ""type"": ""boolean"" + }, + ""disabled"": { + ""type"": ""boolean"", + ""default"": false, + ""description"": ""When set to true, ignores this proxy configuration entity"" + } + } + }, + ""request"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/request"", + ""title"": ""Request"", + ""description"": ""A request represents an HTTP request. If a string, the string is assumed to be the request URL and the method is assumed to be 'GET'."", + ""oneOf"": [ + { + ""type"": ""object"", + ""title"": ""Request"", + ""properties"": { + ""url"": { + ""$ref"": ""#/definitions/url"" + }, + ""auth"": { + ""oneOf"": [ + { + ""type"": ""null"" + }, + { + ""$ref"": ""#/definitions/auth"" + } + ] + }, + ""proxy"": { + ""$ref"": ""#/definitions/proxy-config"" + }, + ""certificate"": { + ""$ref"": ""#/definitions/certificate"" + }, + ""method"": { + ""anyOf"": [ + { + ""description"": ""The Standard HTTP method associated with this request."", + ""type"": ""string"", + ""enum"": [ + ""GET"", + ""PUT"", + ""POST"", + ""PATCH"", + ""DELETE"", + ""COPY"", + ""HEAD"", + ""OPTIONS"", + ""LINK"", + ""UNLINK"", + ""PURGE"", + ""LOCK"", + ""UNLOCK"", + ""PROPFIND"", + ""VIEW"" + ] + }, + { + ""description"": ""The Custom HTTP method associated with this request."", + ""type"": ""string"" + } + ] + }, + ""description"": { + ""$ref"": ""#/definitions/description"" + }, + ""header"": { + ""oneOf"": [ + { + ""$ref"": ""#/definitions/header-list"" + }, + { + ""type"": ""string"" + } + ] + }, + ""body"": { + ""oneOf"": [ + { + ""type"": ""object"", + ""description"": ""This field contains the data usually contained in the request body."", + ""properties"": { + ""mode"": { + ""description"": ""Postman stores the type of data associated with this request in this field."", + ""enum"": [ + ""raw"", + ""urlencoded"", + ""formdata"", + ""file"", + ""graphql"" + ] + }, + ""raw"": { + ""type"": ""string"" + }, + ""urlencoded"": { + ""type"": ""array"", + ""items"": { + ""type"": ""object"", + ""title"": ""UrlEncodedParameter"", + ""properties"": { + ""key"": { + ""type"": ""string"" + }, + ""value"": { + ""type"": ""string"" + }, + ""disabled"": { + ""type"": ""boolean"", + ""default"": false + }, + ""description"": { + ""$ref"": ""#/definitions/description"" + } + }, + ""required"": [ + ""key"" + ] + } + }, + ""formdata"": { + ""type"": ""array"", + ""items"": { + ""type"": ""object"", + ""title"": ""FormParameter"", + ""anyOf"": [ + { + ""properties"": { + ""key"": { + ""type"": ""string"" + }, + ""value"": { + ""type"": ""string"" + }, + ""disabled"": { + ""type"": ""boolean"", + ""default"": false, + ""description"": ""When set to true, prevents this form data entity from being sent."" + }, + ""type"": { + ""type"": ""string"", + ""enum"": [ + ""text"" + ] + }, + ""contentType"": { + ""type"": ""string"", + ""description"": ""Override Content-Type header of this form data entity."" + }, + ""description"": { + ""$ref"": ""#/definitions/description"" + } + }, + ""required"": [ + ""key"" + ] + }, + { + ""properties"": { + ""key"": { + ""type"": ""string"" + }, + ""src"": { + ""oneOf"": [ + { + ""type"": ""string"" + }, + { + ""type"": ""null"" + }, + { + ""type"": ""array"" + } + ] + }, + ""disabled"": { + ""type"": ""boolean"", + ""default"": false, + ""description"": ""When set to true, prevents this form data entity from being sent."" + }, + ""type"": { + ""type"": ""string"", + ""enum"": [ + ""file"" + ] + }, + ""contentType"": { + ""type"": ""string"", + ""description"": ""Override Content-Type header of this form data entity."" + }, + ""description"": { + ""$ref"": ""#/definitions/description"" + } + }, + ""required"": [ + ""key"" + ] + } + ] + } + }, + ""file"": { + ""type"": ""object"", + ""properties"": { + ""src"": { + ""oneOf"": [ + { + ""type"": ""string"", + ""description"": ""Contains the name of the file to upload. _Not the path_."" + }, + { + ""type"": ""null"", + ""description"": ""A null src indicates that no file has been selected as a part of the request body"" + } + ] + }, + ""content"": { + ""type"": ""string"" + } + } + }, + ""graphql"": { + ""type"": ""object"" + }, + ""options"": { + ""type"": ""object"", + ""description"": ""Additional configurations and options set for various body modes."" + }, + ""disabled"": { + ""type"": ""boolean"", + ""default"": false, + ""description"": ""When set to true, prevents request body from being sent."" + } + } + }, + { + ""type"": ""null"" + } + ] + } + } + }, + { + ""type"": ""string"" + } + ] + }, + ""response"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/response"", + ""title"": ""Response"", + ""description"": ""A response represents an HTTP response."", + ""properties"": { + ""id"": { + ""description"": ""A unique, user defined identifier that can be used to refer to this response from requests."", + ""type"": ""string"" + }, + ""originalRequest"": { + ""$ref"": ""#/definitions/request"" + }, + ""responseTime"": { + ""title"": ""ResponseTime"", + ""oneOf"": [ + { + ""type"": ""null"" + }, + { + ""type"": ""string"" + }, + { + ""type"": ""number"" + } + ], + ""description"": ""The time taken by the request to complete. If a number, the unit is milliseconds. If the response is manually created, this can be set to `null`."" + }, + ""timings"": { + ""title"": ""Response Timings"", + ""description"": ""Set of timing information related to request and response in milliseconds"", + ""oneOf"": [ + { + ""type"": ""object"" + }, + { + ""type"": ""null"" + } + ] + }, + ""header"": { + ""title"": ""Headers"", + ""oneOf"": [ + { + ""type"": ""array"", + ""title"": ""Header"", + ""description"": ""No HTTP request is complete without its headers, and the same is true for a Postman request. This field is an array containing all the headers."", + ""items"": { + ""oneOf"": [ + { + ""$ref"": ""#/definitions/header"" + }, + { + ""title"": ""Header"", + ""type"": ""string"" + } + ] + } + }, + { + ""type"": ""string"" + }, + { + ""type"": ""null"" + } + ] + }, + ""cookie"": { + ""type"": ""array"", + ""items"": { + ""$ref"": ""#/definitions/cookie"" + } + }, + ""body"": { + ""type"": [ + ""null"", + ""string"" + ], + ""description"": ""The raw text of the response."" + }, + ""status"": { + ""type"": ""string"", + ""description"": ""The response status, e.g: '200 OK'"" + }, + ""code"": { + ""type"": ""integer"", + ""description"": ""The numerical response code, example: 200, 201, 404, etc."" + } + } + }, + ""script"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/script"", + ""title"": ""Script"", + ""type"": ""object"", + ""description"": ""A script is a snippet of Javascript code that can be used to to perform setup or teardown operations on a particular response."", + ""properties"": { + ""id"": { + ""description"": ""A unique, user defined identifier that can be used to refer to this script from requests."", + ""type"": ""string"" + }, + ""type"": { + ""description"": ""Type of the script. E.g: 'text/javascript'"", + ""type"": ""string"" + }, + ""exec"": { + ""oneOf"": [ + { + ""type"": ""array"", + ""description"": ""This is an array of strings, where each line represents a single line of code. Having lines separate makes it possible to easily track changes made to scripts."", + ""items"": { + ""type"": ""string"" + } + }, + { + ""type"": ""string"" + } + ] + }, + ""src"": { + ""$ref"": ""#/definitions/url"" + }, + ""name"": { + ""type"": ""string"", + ""description"": ""Script name"" + } + } + }, + ""url"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""description"": ""If object, contains the complete broken-down URL for this request. If string, contains the literal request URL."", + ""id"": ""#/definitions/url"", + ""title"": ""Url"", + ""oneOf"": [ + { + ""type"": ""object"", + ""properties"": { + ""raw"": { + ""type"": ""string"", + ""description"": ""The string representation of the request URL, including the protocol, host, path, hash, query parameter(s) and path variable(s)."" + }, + ""protocol"": { + ""type"": ""string"", + ""description"": ""The protocol associated with the request, E.g: 'http'"" + }, + ""host"": { + ""title"": ""Host"", + ""description"": ""The host for the URL, E.g: api.yourdomain.com. Can be stored as a string or as an array of strings."", + ""oneOf"": [ + { + ""type"": ""string"" + }, + { + ""type"": ""array"", + ""items"": { + ""type"": ""string"" + }, + ""description"": ""The host, split into subdomain strings."" + } + ] + }, + ""path"": { + ""oneOf"": [ + { + ""type"": ""string"" + }, + { + ""type"": ""array"", + ""description"": ""The complete path of the current url, broken down into segments. A segment could be a string, or a path variable."", + ""items"": { + ""oneOf"": [ + { + ""type"": ""string"" + }, + { + ""type"": ""object"", + ""properties"": { + ""type"": { + ""type"": ""string"" + }, + ""value"": { + ""type"": ""string"" + } + } + } + ] + } + } + ] + }, + ""port"": { + ""type"": ""string"", + ""description"": ""The port number present in this URL. An empty value implies 80/443 depending on whether the protocol field contains http/https."" + }, + ""query"": { + ""type"": ""array"", + ""description"": ""An array of QueryParams, which is basically the query string part of the URL, parsed into separate variables"", + ""items"": { + ""type"": ""object"", + ""title"": ""QueryParam"", + ""properties"": { + ""key"": { + ""oneOf"": [ + { + ""type"": ""string"" + }, + { + ""type"": ""null"" + } + ] + }, + ""value"": { + ""oneOf"": [ + { + ""type"": ""string"" + }, + { + ""type"": ""null"" + } + ] + }, + ""disabled"": { + ""type"": ""boolean"", + ""default"": false, + ""description"": ""If set to true, the current query parameter will not be sent with the request."" + }, + ""description"": { + ""$ref"": ""#/definitions/description"" + } + } + } + }, + ""hash"": { + ""description"": ""Contains the URL fragment (if any). Usually this is not transmitted over the network, but it could be useful to store this in some cases."", + ""type"": ""string"" + }, + ""variable"": { + ""type"": ""array"", + ""description"": ""Postman supports path variables with the syntax `/path/:variableName/to/somewhere`. These variables are stored in this field."", + ""items"": { + ""$ref"": ""#/definitions/variable"" + } + } + } + }, + { + ""type"": ""string"" + } + ] + }, + ""variable-list"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/variable-list"", + ""title"": ""Variable List"", + ""description"": ""Collection variables allow you to define a set of variables, that are a *part of the collection*, as opposed to environments, which are separate entities.\n*Note: Collection variables must not contain any sensitive information.*"", + ""type"": ""array"", + ""items"": { + ""$ref"": ""#/definitions/variable"" + } + }, + ""variable"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/variable"", + ""title"": ""Variable"", + ""description"": ""Using variables in your Postman requests eliminates the need to duplicate requests, which can save a lot of time. Variables can be defined, and referenced to from any part of a request."", + ""type"": ""object"", + ""properties"": { + ""id"": { + ""description"": ""A variable ID is a unique user-defined value that identifies the variable within a collection. In traditional terms, this would be a variable name."", + ""type"": ""string"" + }, + ""key"": { + ""description"": ""A variable key is a human friendly value that identifies the variable within a collection. In traditional terms, this would be a variable name."", + ""type"": ""string"" + }, + ""value"": { + ""description"": ""The value that a variable holds in this collection. Ultimately, the variables will be replaced by this value, when say running a set of requests from a collection"" + }, + ""type"": { + ""description"": ""A variable may have multiple types. This field specifies the type of the variable."", + ""type"": ""string"", + ""enum"": [ + ""string"", + ""boolean"", + ""any"", + ""number"" + ] + }, + ""name"": { + ""type"": ""string"", + ""description"": ""Variable name"" + }, + ""description"": { + ""$ref"": ""#/definitions/description"" + }, + ""system"": { + ""type"": ""boolean"", + ""default"": false, + ""description"": ""When set to true, indicates that this variable has been set by Postman"" + }, + ""disabled"": { + ""type"": ""boolean"", + ""default"": false + } + }, + ""anyOf"": [ + { + ""required"": [ + ""id"" + ] + }, + { + ""required"": [ + ""key"" + ] + }, + { + ""required"": [ + ""id"", + ""key"" + ] + } + ] + }, + ""version"": { + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""id"": ""#/definitions/version"", + ""title"": ""Collection Version"", + ""description"": ""Postman allows you to version your collections as they grow, and this field holds the version number. While optional, it is recommended that you use this field to its fullest extent!"", + ""oneOf"": [ + { + ""type"": ""object"", + ""properties"": { + ""major"": { + ""description"": ""Increment this number if you make changes to the collection that changes its behaviour. E.g: Removing or adding new test scripts. (partly or completely)."", + ""minimum"": 0, + ""type"": ""integer"" + }, + ""minor"": { + ""description"": ""You should increment this number if you make changes that will not break anything that uses the collection. E.g: removing a folder."", + ""minimum"": 0, + ""type"": ""integer"" + }, + ""patch"": { + ""description"": ""Ideally, minor changes to a collection should result in the increment of this number."", + ""minimum"": 0, + ""type"": ""integer"" + }, + ""identifier"": { + ""description"": ""A human friendly identifier to make sense of the version numbers. E.g: 'beta-3'"", + ""type"": ""string"", + ""maxLength"": 10 + }, + ""meta"": {} + }, + ""required"": [ + ""major"", + ""minor"", + ""patch"" + ] + }, + { + ""type"": ""string"" + } + ] + } + } + }", + _ => string.Empty, + }; } - }, - ""required"": [ - ""info"", - ""exploreSpaces"" - ] -}"; + + public static async Task ValidateSchema(string jsonAsString, string schemaName) + { + var validationResult = new SchemaValidationResult(); + var schemaAsString = GetSchemaByApplicationName(schemaName); //var schema = await JsonSchema.FromFileAsync($"/schemas/{schemaName}"); var schema = await JsonSchema.FromJsonAsync(schemaAsString); @@ -267,4 +1463,11 @@ public static bool IsValidFilePath(ref string filePath) } return true; } + + public static string? ExtractXSRFTokenFromCookie(string sessionCookie) + { + var cookieComponents = sessionCookie.Split(";"); + var xsrfComponent = cookieComponents.FirstOrDefault(x => x.Trim().ToUpperInvariant().StartsWith("XSRF-TOKEN")); + return xsrfComponent?.Split("=")[1]; + } } \ No newline at end of file diff --git a/test/Explore.Cli.Tests/MappingHelperTests.cs b/test/Explore.Cli.Tests/MappingHelperTests.cs index 820065b..0bd74e0 100644 --- a/test/Explore.Cli.Tests/MappingHelperTests.cs +++ b/test/Explore.Cli.Tests/MappingHelperTests.cs @@ -1,467 +1,9 @@ -using System.Text.Json; using Explore.Cli.Models; -using Spectre.Console; namespace Explore.Cli.Tests; public class MappingHelperTests { - [Fact] - public void CollectionEntriesNotLimitedToSoap_ShouldReturnFalseForNull() - { - bool expected = false; - var actual = MappingHelper.CollectionEntriesNotLimitedToSoap(null); - - Assert.Equal(expected, actual); - } - - [Fact] - public void CollectionEntriesNotLimitedToSoap_FalseWhenOnlySoap() - { - bool expected = false; - - List setupCollection = new List() - { - new CollectionEntry() - { - Type = "SOAP" - } - }; - - var actual = MappingHelper.CollectionEntriesNotLimitedToSoap(setupCollection); - - Assert.Equal(expected, actual); - } - - [Fact] - public void CollectionEntriesNotLimitedToSoap_FalseWhenOnlyWSDL() - { - bool expected = false; - - List setupCollection = new List() - { - new CollectionEntry() - { - Type = "WSDL" - } - }; - - var actual = MappingHelper.CollectionEntriesNotLimitedToSoap(setupCollection); - - Assert.Equal(expected, actual); - } - - [Fact] - public void CollectionEntriesNotLimitedToSoap_TrueWhenOther() - { - bool expected = true; - - List setupCollection = new List() - { - new CollectionEntry() - { - Type = "OTHER" - } - }; - - var actual = MappingHelper.CollectionEntriesNotLimitedToSoap(setupCollection); - - Assert.Equal(expected, actual); - } - - [Theory] - [InlineData("XSRF-TOKEN=be05885a-41fc-4820-83fb-5db17015ed4a", "be05885a-41fc-4820-83fb-5db17015ed4a")] - [InlineData("xsrf-token=dd3424c9-17ec-4b20-a89c-ca89d98bbd3b", "dd3424c9-17ec-4b20-a89c-ca89d98bbd3b")] - [InlineData("Xsrf-Token=dd3424c9-17ec-4b20-a89c-ca89d98bbd3b", "dd3424c9-17ec-4b20-a89c-ca89d98bbd3b")] - [InlineData("bf936dc3-6c70-43a0-a4c5-ddb42569a9c8", null)] - public void ExtractXSRFTokenFromCookie_Tests(string cookie, string expected) - { - var actual = MappingHelper.ExtractXSRFTokenFromCookie(cookie); - Assert.Equal(expected, actual); - } - - [Fact] - public void MapEntryBodyToContentExamples_ShouldMapJson() - { - var expected = "{\n \"owner\": \"frank-kilcommins\",\n \"name\": \"Common Domains\",\n \"description\": \"common components for all APIs\",\n \"apis\": [],\n \"domains\": [\n \"Problem\",\n \"ErrorResponses\"\n ]\n}"; - var actual = MappingHelper.MapEntryBodyToContentExamples(expected); - - Assert.Equal(expected, actual.Example?.Value); - } - - [Fact] - public void MapEntryBodyToContentExamples_ShouldMapXML() - { - var expected = "\n \n \n \n 10\n 10\n \n \n"; - var actual = MappingHelper.MapEntryBodyToContentExamples(expected); - - Assert.Equal(expected, actual.Example?.Value); - } - - [Fact] - public void MapInspectorParamsToExploreParams_ShouldMapHeadersParams() - { - Models.Parameter expectedHeader = new Models.Parameter() - { - In = "header", - Name = "Authorization", - Examples = new Models.Examples() - { - Example = new Models.Example() - { - Value = "0648454d-8307-40c9-b0ac-73fa4a48354e" - } - } - }; - - var entryAsJsonString = @" { - ""_id"": { - ""timestamp"": 1684423109, - ""counter"": 13169316, - ""time"": 1684423109000, - ""date"": ""2023-05-18T15:18:29Z"", - ""machineIdentifier"": 8669634, - ""processIdentifier"": 30503, - ""timeSecond"": 1684423109 - }, - ""modelClass"": ""com.smartbear.readyapi.inspector.services.repository.models.HistoryEntry"", - ""userId"": ""59bff4e3-89be-4078-bf9c-76cff2b9f2dc"", - ""endpoint"": ""https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net/api/payees?country_of_registration=IE&name=ltd"", - ""uri"": { - ""modelClass"": ""com.smartbear.readyapi.inspector.services.repository.models.URI"", - ""scheme"": ""https"", - ""host"": ""sbdevrel-fua-smartbearcoin-prd.azurewebsites.net"", - ""path"": ""/api/payees"", - ""query"": ""country_of_registration=IE&name=ltd"" - }, - ""method"": ""GET"", - ""body"": ""{\n \""owner\"": \""frank-kilcommins\"",\n \""name\"": \""Common Domains\"",\n \""description\"": \""common components for all APIs\"",\n \""apis\"": [],\n \""domains\"": [\n \""Problem\"",\n \""ErrorResponses\""\n ]\n}"", - ""authentication"": """", - ""headers"": [ - { - ""modelClass"": ""com.smartbear.readyapi.inspector.services.repository.models.HeaderWithValue"", - ""name"": ""Authorization"", - ""value"": ""0648454d-8307-40c9-b0ac-73fa4a48354e"" - } - ], - ""type"": ""OTHER"", - ""entryId"": ""f5f0874f-d656-4b3f-9707-b879fadd7e8c"", - ""timestamp"": ""2023-05-18T15:18:37Z"", - ""ciHost"": ""https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net"", - ""name"": ""FinTech Workshop Request 0"" - }"; - - CollectionEntry? entry = JsonSerializer.Deserialize(entryAsJsonString); - var result = MappingHelper.MapInspectorParamsToExploreParams(entry == null ? new CollectionEntry(){} : entry); - - Assert.Equal(1, result.Count(x => x.Name?.ToLowerInvariant() == "authorization")); - Assert.Equal(1, result.Count(x => x.In == expectedHeader.In && x.Name == expectedHeader.Name && x.Examples?.Example?.Value == expectedHeader.Examples.Example.Value)); - - } - - [Fact] - public void MapInspectorParamsToExploreParams_ShouldMapQueryParams() - { - Models.Parameter expectedQuery = new Models.Parameter() - { - In = "query", - Name = "name", - Examples = new Models.Examples() - { - Example = new Models.Example() - { - Value = "ltd" - } - } - }; - - var entryAsJsonString = @" { - ""_id"": { - ""timestamp"": 1684423109, - ""counter"": 13169316, - ""time"": 1684423109000, - ""date"": ""2023-05-18T15:18:29Z"", - ""machineIdentifier"": 8669634, - ""processIdentifier"": 30503, - ""timeSecond"": 1684423109 - }, - ""modelClass"": ""com.smartbear.readyapi.inspector.services.repository.models.HistoryEntry"", - ""userId"": ""59bff4e3-89be-4078-bf9c-76cff2b9f2dc"", - ""endpoint"": ""https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net/api/payees?country_of_registration=IE&name=ltd"", - ""uri"": { - ""modelClass"": ""com.smartbear.readyapi.inspector.services.repository.models.URI"", - ""scheme"": ""https"", - ""host"": ""sbdevrel-fua-smartbearcoin-prd.azurewebsites.net"", - ""path"": ""/api/payees"", - ""query"": ""country_of_registration=IE&name=ltd"" - }, - ""method"": ""GET"", - ""body"": ""{\n \""owner\"": \""frank-kilcommins\"",\n \""name\"": \""Common Domains\"",\n \""description\"": \""common components for all APIs\"",\n \""apis\"": [],\n \""domains\"": [\n \""Problem\"",\n \""ErrorResponses\""\n ]\n}"", - ""authentication"": """", - ""headers"": [ - { - ""modelClass"": ""com.smartbear.readyapi.inspector.services.repository.models.HeaderWithValue"", - ""name"": ""Authorization"", - ""value"": ""0648454d-8307-40c9-b0ac-73fa4a48354e"" - } - ], - ""type"": ""OTHER"", - ""entryId"": ""f5f0874f-d656-4b3f-9707-b879fadd7e8c"", - ""timestamp"": ""2023-05-18T15:18:37Z"", - ""ciHost"": ""https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net"", - ""name"": ""FinTech Workshop Request 0"" - }"; - - CollectionEntry? entry = JsonSerializer.Deserialize(entryAsJsonString); - var result = MappingHelper.MapInspectorParamsToExploreParams(entry == null ? new CollectionEntry(){} : entry); - - Assert.Equal(1, result.Count(x => x.Name?.ToLowerInvariant() == "name")); - Assert.Equal(1, result.Count(x => x.In == expectedQuery.In && x.Name == expectedQuery.Name && x.Examples?.Example?.Value == expectedQuery.Examples.Example.Value)); - - } - - [Fact] - public void MapInspectorAuthenticationToCredentials_BasicAuth() - { - var input = "Basic Authentication/username:password"; - var expected = new Credentials() { Type = "BasicAuthCredentials", Username = "username", Password = "password" }; - var actual = MappingHelper.MapInspectorAuthenticationToCredentials(input); - - Assert.Equal(expected.Type, actual?.Type); - Assert.Equal(expected.Username, actual?.Username); - Assert.Equal(expected.Password, actual?.Password); - } - - [Fact] - public void MapInspectorAuthenticationToCredentials_TokenAuth() - { - var input = "OAuth 2.0/0648454d-8307-40c9-b0ac-73fa4a48354e"; - var expected = new Credentials() { Type = "TokenCredentials", Token = "0648454d-8307-40c9-b0ac-73fa4a48354e"}; - var actual = MappingHelper.MapInspectorAuthenticationToCredentials(input); - - Assert.Equal(expected.Type, actual?.Type); - Assert.Equal(expected.Token, actual?.Token); - } - - [Fact] - public void MapConnectionServersToServerList() - { - var entryAsJsonString = @"{ - ""id"": ""b11a0d72-bb2c-454f-8f19-3d501a7ac65f"", - ""name"": ""REST"", - ""schema"": ""OpenAPI"", - ""schemaVersion"": ""3.0.1"", - ""connectionDefinition"": { - ""info"": { - ""title"": ""app1"", - ""version"": ""version2"" - }, - ""tags"": [], - ""paths"": { - ""/api/payees"": { - ""get"": { - ""responses"": [], - ""parameters"": [ - { - ""in"": ""query"", - ""name"": ""country_of_registration"", - ""examples"": { - ""example"": { - ""value"": ""IE"" - } - } - }, - { - ""in"": ""query"", - ""name"": ""name"", - ""examples"": { - ""example"": { - ""value"": ""ltd"" - } - } - } - ] - } - } - }, - ""openapi"": ""3.0.0"", - ""servers"": [ - { - ""url"": ""https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net"" - } - ] - }, - ""settings"": { - ""type"": ""RestConnectionSettings"", - ""encodeUrl"": true, - ""connectTimeout"": 30, - ""followRedirects"": true - }, - ""credentials"": null, - ""_links"": { - ""self"": { - ""href"": ""https://api.explore.swaggerhub.com/spaces-api/v1/spaces/dd4ad781-a47a-4dd5-84c1-5c799aa8e1b8/apis/89907a82-5047-499f-8236-ea796935248d/connections/b11a0d72-bb2c-454f-8f19-3d501a7ac65f"" - } - } - }"; - - var expected = new Connection() {Id = "b11a0d72-bb2c-454f-8f19-3d501a7ac65f", - ConnectionDefinition = new ConnectionDefinition() { - Servers = new List() { - new Server() { Url = "https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net" } - } } - }; - - var actual = JsonSerializer.Deserialize(entryAsJsonString); - - Assert.Equal(expected.Id, actual?.Id); - Assert.Equal(expected.ConnectionDefinition.Servers.FirstOrDefault()?.Url, actual?.ConnectionDefinition?.Servers?.FirstOrDefault()?.Url); - } - - [Fact] - public void MapPagedConnectionsJsonToObjects() - { - var entryAsJsonString = @"{ - ""_embedded"": { - ""connections"": [ - { - ""id"": ""b11a0d72-bb2c-454f-8f19-3d501a7ac65f"", - ""name"": ""REST"", - ""schema"": ""OpenAPI"", - ""schemaVersion"": ""3.0.1"", - ""connectionDefinition"": { - ""info"": { - ""title"": ""app1"", - ""version"": ""version2"" - }, - ""tags"": [], - ""paths"": { - ""/api/payees"": { - ""get"": { - ""responses"": [], - ""parameters"": [ - { - ""in"": ""query"", - ""name"": ""country_of_registration"", - ""examples"": { - ""example"": { - ""value"": ""IE"" - } - } - }, - { - ""in"": ""query"", - ""name"": ""name"", - ""examples"": { - ""example"": { - ""value"": ""ltd"" - } - } - } - ] - } - } - }, - ""openapi"": ""3.0.0"", - ""servers"": [ - { - ""url"": ""https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net"" - } - ] - }, - ""settings"": { - ""type"": ""RestConnectionSettings"", - ""encodeUrl"": true, - ""connectTimeout"": 30, - ""followRedirects"": true - }, - ""credentials"": null, - ""_links"": { - ""self"": { - ""href"": ""https://api.explore.swaggerhub.com/spaces-api/v1/spaces/dd4ad781-a47a-4dd5-84c1-5c799aa8e1b8/apis/89907a82-5047-499f-8236-ea796935248d/connections/b11a0d72-bb2c-454f-8f19-3d501a7ac65f"" - } - } - }, - { - ""id"": ""8184596a-bbee-4c2e-a050-a7ef74d4e3bc"", - ""name"": ""REST"", - ""schema"": ""OpenAPI"", - ""schemaVersion"": ""3.0.1"", - ""connectionDefinition"": { - ""info"": { - ""title"": ""app1"", - ""version"": ""version2"" - }, - ""tags"": [], - ""paths"": { - ""/api/payees"": { - ""get"": { - ""responses"": [], - ""parameters"": [ - { - ""in"": ""query"", - ""name"": ""country_of_registration"", - ""examples"": { - ""example"": { - ""value"": ""IT"" - } - } - }, - { - ""in"": ""query"", - ""name"": ""jurisdiction_identifier_type"", - ""examples"": { - ""example"": { - ""value"": ""fiscal-code"" - } - } - } - ] - } - } - }, - ""openapi"": ""3.0.0"", - ""servers"": [ - { - ""url"": ""https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net"" - } - ] - }, - ""settings"": { - ""type"": ""RestConnectionSettings"", - ""encodeUrl"": true, - ""connectTimeout"": 30, - ""followRedirects"": true - }, - ""credentials"": null, - ""_links"": { - ""self"": { - ""href"": ""https://api.explore.swaggerhub.com/spaces-api/v1/spaces/dd4ad781-a47a-4dd5-84c1-5c799aa8e1b8/apis/89907a82-5047-499f-8236-ea796935248d/connections/8184596a-bbee-4c2e-a050-a7ef74d4e3bc"" - } - } - } - ] - }, - ""_links"": { - ""self"": { - ""href"": ""https://api.explore.swaggerhub.com/spaces-api/v1/spaces/dd4ad781-a47a-4dd5-84c1-5c799aa8e1b8/apis/89907a82-5047-499f-8236-ea796935248d/connections?page=0&size=2000"" - } - }, - ""page"": { - ""size"": 2000, - ""totalElements"": 2, - ""totalPages"": 1, - ""number"": 0 - } - }"; - - - var actual = JsonSerializer.Deserialize(entryAsJsonString); - - Assert.Equal(2, actual?.Embedded?.Connections?.Count()); - Assert.Equal("https://sbdevrel-fua-smartbearcoin-prd.azurewebsites.net", actual?.Embedded?.Connections?.FirstOrDefault(c => c.Id == "b11a0d72-bb2c-454f-8f19-3d501a7ac65f")?.ConnectionDefinition?.Servers?.FirstOrDefault()?.Url); - } - [Fact] public static void MassageConnectionExportForImport_Should_Pass() { diff --git a/test/Explore.Cli.Tests/PostmanCollectionMappingHelperTests.cs b/test/Explore.Cli.Tests/PostmanCollectionMappingHelperTests.cs new file mode 100644 index 0000000..36de3ba --- /dev/null +++ b/test/Explore.Cli.Tests/PostmanCollectionMappingHelperTests.cs @@ -0,0 +1,117 @@ +using Explore.Cli.Models; +using System.Text.Json; + +public class PostmanCollectionMappingHelperTests +{ + [Fact] + public void MapEntryBodyToContentExamples_ShouldReturnExamples() + { + // Arrange + var rawBody = "raw body"; + + // Act + var result = PostmanCollectionMappingHelper.MapEntryBodyToContentExamples(rawBody); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void CreatePathsDictionary_ShouldReturnDictionary() + { + // Arrange + var mockRequestAsJson = "{\r\n\t\"method\": \"GET\",\r\n\t\"header\": [],\r\n\t\"body\": {\r\n\t\t\"mode\": \"formdata\",\r\n\t\t\"formdata\": []\r\n\t},\r\n\t\"url\": {\r\n\t\t\"raw\": \"http://localhost:17456/api/apilogs?start_date_time=2017-09-27%2010%3A20%3A00&end_date_time=2017-09-30%2010%3A20%3A00\",\r\n\t\t\"protocol\": \"http\",\r\n\t\t\"host\": [\r\n\t\t\t\"localhost\"\r\n\t\t],\r\n\t\t\"port\": \"17456\",\r\n\t\t\"path\": [\r\n\t\t\t\"api\",\r\n\t\t\t\"apilogs\"\r\n\t\t],\r\n\t\t\"query\": [\r\n\t\t\t{\r\n\t\t\t\t\"key\": \"start_date_time\",\r\n\t\t\t\t\"value\": \"2017-09-27%2010%3A20%3A00\"\r\n\t\t\t},\r\n\t\t\t{\r\n\t\t\t\t\"key\": \"end_date_time\",\r\n\t\t\t\t\"value\": \"2017-09-30%2010%3A20%3A00\"\r\n\t\t\t}\r\n\t\t]\r\n\t}\r\n}"; + Request? request = JsonSerializer.Deserialize(mockRequestAsJson); + + // Act + var result = PostmanCollectionMappingHelper.CreatePathsDictionary(request); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + } + + [Fact] + public void MapHeaderAndQueryParams_ShouldReturnListOfParameters() + { + // Arrange + var mockRequestAsJson = "{\r\n\t\"method\": \"GET\",\r\n\t\"header\": [],\r\n\t\"body\": {\r\n\t\t\"mode\": \"formdata\",\r\n\t\t\"formdata\": []\r\n\t},\r\n\t\"url\": {\r\n\t\t\"raw\": \"http://localhost:17456/api/apilogs?start_date_time=2017-09-27%2010%3A20%3A00&end_date_time=2017-09-30%2010%3A20%3A00\",\r\n\t\t\"protocol\": \"http\",\r\n\t\t\"host\": [\r\n\t\t\t\"localhost\"\r\n\t\t],\r\n\t\t\"port\": \"17456\",\r\n\t\t\"path\": [\r\n\t\t\t\"api\",\r\n\t\t\t\"apilogs\"\r\n\t\t],\r\n\t\t\"query\": [\r\n\t\t\t{\r\n\t\t\t\t\"key\": \"start_date_time\",\r\n\t\t\t\t\"value\": \"2017-09-27%2010%3A20%3A00\"\r\n\t\t\t},\r\n\t\t\t{\r\n\t\t\t\t\"key\": \"end_date_time\",\r\n\t\t\t\t\"value\": \"2017-09-30%2010%3A20%3A00\"\r\n\t\t\t}\r\n\t\t]\r\n\t}\r\n}"; + Request? request = JsonSerializer.Deserialize(mockRequestAsJson); + + // Act + var result = PostmanCollectionMappingHelper.MapHeaderAndQueryParams(request); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + } + + [Fact] + public void GetServerUrlFromItemRequest_ShouldReturnUrl() + { + // Arrange + var mockRequestAsJson = "{\r\n\t\"method\": \"GET\",\r\n\t\"header\": [],\r\n\t\"body\": {\r\n\t\t\"mode\": \"formdata\",\r\n\t\t\"formdata\": []\r\n\t},\r\n\t\"url\": {\r\n\t\t\"raw\": \"http://localhost:17456/api/apilogs?start_date_time=2017-09-27%2010%3A20%3A00&end_date_time=2017-09-30%2010%3A20%3A00\",\r\n\t\t\"protocol\": \"http\",\r\n\t\t\"host\": [\r\n\t\t\t\"localhost\"\r\n\t\t],\r\n\t\t\"port\": \"17456\",\r\n\t\t\"path\": [\r\n\t\t\t\"api\",\r\n\t\t\t\"apilogs\"\r\n\t\t],\r\n\t\t\"query\": [\r\n\t\t\t{\r\n\t\t\t\t\"key\": \"start_date_time\",\r\n\t\t\t\t\"value\": \"2017-09-27%2010%3A20%3A00\"\r\n\t\t\t},\r\n\t\t\t{\r\n\t\t\t\t\"key\": \"end_date_time\",\r\n\t\t\t\t\"value\": \"2017-09-30%2010%3A20%3A00\"\r\n\t\t\t}\r\n\t\t]\r\n\t}\r\n}"; + Request? request = JsonSerializer.Deserialize(mockRequestAsJson); + + // Act + var result = PostmanCollectionMappingHelper.GetServerUrlFromItemRequest(request); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void MapPostmanCollectionItemToExploreConnection_ShouldReturnConnection() + { + // Arrange + var mockItemAsJson = "{\r\n\t\"name\": \"GET apilogs\",\r\n\t\"request\": {\r\n\t\t\"method\": \"GET\",\r\n\t\t\"header\": [],\r\n\t\t\"body\": {\r\n\t\t\t\"mode\": \"formdata\",\r\n\t\t\t\"formdata\": []\r\n\t\t},\r\n\t\t\"url\": {\r\n\t\t\t\"raw\": \"http://localhost:17456/api/apilogs?start_date_time=2017-09-27%2010%3A20%3A00&end_date_time=2017-09-30%2010%3A20%3A00\",\r\n\t\t\t\"protocol\": \"http\",\r\n\t\t\t\"host\": [\r\n\t\t\t\t\"localhost\"\r\n\t\t\t],\r\n\t\t\t\"port\": \"17456\",\r\n\t\t\t\"path\": [\r\n\t\t\t\t\"api\",\r\n\t\t\t\t\"apilogs\"\r\n\t\t\t],\r\n\t\t\t\"query\": [\r\n\t\t\t\t{\r\n\t\t\t\t\t\"key\": \"start_date_time\",\r\n\t\t\t\t\t\"value\": \"2017-09-27%2010%3A20%3A00\"\r\n\t\t\t\t},\r\n\t\t\t\t{\r\n\t\t\t\t\t\"key\": \"end_date_time\",\r\n\t\t\t\t\t\"value\": \"2017-09-30%2010%3A20%3A00\"\r\n\t\t\t\t}\r\n\t\t\t]\r\n\t\t}\r\n\t}\r\n}"; + Item? item = JsonSerializer.Deserialize(mockItemAsJson); + + // Act + var result = PostmanCollectionMappingHelper.MapPostmanCollectionItemToExploreConnection(item ?? new Item()); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void IsCollectionVersion2_1_ShouldReturnTrue() + { + // Arrange + var mockCollectionAsJson = "{\r\n\t\"info\": {\r\n\t\t\"_postman_id\": \"d5e3f3a0-5f1e-4b0e-8f2a-2b1b8b7c9b1a\",\r\n\t\t\"name\": \"Explore\",\r\n\t\t\"schema\": \"https://schema.getpostman.com/json/collection/v2.1.0/collection.json\"\r\n\t}\r\n}"; + //var mockCollectionAsJson2 = "{\r\n\t\"info\": {\r\n\t\t\"_postman_id\": \"d5e3f3a0-5f1e-4b0e-8f2a-2b1b8b7c9b1a\",\r\n\t\t\"name\": \"Explore\",\r\n\t\t\"schema\": \"https://schema.getpostman.com/json/collection/v2.0.0/collection.json\"\r\n\t}\r\n}"; + //var mockCollectionAsJson3 = "{\r\n\t\"info\": {\r\n\t\t\"_postman_id\": \"d5e3f3a0-5f1e-4b0e-8f2a-2b1b8b7c9b1a\",\r\n\t\t\"name\": \"Explore\",\r\n\t\t\"schema\": \"https://schema.getpostman.com/json/collection/v2.2.0/collection.json\"\r\n\t}\r\n}"; + + // Act + var result = PostmanCollectionMappingHelper.IsCollectionVersion2_1(mockCollectionAsJson); + //var result2 = PostmanCollectionMappingHelper.IsCollectionVersion2_1(mockCollectionAsJson2); + //var result3 = PostmanCollectionMappingHelper.IsCollectionVersion2_1(mockCollectionAsJson3); + + // Assert + Assert.True(result); + //Assert.False(result2); + //Assert.False(result3); + } + + [Fact] + public void IsCollectionVersion2_1_ShouldReturnFalse() + { + // Arrange + + var mockCollectionAsJson2 = "{\r\n\t\"info\": {\r\n\t\t\"_postman_id\": \"d5e3f3a0-5f1e-4b0e-8f2a-2b1b8b7c9b1a\",\r\n\t\t\"name\": \"Explore\",\r\n\t\t\"schema\": \"https://schema.getpostman.com/json/collection/v2.0.0/collection.json\"\r\n\t}\r\n}"; + var mockCollectionAsJson3 = "{\r\n\t\"info\": {\r\n\t\t\"_postman_id\": \"d5e3f3a0-5f1e-4b0e-8f2a-2b1b8b7c9b1a\",\r\n\t\t\"name\": \"Explore\",\r\n\t\t\"schema\": \"https://schema.getpostman.com/json/collection/v2.2.0/collection.json\"\r\n\t}\r\n}"; + + // Act + + var result2 = PostmanCollectionMappingHelper.IsCollectionVersion2_1(mockCollectionAsJson2); + var result3 = PostmanCollectionMappingHelper.IsCollectionVersion2_1(mockCollectionAsJson3); + + // Assert + + Assert.False(result2); + Assert.False(result3); + } +} \ No newline at end of file diff --git a/test/Explore.Cli.Tests/UtilityHelperTests.cs b/test/Explore.Cli.Tests/UtilityHelperTests.cs index 9d65586..48a3c24 100644 --- a/test/Explore.Cli.Tests/UtilityHelperTests.cs +++ b/test/Explore.Cli.Tests/UtilityHelperTests.cs @@ -1,5 +1,3 @@ -using System.Collections.ObjectModel; -using System.Net; using System.Net.Http.Headers; namespace Explore.Cli.Tests; @@ -27,7 +25,7 @@ public async void SchemaValidation_ExploreSpaces_Invalid_Should_Fail() string entryAsJsonString = @"{""info"": {""version"": ""0.0.1"", ""exportedAt"": ""10:17:50 AM"" }}"; string expectedError = "1 total errors"; - var validationResult = await UtilityHelper.ValidateSchema(entryAsJsonString, "ExploreSpaces.schema.json"); + var validationResult = await UtilityHelper.ValidateSchema(entryAsJsonString, "explore"); Assert.False(validationResult.isValid); Assert.Contains(expectedError, validationResult.Message); @@ -101,4 +99,16 @@ public void IsValidFilePath_Should_Pass(string input) { Assert.True(UtilityHelper.IsValidFilePath(ref input)); } + + [Theory] + [InlineData("XSRF-TOKEN=be05885a-41fc-4820-83fb-5db17015ed4a", "be05885a-41fc-4820-83fb-5db17015ed4a")] + [InlineData("xsrf-token=dd3424c9-17ec-4b20-a89c-ca89d98bbd3b", "dd3424c9-17ec-4b20-a89c-ca89d98bbd3b")] + [InlineData("Xsrf-Token=dd3424c9-17ec-4b20-a89c-ca89d98bbd3b", "dd3424c9-17ec-4b20-a89c-ca89d98bbd3b")] + [InlineData("bf936dc3-6c70-43a0-a4c5-ddb42569a9c8", null)] + public void ExtractXSRFTokenFromCookie_Tests(string cookie, string expected) + { + var actual = UtilityHelper.ExtractXSRFTokenFromCookie(cookie); + Assert.Equal(expected, actual); + } + } \ No newline at end of file