diff --git a/WireMock.Net Solution.sln.DotSettings b/WireMock.Net Solution.sln.DotSettings index c3564a0f..a0261229 100644 --- a/WireMock.Net Solution.sln.DotSettings +++ b/WireMock.Net Solution.sln.DotSettings @@ -14,6 +14,7 @@ PATCH POST PUT + QL RSA SSL TE diff --git a/examples/WireMock.Net.Console.NET6/WireMock.Net.Console.NET6.csproj b/examples/WireMock.Net.Console.NET6/WireMock.Net.Console.NET6.csproj index 32d059a3..8954b1f8 100644 --- a/examples/WireMock.Net.Console.NET6/WireMock.Net.Console.NET6.csproj +++ b/examples/WireMock.Net.Console.NET6/WireMock.Net.Console.NET6.csproj @@ -27,10 +27,8 @@ - - diff --git a/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs b/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs index 37d9f08a..f7c60bfe 100644 --- a/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs +++ b/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs @@ -42,6 +42,36 @@ namespace WireMock.Net.ConsoleApplication public static class MainApp { + private const string TestSchema = @" + input MessageInput { + content: String + author: String + } + + type Message { + id: ID! + content: String + author: String + } + + type Mutation { + createMessage(input: MessageInput): Message + updateMessage(id: ID!, input: MessageInput): Message + } + + type Query { + greeting:String + students:[Student] + studentById(id:ID!):Student + } + + type Student { + id:ID! + firstName:String + lastName:String + fullName:String + }"; + public static void Run() { var mappingBuilder = new MappingBuilder(); @@ -137,6 +167,16 @@ namespace WireMock.Net.ConsoleApplication // server.AllowPartialMapping(); + server + .Given(Request.Create() + .WithPath("/graphql") + .UsingPost() + .WithGraphQLSchema(TestSchema) + ) + .RespondWith(Response.Create() + .WithBody("GraphQL is ok") + ); + // 400 ms server .Given(Request.Create() diff --git a/src/WireMock.Net/Matchers/GraphQLMatcher.cs b/src/WireMock.Net/Matchers/GraphQLMatcher.cs new file mode 100644 index 00000000..92eca1ff --- /dev/null +++ b/src/WireMock.Net/Matchers/GraphQLMatcher.cs @@ -0,0 +1,139 @@ +#if GRAPHQL +using System; +using System.Collections.Generic; +using System.Linq; +using AnyOfTypes; +using GraphQL; +using GraphQL.Types; +using Newtonsoft.Json; +using Stef.Validation; +using WireMock.Models; + +namespace WireMock.Matchers; + +/// +/// GrapQLMatcher Schema Matcher +/// +/// +public class GraphQLMatcher : IStringMatcher +{ + private sealed class GraphQLRequest + { + public string? Query { get; set; } + + public Dictionary? Variables { get; set; } + } + + private readonly AnyOf[] _patterns; + + private readonly ISchema _schema; + + /// + public MatchBehaviour MatchBehaviour { get; } + + /// + public bool ThrowException { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The schema. + /// The match behaviour. + /// Throw an exception when the internal matching fails because of invalid input. + /// The to use. (default = "Or") + public GraphQLMatcher(AnyOf schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, bool throwException = false, MatchOperator matchOperator = MatchOperator.Or) + { + Guard.NotNull(schema); + MatchBehaviour = matchBehaviour; + ThrowException = throwException; + MatchOperator = matchOperator; + + var patterns = new List>(); + switch (schema.CurrentType) + { + case AnyOfType.First: + patterns.Add(schema.First); + _schema = BuildSchema(schema); + break; + + case AnyOfType.Second: + patterns.Add(schema.Second); + _schema = BuildSchema(schema.Second.Pattern); + break; + + case AnyOfType.Third: + _schema = schema.Third; + break; + + default: + throw new NotSupportedException(); + } + _patterns = patterns.ToArray(); + } + + /// + public double IsMatch(string? input) + { + var match = MatchScores.Mismatch; + + try + { + var graphQLRequest = JsonConvert.DeserializeObject(input!)!; + + var executionResult = new DocumentExecuter().ExecuteAsync(_ => + { + _.ThrowOnUnhandledException = true; + + _.Schema = _schema; + _.Query = graphQLRequest.Query; + + if (graphQLRequest.Variables != null) + { + _.Variables = new Inputs(graphQLRequest.Variables); + } + }).GetAwaiter().GetResult(); + + if (executionResult.Errors == null || executionResult.Errors.Count == 0) + { + match = MatchScores.Perfect; + } + else + { + var exceptions = executionResult.Errors.OfType().ToArray(); + if (exceptions.Length == 1) + { + throw exceptions[0]; + } + + throw new AggregateException(exceptions); + } + } + catch + { + if (ThrowException) + { + throw; + } + } + + return MatchBehaviourHelper.Convert(MatchBehaviour, match); + } + + /// + public AnyOf[] GetPatterns() + { + return _patterns; + } + + /// + public MatchOperator MatchOperator { get; } + + /// + public string Name => nameof(GraphQLMatcher); + + private static ISchema BuildSchema(string schema) + { + return Schema.For(schema); + } +} +#endif \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/LinqMatcher.cs b/src/WireMock.Net/Matchers/LinqMatcher.cs index f8069a8d..34577b60 100644 --- a/src/WireMock.Net/Matchers/LinqMatcher.cs +++ b/src/WireMock.Net/Matchers/LinqMatcher.cs @@ -69,7 +69,7 @@ public class LinqMatcher : IObjectMatcher, IStringMatcher MatchOperator = matchOperator; } - /// + /// public double IsMatch(string? input) { double match = MatchScores.Mismatch; @@ -95,7 +95,7 @@ public class LinqMatcher : IObjectMatcher, IStringMatcher return MatchBehaviourHelper.Convert(MatchBehaviour, match); } - /// + /// public double IsMatch(object? input) { double match = MatchScores.Mismatch; @@ -110,41 +110,15 @@ public class LinqMatcher : IObjectMatcher, IStringMatcher jArray = new JArray { JToken.FromObject(input) }; } - //enumerable = jArray.ToDynamicClassArray(); - - //JObject value; - //switch (input) - //{ - // case JObject valueAsJObject: - // value = valueAsJObject; - // break; - - // case { } valueAsObject: - // value = JObject.FromObject(valueAsObject); - // break; - - // default: - // return MatchScores.Mismatch; - //} - // Convert a single object to a Queryable JObject-list with 1 entry. - //var queryable1 = new[] { value }.AsQueryable(); var queryable = jArray.ToDynamicClassArray().AsQueryable(); try { - // Generate the DynamicLinq select statement. - //string dynamicSelect = JsonUtils.GenerateDynamicLinqStatement(value); - - // Execute DynamicLinq Select statement. - //var queryable2 = queryable1.Select(dynamicSelect); - - // Use the Any(...) method to check if the result matches. - var patternsAsStringArray = _patterns.Select(p => p.GetPattern()).ToArray(); var scores = patternsAsStringArray.Select(p => queryable.Any(p)).ToArray(); - match = MatchScores.ToScore(_patterns.Select(pattern => queryable.Any(pattern.GetPattern())).ToArray(), MatchOperator); + match = MatchScores.ToScore(scores, MatchOperator); return MatchBehaviourHelper.Convert(MatchBehaviour, match); } @@ -159,7 +133,7 @@ public class LinqMatcher : IObjectMatcher, IStringMatcher return MatchBehaviourHelper.Convert(MatchBehaviour, match); } - /// + /// public AnyOf[] GetPatterns() { return _patterns; @@ -168,6 +142,6 @@ public class LinqMatcher : IObjectMatcher, IStringMatcher /// public MatchOperator MatchOperator { get; } - /// + /// public string Name => "LinqMatcher"; } \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/Request/RequestMessageGraphQLMatcher.cs b/src/WireMock.Net/Matchers/Request/RequestMessageGraphQLMatcher.cs new file mode 100644 index 00000000..21719ca9 --- /dev/null +++ b/src/WireMock.Net/Matchers/Request/RequestMessageGraphQLMatcher.cs @@ -0,0 +1,104 @@ +using System.Linq; +using Stef.Validation; +using WireMock.Types; + +namespace WireMock.Matchers.Request; + +/// +/// The request body GraphQL matcher. +/// +public class RequestMessageGraphQLMatcher : IRequestMatcher +{ + /// + /// The matchers. + /// + public IMatcher[]? Matchers { get; } + + /// + /// The + /// + public MatchOperator MatchOperator { get; } = MatchOperator.Or; + + /// + /// Initializes a new instance of the class. + /// + /// The match behaviour. + /// The schema. + public RequestMessageGraphQLMatcher(MatchBehaviour matchBehaviour, string schema) : + this(CreateMatcherArray(matchBehaviour, schema)) + { + } + +#if GRAPHQL + /// + /// Initializes a new instance of the class. + /// + /// The match behaviour. + /// The schema. + public RequestMessageGraphQLMatcher(MatchBehaviour matchBehaviour, GraphQL.Types.ISchema schema) : + this(CreateMatcherArray(matchBehaviour, new AnyOfTypes.AnyOf(schema))) + { + } +#endif + /// + /// Initializes a new instance of the class. + /// + /// The matchers. + public RequestMessageGraphQLMatcher(params IMatcher[] matchers) + { + Matchers = Guard.NotNull(matchers); + } + + /// + /// Initializes a new instance of the class. + /// + /// The matchers. + /// The to use. + public RequestMessageGraphQLMatcher(MatchOperator matchOperator, params IMatcher[] matchers) + { + Matchers = Guard.NotNull(matchers); + MatchOperator = matchOperator; + } + + /// + public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) + { + var score = CalculateMatchScore(requestMessage); + return requestMatchResult.AddScore(GetType(), score); + } + + private static double CalculateMatchScore(IRequestMessage requestMessage, IMatcher matcher) + { + // Check if the matcher is a IStringMatcher + // If the body is a Json or a String, use the BodyAsString to match on. + if (matcher is IStringMatcher stringMatcher && requestMessage.BodyData?.DetectedBodyType is BodyType.Json or BodyType.String or BodyType.FormUrlEncoded) + { + return stringMatcher.IsMatch(requestMessage.BodyData.BodyAsString); + } + + return MatchScores.Mismatch; + } + + private double CalculateMatchScore(IRequestMessage requestMessage) + { + if (Matchers == null) + { + return MatchScores.Mismatch; + } + + var matchersResult = Matchers.Select(matcher => CalculateMatchScore(requestMessage, matcher)).ToArray(); + return MatchScores.ToScore(matchersResult, MatchOperator); + } + +#if GRAPHQL + private static IMatcher[] CreateMatcherArray(MatchBehaviour matchBehaviour, AnyOfTypes.AnyOf schema) + { + return new[] { new GraphQLMatcher(schema, matchBehaviour) }.Cast().ToArray(); + } +#else + private static IMatcher[] CreateMatcherArray(MatchBehaviour matchBehaviour, object schema) + { + throw new System.NotSupportedException("The GrapQLMatcher can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower."); + } +#endif +} \ No newline at end of file diff --git a/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs index a6459c45..ae91756a 100644 --- a/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs +++ b/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs @@ -10,7 +10,7 @@ namespace WireMock.RequestBuilders; /// /// The BodyRequestBuilder interface. /// -public interface IBodyRequestBuilder : IRequestMatcher +public interface IBodyRequestBuilder : IGraphQLRequestBuilder { /// /// WithBody: IMatcher @@ -103,4 +103,12 @@ public interface IBodyRequestBuilder : IRequestMatcher /// The form-urlencoded values. /// The . IRequestBuilder WithBody(Func?, bool> func); + + /// + /// WithBodyAsGraphQLSchema: Body as GraphQL schema as a string. + /// + /// The GraphQL schema. + /// The match behaviour. (Default is MatchBehaviour.AcceptOnMatch). + /// The . + IRequestBuilder WithBodyAsGraphQLSchema(string body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch); } \ No newline at end of file diff --git a/src/WireMock.Net/RequestBuilders/IGraphQLRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IGraphQLRequestBuilder.cs new file mode 100644 index 00000000..a90b78ae --- /dev/null +++ b/src/WireMock.Net/RequestBuilders/IGraphQLRequestBuilder.cs @@ -0,0 +1,28 @@ +using WireMock.Matchers; +using WireMock.Matchers.Request; + +namespace WireMock.RequestBuilders; + +/// +/// The GraphQLRequestBuilder interface. +/// +public interface IGraphQLRequestBuilder : IRequestMatcher +{ + /// + /// WithGraphQLSchema: The GraphQL schema as a string. + /// + /// The GraphQL schema. + /// The match behaviour. (Default is MatchBehaviour.AcceptOnMatch). + /// The . + IRequestBuilder WithGraphQLSchema(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch); + +#if GRAPHQL + /// + /// WithGraphQLSchema: The GraphQL schema as a ISchema. + /// + /// The GraphQL schema. + /// The match behaviour. (Default is MatchBehaviour.AcceptOnMatch). + /// The . + IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch); +#endif +} \ No newline at end of file diff --git a/src/WireMock.Net/RequestBuilders/Request.WithBody.cs b/src/WireMock.Net/RequestBuilders/Request.WithBody.cs index 08ffa7b3..884e4a1d 100644 --- a/src/WireMock.Net/RequestBuilders/Request.WithBody.cs +++ b/src/WireMock.Net/RequestBuilders/Request.WithBody.cs @@ -109,4 +109,10 @@ public partial class Request _requestMatchers.Add(new RequestMessageBodyMatcher(Guard.NotNull(func))); return this; } + + /// + public IRequestBuilder WithBodyAsGraphQLSchema(string body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + return WithGraphQLSchema(body, matchBehaviour); + } } \ No newline at end of file diff --git a/src/WireMock.Net/RequestBuilders/Request.WithGraphQL.cs b/src/WireMock.Net/RequestBuilders/Request.WithGraphQL.cs new file mode 100644 index 00000000..15a912d8 --- /dev/null +++ b/src/WireMock.Net/RequestBuilders/Request.WithGraphQL.cs @@ -0,0 +1,23 @@ +using WireMock.Matchers; +using WireMock.Matchers.Request; + +namespace WireMock.RequestBuilders; + +public partial class Request +{ + /// + public IRequestBuilder WithGraphQLSchema(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + _requestMatchers.Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema)); + return this; + } + +#if GRAPHQL + /// + public IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + _requestMatchers.Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema)); + return this; + } +#endif +} \ No newline at end of file diff --git a/src/WireMock.Net/Serialization/MappingConverter.cs b/src/WireMock.Net/Serialization/MappingConverter.cs index ff40cbd2..95a346ee 100644 --- a/src/WireMock.Net/Serialization/MappingConverter.cs +++ b/src/WireMock.Net/Serialization/MappingConverter.cs @@ -46,7 +46,8 @@ internal class MappingConverter var cookieMatchers = request.GetRequestMessageMatchers(); var paramsMatchers = request.GetRequestMessageMatchers(); var methodMatcher = request.GetRequestMessageMatcher(); - var bodyMatcher = request.GetRequestMessageMatcher(); + var requestMessageBodyMatcher = request.GetRequestMessageMatcher(); + var requestMessageGraphQLMatcher = request.GetRequestMessageMatcher(); var sb = new StringBuilder(); @@ -105,13 +106,23 @@ internal class MappingConverter sb.AppendLine($" .WithCookie(\"{cookieMatcher.Name}\", {ToValueArguments(GetStringArray(cookieMatcher.Matchers!))}, true)"); } - if (bodyMatcher is { Matchers: { } }) +#if GRAPHQL + if (requestMessageGraphQLMatcher is { Matchers: { } }) { - if (bodyMatcher.Matchers.OfType().FirstOrDefault() is { } wildcardMatcher && wildcardMatcher.GetPatterns().Any()) + if (requestMessageGraphQLMatcher.Matchers.OfType().FirstOrDefault() is { } graphQLMatcher && graphQLMatcher.GetPatterns().Any()) + { + sb.AppendLine($" .WithGraphQLSchema({GetString(graphQLMatcher)})"); + } + } + else +#endif + if (requestMessageBodyMatcher is { Matchers: { } }) + { + if (requestMessageBodyMatcher.Matchers.OfType().FirstOrDefault() is { } wildcardMatcher && wildcardMatcher.GetPatterns().Any()) { sb.AppendLine($" .WithBody({GetString(wildcardMatcher)})"); } - else if (bodyMatcher.Matchers.OfType().FirstOrDefault() is { Value: { } } jsonPartialMatcher) + else if (requestMessageBodyMatcher.Matchers.OfType().FirstOrDefault() is { Value: { } } jsonPartialMatcher) { sb.AppendLine(@$" .WithBody(new JsonPartialMatcher( value: {ToCSharpStringLiteral(jsonPartialMatcher.Value.ToString())}, @@ -120,7 +131,7 @@ internal class MappingConverter regex: {ToCSharpBooleanLiteral(jsonPartialMatcher.Regex)} ))"); } - else if (bodyMatcher.Matchers.OfType().FirstOrDefault() is { Value: { } } jsonPartialWildcardMatcher) + else if (requestMessageBodyMatcher.Matchers.OfType().FirstOrDefault() is { Value: { } } jsonPartialWildcardMatcher) { sb.AppendLine(@$" .WithBody(new JsonPartialWildcardMatcher( value: {ToCSharpStringLiteral(jsonPartialWildcardMatcher.Value.ToString())}, @@ -216,6 +227,7 @@ internal class MappingConverter var paramsMatchers = request.GetRequestMessageMatchers(); var methodMatcher = request.GetRequestMessageMatcher(); var bodyMatcher = request.GetRequestMessageMatcher(); + var graphQLMatcher = request.GetRequestMessageMatcher(); var mappingModel = new MappingModel { @@ -301,7 +313,7 @@ internal class MappingConverter mappingModel.Response.Delay = (int?)(response.Delay == Timeout.InfiniteTimeSpan ? TimeSpan.MaxValue.TotalMilliseconds : response.Delay?.TotalMilliseconds); } - var nonNullableWebHooks = mapping.Webhooks?.Where(wh => wh != null).ToArray() ?? new IWebhook[0]; + var nonNullableWebHooks = mapping.Webhooks?.Where(wh => wh != null).ToArray() ?? EmptyArray.Value; if (nonNullableWebHooks.Length == 1) { mappingModel.Webhook = WebhookMapper.Map(nonNullableWebHooks[0]); @@ -311,18 +323,20 @@ internal class MappingConverter mappingModel.Webhooks = mapping.Webhooks.Select(WebhookMapper.Map).ToArray(); } - if (bodyMatcher?.Matchers != null) + var graphQLOrBodyMatchers = graphQLMatcher?.Matchers ?? bodyMatcher?.Matchers; + var matchOperator = graphQLMatcher?.MatchOperator ?? bodyMatcher?.MatchOperator; + if (graphQLOrBodyMatchers != null && matchOperator != null) { mappingModel.Request.Body = new BodyModel(); - if (bodyMatcher.Matchers.Length == 1) + if (graphQLOrBodyMatchers.Length == 1) { - mappingModel.Request.Body.Matcher = _mapper.Map(bodyMatcher.Matchers[0]); + mappingModel.Request.Body.Matcher = _mapper.Map(graphQLOrBodyMatchers[0]); } - else if (bodyMatcher.Matchers.Length > 1) + else if (graphQLOrBodyMatchers.Length > 1) { - mappingModel.Request.Body.Matchers = _mapper.Map(bodyMatcher.Matchers); - mappingModel.Request.Body.MatchOperator = bodyMatcher.MatchOperator.ToString(); + mappingModel.Request.Body.Matchers = _mapper.Map(graphQLOrBodyMatchers); + mappingModel.Request.Body.MatchOperator = matchOperator.ToString(); } } diff --git a/src/WireMock.Net/Serialization/MatcherMapper.cs b/src/WireMock.Net/Serialization/MatcherMapper.cs index df18842e..ffd2cb44 100644 --- a/src/WireMock.Net/Serialization/MatcherMapper.cs +++ b/src/WireMock.Net/Serialization/MatcherMapper.cs @@ -71,7 +71,10 @@ internal class MatcherMapper case nameof(ExactObjectMatcher): return CreateExactObjectMatcher(matchBehaviour, stringPatterns[0], throwExceptionWhenMatcherFails); - +#if GRAPHQL + case nameof(GraphQLMatcher): + return new GraphQLMatcher(stringPatterns[0].GetPattern(), matchBehaviour, throwExceptionWhenMatcherFails, matchOperator); +#endif case nameof(RegexMatcher): return new RegexMatcher(matchBehaviour, stringPatterns, ignoreCase, throwExceptionWhenMatcherFails, useRegexExtended, matchOperator); diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index 2759924e..527b00e6 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -50,16 +50,19 @@ $(DefineConstants);OPENAPIPARSER + + $(DefineConstants);GRAPHQL + + - - + @@ -154,6 +157,11 @@ + + + + + all diff --git a/test/WireMock.Net.Tests/Matchers/GraphQLMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/GraphQLMatcherTests.cs new file mode 100644 index 00000000..c83b6249 --- /dev/null +++ b/test/WireMock.Net.Tests/Matchers/GraphQLMatcherTests.cs @@ -0,0 +1,142 @@ +#if GRAPHQL +using System; +using FluentAssertions; +using GraphQLParser.Exceptions; +using WireMock.Matchers; +using WireMock.Models; +using Xunit; + +namespace WireMock.Net.Tests.Matchers; + +public class GraphQLMatcherTests +{ + private const string TestSchema = @" + input MessageInput { + content: String + author: String + } + + type Message { + id: ID! + content: String + author: String + } + + type Mutation { + createMessage(input: MessageInput): Message + updateMessage(id: ID!, input: MessageInput): Message + } + + type Query { + greeting:String + students:[Student] + studentById(id:ID!):Student + } + + type Student { + id:ID! + firstName:String + lastName:String + fullName:String + }"; + + [Fact] + public void GraphQLMatcher_For_ValidSchema_And_CorrectQuery_IsMatch() + { + // Arrange + var input = "{\"query\":\"{\\r\\n students {\\r\\n fullName\\r\\n id\\r\\n }\\r\\n}\"}"; + + // Act + var matcher = new GraphQLMatcher(TestSchema); + var result = matcher.IsMatch(input); + + // Assert + result.Should().Be(MatchScores.Perfect); + + matcher.GetPatterns().Should().Contain(TestSchema); + } + + [Fact] + public void GraphQLMatcher_For_ValidSchema_And_CorrectGraphQLQuery_IsMatch() + { + // Arrange + var input = @"{ + ""query"": ""query ($sid: ID!)\r\n{\r\n studentById(id: $sid) {\r\n fullName\r\n id\r\n }\r\n}"", + ""variables"": { + ""sid"": ""1"" + } +}"; + // Act + var matcher = new GraphQLMatcher(TestSchema); + var result = matcher.IsMatch(input); + + // Assert + result.Should().Be(MatchScores.Perfect); + + matcher.GetPatterns().Should().Contain(TestSchema); + } + + [Fact] + public void GraphQLMatcher_For_ValidSchema_And_IncorrectQuery_IsMismatch() + { + // Arrange + var input = @" +{ + students { + fullName + id + abc + } +}"; + // Act + var matcher = new GraphQLMatcher(TestSchema); + var result = matcher.IsMatch(input); + + // Assert + result.Should().Be(MatchScores.Mismatch); + } + + [Fact] + public void GraphQLMatcher_For_ValidSchemaAsStringPattern_And_CorrectQuery_IsMatch() + { + // Arrange + var input = "{\"query\":\"{\\r\\n students {\\r\\n fullName\\r\\n id\\r\\n }\\r\\n}\"}"; + var schema = new StringPattern + { + Pattern = TestSchema + }; + + // Act + var matcher = new GraphQLMatcher(schema); + var result = matcher.IsMatch(input); + + // Assert + result.Should().Be(MatchScores.Perfect); + } + + [Fact] + public void GraphQLMatcher_For_ValidSchema_And_IncorrectQueryWithError_WithThrowExceptionTrue_ThrowsException() + { + // Arrange + var input = "{\"query\":\"{\\r\\n studentsX {\\r\\n fullName\\r\\n X\\r\\n }\\r\\n}\"}"; + + // Act + var matcher = new GraphQLMatcher(TestSchema, MatchBehaviour.AcceptOnMatch, true); + Action action = () => matcher.IsMatch(input); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void GraphQLMatcher_For_InvalidSchema_ThrowsGraphQLSyntaxErrorException() + { + // Act + // ReSharper disable once ObjectCreationAsStatement + Action action = () => _ = new GraphQLMatcher("in va lid"); + + // Assert + action.Should().Throw(); + } +} +#endif \ No newline at end of file diff --git a/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithGraphQLSchemaTests.cs b/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithGraphQLSchemaTests.cs new file mode 100644 index 00000000..81ed5ef6 --- /dev/null +++ b/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithGraphQLSchemaTests.cs @@ -0,0 +1,71 @@ +#if GRAPHQL +using System.Collections.Generic; +using FluentAssertions; +using GraphQL.Types; +using WireMock.Matchers; +using WireMock.Matchers.Request; +using WireMock.RequestBuilders; +using Xunit; + +namespace WireMock.Net.Tests.RequestBuilders; + +public class RequestBuilderWithGraphQLSchemaTests +{ + private const string TestSchema = @" + input MessageInput { + content: String + author: String + } + + type Message { + id: ID! + content: String + author: String + } + + type Mutation { + createMessage(input: MessageInput): Message + updateMessage(id: ID!, input: MessageInput): Message + } + + type Query { + greeting:String + students:[Student] + studentById(id:ID!):Student + } + + type Student { + id:ID! + firstName:String + lastName:String + fullName:String + }"; + + [Fact] + public void RequestBuilder_WithGraphQLSchema_SchemaAsString() + { + // Act + var requestBuilder = (Request)Request.Create().WithGraphQLSchema(TestSchema); + + // Assert + var matchers = requestBuilder.GetPrivateFieldValue>("_requestMatchers"); + matchers.Should().HaveCount(1); + ((RequestMessageGraphQLMatcher)matchers[0]).Matchers.Should().ContainItemsAssignableTo(); + } + + [Fact] + public void RequestBuilder_WithGraphQLSchema_SchemaAsISchema() + { + // Arrange + var schema = Schema.For(TestSchema); + + // Act + var requestBuilder = (Request)Request.Create().WithGraphQLSchema(schema); + + // Assert + var matchers = requestBuilder.GetPrivateFieldValue>("_requestMatchers"); + matchers.Should().HaveCount(1); + ((RequestMessageGraphQLMatcher)matchers[0]).Matchers.Should().ContainItemsAssignableTo(); + } +} +#endif \ No newline at end of file diff --git a/test/WireMock.Net.Tests/RequestMatchers/RequestMessageGraphQLMatcherTests.cs b/test/WireMock.Net.Tests/RequestMatchers/RequestMessageGraphQLMatcherTests.cs new file mode 100644 index 00000000..dbddf918 --- /dev/null +++ b/test/WireMock.Net.Tests/RequestMatchers/RequestMessageGraphQLMatcherTests.cs @@ -0,0 +1,194 @@ +#if GRAPHQL +using System.Linq; +using FluentAssertions; +using Moq; +using WireMock.Matchers; +using WireMock.Matchers.Request; +using WireMock.Models; +using WireMock.Types; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests.RequestMatchers; + +public class RequestMessageGraphQLMatcherTests +{ + [Fact] + public void RequestMessageGraphQLMatcher_GetMatchingScore_BodyAsString_IStringMatcher() + { + // Assign + var body = new BodyData + { + BodyAsString = "b", + DetectedBodyType = BodyType.String + }; + var stringMatcherMock = new Mock(); + stringMatcherMock.Setup(m => m.IsMatch(It.IsAny())).Returns(1d); + + var requestMessage = new RequestMessage(new UrlDetails("http://localhost"), "GET", "127.0.0.1", body); + + var matcher = new RequestMessageGraphQLMatcher(stringMatcherMock.Object); + + // Act + var result = new RequestMatchResult(); + double score = matcher.GetMatchingScore(requestMessage, result); + + // Assert + score.Should().Be(MatchScores.Perfect); + + // Verify + stringMatcherMock.Verify(m => m.GetPatterns(), Times.Never); + stringMatcherMock.Verify(m => m.IsMatch("b"), Times.Once); + } + + [Theory] + [InlineData(1d, 1d, 1d)] + [InlineData(0d, 1d, 1d)] + [InlineData(1d, 0d, 1d)] + [InlineData(0d, 0d, 0d)] + public void RequestMessageGraphQLMatcher_GetMatchingScore_BodyAsString_IStringMatchers_Or(double one, double two, double expected) + { + // Assign + var body = new BodyData + { + BodyAsString = "b", + DetectedBodyType = BodyType.String + }; + var stringMatcherMock1 = new Mock(); + stringMatcherMock1.Setup(m => m.IsMatch(It.IsAny())).Returns(one); + + var stringMatcherMock2 = new Mock(); + stringMatcherMock2.Setup(m => m.IsMatch(It.IsAny())).Returns(two); + + var matchers = new[] { stringMatcherMock1.Object, stringMatcherMock2.Object }; + + var requestMessage = new RequestMessage(new UrlDetails("http://localhost"), "GET", "127.0.0.1", body); + + var matcher = new RequestMessageGraphQLMatcher(MatchOperator.Or, matchers.Cast().ToArray()); + + // Act + var result = new RequestMatchResult(); + double score = matcher.GetMatchingScore(requestMessage, result); + + // Assert + score.Should().Be(expected); + + // Verify + stringMatcherMock1.Verify(m => m.GetPatterns(), Times.Never); + stringMatcherMock1.Verify(m => m.IsMatch("b"), Times.Once); + + stringMatcherMock2.Verify(m => m.GetPatterns(), Times.Never); + stringMatcherMock2.Verify(m => m.IsMatch("b"), Times.Once); + stringMatcherMock2.VerifyNoOtherCalls(); + } + + [Theory] + [InlineData(1d, 1d, 1d)] + [InlineData(0d, 1d, 0d)] + [InlineData(1d, 0d, 0d)] + [InlineData(0d, 0d, 0d)] + public void RequestMessageGraphQLMatcher_GetMatchingScore_BodyAsString_IStringMatchers_And(double one, double two, double expected) + { + // Assign + var body = new BodyData + { + BodyAsString = "b", + DetectedBodyType = BodyType.String + }; + var stringMatcherMock1 = new Mock(); + stringMatcherMock1.Setup(m => m.IsMatch(It.IsAny())).Returns(one); + + var stringMatcherMock2 = new Mock(); + stringMatcherMock2.Setup(m => m.IsMatch(It.IsAny())).Returns(two); + + var matchers = new[] { stringMatcherMock1.Object, stringMatcherMock2.Object }; + + var requestMessage = new RequestMessage(new UrlDetails("http://localhost"), "GET", "127.0.0.1", body); + + var matcher = new RequestMessageGraphQLMatcher(MatchOperator.And, matchers.Cast().ToArray()); + + // Act + var result = new RequestMatchResult(); + double score = matcher.GetMatchingScore(requestMessage, result); + + // Assert + score.Should().Be(expected); + + // Verify + stringMatcherMock1.Verify(m => m.GetPatterns(), Times.Never); + stringMatcherMock1.Verify(m => m.IsMatch("b"), Times.Once); + + stringMatcherMock2.Verify(m => m.GetPatterns(), Times.Never); + stringMatcherMock2.Verify(m => m.IsMatch("b"), Times.Once); + stringMatcherMock2.VerifyNoOtherCalls(); + } + + [Theory] + [InlineData(1d, 1d, 1d)] + [InlineData(0d, 1d, 0.5d)] + [InlineData(1d, 0d, 0.5d)] + [InlineData(0d, 0d, 0d)] + public void RequestMessageGraphQLMatcher_GetMatchingScore_BodyAsString_IStringMatchers_Average(double one, double two, double expected) + { + // Assign + var body = new BodyData + { + BodyAsString = "b", + DetectedBodyType = BodyType.String + }; + var stringMatcherMock1 = new Mock(); + stringMatcherMock1.Setup(m => m.IsMatch(It.IsAny())).Returns(one); + + var stringMatcherMock2 = new Mock(); + stringMatcherMock2.Setup(m => m.IsMatch(It.IsAny())).Returns(two); + + var matchers = new[] { stringMatcherMock1.Object, stringMatcherMock2.Object }; + + var requestMessage = new RequestMessage(new UrlDetails("http://localhost"), "GET", "127.0.0.1", body); + + var matcher = new RequestMessageGraphQLMatcher(MatchOperator.Average, matchers.Cast().ToArray()); + + // Act + var result = new RequestMatchResult(); + double score = matcher.GetMatchingScore(requestMessage, result); + + // Assert + score.Should().Be(expected); + + // Verify + stringMatcherMock1.Verify(m => m.GetPatterns(), Times.Never); + stringMatcherMock1.Verify(m => m.IsMatch("b"), Times.Once); + + stringMatcherMock2.Verify(m => m.GetPatterns(), Times.Never); + stringMatcherMock2.Verify(m => m.IsMatch("b"), Times.Once); + } + + [Fact] + public void RequestMessageGraphQLMatcher_GetMatchingScore_BodyAsBytes_IStringMatcher_ReturnMisMatch() + { + // Assign + var body = new BodyData + { + BodyAsBytes = new byte[] { 1 }, + DetectedBodyType = BodyType.Bytes + }; + var stringMatcherMock = new Mock(); + stringMatcherMock.Setup(m => m.IsMatch(It.IsAny())).Returns(0.5d); + + var requestMessage = new RequestMessage(new UrlDetails("http://localhost"), "GET", "127.0.0.1", body); + + var matcher = new RequestMessageGraphQLMatcher(stringMatcherMock.Object); + + // Act + var result = new RequestMatchResult(); + double score = matcher.GetMatchingScore(requestMessage, result); + + // Assert + score.Should().Be(MatchScores.Mismatch); + + // Verify + stringMatcherMock.Verify(m => m.GetPatterns(), Times.Never); + stringMatcherMock.Verify(m => m.IsMatch(It.IsAny()), Times.Never); + } +} +#endif \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsGraphQLSchema_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsGraphQLSchema_ReturnsCorrectModel.verified.txt new file mode 100644 index 00000000..a0c07a93 --- /dev/null +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsGraphQLSchema_ReturnsCorrectModel.verified.txt @@ -0,0 +1,29 @@ +{ + Guid: Guid_1, + UpdatedAt: DateTime_1, + Title: , + Description: , + Priority: 42, + Request: { + Body: { + Matcher: { + Name: GraphQLMatcher, + Pattern: + type Query { + greeting:String + students:[Student] + studentById(id:ID!):Student + } + + type Student { + id:ID! + firstName:String + lastName:String + fullName:String + } + } + } + }, + Response: {}, + UseWebhooksFireAndForget: false +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithClientIP_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithClientIP_ReturnsCorrectModel.verified.txt new file mode 100644 index 00000000..f3b5f431 --- /dev/null +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithClientIP_ReturnsCorrectModel.verified.txt @@ -0,0 +1,20 @@ +{ + Guid: Guid_1, + UpdatedAt: DateTime_1, + Title: , + Description: , + Priority: 42, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: 1.2.3.4, + IgnoreCase: false + } + ] + } + }, + Response: {}, + UseWebhooksFireAndForget: false +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_WithDelayAsMilleSeconds_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_WithDelayAsMilleSeconds_ReturnsCorrectModel.verified.txt deleted file mode 100644 index 5f282702..00000000 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_WithDelayAsMilleSeconds_ReturnsCorrectModel.verified.txt +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_WithDelayAsMilliSeconds_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_WithDelay_ReturnsCorrectModel.verified.txt similarity index 100% rename from test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_WithDelayAsMilliSeconds_ReturnsCorrectModel.verified.txt rename to test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_WithDelay_ReturnsCorrectModel.verified.txt diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs index 4e0f47a7..3e648f82 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs @@ -260,7 +260,7 @@ public partial class MappingConverterTests } [Fact] - public Task ToMappingModel_WithDelayAsMilliSeconds_ReturnsCorrectModel() + public Task ToMappingModel_WithDelay_ReturnsCorrectModel() { // Assign var delay = 1000; @@ -343,5 +343,56 @@ public partial class MappingConverterTests // Verify return Verifier.Verify(model); } + + [Fact] + public Task ToMappingModel_Request_WithClientIP_ReturnsCorrectModel() + { + // Arrange + var request = Request.Create().WithClientIP("1.2.3.4"); + var response = Response.Create(); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, null, null); + + // Act + var model = _sut.ToMappingModel(mapping); + + // Assert + model.Should().NotBeNull(); + + // Verify + return Verifier.Verify(model); + } + +#if GRAPHQL + [Fact] + public Task ToMappingModel_Request_WithBodyAsGraphQLSchema_ReturnsCorrectModel() + { + // Arrange + var schema = @" + type Query { + greeting:String + students:[Student] + studentById(id:ID!):Student + } + + type Student { + id:ID! + firstName:String + lastName:String + fullName:String + }"; + var request = Request.Create().WithBodyAsGraphQLSchema(schema); + var response = Response.Create(); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, null, null); + + // Act + var model = _sut.ToMappingModel(mapping); + + // Assert + model.Should().NotBeNull(); + + // Verify + return Verifier.Verify(model); + } +#endif } #endif \ 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 be56db36..833e2419 100644 --- a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj +++ b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj @@ -24,6 +24,10 @@ NETFRAMEWORK + + $(DefineConstants);GRAPHQL + + @@ -65,7 +69,6 @@ -