Skip to content

Commit ff6f37c

Browse files
committed
feat: add array contains matcher
1 parent 8f98cb5 commit ff6f37c

File tree

11 files changed

+555
-4
lines changed

11 files changed

+555
-4
lines changed

samples/OrdersApi/Consumer.Tests/OrdersClientTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,58 @@ await this.pact.VerifyAsync(async ctx =>
8484
});
8585
}
8686

87+
[Fact]
88+
public async Task GetOrdersAsync_WhenCalled_ReturnsMultipleOrders()
89+
{
90+
var expected1 = new OrderDto(1, OrderStatus.Pending, new DateTimeOffset(2023, 6, 28, 12, 13, 14, TimeSpan.FromHours(1)));
91+
var expected2 = new OrderDto(2, OrderStatus.Pending, new DateTimeOffset(2023, 6, 29, 12, 13, 14, TimeSpan.FromHours(1)));
92+
93+
this.pact
94+
.UponReceiving("a request for multiple orders by id")
95+
.Given("orders with ids {ids} exist", new Dictionary<string, string> { ["ids"] = "1,2" })
96+
.WithRequest(HttpMethod.Get, "/api/orders/many/1,2")
97+
.WithHeader("Accept", "application/json")
98+
.WillRespond()
99+
.WithStatus(HttpStatusCode.OK)
100+
.WithJsonBody(Match.ArrayContains(new dynamic[]
101+
{
102+
new
103+
{
104+
Id = Match.Integer(expected1.Id),
105+
Status = Match.Regex(expected1.Status.ToString(), string.Join("|", Enum.GetNames<OrderStatus>())),
106+
Date = Match.Type(expected1.Date.ToString("O"))
107+
},
108+
new
109+
{
110+
Id = Match.Integer(expected2.Id),
111+
Status = Match.Regex(expected2.Status.ToString(), string.Join("|", Enum.GetNames<OrderStatus>())),
112+
Date = Match.Type(expected2.Date.ToString("O"))
113+
},
114+
}));
115+
116+
await this.pact.VerifyAsync(async ctx =>
117+
{
118+
this.mockFactory
119+
.Setup(f => f.CreateClient("Orders"))
120+
.Returns(() => new HttpClient
121+
{
122+
BaseAddress = ctx.MockServerUri,
123+
DefaultRequestHeaders =
124+
{
125+
Accept = { MediaTypeWithQualityHeaderValue.Parse("application/json") }
126+
}
127+
});
128+
129+
var client = new OrdersClient(this.mockFactory.Object);
130+
131+
OrderDto[] orders = await client.GetOrdersAsync(new[] { 1, 2 });
132+
133+
orders.Should().HaveCount(2);
134+
orders[0].Should().Be(expected1);
135+
orders[1].Should().Be(expected2);
136+
});
137+
}
138+
87139
[Fact]
88140
public async Task GetOrderAsync_UnknownOrder_ReturnsNotFound()
89141
{

samples/OrdersApi/Consumer.Tests/pacts/Fulfilment API-Orders API.json

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,126 @@
8888
},
8989
"type": "Synchronous/HTTP"
9090
},
91+
{
92+
"description": "a request for multiple orders by id",
93+
"pending": false,
94+
"providerStates": [
95+
{
96+
"name": "orders with ids {ids} exist",
97+
"params": {
98+
"ids": "1,2"
99+
}
100+
}
101+
],
102+
"request": {
103+
"headers": {
104+
"Accept": [
105+
"application/json"
106+
]
107+
},
108+
"method": "GET",
109+
"path": "/api/orders/many/1,2"
110+
},
111+
"response": {
112+
"body": {
113+
"content": [
114+
{
115+
"date": "2023-06-28T12:13:14.0000000+01:00",
116+
"id": 1,
117+
"status": "Pending"
118+
},
119+
{
120+
"date": "2023-06-29T12:13:14.0000000+01:00",
121+
"id": 2,
122+
"status": "Pending"
123+
}
124+
],
125+
"contentType": "application/json",
126+
"encoded": false
127+
},
128+
"headers": {
129+
"Content-Type": [
130+
"application/json"
131+
]
132+
},
133+
"matchingRules": {
134+
"body": {
135+
"$": {
136+
"combine": "AND",
137+
"matchers": [
138+
{
139+
"match": "arrayContains",
140+
"variants": [
141+
{
142+
"index": 0,
143+
"rules": {
144+
"$.date": {
145+
"combine": "AND",
146+
"matchers": [
147+
{
148+
"match": "type"
149+
}
150+
]
151+
},
152+
"$.id": {
153+
"combine": "AND",
154+
"matchers": [
155+
{
156+
"match": "integer"
157+
}
158+
]
159+
},
160+
"$.status": {
161+
"combine": "AND",
162+
"matchers": [
163+
{
164+
"match": "regex",
165+
"regex": "Pending|Fulfilling|Shipped"
166+
}
167+
]
168+
}
169+
}
170+
},
171+
{
172+
"index": 1,
173+
"rules": {
174+
"$.date": {
175+
"combine": "AND",
176+
"matchers": [
177+
{
178+
"match": "type"
179+
}
180+
]
181+
},
182+
"$.id": {
183+
"combine": "AND",
184+
"matchers": [
185+
{
186+
"match": "integer"
187+
}
188+
]
189+
},
190+
"$.status": {
191+
"combine": "AND",
192+
"matchers": [
193+
{
194+
"match": "regex",
195+
"regex": "Pending|Fulfilling|Shipped"
196+
}
197+
]
198+
}
199+
}
200+
}
201+
]
202+
}
203+
]
204+
}
205+
}
206+
},
207+
"status": 200
208+
},
209+
"type": "Synchronous/HTTP"
210+
},
91211
{
92212
"description": "a request to update the status of an order",
93213
"pending": false,

samples/OrdersApi/Consumer/OrdersClient.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Net.Http;
1+
using System.Collections.Generic;
2+
using System.Net.Http;
23
using System.Net.Http.Json;
34
using System.Text.Json;
45
using System.Text.Json.Serialization;
@@ -40,6 +41,19 @@ public async Task<OrderDto> GetOrderAsync(int orderId)
4041
return order;
4142
}
4243

