diff --git a/WireMock.Net Solution.sln.DotSettings b/WireMock.Net Solution.sln.DotSettings index 92eaabab2..6f5a8ca10 100644 --- a/WireMock.Net Solution.sln.DotSettings +++ b/WireMock.Net Solution.sln.DotSettings @@ -27,6 +27,7 @@ True True True + True True True True diff --git a/examples/WireMock.Net.OpenApiParser.ConsoleApp/DynamicDataGeneration.cs b/examples/WireMock.Net.OpenApiParser.ConsoleApp/DynamicDataGeneration.cs index 7d51dc739..d22fe8b3f 100644 --- a/examples/WireMock.Net.OpenApiParser.ConsoleApp/DynamicDataGeneration.cs +++ b/examples/WireMock.Net.OpenApiParser.ConsoleApp/DynamicDataGeneration.cs @@ -11,7 +11,7 @@ public override string String get { // Since you have your Schema, you can get if max-length is set. You can generate accurate examples with this settings - var maxLength = Schema.MaxLength ?? 9; + var maxLength = Schema?.MaxLength ?? 9; return RandomizerFactory.GetRandomizer(new FieldOptionsTextRegex { diff --git a/examples/WireMock.Net.OpenApiParser.ConsoleApp/Program.cs b/examples/WireMock.Net.OpenApiParser.ConsoleApp/Program.cs index c959d548c..30b8b23c5 100644 --- a/examples/WireMock.Net.OpenApiParser.ConsoleApp/Program.cs +++ b/examples/WireMock.Net.OpenApiParser.ConsoleApp/Program.cs @@ -17,7 +17,7 @@ static void Main(string[] args) private static void RunMockServerWithDynamicExampleGeneration() { - //Run your mocking framework specifieing youur Example Values generator class. + // Run your mocking framework specifying your Example Values generator class. var serverCustomer_V2_json = Run.RunServer(Path.Combine(Folder, "Swagger_Customer_V2.0.json"), "http://localhost:8090/", true, new DynamicDataGeneration(), Types.ExampleValueType.Value, Types.ExampleValueType.Value); Console.WriteLine("Press any key to stop the servers"); @@ -27,15 +27,15 @@ private static void RunMockServerWithDynamicExampleGeneration() private static void RunOthersOpenApiParserExample() { - var serverOpenAPIExamples = Run.RunServer(Path.Combine(Folder, "openAPIExamples.yaml"), "https://localhost:9091/"); - var serverPetstore_V2_json = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V2.0.json"), "https://localhost:9092/"); - var serverPetstore_V2_yaml = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V2.0.yaml"), "https://localhost:9093/"); - var serverPetstore_V300_yaml = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V3.0.0.yaml"), "https://localhost:9094/"); - var serverPetstore_V302_json = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V3.0.2.json"), "https://localhost:9095/"); - var testopenapifile_json = Run.RunServer(Path.Combine(Folder, "testopenapifile.json"), "https://localhost:9096/"); - var file_errorYaml = Run.RunServer(Path.Combine(Folder, "file_error.yaml"), "https://localhost:9097/"); - var file_petJson = Run.RunServer(Path.Combine(Folder, "pet.json"), "https://localhost:9098/"); - var refsYaml = Run.RunServer(Path.Combine(Folder, "refs.yaml"), "https://localhost:9099/"); + var serverOpenAPIExamples = Run.RunServer(Path.Combine(Folder, "openAPIExamples.yaml"), "http://localhost:9091/"); + var serverPetstore_V2_json = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V2.0.json"), "http://localhost:9092/"); + var serverPetstore_V2_yaml = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V2.0.yaml"), "http://localhost:9093/"); + var serverPetstore_V300_yaml = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V3.0.0.yaml"), "http://localhost:9094/"); + var serverPetstore_V302_json = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V3.0.2.json"), "http://localhost:9095/"); + var testopenapifile_json = Run.RunServer(Path.Combine(Folder, "testopenapifile.json"), "http://localhost:9096/"); + var file_errorYaml = Run.RunServer(Path.Combine(Folder, "file_error.yaml"), "http://localhost:9097/"); + var file_petJson = Run.RunServer(Path.Combine(Folder, "pet.json"), "http://localhost:9098/"); + var refsYaml = Run.RunServer(Path.Combine(Folder, "refs.yaml"), "http://localhost:9099/"); testopenapifile_json .Given(Request.Create().WithPath("/x").UsingGet()) diff --git a/examples/WireMock.Net.OpenApiParser.ConsoleApp/Run.cs b/examples/WireMock.Net.OpenApiParser.ConsoleApp/Run.cs index ae8fc9ec3..4ff3f7758 100644 --- a/examples/WireMock.Net.OpenApiParser.ConsoleApp/Run.cs +++ b/examples/WireMock.Net.OpenApiParser.ConsoleApp/Run.cs @@ -9,64 +9,70 @@ using WireMock.Server; using WireMock.Settings; -namespace WireMock.Net.OpenApiParser.ConsoleApp +namespace WireMock.Net.OpenApiParser.ConsoleApp; + +public static class Run { - public static class Run + public static WireMockServer RunServer( + string path, + string url, + bool dynamicExamples = true, + IWireMockOpenApiParserExampleValues? examplesValuesGenerator = null, + ExampleValueType pathPatternToUse = ExampleValueType.Wildcard, + ExampleValueType headerPatternToUse = ExampleValueType.Wildcard + ) { - public static WireMockServer RunServer(string path, string url, bool dynamicExamples = true, IWireMockOpenApiParserExampleValues examplesValuesGenerator = null, ExampleValueType pathPatternToUse = ExampleValueType.Wildcard, ExampleValueType headerPatternToUse = ExampleValueType.Wildcard) + var server = WireMockServer.Start(new WireMockServerSettings { - var server = WireMockServer.Start(new WireMockServerSettings - { - AllowCSharpCodeMatcher = true, - Urls = new[] { url }, - StartAdminInterface = true, - ReadStaticMappings = true, - WatchStaticMappings = false, - WatchStaticMappingsInSubdirectories = false, - Logger = new WireMockConsoleLogger(), - SaveUnmatchedRequests = true - }); + AllowCSharpCodeMatcher = true, + Urls = new[] { url }, + StartAdminInterface = true, + ReadStaticMappings = true, + WatchStaticMappings = false, + WatchStaticMappingsInSubdirectories = false, + Logger = new WireMockConsoleLogger(), + SaveUnmatchedRequests = true + }); + + Console.WriteLine("WireMockServer listening at {0}", string.Join(",", server.Urls)); - Console.WriteLine("WireMockServer listening at {0}", string.Join(",", server.Urls)); + //server.SetBasicAuthentication("a", "b"); - //server.SetBasicAuthentication("a", "b"); + var settings = new WireMockOpenApiParserSettings + { + DynamicExamples = dynamicExamples, + ExampleValues = examplesValuesGenerator, + PathPatternToUse = pathPatternToUse, + HeaderPatternToUse = headerPatternToUse, + }; - var settings = new WireMockOpenApiParserSettings - { - DynamicExamples = dynamicExamples, - ExampleValues = examplesValuesGenerator, - PathPatternToUse = pathPatternToUse, - HeaderPatternToUse = headerPatternToUse, - }; + server.WithMappingFromOpenApiFile(path, settings, out var diag); - server.WithMappingFromOpenApiFile(path, settings, out var diag); + return server; + } - return server; - } + public static void RunServer(IEnumerable mappings) + { + string url1 = "http://localhost:9091/"; - public static void RunServer(IEnumerable mappings) + var server = WireMockServer.Start(new WireMockServerSettings { - string url1 = "http://localhost:9091/"; - - var server = WireMockServer.Start(new WireMockServerSettings - { - AllowCSharpCodeMatcher = true, - Urls = new[] { url1 }, - StartAdminInterface = true, - ReadStaticMappings = false, - WatchStaticMappings = false, - WatchStaticMappingsInSubdirectories = false, - Logger = new WireMockConsoleLogger(), - }); - Console.WriteLine("WireMockServer listening at {0}", string.Join(",", server.Urls)); + AllowCSharpCodeMatcher = true, + Urls = new[] { url1 }, + StartAdminInterface = true, + ReadStaticMappings = false, + WatchStaticMappings = false, + WatchStaticMappingsInSubdirectories = false, + Logger = new WireMockConsoleLogger(), + }); + Console.WriteLine("WireMockServer listening at {0}", string.Join(",", server.Urls)); - server.SetBasicAuthentication("a", "b"); + server.SetBasicAuthentication("a", "b"); - server.WithMapping(mappings.ToArray()); + server.WithMapping(mappings.ToArray()); - Console.WriteLine("Press any key to stop the server"); - System.Console.ReadKey(); - server.Stop(); - } + Console.WriteLine("Press any key to stop the server"); + System.Console.ReadKey(); + server.Stop(); } } \ No newline at end of file diff --git a/examples/WireMock.Net.StandAlone.NETCoreApp/Program.cs b/examples/WireMock.Net.StandAlone.NETCoreApp/Program.cs index ff7105953..48f1577a1 100644 --- a/examples/WireMock.Net.StandAlone.NETCoreApp/Program.cs +++ b/examples/WireMock.Net.StandAlone.NETCoreApp/Program.cs @@ -24,8 +24,8 @@ static class Program static async Task Main(string[] args) { - await TestAsync().ConfigureAwait(false); - return; + //await TestAsync().ConfigureAwait(false); + //return; XmlConfigurator.Configure(LogRepository, new FileInfo("log4net.config")); diff --git a/examples/WireMock.Net.StandAlone.NETCoreApp/WireMockLog4NetLogger.cs b/examples/WireMock.Net.StandAlone.NETCoreApp/WireMockLog4NetLogger.cs index 27add78ac..f95113a45 100644 --- a/examples/WireMock.Net.StandAlone.NETCoreApp/WireMockLog4NetLogger.cs +++ b/examples/WireMock.Net.StandAlone.NETCoreApp/WireMockLog4NetLogger.cs @@ -1,44 +1,43 @@ -using System; +using System; using log4net; using Newtonsoft.Json; using WireMock.Admin.Requests; using WireMock.Logging; -namespace WireMock.Net.StandAlone.NETCoreApp +namespace WireMock.Net.StandAlone.NETCoreApp; + +internal class WireMockLog4NetLogger : IWireMockLogger { - internal class WireMockLog4NetLogger : IWireMockLogger + private static readonly ILog Log = LogManager.GetLogger(typeof(Program)); + + public void Debug(string formatString, params object[] args) + { + Log.DebugFormat(formatString, args); + } + + public void Info(string formatString, params object[] args) + { + Log.InfoFormat(formatString, args); + } + + public void Warn(string formatString, params object[] args) + { + Log.WarnFormat(formatString, args); + } + + public void Error(string formatString, params object[] args) + { + Log.ErrorFormat(formatString, args); + } + + public void Error(string message, Exception exception) + { + Log.Error(message, exception); + } + + public void DebugRequestResponse(LogEntryModel logEntryModel, bool isAdminRequest) { - private static readonly ILog Log = LogManager.GetLogger(typeof(Program)); - - public void Debug(string formatString, params object[] args) - { - Log.DebugFormat(formatString, args); - } - - public void Info(string formatString, params object[] args) - { - Log.InfoFormat(formatString, args); - } - - public void Warn(string formatString, params object[] args) - { - Log.WarnFormat(formatString, args); - } - - public void Error(string formatString, params object[] args) - { - Log.ErrorFormat(formatString, args); - } - - public void Error(string message, Exception exception) - { - Log.Error(message, exception); - } - - public void DebugRequestResponse(LogEntryModel logEntryModel, bool isAdminRequest) - { - string message = JsonConvert.SerializeObject(logEntryModel, Formatting.Indented); - Log.DebugFormat("Admin[{0}] {1}", isAdminRequest, message); - } + string message = JsonConvert.SerializeObject(logEntryModel, Formatting.Indented); + Log.DebugFormat("Admin[{0}] {1}", isAdminRequest, message); } } \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/Extensions/WireMockServerExtensions.cs b/src/WireMock.Net.OpenApiParser/Extensions/WireMockServerExtensions.cs index f174ca18e..cba9daec0 100644 --- a/src/WireMock.Net.OpenApiParser/Extensions/WireMockServerExtensions.cs +++ b/src/WireMock.Net.OpenApiParser/Extensions/WireMockServerExtensions.cs @@ -31,13 +31,13 @@ public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer se /// /// The WireMockServer instance /// Path containing OpenAPI file to parse and use the mappings. - /// Returns diagnostic object containing errors detected during parsing /// Additional settings + /// Returns diagnostic object containing errors detected during parsing [PublicAPI] public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer server, string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) { - Guard.NotNull(server, nameof(server)); - Guard.NotNullOrEmpty(path, nameof(path)); + Guard.NotNull(server); + Guard.NotNullOrEmpty(path); var mappings = new WireMockOpenApiParser().FromFile(path, settings, out diagnostic); @@ -80,9 +80,9 @@ public static IWireMockServer WithMappingFromOpenApiStream(this IWireMockServer /// /// The WireMockServer instance /// The OpenAPI document to use as mappings. - /// Additional settings [optional] + /// Additional settings [optional]. [PublicAPI] - public static IWireMockServer WithMappingFromOpenApiDocument(this IWireMockServer server, OpenApiDocument document, WireMockOpenApiParserSettings settings) + public static IWireMockServer WithMappingFromOpenApiDocument(this IWireMockServer server, OpenApiDocument document, WireMockOpenApiParserSettings? settings = null) { Guard.NotNull(server); Guard.NotNull(document); diff --git a/src/WireMock.Net.OpenApiParser/IWireMockOpenApiParser.cs b/src/WireMock.Net.OpenApiParser/IWireMockOpenApiParser.cs index a22e144bf..c85304d83 100644 --- a/src/WireMock.Net.OpenApiParser/IWireMockOpenApiParser.cs +++ b/src/WireMock.Net.OpenApiParser/IWireMockOpenApiParser.cs @@ -5,53 +5,69 @@ using WireMock.Admin.Mappings; using WireMock.Net.OpenApiParser.Settings; -namespace WireMock.Net.OpenApiParser +namespace WireMock.Net.OpenApiParser; + +/// +/// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock MappingModels. +/// +public interface IWireMockOpenApiParser { /// - /// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock MappingModels. + /// Generate from a file-path. + /// + /// The path to read the OpenApi/Swagger/V2/V3 or Raml file. + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromFile(string path, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from a file-path. + /// + /// The path to read the OpenApi/Swagger/V2/V3 or Raml file. + /// Additional settings + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from an . + /// + /// The source OpenApiDocument + /// Additional settings [optional] + /// MappingModel + IReadOnlyList FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings? settings = null); + + /// + /// Generate from a . + /// + /// The source stream + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromStream(Stream stream, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from a . + /// + /// The source stream + /// Additional settings + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from a . + /// + /// The source text + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromText(string text, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from a . /// - public interface IWireMockOpenApiParser - { - /// - /// Generate from a file-path. - /// - /// The path to read the OpenApi/Swagger/V2/V3 or Raml file. - /// OpenApiDiagnostic output - /// MappingModel - IEnumerable FromFile(string path, out OpenApiDiagnostic diagnostic); - - /// - /// Generate from a file-path. - /// - /// The path to read the OpenApi/Swagger/V2/V3 or Raml file. - /// Additional settings - /// OpenApiDiagnostic output - /// MappingModel - IEnumerable FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); - - /// - /// Generate from an . - /// - /// The source OpenApiDocument - /// Additional settings [optional] - /// MappingModel - IEnumerable FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings? settings = null); - - /// - /// Generate from a . - /// - /// The source stream - /// OpenApiDiagnostic output - /// MappingModel - IEnumerable FromStream(Stream stream, out OpenApiDiagnostic diagnostic); - - /// - /// Generate from a . - /// - /// The source stream - /// Additional settings - /// OpenApiDiagnostic output - /// MappingModel - IEnumerable FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); - } + /// The source text + /// Additional settings + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromText(string text, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); } \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/Mappers/OpenApiPathsMapper.cs b/src/WireMock.Net.OpenApiParser/Mappers/OpenApiPathsMapper.cs index ae1701bc7..09a5ae4ca 100644 --- a/src/WireMock.Net.OpenApiParser/Mappers/OpenApiPathsMapper.cs +++ b/src/WireMock.Net.OpenApiParser/Mappers/OpenApiPathsMapper.cs @@ -31,19 +31,29 @@ public OpenApiPathsMapper(WireMockOpenApiParserSettings settings) _exampleValueGenerator = new ExampleValueGenerator(settings); } - public IEnumerable ToMappingModels(OpenApiPaths paths, IList servers) + public IReadOnlyList ToMappingModels(OpenApiPaths? paths, IList servers) { - return paths.Select(p => MapPath(p.Key, p.Value, servers)).SelectMany(x => x); + return paths? + .OrderBy(p => p.Key) + .Select(p => MapPath(p.Key, p.Value, servers)) + .SelectMany(x => x) + .ToArray() ?? + Array.Empty(); } - private IEnumerable MapPaths(OpenApiPaths paths, IList servers) + private IReadOnlyList MapPaths(OpenApiPaths? paths, IList servers) { - return paths.Select(p => MapPath(p.Key, p.Value, servers)).SelectMany(x => x); + return paths? + .OrderBy(p => p.Key) + .Select(p => MapPath(p.Key, p.Value, servers)) + .SelectMany(x => x) + .ToArray() ?? + Array.Empty(); } - private IEnumerable MapPath(string path, OpenApiPathItem pathItem, IList servers) + private IReadOnlyList MapPath(string path, OpenApiPathItem pathItem, IList servers) { - return pathItem.Operations.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value, servers)); + return pathItem.Operations.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value, servers)).ToArray(); } private MappingModel MapOperationToMappingModel(string path, string httpMethod, OpenApiOperation operation, IList servers) @@ -123,7 +133,7 @@ private MappingModel MapOperationToMappingModel(string path, string httpMethod, }; } - private bool TryGetContent(IDictionary? contents, [NotNullWhen(true)] out OpenApiMediaType? openApiMediaType, [NotNullWhen(true)] out string? contentType) + private static bool TryGetContent(IDictionary? contents, [NotNullWhen(true)] out OpenApiMediaType? openApiMediaType, [NotNullWhen(true)] out string? contentType) { openApiMediaType = null; contentType = null; @@ -305,19 +315,19 @@ private string MapBasePath(IList? servers) return JObject.Parse(outputString.ToString()); } - private IDictionary? MapHeaders(string responseContentType, IDictionary headers) + private IDictionary? MapHeaders(string? responseContentType, IDictionary? headers) { - var mappedHeaders = headers.ToDictionary( + var mappedHeaders = headers?.ToDictionary( item => item.Key, - _ => GetExampleMatcherModel(null, _settings.HeaderPatternToUse).Pattern + _ => GetExampleMatcherModel(null, _settings.HeaderPatternToUse).Pattern! ); if (!string.IsNullOrEmpty(responseContentType)) { - mappedHeaders.TryAdd(HeaderContentType, responseContentType); + mappedHeaders.TryAdd(HeaderContentType, responseContentType!); } - return mappedHeaders.Keys.Any() ? mappedHeaders : null; + return mappedHeaders?.Keys.Any() == true ? mappedHeaders : null; } private IList? MapQueryParameters(IEnumerable queryParameters) @@ -360,9 +370,18 @@ private MatcherModel GetExampleMatcherModel(OpenApiSchema? schema, ExampleValueT { return type switch { - ExampleValueType.Value => new MatcherModel { Name = "ExactMatcher", Pattern = GetExampleValueAsStringForSchemaType(schema), IgnoreCase = _settings.IgnoreCaseExampleValues }, + ExampleValueType.Value => new MatcherModel + { + Name = "ExactMatcher", + Pattern = GetExampleValueAsStringForSchemaType(schema), + IgnoreCase = _settings.IgnoreCaseExampleValues + }, - _ => new MatcherModel { Name = "WildcardMatcher", Pattern = "*" } + _ => new MatcherModel + { + Name = "WildcardMatcher", + Pattern = "*" + } }; } diff --git a/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserExampleValues.cs b/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserExampleValues.cs index 6b3af5096..5f550d195 100644 --- a/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserExampleValues.cs +++ b/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserExampleValues.cs @@ -36,5 +36,5 @@ public class WireMockOpenApiParserExampleValues : IWireMockOpenApiParserExampleV public virtual string String { get; set; } = "example-string"; /// - public virtual OpenApiSchema? Schema { get; set; } = new OpenApiSchema(); + public virtual OpenApiSchema? Schema { get; set; } = new(); } \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/Utils/ExampleValueGenerator.cs b/src/WireMock.Net.OpenApiParser/Utils/ExampleValueGenerator.cs index 2bc15d1ad..9edde43d3 100644 --- a/src/WireMock.Net.OpenApiParser/Utils/ExampleValueGenerator.cs +++ b/src/WireMock.Net.OpenApiParser/Utils/ExampleValueGenerator.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Linq; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Stef.Validation; @@ -11,116 +10,108 @@ namespace WireMock.Net.OpenApiParser.Utils; internal class ExampleValueGenerator { - private readonly WireMockOpenApiParserSettings _settings; + private readonly IWireMockOpenApiParserExampleValues _exampleValues; public ExampleValueGenerator(WireMockOpenApiParserSettings settings) { - _settings = Guard.NotNull(settings); + Guard.NotNull(settings); // Check if user provided an own implementation if (settings.ExampleValues is null) { - if (_settings.DynamicExamples) + if (settings.DynamicExamples) { - _settings.ExampleValues = new WireMockOpenApiParserDynamicExampleValues(); + _exampleValues = new WireMockOpenApiParserDynamicExampleValues(); } else { - _settings.ExampleValues = new WireMockOpenApiParserExampleValues(); + _exampleValues = new WireMockOpenApiParserExampleValues(); } } + else + { + _exampleValues = settings.ExampleValues; + } } public object GetExampleValue(OpenApiSchema? schema) { var schemaExample = schema?.Example; - var schemaEnum = GetRandomEnumValue(schema?.Enum); + var schemaEnum = schema?.Enum?.FirstOrDefault(); - _settings.ExampleValues.Schema = schema; + _exampleValues.Schema = schema; switch (schema?.GetSchemaType()) { case SchemaType.Boolean: var exampleBoolean = schemaExample as OpenApiBoolean; - return exampleBoolean is null ? _settings.ExampleValues.Boolean : exampleBoolean.Value; + return exampleBoolean?.Value ?? _exampleValues.Boolean; case SchemaType.Integer: switch (schema?.GetSchemaFormat()) { case SchemaFormat.Int64: - var exampleLong = (OpenApiLong)schemaExample; - var enumLong = (OpenApiLong)schemaEnum; - var valueLongEnumOrExample = enumLong is null ? exampleLong?.Value : enumLong?.Value; - return valueLongEnumOrExample ?? _settings.ExampleValues.Integer; + var exampleLong = schemaExample as OpenApiLong; + var enumLong = schemaEnum as OpenApiLong; + var valueLongEnumOrExample = enumLong?.Value ?? exampleLong?.Value; + return valueLongEnumOrExample ?? _exampleValues.Integer; default: - var exampleInteger = (OpenApiInteger)schemaExample; - var enumInteger = (OpenApiInteger)schemaEnum; - var valueIntegerEnumOrExample = enumInteger is null ? exampleInteger?.Value : enumInteger?.Value; - return valueIntegerEnumOrExample ?? _settings.ExampleValues.Integer; + var exampleInteger = schemaExample as OpenApiInteger; + var enumInteger = schemaEnum as OpenApiInteger; + var valueIntegerEnumOrExample = enumInteger?.Value ?? exampleInteger?.Value; + return valueIntegerEnumOrExample ?? _exampleValues.Integer; } case SchemaType.Number: switch (schema?.GetSchemaFormat()) { case SchemaFormat.Float: - var exampleFloat = (OpenApiFloat)schemaExample; - var enumFloat = (OpenApiFloat)schemaEnum; - var valueFloatEnumOrExample = enumFloat is null ? exampleFloat?.Value : enumFloat?.Value; - return valueFloatEnumOrExample ?? _settings.ExampleValues.Float; + var exampleFloat = schemaExample as OpenApiFloat; + var enumFloat = schemaEnum as OpenApiFloat; + var valueFloatEnumOrExample = enumFloat?.Value ?? exampleFloat?.Value; + return valueFloatEnumOrExample ?? _exampleValues.Float; default: - var exampleDouble = (OpenApiDouble)schemaExample; - var enumDouble = (OpenApiDouble)schemaEnum; - var valueDoubleEnumOrExample = enumDouble is null ? exampleDouble?.Value : enumDouble?.Value; - return valueDoubleEnumOrExample ?? _settings.ExampleValues.Double; + var exampleDouble = schemaExample as OpenApiDouble; + var enumDouble = schemaEnum as OpenApiDouble; + var valueDoubleEnumOrExample = enumDouble?.Value ?? exampleDouble?.Value; + return valueDoubleEnumOrExample ?? _exampleValues.Double; } default: switch (schema?.GetSchemaFormat()) { case SchemaFormat.Date: - var exampleDate = (OpenApiDate)schemaExample; - var enumDate = (OpenApiDate)schemaEnum; - var valueDateEnumOrExample = enumDate is null ? exampleDate?.Value : enumDate?.Value; - return DateTimeUtils.ToRfc3339Date(valueDateEnumOrExample ?? _settings.ExampleValues.Date()); + var exampleDate = schemaExample as OpenApiDate; + var enumDate = schemaEnum as OpenApiDate; + var valueDateEnumOrExample = enumDate?.Value ?? exampleDate?.Value; + return DateTimeUtils.ToRfc3339Date(valueDateEnumOrExample ?? _exampleValues.Date()); case SchemaFormat.DateTime: - var exampleDateTime = (OpenApiDateTime)schemaExample; - var enumDateTime = (OpenApiDateTime)schemaEnum; - var valueDateTimeEnumOrExample = enumDateTime is null ? exampleDateTime?.Value : enumDateTime?.Value; - return DateTimeUtils.ToRfc3339DateTime(valueDateTimeEnumOrExample?.DateTime ?? _settings.ExampleValues.DateTime()); + var exampleDateTime = schemaExample as OpenApiDateTime; + var enumDateTime = schemaEnum as OpenApiDateTime; + var valueDateTimeEnumOrExample = enumDateTime?.Value ?? exampleDateTime?.Value; + return DateTimeUtils.ToRfc3339DateTime(valueDateTimeEnumOrExample?.DateTime ?? _exampleValues.DateTime()); case SchemaFormat.Byte: - var exampleByte = (OpenApiByte)schemaExample; - var enumByte = (OpenApiByte)schemaEnum; - var valueByteEnumOrExample = enumByte is null ? exampleByte?.Value : enumByte?.Value; - return valueByteEnumOrExample ?? _settings.ExampleValues.Bytes; + var exampleByte = schemaExample as OpenApiByte; + var enumByte = schemaEnum as OpenApiByte; + var valueByteEnumOrExample = enumByte?.Value ?? exampleByte?.Value; + return valueByteEnumOrExample ?? _exampleValues.Bytes; case SchemaFormat.Binary: - var exampleBinary = (OpenApiBinary)schemaExample; - var enumBinary = (OpenApiBinary)schemaEnum; - var valueBinaryEnumOrExample = enumBinary is null ? exampleBinary?.Value : enumBinary?.Value; - return valueBinaryEnumOrExample ?? _settings.ExampleValues.Object; + var exampleBinary = schemaExample as OpenApiBinary; + var enumBinary = schemaEnum as OpenApiBinary; + var valueBinaryEnumOrExample = enumBinary?.Value ?? exampleBinary?.Value; + return valueBinaryEnumOrExample ?? _exampleValues.Object; default: - var exampleString = (OpenApiString)schemaExample; - var enumString = (OpenApiString)schemaEnum; - var valueStringEnumOrExample = enumString is null ? exampleString?.Value : enumString?.Value; - return valueStringEnumOrExample ?? _settings.ExampleValues.String; + var exampleString = schemaExample as OpenApiString; + var enumString = schemaEnum as OpenApiString; + var valueStringEnumOrExample = enumString?.Value ?? exampleString?.Value; + return valueStringEnumOrExample ?? _exampleValues.String; } } } - - private static IOpenApiAny? GetRandomEnumValue(IList? schemaEnum) - { - if (schemaEnum?.Count > 0) - { - int maxValue = schemaEnum.Count - 1; - int randomEnum = new Random().Next(0, maxValue); - return schemaEnum[randomEnum]; - } - - return null; - } } \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/WireMock.Net.OpenApiParser.csproj b/src/WireMock.Net.OpenApiParser/WireMock.Net.OpenApiParser.csproj index 12636783c..ee1972f46 100644 --- a/src/WireMock.Net.OpenApiParser/WireMock.Net.OpenApiParser.csproj +++ b/src/WireMock.Net.OpenApiParser/WireMock.Net.OpenApiParser.csproj @@ -4,7 +4,7 @@ An OpenApi (swagger) parser to generate MappingModel or mapping.json file. net46;netstandard2.0;netstandard2.1 true - wiremock;openapi;OAS;converter;parser;openapiparser + wiremock;openapi;OAS;raml;converter;parser;openapiparser {D3804228-91F4-4502-9595-39584E5AADAD} true ../WireMock.Net/WireMock.Net.ruleset diff --git a/src/WireMock.Net.OpenApiParser/WireMockOpenApiParser.cs b/src/WireMock.Net.OpenApiParser/WireMockOpenApiParser.cs index e9ba071c8..d690f8cb7 100644 --- a/src/WireMock.Net.OpenApiParser/WireMockOpenApiParser.cs +++ b/src/WireMock.Net.OpenApiParser/WireMockOpenApiParser.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using JetBrains.Annotations; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers; @@ -16,18 +17,18 @@ namespace WireMock.Net.OpenApiParser; /// public class WireMockOpenApiParser : IWireMockOpenApiParser { - private readonly OpenApiStreamReader _reader = new OpenApiStreamReader(); + private readonly OpenApiStreamReader _reader = new(); - /// + /// [PublicAPI] - public IEnumerable FromFile(string path, out OpenApiDiagnostic diagnostic) + public IReadOnlyList FromFile(string path, out OpenApiDiagnostic diagnostic) { return FromFile(path, new WireMockOpenApiParserSettings(), out diagnostic); } - /// + /// [PublicAPI] - public IEnumerable FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) + public IReadOnlyList FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) { OpenApiDocument document; if (Path.GetExtension(path).EndsWith("raml", StringComparison.OrdinalIgnoreCase)) @@ -44,24 +45,38 @@ public IEnumerable FromFile(string path, WireMockOpenApiParserSett return FromDocument(document, settings); } - /// + /// [PublicAPI] - public IEnumerable FromStream(Stream stream, out OpenApiDiagnostic diagnostic) + public IReadOnlyList FromDocument(OpenApiDocument openApiDocument, WireMockOpenApiParserSettings? settings = null) + { + return new OpenApiPathsMapper(settings ?? new WireMockOpenApiParserSettings()).ToMappingModels(openApiDocument.Paths, openApiDocument.Servers); + } + + /// + [PublicAPI] + public IReadOnlyList FromStream(Stream stream, out OpenApiDiagnostic diagnostic) { return FromDocument(_reader.Read(stream, out diagnostic)); } - /// + /// [PublicAPI] - public IEnumerable FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) + public IReadOnlyList FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) { return FromDocument(_reader.Read(stream, out diagnostic), settings); } - /// + /// + [PublicAPI] + public IReadOnlyList FromText(string text, out OpenApiDiagnostic diagnostic) + { + return FromStream(new MemoryStream(Encoding.UTF8.GetBytes(text)), out diagnostic); + } + + /// [PublicAPI] - public IEnumerable FromDocument(OpenApiDocument openApiDocument, WireMockOpenApiParserSettings? settings = null) + public IReadOnlyList FromText(string text, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) { - return new OpenApiPathsMapper(settings).ToMappingModels(openApiDocument.Paths, openApiDocument.Servers); + return FromStream(new MemoryStream(Encoding.UTF8.GetBytes(text)), settings, out diagnostic); } } \ No newline at end of file diff --git a/src/WireMock.Net.RestClient/IWireMockAdminApi.cs b/src/WireMock.Net.RestClient/IWireMockAdminApi.cs index d304e9db5..92fdcb709 100644 --- a/src/WireMock.Net.RestClient/IWireMockAdminApi.cs +++ b/src/WireMock.Net.RestClient/IWireMockAdminApi.cs @@ -280,4 +280,20 @@ public interface IWireMockAdminApi /// The optional cancellationToken. [Head("files/{filename}")] Task FileExistsAsync([Path] string filename, CancellationToken cancellationToken = default); + + /// + /// Convert an OpenApi / RAML document to mappings. + /// + /// The OpenApi or RAML document as text. + /// The optional cancellationToken. + [Post("openapi/convert")] + Task> OpenApiConvertAsync([Body] string text, CancellationToken cancellationToken = default); + + /// + /// Convert an OpenApi / RAML document to mappings and save these. + /// + /// The OpenApi or RAML document as text. + /// The optional cancellationToken. + [Post("openapi/save")] + Task OpenApiSaveAsync([Body] string text, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/WireMock.Net/IMapping.cs b/src/WireMock.Net/IMapping.cs index 86f35dd50..b53caa0a0 100644 --- a/src/WireMock.Net/IMapping.cs +++ b/src/WireMock.Net/IMapping.cs @@ -121,7 +121,7 @@ public interface IMapping /// /// Use Fire and Forget for the defined webhook(s). [Optional] /// - bool? UseWebhooksFireAndForget { get; set; } + bool? UseWebhooksFireAndForget { get; } /// /// Data Object which can be used when WithTransformer is used. @@ -130,7 +130,7 @@ public interface IMapping /// lookup data "1" /// /// - object? Data { get; set; } + object? Data { get; } /// /// ProvideResponseAsync diff --git a/src/WireMock.Net/Mapping.cs b/src/WireMock.Net/Mapping.cs index e5823a1e8..6323fbe22 100644 --- a/src/WireMock.Net/Mapping.cs +++ b/src/WireMock.Net/Mapping.cs @@ -67,13 +67,13 @@ public class Mapping : IMapping public IWebhook[]? Webhooks { get; } /// - public bool? UseWebhooksFireAndForget { get; set; } + public bool? UseWebhooksFireAndForget { get; } /// public ITimeSettings? TimeSettings { get; } /// - public object? Data { get; set; } + public object? Data { get; } /// /// Initializes a new instance of the class. diff --git a/src/WireMock.Net/Models/UrlDetails.cs b/src/WireMock.Net/Models/UrlDetails.cs index 9459a0c19..7f227fbd5 100644 --- a/src/WireMock.Net/Models/UrlDetails.cs +++ b/src/WireMock.Net/Models/UrlDetails.cs @@ -1,51 +1,47 @@ -using System; +using System; using Stef.Validation; -namespace WireMock.Models +namespace WireMock.Models; + +/// +/// UrlDetails +/// +public class UrlDetails { /// - /// UrlDetails + /// Gets the url (relative). /// - public class UrlDetails - { - /// - /// Gets the url (relative). - /// - public Uri Url { get; } + public Uri Url { get; } - /// - /// Gets the AbsoluteUrl. - /// - public Uri AbsoluteUrl { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The URL. - public UrlDetails(string url) : this(new Uri(url)) - { - } + /// + /// Gets the AbsoluteUrl. + /// + public Uri AbsoluteUrl { get; } - /// - /// Initializes a new instance of the class. - /// - /// The URL. - public UrlDetails(Uri url) : this(url, url) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The URL. + public UrlDetails(string url) : this(new Uri(url)) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The absolute URL. - /// The URL (relative). - public UrlDetails(Uri absoluteUrl, Uri url) - { - Guard.NotNull(absoluteUrl, nameof(absoluteUrl)); - Guard.NotNull(url, nameof(url)); + /// + /// Initializes a new instance of the class. + /// + /// The URL. + public UrlDetails(Uri url) : this(url, url) + { + } - AbsoluteUrl = absoluteUrl; - Url = url; - } + /// + /// Initializes a new instance of the class. + /// + /// The absolute URL. + /// The URL (relative). + public UrlDetails(Uri absoluteUrl, Uri url) + { + AbsoluteUrl = Guard.NotNull(absoluteUrl); + Url = Guard.NotNull(url); } } \ No newline at end of file diff --git a/src/WireMock.Net/ResponseBuilders/IProxyResponseBuilder.cs b/src/WireMock.Net/ResponseBuilders/IProxyResponseBuilder.cs index 716cdd0dc..e5ff1d01a 100644 --- a/src/WireMock.Net/ResponseBuilders/IProxyResponseBuilder.cs +++ b/src/WireMock.Net/ResponseBuilders/IProxyResponseBuilder.cs @@ -27,7 +27,7 @@ public interface IProxyResponseBuilder : IStatusCodeResponseBuilder /// WithProxy using . /// /// The proxy url. - /// The X509Certificate2. + /// The X509Certificate2. /// A . IResponseBuilder WithProxy(string proxyUrl, X509Certificate2 certificate); } \ No newline at end of file diff --git a/src/WireMock.Net/Serialization/SwaggerMapper.cs b/src/WireMock.Net/Serialization/SwaggerMapper.cs index 9439974d2..5cbdf502d 100644 --- a/src/WireMock.Net/Serialization/SwaggerMapper.cs +++ b/src/WireMock.Net/Serialization/SwaggerMapper.cs @@ -54,10 +54,12 @@ public static string ToSwagger(WireMockServer server) { operation.Parameters.Add(openApiParameter); } + foreach (var openApiParameter in MapRequestHeaders(mapping.Request.Headers)) { operation.Parameters.Add(openApiParameter); } + foreach (var openApiParameter in MapRequestCookies(mapping.Request.Cookies)) { operation.Parameters.Add(openApiParameter); @@ -94,7 +96,7 @@ public static string ToSwagger(WireMockServer server) return openApiDocument.ToJson(SchemaType.OpenApi3, Formatting.Indented); } - private static IEnumerable MapRequestQueryParameters(IList? queryParameters) + private static IReadOnlyList MapRequestQueryParameters(IList? queryParameters) { if (queryParameters == null) { @@ -146,7 +148,7 @@ private static IEnumerable MapRequestHeaders(IList MapRequestCookies(IList? cookies) + private static IReadOnlyList MapRequestCookies(IList? cookies) { if (cookies == null) { diff --git a/src/WireMock.Net/Server/WireMockServer.Admin.cs b/src/WireMock.Net/Server/WireMockServer.Admin.cs index 90294f682..1864cc11d 100644 --- a/src/WireMock.Net/Server/WireMockServer.Admin.cs +++ b/src/WireMock.Net/Server/WireMockServer.Admin.cs @@ -37,6 +37,8 @@ public partial class WireMockServer private const string AdminRequests = "/__admin/requests"; private const string AdminSettings = "/__admin/settings"; private const string AdminScenarios = "/__admin/scenarios"; + private const string AdminOpenApi = "/__admin/openapi"; + private const string QueryParamReloadStaticMappings = "reloadStaticMappings"; private static readonly Guid ProxyMappingGuid = new("e59914fd-782e-428e-91c1-4810ffb86567"); @@ -113,6 +115,10 @@ private void InitAdmin() Given(Request.Create().WithPath(_adminFilesFilenamePathMatcher).UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(FileGet)); Given(Request.Create().WithPath(_adminFilesFilenamePathMatcher).UsingHead()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(FileHead)); Given(Request.Create().WithPath(_adminFilesFilenamePathMatcher).UsingDelete()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(FileDelete)); + + // __admin/openapi + Given(Request.Create().WithPath($"{AdminOpenApi}/convert").UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(OpenApiConvertToMappings)); + Given(Request.Create().WithPath($"{AdminOpenApi}/save").UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(OpenApiSaveToMappings)); } #endregion @@ -737,7 +743,7 @@ private void EnhancedFileSystemWatcherDeleted(object sender, FileSystemEventArgs return encodingModel != null ? Encoding.GetEncoding(encodingModel.CodePage) : null; } - private static ResponseMessage ToJson(T result, bool keepNullValues = false) + private static ResponseMessage ToJson(T result, bool keepNullValues = false, object? statusCode = null) { return new ResponseMessage { @@ -746,7 +752,7 @@ private static ResponseMessage ToJson(T result, bool keepNullValues = false) DetectedBodyType = BodyType.String, BodyAsString = JsonConvert.SerializeObject(result, keepNullValues ? JsonSerializationConstants.JsonSerializerSettingsIncludeNullValues : JsonSerializationConstants.JsonSerializerSettingsDefault) }, - StatusCode = (int)HttpStatusCode.OK, + StatusCode = statusCode ?? (int)HttpStatusCode.OK, Headers = new Dictionary> { { HttpKnownHeaderNames.ContentType, new WireMockList(WireMockConstants.ContentTypeJson) } } }; } diff --git a/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs b/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs index 052440ad9..3786d839a 100644 --- a/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs +++ b/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Stef.Validation; using WireMock.Admin.Mappings; @@ -14,7 +15,7 @@ namespace WireMock.Server; public partial class WireMockServer { - private void ConvertMappingsAndRegisterAsRespondProvider(MappingModel[] mappingModels, string? path = null) + private void ConvertMappingsAndRegisterAsRespondProvider(IReadOnlyList mappingModels, string? path = null) { var duplicateGuids = mappingModels .Where(m => m.Guid != null) @@ -46,7 +47,7 @@ private void ConvertMappingsAndRegisterAsRespondProvider(MappingModel[] mappingM } var respondProvider = Given(requestBuilder, mappingModel.SaveToFile == true); - + if (guid != null) { respondProvider = respondProvider.WithGuid(guid.Value); @@ -107,7 +108,10 @@ private void ConvertMappingsAndRegisterAsRespondProvider(MappingModel[] mappingM respondProvider = respondProvider.WithWebhook(webhooks); } - respondProvider.WithWebhookFireAndForget(mappingModel.UseWebhooksFireAndForget ?? false); + if (mappingModel.UseWebhooksFireAndForget == true) + { + respondProvider.WithWebhookFireAndForget(mappingModel.UseWebhooksFireAndForget.Value); + } var responseBuilder = InitResponseBuilder(mappingModel.Response); respondProvider.RespondWith(responseBuilder); @@ -205,11 +209,11 @@ private void ConvertMappingsAndRegisterAsRespondProvider(MappingModel[] mappingM { foreach (var cookieModel in requestModel.Cookies.Where(c => c.Matchers != null)) { - requestBuilder = requestBuilder.WithCookie( - cookieModel.Name, - cookieModel.IgnoreCase == true, - cookieModel.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch, - cookieModel.Matchers!.Select(_matcherMapper.Map).OfType().ToArray()); + requestBuilder = requestBuilder.WithCookie( + cookieModel.Name, + cookieModel.IgnoreCase == true, + cookieModel.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch, + cookieModel.Matchers!.Select(_matcherMapper.Map).OfType().ToArray()); } } diff --git a/src/WireMock.Net/Server/WireMockServer.OpenApiParser.cs b/src/WireMock.Net/Server/WireMockServer.OpenApiParser.cs new file mode 100644 index 000000000..38284c611 --- /dev/null +++ b/src/WireMock.Net/Server/WireMockServer.OpenApiParser.cs @@ -0,0 +1,54 @@ +#if OPENAPIPARSER +using System; +using System.Linq; +using System.Net; +using WireMock.Net.OpenApiParser; +#endif + +namespace WireMock.Server; + +public partial class WireMockServer +{ + private IResponseMessage OpenApiConvertToMappings(IRequestMessage requestMessage) + { +#if OPENAPIPARSER + try + { + var mappingModels = new WireMockOpenApiParser().FromText(requestMessage.Body, out var diagnostic); + return diagnostic.Errors.Any() ? ToJson(diagnostic, false, HttpStatusCode.BadRequest) : ToJson(mappingModels); + } + catch (Exception e) + { + _settings.Logger.Error("HttpStatusCode set to {0} {1}", HttpStatusCode.BadRequest, e); + return ResponseMessageBuilder.Create(e.Message, HttpStatusCode.BadRequest); + } +#else + return ResponseMessageBuilder.Create("Not supported for .NETStandard 1.3 and .NET 4.5.2 or lower.", 400); +#endif + } + + private IResponseMessage OpenApiSaveToMappings(IRequestMessage requestMessage) + { +#if OPENAPIPARSER + try + { + var mappingModels = new WireMockOpenApiParser().FromText(requestMessage.Body, out var diagnostic); + if (diagnostic.Errors.Any()) + { + return ToJson(diagnostic, false, HttpStatusCode.BadRequest); + } + + ConvertMappingsAndRegisterAsRespondProvider(mappingModels); + + return ResponseMessageBuilder.Create("OpenApi document converted to Mappings", HttpStatusCode.Created); + } + catch (Exception e) + { + _settings.Logger.Error("HttpStatusCode set to {0} {1}", HttpStatusCode.BadRequest, e); + return ResponseMessageBuilder.Create(e.Message, HttpStatusCode.BadRequest); + } +#else + return ResponseMessageBuilder.Create("Not supported for .NETStandard 1.3 and .NET 4.5.2 or lower.", 400); +#endif + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Util/BytesEncodingUtils.cs b/src/WireMock.Net/Util/BytesEncodingUtils.cs index 44af91932..9dc854fd2 100644 --- a/src/WireMock.Net/Util/BytesEncodingUtils.cs +++ b/src/WireMock.Net/Util/BytesEncodingUtils.cs @@ -108,7 +108,7 @@ private static bool IsValid(IReadOnlyList buffer, int position, int length return true; } - if (ch >= 0xc2 && ch <= 0xdf) + if (ch is >= 0xc2 and <= 0xdf) { if (position >= length - 2) { @@ -145,7 +145,7 @@ private static bool IsValid(IReadOnlyList buffer, int position, int length return true; } - if (ch >= 0xe1 && ch <= 0xef) + if (ch is >= 0xe1 and <= 0xef) { if (position >= length - 3) { @@ -204,7 +204,7 @@ private static bool IsValid(IReadOnlyList buffer, int position, int length return true; } - if (ch >= 0xf1 && ch <= 0xf3) + if (ch is >= 0xf1 and <= 0xf3) { if (position >= length - 4) { diff --git a/src/WireMock.Net/Util/RegexUtils.cs b/src/WireMock.Net/Util/RegexUtils.cs index 9b33aff81..3d1e3742d 100644 --- a/src/WireMock.Net/Util/RegexUtils.cs +++ b/src/WireMock.Net/Util/RegexUtils.cs @@ -25,7 +25,7 @@ public static Dictionary GetNamedGroups(Regex regex, string inpu return namedGroupsDictionary; } - public static (bool IsValid, bool Result) MatchRegex(string pattern, string input, bool useRegexExtended = true) + public static (bool IsValid, bool Result) MatchRegex(string? pattern, string input, bool useRegexExtended = true) { if (string.IsNullOrEmpty(pattern)) { diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index fb0d137c8..2759924ec 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -35,15 +35,19 @@ - NETSTANDARD;USE_ASPNETCORE + $(DefineConstants);NETSTANDARD;USE_ASPNETCORE - USE_ASPNETCORE + $(DefineConstants);USE_ASPNETCORE - USE_ASPNETCORE;NET46 + $(DefineConstants);USE_ASPNETCORE;NET46 + + + + $(DefineConstants);OPENAPIPARSER @@ -189,4 +193,8 @@ + + + + \ No newline at end of file diff --git a/test/WireMock.Net.Tests/OpenApiParser/petstore-openapi3.json b/test/WireMock.Net.Tests/OpenApiParser/petstore-openapi3.json new file mode 100644 index 000000000..9f4c45960 --- /dev/null +++ b/test/WireMock.Net.Tests/OpenApiParser/petstore-openapi3.json @@ -0,0 +1,840 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Swagger Petstore - OpenAPI 3.0", + "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) ", + "termsOfService": "http://swagger.io/terms/", + "contact": { "email": "apiteam@swagger.io" }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.4" + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + }, + "servers": [ { "url": "/api/v3" } ], + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Operations about user" + }, + { + "name": "user", + "description": "Access to Petstore orders", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + } + ], + "paths": { + "/pet": { + "put": { + "tags": [ "pet" ], + "summary": "Update an existing pet", + "description": "Update an existing pet by Id", + "operationId": "updatePet", + "requestBody": { + "description": "Update an existent pet in the store", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/Pet" } } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } } + } + }, + "400": { "description": "Invalid ID supplied" }, + "404": { "description": "Pet not found" }, + "405": { "description": "Validation exception" } + }, + "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] + }, + "post": { + "tags": [ "pet" ], + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "operationId": "addPet", + "requestBody": { + "description": "Create a new pet in the store", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/Pet" } } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } } + } + }, + "405": { "description": "Invalid input" } + }, + "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ "pet" ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": false, + "explode": true, + "schema": { + "type": "string", + "default": "available", + "enum": [ "available", "pending", "sold" ] + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/Pet" } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/Pet" } + } + } + } + }, + "400": { "description": "Invalid status value" } + }, + "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] + } + }, + "/pet/findByTags": { + "get": { + "tags": [ "pet" ], + "summary": "Finds Pets by tags", + "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": false, + "explode": true, + "schema": { + "type": "array", + "items": { "type": "string" } + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/Pet" } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/Pet" } + } + } + } + }, + "400": { "description": "Invalid tag value" } + }, + "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] + } + }, + "/pet/{petId}": { + "get": { + "tags": [ "pet" ], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } } + } + }, + "400": { "description": "Invalid ID supplied" }, + "404": { "description": "Pet not found" } + }, + "security": [ + { "api_key": [] }, + { "petstore_auth": [ "write:pets", "read:pets" ] } + ] + }, + "post": { + "tags": [ "pet" ], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "name", + "in": "query", + "description": "Name of pet that needs to be updated", + "schema": { "type": "string" } + }, + { + "name": "status", + "in": "query", + "description": "Status of pet that needs to be updated", + "schema": { "type": "string" } + } + ], + "responses": { "405": { "description": "Invalid input" } }, + "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] + }, + "delete": { + "tags": [ "pet" ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "parameters": [ + { + "name": "api_key", + "in": "header", + "description": "", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { "400": { "description": "Invalid pet value" } }, + "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ "pet" ], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "additionalMetadata", + "in": "query", + "description": "Additional Metadata", + "required": false, + "schema": { "type": "string" } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } + } + }, + "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] + } + }, + "/store/inventory": { + "get": { + "tags": [ "store" ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + } + } + }, + "security": [ { "api_key": [] } ] + } + }, + "/store/order": { + "post": { + "tags": [ "store" ], + "summary": "Place an order for a pet", + "description": "Place a new order in the store", + "operationId": "placeOrder", + "requestBody": { + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/Order" } }, + "application/xml": { "schema": { "$ref": "#/components/schemas/Order" } }, + "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/Order" } } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Order" } } } + }, + "405": { "description": "Invalid input" } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": [ "store" ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions", + "operationId": "getOrderById", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of order that needs to be fetched", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { "schema": { "$ref": "#/components/schemas/Order" } }, + "application/json": { "schema": { "$ref": "#/components/schemas/Order" } } + } + }, + "400": { "description": "Invalid ID supplied" }, + "404": { "description": "Order not found" } + } + }, + "delete": { + "tags": [ "store" ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", + "operationId": "deleteOrder", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "400": { "description": "Invalid ID supplied" }, + "404": { "description": "Order not found" } + } + } + }, + "/user": { + "post": { + "tags": [ "user" ], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "requestBody": { + "description": "Created user object", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/User" } }, + "application/xml": { "schema": { "$ref": "#/components/schemas/User" } }, + "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/User" } } + } + }, + "responses": { + "default": { + "description": "successful operation", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/User" } }, + "application/xml": { "schema": { "$ref": "#/components/schemas/User" } } + } + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ "user" ], + "summary": "Creates list of users with given input array", + "description": "Creates list of users with given input array", + "operationId": "createUsersWithListInput", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/User" } + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { "schema": { "$ref": "#/components/schemas/User" } }, + "application/json": { "schema": { "$ref": "#/components/schemas/User" } } + } + }, + "default": { "description": "successful operation" } + } + } + }, + "/user/login": { + "get": { + "tags": [ "user" ], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": false, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "successful operation", + "headers": { + "X-Rate-Limit": { + "description": "calls per hour allowed by the user", + "schema": { + "type": "integer", + "format": "int32" + } + }, + "X-Expires-After": { + "description": "date in UTC when toekn expires", + "schema": { + "type": "string", + "format": "date-time" + } + } + }, + "content": { + "application/xml": { "schema": { "type": "string" } }, + "application/json": { "schema": { "type": "string" } } + } + }, + "400": { "description": "Invalid username/password supplied" } + } + } + }, + "/user/logout": { + "get": { + "tags": [ "user" ], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "parameters": [], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/user/{username}": { + "get": { + "tags": [ "user" ], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { "schema": { "$ref": "#/components/schemas/User" } }, + "application/json": { "schema": { "$ref": "#/components/schemas/User" } } + } + }, + "400": { "description": "Invalid username supplied" }, + "404": { "description": "User not found" } + } + }, + "put": { + "tags": [ "user" ], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be deleted", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Update an existent user in the store", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/User" } }, + "application/xml": { "schema": { "$ref": "#/components/schemas/User" } }, + "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/User" } } + } + }, + "responses": { "default": { "description": "successful operation" } } + }, + "delete": { + "tags": [ "user" ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "400": { "description": "Invalid username supplied" }, + "404": { "description": "User not found" } + } + } + } + }, + "components": { + "schemas": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "petId": { + "type": "integer", + "format": "int64", + "example": 198772 + }, + "quantity": { + "type": "integer", + "format": "int32", + "example": 7 + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "example": "approved", + "enum": [ "placed", "approved", "delivered" ] + }, + "complete": { "type": "boolean" } + }, + "xml": { "name": "order" } + }, + "Customer": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 100000 + }, + "username": { + "type": "string", + "example": "fehguy" + }, + "address": { + "type": "array", + "xml": { + "name": "addresses", + "wrapped": true + }, + "items": { "$ref": "#/components/schemas/Address" } + } + }, + "xml": { "name": "customer" } + }, + "Address": { + "type": "object", + "properties": { + "street": { + "type": "string", + "example": "437 Lytton" + }, + "city": { + "type": "string", + "example": "Palo Alto" + }, + "state": { + "type": "string", + "example": "CA" + }, + "zip": { + "type": "string", + "example": "94301" + } + }, + "xml": { "name": "address" } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1 + }, + "name": { + "type": "string", + "example": "Dogs" + } + }, + "xml": { "name": "category" } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "username": { + "type": "string", + "example": "theUser" + }, + "firstName": { + "type": "string", + "example": "John" + }, + "lastName": { + "type": "string", + "example": "James" + }, + "email": { + "type": "string", + "example": "john@email.com" + }, + "password": { + "type": "string", + "example": "12345" + }, + "phone": { + "type": "string", + "example": "12345" + }, + "userStatus": { + "type": "integer", + "description": "User Status", + "format": "int32", + "example": 1 + } + }, + "xml": { "name": "user" } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { "type": "string" } + }, + "xml": { "name": "tag" } + }, + "Pet": { + "required": [ "name", "photoUrls" ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "name": { + "type": "string", + "example": "doggie" + }, + "category": { "$ref": "#/components/schemas/Category" }, + "photoUrls": { + "type": "array", + "xml": { "wrapped": true }, + "items": { + "type": "string", + "xml": { "name": "photoUrl" } + } + }, + "tags": { + "type": "array", + "xml": { "wrapped": true }, + "items": { "$ref": "#/components/schemas/Tag" } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ "available", "pending", "sold" ] + } + }, + "xml": { "name": "pet" } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { "type": "string" }, + "message": { "type": "string" } + }, + "xml": { "name": "##default" } + } + }, + "requestBodies": { + "Pet": { + "description": "Pet object that needs to be added to the store", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } } + } + }, + "UserArray": { + "description": "List of user object", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/User" } + } + } + } + } + }, + "securitySchemes": { + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://petstore.swagger.io/oauth/authorize", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + } + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/OpenApiParser/petstore.yml b/test/WireMock.Net.Tests/OpenApiParser/petstore.yml new file mode 100644 index 000000000..9383878a5 --- /dev/null +++ b/test/WireMock.Net.Tests/OpenApiParser/petstore.yml @@ -0,0 +1,730 @@ +swagger: '2.0' +info: + description: 'This is a sample server Petstore server. Copied from https://github.com/swagger-api/swagger-codegen/blob/master/modules/swagger-codegen/src/test/resources/2_0/petstore.yaml.' + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache-2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +host: petstore.swagger.io +basePath: /v2 +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +schemes: + - http +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + consumes: + - application/json + - application/xml + produces: + - application/xml + - application/json + parameters: + - in: body + name: body + description: Pet object that needs to be added to the store + required: true + schema: + $ref: '#/definitions/Pet' + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + consumes: + - application/json + - application/xml + produces: + - application/xml + - application/json + parameters: + - in: body + name: body + description: Pet object that needs to be added to the store + required: true + schema: + $ref: '#/definitions/Pet' + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + produces: + - application/xml + - application/json + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + collectionFormat: csv + responses: + '200': + description: successful operation + schema: + type: array + items: + $ref: '#/definitions/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: 'Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.' + operationId: findPetsByTags + produces: + - application/xml + - application/json + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + type: array + items: + type: string + collectionFormat: csv + responses: + '200': + description: successful operation + schema: + type: array + items: + $ref: '#/definitions/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + deprecated: true + '/pet/{petId}': + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + produces: + - application/xml + - application/json + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + type: integer + format: int64 + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [] + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: '' + operationId: updatePetWithForm + consumes: + - application/x-www-form-urlencoded + produces: + - application/xml + - application/json + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + type: integer + format: int64 + - name: name + in: formData + description: Updated name of the pet + required: false + type: string + - name: status + in: formData + description: Updated status of the pet + required: false + type: string + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + produces: + - application/xml + - application/json + parameters: + - name: api_key + in: header + required: false + type: string + - name: petId + in: path + description: Pet id to delete + required: true + type: integer + format: int64 + responses: + '400': + description: Invalid pet value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image + description: '' + operationId: uploadFile + consumes: + - multipart/form-data + produces: + - application/json + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + type: integer + format: int64 + - name: additionalMetadata + in: formData + description: Additional data to pass to server + required: false + type: string + - name: file + in: formData + description: file to upload + required: false + type: file + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + produces: + - application/json + parameters: [] + responses: + '200': + description: successful operation + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: '' + operationId: placeOrder + produces: + - application/xml + - application/json + parameters: + - in: body + name: body + description: order placed for purchasing the pet + required: true + schema: + $ref: '#/definitions/Order' + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/Order' + '400': + description: Invalid Order + '/store/order/{orderId}': + get: + tags: + - store + summary: Find purchase order by ID + description: 'For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions' + operationId: getOrderById + produces: + - application/xml + - application/json + parameters: + - name: orderId + in: path + description: ID of pet that needs to be fetched + required: true + type: integer + maximum: 5 + minimum: 1 + format: int64 + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors + operationId: deleteOrder + produces: + - application/xml + - application/json + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + type: string + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + produces: + - application/xml + - application/json + parameters: + - in: body + name: body + description: Created user object + required: true + schema: + $ref: '#/definitions/User' + responses: + default: + description: successful operation + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithArrayInput + produces: + - application/xml + - application/json + parameters: + - in: body + name: body + description: List of user object + required: true + schema: + type: array + items: + $ref: '#/definitions/User' + responses: + default: + description: successful operation + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithListInput + produces: + - application/xml + - application/json + parameters: + - in: body + name: body + description: List of user object + required: true + schema: + type: array + items: + $ref: '#/definitions/User' + responses: + default: + description: successful operation + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + produces: + - application/xml + - application/json + parameters: + - name: username + in: query + description: The user name for login + required: true + type: string + - name: password + in: query + description: The password for login in clear text + required: true + type: string + responses: + '200': + description: successful operation + schema: + type: string + headers: + X-Rate-Limit: + type: integer + format: int32 + description: calls per hour allowed by the user + X-Expires-After: + type: string + format: date-time + description: date in UTC when toekn expires + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: '' + operationId: logoutUser + produces: + - application/xml + - application/json + parameters: [] + responses: + default: + description: successful operation + '/user/{username}': + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + produces: + - application/xml + - application/json + parameters: + - name: username + in: path + description: 'The name that needs to be fetched. Use user1 for testing.' + required: true + type: string + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + produces: + - application/xml + - application/json + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + type: string + - in: body + name: body + description: Updated user object + required: true + schema: + $ref: '#/definitions/User' + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + produces: + - application/xml + - application/json + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found +securityDefinitions: + petstore_auth: + type: oauth2 + authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog' + flow: implicit + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header +definitions: + Order: + title: Pet Order + description: An order for a pets from the pet store + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + title: Pet category + description: A category for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Category + User: + title: a User + description: A User who is purchasing from the pet store + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + title: Pet Tag + description: A tag for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + title: a Pet + description: A pet for sale in the pet store + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/definitions/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/definitions/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + title: An uploaded response + description: Describes the result of uploading an image resource + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + #issue: https://github.com/swagger-api/swagger-codegen/issues/7980 + Amount: + type: object + description: > + some description + properties: + value: + format: double + type: number + minimum: 0.01 + maximum: 1000000000000000 + description: > + some description + currency: + $ref: '#/definitions/Currency' + required: + - value + - currency + Currency: + type: string + pattern: '^[A-Z]{3,3}$' + description: > + some description +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Util/CompressionUtilsTests.cs b/test/WireMock.Net.Tests/Util/CompressionUtilsTests.cs new file mode 100644 index 000000000..09a89c7ee --- /dev/null +++ b/test/WireMock.Net.Tests/Util/CompressionUtilsTests.cs @@ -0,0 +1,62 @@ +using System; +using System.Text; +using FluentAssertions; +using RandomDataGenerator.FieldOptions; +using RandomDataGenerator.Randomizers; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests.Util; + +public class CompressionUtilsTests +{ + [Theory] + [InlineData("gzip")] + [InlineData("deflate")] + public void CompressDecompress_ValidInput_ReturnsOriginalData(string contentEncoding) + { + // Arrange + byte[] data = Encoding.UTF8.GetBytes("Test data for compression"); + + // Act + byte[] compressedData = CompressionUtils.Compress(contentEncoding, data); + byte[] decompressedData = CompressionUtils.Decompress(contentEncoding, compressedData); + + // Assert + decompressedData.Should().BeEquivalentTo(data); + } + + [Theory] + [InlineData("gzip")] + [InlineData("deflate")] + public void Compress_ValidInput_ReturnsCompressedData(string contentEncoding) + { + // Arrange + var text = RandomizerFactory.GetRandomizer(new FieldOptionsTextRegex { Pattern = "[0-9A-Z]{1000}" }).Generate()!; + byte[] data = Encoding.UTF8.GetBytes(text); + + // Act + byte[] compressedData = CompressionUtils.Compress(contentEncoding, data); + + // Assert + compressedData.Length.Should().BeLessThan(data.Length); + } + + [Fact] + public void CompressDecompress_InvalidContentEncoding_ThrowsNotSupportedException() + { + // Arrange + byte[] data = Encoding.UTF8.GetBytes("Test data for compression"); + string contentEncoding = "invalid"; + + // Act + Action compressAction = () => CompressionUtils.Compress(contentEncoding, data); + Action decompressAction = () => CompressionUtils.Decompress(contentEncoding, data); + + // Assert + compressAction.Should().Throw() + .WithMessage($"ContentEncoding '{contentEncoding}' is not supported."); + decompressAction.Should().Throw() + .WithMessage($"ContentEncoding '{contentEncoding}' is not supported."); + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Util/CultureInfoUtilsTests.cs b/test/WireMock.Net.Tests/Util/CultureInfoUtilsTests.cs new file mode 100644 index 000000000..2dc1e0937 --- /dev/null +++ b/test/WireMock.Net.Tests/Util/CultureInfoUtilsTests.cs @@ -0,0 +1,83 @@ +using System; +using System.Globalization; +using FluentAssertions; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests.Util; + +public class CultureInfoUtilsTests +{ + [Theory] + [InlineData(null, typeof(CultureInfo))] + [InlineData("en-US", typeof(CultureInfo))] + [InlineData("fr-FR", typeof(CultureInfo))] + [InlineData("es-ES", typeof(CultureInfo))] + public void Parse_ValidInputs_ReturnsExpectedCultureInfo(string? value, Type expectedType) + { + // Act + var result = CultureInfoUtils.Parse(value); + + // Assert + result.Should().BeOfType(expectedType); + } + + [Theory] + [InlineData("InvalidCulture")] + [InlineData("123456")] + public void Parse_InvalidInputs_ReturnsCurrentCulture(string? value) + { + // Arrange + var expectedCulture = CultureInfo.CurrentCulture; + + // Act + var result = CultureInfoUtils.Parse(value); + + // Assert + result.Should().Be(expectedCulture); + } + +#if !NETSTANDARD1_3 + [Fact] + public void Parse_IntegerInput_ReturnsExpectedCultureInfo() + { + // Arrange + string value = "1033"; // en-US culture identifier + var expectedCulture = new CultureInfo(1033); + + // Act + var result = CultureInfoUtils.Parse(value); + + // Assert + result.Should().Be(expectedCulture); + } +#endif + + [Fact] + public void Parse_CurrentCultureInput_ReturnsCurrentCulture() + { + // Arrange + string value = nameof(CultureInfo.CurrentCulture); + var expectedCulture = CultureInfo.CurrentCulture; + + // Act + var result = CultureInfoUtils.Parse(value); + + // Assert + result.Should().Be(expectedCulture); + } + + [Fact] + public void Parse_InvariantCultureInput_ReturnsInvariantCulture() + { + // Arrange + string value = nameof(CultureInfo.InvariantCulture); + var expectedCulture = CultureInfo.InvariantCulture; + + // Act + var result = CultureInfoUtils.Parse(value); + + // Assert + result.Should().Be(expectedCulture); + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Util/RegexUtilsTests.cs b/test/WireMock.Net.Tests/Util/RegexUtilsTests.cs new file mode 100644 index 000000000..d00da00ed --- /dev/null +++ b/test/WireMock.Net.Tests/Util/RegexUtilsTests.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; +using FluentAssertions; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests.Util; + +public class RegexUtilsTests +{ + [Fact] + public void GetNamedGroups_ValidRegexWithNamedGroups_ReturnsNamedGroupsDictionary() + { + // Arrange + var pattern = @"^(?\w+)\s(?\d+)$"; + var input = "MainStreet 123"; + var regex = new Regex(pattern); + + // Act + var namedGroupsDictionary = RegexUtils.GetNamedGroups(regex, input); + + // Assert + namedGroupsDictionary.Should().NotBeEmpty() + .And.Contain(new KeyValuePair("street", "MainStreet")) + .And.Contain(new KeyValuePair("number", "123")); + } + + [Theory] + [InlineData("", "test", false, false)] + [InlineData(null, "test", false, false)] + [InlineData(".*", "test", true, true)] + [InlineData("invalid[", "test", false, false)] + public void MatchRegex_WithVariousPatterns_ReturnsExpectedResults( + string? pattern, string input, bool expectedIsValid, bool expectedResult) + { + // Act + var (isValidResult, matchResult) = RegexUtils.MatchRegex(pattern, input); + + // Assert + isValidResult.Should().Be(expectedIsValid); + matchResult.Should().Be(expectedResult); + } + + [Theory] + [InlineData("", "test", false, false)] + [InlineData(null, "test", false, false)] + [InlineData(".*", "test", true, true)] + [InlineData("invalid[", "test", false, false)] + public void MatchRegex_WithVariousPatternsAndExtendedRegex_ReturnsExpectedResults( + string? pattern, string input, bool expectedIsValid, bool expectedResult) + { + // Act + var (isValidResult, matchResult) = RegexUtils.MatchRegex(pattern, input, useRegexExtended: true); + + // Assert + isValidResult.Should().Be(expectedIsValid); + matchResult.Should().Be(expectedResult); + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj index 32c944099..006f88685 100644 --- a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj +++ b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj @@ -103,6 +103,12 @@ + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingAsync_WithBodyModelMatcherModel_WithoutMethods_ShouldReturnCorrectMappingModel.verified.txt b/test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingAsync_WithBodyModelMatcherModel_WithoutMethods_ShouldReturnCorrectMappingModel.verified.txt index 8dbeadf19..215fa133a 100644 --- a/test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingAsync_WithBodyModelMatcherModel_WithoutMethods_ShouldReturnCorrectMappingModel.verified.txt +++ b/test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingAsync_WithBodyModelMatcherModel_WithoutMethods_ShouldReturnCorrectMappingModel.verified.txt @@ -21,6 +21,5 @@ }, Response: { Body: world - }, - UseWebhooksFireAndForget: false + } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WireMockAdminApiTests.cs b/test/WireMock.Net.Tests/WireMockAdminApiTests.cs index 55a60812c..8d794a4bf 100644 --- a/test/WireMock.Net.Tests/WireMockAdminApiTests.cs +++ b/test/WireMock.Net.Tests/WireMockAdminApiTests.cs @@ -1,5 +1,6 @@ #if !(NET452 || NET461 || NETCOREAPP3_1) using System; +using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -760,5 +761,81 @@ public async Task IWireMockAdminApi_GetMappingsCode() server.Stop(); } + + [Fact] + public async Task IWireMockAdminApi_OpenApiConvert_Yml() + { + // Arrange + var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore.yml")); + + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Url); + + // Act + var mappings = await api.OpenApiConvertAsync(openApiDocument).ConfigureAwait(false); + + // Assert + server.MappingModels.Should().BeEmpty(); + mappings.Should().HaveCount(20); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_OpenApiConvert_Json() + { + // Arrange + var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore-openapi3.json")); + + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Url); + + // Act + var mappings = await api.OpenApiConvertAsync(openApiDocument).ConfigureAwait(false); + + // Assert + server.MappingModels.Should().BeEmpty(); + mappings.Should().HaveCount(19); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_OpenApiSave_Json() + { + // Arrange + var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore-openapi3.json")); + + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Url); + + // Act + var statusModel = await api.OpenApiSaveAsync(openApiDocument).ConfigureAwait(false); + + // Assert + statusModel.Status.Should().Be("OpenApi document converted to Mappings"); + server.MappingModels.Should().HaveCount(19); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_OpenApiSave_Yml() + { + // Arrange + var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore.yml")); + + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Url); + + // Act + var mappings = await api.OpenApiConvertAsync(openApiDocument).ConfigureAwait(false); + + // Assert + server.MappingModels.Should().BeEmpty(); + mappings.Should().HaveCount(20); + + server.Stop(); + } } #endif \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WireMockServer.Proxy.cs b/test/WireMock.Net.Tests/WireMockServer.Proxy.cs index 7955222b3..25cc876c8 100644 --- a/test/WireMock.Net.Tests/WireMockServer.Proxy.cs +++ b/test/WireMock.Net.Tests/WireMockServer.Proxy.cs @@ -117,7 +117,7 @@ public async Task WireMockServer_Proxy_AdminTrue_With_SaveMapping_Is_True_And_Sa } // Assert - server.Mappings.Should().HaveCount(32); + server.Mappings.Should().HaveCount(34); } [Fact] diff --git a/test/WireMock.Net.Tests/WireMockServer.Settings.cs b/test/WireMock.Net.Tests/WireMockServer.Settings.cs index 83413879e..e0974cb25 100644 --- a/test/WireMock.Net.Tests/WireMockServer.Settings.cs +++ b/test/WireMock.Net.Tests/WireMockServer.Settings.cs @@ -81,7 +81,7 @@ public void WireMockServer_WireMockServerSettings_PriorityFromAllAdminMappingsIs // Assert server.Mappings.Should().NotBeNull(); - server.Mappings.Should().HaveCount(30); + server.Mappings.Should().HaveCount(32); server.Mappings.All(m => m.Priority == WireMockConstants.AdminPriority).Should().BeTrue(); } @@ -100,9 +100,9 @@ public void WireMockServer_WireMockServerSettings_ProxyAndRecordSettings_ProxyPr // Assert server.Mappings.Should().NotBeNull(); - server.Mappings.Should().HaveCount(31); + server.Mappings.Should().HaveCount(33); - server.Mappings.Count(m => m.Priority == WireMockConstants.AdminPriority).Should().Be(30); + server.Mappings.Count(m => m.Priority == WireMockConstants.AdminPriority).Should().Be(32); server.Mappings.Count(m => m.Priority == WireMockConstants.ProxyPriority).Should().Be(1); }