Skip to content

Commit

Permalink
feat: add array contains matcher
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxCampman committed Sep 8, 2024
1 parent 8f98cb5 commit 1915b96
Show file tree
Hide file tree
Showing 12 changed files with 556 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,4 @@ dist/
samples/ReadMe/Provider.Tests/Properties/launchSettings.json
samples/ReadMe/Provider/Properties/launchSettings.json

.idea/
.idea/
52 changes: 52 additions & 0 deletions samples/OrdersApi/Consumer.Tests/OrdersClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,58 @@ await this.pact.VerifyAsync(async ctx =>
});
}

[Fact]
public async Task GetOrdersAsync_WhenCalled_ReturnsMultipleOrders()
{
var expected1 = new OrderDto(1, OrderStatus.Pending, new DateTimeOffset(2023, 6, 28, 12, 13, 14, TimeSpan.FromHours(1)));
var expected2 = new OrderDto(2, OrderStatus.Pending, new DateTimeOffset(2023, 6, 29, 12, 13, 14, TimeSpan.FromHours(1)));

this.pact
.UponReceiving("a request for multiple orders by id")
.Given("orders with ids {ids} exist", new Dictionary<string, string> { ["ids"] = "1,2" })
.WithRequest(HttpMethod.Get, "/api/orders/many/1,2")
.WithHeader("Accept", "application/json")
.WillRespond()
.WithStatus(HttpStatusCode.OK)
.WithJsonBody(Match.ArrayContains(new dynamic[]
{
new
{
Id = Match.Integer(expected1.Id),
Status = Match.Regex(expected1.Status.ToString(), string.Join("|", Enum.GetNames<OrderStatus>())),
Date = Match.Type(expected1.Date.ToString("O"))
},
new
{
Id = Match.Integer(expected2.Id),
Status = Match.Regex(expected2.Status.ToString(), string.Join("|", Enum.GetNames<OrderStatus>())),
Date = Match.Type(expected2.Date.ToString("O"))
},
}));

await this.pact.VerifyAsync(async ctx =>
{
this.mockFactory
.Setup(f => f.CreateClient("Orders"))
.Returns(() => new HttpClient
{
BaseAddress = ctx.MockServerUri,
DefaultRequestHeaders =
{
Accept = { MediaTypeWithQualityHeaderValue.Parse("application/json") }
}
});
var client = new OrdersClient(this.mockFactory.Object);
OrderDto[] orders = await client.GetOrdersAsync(new[] { 1, 2 });
orders.Should().HaveCount(2);
orders[0].Should().Be(expected1);
orders[1].Should().Be(expected2);
});
}

[Fact]
public async Task GetOrderAsync_UnknownOrder_ReturnsNotFound()
{
Expand Down
120 changes: 120 additions & 0 deletions samples/OrdersApi/Consumer.Tests/pacts/Fulfilment API-Orders API.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,126 @@
},
"type": "Synchronous/HTTP"
},
{
"description": "a request for multiple orders by id",
"pending": false,
"providerStates": [
{
"name": "orders with ids {ids} exist",
"params": {
"ids": "1,2"
}
}
],
"request": {
"headers": {
"Accept": [
"application/json"
]
},
"method": "GET",
"path": "/api/orders/many/1,2"
},
"response": {
"body": {
"content": [
{
"date": "2023-06-28T12:13:14.0000000+01:00",
"id": 1,
"status": "Pending"
},
{
"date": "2023-06-29T12:13:14.0000000+01:00",
"id": 2,
"status": "Pending"
}
],
"contentType": "application/json",
"encoded": false
},
"headers": {
"Content-Type": [
"application/json"
]
},
"matchingRules": {
"body": {
"$": {
"combine": "AND",
"matchers": [
{
"match": "arrayContains",
"variants": [
{
"index": 0,
"rules": {
"$.date": {
"combine": "AND",
"matchers": [
{
"match": "type"
}
]
},
"$.id": {
"combine": "AND",
"matchers": [
{
"match": "integer"
}
]
},
"$.status": {
"combine": "AND",
"matchers": [
{
"match": "regex",
"regex": "Pending|Fulfilling|Shipped"
}
]
}
}
},
{
"index": 1,
"rules": {
"$.date": {
"combine": "AND",
"matchers": [
{
"match": "type"
}
]
},
"$.id": {
"combine": "AND",
"matchers": [
{
"match": "integer"
}
]
},
"$.status": {
"combine": "AND",
"matchers": [
{
"match": "regex",
"regex": "Pending|Fulfilling|Shipped"
}
]
}
}
}
]
}
]
}
}
},
"status": 200
},
"type": "Synchronous/HTTP"
},
{
"description": "a request to update the status of an order",
"pending": false,
Expand Down
16 changes: 15 additions & 1 deletion samples/OrdersApi/Consumer/OrdersClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net.Http;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -40,6 +41,19 @@ public async Task<OrderDto> GetOrderAsync(int orderId)
return order;
}