44+
/// <summary>
45+
/// Get a orders by ID
46+
/// </summary>
47+
/// <param name="orderIds">Order IDs</param>
48+
/// <returns>Order</returns>
49+
public async Task<OrderDto[]> GetOrdersAsync(IEnumerable<int> orderIds)
50+
{
51+
using HttpClient client = this.factory.CreateClient("Orders");
52+
53+
OrderDto[] orders = await client.GetFromJsonAsync<OrderDto[]>($"/api/orders/many/{string.Join(',', orderIds)}", Options);
54+
return orders;
55+
}
56+
4357
/// <summary>
4458
/// Update the status of an order
4559
/// </summary>

samples/OrdersApi/Provider.Tests/ProviderStateMiddleware.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.Linq;
45
using System.Net.Http;
56
using System.Text;
67
using System.Text.Json;
@@ -36,7 +37,8 @@ public ProviderStateMiddleware(RequestDelegate next, IOrderRepository orders)
3637

3738
this.providerStates = new Dictionary<string, Func<IDictionary<string, object>, Task>>
3839
{
39-
["an order with ID {id} exists"] = this.EnsureEventExistsAsync
40+
["an order with ID {id} exists"] = this.EnsureEventExistsAsync,
41+
["orders with ids {ids} exist"] = this.EnsureEventsExistAsync
4042
};
4143
}
4244

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

