From f437c9542598da9b52dfc9de471845d25d992caf Mon Sep 17 00:00:00 2001 From: Tim Mulholland Date: Thu, 29 Oct 2020 00:38:07 -0700 Subject: [PATCH 1/2] Add more preferences for configuring openapi search paths --- src/Microsoft.HttpRepl/ApiConnection.cs | 39 +----- .../OpenApi/IOpenApiSearchPathsProvider.cs | 14 ++ .../Preferences/OpenApiSearchPathsProvider.cs | 65 +++++++++ .../Preferences/WellKnownPreference.cs | 4 + .../FakePreferences.cs | 72 ++++++++++ .../OpenApiSearchPathsProviderTests.cs | 124 ++++++++++++++++++ 6 files changed, 284 insertions(+), 34 deletions(-) create mode 100644 src/Microsoft.HttpRepl/OpenApi/IOpenApiSearchPathsProvider.cs create mode 100644 src/Microsoft.HttpRepl/Preferences/OpenApiSearchPathsProvider.cs create mode 100644 test/Microsoft.HttpRepl.Fakes/FakePreferences.cs create mode 100644 test/Microsoft.HttpRepl.Tests/Preferences/OpenApiSearchPathsProviderTests.cs diff --git a/src/Microsoft.HttpRepl/ApiConnection.cs b/src/Microsoft.HttpRepl/ApiConnection.cs index 0e472694d..4b9f3eea2 100644 --- a/src/Microsoft.HttpRepl/ApiConnection.cs +++ b/src/Microsoft.HttpRepl/ApiConnection.cs @@ -17,26 +17,9 @@ namespace Microsoft.HttpRepl { internal class ApiConnection { - // OpenAPI description search paths are appended to the base url to - // attempt to find the description document. A search path is a - // relative url that is appended to the base url using Uri.TryCreate, - // so the semantics of relative urls matter here. - // Example: Base path https://localhost/v1/ and search path openapi.json - // will result in https://localhost/v1/openapi.json being tested. - // Example: Base path https://localhost/v1/ and search path /openapi.json - // will result in https://localhost/openapi.json being tested. - private static readonly string[] OpenApiDescriptionSearchPaths = new[] { - "swagger.json", - "/swagger.json", - "swagger/v1/swagger.json", - "/swagger/v1/swagger.json", - "openapi.json", - "/openapi.json", - }; - - private readonly IPreferences _preferences; private readonly IWritable _logger; private readonly bool _logVerboseMessages; + private readonly IOpenApiSearchPathsProvider _searchPaths; public Uri? RootUri { get; set; } public bool HasRootUri => RootUri is object; @@ -48,11 +31,12 @@ internal class ApiConnection public bool HasSwaggerDocument => SwaggerDocument is object; public bool AllowBaseOverrideBySwagger { get; set; } - public ApiConnection(IPreferences preferences, IWritable logger, bool logVerboseMessages) + public ApiConnection(IPreferences preferences, IWritable logger, bool logVerboseMessages, IOpenApiSearchPathsProvider? openApiSearchPaths = null) { - _preferences = preferences ?? throw new ArgumentNullException(nameof(preferences)); + _ = preferences ?? throw new ArgumentNullException(nameof(preferences)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logVerboseMessages = logVerboseMessages; + _searchPaths = openApiSearchPaths ?? new OpenApiSearchPathsProvider(preferences); } private async Task FindSwaggerDoc(HttpClient client, IEnumerable swaggerSearchPaths, CancellationToken cancellationToken) @@ -157,7 +141,7 @@ public async Task SetupHttpState(HttpState httpState, bool performAutoDetect, Ca } else if (performAutoDetect) { - await FindSwaggerDoc(httpState.Client, GetSwaggerSearchPaths(), cancellationToken); + await FindSwaggerDoc(httpState.Client, _searchPaths.GetOpenApiSearchPaths(), cancellationToken); } if (HasSwaggerDocument) @@ -177,20 +161,7 @@ public async Task SetupHttpState(HttpState httpState, bool performAutoDetect, Ca } } - private IEnumerable GetSwaggerSearchPaths() - { - string rawValue = _preferences.GetValue(WellKnownPreference.SwaggerSearchPaths); - if (rawValue is null) - { - return OpenApiDescriptionSearchPaths; - } - else - { - string[] paths = rawValue.Split('|', StringSplitOptions.RemoveEmptyEntries); - return paths; - } - } private void WriteVerbose(string s) { diff --git a/src/Microsoft.HttpRepl/OpenApi/IOpenApiSearchPathsProvider.cs b/src/Microsoft.HttpRepl/OpenApi/IOpenApiSearchPathsProvider.cs new file mode 100644 index 000000000..76fc7a42d --- /dev/null +++ b/src/Microsoft.HttpRepl/OpenApi/IOpenApiSearchPathsProvider.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System.Collections.Generic; + +namespace Microsoft.HttpRepl.OpenApi +{ + internal interface IOpenApiSearchPathsProvider + { + IEnumerable GetOpenApiSearchPaths(); + } +} diff --git a/src/Microsoft.HttpRepl/Preferences/OpenApiSearchPathsProvider.cs b/src/Microsoft.HttpRepl/Preferences/OpenApiSearchPathsProvider.cs new file mode 100644 index 000000000..04e35d214 --- /dev/null +++ b/src/Microsoft.HttpRepl/Preferences/OpenApiSearchPathsProvider.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.HttpRepl.OpenApi; + +namespace Microsoft.HttpRepl.Preferences +{ + internal class OpenApiSearchPathsProvider : IOpenApiSearchPathsProvider + { + // OpenAPI description search paths are appended to the base url to + // attempt to find the description document. A search path is a + // relative url that is appended to the base url using Uri.TryCreate, + // so the semantics of relative urls matter here. + // Example: Base path https://localhost/v1/ and search path openapi.json + // will result in https://localhost/v1/openapi.json being tested. + // Example: Base path https://localhost/v1/ and search path /openapi.json + // will result in https://localhost/openapi.json being tested. + internal static IEnumerable DefaultSearchPaths { get; } = new[] { + "swagger.json", + "/swagger.json", + "swagger/v1/swagger.json", + "/swagger/v1/swagger.json", + "openapi.json", + "/openapi.json", + }; + + private readonly IPreferences _preferences; + public OpenApiSearchPathsProvider(IPreferences preferences) + { + _preferences = preferences; + } + + public IEnumerable GetOpenApiSearchPaths() + { + string[] configSearchPaths = Split(_preferences.GetValue(WellKnownPreference.SwaggerSearchPaths)); + + if (configSearchPaths.Length > 0) + { + return configSearchPaths; + } + + string[] addToSearchPaths = Split(_preferences.GetValue(WellKnownPreference.SwaggerAddToSearchPaths)); + string[] removeFromSearchPaths = Split(_preferences.GetValue(WellKnownPreference.SwaggerRemoveFromSearchPaths)); + + return DefaultSearchPaths.Union(addToSearchPaths).Except(removeFromSearchPaths); + } + + private static string[] Split(string searchPaths) + { + if (string.IsNullOrWhiteSpace(searchPaths)) + { + return Array.Empty(); + } + else + { + return searchPaths.Split('|', StringSplitOptions.RemoveEmptyEntries); + } + } + } +} diff --git a/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs b/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs index 377a0bdd2..f539ad99d 100644 --- a/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs +++ b/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs @@ -174,6 +174,10 @@ public static IReadOnlyList Names public static string SwaggerUIEndpoint { get; } = "swagger.uiEndpoint"; + public static string SwaggerRemoveFromSearchPaths => "swagger.removeFromSearchPaths"; + + public static string SwaggerAddToSearchPaths => "swagger.addToSearchPaths"; + public static string UseDefaultCredentials { get; } = "httpClient.useDefaultCredentials"; public static string HttpClientUserAgent { get; } = "httpClient.userAgent"; diff --git a/test/Microsoft.HttpRepl.Fakes/FakePreferences.cs b/test/Microsoft.HttpRepl.Fakes/FakePreferences.cs new file mode 100644 index 000000000..dd4db4638 --- /dev/null +++ b/test/Microsoft.HttpRepl.Fakes/FakePreferences.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.HttpRepl.Preferences; +using Microsoft.Repl.ConsoleHandling; + +namespace Microsoft.HttpRepl.Fakes +{ + public class FakePreferences : IPreferences + { + private readonly Dictionary _currentPreferences; + + public FakePreferences() + { + DefaultPreferences = new Dictionary(); + _currentPreferences = new(); + } + + public IReadOnlyDictionary DefaultPreferences { get; } + public IReadOnlyDictionary CurrentPreferences => _currentPreferences; + + public bool GetBoolValue(string preference, bool defaultValue = false) + { + if (CurrentPreferences.TryGetValue(preference, out string value) && bool.TryParse(value, out bool result)) + { + return result; + } + + return defaultValue; + } + + public AllowedColors GetColorValue(string preference, AllowedColors defaultValue = AllowedColors.None) + { + if (CurrentPreferences.TryGetValue(preference, out string value) && Enum.TryParse(value, true, out AllowedColors result)) + { + return result; + } + + return defaultValue; + } + + public int GetIntValue(string preference, int defaultValue = 0) + { + if (CurrentPreferences.TryGetValue(preference, out string value) && int.TryParse(value, out int result)) + { + return result; + } + + return defaultValue; + } + + public string GetValue(string preference, string defaultValue = null) + { + if (CurrentPreferences.TryGetValue(preference, out string value)) + { + return value; + } + + return defaultValue; + } + + public bool SetValue(string preference, string value) + { + _currentPreferences[preference] = value; + return true; + } + + public bool TryGetValue(string preference, out string value) => CurrentPreferences.TryGetValue(preference, out value); + } +} diff --git a/test/Microsoft.HttpRepl.Tests/Preferences/OpenApiSearchPathsProviderTests.cs b/test/Microsoft.HttpRepl.Tests/Preferences/OpenApiSearchPathsProviderTests.cs new file mode 100644 index 000000000..0035401ae --- /dev/null +++ b/test/Microsoft.HttpRepl.Tests/Preferences/OpenApiSearchPathsProviderTests.cs @@ -0,0 +1,124 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.HttpRepl.Fakes; +using Microsoft.HttpRepl.Preferences; +using Xunit; + +namespace Microsoft.HttpRepl.Tests.Preferences +{ + public class OpenApiSearchPathsProviderTests + { + [Fact] + public void WithNoOverrides_ReturnsDefault() + { + // Arrange + NullPreferences preferences = new(); + OpenApiSearchPathsProvider provider = new(preferences); + IEnumerable expectedPaths = OpenApiSearchPathsProvider.DefaultSearchPaths; + + // Act + IEnumerable paths = provider.GetOpenApiSearchPaths(); + + // Assert + AssertPathLists(expectedPaths, paths); + } + + [Fact] + public void WithFullOverride_ReturnsConfiguredOverride() + { + // Arrange + string searchPathOverrides = "/red|/green|/blue"; + FakePreferences preferences = new(); + preferences.SetValue(WellKnownPreference.SwaggerSearchPaths, searchPathOverrides); + OpenApiSearchPathsProvider provider = new(preferences); + string[] expectedPaths = searchPathOverrides.Split('|'); + + // Act + IEnumerable paths = provider.GetOpenApiSearchPaths(); + + // Assert + AssertPathLists(expectedPaths, paths); + } + + [Fact] + public void WithAdditions_ReturnsDefaultPlusAdditions() + { + // Arrange + string[] searchPathAdditions = new[] { "/red", "/green", "/blue" }; + FakePreferences preferences = new(); + preferences.SetValue(WellKnownPreference.SwaggerAddToSearchPaths, string.Join('|', searchPathAdditions)); + OpenApiSearchPathsProvider provider = new(preferences); + IEnumerable expectedPaths = OpenApiSearchPathsProvider.DefaultSearchPaths.Union(searchPathAdditions); + + // Act + IEnumerable paths = provider.GetOpenApiSearchPaths(); + + // Assert + AssertPathLists(expectedPaths, paths); + } + + [Fact] + public void WithRemovals_ReturnsDefaultMinusRemovals() + { + // Arrange + string[] searchPathRemovals = new[] { "swagger.json", "/swagger.json", "swagger/v1/swagger.json", "/swagger/v1/swagger.json" }; + FakePreferences preferences = new(); + preferences.SetValue(WellKnownPreference.SwaggerRemoveFromSearchPaths, string.Join('|', searchPathRemovals)); + OpenApiSearchPathsProvider provider = new(preferences); + IEnumerable expectedPaths = OpenApiSearchPathsProvider.DefaultSearchPaths.Except(searchPathRemovals); + + // Act + IEnumerable paths = provider.GetOpenApiSearchPaths(); + + // Assert + AssertPathLists(expectedPaths, paths); + } + + [Fact] + public void WithAdditionsAndRemovals_ReturnsCorrectSet() + { + // Arrange + string[] searchPathAdditions = new[] { "/red", "/green", "/blue" }; + string[] searchPathRemovals = new[] { "swagger.json", "/swagger.json", "swagger/v1/swagger.json", "/swagger/v1/swagger.json" }; + FakePreferences preferences = new(); + preferences.SetValue(WellKnownPreference.SwaggerAddToSearchPaths, string.Join('|', searchPathAdditions)); + preferences.SetValue(WellKnownPreference.SwaggerRemoveFromSearchPaths, string.Join('|', searchPathRemovals)); + OpenApiSearchPathsProvider provider = new(preferences); + IEnumerable expectedPaths = OpenApiSearchPathsProvider.DefaultSearchPaths.Union(searchPathAdditions).Except(searchPathRemovals); + + // Act + IEnumerable paths = provider.GetOpenApiSearchPaths(); + + // Assert + AssertPathLists(expectedPaths, paths); + } + + private static void AssertPathLists(IEnumerable expectedPaths, IEnumerable paths) + { + Assert.NotNull(expectedPaths); + Assert.NotNull(paths); + + IEnumerator expectedPathsEnumerator = expectedPaths.GetEnumerator(); + IEnumerator pathsEnumerator = paths.GetEnumerator(); + + while (expectedPathsEnumerator.MoveNext()) + { + Assert.True(pathsEnumerator.MoveNext(), $"Missing path \"{expectedPathsEnumerator.Current}\""); + Assert.Equal(expectedPathsEnumerator.Current, pathsEnumerator.Current, StringComparer.Ordinal); + } + + if (pathsEnumerator.MoveNext()) + { + // We can't do a one-liner here like the Missing path version above because + // the order the second parameter is evaluated regardless of the result of the + // evaluation of the first parameter. Also xUnit doesn't have an Assert.Fail, + // so we have to use Assert.True(false) per their comparison chart. + Assert.True(false, $"Extra path \"{pathsEnumerator.Current}\""); + } + } + } +} From 0140a19f71233a191574a1548acf424d910ff63a Mon Sep 17 00:00:00 2001 From: Tim Mulholland Date: Thu, 29 Oct 2020 23:14:35 -0700 Subject: [PATCH 2/2] Seal a couple classes, move a null check --- src/Microsoft.HttpRepl/ApiConnection.cs | 1 - .../Preferences/OpenApiSearchPathsProvider.cs | 4 ++-- test/Microsoft.HttpRepl.Fakes/FakePreferences.cs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.HttpRepl/ApiConnection.cs b/src/Microsoft.HttpRepl/ApiConnection.cs index 4b9f3eea2..ef9cf76bc 100644 --- a/src/Microsoft.HttpRepl/ApiConnection.cs +++ b/src/Microsoft.HttpRepl/ApiConnection.cs @@ -33,7 +33,6 @@ internal class ApiConnection public ApiConnection(IPreferences preferences, IWritable logger, bool logVerboseMessages, IOpenApiSearchPathsProvider? openApiSearchPaths = null) { - _ = preferences ?? throw new ArgumentNullException(nameof(preferences)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logVerboseMessages = logVerboseMessages; _searchPaths = openApiSearchPaths ?? new OpenApiSearchPathsProvider(preferences); diff --git a/src/Microsoft.HttpRepl/Preferences/OpenApiSearchPathsProvider.cs b/src/Microsoft.HttpRepl/Preferences/OpenApiSearchPathsProvider.cs index 04e35d214..5bc8e36be 100644 --- a/src/Microsoft.HttpRepl/Preferences/OpenApiSearchPathsProvider.cs +++ b/src/Microsoft.HttpRepl/Preferences/OpenApiSearchPathsProvider.cs @@ -10,7 +10,7 @@ namespace Microsoft.HttpRepl.Preferences { - internal class OpenApiSearchPathsProvider : IOpenApiSearchPathsProvider + internal sealed class OpenApiSearchPathsProvider : IOpenApiSearchPathsProvider { // OpenAPI description search paths are appended to the base url to // attempt to find the description document. A search path is a @@ -32,7 +32,7 @@ internal class OpenApiSearchPathsProvider : IOpenApiSearchPathsProvider private readonly IPreferences _preferences; public OpenApiSearchPathsProvider(IPreferences preferences) { - _preferences = preferences; + _preferences = preferences ?? throw new ArgumentNullException(nameof(preferences)); } public IEnumerable GetOpenApiSearchPaths() diff --git a/test/Microsoft.HttpRepl.Fakes/FakePreferences.cs b/test/Microsoft.HttpRepl.Fakes/FakePreferences.cs index dd4db4638..46707afaf 100644 --- a/test/Microsoft.HttpRepl.Fakes/FakePreferences.cs +++ b/test/Microsoft.HttpRepl.Fakes/FakePreferences.cs @@ -8,7 +8,7 @@ namespace Microsoft.HttpRepl.Fakes { - public class FakePreferences : IPreferences + public sealed class FakePreferences : IPreferences { private readonly Dictionary _currentPreferences;