/// <summary>
/// Get a orders by ID
/// </summary>
/// <param name="orderIds">Order IDs</param>
/// <returns>Order</returns>
public async Task<OrderDto[]> GetOrdersAsync(IEnumerable<int> orderIds)
{
using HttpClient client = this.factory.CreateClient("Orders");

OrderDto[] orders = await client.GetFromJsonAsync<OrderDto[]>($"/api/orders/many/{string.Join(',', orderIds)}", Options);
return orders;
}

/// <summary>
/// Update the status of an order
/// </summary>
Expand Down
15 changes: 13 additions & 2 deletions samples/OrdersApi/Provider.Tests/ProviderStateMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
Expand Down Expand Up @@ -36,7 +37,8 @@ public ProviderStateMiddleware(RequestDelegate next, IOrderRepository orders)

this.providerStates = new Dictionary<string, Func<IDictionary<string, object>, Task>>
{
["an order with ID {id} exists"] = this.EnsureEventExistsAsync
["an order with ID {id} exists"] = this.EnsureEventExistsAsync,
["orders with ids {ids} exist"] = this.EnsureEventsExistAsync
};
}

Expand All @@ -52,6 +54,15 @@ private async Task EnsureEventExistsAsync(IDictionary<string, object> parameters
await this.orders.InsertAsync(new OrderDto(id.GetInt32(), OrderStatus.Fulfilling, DateTimeOffset.Now));
}

private async Task EnsureEventsExistAsync(IDictionary<string, object> parameters)
{
var ids = (JsonElement)parameters["ids"];
foreach (var id in ids.GetString()!.Split(',').Select(int.Parse))
{
await this.orders.InsertAsync(new OrderDto(id, OrderStatus.Fulfilling, DateTimeOffset.Now));
}
}

/// <summary>
/// Handle the request
/// </summary>
Expand Down Expand Up @@ -79,7 +90,7 @@ public async Task InvokeAsync(HttpContext context)
try
{
ProviderState providerState = JsonSerializer.Deserialize<ProviderState>(jsonRequestBody, Options);

if (!string.IsNullOrEmpty(providerState?.State))
{
await this.providerStates[providerState.State].Invoke(providerState.Params);
Expand Down
24 changes: 24 additions & 0 deletions samples/OrdersApi/Provider/Orders/OrdersController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -46,6 +47,29 @@ public async Task<IActionResult> GetByIdAsync(int id)
}
}

[HttpGet("many/{ids}", Name = "getMany")]
[ProducesResponseType(typeof(OrderDto[]), StatusCodes.Status200OK)]
public async Task<IActionResult> GetManyAsync(string ids)
{
try
{
var idsAsInts = ids.Split(',').Select(int.Parse);

List<OrderDto> result = new List<OrderDto>();
foreach (int id in idsAsInts)
{
var order = await this.orders.GetAsync(id);
result.Add(order);
}

return this.Ok(result.ToArray());
}
catch (KeyNotFoundException)
{
return this.NotFound();
}
}

/// <summary>
/// Create a new pending order
/// </summary>
Expand Down
24 changes: 24 additions & 0 deletions src/PactNet.Abstractions/Matchers/ArrayContainsMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;

namespace PactNet.Matchers
{
public class ArrayContainsMatcher : IMatcher
{
/// <summary>
/// Type of the matcher
/// </summary>
[JsonPropertyName("pact:matcher:type")]
public string Type => "array-contains";

/// <summary>
/// The items expected to be in the array.
/// </summary>
[JsonPropertyName("variants")]
public dynamic Value { get; }

public ArrayContainsMatcher(dynamic[] variants)
{
Value = variants;
}
}
}
5 changes: 5 additions & 0 deletions src/PactNet.Abstractions/Matchers/Match.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,10 @@ public static IMatcher Include(string example)
{
return new IncludeMatcher(example);
}

public static IMatcher ArrayContains(dynamic[] variations)
{
return new ArrayContainsMatcher(variations);
}
}
}
3 changes: 3 additions & 0 deletions src/PactNet.Abstractions/Matchers/MatcherConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public override void Write(Utf8JsonWriter writer, IMatcher value, JsonSerializer
case TypeMatcher matcher:
JsonSerializer.Serialize(writer, matcher, options);
break;
case ArrayContainsMatcher matcher:
JsonSerializer.Serialize(writer, matcher, options);
break;
default:
throw new ArgumentOutOfRangeException($"Unsupported matcher: {value.GetType()}");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Text.Json;
using FluentAssertions;
using PactNet.Matchers;
using Xunit;

namespace PactNet.Abstractions.Tests.Matchers
{
public class ArrayContainsMatcherTests
{
[Fact]
public void Ctor_String_SerializesCorrectly()
{
// Arrange
var example = new[]
{
"Thing1",
"Thing2",
};

var matcher = new ArrayContainsMatcher(example);

// Act
var actual = JsonSerializer.Serialize(matcher);

// Assert
actual.Should().Be(@"{""pact:matcher:type"":""array-contains"",""variants"":[""Thing1"",""Thing2""]}");
}
}
}
Loading

0 comments on commit 1915b96

Please sign in to comment.