57+
private async Task EnsureEventsExistAsync(IDictionary<string, object> parameters)
58+
{
59+
var ids = (JsonElement)parameters["ids"];
60+
foreach (var id in ids.GetString()!.Split(',').Select(int.Parse))
61+
{
62+
await this.orders.InsertAsync(new OrderDto(id, OrderStatus.Fulfilling, DateTimeOffset.Now));
63+
}
64+
}
65+
5566
/// <summary>
5667
/// Handle the request
5768
/// </summary>
@@ -79,7 +90,7 @@ public async Task InvokeAsync(HttpContext context)
7990
try
8091
{
8192
ProviderState providerState = JsonSerializer.Deserialize<ProviderState>(jsonRequestBody, Options);
82-
93+
8394
if (!string.IsNullOrEmpty(providerState?.State))
8495
{
8596
await this.providerStates[providerState.State].Invoke(providerState.Params);

samples/OrdersApi/Provider/Orders/OrdersController.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using System.Threading.Tasks;
45
using Microsoft.AspNetCore.Http;
56
using Microsoft.AspNetCore.Mvc;
@@ -46,6 +47,29 @@ public async Task<IActionResult> GetByIdAsync(int id)
4647
}
4748
}
4849

50+
[HttpGet("many/{ids}", Name = "getMany")]
51+
[ProducesResponseType(typeof(OrderDto[]), StatusCodes.Status200OK)]
52+
public async Task<IActionResult> GetManyAsync(string ids)
53+
{
54+
try
55+
{
56+
var idsAsInts = ids.Split(',').Select(int.Parse);
57+
58+
List<OrderDto> result = new List<OrderDto>();
59+
foreach (int id in idsAsInts)
60+
{
61+
var order = await this.orders.GetAsync(id);
62+
result.Add(order);
63+
}
64+
65+
return this.Ok(result.ToArray());
66+
}
67+
catch (KeyNotFoundException)
68+
{
69+
return this.NotFound();
70+
}
71+
}
72+
4973
/// <summary>
5074
/// Create a new pending order
5175
/// </summary>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace PactNet.Matchers
4+
{
5+
public class ArrayContainsMatcher : IMatcher
6+
{
7+
/// <summary>
8+
/// Type of the matcher
9+
/// </summary>
10+
[JsonPropertyName("pact:matcher:type")]
11+
public string Type => "array-contains";
12+
13+
/// <summary>
14+
/// The items expected to be in the array.
15+
/// </summary>
16+
[JsonPropertyName("variants")]
17+
public dynamic Value { get; }
18+
19+
public ArrayContainsMatcher(dynamic[] variants)
20+
{
21+
Value = variants;
22+
}
23+
}
24+
}

src/PactNet.Abstractions/Matchers/Match.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,5 +168,10 @@ public static IMatcher Include(string example)
168168
{
169169
return new IncludeMatcher(example);
170170
}
171+
172+
public static IMatcher ArrayContains(dynamic[] variations)
173+
{
174+
return new ArrayContainsMatcher(variations);
175+
}
171176
}
172177
}

src/PactNet.Abstractions/Matchers/MatcherConverter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ public override void Write(Utf8JsonWriter writer, IMatcher value, JsonSerializer
5252
case TypeMatcher matcher:
5353
JsonSerializer.Serialize(writer, matcher, options);
5454
break;
55+
case ArrayContainsMatcher matcher:
56+
JsonSerializer.Serialize(writer, matcher, options);
57+
break;
5558
default:
5659
throw new ArgumentOutOfRangeException($"Unsupported matcher: {value.GetType()}");
5760
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Text.Json;
2+
using FluentAssertions;
3+
using PactNet.Matchers;
4+
using Xunit;
5+
6+
namespace PactNet.Abstractions.Tests.Matchers
7+
{
8+
public class ArrayContainsMatcherTests
9+
{
10+
[Fact]
11+
public void Ctor_String_SerializesCorrectly()
12+
{
13+
// Arrange
14+
var example = new[]
15+
{
16+
"Thing1",
17+
"Thing2",
18+
};
19+
20+
var matcher = new ArrayContainsMatcher(example);
21+
22+
// Act
23+
var actual = JsonSerializer.Serialize(matcher);
24+
25+
// Assert
26+
actual.Should().Be(@"{""pact:matcher:type"":""array-contains"",""variants"":[""Thing1"",""Thing2""]}");
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)