├── .gitignore ├── ComPact.ConsumerTests ├── ComPact.ConsumerTests.csproj ├── Handler │ └── RecipeAddedHandler.cs ├── MessagePactTests.cs ├── PactV2ConsumerTests.cs ├── PactV3ConsumerTests.cs └── TestSupport │ └── Contract.cs ├── ComPact.ProviderTests ├── ComPact.ProviderTests.csproj ├── Properties │ └── launchSettings.json ├── ProviderTests.cs ├── TestSupport │ ├── FakeRecipeRepository.cs │ ├── IRecipeRepository.cs │ ├── MessageProviderStateHandler.cs │ ├── MessageSender.cs │ ├── ProviderStateHandler.cs │ ├── RecipesController.cs │ └── TestStartup.cs ├── appsettings.Development.json ├── appsettings.json └── pacts │ ├── messageConsumer-messageProvider.json │ └── recipe-consumer-recipe-service.json ├── ComPact.Tests.Shared ├── ComPact.Tests.Shared.csproj ├── FakePactBrokerMessageHandler.cs ├── Ingredient.cs ├── Recipe.cs └── RecipeAdded.cs ├── ComPact.UnitTests ├── Builders │ ├── CreateV2MatchingRulesTests.cs │ ├── CreateV3MatchingRulesTests.cs │ ├── PactJsonContentDslTests.cs │ ├── PactJsonContentV3DslTests.cs │ └── PactPublisherTests.cs ├── ComPact.UnitTests.csproj ├── JsonHelpers │ └── JTokenExtensionsTests.cs ├── Matching │ ├── MatcherTests.cs │ ├── RequestTests │ │ ├── QueryTests │ │ │ ├── QueryMatchingTests.cs │ │ │ └── Testcases │ │ │ │ ├── different order.json │ │ │ │ ├── different params.json │ │ │ │ ├── matches with equals in the query value.json │ │ │ │ ├── matches.json │ │ │ │ ├── missing params.json │ │ │ │ ├── same parameter different values.json │ │ │ │ ├── same parameter multiple times in different order.json │ │ │ │ ├── same parameter multiple times.json │ │ │ │ └── unexpected param.json │ │ └── Testcase.cs │ ├── ResponseTests │ │ ├── BodyTests │ │ │ ├── BodyMatchingTests.cs │ │ │ └── Testcases │ │ │ │ ├── additional property with type matcher that does not match.json │ │ │ │ ├── additional property with type matcher.json │ │ │ │ ├── array at top level with matchers.json │ │ │ │ ├── array at top level.json │ │ │ │ ├── array in different order.json │ │ │ │ ├── array with regex matcher.json │ │ │ │ ├── array with type matcher mismatch.json │ │ │ │ ├── array with type matcher.json │ │ │ │ ├── compact - actual type does not match expected type.json │ │ │ │ ├── compact - additional child element different type.json │ │ │ │ ├── compact - additional child element different type2.json │ │ │ │ ├── compact - array of objects with type matcher.json │ │ │ │ ├── compact - datetime matching regex.json │ │ │ │ ├── compact - datetime not matching regex.json │ │ │ │ ├── compact - empty matchingrules object.json │ │ │ │ ├── compact - type matcher on decimal allows integer.json │ │ │ │ ├── compact - type matcher on integer allows decimal.json │ │ │ │ ├── decimal matcher does not match.json │ │ │ │ ├── decimal matcher.json │ │ │ │ ├── deeply nested objects.json │ │ │ │ ├── different value found at index.json │ │ │ │ ├── different value found at key.json │ │ │ │ ├── empty body no content type.json │ │ │ │ ├── empty body.json │ │ │ │ ├── equality matcher overrides type matcher.json │ │ │ │ ├── include matcher does not match.json │ │ │ │ ├── include matcher.json │ │ │ │ ├── integer matcher does not match.json │ │ │ │ ├── integer matcher.json │ │ │ │ ├── keys out of order match.json │ │ │ │ ├── matches with floats.json │ │ │ │ ├── matches with integers.json │ │ │ │ ├── matches with regex.json │ │ │ │ ├── matches with type.json │ │ │ │ ├── matches.json │ │ │ │ ├── missing body found when empty expected.json │ │ │ │ ├── missing body no content type.json │ │ │ │ ├── missing body.json │ │ │ │ ├── missing index.json │ │ │ │ ├── missing key.json │ │ │ │ ├── no body no content type.json │ │ │ │ ├── not null found at key when null expected.json │ │ │ │ ├── not null found in array when null expected.json │ │ │ │ ├── null body no content type.json │ │ │ │ ├── null body.json │ │ │ │ ├── null found at key where not null expected.json │ │ │ │ ├── null found in array when not null expected.json │ │ │ │ ├── null matcher does not match when key not present.json │ │ │ │ ├── null matcher does not match.json │ │ │ │ ├── null matcher.json │ │ │ │ ├── number found at key when string expected.json │ │ │ │ ├── number found in array when string expected.json │ │ │ │ ├── objects in array first matches.json │ │ │ │ ├── objects in array no matches.json │ │ │ │ ├── objects in array second matches.json │ │ │ │ ├── objects in array type matching.json │ │ │ │ ├── objects in array with type mismatching.json │ │ │ │ ├── plain text empty body.json │ │ │ │ ├── plain text missing body.json │ │ │ │ ├── plain text regex matching missing body.json │ │ │ │ ├── plain text regex matching that does not match.json │ │ │ │ ├── plain text regex matching.json │ │ │ │ ├── plain text that does not match.json │ │ │ │ ├── plain text that matches.json │ │ │ │ ├── property name is different case.json │ │ │ │ ├── string found at key when number expected.json │ │ │ │ ├── string found in array when number expected.json │ │ │ │ ├── unexpected index with not null value.json │ │ │ │ ├── unexpected index with null value.json │ │ │ │ ├── unexpected key with not null value.json │ │ │ │ └── unexpected key with null value.json │ │ ├── HeaderTests │ │ │ ├── HeaderMatchingTests.cs │ │ │ └── Testcases │ │ │ │ ├── content type parameters do not match.json │ │ │ │ ├── empty headers.json │ │ │ │ ├── header name is different case.json │ │ │ │ ├── header value is different case.json │ │ │ │ ├── matches content type with charset.json │ │ │ │ ├── matches content type with parameters in different order.json │ │ │ │ ├── matches with regex.json │ │ │ │ ├── matches.json │ │ │ │ ├── order of comma separated header values different.json │ │ │ │ ├── unexpected header found.json │ │ │ │ └── whitespace after comma different.json │ │ └── Testcase.cs │ └── TryGetApplicableMatcherListTests.cs ├── MockProvider │ └── MatchableInteractionListTests.cs ├── Models │ ├── HeadersFromHeaderDictionaryTests.cs │ ├── HeadersMatchTests.cs │ ├── MatcherTypeEnumTests.cs │ ├── MatchingRulesUpgradeTests.cs │ ├── V2 │ │ └── SetEmptyValuesToNullTests.cs │ └── V3 │ │ ├── HttpRequestMessageFromRequestTests.cs │ │ ├── QueryToAndFromQueryStringTests.cs │ │ ├── RequestFromHttpRequestTests.cs │ │ ├── RequestMatchTests.cs │ │ ├── ResponseFromHttpResponseMessageTests.cs │ │ └── SetEmptyValuesToNullTests.cs └── Verifier │ ├── GetPactFromPactBrokerTests.cs │ ├── InvokeProviderStateHandlerTests.cs │ ├── PublishVerificationResultsTests.cs │ ├── TestToTestMessageStringTests.cs │ ├── VerifyInteractionsTests.cs │ └── VerifyMessagesTests.cs ├── ComPact.sln ├── ComPact ├── Builders │ ├── PactBuilderBase.cs │ ├── PactJsonContentDsl.cs │ ├── PactPublisher.cs │ ├── PactWriter.cs │ ├── V2 │ │ ├── InteractionBuilder.cs │ │ ├── Pact.cs │ │ ├── PactBuilder.cs │ │ ├── RequestBuilder.cs │ │ └── ResponseBuilder.cs │ └── V3 │ │ ├── InteractionBuilder.cs │ │ ├── MessageBuilder.cs │ │ ├── MessagePactBuilder.cs │ │ ├── Pact.cs │ │ ├── PactBuilder.cs │ │ ├── PactJsonContentV3Dsl.cs │ │ ├── RequestBuilder.cs │ │ └── ResponseBuilder.cs ├── ComPact.csproj ├── Exceptions │ ├── PactException.cs │ └── PactVerificationException.cs ├── JsonHelpers │ ├── JTokenExtensions.cs │ ├── JTokenParser.cs │ └── StringEnumWithDefaultConverter.cs ├── MockProvider │ ├── MatchableInteraction.cs │ ├── MatchableInteractionList.cs │ ├── ProviderWebHost.cs │ ├── RequestResponseMatcher.cs │ └── RequestResponseMatchingErrorResponse.cs ├── Models │ ├── BodyMatcher.cs │ ├── ContractWithSomeVersion.cs │ ├── Headers.cs │ ├── IContract.cs │ ├── Matcher.cs │ ├── MatcherList.cs │ ├── MatcherType.cs │ ├── MatchingRuleCollection.cs │ ├── Metadata.cs │ ├── Method.cs │ ├── Pacticipant.cs │ ├── ProviderState.cs │ ├── V2 │ │ ├── Contract.cs │ │ ├── Interaction.cs │ │ ├── Request.cs │ │ └── Response.cs │ └── V3 │ │ ├── Contract.cs │ │ ├── Interaction.cs │ │ ├── Message.cs │ │ ├── MessageContract.cs │ │ ├── Query.cs │ │ ├── Request.cs │ │ └── Response.cs └── Verifier │ ├── PactVerifier.cs │ ├── PactVerifierConfig.cs │ └── VerificationResults.cs ├── LICENSE └── README.md /ComPact.ConsumerTests/ComPact.ConsumerTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | ComPact.ConsumerTests 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ComPact.ConsumerTests/Handler/RecipeAddedHandler.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Tests.Shared; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace ComPact.ConsumerTests.Handler 7 | { 8 | public class RecipeAddedHandler 9 | { 10 | public List ReceivedRecipes { get; set; } = new List(); 11 | 12 | public void Handle(RecipeAdded recipeAdded) 13 | { 14 | if (recipeAdded is null) 15 | { 16 | throw new ArgumentNullException(nameof(recipeAdded)); 17 | } 18 | 19 | ReceivedRecipes.Add(recipeAdded.Recipe); 20 | } 21 | 22 | public Task HandleAsync(RecipeAdded recipeAdded) 23 | { 24 | if (recipeAdded is null) 25 | { 26 | throw new ArgumentNullException(nameof(recipeAdded)); 27 | } 28 | 29 | ReceivedRecipes.Add(recipeAdded.Recipe); 30 | return Task.CompletedTask; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ComPact.ConsumerTests/TestSupport/Contract.cs: -------------------------------------------------------------------------------- 1 | namespace ComPact.ConsumerTests.TestSupport 2 | { 3 | public class Contract 4 | { 5 | public Pacticipant Consumer { get; set; } 6 | } 7 | 8 | public class Pacticipant 9 | { 10 | public string Name { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ComPact.ProviderTests/ComPact.ProviderTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | InProcess 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ComPact.ProviderTests/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:50519", 8 | "sslPort": 44374 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "api/values", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "ComPact.ProviderTests": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "api/values", 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /ComPact.ProviderTests/TestSupport/FakeRecipeRepository.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Tests.Shared; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace ComPact.ProviderTests.TestSupport 7 | { 8 | public class FakeRecipeRepository : IRecipeRepository 9 | { 10 | public List Recipes { get; set; } = new List(); 11 | 12 | public void Add(Recipe recipe) 13 | { 14 | Recipes.Add(recipe); 15 | } 16 | 17 | public Recipe GetById(Guid id) 18 | { 19 | return Recipes.FirstOrDefault(r => r.Id == id); 20 | } 21 | 22 | public Recipe GetLatestAdded() 23 | { 24 | return Recipes.LastOrDefault(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ComPact.ProviderTests/TestSupport/IRecipeRepository.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Tests.Shared; 2 | using System; 3 | 4 | namespace ComPact.ProviderTests 5 | { 6 | public interface IRecipeRepository 7 | { 8 | Recipe GetById(Guid id); 9 | Recipe GetLatestAdded(); 10 | void Add(Recipe recipe); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ComPact.ProviderTests/TestSupport/MessageProviderStateHandler.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using ComPact.Tests.Shared; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace ComPact.ProviderTests.TestSupport 7 | { 8 | public class MessageProviderStateHandler 9 | { 10 | private FakeRecipeRepository _recipeRepository; 11 | 12 | public MessageProviderStateHandler(FakeRecipeRepository recipeRepository) 13 | { 14 | _recipeRepository = recipeRepository; 15 | } 16 | 17 | public void Handle(IEnumerable providerStates) 18 | { 19 | foreach (var providerState in providerStates) 20 | { 21 | if (providerState.Name.StartsWith("A new recipe has been added")) 22 | { 23 | var id = providerState.Params["recipeId"]; 24 | 25 | var recipe = new Recipe 26 | { 27 | Id = Guid.Parse(id), 28 | Name = "Pizza dough", 29 | Instructions = "Mix the yeast with a little water and the sugar. Let it sit for 10 minutes. " + 30 | "Add the flour, then add the salt and the oil and mix it all up. Then add the rest of the water. " + 31 | "Knead for a good 10 or 15 minutes until the dough can be stetched and isn't too sticky to handle any more. " + 32 | "Let it proof for about an hour covered with a tea towel or some plastic wrap.", 33 | Ingredients = new List 34 | { 35 | new Ingredient { Name = "Flour", Amount = 190, Unit = "gram" }, 36 | new Ingredient { Name = "Yeast", Amount = 5, Unit = "gram" }, 37 | new Ingredient { Name = "Sugar", Amount = 10, Unit = "gram" }, 38 | new Ingredient { Name = "Water", Amount = 120, Unit = "ml" }, 39 | new Ingredient { Name = "Olive oil", Amount = 10, Unit = "ml" }, 40 | new Ingredient { Name = "Salt", Amount = 5, Unit = "gram" } 41 | } 42 | }; 43 | 44 | _recipeRepository.Add(recipe); 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ComPact.ProviderTests/TestSupport/MessageSender.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using ComPact.Tests.Shared; 3 | using System; 4 | 5 | namespace ComPact.ProviderTests.TestSupport 6 | { 7 | public class MessageSender 8 | { 9 | public FakeRecipeRepository RecipeRepository { get; set; } 10 | 11 | public MessageSender(FakeRecipeRepository recipeRepository) 12 | { 13 | RecipeRepository = recipeRepository; 14 | } 15 | 16 | public RecipeAdded Send(string description) 17 | { 18 | return new RecipeAdded 19 | { 20 | EventId = Guid.NewGuid(), 21 | Recipe = RecipeRepository.GetLatestAdded() 22 | }; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ComPact.ProviderTests/TestSupport/ProviderStateHandler.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Exceptions; 2 | using ComPact.Models; 3 | using ComPact.Tests.Shared; 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace ComPact.ProviderTests.TestSupport 8 | { 9 | public class ProviderStateHandler 10 | { 11 | public FakeRecipeRepository RecipeRepository { get; set; } 12 | 13 | public ProviderStateHandler(FakeRecipeRepository recipeRepository) 14 | { 15 | RecipeRepository = recipeRepository; 16 | } 17 | 18 | public void Handle(ProviderState providerState) 19 | { 20 | string id = null; 21 | 22 | if (providerState.Name.StartsWith("A new recipe has been added")) 23 | { 24 | id = providerState.Params["recipeId"]; 25 | } 26 | else if (providerState.Name.StartsWith("There is a recipe with id")) 27 | { 28 | id = providerState.Name.Split('`')[1]; 29 | } 30 | else 31 | { 32 | throw new PactVerificationException("Unknown provider state"); 33 | } 34 | 35 | if (id != null) 36 | { 37 | var recipe = new Recipe 38 | { 39 | Id = Guid.Parse(id), 40 | Name = "Pizza dough", 41 | Instructions = "Mix the yeast with a little water and the sugar. Let it sit for 10 minutes. " + 42 | "Add the flour, then add the salt and the oil and mix it all up. Then add the rest of the water. " + 43 | "Knead for a good 10 or 15 minutes until the dough can be stetched and isn't too sticky to handle any more. " + 44 | "Let it proof for about an hour covered with a tea towel or some plastic wrap.", 45 | Ingredients = new List 46 | { 47 | new Ingredient { Name = "Flour", Amount = 190, Unit = "gram" }, 48 | new Ingredient { Name = "Yeast", Amount = 5, Unit = "gram" }, 49 | new Ingredient { Name = "Sugar", Amount = 10, Unit = "gram" }, 50 | new Ingredient { Name = "Water", Amount = 120, Unit = "ml" }, 51 | new Ingredient { Name = "Olive oil", Amount = 10, Unit = "ml" }, 52 | new Ingredient { Name = "Salt", Amount = 5, Unit = "gram" } 53 | } 54 | }; 55 | 56 | RecipeRepository.Add(recipe); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ComPact.ProviderTests/TestSupport/RecipesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ComPact.Tests.Shared; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace ComPact.ProviderTests.TestSupport 6 | { 7 | [Route("api/[controller]")] 8 | [ApiController] 9 | public class RecipesController : ControllerBase 10 | { 11 | private readonly IRecipeRepository _recipeRepo; 12 | 13 | public RecipesController(IRecipeRepository recipeRepo) 14 | { 15 | _recipeRepo = recipeRepo; 16 | } 17 | 18 | // GET api/recipes/acb609ce-c5af-4391-a36f-700ac5ab5e88 19 | [HttpGet("{id}")] 20 | public ActionResult Get(string id) 21 | { 22 | if (!Guid.TryParse(id, out var guid)) 23 | { 24 | return BadRequest($"Id {id} is not a valid Guid"); 25 | } 26 | var recipe = _recipeRepo.GetById(guid); 27 | if (recipe == null) 28 | { 29 | return BadRequest($"No recipe found with id {id}"); 30 | } 31 | return Ok(recipe); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ComPact.ProviderTests/TestSupport/TestStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace ComPact.ProviderTests.TestSupport 5 | { 6 | public class TestStartup 7 | { 8 | // This method gets called by the runtime. Use this method to add services to the container. 9 | public void ConfigureServices(IServiceCollection services) 10 | { 11 | services.AddControllers(); 12 | } 13 | 14 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 15 | public void Configure(IApplicationBuilder app) 16 | { 17 | app.UseHttpsRedirection(); 18 | app.UseRouting(); 19 | app.UseEndpoints(endpoints => 20 | { 21 | endpoints.MapControllers(); 22 | }); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ComPact.ProviderTests/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ComPact.ProviderTests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /ComPact.ProviderTests/pacts/messageConsumer-messageProvider.json: -------------------------------------------------------------------------------- 1 | { 2 | "consumer": { 3 | "name": "messageConsumer" 4 | }, 5 | "provider": { 6 | "name": "messageProvider" 7 | }, 8 | "messages": [ 9 | { 10 | "providerStates": [ 11 | { 12 | "name": "A new recipe has been added.", 13 | "params": { "recipeId": "7169de6d-df9b-4cf5-8cdc-2654062e5cdc" } 14 | } 15 | ], 16 | "description": "a RecipeAdded event.", 17 | "contents": { 18 | "eventId": "f84fe18f-d871-4dad-9723-65b6dc9b0578", 19 | "recipe": { 20 | "id": "7169de6d-df9b-4cf5-8cdc-2654062e5cdc", 21 | "name": "A Recipe", 22 | "instructions": "Mix it up", 23 | "ingredients": [ 24 | { 25 | "name": "Salt", 26 | "amount": 5.5, 27 | "unit": "gram" 28 | } 29 | ] 30 | } 31 | }, 32 | "matchingRules": { 33 | "body": { 34 | "$.eventId": { 35 | "combine": "AND", 36 | "matchers": [ 37 | { 38 | "match": "regex", 39 | "regex": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" 40 | } 41 | ] 42 | }, 43 | "$.recipe.name": { 44 | "combine": "AND", 45 | "matchers": [ 46 | { 47 | "match": "type" 48 | } 49 | ] 50 | }, 51 | "$.recipe.instructions": { 52 | "combine": "AND", 53 | "matchers": [ 54 | { 55 | "match": "type" 56 | } 57 | ] 58 | }, 59 | "$.recipe.ingredients": { 60 | "combine": "AND", 61 | "matchers": [ 62 | { 63 | "match": "type", 64 | "min": 1 65 | } 66 | ] 67 | }, 68 | "$.recipe.ingredients[*].name": { 69 | "combine": "AND", 70 | "matchers": [ 71 | { 72 | "match": "type" 73 | } 74 | ] 75 | }, 76 | "$.recipe.ingredients[*].amount": { 77 | "combine": "AND", 78 | "matchers": [ 79 | { 80 | "match": "type" 81 | } 82 | ] 83 | }, 84 | "$.recipe.ingredients[*].unit": { 85 | "combine": "AND", 86 | "matchers": [ 87 | { 88 | "match": "type" 89 | } 90 | ] 91 | } 92 | } 93 | }, 94 | "metaData": { 95 | "ContentType": "application/json" 96 | } 97 | } 98 | ], 99 | "metadata": { 100 | "pactSpecification": { 101 | "version": "3.0.0" 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /ComPact.ProviderTests/pacts/recipe-consumer-recipe-service.json: -------------------------------------------------------------------------------- 1 | { 2 | "consumer": { 3 | "name": "recipe-consumer" 4 | }, 5 | "provider": { 6 | "name": "recipe-service" 7 | }, 8 | "interactions": [ 9 | { 10 | "description": "a request", 11 | "providerState": "There is a recipe with id `2860dedb-a193-425f-b73e-ef02db0aa8cf`", 12 | "request": { 13 | "method": "GET", 14 | "path": "/api/recipes/2860dedb-a193-425f-b73e-ef02db0aa8cf", 15 | "headers": { 16 | "Accept": "application/json" 17 | }, 18 | "query": "" 19 | }, 20 | "response": { 21 | "status": 200, 22 | "headers": { 23 | "Content-Type": "application/json; charset=utf-8" 24 | }, 25 | "body": { 26 | "name": "A Recipe", 27 | "instructions": "Mix it up", 28 | "ingredients": [ 29 | { 30 | "name": "Salt", 31 | "amount": 5.5, 32 | "unit": "gram" 33 | } 34 | ] 35 | }, 36 | "matchingRules": { 37 | "$.body.name": { 38 | "match": "type" 39 | }, 40 | "$.body.instructions": { 41 | "match": "type" 42 | }, 43 | "$.body.ingredients": { 44 | "match": "type", 45 | "min": "1" 46 | } 47 | } 48 | } 49 | } 50 | ], 51 | "metadata": { 52 | "pactSpecification": { 53 | "version": "2.0.0" 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /ComPact.Tests.Shared/ComPact.Tests.Shared.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ComPact.Tests.Shared/Ingredient.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace ComPact.Tests.Shared 4 | { 5 | public class Ingredient 6 | { 7 | [JsonProperty("name")] 8 | public string Name { get; set; } 9 | [JsonProperty("amount")] 10 | public decimal Amount { get; set; } 11 | [JsonProperty("unit")] 12 | public string Unit { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ComPact.Tests.Shared/Recipe.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace ComPact.Tests.Shared 6 | { 7 | public class Recipe 8 | { 9 | [JsonProperty("id")] 10 | public Guid Id { get; set; } 11 | [JsonProperty("name")] 12 | public string Name { get; set; } 13 | [JsonProperty("ingredients")] 14 | public List Ingredients { get; set; } 15 | [JsonProperty("instructions")] 16 | public string Instructions { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ComPact.Tests.Shared/RecipeAdded.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | namespace ComPact.Tests.Shared 5 | { 6 | public class RecipeAdded 7 | { 8 | [JsonProperty("eventId")] 9 | public Guid EventId { get; set; } 10 | [JsonProperty("recipe")] 11 | public Recipe Recipe { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Builders/PactJsonContentV3DslTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Builders.V3; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using Newtonsoft.Json; 4 | 5 | namespace ComPact.UnitTests.Builders 6 | { 7 | [TestClass] 8 | public class PactJsonContentV3DslTests 9 | { 10 | [TestMethod] 11 | public void Integer() 12 | { 13 | var pactJsonBody = Pact.JsonContent.With(Some.Integer.Like(1).Named("number")).ToJToken(); 14 | 15 | var expectedObject = new { number = 1 }; 16 | 17 | Assert.AreEqual(JsonConvert.SerializeObject(expectedObject), JsonConvert.SerializeObject(pactJsonBody)); 18 | } 19 | 20 | [TestMethod] 21 | public void Decimal() 22 | { 23 | var pactJsonBody = Pact.JsonContent.With(Some.Decimal.Like(1).Named("number")).ToJToken(); 24 | 25 | var expectedObject = new { number = 1.0 }; 26 | 27 | Assert.AreEqual(JsonConvert.SerializeObject(expectedObject), JsonConvert.SerializeObject(pactJsonBody)); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ComPact.UnitTests/ComPact.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | ComPact.UnitTests 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /ComPact.UnitTests/JsonHelpers/JTokenExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using Newtonsoft.Json.Linq; 3 | using ComPact.JsonHelpers; 4 | using System.Linq; 5 | 6 | namespace ComPact.UnitTests.JsonHelpers 7 | { 8 | [TestClass] 9 | public class JTokenExtensionsTests 10 | { 11 | [TestMethod] 12 | public void ShouldReturnFlatEnumerableBaseOnJTokenTree() 13 | { 14 | var someObject = new 15 | { 16 | name = "test", 17 | number = 1, 18 | someNestedObject = new 19 | { 20 | decimalNumber = 1.1, 21 | array = new[] 22 | { 23 | 1, 24 | 2, 25 | 3 26 | } 27 | } 28 | }; 29 | 30 | var jToken = JToken.FromObject(someObject); 31 | var flatEnumerable = jToken.ThisTokenAndAllItsDescendants(); 32 | 33 | Assert.AreEqual(14, flatEnumerable.Count()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/MatcherTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using Newtonsoft.Json.Linq; 4 | using System.Linq; 5 | 6 | namespace ComPact.UnitTests.Matching 7 | { 8 | [TestClass] 9 | public class MatcherTests 10 | { 11 | [TestMethod] 12 | public void TypeMatchForString() 13 | { 14 | var matcher = new Matcher { MatcherType = MatcherType.type }; 15 | 16 | var differences = matcher.Match("expected", "actual"); 17 | 18 | Assert.AreEqual(0, differences.Count); 19 | } 20 | 21 | [TestMethod] 22 | public void TypeMismatchForString() 23 | { 24 | var matcher = new Matcher { MatcherType = MatcherType.type }; 25 | 26 | var differences = matcher.Match("expected", 1); 27 | 28 | Assert.AreEqual("Expected value of type String (like: 'expected') at , but was value of type Integer.", differences.First()); 29 | } 30 | 31 | [TestMethod] 32 | public void RegexMatchForString() 33 | { 34 | var matcher = new Matcher { MatcherType = MatcherType.regex, Regex = "^ex.*$" }; 35 | 36 | var differences = matcher.Match("expected", "example"); 37 | 38 | Assert.AreEqual(0, differences.Count); 39 | } 40 | 41 | [TestMethod] 42 | public void TypeMismatchForRegex() 43 | { 44 | var matcher = new Matcher { MatcherType = MatcherType.regex, Regex = "^ex.*$" }; 45 | 46 | var differences = matcher.Match("expected", "actual"); 47 | 48 | Assert.AreEqual("Expected value matching '^ex.*$' (like: 'expected') at , but was 'actual'.", differences.First()); 49 | } 50 | 51 | [TestMethod] 52 | public void ShouldReturnPathInMessage() 53 | { 54 | var matcher = new Matcher { MatcherType = MatcherType.type }; 55 | 56 | var expectedObject = JToken.FromObject(new { body = new { name = "expected" } }); 57 | var differences = matcher.Match(expectedObject.SelectToken("body.name"), 1); 58 | 59 | Assert.AreEqual("Expected value of type String (like: 'expected') at body.name, but was value of type Integer.", differences.First()); 60 | } 61 | 62 | [TestMethod] 63 | public void ArrayWithTooFewItems() 64 | { 65 | var matcher = new Matcher { MatcherType = MatcherType.type, Min = 2 }; 66 | 67 | var differences = matcher.Match(new [] { 1, 2 }, new[] { 1 } ); 68 | 69 | Assert.AreEqual("Expected an array with at least 2 item(s) at , but was 1 items(s).", differences.First()); 70 | } 71 | 72 | [TestMethod] 73 | public void ArrayWithTooManyItems() 74 | { 75 | var matcher = new Matcher { MatcherType = MatcherType.type, Min = 1, Max = 1 }; 76 | 77 | var differences = matcher.Match(new[] { 1 }, new[] { 1, 2 }); 78 | 79 | Assert.AreEqual("Expected an array with at most 1 item(s) at , but was 2 items(s).", differences.First()); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/RequestTests/QueryTests/QueryMatchingTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | 8 | namespace ComPact.UnitTests.Matching.RequestTests.QueryTests 9 | { 10 | [TestClass] 11 | public class QueryMatchingTests 12 | { 13 | [TestMethod] 14 | public void ShouldSuccessfullyExecuteAllTestcase() 15 | { 16 | var testcasesDir = Path.GetFullPath($"{AppContext.BaseDirectory}{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}.." + 17 | $"{Path.DirectorySeparatorChar}Matching{Path.DirectorySeparatorChar}RequestTests{Path.DirectorySeparatorChar}QueryTests{Path.DirectorySeparatorChar}Testcases{Path.DirectorySeparatorChar}"); 18 | 19 | var testcaseFiles = Directory.GetFiles(testcasesDir); 20 | 21 | var failedCases = new List(); 22 | 23 | foreach (var file in testcaseFiles) 24 | { 25 | var testcase = JsonConvert.DeserializeObject(File.ReadAllText(file)); 26 | if (file.Split(Path.DirectorySeparatorChar).Last().StartsWith("same parameter multiple times in different order")) 27 | { 28 | var isMatch = testcase.Expected.Match(testcase.Actual); 29 | if (isMatch != testcase.Match) 30 | { 31 | failedCases.Add(file.Split(Path.DirectorySeparatorChar).Last()); 32 | } 33 | } 34 | } 35 | 36 | Assert.AreEqual(0, failedCases.Count, "Failed cases: " + Environment.NewLine + string.Join(Environment.NewLine, failedCases)); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/RequestTests/QueryTests/Testcases/different order.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Queries are the same but in different key order", 4 | "expected" : { 5 | "method": "GET", 6 | "path": "/path", 7 | "query": { 8 | "alligator": ["Mary"], 9 | "hippo": ["John"] 10 | }, 11 | "headers": {} 12 | }, 13 | "actual": { 14 | "method": "GET", 15 | "path": "/path", 16 | "query": { 17 | "hippo": ["John"], 18 | "alligator": ["Mary"] 19 | }, 20 | "headers": {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/RequestTests/QueryTests/Testcases/different params.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Queries are not the same - hippo is Fred instead of John", 4 | "expected" : { 5 | "method": "GET", 6 | "path": "/path", 7 | "query": { 8 | "alligator": ["Mary"], 9 | "hippo": ["John"] 10 | }, 11 | "headers": {} 12 | }, 13 | "actual": { 14 | "method": "GET", 15 | "path": "/path", 16 | "query": { 17 | "alligator": ["Mary"], 18 | "hippo": ["Fred"] 19 | }, 20 | "headers": {} 21 | } 22 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/RequestTests/QueryTests/Testcases/matches with equals in the query value.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Queries are equivalent", 4 | "expected" : { 5 | "method": "GET", 6 | "path": "/path", 7 | "query": { 8 | "options": ["delete.topic.enable=true"], 9 | "broker": ["1"] 10 | }, 11 | "headers": {} 12 | }, 13 | "actual": { 14 | "method": "GET", 15 | "path": "/path", 16 | "query": { 17 | "options": ["delete.topic.enable=true"], 18 | "broker": ["1"] 19 | }, 20 | "headers": {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/RequestTests/QueryTests/Testcases/matches.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Queries are the same", 4 | "expected" : { 5 | "method": "GET", 6 | "path": "/path", 7 | "query": { 8 | "alligator": ["Mary"], 9 | "hippo": ["John"] 10 | }, 11 | "headers": {} 12 | }, 13 | "actual": { 14 | "method": "GET", 15 | "path": "/path", 16 | "query": { 17 | "alligator": ["Mary"], 18 | "hippo": ["John"] 19 | }, 20 | "headers": {} 21 | } 22 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/RequestTests/QueryTests/Testcases/missing params.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Queries are not the same - elephant is missing", 4 | "expected" : { 5 | "method": "GET", 6 | "path": "/path", 7 | "query": { 8 | "alligator": ["Mary"], 9 | "hippo": ["Fred"], 10 | "elephant": ["missing"] 11 | }, 12 | "headers": {} 13 | }, 14 | "actual": { 15 | "method": "GET", 16 | "path": "/path", 17 | "query": { 18 | "alligator": ["Mary"], 19 | "hippo": ["Fred"] 20 | }, 21 | "headers": {} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/RequestTests/QueryTests/Testcases/same parameter different values.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Queries are not the same - animals are alligator, hippo versus alligator, elephant", 4 | "expected" : { 5 | "method": "GET", 6 | "path": "/path", 7 | "query": { 8 | "animal": ["alligator", "hippo"] 9 | }, 10 | "headers": {} 11 | }, 12 | "actual": { 13 | "method": "GET", 14 | "path": "/path", 15 | "query": { 16 | "animal": ["alligator", "elephant"] 17 | }, 18 | "headers": {} 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/RequestTests/QueryTests/Testcases/same parameter multiple times in different order.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Queries are not the same - values are in different order", 4 | "expected" : { 5 | "method": "GET", 6 | "path": "/path", 7 | "query": { 8 | "animal": ["alligator", "hippo", "elephant"] 9 | }, 10 | "headers": {} 11 | }, 12 | "actual": { 13 | "method": "GET", 14 | "path": "/path", 15 | "query": { 16 | "animal": ["hippo", "alligator", "elephant"] 17 | }, 18 | "headers": {} 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/RequestTests/QueryTests/Testcases/same parameter multiple times.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Queries are the same - multiple values are in same order", 4 | "expected" : { 5 | "method": "GET", 6 | "path": "/path", 7 | "query": { 8 | "animal": ["alligator", "hippo", "elephant"], 9 | "hippo": ["Fred"] 10 | }, 11 | "headers": {} 12 | }, 13 | "actual": { 14 | "method": "GET", 15 | "path": "/path", 16 | "query": { 17 | "hippo": ["Fred"], 18 | "animal": ["alligator", "hippo", "elephant"] 19 | }, 20 | "headers": {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/RequestTests/QueryTests/Testcases/unexpected param.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Queries are not the same - elephant is not expected", 4 | "expected" : { 5 | "method": "GET", 6 | "path": "/path", 7 | "query": { 8 | "alligator": ["Mary"], 9 | "hippo": ["John"] 10 | }, 11 | "headers": {} 12 | }, 13 | "actual": { 14 | "method": "GET", 15 | "path": "/path", 16 | "query": { 17 | "alligator": ["Mary"], 18 | "hippo": ["John"], 19 | "elephant": ["unexpected"] 20 | }, 21 | "headers": {} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/RequestTests/Testcase.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models.V3; 2 | 3 | namespace ComPact.UnitTests.Matching.RequestTests 4 | { 5 | internal class Testcase 6 | { 7 | public bool Match { get; set; } 8 | public string Comment { get; set; } 9 | public Request Expected { get; set; } 10 | public Request Actual { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/BodyMatchingTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using Newtonsoft.Json; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | 9 | namespace ComPact.UnitTests.Matching.ResponseTests.BodyTests 10 | { 11 | [TestClass] 12 | public class BodyMatchingTests 13 | { 14 | [TestMethod] 15 | public void ShouldSuccessfullyExecuteAllTestcases() 16 | { 17 | var testcasesDir = Path.GetFullPath($"{AppContext.BaseDirectory}{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}.." + 18 | $"{Path.DirectorySeparatorChar}Matching{Path.DirectorySeparatorChar}ResponseTests{Path.DirectorySeparatorChar}BodyTests{Path.DirectorySeparatorChar}Testcases{Path.DirectorySeparatorChar}"); 19 | 20 | var testcaseFiles = Directory.GetFiles(testcasesDir); 21 | 22 | var failedCases = new List(); 23 | 24 | foreach (var file in testcaseFiles) 25 | { 26 | var testcase = JsonConvert.DeserializeObject(File.ReadAllText(file)); 27 | if (file.Split(Path.DirectorySeparatorChar).Last().StartsWith("")) 28 | { 29 | List differences = BodyMatcher.Match(testcase.Expected.Body, testcase.Actual.Body, testcase.Expected.MatchingRules); 30 | if (differences.Any() == testcase.Match) 31 | { 32 | failedCases.Add(file.Split(Path.DirectorySeparatorChar).Last()); 33 | failedCases.AddRange(differences.Select(d => "- " + d)); 34 | } 35 | else if (testcase.ExpectedMessage != null && !differences.Contains(testcase.ExpectedMessage)) 36 | { 37 | failedCases.Add(file.Split(Path.DirectorySeparatorChar).Last()); 38 | failedCases.Add($"- Expected message was not returned. Expected: {testcase.ExpectedMessage}, actual: {differences.First()}"); 39 | } 40 | } 41 | } 42 | 43 | Assert.AreEqual(0, failedCases.Count, "Failed cases: " + Environment.NewLine + string.Join(Environment.NewLine, failedCases)); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/additional property with type matcher that does not match.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "additional property with type matcher wildcards that don't match", 4 | "expected": { 5 | "headers": {}, 6 | "body" : { 7 | "myPerson": { 8 | "name": "Any name" 9 | } 10 | }, 11 | "matchingRules" : { 12 | "body": { 13 | "$.myPerson.*": { 14 | "matchers": [ 15 | { 16 | "match": "type" 17 | } 18 | ] 19 | } 20 | } 21 | } 22 | }, 23 | "actual": { 24 | "headers": {}, 25 | "body": { 26 | "myPerson": { 27 | "name": 39, 28 | "age": 39, 29 | "nationality": "Australian" 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/additional property with type matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "additional property with type matcher wildcards", 4 | "expected": { 5 | "headers": {}, 6 | "body" : { 7 | "myPerson": { 8 | "name": "Any name" 9 | } 10 | }, 11 | "matchingRules" : { 12 | "body": { 13 | "$.myPerson.*": { 14 | "matchers": [ 15 | { 16 | "match": "type" 17 | } 18 | ] 19 | } 20 | } 21 | } 22 | }, 23 | "actual": { 24 | "headers": {}, 25 | "body": { 26 | "myPerson": { 27 | "name": "Jon Peterson", 28 | "age": "39", 29 | "nationality": "Australian" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/array at top level with matchers.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "top level array matches", 4 | "expected": { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body" : [ { 7 | "dob" : "06/11/2015", 8 | "name" : "Rogger the Dogger", 9 | "id" : 3380634027, 10 | "timestamp" : "2015-06-11T13:17:29" 11 | }, { 12 | "dob" : "06/11/2015", 13 | "name" : "Cat in the Hat", 14 | "id" : 1284270029, 15 | "timestamp" : "2015-06-11T13:17:29" 16 | } ], 17 | "matchingRules" : { 18 | "body": { 19 | "$[0].id": { 20 | "matchers": [ 21 | { 22 | "match": "type" 23 | } 24 | ] 25 | }, 26 | "$[1].id": { 27 | "matchers": [ 28 | { 29 | "match": "type" 30 | } 31 | ] 32 | }, 33 | "$[0].name": { 34 | "matchers": [ 35 | { 36 | "match": "type" 37 | } 38 | ] 39 | }, 40 | "$[1].name": { 41 | "matchers": [ 42 | { 43 | "match": "type" 44 | } 45 | ] 46 | }, 47 | "$[1].dob": { 48 | "matchers": [ 49 | { 50 | "match": "regex", 51 | "regex": "\\d{2}/\\d{2}/\\d{4}" 52 | } 53 | ] 54 | }, 55 | "$[1].timestamp": { 56 | "matchers": [ 57 | { 58 | "match": "regex", 59 | "regex": "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}" 60 | } 61 | ] 62 | }, 63 | "$[0].timestamp": { 64 | "matchers": [ 65 | { 66 | "match": "regex", 67 | "regex": "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}" 68 | } 69 | ] 70 | }, 71 | "$[0].dob": { 72 | "matchers": [ 73 | { 74 | "match": "regex", 75 | "regex": "\\d{2}/\\d{2}/\\d{4}" 76 | } 77 | ] 78 | } 79 | } 80 | } 81 | }, 82 | "actual": { 83 | "headers": {"Content-Type": "application/json"}, 84 | "body": [ 85 | { 86 | "dob": "11/06/2015", 87 | "name": "Bob The Builder", 88 | "id": 1234567890, 89 | "timestamp": "2000-06-10T20:41:37" 90 | }, 91 | { 92 | "dob": "12/10/2000", 93 | "name": "Slinky Malinky", 94 | "id": 6677889900, 95 | "timestamp": "2015-06-10T22:98:78" 96 | } 97 | ] 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/array at top level.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "top level array matches", 4 | "expected": { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": [ 7 | { 8 | "dob": "06/10/2015", 9 | "name": "Rogger the Dogger", 10 | "id": 1014753708, 11 | "timestamp": "2015-06-10T20:41:37" 12 | }, 13 | { 14 | "dob": "06/10/2015", 15 | "name": "Cat in the Hat", 16 | "id": 8858030303, 17 | "timestamp": "2015-06-10T20:41:37" 18 | } 19 | ] 20 | }, 21 | "actual": { 22 | "headers": {"Content-Type": "application/json"}, 23 | "body": [ 24 | { 25 | "dob": "06/10/2015", 26 | "name": "Rogger the Dogger", 27 | "id": 1014753708, 28 | "timestamp": "2015-06-10T20:41:37" 29 | }, 30 | { 31 | "dob": "06/10/2015", 32 | "name": "Cat in the Hat", 33 | "id": 8858030303, 34 | "timestamp": "2015-06-10T20:41:37" 35 | } 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/array in different order.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Favourite colours in wrong order", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "favouriteColours": ["red","blue"] 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "favouriteColours": ["blue", "red"] 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/array with regex matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "array with regex matcher", 4 | "expected": { 5 | "headers": {}, 6 | "body" : { 7 | "myDates": [ 8 | "29/10/2015" 9 | ] 10 | }, 11 | "matchingRules" : { 12 | "body": { 13 | "$.myDates": { 14 | "matchers": [ 15 | { 16 | "match": "type" 17 | } 18 | ] 19 | }, 20 | "$.myDates[*]": { 21 | "matchers": [ 22 | { 23 | "match": "regex", 24 | "regex": "\\d{2}/\\d{2}/\\d{4}" 25 | } 26 | ] 27 | } 28 | } 29 | } 30 | }, 31 | "actual": { 32 | "headers": {}, 33 | "body": { 34 | "myDates": [ 35 | "01/11/2010", 36 | "15/12/2014", 37 | "30/06/2015" 38 | ] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/array with type matcher mismatch.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "array with type matcher mismatch", 4 | "expected": { 5 | "headers": {}, 6 | "body" : { 7 | "myDates": [ 8 | 10 9 | ] 10 | }, 11 | "matchingRules" : { 12 | "body": { 13 | "$.myDates[*]": { 14 | "matchers": [ 15 | { 16 | "match": "type" 17 | } 18 | ] 19 | } 20 | } 21 | } 22 | }, 23 | "actual": { 24 | "headers": {}, 25 | "body": { 26 | "myDates": [ 27 | 20, 28 | 5, 29 | "100299" 30 | ] 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/array with type matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "array with type matcher", 4 | "expected": { 5 | "headers": {}, 6 | "body" : { 7 | "myDates": [ 8 | 10 9 | ] 10 | }, 11 | "matchingRules" : { 12 | "body": { 13 | "$.myDates": { 14 | "matchers": [ 15 | { 16 | "match": "type" 17 | } 18 | ] 19 | }, 20 | "$.myDates[*]": { 21 | "matchers": [ 22 | { 23 | "match": "type" 24 | } 25 | ] 26 | } 27 | } 28 | } 29 | }, 30 | "actual": { 31 | "headers": {}, 32 | "body": { 33 | "myDates": [ 34 | 20, 35 | 5, 36 | 1910 37 | ] 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/compact - actual type does not match expected type.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "actual type does not match expected type", 4 | "expected": { 5 | "headers": {}, 6 | "body": "test" 7 | }, 8 | "actual": { 9 | "headers": {}, 10 | "body": { 11 | "myPerson": { 12 | "name": " test" 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/compact - additional child element different type.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "additional child element different type", 4 | "expected": { 5 | "headers": {}, 6 | "body": { 7 | "myObjects": [ 8 | { "letter": "a" } 9 | ] 10 | }, 11 | "matchingRules": { 12 | "body": { 13 | "$.myObjects": { 14 | "matchers": [ 15 | { 16 | "match": "min", 17 | "min": 1 18 | } 19 | ] 20 | }, 21 | "$.myObjects[*].*": { 22 | "matchers": [ 23 | { 24 | "match": "type" 25 | } 26 | ] 27 | }, 28 | "$.myObjects[*].letter": { 29 | "matchers": [ 30 | { 31 | "match": "type" 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | }, 38 | "actual": { 39 | "headers": {}, 40 | "body": { 41 | "myObjects": [ 42 | { 43 | "letter": "a" 44 | }, 45 | { 46 | "letter": "a", 47 | "number": 2 48 | } 49 | ] 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/compact - additional child element different type2.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "compact - additional child element different type 2", 4 | "expected": { 5 | "headers": {}, 6 | "body": { 7 | "myObjects": [ 8 | { 9 | "letter": "a", 10 | "number": 1 11 | } 12 | ] 13 | }, 14 | "matchingRules": { 15 | "body": { 16 | "$.myObjects": { 17 | "matchers": [ 18 | { 19 | "match": "min", 20 | "min": 1 21 | } 22 | ] 23 | }, 24 | "$.myObjects[*].*": { 25 | "matchers": [ 26 | { 27 | "match": "type" 28 | } 29 | ] 30 | }, 31 | "$.myObjects[*].letter": { 32 | "matchers": [ 33 | { 34 | "match": "type" 35 | } 36 | ] 37 | } 38 | } 39 | } 40 | }, 41 | "actual": { 42 | "headers": {}, 43 | "body": { 44 | "myObjects": [ 45 | { 46 | "letter": "a", 47 | "number": 1 48 | }, 49 | { 50 | "letter": "a", 51 | "number": 2, 52 | "otherNumber": 1 53 | } 54 | ] 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/compact - array of objects with type matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "array of objects with type matcher", 4 | "expected": { 5 | "headers": {}, 6 | "body": { 7 | "myObjects": [ 8 | { "letter": "a" } 9 | ] 10 | }, 11 | "matchingRules": { 12 | "body": { 13 | "$.myObjects": { 14 | "matchers": [ 15 | { 16 | "match": "min", 17 | "min": 1 18 | } 19 | ] 20 | }, 21 | "$.myObjects[*].letter": { 22 | "matchers": [ 23 | { 24 | "match": "type" 25 | } 26 | ] 27 | } 28 | } 29 | } 30 | }, 31 | "actual": { 32 | "headers": {}, 33 | "body": { 34 | "myObjects": [ 35 | { "letter": "a" }, 36 | { "letter": 2 } 37 | ] 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/compact - datetime matching regex.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "iso datetime should be matched as a string, not as a datetime", 4 | "expected": { 5 | "headers": {}, 6 | "body": "2020-06-05T13:22:59", 7 | "matchingRules": { 8 | "body": { 9 | "$": { 10 | "matchers": [ 11 | { 12 | "match": "regex", 13 | "regex": "^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24\\:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$" 14 | } 15 | ] 16 | } 17 | } 18 | } 19 | }, 20 | "actual": { 21 | "headers": {}, 22 | "body": "1999-04-01T00:00:00" 23 | } 24 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/compact - datetime not matching regex.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "iso datetime should be matched as a string, not as a datetime", 4 | "expected": { 5 | "headers": {}, 6 | "body": "2020-06-05T13:22:59", 7 | "matchingRules": { 8 | "body": { 9 | "$": { 10 | "matchers": [ 11 | { 12 | "match": "regex", 13 | "regex": "2020.*" 14 | } 15 | ] 16 | } 17 | } 18 | } 19 | }, 20 | "actual": { 21 | "headers": {}, 22 | "body": "1999-04-01T00:00:00" 23 | } 24 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/compact - empty matchingrules object.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "empty matchingrules object", 4 | "expected": { 5 | "headers": {}, 6 | "body": { 7 | "myPerson": { 8 | "name": "name" 9 | } 10 | }, 11 | "matchingRules": { 12 | } 13 | }, 14 | "actual": { 15 | "headers": {}, 16 | "body": { 17 | "myPerson": { 18 | "name": "name" 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/compact - type matcher on decimal allows integer.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "type matcher on integer allows decimal", 4 | "expected": { 5 | "headers": { "Content-Type": "application/json" }, 6 | "matchingRules": { 7 | "body": { 8 | "$.alligator.name": { 9 | "matchers": [ 10 | { 11 | "match": "type" 12 | } 13 | ] 14 | }, 15 | "$.alligator.length": { 16 | "matchers": [ 17 | { 18 | "match": "type" 19 | } 20 | ] 21 | } 22 | } 23 | }, 24 | "body": { 25 | "alligator": { 26 | "name": "Mary", 27 | "length": 4.2, 28 | "favouriteColours": [ "red", "blue" ] 29 | } 30 | } 31 | }, 32 | "actual": { 33 | "headers": { "Content-Type": "application/json" }, 34 | "body": { 35 | "alligator": { 36 | "length": 5, 37 | "name": "Harry the very long alligator", 38 | "favouriteColours": [ "red", "blue" ] 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/compact - type matcher on integer allows decimal.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "type matcher on decimal allows integer", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "matchingRules": { 7 | "body": { 8 | "$.alligator.name": { 9 | "matchers": [ 10 | { 11 | "match": "type" 12 | } 13 | ] 14 | }, 15 | "$.alligator.length": { 16 | "matchers": [ 17 | { 18 | "match": "type" 19 | } 20 | ] 21 | } 22 | } 23 | }, 24 | "body": { 25 | "alligator":{ 26 | "name": "Mary", 27 | "length": 4.2, 28 | "favouriteColours": ["red","blue"] 29 | } 30 | } 31 | }, 32 | "actual": { 33 | "headers": {"Content-Type": "application/json"}, 34 | "body": { 35 | "alligator": { 36 | "length": 5, 37 | "name": "Harry the very long alligator", 38 | "favouriteColours": [ "red", "blue" ] 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/decimal matcher does not match.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "response does not match because it does not contain a decimal", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "matchingRules": { 7 | "body": { 8 | "$.alligator.name": { 9 | "matchers": [ 10 | { 11 | "match": "type" 12 | } 13 | ] 14 | }, 15 | "$.alligator.length": { 16 | "matchers": [ 17 | { 18 | "match": "decimal" 19 | } 20 | ] 21 | } 22 | } 23 | }, 24 | "body": { 25 | "alligator":{ 26 | "name": "Mary", 27 | "length": 4.2, 28 | "favouriteColours": ["red","blue"] 29 | } 30 | } 31 | }, 32 | "actual": { 33 | "headers": {"Content-Type": "application/json"}, 34 | "body": { 35 | "alligator": { 36 | "length": 5, 37 | "name": "Harry the very long alligator", 38 | "favouriteColours": [ "red", "blue" ] 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/decimal matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "response matches because it contains a decimal", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "matchingRules": { 7 | "body": { 8 | "$.alligator.name": { 9 | "matchers": [ 10 | { 11 | "match": "type" 12 | } 13 | ] 14 | }, 15 | "$.alligator.length": { 16 | "matchers": [ 17 | { 18 | "match": "decimal" 19 | } 20 | ] 21 | } 22 | } 23 | }, 24 | "body": { 25 | "alligator":{ 26 | "name": "Mary", 27 | "length": 4.2, 28 | "favouriteColours": ["red","blue"] 29 | } 30 | } 31 | }, 32 | "actual": { 33 | "headers": {"Content-Type": "application/json"}, 34 | "body": { 35 | "alligator": { 36 | "length": 5.0, 37 | "name": "Harry the very long alligator", 38 | "favouriteColours": [ "red", "blue" ] 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/deeply nested objects.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Comparisons should work even on nested objects", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "object1": { 8 | "object2": { 9 | "object4": { 10 | "object5": { 11 | "name": "Mary", 12 | "friends": ["Fred", "John"] 13 | }, 14 | "object6": { 15 | "phoneNumber": 1234567890 16 | } 17 | } 18 | } 19 | } 20 | } 21 | }, 22 | "actual": { 23 | "headers": {"Content-Type": "application/json"}, 24 | "body": { 25 | "object1":{ 26 | "object2": { 27 | "object4":{ 28 | "object5": { 29 | "name": "Mary", 30 | "friends": ["Fred", "John"], 31 | "gender": "F" 32 | }, 33 | "object6": { 34 | "phoneNumber": 1234567890 35 | } 36 | } 37 | }, 38 | "color": "red" 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/different value found at index.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Incorrect favourite colour", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "favouriteColours": ["red","blue"] 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "favouriteColours": ["red","taupe"] 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/different value found at key.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Incorrect value at alligator name", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "name": "Mary" 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "name": "Fred" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/empty body no content type.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Empty body, no content-type", 4 | "expected" : { 5 | "body": "" 6 | }, 7 | "actual": { 8 | "headers": {"Content-Type": "application/json"}, 9 | "body": "" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/empty body.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Empty body", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": "" 7 | }, 8 | "actual": { 9 | "headers": {"Content-Type": "application/json"}, 10 | "body": "" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/equality matcher overrides type matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "equality matcher overrides type matcher", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "matchingRules": { 7 | "body": { 8 | "$.alligator": { 9 | "matchers": [ 10 | { 11 | "match": "type" 12 | } 13 | ] 14 | }, 15 | "$.alligator.feet": { 16 | "matchers": [ 17 | { 18 | "match": "equality" 19 | } 20 | ] 21 | } 22 | } 23 | }, 24 | "body": { 25 | "alligator":{ 26 | "name": "Mary", 27 | "feet": 4, 28 | "favouriteColours": ["red","blue"] 29 | } 30 | } 31 | }, 32 | "actual": { 33 | "headers": {"Content-Type": "application/json"}, 34 | "body": { 35 | "alligator":{ 36 | "feet": 5, 37 | "name": "Harry the very hungry alligator with an extra foot", 38 | "favouriteColours": ["red","blue"] 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/include matcher does not match.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "actual value does not include the value associated with the matcher", 4 | "expected": { 5 | "headers": { "Content-Type": "application/json" }, 6 | "matchingRules": { 7 | "body": { 8 | "$.alligator.description": { 9 | "matchers": [ 10 | { 11 | "match": "include", 12 | "value": "Mary" 13 | } 14 | ] 15 | } 16 | } 17 | }, 18 | "body": { 19 | "alligator": { 20 | "description": "An alligator called Mary" 21 | } 22 | } 23 | }, 24 | "actual": { 25 | "headers": { "Content-Type": "application/json" }, 26 | "body": { 27 | "alligator": { 28 | "description": "Harry the very hungry alligator with an extra foot" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/include matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "actual value includes the value associated with the matcher", 4 | "expected": { 5 | "headers": { "Content-Type": "application/json" }, 6 | "matchingRules": { 7 | "body": { 8 | "$.alligator.description": { 9 | "matchers": [ 10 | { 11 | "match": "include", 12 | "value": "Harry" 13 | } 14 | ] 15 | } 16 | } 17 | }, 18 | "body": { 19 | "alligator": { 20 | "description": "An alligator called Harry" 21 | } 22 | } 23 | }, 24 | "actual": { 25 | "headers": { "Content-Type": "application/json" }, 26 | "body": { 27 | "alligator": { 28 | "description": "Harry the very hungry alligator with an extra foot" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/integer matcher does not match.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "response does not match because it does not contain an integer", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "matchingRules": { 7 | "body": { 8 | "$.alligator.name": { 9 | "matchers": [ 10 | { 11 | "match": "type" 12 | } 13 | ] 14 | }, 15 | "$.alligator.feet": { 16 | "matchers": [ 17 | { 18 | "match": "integer" 19 | } 20 | ] 21 | } 22 | } 23 | }, 24 | "body": { 25 | "alligator":{ 26 | "name": "Mary", 27 | "feet": 4, 28 | "favouriteColours": ["red","blue"] 29 | } 30 | } 31 | }, 32 | "actual": { 33 | "headers": {"Content-Type": "application/json"}, 34 | "body": { 35 | "alligator":{ 36 | "feet": 4.5, 37 | "name": "Harry the very hungry alligator with half an extra foot", 38 | "favouriteColours": ["red","blue"] 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/integer matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "response matches because it contains an integer", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "matchingRules": { 7 | "body": { 8 | "$.alligator.name": { 9 | "matchers": [ 10 | { 11 | "match": "type" 12 | } 13 | ] 14 | }, 15 | "$.alligator.feet": { 16 | "matchers": [ 17 | { 18 | "match": "integer" 19 | } 20 | ] 21 | } 22 | } 23 | }, 24 | "body": { 25 | "alligator":{ 26 | "name": "Mary", 27 | "feet": 4, 28 | "favouriteColours": ["red","blue"] 29 | } 30 | } 31 | }, 32 | "actual": { 33 | "headers": {"Content-Type": "application/json"}, 34 | "body": { 35 | "alligator":{ 36 | "feet": 5, 37 | "name": "Harry the very hungry alligator with an extra foot", 38 | "favouriteColours": ["red","blue"] 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/keys out of order match.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Favourite number and favourite colours out of order", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "favouriteNumber": 7, 8 | "favouriteColours": ["red","blue"] 9 | } 10 | }, 11 | "actual": { 12 | "headers": {"Content-Type": "application/json"}, 13 | "body": { 14 | "favouriteColours": ["red","blue"], 15 | "favouriteNumber": 7 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/matches with floats.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Response match with floats", 4 | "expected": { 5 | "headers": {"Content-Type": "application/json"}, 6 | "matchingRules": { 7 | "body": { 8 | "$.product.price": { 9 | "matchers": [ 10 | { 11 | "match": "regex", 12 | "regex": "\\d(\\.\\d{1,2})" 13 | } 14 | ] 15 | } 16 | } 17 | }, 18 | "body": [ 19 | { 20 | "product": { 21 | "id": 123, 22 | "description": "Television", 23 | "price": 500.55 24 | } 25 | } 26 | ] 27 | }, 28 | "actual": { 29 | "headers": {"Content-Type": "application/json"}, 30 | "body": [ 31 | { 32 | "product": { 33 | "id": 123, 34 | "description": "Television", 35 | "price": 500.55 36 | } 37 | } 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/matches with integers.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Response match with integers", 4 | "expected" : { 5 | "method": "POST", 6 | "path": "/", 7 | "query": {}, 8 | "headers": {"Content-Type": "application/json"}, 9 | "matchingRules": { 10 | "body": { 11 | "$.alligator.feet": { 12 | "matchers": [ 13 | { 14 | "match": "regex", 15 | "regex": "[0-9]" 16 | } 17 | ] 18 | } 19 | } 20 | }, 21 | "body": { 22 | "alligator":{ 23 | "name": "Mary", 24 | "feet": 4, 25 | "favouriteColours": ["red","blue"] 26 | } 27 | } 28 | }, 29 | "actual": { 30 | "method": "POST", 31 | "path": "/", 32 | "query": {}, 33 | "headers": {"Content-Type": "application/json"}, 34 | "body": { 35 | "alligator":{ 36 | "feet": 4, 37 | "name": "Mary", 38 | "favouriteColours": ["red","blue"] 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/matches with regex.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Requests match with regex", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "matchingRules": { 7 | "body": { 8 | "$.alligator.name": { 9 | "matchers": [ 10 | { 11 | "match": "regex", 12 | "regex": "\\w+" 13 | } 14 | ] 15 | } 16 | } 17 | }, 18 | "body": { 19 | "alligator":{ 20 | "name": "Mary", 21 | "feet": 4, 22 | "favouriteColours": ["red","blue"] 23 | } 24 | } 25 | }, 26 | "actual": { 27 | "headers": {"Content-Type": "application/json"}, 28 | "body": { 29 | "alligator":{ 30 | "feet": 4, 31 | "name": "Harry", 32 | "favouriteColours": ["red","blue"] 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/matches with type.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Response match with same type", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "matchingRules": { 7 | "body": { 8 | "$.alligator.name": { 9 | "matchers": [ 10 | { 11 | "match": "type" 12 | } 13 | ] 14 | }, 15 | "$.alligator.feet": { 16 | "matchers": [ 17 | { 18 | "match": "type" 19 | } 20 | ] 21 | } 22 | } 23 | }, 24 | "body": { 25 | "alligator":{ 26 | "name": "Mary", 27 | "feet": 4, 28 | "favouriteColours": ["red","blue"] 29 | } 30 | } 31 | }, 32 | "actual": { 33 | "headers": {"Content-Type": "application/json"}, 34 | "body": { 35 | "alligator":{ 36 | "feet": 5, 37 | "name": "Harry the very hungry alligator with an extra foot", 38 | "favouriteColours": ["red","blue"] 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/matches.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Responses match", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "name": "Mary", 9 | "feet": 4, 10 | "favouriteColours": ["red","blue"] 11 | } 12 | } 13 | }, 14 | "actual": { 15 | "headers": {"Content-Type": "application/json"}, 16 | "body": { 17 | "alligator":{ 18 | "feet": 4, 19 | "name": "Mary", 20 | "favouriteColours": ["red","blue"] 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/missing body found when empty expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Missing body found, when an empty body was expected", 4 | "expected" : { 5 | "body": null 6 | }, 7 | "actual": { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/missing body no content type.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Missing body, no content-type", 4 | "expected" : { 5 | }, 6 | "actual": { 7 | "headers": {"Content-Type": "application/json"}, 8 | "body": { 9 | "alligator":{ 10 | "feet": 4, 11 | "name": "Mary", 12 | "favouriteColours": ["red","blue"] 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/missing body.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Missing body", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"} 6 | }, 7 | "actual": { 8 | "headers": {"Content-Type": "application/json"}, 9 | "body": { 10 | "alligator":{ 11 | "feet": 4, 12 | "name": "Mary", 13 | "favouriteColours": ["red","blue"] 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/missing index.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Missing favorite colour", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "favouriteColours": ["red","blue"] 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator": { 16 | "favouriteColours": ["red"] 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/missing key.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Missing key alligator name", 4 | "expected": { 5 | "headers": { "Content-Type": "application/json" }, 6 | "body": { 7 | "alligator": { 8 | "name": "Mary", 9 | "age": 3 10 | } 11 | } 12 | }, 13 | "actual": { 14 | "headers": { "Content-Type": "application/json" }, 15 | "body": { 16 | "alligator": { 17 | "age": 3 18 | } 19 | } 20 | }, 21 | "expectedMessage": "Property 'alligator.name' was not present in the actual response." 22 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/no body no content type.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "No body, no content-type", 4 | "expected" : { 5 | }, 6 | "actual": { 7 | "headers": {"Content-Type": "application/json"}, 8 | "body": { 9 | "alligator":{ 10 | "feet": 4, 11 | "name": "Mary", 12 | "favouriteColours": ["red","blue"] 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/not null found at key when null expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Name should be null", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "name": null 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "name": "Fred" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/not null found in array when null expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Favourite numbers expected to contain null, but not null found", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "favouriteNumbers": ["1",null,"3"] 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "favouriteNumbers": ["1","2","3"] 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/null body no content type.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "NULL body, no content-type", 4 | "expected" : { 5 | "body": null 6 | }, 7 | "actual": { 8 | "headers": {"Content-Type": "application/json"}, 9 | "body": null 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/null body.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "NULL body", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": null 7 | }, 8 | "actual": { 9 | "headers": {"Content-Type": "application/json"}, 10 | "body": null 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/null found at key where not null expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Name should not be null", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "name": "Mary" 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "name": null 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/null found in array when not null expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Favourite numbers expected to be strings found a null", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "favouriteNumbers": ["1","2","3"] 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "favouriteNumbers": ["1",null,"3"] 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/null matcher does not match when key not present.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Response does not match because it does not contain null", 4 | "expected": { 5 | "headers": { "Content-Type": "application/json" }, 6 | "matchingRules": { 7 | "body": { 8 | "$.alligator.name": { 9 | "matchers": [ 10 | { 11 | "match": "type" 12 | } 13 | ] 14 | }, 15 | "$.alligator.wings": { 16 | "matchers": [ 17 | { 18 | "match": "null" 19 | } 20 | ] 21 | } 22 | } 23 | }, 24 | "body": { 25 | "alligator": { 26 | "name": "Mary", 27 | "wings": null, 28 | "favouriteColours": [ "red", "blue" ] 29 | } 30 | } 31 | }, 32 | "actual": { 33 | "headers": { "Content-Type": "application/json" }, 34 | "body": { 35 | "alligator": { 36 | "name": "Harry", 37 | "favouriteColours": [ "red", "blue" ] 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/null matcher does not match.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Response does not match because it does not contain null", 4 | "expected": { 5 | "headers": { "Content-Type": "application/json" }, 6 | "matchingRules": { 7 | "body": { 8 | "$.alligator.name": { 9 | "matchers": [ 10 | { 11 | "match": "type" 12 | } 13 | ] 14 | }, 15 | "$.alligator.wings": { 16 | "matchers": [ 17 | { 18 | "match": "null" 19 | } 20 | ] 21 | } 22 | } 23 | }, 24 | "body": { 25 | "alligator": { 26 | "name": "Mary", 27 | "wings": null, 28 | "favouriteColours": [ "red", "blue" ] 29 | } 30 | } 31 | }, 32 | "actual": { 33 | "headers": { "Content-Type": "application/json" }, 34 | "body": { 35 | "alligator": { 36 | "wings": 2, 37 | "name": "Harry", 38 | "favouriteColours": [ "red", "blue" ] 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/null matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Response matches because it contains null", 4 | "expected": { 5 | "headers": { "Content-Type": "application/json" }, 6 | "matchingRules": { 7 | "body": { 8 | "$.alligator.name": { 9 | "matchers": [ 10 | { 11 | "match": "type" 12 | } 13 | ] 14 | }, 15 | "$.alligator.wings": { 16 | "matchers": [ 17 | { 18 | "match": "null" 19 | } 20 | ] 21 | } 22 | } 23 | }, 24 | "body": { 25 | "alligator": { 26 | "name": "Mary", 27 | "wings": null, 28 | "favouriteColours": [ "red", "blue" ] 29 | } 30 | } 31 | }, 32 | "actual": { 33 | "headers": { "Content-Type": "application/json" }, 34 | "body": { 35 | "alligator": { 36 | "wings": null, 37 | "name": "Harry", 38 | "favouriteColours": [ "red", "blue" ] 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/number found at key when string expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Number of feet expected to be string but was number", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "feet": "4" 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "feet": 4 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/number found in array when string expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Favourite numbers expected to be strings found a number", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "favouriteNumbers": ["1","2","3"] 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "favouriteNumbers": ["1",2,"3"] 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/objects in array first matches.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Properties match but unexpected element received", 4 | "expected": { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": [ 7 | { 8 | "favouriteColor": "red" 9 | } 10 | ] 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": [ 15 | { 16 | "favouriteColor": "red", 17 | "favouriteNumber": 2 18 | }, 19 | { 20 | "favouriteColor": "blue", 21 | "favouriteNumber": 2 22 | } 23 | ] 24 | } 25 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/objects in array no matches.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Array of objects, properties match on incorrect objects", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": [ 7 | {"favouriteColor": "red"}, 8 | {"favouriteNumber": 2} 9 | ] 10 | }, 11 | "actual": { 12 | "headers": {"Content-Type": "application/json"}, 13 | "body": [ 14 | {"favouriteColor": "blue", 15 | "favouriteNumber": 4}, 16 | {"favouriteColor": "red", 17 | "favouriteNumber": 2} 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/objects in array second matches.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Property of second object matches, but unexpected element recieved", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": [ 7 | {"favouriteColor": "red"} 8 | ] 9 | }, 10 | "actual": { 11 | "headers": {"Content-Type": "application/json"}, 12 | "body": [ 13 | {"favouriteColor": "blue", 14 | "favouriteNumber": 4}, 15 | {"favouriteColor": "red", 16 | "favouriteNumber": 2} 17 | ] 18 | } 19 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/objects in array type matching.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "objects in array type matching", 4 | "expected": { 5 | "headers": {}, 6 | "body": [{ 7 | "name": "John Smith", 8 | "age": 50 9 | }], 10 | "matchingRules": { 11 | "body": { 12 | "$": { 13 | "matchers": [ 14 | { 15 | "match": "type" 16 | } 17 | ] 18 | }, 19 | "$[*]": { 20 | "matchers": [ 21 | { 22 | "match": "type" 23 | } 24 | ] 25 | } 26 | } 27 | } 28 | }, 29 | "actual": { 30 | "headers": {}, 31 | "body": [{ 32 | "name": "Peter Peterson", 33 | "age": 22, 34 | "gender": "Male" 35 | }, { 36 | "name": "John Johnston", 37 | "age": 64 38 | }] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/objects in array with type mismatching.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "objects in array with type mismatching", 4 | "expected": { 5 | "headers": {}, 6 | "body": [{ 7 | "Name": "John Smith", 8 | "Age": 50 9 | }], 10 | "matchingRules": { 11 | "body": { 12 | "$[*]": { 13 | "matchers": [ 14 | { 15 | "match": "type" 16 | } 17 | ] 18 | }, 19 | "$[*].*": { 20 | "matchers": [ 21 | { 22 | "match": "type" 23 | } 24 | ] 25 | } 26 | } 27 | } 28 | }, 29 | "actual": { 30 | "headers": {}, 31 | "body": [{ 32 | "name": "Peter Peterson", 33 | "age": 22, 34 | "gender": "Male" 35 | }, {}] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/plain text empty body.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Plain text that matches", 4 | "expected" : { 5 | "headers": { "Content-Type": "text/plain" }, 6 | "body": "" 7 | }, 8 | "actual": { 9 | "headers": { "Content-Type": "text/plain" }, 10 | "body": "" 11 | 12 | } 13 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/plain text missing body.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Plain text that matches", 4 | "expected" : { 5 | "headers": { "Content-Type": "text/plain" } 6 | }, 7 | "actual": { 8 | "headers": { "Content-Type": "text/plain" } 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/plain text regex matching missing body.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Plain text that matches", 4 | "expected" : { 5 | "headers": { "Content-Type": "text/plain" }, 6 | "body": "alligator named mary", 7 | "matchingRules": { 8 | "body": { 9 | "$": { 10 | "matchers": [ 11 | { 12 | "match": "regex", 13 | "regex": "alligator named .{4}" 14 | } 15 | ] 16 | } 17 | } 18 | } 19 | }, 20 | "actual": { 21 | "headers": { "Content-Type": "text/plain" } 22 | } 23 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/plain text regex matching that does not match.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Plain text that matches", 4 | "expected" : { 5 | "headers": { "Content-Type": "text/plain" }, 6 | "body": "alligator named mary", 7 | "matchingRules": { 8 | "body": { 9 | "$": { 10 | "matchers": [ 11 | { 12 | "match": "regex", 13 | "regex": "alligator named .{4}" 14 | } 15 | ] 16 | } 17 | } 18 | } 19 | }, 20 | "actual": { 21 | "headers": { "Content-Type": "text/plain" }, 22 | "body": "alligator named brent" 23 | } 24 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/plain text regex matching.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Plain text that matches", 4 | "expected" : { 5 | "headers": { "Content-Type": "text/plain" }, 6 | "body": "alligator named mary", 7 | "matchingRules": { 8 | "body": { 9 | "$": { 10 | "matchers": [ 11 | { 12 | "match": "regex", 13 | "regex": "alligator.*" 14 | } 15 | ] 16 | } 17 | } 18 | } 19 | }, 20 | "actual": { 21 | "headers": { "Content-Type": "text/plain" }, 22 | "body": "alligator named brent" 23 | } 24 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/plain text that does not match.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Plain text that does not match", 4 | "expected" : { 5 | "headers": { "Content-Type": "text/plain" }, 6 | "body": "alligator named mary" 7 | }, 8 | "actual": { 9 | "headers": { "Content-Type": "text/plain" }, 10 | "body": "alligator named fred" 11 | } 12 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/plain text that matches.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Plain text that matches", 4 | "expected" : { 5 | "headers": { "Content-Type": "text/plain" }, 6 | "body": "alligator named mary" 7 | }, 8 | "actual": { 9 | "headers": { "Content-Type": "text/plain" }, 10 | "body": "alligator named mary" 11 | } 12 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/property name is different case.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Property names on objects are case sensitive", 4 | "expected": { 5 | "headers": { "Content-Type": "application/json" }, 6 | "body": { 7 | "alligator": { 8 | "FavouriteColour": "red" 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": { "Content-Type": "application/json" }, 14 | "body": { 15 | "alligator": { 16 | "favouritecolour": "red" 17 | } 18 | } 19 | }, 20 | "expectedMessage": "A property with a name like 'alligator.FavouriteColour' was present in the actual response, but the case did not match. Note that Pact is case sensitive." 21 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/string found at key when number expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Number of feet expected to be number but was string", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "feet": 4 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "feet": "4" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/string found in array when number expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Favourite Numbers expected to be numbers, but 2 is a string", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "favouriteNumbers": [1,2,3] 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "favouriteNumbers": [1,"2",3] 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/unexpected index with not null value.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Unexpected favourite colour", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "favouriteColours": ["red","blue"] 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "favouriteColours": ["red","blue","taupe"] 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/unexpected index with null value.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Unexpected favourite colour with null value", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "favouriteColours": ["red","blue"] 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "favouriteColours": ["red","blue", null] 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/unexpected key with not null value.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Unexpected phone number", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "name": "Mary" 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "name": "Mary", 17 | "phoneNumber": "12345678" 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/BodyTests/Testcases/unexpected key with null value.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Unexpected phone number with null value", 4 | "expected" : { 5 | "headers": {"Content-Type": "application/json"}, 6 | "body": { 7 | "alligator":{ 8 | "name": "Mary" 9 | } 10 | } 11 | }, 12 | "actual": { 13 | "headers": {"Content-Type": "application/json"}, 14 | "body": { 15 | "alligator":{ 16 | "name": "Mary", 17 | "phoneNumber": null 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/HeaderTests/HeaderMatchingTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using Newtonsoft.Json; 6 | using System.Linq; 7 | 8 | namespace ComPact.UnitTests.Matching.ResponseTests.HeaderTests 9 | { 10 | [TestClass] 11 | public class HeaderMatchingTests 12 | { 13 | [TestMethod] 14 | public void ShouldSuccessfullyExecuteAllTestcase() 15 | { 16 | var testcasesDir = Path.GetFullPath($"{AppContext.BaseDirectory}{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}.." + 17 | $"{Path.DirectorySeparatorChar}Matching{Path.DirectorySeparatorChar}ResponseTests{Path.DirectorySeparatorChar}HeaderTests{Path.DirectorySeparatorChar}Testcases{Path.DirectorySeparatorChar}"); 18 | 19 | var testcaseFiles = Directory.GetFiles(testcasesDir); 20 | 21 | var failedCases = new List(); 22 | 23 | foreach(var file in testcaseFiles) 24 | { 25 | var testcase = JsonConvert.DeserializeObject(File.ReadAllText(file)); 26 | var differences = testcase.Expected.Headers.Match(testcase.Actual.Headers, testcase.Expected.MatchingRules); 27 | if (differences.Any() == testcase.Match) 28 | { 29 | failedCases.Add(testcase.Comment); 30 | } 31 | } 32 | 33 | Assert.AreEqual(0, failedCases.Count, "Failed cases: " + Environment.NewLine + string.Join(Environment.NewLine, failedCases)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/HeaderTests/Testcases/content type parameters do not match.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Headers don't match when the parameters are different", 4 | "expected" : { 5 | "headers": { 6 | "Content-Type": "application/json; charset=UTF-16" 7 | } 8 | }, 9 | "actual": { 10 | "headers": { 11 | "Content-Type": "application/json; charset=UTF-8" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/HeaderTests/Testcases/empty headers.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Empty headers match", 4 | "expected" : { 5 | "headers": {} 6 | }, 7 | "actual": { 8 | "headers": {} 9 | } 10 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/HeaderTests/Testcases/header name is different case.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Header name is case insensitive", 4 | "expected" : { 5 | "headers": { 6 | "Accept": "alligators" 7 | } 8 | }, 9 | "actual": { 10 | "headers": { 11 | "ACCEPT": "alligators" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/HeaderTests/Testcases/header value is different case.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Headers values are case sensitive", 4 | "expected" : { 5 | "headers": { 6 | "Accept": "alligators" 7 | } 8 | }, 9 | "actual": { 10 | "headers": { 11 | "Accept": "Alligators" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/HeaderTests/Testcases/matches content type with charset.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Headers match when the actual includes additional parameters", 4 | "expected" : { 5 | "headers": { 6 | "Content-Type": "application/json" 7 | } 8 | }, 9 | "actual": { 10 | "headers": { 11 | "Content-Type": "application/json; charset=UTF-8" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/HeaderTests/Testcases/matches content type with parameters in different order.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Headers match when the content type parameters are in a different order", 4 | "expected" : { 5 | "headers": { 6 | "Content-Type": "Text/x-Okie; charset=iso-8859-1;\n declaration=\"<950118.AEB0@XIson.com>\"" 7 | } 8 | }, 9 | "actual": { 10 | "headers": { 11 | "Content-Type": "Text/x-Okie; declaration=\"<950118.AEB0@XIson.com>\";\n charset=iso-8859-1" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/HeaderTests/Testcases/matches with regex.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Headers match with regex", 4 | "expected" : { 5 | "headers": { 6 | "Accept": "alligators", 7 | "Content-Type": "hippos" 8 | }, 9 | "matchingRules": { 10 | "header": { 11 | "Accept": { 12 | "matchers": [ 13 | { 14 | "match": "regex", 15 | "regex": "\\w+" 16 | } 17 | ] 18 | } 19 | } 20 | } 21 | }, 22 | "actual": { 23 | "headers": { 24 | "Content-Type": "hippos", 25 | "Accept": "godzilla" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/HeaderTests/Testcases/matches.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Headers match", 4 | "expected" : { 5 | "headers": { 6 | "Accept": "alligators", 7 | "Content-Type": "hippos" 8 | } 9 | }, 10 | "actual": { 11 | "headers": { 12 | "Content-Type": "hippos", 13 | "Accept": "alligators" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/HeaderTests/Testcases/order of comma separated header values different.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false, 3 | "comment": "Comma separated headers out of order, order can matter http://tools.ietf.org/html/rfc2616", 4 | "expected" : { 5 | "headers": { 6 | "Accept": "alligators, hippos" 7 | } 8 | }, 9 | "actual": { 10 | "headers": { 11 | "Accept": "hippos, alligators" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/HeaderTests/Testcases/unexpected header found.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Extra headers allowed", 4 | "expected" : { 5 | "headers": {} 6 | }, 7 | "actual": { 8 | "headers": { 9 | "Accept": "alligators" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/HeaderTests/Testcases/whitespace after comma different.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true, 3 | "comment": "Whitespace between comma separated headers does not matter", 4 | "expected" : { 5 | "headers": { 6 | "Accept": "alligators,hippos" 7 | } 8 | }, 9 | "actual": { 10 | "headers": { 11 | "Accept": "alligators, hippos" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /ComPact.UnitTests/Matching/ResponseTests/Testcase.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | 3 | namespace ComPact.UnitTests.Matching.ResponseTests 4 | { 5 | internal class Testcase 6 | { 7 | public bool Match { get; set; } 8 | public string Comment { get; set; } 9 | public Expected Expected { get; set; } 10 | public Actual Actual { get; set; } 11 | public string ExpectedMessage { get; set; } 12 | } 13 | 14 | internal class Expected 15 | { 16 | public Headers Headers { get; set; } 17 | public dynamic Body { get; set; } 18 | public MatchingRuleCollection MatchingRules { get; set; } 19 | } 20 | 21 | internal class Actual 22 | { 23 | public Headers Headers { get; set; } 24 | public dynamic Body { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ComPact.UnitTests/MockProvider/MatchableInteractionListTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Exceptions; 2 | using ComPact.MockProvider; 3 | using ComPact.Models; 4 | using ComPact.Models.V3; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using System.Collections.Generic; 7 | 8 | namespace ComPact.UnitTests.MockProvider 9 | { 10 | [TestClass] 11 | public class MatchableInteractionListTests 12 | { 13 | [TestMethod] 14 | public void ShoudlBeAbleToAddUniqueInteraction() 15 | { 16 | var interaction = new Interaction(); 17 | var differentInteraction = new Interaction { Request = new Request { Path = "/test" } }; 18 | var list = new MatchableInteractionList { new MatchableInteraction(interaction) }; 19 | 20 | list.AddUnique(new MatchableInteraction(differentInteraction)); 21 | 22 | Assert.AreEqual(2, list.Count); 23 | } 24 | 25 | [TestMethod] 26 | public void ShoudlBeAbleToAddInteractionWithSameRequestIfProviderStateIsDifferent() 27 | { 28 | var interaction = new Interaction(); 29 | var differentInteraction = new Interaction { ProviderStates = new List { new ProviderState { Name = "some state" } } }; 30 | var list = new MatchableInteractionList { new MatchableInteraction(interaction) }; 31 | 32 | list.AddUnique(new MatchableInteraction(differentInteraction)); 33 | 34 | Assert.AreEqual(2, list.Count); 35 | } 36 | 37 | [TestMethod] 38 | [ExpectedException(typeof(PactException))] 39 | public void ShoudlNotBeAbleToAddInteractionThatIsNotDistinguishableFromExisting() 40 | { 41 | var interaction = new Interaction { Request = new Request { Path = "/test" } }; 42 | var differentInteraction = new Interaction { Request = new Request { Path = "/test" } }; 43 | var list = new MatchableInteractionList { new MatchableInteraction(interaction) }; 44 | 45 | try 46 | { 47 | list.AddUnique(new MatchableInteraction(differentInteraction)); 48 | } 49 | catch (PactException e) 50 | { 51 | Assert.AreEqual("Cannot add multiple interactions with the same provider states and requests. " + 52 | "The provider will not be able to distinguish between them.", e.Message); 53 | throw; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Models/HeadersFromHeaderDictionaryTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.Primitives; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using System; 6 | 7 | namespace ComPact.UnitTests.Models 8 | { 9 | [TestClass] 10 | public class HeadersFromHeaderDictionaryTests 11 | { 12 | [TestMethod] 13 | public void ShouldSupportMultipleHeaders() 14 | { 15 | var actualHeaders = new HeaderDictionary 16 | { 17 | { "Accept", new StringValues("application/json") }, 18 | { "Host", new StringValues("test") } 19 | }; 20 | 21 | var pactHeaders = new Headers(actualHeaders); 22 | 23 | Assert.AreEqual(actualHeaders.Count, pactHeaders.Count); 24 | Assert.AreEqual("application/json", pactHeaders["Accept"]); 25 | Assert.AreEqual("test", pactHeaders["Host"]); 26 | } 27 | 28 | [TestMethod] 29 | public void ShouldJoinMultipleValuesAsCommaSeparatedList() 30 | { 31 | var actualHeaders = new HeaderDictionary 32 | { 33 | { "Key", new StringValues(new string[] { "value1", "value2", "value3" }) } 34 | }; 35 | 36 | var pactHeaders = new Headers(actualHeaders); 37 | 38 | Assert.AreEqual("value1,value2,value3", pactHeaders["Key"]); 39 | } 40 | 41 | [TestMethod] 42 | public void ShouldAllowForEmptyDictionary() 43 | { 44 | var actualHeaders = new HeaderDictionary(); 45 | 46 | var pactHeaders = new Headers(actualHeaders); 47 | 48 | Assert.AreEqual(0, pactHeaders.Count); 49 | } 50 | 51 | [TestMethod] 52 | [ExpectedException(typeof(ArgumentNullException))] 53 | public void ShouldThrowWhenNull() 54 | { 55 | new Headers(null as IHeaderDictionary); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Models/HeadersMatchTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System; 4 | 5 | namespace ComPact.UnitTests.Models 6 | { 7 | [TestClass] 8 | public class HeadersMatchTests 9 | { 10 | [TestMethod] 11 | public void ShouldMatchForExactMatchingHeaders() 12 | { 13 | var actual = new Headers { { "Accept", "application/json" } }; 14 | var expected = new Headers { { "Accept", "application/json" } }; 15 | 16 | Assert.IsTrue(expected.Match(actual)); 17 | } 18 | 19 | [TestMethod] 20 | public void ShouldNotMatchWhenKeysDoNotMatch() 21 | { 22 | var actual = new Headers { { "Accept", "application/json" } }; 23 | var expected = new Headers { { "Accep", "application/json" } }; 24 | 25 | Assert.IsFalse(expected.Match(actual)); 26 | } 27 | 28 | [TestMethod] 29 | public void ShouldNotMatchWhenValuesDoNotMatch() 30 | { 31 | var actual = new Headers { { "Accept", "application/json" } }; 32 | var expected = new Headers { { "Accept", "application/hal+json" } }; 33 | 34 | Assert.IsFalse(expected.Match(actual)); 35 | } 36 | 37 | [TestMethod] 38 | public void ShouldAllowForAdditionalActualHeader() 39 | { 40 | var actual = new Headers { { "Accept", "application/json" }, { "Host", "test" } }; 41 | var expected = new Headers { { "Accept", "application/json" } }; 42 | 43 | Assert.IsTrue(expected.Match(actual)); 44 | } 45 | 46 | [TestMethod] 47 | public void ShouldNotAllowForAdditionalExpectedHeader() 48 | { 49 | var actual = new Headers { { "Accept", "application/json" } }; 50 | var expected = new Headers { { "Accept", "application/json" }, { "Host", "test" } }; 51 | 52 | Assert.IsFalse(expected.Match(actual)); 53 | } 54 | 55 | [TestMethod] 56 | public void ShouldMatchWhenNoHeadersAreExpected() 57 | { 58 | var actual = new Headers(); 59 | var expected = new Headers(); 60 | 61 | Assert.IsTrue(expected.Match(actual)); 62 | } 63 | 64 | [TestMethod] 65 | [ExpectedException(typeof(ArgumentNullException))] 66 | public void ShouldThrowWhenNull() 67 | { 68 | new Headers().Match(null); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Models/MatcherTypeEnumTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using Newtonsoft.Json; 4 | 5 | namespace ComPact.UnitTests.Models 6 | { 7 | [TestClass] 8 | public class MatcherTypeEnumTests 9 | { 10 | [TestMethod] 11 | public void ShouldDeserializeTypeToType() 12 | { 13 | var jsonString = "{ \"match\":\"type\"}"; 14 | 15 | var matcher = JsonConvert.DeserializeObject(jsonString); 16 | 17 | Assert.AreEqual(MatcherType.type, matcher.MatcherType); 18 | } 19 | 20 | [TestMethod] 21 | public void ShouldDeserializeRegexToRegex() 22 | { 23 | var jsonString = "{ \"match\":\"regex\"}"; 24 | 25 | var matcher = JsonConvert.DeserializeObject(jsonString); 26 | 27 | Assert.AreEqual(MatcherType.regex, matcher.MatcherType); 28 | } 29 | 30 | [TestMethod] 31 | public void ShouldDeserializeUnknownValueToType() 32 | { 33 | var jsonString = "{ \"match\":\"number\"}"; 34 | 35 | var matcher = JsonConvert.DeserializeObject(jsonString); 36 | 37 | Assert.AreEqual(MatcherType.type, matcher.MatcherType); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Models/MatchingRulesUpgradeTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace ComPact.UnitTests.Models 7 | { 8 | [TestClass] 9 | public class MatchingRulesUpgradeTests 10 | { 11 | [TestMethod] 12 | public void ShouldUpgradeOldMatchingRulesToNew() 13 | { 14 | var oldMatchingRules = new Dictionary() 15 | { 16 | { "$.body", new Matcher { MatcherType = MatcherType.type} }, 17 | { "$.body.name", new Matcher { MatcherType = MatcherType.regex, Regex = "\\w+" } }, 18 | { "$.headers.content-type", new Matcher { MatcherType = MatcherType.type} } 19 | }; 20 | 21 | var newMatchingRules = new MatchingRuleCollection(oldMatchingRules); 22 | 23 | Assert.AreEqual(2, newMatchingRules.Body.Count); 24 | Assert.AreEqual(1, newMatchingRules.Body.First().Value.Matchers.Count); 25 | Assert.AreEqual(MatcherType.type, newMatchingRules.Body["$"].Matchers.First().MatcherType); 26 | Assert.AreEqual("\\w+", newMatchingRules.Body["$.name"].Matchers.First().Regex); 27 | Assert.AreEqual(MatcherType.regex, newMatchingRules.Body["$.name"].Matchers.First().MatcherType); 28 | 29 | Assert.AreEqual(1, newMatchingRules.Header.Count); 30 | Assert.AreEqual(MatcherType.type, newMatchingRules.Header["content-type"].Matchers.First().MatcherType); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Models/V2/SetEmptyValuesToNullTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using ComPact.Models.V2; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System.Collections.Generic; 5 | 6 | namespace ComPact.UnitTests.Models.V2 7 | { 8 | [TestClass] 9 | public class SetEmptyValuesToNullTests 10 | { 11 | [TestMethod] 12 | public void ShouldSetEmptyValuesToNull() 13 | { 14 | var interaction = new Interaction(); 15 | 16 | interaction.SetEmptyValuesToNull(); 17 | 18 | Assert.IsNull(interaction.ProviderState); 19 | Assert.IsNull(interaction.Request.Headers); 20 | Assert.IsNull(interaction.Request.Body); 21 | Assert.IsNull(interaction.Request.Query); 22 | Assert.IsNull(interaction.Response.Headers); 23 | Assert.IsNull(interaction.Response.Body); 24 | Assert.IsNull(interaction.Response.MatchingRules); 25 | } 26 | 27 | [TestMethod] 28 | public void ShouldNotSetNonEmptyValuesToNull() 29 | { 30 | var interaction = new Interaction 31 | { 32 | ProviderState = "provider state", 33 | Request = new Request 34 | { 35 | Headers = new Headers { { "Accept", "application/json" } }, 36 | Body = "test", 37 | Query = "skip=100&take=10" 38 | }, 39 | Response = new Response 40 | { 41 | Headers = new Headers { { "Content-Type", "application/json" } }, 42 | Body = "test", 43 | MatchingRules = new Dictionary { { "$", new Matcher { MatcherType = MatcherType.type } } } 44 | } 45 | }; 46 | 47 | interaction.SetEmptyValuesToNull(); 48 | 49 | Assert.IsNotNull(interaction.ProviderState); 50 | Assert.IsNotNull(interaction.Description); 51 | Assert.IsNotNull(interaction.Request.Headers); 52 | Assert.IsNotNull(interaction.Request.Body); 53 | Assert.IsNotNull(interaction.Request.Query); 54 | Assert.IsNotNull(interaction.Request.Path); 55 | Assert.IsNotNull(interaction.Request.Method); 56 | Assert.IsNotNull(interaction.Response.Headers); 57 | Assert.IsNotNull(interaction.Response.Body); 58 | Assert.IsNotNull(interaction.Response.MatchingRules); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Models/V3/QueryToAndFromQueryStringTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models.V3; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Linq; 4 | 5 | namespace ComPact.UnitTests.Models.V3 6 | { 7 | [TestClass] 8 | public class QueryToAndFromQueryStringTests 9 | { 10 | [TestMethod] 11 | public void TwoSimpleParameters() 12 | { 13 | var inputQueryString ="skip=100&take=10"; 14 | var query = new Query(inputQueryString); 15 | var outputQueryString = query.ToQueryString(); 16 | 17 | Assert.AreEqual(inputQueryString, outputQueryString); 18 | Assert.AreEqual("100", query["skip"].First()); 19 | Assert.AreEqual(1, query["skip"].Count()); 20 | Assert.AreEqual("10", query["take"].First()); 21 | Assert.AreEqual(1, query["skip"].Count()); 22 | } 23 | 24 | [TestMethod] 25 | public void EmptyQueryString() 26 | { 27 | var inputQueryString = string.Empty; 28 | var query = new Query(inputQueryString); 29 | var outputQueryString = query.ToQueryString(); 30 | 31 | Assert.AreEqual(inputQueryString, outputQueryString); 32 | Assert.AreEqual(0, query.Count()); 33 | } 34 | 35 | [TestMethod] 36 | public void NullQueryString() 37 | { 38 | string inputQueryString = null; 39 | var query = new Query(inputQueryString); 40 | var outputQueryString = query.ToQueryString(); 41 | 42 | Assert.AreEqual(string.Empty, outputQueryString); 43 | Assert.AreEqual(0, query.Count()); 44 | } 45 | 46 | [TestMethod] 47 | public void OneSimpleParameter() 48 | { 49 | var inputQueryString = "skip=100"; 50 | var query = new Query(inputQueryString); 51 | var outputQueryString = query.ToQueryString(); 52 | 53 | Assert.AreEqual(inputQueryString, outputQueryString); 54 | Assert.AreEqual("100", query["skip"].First()); 55 | Assert.AreEqual(1, query["skip"].Count()); 56 | } 57 | 58 | [TestMethod] 59 | public void OneParameterWithMultipleValues() 60 | { 61 | var inputQueryString = "colors=red,blue"; 62 | var query = new Query(inputQueryString); 63 | var outputQueryString = query.ToQueryString(); 64 | 65 | Assert.AreEqual(inputQueryString, outputQueryString); 66 | Assert.AreEqual(2, query["colors"].Count()); 67 | Assert.AreEqual("red", query["colors"][0]); 68 | Assert.AreEqual("blue", query["colors"][1]); 69 | } 70 | 71 | [TestMethod] 72 | public void MultipleParametersWithSameKeyAreJoined() 73 | { 74 | var inputQueryString = "color=red&color=blue"; 75 | var query = new Query(inputQueryString); 76 | var outputQueryString = query.ToQueryString(); 77 | 78 | Assert.AreEqual("color=blue,red", outputQueryString); 79 | Assert.AreEqual(2, query["color"].Count()); 80 | Assert.AreEqual("blue", query["color"][0]); 81 | Assert.AreEqual("red", query["color"][1]); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Models/V3/ResponseFromHttpResponseMessageTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Exceptions; 2 | using ComPact.Models.V3; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Text; 7 | 8 | namespace ComPact.UnitTests.Models.V3 9 | { 10 | [TestClass] 11 | public class ResponseFromHttpResponseMessageTests 12 | { 13 | [TestMethod] 14 | public void ShouldCreateResponseFromHttpResponseMessage() 15 | { 16 | var httpResponseMessage = new HttpResponseMessage(System.Net.HttpStatusCode.OK); 17 | httpResponseMessage.Headers.Add("Some-Header", "some value"); 18 | httpResponseMessage.Content = new StringContent("\"text\"", Encoding.UTF8, "application/json"); 19 | 20 | var response = new Response(httpResponseMessage); 21 | 22 | Assert.AreEqual(200, response.Status); 23 | Assert.IsTrue(response.Headers.Any(h => h.Key == "Some-Header" && h.Value == "some value")); 24 | Assert.IsTrue(response.Headers.Any(h => h.Key == "Content-Type" && h.Value == "application/json; charset=utf-8")); 25 | Assert.AreEqual("text", response.Body); 26 | } 27 | 28 | [TestMethod] 29 | public void ShouldCreateResponseFromHttpResponseMessageWithoutContent() 30 | { 31 | var httpResponseMessage = new HttpResponseMessage(System.Net.HttpStatusCode.NotFound); 32 | httpResponseMessage.Headers.Add("Some-Header", "some value"); 33 | 34 | var response = new Response(httpResponseMessage); 35 | 36 | Assert.AreEqual(404, response.Status); 37 | Assert.AreEqual(1, response.Headers.Count); 38 | Assert.IsTrue(response.Headers.Any(h => h.Key == "Some-Header" && h.Value == "some value")); 39 | Assert.IsNull(response.Body); 40 | } 41 | 42 | [TestMethod] 43 | [ExpectedException(typeof(PactException))] 44 | public void ShouldThrowWhenContentCannotBeDeserialized() 45 | { 46 | var httpResponseMessage = new HttpResponseMessage(System.Net.HttpStatusCode.OK); 47 | httpResponseMessage.Content = new StringContent("text", Encoding.UTF8, "text/plain"); 48 | 49 | try 50 | { 51 | var response = new Response(httpResponseMessage); 52 | } 53 | catch (PactException e) 54 | { 55 | Assert.AreEqual("Response body could not be deserialized from JSON. Content-Type was text/plain; charset=utf-8", e.Message); 56 | throw; 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Verifier/InvokeProviderStateHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Exceptions; 2 | using ComPact.Models; 3 | using ComPact.Verifier; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace ComPact.UnitTests.Verifier 10 | { 11 | [TestClass] 12 | public class InvokeProviderStateHandlerTests 13 | { 14 | [TestMethod] 15 | public void ShouldInvokeProviderStateHandlerForEveryProviderState() 16 | { 17 | var invocations = new List(); 18 | 19 | PactVerifier.InvokeProviderStateHandler( 20 | new List { new ProviderState { Name = "ps1" }, new ProviderState { Name = "ps2" } }, 21 | (p) => invocations.Add(p.Name)); 22 | 23 | Assert.AreEqual(2, invocations.Count); 24 | } 25 | 26 | [TestMethod] 27 | [ExpectedException(typeof(PactException))] 28 | public void ShouldThrowPactExceptionWhenProviderStateHandlerIsNotConfigured() 29 | { 30 | try 31 | { 32 | PactVerifier.InvokeProviderStateHandler(new List { new ProviderState { Name = "ps1" } }, null); 33 | } 34 | catch (PactException e) 35 | { 36 | Assert.AreEqual("Cannot verify this Pact contract because a ProviderStateHandler was not configured.", e.Message); 37 | throw; 38 | } 39 | } 40 | 41 | [TestMethod] 42 | public void ShouldReturnVerificationMessagesWhenHandlerThrowsPactVerificationException() 43 | { 44 | var verificationMessages = PactVerifier.InvokeProviderStateHandler( 45 | new List { new ProviderState { Name = "ps1" } }, 46 | (p) => throw new PactVerificationException("Unknown provider state.")); 47 | 48 | Assert.AreEqual($"Provider could not handle provider state \"ps1\": Unknown provider state.", verificationMessages.First()); 49 | } 50 | 51 | [TestMethod] 52 | [ExpectedException(typeof(PactException))] 53 | public void ShouldThrowPactExceptionWhenProviderStateHandlerThrowsAnyOtherException() 54 | { 55 | try 56 | { 57 | PactVerifier.InvokeProviderStateHandler(new List { new ProviderState { Name = "ps1" } }, (p) => throw new ArgumentNullException()); 58 | } 59 | catch (PactException e) 60 | { 61 | Assert.AreEqual("Exception occured while invoking ProviderStateHandler.", e.Message); 62 | throw; 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Verifier/TestToTestMessageStringTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Verifier; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace ComPact.UnitTests.Verifier 7 | { 8 | [TestClass] 9 | public class TestToTestMessageStringTests 10 | { 11 | [TestMethod] 12 | public void ShouldReturnDescriptionAndStatusForPassedTest() 13 | { 14 | var test = new Test 15 | { 16 | Description = "An interaction" 17 | }; 18 | 19 | Assert.AreEqual("An interaction (passed)", test.ToTestMessageString()); 20 | } 21 | 22 | [TestMethod] 23 | public void ShouldReturnDescriptionStatusAndListOfIssuesForFailedTest() 24 | { 25 | var test = new Test 26 | { 27 | Description = "A failed interaction", 28 | Issues = new List { "issue 1", "issue 2" } 29 | }; 30 | 31 | Assert.AreEqual("A failed interaction (failed):" + Environment.NewLine + "- issue 1" + Environment.NewLine + "- issue 2", test.ToTestMessageString()); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Verifier/VerifyInteractionsTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Exceptions; 2 | using ComPact.Models.V3; 3 | using ComPact.Verifier; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Threading.Tasks; 10 | 11 | namespace ComPact.UnitTests.Verifier 12 | { 13 | [TestClass] 14 | public class VerifyInteractionsTests 15 | { 16 | private List _interactions; 17 | 18 | public VerifyInteractionsTests() 19 | { 20 | _interactions = new List 21 | { 22 | new Interaction 23 | { 24 | Description = "An interaction", 25 | ProviderStates = new List {new ComPact.Models.ProviderState { Name = "Some state"} }, 26 | Request = new Request 27 | { 28 | Method = ComPact.Models.Method.POST 29 | }, 30 | Response = new Response 31 | { 32 | Status = 200 33 | } 34 | }, 35 | new Interaction 36 | { 37 | Description = "Another interaction", 38 | ProviderStates = new List {new ComPact.Models.ProviderState { Name = "Some state"} }, 39 | Request = new Request 40 | { 41 | Method = ComPact.Models.Method.PUT 42 | }, 43 | Response = new Response 44 | { 45 | Status = 200 46 | } 47 | } 48 | }; 49 | } 50 | 51 | [TestMethod] 52 | public async Task ShouldReturnSuccessfulTest() 53 | { 54 | var tests = await PactVerifier.VerifyInteractions(_interactions, (req) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)), (p) => { }); 55 | 56 | Assert.AreEqual(2, tests.Count); 57 | Assert.AreEqual("passed", tests.First().Status); 58 | Assert.AreEqual("An interaction", tests.First().Description); 59 | } 60 | 61 | [TestMethod] 62 | public async Task ShouldReturnFailedTestWhenResponsesDoNotMatch() 63 | { 64 | var tests = await PactVerifier.VerifyInteractions(_interactions, (req) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)), (p) => { }); 65 | 66 | Assert.AreEqual(2, tests.Count); 67 | Assert.AreEqual("failed", tests.First().Status); 68 | Assert.AreEqual("An interaction", tests.First().Description); 69 | } 70 | 71 | [TestMethod] 72 | public async Task ShouldReturnFailedTestWhenHandlerThrowsPactVerificationException() 73 | { 74 | var tests = await PactVerifier.VerifyInteractions(_interactions, (req) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)), 75 | (p) => throw new PactVerificationException("Unknown provider state.")); 76 | 77 | Assert.AreEqual(2, tests.Count); 78 | Assert.AreEqual("failed", tests.First().Status); 79 | Assert.AreEqual("An interaction", tests.First().Description); 80 | Assert.IsTrue(tests.First().Issues.First().Contains("Unknown provider state.")); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /ComPact.UnitTests/Verifier/VerifyMessagesTests.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Exceptions; 2 | using ComPact.Models.V3; 3 | using ComPact.Verifier; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace ComPact.UnitTests.Verifier 9 | { 10 | [TestClass] 11 | public class VerifyMessagesTests 12 | { 13 | private List _messages; 14 | 15 | public VerifyMessagesTests() 16 | { 17 | _messages = new List 18 | { 19 | new Message 20 | { 21 | Description = "A message", 22 | ProviderStates = new List {new ComPact.Models.ProviderState { Name = "Some state"} }, 23 | Contents = new { text = "test" } 24 | }, 25 | new Message 26 | { 27 | Description = "Another message", 28 | ProviderStates = new List {new ComPact.Models.ProviderState { Name = "Other state"} }, 29 | Contents = new { text = "test" } 30 | } 31 | }; 32 | } 33 | 34 | [TestMethod] 35 | public void ShouldReturnSuccessfulTest() 36 | { 37 | var tests = PactVerifier.VerifyMessages(_messages, (p) => { }, (d) => new { text = "test" }); 38 | 39 | Assert.AreEqual(2, tests.Count); 40 | Assert.AreEqual("passed", tests.First().Status); 41 | Assert.AreEqual("A message", tests.First().Description); 42 | } 43 | 44 | [TestMethod] 45 | public void ShouldReturnFailedTestWhenWrongMessageIsReturned() 46 | { 47 | var tests = PactVerifier.VerifyMessages(_messages, (p) => { }, (d) => new { text = "wrong" }); 48 | 49 | Assert.AreEqual(2, tests.Count); 50 | Assert.AreEqual("failed", tests.First().Status); 51 | Assert.AreEqual("A message", tests.First().Description); 52 | } 53 | 54 | [TestMethod] 55 | public void ShouldReturnFailedTestWhenHandlerThrowsPactVerificationException() 56 | { 57 | var tests = PactVerifier.VerifyMessages(_messages, (p) => throw new PactVerificationException("Unknown provider state."), (d) => new { text = "test" }); 58 | 59 | Assert.AreEqual(2, tests.Count); 60 | Assert.AreEqual("failed", tests.First().Status); 61 | Assert.AreEqual("A message", tests.First().Description); 62 | Assert.IsTrue(tests.First().Issues.First().Contains("Unknown provider state.")); 63 | } 64 | 65 | [TestMethod] 66 | public void ShouldReturnFailedTestWhenMessageProducerThrowsPactVerificationException() 67 | { 68 | var tests = PactVerifier.VerifyMessages(_messages, (p) => { }, (d) => throw new PactVerificationException("Unknown description.")); 69 | 70 | Assert.AreEqual(2, tests.Count); 71 | Assert.AreEqual("failed", tests.First().Status); 72 | Assert.AreEqual("A message", tests.First().Description); 73 | Assert.IsTrue(tests.First().Issues.First().Contains("Unknown description.")); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ComPact.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28803.352 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComPact", "ComPact\ComPact.csproj", "{9A622242-5199-4EC0-9953-4E543759153F}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComPact.UnitTests", "ComPact.UnitTests\ComPact.UnitTests.csproj", "{0191CA45-2A47-4262-8065-BC18263EA67D}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComPact.ConsumerTests", "ComPact.ConsumerTests\ComPact.ConsumerTests.csproj", "{BDF3F200-B8DB-47EA-BD13-5BE7C609B467}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComPact.ProviderTests", "ComPact.ProviderTests\ComPact.ProviderTests.csproj", "{3A46C75D-A045-41B0-A4BE-523F9DC0CDAA}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComPact.Tests.Shared", "ComPact.Tests.Shared\ComPact.Tests.Shared.csproj", "{E97B3FF9-B6F6-451D-95CE-E650871C0173}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {9A622242-5199-4EC0-9953-4E543759153F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {9A622242-5199-4EC0-9953-4E543759153F}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {9A622242-5199-4EC0-9953-4E543759153F}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {9A622242-5199-4EC0-9953-4E543759153F}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {0191CA45-2A47-4262-8065-BC18263EA67D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {0191CA45-2A47-4262-8065-BC18263EA67D}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {0191CA45-2A47-4262-8065-BC18263EA67D}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {0191CA45-2A47-4262-8065-BC18263EA67D}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {BDF3F200-B8DB-47EA-BD13-5BE7C609B467}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {BDF3F200-B8DB-47EA-BD13-5BE7C609B467}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {BDF3F200-B8DB-47EA-BD13-5BE7C609B467}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {BDF3F200-B8DB-47EA-BD13-5BE7C609B467}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {3A46C75D-A045-41B0-A4BE-523F9DC0CDAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {3A46C75D-A045-41B0-A4BE-523F9DC0CDAA}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {3A46C75D-A045-41B0-A4BE-523F9DC0CDAA}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {3A46C75D-A045-41B0-A4BE-523F9DC0CDAA}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {E97B3FF9-B6F6-451D-95CE-E650871C0173}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {E97B3FF9-B6F6-451D-95CE-E650871C0173}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {E97B3FF9-B6F6-451D-95CE-E650871C0173}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {E97B3FF9-B6F6-451D-95CE-E650871C0173}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {C53C8AD1-45FE-4F2F-B995-CE0F935F1234} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /ComPact/Builders/PactBuilderBase.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Exceptions; 2 | using ComPact.MockProvider; 3 | using ComPact.Models; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace ComPact.Builders 9 | { 10 | public abstract class PactBuilderBase 11 | { 12 | protected readonly string _consumer; 13 | protected readonly string _provider; 14 | protected readonly string _pactDir; 15 | protected readonly PactPublisher _pactPublisher; 16 | protected readonly CancellationTokenSource _cts; 17 | private readonly RequestResponseMatcher _matcher; 18 | internal MatchableInteractionList MatchableInteractions { get; private set; } 19 | 20 | internal PactBuilderBase(string consumer, string provider, string mockProviderServiceBaseUri, PactPublisher pactPublisher = null, string pactDir = null) 21 | { 22 | if (mockProviderServiceBaseUri is null) 23 | { 24 | throw new System.ArgumentNullException(nameof(mockProviderServiceBaseUri)); 25 | } 26 | 27 | _pactDir = pactDir; 28 | _pactPublisher = pactPublisher; 29 | 30 | _cts = new CancellationTokenSource(); 31 | 32 | _consumer = consumer ?? throw new System.ArgumentNullException(nameof(consumer)); 33 | _provider = provider ?? throw new System.ArgumentNullException(nameof(provider)); 34 | MatchableInteractions = new MatchableInteractionList(); 35 | 36 | _matcher = new RequestResponseMatcher(MatchableInteractions); 37 | 38 | ProviderWebHost.Run(mockProviderServiceBaseUri, _matcher, _cts); 39 | } 40 | 41 | internal void SetUp(MatchableInteraction matchableInteraction) 42 | { 43 | MatchableInteractions.AddUnique(matchableInteraction); 44 | } 45 | 46 | internal void ClearMatchableInteractions() 47 | { 48 | MatchableInteractions.Clear(); 49 | } 50 | 51 | internal async Task BuildAsync(IContract pact) 52 | { 53 | _cts.Cancel(); 54 | 55 | if (!MatchableInteractions.Any()) 56 | { 57 | throw new PactException("Cannot build pact. No interactions."); 58 | } 59 | 60 | if (!_matcher.AllHaveBeenMatched()) 61 | { 62 | throw new PactException("Cannot build pact. Not all mocked interactions have been called."); 63 | } 64 | 65 | pact.Consumer = new Pacticipant { Name = _consumer }; 66 | pact.Provider = new Pacticipant { Name = _provider }; 67 | 68 | if (_pactDir != null) 69 | { 70 | PactWriter.Write(pact, _pactDir); 71 | } 72 | else 73 | { 74 | PactWriter.Write(pact); 75 | } 76 | 77 | if (_pactPublisher != null) 78 | { 79 | await _pactPublisher.PublishAsync(pact); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /ComPact/Builders/PactPublisher.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Exceptions; 2 | using ComPact.Models; 3 | using Newtonsoft.Json; 4 | using System; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace ComPact.Builders 10 | { 11 | public class PactPublisher 12 | { 13 | private readonly HttpClient _pactBrokerClient; 14 | private readonly string _consumerVersion; 15 | private readonly string _consumerTag; 16 | 17 | /// 18 | /// Publishes generated contracts to your Pact Broker. 19 | /// 20 | /// Client that can be used to connect to your Pact Broker. Should be set up with the correct base URL and if needed any necessary headers. 21 | /// The version of your consumer application. 22 | /// An optional tag to tag your consumer version with. 23 | public PactPublisher(HttpClient pactBrokerClient, string consumerVersion, string consumerTag = null) 24 | { 25 | if (pactBrokerClient?.BaseAddress == null) 26 | { 27 | throw new PactException("A pactBrokerClient with at least a BaseAddress should be configured to be able to publish contracts."); 28 | } 29 | 30 | if (string.IsNullOrWhiteSpace(consumerVersion)) 31 | { 32 | throw new PactException("ConsumerVersion should be configured to be able to publish contracts."); 33 | } 34 | 35 | _pactBrokerClient = pactBrokerClient ?? throw new ArgumentNullException(nameof(pactBrokerClient)); 36 | 37 | _consumerVersion = consumerVersion ?? throw new ArgumentNullException(nameof(consumerVersion)); 38 | _consumerTag = consumerTag; 39 | } 40 | 41 | internal async Task PublishAsync(IContract pact) 42 | { 43 | var settings = new JsonSerializerSettings 44 | { 45 | NullValueHandling = NullValueHandling.Ignore, 46 | Formatting = Formatting.None 47 | }; 48 | 49 | var content = new StringContent(JsonConvert.SerializeObject(pact, settings), Encoding.UTF8, "application/json"); 50 | 51 | HttpResponseMessage response; 52 | 53 | try 54 | { 55 | response = await _pactBrokerClient.PutAsync($"pacts/provider/{pact.Provider.Name}/consumer/{pact.Consumer.Name}/version/{_consumerVersion}", content); 56 | } 57 | catch (Exception e) 58 | { 59 | throw new PactException($"Pact cannot be published using the provided Pact Broker Client: {e.Message}"); 60 | } 61 | if (!response.IsSuccessStatusCode) 62 | { 63 | throw new PactException("Publishing contract failed. Pact Broker returned " + response.StatusCode); 64 | } 65 | 66 | if (_consumerTag != null) 67 | { 68 | var tagResponse = await _pactBrokerClient.PutAsync($"pacticipants/{pact.Consumer.Name}/versions/{_consumerVersion}/tags/{_consumerTag}", new StringContent(string.Empty, Encoding.UTF8, "application/json")); 69 | if (!tagResponse.IsSuccessStatusCode) 70 | { 71 | throw new PactException("Tagging consumer version failed. Pact Broker returned " + response.StatusCode); 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ComPact/Builders/PactWriter.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.IO; 5 | 6 | namespace ComPact.Builders 7 | { 8 | internal static class PactWriter 9 | { 10 | public static void Write(IContract pact) 11 | { 12 | #if USE_NET4X 13 | var buildDirectory = new Uri(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase.Replace("file:///", ""))).LocalPath; 14 | var pactDir = Path.GetFullPath($"{buildDirectory}{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}pacts{Path.DirectorySeparatorChar}"); 15 | #else 16 | string buildDirectory = AppContext.BaseDirectory; 17 | string pactDir = Path.GetFullPath($"{buildDirectory}{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}pacts{Path.DirectorySeparatorChar}"); 18 | #endif 19 | Write(pact, pactDir); 20 | } 21 | 22 | public static void Write(IContract pact, string pactDir) 23 | { 24 | pact.SetEmptyValuesToNull(); 25 | 26 | JsonSerializerSettings settings = new JsonSerializerSettings 27 | { 28 | NullValueHandling = NullValueHandling.Ignore, 29 | Formatting = Formatting.Indented 30 | }; 31 | string serializedPact = JsonConvert.SerializeObject(pact, settings); 32 | 33 | Directory.CreateDirectory(pactDir); 34 | File.WriteAllText($"{pactDir}{Path.DirectorySeparatorChar}{pact.Consumer.Name}-{pact.Provider.Name}.json", serializedPact); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ComPact/Builders/V2/InteractionBuilder.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models.V2; 2 | using System; 3 | 4 | namespace ComPact.Builders.V2 5 | { 6 | public class InteractionBuilder 7 | { 8 | private readonly Interaction _interaction = new Interaction(); 9 | 10 | public InteractionBuilder Given(string providerState) 11 | { 12 | _interaction.ProviderState = providerState ?? throw new ArgumentNullException(nameof(providerState)); 13 | return this; 14 | } 15 | 16 | public InteractionBuilder UponReceiving(string description) 17 | { 18 | _interaction.Description = description ?? throw new ArgumentNullException(nameof(description)); 19 | return this; 20 | } 21 | 22 | /// 23 | /// Type Pact.Request... 24 | /// 25 | /// 26 | /// 27 | public InteractionBuilder With(RequestBuilder request) 28 | { 29 | if (request == null) 30 | { 31 | throw new ArgumentNullException(nameof(request)); 32 | } 33 | 34 | _interaction.Request = request.Build(); 35 | return this; 36 | } 37 | 38 | /// 39 | /// Type Pact.Response... 40 | /// 41 | /// 42 | /// 43 | public InteractionBuilder WillRespondWith(ResponseBuilder response) 44 | { 45 | if (response == null) 46 | { 47 | throw new ArgumentNullException(nameof(response)); 48 | } 49 | 50 | _interaction.Response = response.Build(); 51 | return this; 52 | } 53 | 54 | internal Interaction Build() 55 | { 56 | return _interaction; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ComPact/Builders/V2/Pact.cs: -------------------------------------------------------------------------------- 1 | namespace ComPact.Builders.V2 2 | { 3 | public static class Pact 4 | { 5 | public static InteractionBuilder Interaction => new InteractionBuilder(); 6 | public static RequestBuilder Request => new RequestBuilder(); 7 | public static ResponseBuilder Response => new ResponseBuilder(); 8 | public static PactJsonContent JsonContent => new PactJsonContent(); 9 | } 10 | 11 | public static class Some 12 | { 13 | public static UnknownSimpleValue Element => new UnknownSimpleValue(); 14 | public static UnknownString String => new UnknownString(); 15 | public static UnknownObject Object => new UnknownObject(); 16 | public static UnknownArray Array => new UnknownArray(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ComPact/Builders/V2/PactBuilder.cs: -------------------------------------------------------------------------------- 1 | using ComPact.MockProvider; 2 | using ComPact.Models.V2; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace ComPact.Builders.V2 7 | { 8 | public class PactBuilder: PactBuilderBase 9 | { 10 | private List _interactions; 11 | 12 | /// 13 | /// Sets up a mock provider service, generates a V2 contract between a consumer and provider, 14 | /// writes the contract to disk and optionally publishes to a Pact Broker using the supplied client. 15 | /// 16 | /// Name of consuming party of the contract. 17 | /// Name of the providing party of the contract. 18 | /// URL where you will call the mock provider service to verify your consumer. 19 | /// If not supplied the contract will not be published. 20 | /// Directory where the generated pact file will be written to. Defaults to the current project directory. 21 | public PactBuilder(string consumer, string provider, string mockProviderServiceBaseUri, PactPublisher pactPublisher = null, string pactDir = null) 22 | : base(consumer, provider, mockProviderServiceBaseUri, pactPublisher, pactDir) 23 | { 24 | _interactions = new List(); 25 | } 26 | 27 | /// 28 | /// Type Pact.Interaction... 29 | /// 30 | /// 31 | public void SetUp(InteractionBuilder interactionBuilder) 32 | { 33 | var interaction = interactionBuilder.Build(); 34 | base.SetUp(new MatchableInteraction(new Models.V3.Interaction(interaction))); 35 | _interactions.Add(interaction); 36 | } 37 | 38 | public void ClearInteractions() 39 | { 40 | _interactions = new List(); 41 | ClearMatchableInteractions(); 42 | } 43 | 44 | public async Task BuildAsync() 45 | { 46 | await base.BuildAsync(new Contract { Interactions = _interactions }); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ComPact/Builders/V2/RequestBuilder.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using ComPact.Models.V2; 3 | 4 | namespace ComPact.Builders.V2 5 | { 6 | public class RequestBuilder 7 | { 8 | private readonly Request _request; 9 | 10 | internal RequestBuilder() 11 | { 12 | _request = new Request(); 13 | } 14 | 15 | public RequestBuilder WithPath(string path) 16 | { 17 | _request.Path = path ?? throw new System.ArgumentNullException(nameof(path)); 18 | return this; 19 | } 20 | 21 | public RequestBuilder WithMethod(Method method) 22 | { 23 | _request.Method = method; 24 | return this; 25 | } 26 | 27 | public RequestBuilder WithHeader(string key, string value) 28 | { 29 | if (key == null) 30 | { 31 | throw new System.ArgumentNullException(nameof(key)); 32 | } 33 | 34 | if (value == null) 35 | { 36 | throw new System.ArgumentNullException(nameof(value)); 37 | } 38 | 39 | _request.Headers.Add(key, value); 40 | return this; 41 | } 42 | 43 | public RequestBuilder WithQuery(string query) 44 | { 45 | _request.Query = query ?? throw new System.ArgumentNullException(nameof(query)); 46 | return this; 47 | } 48 | 49 | public RequestBuilder WithBody(object body) 50 | { 51 | _request.Body = body ?? throw new System.ArgumentNullException(nameof(body)); 52 | return this; 53 | } 54 | 55 | internal Request Build() 56 | { 57 | return _request; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ComPact/Builders/V2/ResponseBuilder.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models.V2; 2 | using System; 3 | 4 | namespace ComPact.Builders.V2 5 | { 6 | public class ResponseBuilder 7 | { 8 | private readonly Response _response; 9 | 10 | internal ResponseBuilder() 11 | { 12 | _response = new Response(); 13 | } 14 | 15 | public ResponseBuilder WithStatus(int status) 16 | { 17 | if (status < 100 || status > 599) 18 | { 19 | throw new ArgumentOutOfRangeException("Status should be between 100 and 599."); 20 | } 21 | 22 | _response.Status = status; 23 | return this; 24 | } 25 | 26 | public ResponseBuilder WithHeader(string key, string value) 27 | { 28 | if (key == null) 29 | { 30 | throw new ArgumentNullException(nameof(key)); 31 | } 32 | 33 | if (value == null) 34 | { 35 | throw new ArgumentNullException(nameof(value)); 36 | } 37 | 38 | _response.Headers.Add(key, value); 39 | return this; 40 | } 41 | 42 | /// 43 | /// Type Pact.ResponseBody... 44 | /// 45 | /// 46 | /// 47 | public ResponseBuilder WithBody(PactJsonContent responseBody) 48 | { 49 | _response.Body = responseBody.ToJToken(); 50 | _response.MatchingRules = responseBody.CreateV2MatchingRules(); 51 | return this; 52 | } 53 | 54 | internal Response Build() 55 | { 56 | return _response; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ComPact/Builders/V3/InteractionBuilder.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using ComPact.Models.V3; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace ComPact.Builders.V3 7 | { 8 | public class InteractionBuilder 9 | { 10 | private readonly Interaction _interaction = new Interaction(); 11 | 12 | public InteractionBuilder Given(ProviderState providerState) 13 | { 14 | if (providerState is null) 15 | { 16 | throw new ArgumentNullException(nameof(providerState)); 17 | } 18 | 19 | if (_interaction.ProviderStates == null) 20 | { 21 | _interaction.ProviderStates = new List(); 22 | } 23 | _interaction.ProviderStates.Add(providerState); 24 | return this; 25 | } 26 | 27 | public InteractionBuilder UponReceiving(string description) 28 | { 29 | _interaction.Description = description ?? throw new ArgumentNullException(nameof(description)); 30 | return this; 31 | } 32 | 33 | /// 34 | /// Type Pact.Request... 35 | /// 36 | /// 37 | /// 38 | public InteractionBuilder With(RequestBuilder request) 39 | { 40 | if (request == null) 41 | { 42 | throw new ArgumentNullException(nameof(request)); 43 | } 44 | 45 | _interaction.Request = request.Build(); 46 | return this; 47 | } 48 | 49 | /// 50 | /// Type Pact.Response... 51 | /// 52 | /// 53 | /// 54 | public InteractionBuilder WillRespondWith(ResponseBuilder response) 55 | { 56 | if (response == null) 57 | { 58 | throw new ArgumentNullException(nameof(response)); 59 | } 60 | 61 | _interaction.Response = response.Build(); 62 | return this; 63 | } 64 | 65 | internal Interaction Build() 66 | { 67 | return _interaction; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ComPact/Builders/V3/MessagePactBuilder.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Exceptions; 2 | using ComPact.Models; 3 | using ComPact.Models.V3; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace ComPact.Builders.V3 9 | { 10 | public class MessagePactBuilder 11 | { 12 | private readonly string _consumer; 13 | private readonly string _provider; 14 | private readonly string _pactDir; 15 | private readonly PactPublisher _pactPublisher; 16 | private readonly List _messages; 17 | 18 | /// 19 | /// Generates a V3 message contract between a consumer and provider, 20 | /// writes the contract to disk and optionally publishes to a Pact Broker using the supplied client. 21 | /// 22 | /// Name of consuming party of the contract. 23 | /// Name of the providing party of the contract. 24 | /// If not supplied the contract will not be published. 25 | /// Directory where the generated pact file will be written to. Defaults to the current project directory. 26 | public MessagePactBuilder(string consumer, string provider, PactPublisher pactPublisher = null, string pactDir = null) 27 | { 28 | _consumer = consumer ?? throw new System.ArgumentNullException(nameof(consumer)); 29 | _provider = provider ?? throw new System.ArgumentNullException(nameof(provider)); 30 | 31 | _pactDir = pactDir; 32 | _pactPublisher = pactPublisher; 33 | 34 | _messages = new List(); 35 | } 36 | 37 | /// 38 | /// Type Pact.Message... 39 | /// 40 | /// 41 | public MessagePactBuilder SetUp(MessageBuilder messageBuilder) 42 | { 43 | _messages.Add(messageBuilder.Build()); 44 | return this; 45 | } 46 | 47 | public async Task BuildAsync() 48 | { 49 | if (!_messages.Any()) 50 | { 51 | throw new PactException("Cannot build pact. No messages."); 52 | } 53 | 54 | var pact = new MessageContract 55 | { 56 | Consumer = new Pacticipant { Name = _consumer }, 57 | Provider = new Pacticipant { Name = _provider }, 58 | Messages = _messages 59 | }; 60 | 61 | if (_pactDir != null) 62 | { 63 | PactWriter.Write(pact, _pactDir); 64 | } 65 | else 66 | { 67 | PactWriter.Write(pact); 68 | } 69 | 70 | if (_pactPublisher != null) 71 | { 72 | await _pactPublisher.PublishAsync(pact); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ComPact/Builders/V3/Pact.cs: -------------------------------------------------------------------------------- 1 | namespace ComPact.Builders.V3 2 | { 3 | public static class Pact 4 | { 5 | public static InteractionBuilder Interaction => new InteractionBuilder(); 6 | public static RequestBuilder Request => new RequestBuilder(); 7 | public static ResponseBuilder Response => new ResponseBuilder(); 8 | public static MessageBuilder Message => new MessageBuilder(); 9 | public static PactJsonContent JsonContent => new PactJsonContent(); 10 | } 11 | 12 | public static class Some 13 | { 14 | public static UnknownSimpleValue Element => new UnknownSimpleValue(); 15 | public static UnknownString String => new UnknownString(); 16 | public static UnknownObject Object => new UnknownObject(); 17 | public static UnknownArray Array => new UnknownArray(); 18 | public static UnknownInteger Integer => new UnknownInteger(); 19 | public static UnknownDecimal Decimal => new UnknownDecimal(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ComPact/Builders/V3/PactBuilder.cs: -------------------------------------------------------------------------------- 1 | using ComPact.MockProvider; 2 | using ComPact.Models.V3; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace ComPact.Builders.V3 7 | { 8 | public class PactBuilder : PactBuilderBase 9 | { 10 | /// 11 | /// Sets up a mock provider service, generates a V3 contract between a consumer and provider, 12 | /// writes the contract to disk and optionally publishes to a Pact Broker using the supplied client. 13 | /// 14 | /// Name of consuming party of the contract. 15 | /// Name of the providing party of the contract. 16 | /// URL where you will call the mock provider service to verify your consumer. 17 | /// If not supplied the contract will not be published. 18 | /// Directory where the generated pact file will be written to. Defaults to the current project directory. 19 | public PactBuilder(string consumer, string provider, string mockProviderServiceBaseUri, PactPublisher pactPublisher = null, string pactDir = null) 20 | : base(consumer, provider, mockProviderServiceBaseUri, pactPublisher, pactDir) 21 | { 22 | } 23 | 24 | /// 25 | /// Type Pact.Interaction... 26 | /// 27 | /// 28 | public void SetUp(InteractionBuilder interactionBuilder) 29 | { 30 | base.SetUp(new MatchableInteraction(interactionBuilder.Build())); 31 | } 32 | 33 | public void ClearInteractions() 34 | { 35 | base.ClearMatchableInteractions(); 36 | } 37 | 38 | public async Task BuildAsync() 39 | { 40 | await base.BuildAsync(new Contract { Interactions = MatchableInteractions.Select(m => m.Interaction as Interaction).ToList() }); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ComPact/Builders/V3/PactJsonContentV3Dsl.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | 3 | namespace ComPact.Builders.V3 4 | { 5 | public class UnknownInteger 6 | { 7 | public SimpleValueName Named(string name) => new SimpleValueName(name); 8 | public SimpleValue Like(int example) => new SimpleValue(example, MatcherType.integer); 9 | } 10 | 11 | public class UnknownDecimal 12 | { 13 | public SimpleValueName Named(string name) => new SimpleValueName(name); 14 | public SimpleValue Like(double example) => new SimpleValue(example, MatcherType.@decimal); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ComPact/Builders/V3/RequestBuilder.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using ComPact.Models.V3; 3 | 4 | namespace ComPact.Builders.V3 5 | { 6 | public class RequestBuilder 7 | { 8 | private readonly Request _request; 9 | 10 | internal RequestBuilder() 11 | { 12 | _request = new Request(); 13 | } 14 | 15 | public RequestBuilder WithPath(string path) 16 | { 17 | _request.Path = path ?? throw new System.ArgumentNullException(nameof(path)); 18 | return this; 19 | } 20 | 21 | public RequestBuilder WithMethod(Method method) 22 | { 23 | _request.Method = method; 24 | return this; 25 | } 26 | 27 | public RequestBuilder WithHeader(string key, string value) 28 | { 29 | if (key == null) 30 | { 31 | throw new System.ArgumentNullException(nameof(key)); 32 | } 33 | 34 | if (value == null) 35 | { 36 | throw new System.ArgumentNullException(nameof(value)); 37 | } 38 | 39 | _request.Headers.Add(key, value); 40 | return this; 41 | } 42 | 43 | public RequestBuilder WithQuery(string query) 44 | { 45 | if (query is null) 46 | { 47 | throw new System.ArgumentNullException(nameof(query)); 48 | } 49 | 50 | _request.Query = new Query(query); 51 | return this; 52 | } 53 | 54 | public RequestBuilder WithBody(object body) 55 | { 56 | _request.Body = body ?? throw new System.ArgumentNullException(nameof(body)); 57 | return this; 58 | } 59 | 60 | internal Request Build() 61 | { 62 | return _request; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ComPact/Builders/V3/ResponseBuilder.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using ComPact.Models.V3; 3 | using System; 4 | 5 | namespace ComPact.Builders.V3 6 | { 7 | public class ResponseBuilder 8 | { 9 | private readonly Response _response; 10 | 11 | internal ResponseBuilder() 12 | { 13 | _response = new Response(); 14 | } 15 | 16 | public ResponseBuilder WithStatus(int status) 17 | { 18 | if (status < 100 || status > 599) 19 | { 20 | throw new ArgumentOutOfRangeException("Status should be between 100 and 599."); 21 | } 22 | 23 | _response.Status = status; 24 | return this; 25 | } 26 | 27 | public ResponseBuilder WithHeader(string key, string value) 28 | { 29 | if (key == null) 30 | { 31 | throw new ArgumentNullException(nameof(key)); 32 | } 33 | 34 | if (value == null) 35 | { 36 | throw new ArgumentNullException(nameof(value)); 37 | } 38 | 39 | _response.Headers.Add(key, value); 40 | return this; 41 | } 42 | 43 | /// 44 | /// Type Pact.JsonContent... 45 | /// 46 | /// 47 | /// 48 | public ResponseBuilder WithBody(PactJsonContent responseBody) 49 | { 50 | _response.Body = responseBody.ToJToken(); 51 | if (_response.MatchingRules == null) 52 | { 53 | _response.MatchingRules = new MatchingRuleCollection(); 54 | } 55 | _response.MatchingRules.Body = responseBody.CreateV3MatchingRules(); 56 | return this; 57 | } 58 | 59 | internal Response Build() 60 | { 61 | return _response; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ComPact/ComPact.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | ComPact 6 | 0.4.4 7 | Bart Schotten 8 | A Pact implementation for .NET with support for Pact Specification v3. 9 | 10 | MIT 11 | https://github.com/bartschotten/com-pact 12 | https://github.com/bartschotten/com-pact 13 | git 14 | Pact, PactBroker, Message, Async, v3 15 | true 16 | true 17 | 18 | 19 | 20 | 1701;1702;1591 21 | 22 | 23 | 24 | 1701;1702;1591 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | <_Parameter1>ComPact.UnitTests 38 | 39 | 40 | 41 | 42 | 43 | ..\..\..\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore\2.2.0\lib\netstandard2.0\Microsoft.AspNetCore.dll 44 | 45 | 46 | ..\..\..\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.server.kestrel.core\2.2.0\lib\netcoreapp2.1\Microsoft.AspNetCore.Server.Kestrel.Core.dll 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /ComPact/Exceptions/PactException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ComPact.Exceptions 4 | { 5 | public class PactException: Exception 6 | { 7 | public PactException(string message): base(message) 8 | { 9 | } 10 | public PactException(string message, Exception exception) : base(message,exception) 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ComPact/Exceptions/PactVerificationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ComPact.Exceptions 4 | { 5 | public class PactVerificationException: Exception 6 | { 7 | public PactVerificationException(string message): base(message) 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ComPact/JsonHelpers/JTokenExtensions.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ComPact.JsonHelpers 6 | { 7 | internal static class JTokenExtensions 8 | { 9 | internal static IEnumerable ThisTokenAndAllItsDescendants(this JToken token) 10 | { 11 | var tokenAndItsChildren = new List { token }; 12 | tokenAndItsChildren.AddRange(token.Children().Select(c => c.ThisTokenAndAllItsDescendants()).SelectMany(d => d)); 13 | return tokenAndItsChildren; 14 | } 15 | 16 | internal static bool IsSameJsonTypeAs(this JToken token, JToken otherToken) 17 | { 18 | return token.Type == otherToken.Type || 19 | (token.Type == JTokenType.Float && otherToken.Type == JTokenType.Integer) || 20 | (token.Type == JTokenType.Integer && otherToken.Type == JTokenType.Float); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ComPact/JsonHelpers/JTokenParser.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System.IO; 4 | 5 | namespace ComPact.JsonHelpers 6 | { 7 | internal static class JTokenParser 8 | { 9 | internal static JToken Parse(dynamic body) => Parse(body); 10 | internal static JToken ParseToLower(dynamic body) => Parse(body); 11 | 12 | private static JToken Parse(dynamic body) where T : JsonTextReader 13 | { 14 | // This is necessary because DateParseHandling.None has no effect when using JToken.Parse or JToken.FromObject directly. 15 | var jsonTextReader = new JsonTextReader(new StringReader(JsonConvert.SerializeObject(body))) 16 | { 17 | // We don't want datetime-like strings to be converted to datetime, otherwise regex matching doesn't work. 18 | DateParseHandling = DateParseHandling.None 19 | }; 20 | return JToken.Load(jsonTextReader); 21 | } 22 | } 23 | 24 | internal class LowerCaseJsonReader : JsonTextReader 25 | { 26 | public LowerCaseJsonReader(TextReader reader) : base(reader) { } 27 | 28 | public override object Value 29 | { 30 | get => TokenType == JsonToken.PropertyName ? ((string)base.Value).ToLowerInvariant() : base.Value; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ComPact/JsonHelpers/StringEnumWithDefaultConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Converters; 4 | 5 | namespace ComPact.JsonHelpers 6 | { 7 | public class StringEnumWithDefaultConverter: StringEnumConverter 8 | { 9 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 10 | { 11 | try 12 | { 13 | return base.ReadJson(reader, objectType, existingValue, serializer); 14 | } 15 | catch 16 | { 17 | return existingValue; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ComPact/MockProvider/MatchableInteraction.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models.V3; 2 | 3 | namespace ComPact.MockProvider 4 | { 5 | internal class MatchableInteraction 6 | { 7 | public Interaction Interaction { get; set; } 8 | public bool HasBeenMatched { get; set; } 9 | 10 | public MatchableInteraction(Interaction interaction) 11 | { 12 | Interaction = interaction; 13 | } 14 | 15 | public Response Match(Models.V3.Request request) 16 | { 17 | var response = Interaction.Match(request); 18 | if (response != null) 19 | { 20 | HasBeenMatched = true; 21 | } 22 | return response; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ComPact/MockProvider/MatchableInteractionList.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Exceptions; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ComPact.MockProvider 6 | { 7 | internal class MatchableInteractionList : List 8 | { 9 | internal void AddUnique(MatchableInteraction matchableInteraction) 10 | { 11 | if (this.All(m => m.Interaction.ProviderStatesAndRequestCanBeDistinguishedFrom(matchableInteraction.Interaction))) 12 | { 13 | Add(matchableInteraction); 14 | } 15 | else 16 | { 17 | throw new PactException("Cannot add multiple interactions with the same provider states and requests. The provider will not be able to distinguish between them."); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ComPact/MockProvider/ProviderWebHost.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using System.Threading; 5 | 6 | namespace ComPact.MockProvider 7 | { 8 | internal static class ProviderWebHost 9 | { 10 | internal static void Run(string mockProviderServiceBaseUri, RequestResponseMatcher matcher, CancellationTokenSource cancellationTokenSource) 11 | { 12 | var host = WebHost.CreateDefaultBuilder() 13 | .UseUrls(mockProviderServiceBaseUri) 14 | .ConfigureKestrel(options => options.AllowSynchronousIO = true) 15 | .Configure(app => 16 | { 17 | app.Run(async context => 18 | { 19 | await matcher.MatchRequestAndReturnResponseAsync(context.Request, context.Response); 20 | }); 21 | }) 22 | .Build(); 23 | 24 | host.RunAsync(cancellationTokenSource.Token); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ComPact/MockProvider/RequestResponseMatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System; 3 | using Microsoft.AspNetCore.Http; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Primitives; 6 | using System.Text; 7 | using Newtonsoft.Json; 8 | using ComPact.Models.V3; 9 | 10 | namespace ComPact.MockProvider 11 | { 12 | internal class RequestResponseMatcher 13 | { 14 | private readonly MatchableInteractionList _matchableInteractions; 15 | 16 | public RequestResponseMatcher(MatchableInteractionList interactions) 17 | { 18 | _matchableInteractions = interactions; 19 | } 20 | 21 | public async Task MatchRequestAndReturnResponseAsync(HttpRequest httpRequest, HttpResponse httpResponseToReturn) 22 | { 23 | if (httpRequest == null) 24 | { 25 | throw new ArgumentNullException(nameof(httpRequest)); 26 | } 27 | 28 | var request = new Request(httpRequest); 29 | 30 | var response = _matchableInteractions.Select(m => m.Match(request)).Where(r => r != null).LastOrDefault(); 31 | 32 | string stringToReturn; 33 | if (response != null) 34 | { 35 | httpResponseToReturn.StatusCode = response.Status; 36 | foreach (var header in response.Headers) 37 | { 38 | httpResponseToReturn.Headers.Add(header.Key, new StringValues((string)header.Value)); 39 | } 40 | stringToReturn = JsonConvert.SerializeObject(response.Body); 41 | } 42 | else 43 | { 44 | var errorResponse = new RequestResponseMatchingErrorResponse() 45 | { 46 | ActualRequest = request, 47 | ExpectedRequests = _matchableInteractions.Select(m => m.Interaction.Request).ToList(), 48 | Message = "No matching response set up for this request." 49 | }; 50 | httpResponseToReturn.StatusCode = 400; 51 | stringToReturn = JsonConvert.SerializeObject(errorResponse); 52 | } 53 | await httpResponseToReturn.Body.WriteAsync(Encoding.UTF8.GetBytes(stringToReturn), 0, Encoding.UTF8.GetByteCount(stringToReturn)); 54 | } 55 | 56 | public bool AllHaveBeenMatched() 57 | { 58 | return _matchableInteractions.All(m => m.HasBeenMatched); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ComPact/MockProvider/RequestResponseMatchingErrorResponse.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models.V3; 2 | using Newtonsoft.Json; 3 | using System.Collections.Generic; 4 | 5 | namespace ComPact.MockProvider 6 | { 7 | internal class RequestResponseMatchingErrorResponse 8 | { 9 | [JsonProperty("message")] 10 | internal string Message { get; set; } 11 | [JsonProperty("actualRequests")] 12 | internal Request ActualRequest { get; set; } 13 | [JsonProperty("expectedRequests")] 14 | internal List ExpectedRequests { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ComPact/Models/ContractWithSomeVersion.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace ComPact.Models 4 | { 5 | internal class ContractWithSomeVersion 6 | { 7 | [JsonProperty("metadata")] 8 | internal Metadata Metadata { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ComPact/Models/Headers.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | 6 | namespace ComPact.Models 7 | { 8 | public class Headers : Dictionary 9 | { 10 | public Headers() { } 11 | 12 | internal Headers(IHeaderDictionary headers) 13 | { 14 | if (headers == null) 15 | { 16 | throw new System.ArgumentNullException(nameof(headers)); 17 | } 18 | 19 | foreach (var header in headers) 20 | { 21 | Add(header.Key, string.Join(",", header.Value)); 22 | } 23 | } 24 | 25 | internal Headers(HttpResponseMessage response) 26 | { 27 | if (response == null) 28 | { 29 | throw new System.ArgumentNullException(nameof(response)); 30 | } 31 | 32 | response.Headers.ToList().ForEach(h => Add(h.Key, string.Join(",", h.Value))); 33 | response.Content?.Headers.ToList().ForEach(h => Add(h.Key, string.Join(",", h.Value))); 34 | } 35 | 36 | internal bool Match(Headers actualHeaders) 37 | { 38 | if (actualHeaders == null) 39 | { 40 | throw new System.ArgumentNullException(nameof(actualHeaders)); 41 | } 42 | 43 | return this.All(h => actualHeaders.Any(a => h.Key == a.Key && h.Value == a.Value)); 44 | } 45 | 46 | internal List Match(Headers actualHeaders, MatchingRuleCollection matchingRules) 47 | { 48 | if (actualHeaders == null) 49 | { 50 | throw new System.ArgumentNullException(nameof(actualHeaders)); 51 | } 52 | 53 | var differences = new List(); 54 | foreach(var expectedHeader in KeysToLowerCase(this)) 55 | { 56 | if (KeysToLowerCase(actualHeaders).TryGetValue(expectedHeader.Key, out var actualHeaderValue)) 57 | { 58 | var expectedParts = SplitValueIntoParts(expectedHeader.Value); 59 | var actualParts = SplitValueIntoParts(actualHeaderValue); 60 | 61 | if (matchingRules?.Header != null && KeysToLowerCase(matchingRules.Header).TryGetValue(expectedHeader.Key, out var matchers)) 62 | { 63 | differences.AddRange(matchers.Match(expectedHeader.Value, actualHeaderValue)); 64 | } 65 | else if (!expectedParts.All(e => actualParts.Any(a => RemoveWhiteSpaceAfterCommas(a) == RemoveWhiteSpaceAfterCommas(e)))) 66 | { 67 | differences.Add($"Expected {expectedHeader.Value} for {expectedHeader.Key}, but was {actualHeaderValue}"); 68 | } 69 | } 70 | else 71 | { 72 | differences.Add($"Expected a header named {expectedHeader.Key}, but was not found."); 73 | } 74 | } 75 | 76 | return differences; 77 | } 78 | 79 | private Dictionary KeysToLowerCase(Dictionary headers) 80 | { 81 | return headers.ToDictionary(h => h.Key.ToLowerInvariant(), h => h.Value); 82 | } 83 | 84 | private List SplitValueIntoParts(string value) 85 | { 86 | return value.Split(';').Select(p => p.Trim()).ToList(); 87 | } 88 | 89 | private string RemoveWhiteSpaceAfterCommas(string value) 90 | { 91 | return string.Join(",", value.Split(',').Select(x => x.Trim())); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ComPact/Models/IContract.cs: -------------------------------------------------------------------------------- 1 | namespace ComPact.Models 2 | { 3 | internal interface IContract 4 | { 5 | Pacticipant Consumer { get; set; } 6 | Pacticipant Provider { get; set; } 7 | void SetEmptyValuesToNull(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ComPact/Models/Matcher.cs: -------------------------------------------------------------------------------- 1 | using ComPact.JsonHelpers; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace ComPact.Models 8 | { 9 | internal class Matcher 10 | { 11 | [JsonProperty("match")] 12 | [JsonConverter(typeof(StringEnumWithDefaultConverter))] 13 | internal MatcherType MatcherType { get; set; } 14 | [JsonProperty("min")] 15 | internal int? Min { get; set; } 16 | [JsonProperty("max")] 17 | internal int? Max { get; set; } 18 | [JsonProperty("regex")] 19 | internal string Regex { get; set; } 20 | [JsonProperty("value")] 21 | internal string Value { get; set; } 22 | 23 | internal List Match(JToken expectedToken, JToken actualToken) 24 | { 25 | var differences = new List(); 26 | 27 | if (MatcherType == MatcherType.type && !expectedToken.IsSameJsonTypeAs(actualToken)) 28 | { 29 | differences.Add($"Expected value of type {expectedToken.Type} (like: \'{expectedToken}\') at {expectedToken.Path}, but was value of type {actualToken.Type}."); 30 | } 31 | if (Regex != null && System.Text.RegularExpressions.Regex.Match(actualToken.Value(), Regex).Value != actualToken.Value()) 32 | { 33 | differences.Add($"Expected value matching \'{Regex}\' (like: \'{expectedToken.Value()}\') at {expectedToken.Path}, but was \'{actualToken.Value()}\'."); 34 | } 35 | if (MatcherType == MatcherType.include && !actualToken.Value().Contains(Value)) 36 | { 37 | differences.Add($"Expected value at {expectedToken.Path} to include '{Value}', but was {actualToken.Value()}."); 38 | } 39 | if (MatcherType == MatcherType.integer && actualToken.Type != JTokenType.Integer) 40 | { 41 | differences.Add($"Expected integer (like: \'{expectedToken.Value()}\') at {expectedToken.Path}, but was {actualToken.ToString()}."); 42 | } 43 | if (MatcherType == MatcherType.@decimal && actualToken.Type != JTokenType.Float) 44 | { 45 | differences.Add($"Expected decimal (like: \'{expectedToken.Value()}\') at {expectedToken.Path}, but was {actualToken.ToString()}."); 46 | } 47 | if (MatcherType == MatcherType.@null && actualToken.Type != JTokenType.Null) 48 | { 49 | differences.Add($"Expected null at {expectedToken.Path}, but was {actualToken}."); 50 | } 51 | 52 | if (expectedToken.Type == JTokenType.Array) 53 | { 54 | if (Min != null && actualToken.Children().Count() < Min) 55 | { 56 | differences.Add($"Expected an array with at least {Min} item(s) at {expectedToken.Path}, but was {actualToken.Children().Count()} items(s)."); 57 | } 58 | if (Max != null && actualToken.Children().Count() > Max) 59 | { 60 | differences.Add($"Expected an array with at most {Max} item(s) at {expectedToken.Path}, but was {actualToken.Children().Count()} items(s)."); 61 | } 62 | } 63 | 64 | return differences; 65 | } 66 | 67 | internal List Match(object expectedValue, object actualValue) 68 | { 69 | return Match(JToken.FromObject(expectedValue), JToken.FromObject(actualValue)); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ComPact/Models/MatcherList.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace ComPact.Models 7 | { 8 | public class MatcherList 9 | { 10 | [JsonProperty("combine")] 11 | internal string Combine { get; set; } 12 | [JsonProperty("matchers")] 13 | internal List Matchers { get; set; } 14 | 15 | internal List Match(JToken expectedToken, JToken actualToken) 16 | { 17 | var differences = new List(); 18 | 19 | var anySuccessfulMatchers = false; 20 | foreach (var matcher in Matchers) 21 | { 22 | var matcherDifferences = matcher.Match(expectedToken, actualToken); 23 | if (matcherDifferences.Any()) 24 | { 25 | differences.AddRange(matcherDifferences); 26 | } 27 | else 28 | { 29 | anySuccessfulMatchers = true; 30 | } 31 | } 32 | 33 | if ((Combine != "OR" && differences.Any()) || !anySuccessfulMatchers) 34 | { 35 | return differences; 36 | } 37 | return new List(); 38 | } 39 | 40 | internal List Match(object expectedValue, object actualValue) 41 | { 42 | return Match(JToken.FromObject(expectedValue), JToken.FromObject(actualValue)); 43 | } 44 | 45 | internal bool ImpliesEquality() 46 | { 47 | if (Combine != "OR" && Matchers.Any(m => m.MatcherType == MatcherType.equality)) 48 | { 49 | return true; 50 | } 51 | if (Matchers.All(m => m.MatcherType == MatcherType.equality)) 52 | { 53 | return true; 54 | } 55 | return false; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ComPact/Models/MatcherType.cs: -------------------------------------------------------------------------------- 1 | namespace ComPact.Models 2 | { 3 | public enum MatcherType 4 | { 5 | type, 6 | equality, 7 | regex, 8 | integer, 9 | @decimal, 10 | include, 11 | @null 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ComPact/Models/MatchingRuleCollection.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace ComPact.Models 7 | { 8 | internal class MatchingRuleCollection 9 | { 10 | [JsonProperty("header")] 11 | internal Dictionary Header { get; set; } = new Dictionary(); 12 | [JsonProperty("body")] 13 | internal Dictionary Body { get; set; } = new Dictionary(); 14 | 15 | internal MatchingRuleCollection() { } 16 | 17 | internal MatchingRuleCollection(Dictionary matchingRules) 18 | { 19 | if (matchingRules == null) 20 | { 21 | throw new System.ArgumentNullException(nameof(matchingRules)); 22 | } 23 | 24 | foreach (var rule in matchingRules.Where(m => m.Key.StartsWith("$.body"))) 25 | { 26 | Body.Add(rule.Key.Replace(".body", ""), new MatcherList { Matchers = new List { rule.Value } }); 27 | } 28 | 29 | foreach (var rule in matchingRules.Where(m => m.Key.StartsWith("$.headers"))) 30 | { 31 | Header.Add(rule.Key.Replace("$.headers.", ""), new MatcherList { Matchers = new List { rule.Value } }); 32 | } 33 | } 34 | 35 | internal bool TryGetApplicableMatcherListForToken(JToken token, out MatcherList matcherList) 36 | { 37 | var matchingPaths = new List(); 38 | foreach (var path in Body.Select(b => b.Key)) 39 | { 40 | var matchingTokens = token.Root.SelectTokens(path).ToList(); 41 | if (matchingTokens.Select(t => t.Path).Intersect(token.AncestorsAndSelf().Select(t => t.Path)).Any()) 42 | { 43 | matchingPaths.Add(path); 44 | } 45 | } 46 | 47 | if (!matchingPaths.Any()) 48 | { 49 | matcherList = null; 50 | return false; 51 | } 52 | 53 | var orderedPaths = matchingPaths.OrderBy(m => m, new MatchingRulePathComparer()).ToList(); 54 | matcherList = Body[orderedPaths.Last()]; 55 | if (matcherList.ImpliesEquality()) 56 | { 57 | return false; 58 | } 59 | return true; 60 | } 61 | 62 | internal void SetEmptyValuesToNull() 63 | { 64 | Body = Body?.FirstOrDefault() != null ? Body : null; 65 | Header = Header?.FirstOrDefault() != null ? Header : null; 66 | } 67 | } 68 | 69 | internal class MatchingRulePathComparer : IComparer 70 | { 71 | public int Compare(string x, string y) 72 | { 73 | var lengthComparison = x.Length.CompareTo(y.Length); 74 | if (lengthComparison == 0) 75 | { 76 | return y.Count(c => c == '*').CompareTo(x.Count(c => c == '*')); 77 | } 78 | else 79 | { 80 | return lengthComparison; 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ComPact/Models/Metadata.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace ComPact.Models 4 | { 5 | internal class Metadata 6 | { 7 | [JsonProperty("pactSpecification")] 8 | public PactSpecification PactSpecification { get; set; } 9 | [JsonProperty("pact-specification")] 10 | public PactSpecification PactSpecificationWithDash { get; set; } 11 | [JsonProperty("pactSpecificationVersion")] 12 | public string PactSpecificationVersion { get; set; } 13 | 14 | public SpecificationVersion GetVersion() 15 | { 16 | var stringVersion = PactSpecification?.Version ?? PactSpecificationWithDash?.Version ?? PactSpecificationVersion; 17 | if (stringVersion.StartsWith("2")) 18 | { 19 | return SpecificationVersion.Two; 20 | } 21 | if (stringVersion.StartsWith("3")) 22 | { 23 | return SpecificationVersion.Three; 24 | } 25 | return SpecificationVersion.Unsupported; 26 | } 27 | } 28 | 29 | internal class PactSpecification 30 | { 31 | [JsonProperty("version")] 32 | public string Version { get; set; } 33 | } 34 | 35 | internal enum SpecificationVersion 36 | { 37 | Two, 38 | Three, 39 | Unsupported 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /ComPact/Models/Method.cs: -------------------------------------------------------------------------------- 1 | namespace ComPact.Models 2 | { 3 | public enum Method 4 | { 5 | CONNECT, 6 | DELETE, 7 | GET, 8 | HEAD, 9 | OPTIONS, 10 | POST, 11 | PUT, 12 | TRACE 13 | } 14 | } -------------------------------------------------------------------------------- /ComPact/Models/Pacticipant.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace ComPact.Models 4 | { 5 | internal class Pacticipant 6 | { 7 | [JsonProperty("name")] 8 | public string Name { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ComPact/Models/ProviderState.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | 4 | namespace ComPact.Models 5 | { 6 | public class ProviderState 7 | { 8 | [JsonProperty("name")] 9 | public string Name { get; set; } 10 | [JsonProperty("params")] 11 | public Dictionary Params { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ComPact/Models/V2/Contract.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | 4 | namespace ComPact.Models.V2 5 | { 6 | internal class Contract: IContract 7 | { 8 | [JsonProperty("consumer")] 9 | public Pacticipant Consumer { get; set; } 10 | [JsonProperty("provider")] 11 | public Pacticipant Provider { get; set; } 12 | [JsonProperty("interactions")] 13 | internal List Interactions { get; set; } 14 | [JsonProperty("metadata")] 15 | internal Metadata Metadata { get; set; } = new Metadata { PactSpecification = new PactSpecification { Version = "2.0.0" } }; 16 | public void SetEmptyValuesToNull() 17 | { 18 | Interactions.ForEach(i => i.SetEmptyValuesToNull()); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ComPact/Models/V2/Interaction.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace ComPact.Models.V2 4 | { 5 | internal class Interaction 6 | { 7 | [JsonProperty("description")] 8 | public string Description { get; set; } = string.Empty; 9 | [JsonProperty("providerState")] 10 | public string ProviderState { get; set; } 11 | [JsonProperty("request")] 12 | public Request Request { get; set; } = new Request(); 13 | [JsonProperty("response")] 14 | public Response Response { get; set; } = new Response(); 15 | 16 | public Response Match(Models.V3.Request actualRequest) 17 | { 18 | if (new V3.Request(Request).Match(actualRequest)) 19 | { 20 | return Response; 21 | } 22 | return null; 23 | } 24 | 25 | internal void SetEmptyValuesToNull() 26 | { 27 | ProviderState = string.IsNullOrWhiteSpace(ProviderState) ? null : ProviderState; 28 | Request.SetEmptyValuesToNull(); 29 | Response.SetEmptyValuesToNull(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ComPact/Models/V2/Request.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Converters; 3 | using System.Linq; 4 | 5 | namespace ComPact.Models.V2 6 | { 7 | public class Request 8 | { 9 | [JsonProperty("method")] 10 | [JsonConverter(typeof(StringEnumConverter))] 11 | public Method Method { get; set; } 12 | [JsonProperty("path")] 13 | public string Path { get; set; } = "/"; 14 | [JsonProperty("headers")] 15 | public Headers Headers { get; set; } = new Headers(); 16 | [JsonProperty("query")] 17 | public string Query { get; set; } 18 | [JsonProperty("body")] 19 | public dynamic Body { get; set; } 20 | 21 | public Request() { } 22 | 23 | internal void SetEmptyValuesToNull() 24 | { 25 | Headers = Headers.Any() ? Headers : null; 26 | Query = string.IsNullOrWhiteSpace(Query) ? null : Query; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /ComPact/Models/V2/Response.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ComPact.Models.V2 6 | { 7 | public class Response 8 | { 9 | [JsonProperty("status")] 10 | public int Status { get; set; } = 200; 11 | [JsonProperty("headers")] 12 | public Headers Headers { get; set; } = new Headers(); 13 | [JsonProperty("body")] 14 | public dynamic Body { get; set; } 15 | [JsonProperty("matchingRules")] 16 | internal Dictionary MatchingRules { get; set; } = new Dictionary(); 17 | 18 | internal Response() 19 | { 20 | } 21 | 22 | internal void SetEmptyValuesToNull() 23 | { 24 | Headers = Headers.Any() ? Headers : null; 25 | MatchingRules = MatchingRules.Any() ? MatchingRules : null; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ComPact/Models/V3/Contract.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ComPact.Models.V3 6 | { 7 | internal class Contract: IContract 8 | { 9 | [JsonProperty("consumer")] 10 | public Pacticipant Consumer { get; set; } 11 | [JsonProperty("provider")] 12 | public Pacticipant Provider { get; set; } 13 | [JsonProperty("interactions")] 14 | internal List Interactions { get; set; } 15 | [JsonProperty("messages")] 16 | internal List Messages { get; set; } 17 | [JsonProperty("metadata")] 18 | internal Metadata Metadata { get; set; } = new Metadata { PactSpecification = new PactSpecification { Version = "3.0.0" } }; 19 | 20 | internal Contract() { } 21 | internal Contract(V2.Contract contract) 22 | { 23 | Consumer = contract?.Consumer ?? throw new System.ArgumentNullException(nameof(contract)); 24 | Provider = contract.Provider; 25 | Interactions = contract.Interactions.Select(i => new Interaction(i)).ToList(); 26 | } 27 | 28 | public void SetEmptyValuesToNull() 29 | { 30 | Interactions.ForEach(i => i.SetEmptyValuesToNull()); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ComPact/Models/V3/Interaction.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace ComPact.Models.V3 7 | { 8 | internal class Interaction 9 | { 10 | [JsonProperty("description")] 11 | public string Description { get; set; } = string.Empty; 12 | [JsonProperty("providerStates")] 13 | public List ProviderStates { get; set; } 14 | [JsonProperty("request")] 15 | public Request Request { get; set; } = new Request(); 16 | [JsonProperty("response")] 17 | public Response Response { get; set; } = new Response(); 18 | 19 | public Interaction() { } 20 | public Interaction(V2.Interaction interaction) 21 | { 22 | Description = interaction?.Description ?? throw new ArgumentNullException(nameof(interaction)); 23 | ProviderStates = new List { new ProviderState { Name = interaction.ProviderState } }; 24 | Request = new Request(interaction.Request); 25 | Response = new Response(interaction.Response); 26 | } 27 | 28 | public Response Match(Request actualRequest) 29 | { 30 | if (Request.Match(actualRequest)) 31 | { 32 | return Response; 33 | } 34 | return null; 35 | } 36 | 37 | internal bool ProviderStatesAndRequestCanBeDistinguishedFrom(Interaction otherInteraction) 38 | { 39 | var requestsCanBeDistinguished = !(Request.Match(otherInteraction.Request) && otherInteraction.Request.Match(Request)); 40 | var providerStatesCanBeDistinguished = JsonConvert.SerializeObject(ProviderStates) != JsonConvert.SerializeObject(otherInteraction.ProviderStates); 41 | 42 | return requestsCanBeDistinguished || providerStatesCanBeDistinguished; 43 | } 44 | 45 | internal void SetEmptyValuesToNull() 46 | { 47 | ProviderStates = ProviderStates?.FirstOrDefault() != null ? ProviderStates : null; 48 | Request.SetEmptyValuesToNull(); 49 | Response.SetEmptyValuesToNull(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ComPact/Models/V3/Message.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ComPact.Models.V3 6 | { 7 | internal class Message 8 | { 9 | [JsonProperty("providerStates")] 10 | internal List ProviderStates { get; set; } 11 | [JsonProperty("description")] 12 | internal string Description { get; set; } = string.Empty; 13 | [JsonProperty("contents")] 14 | internal object Contents { get; set; } 15 | [JsonProperty("matchingRules")] 16 | internal MatchingRuleCollection MatchingRules { get; set; } 17 | [JsonProperty("metaData")] 18 | internal object Metadata { get; set; } = new { ContentType = "application/json" }; 19 | 20 | internal List Match(object actualMessage) 21 | { 22 | return BodyMatcher.Match(Contents, actualMessage, MatchingRules); 23 | } 24 | 25 | internal void SetEmptyValuesToNull() 26 | { 27 | ProviderStates = ProviderStates?.FirstOrDefault() != null ? ProviderStates : null; 28 | if (MatchingRules != null) 29 | { 30 | MatchingRules.SetEmptyValuesToNull(); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ComPact/Models/V3/MessageContract.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | 4 | namespace ComPact.Models.V3 5 | { 6 | internal class MessageContract : IContract 7 | { 8 | [JsonProperty("consumer")] 9 | public Pacticipant Consumer { get; set; } 10 | [JsonProperty("provider")] 11 | public Pacticipant Provider { get; set; } 12 | [JsonProperty("messages")] 13 | internal List Messages { get; set; } 14 | [JsonProperty("metadata")] 15 | internal Metadata Metadata { get; set; } = new Metadata { PactSpecification = new PactSpecification { Version = "3.0.0" } }; 16 | public void SetEmptyValuesToNull() 17 | { 18 | Messages.ForEach(i => i.SetEmptyValuesToNull()); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ComPact/Models/V3/Query.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ComPact.Models.V3 6 | { 7 | internal class Query : Dictionary> 8 | { 9 | internal Query() { } 10 | 11 | internal Query(string queryString) 12 | { 13 | if (!string.IsNullOrWhiteSpace(queryString)) 14 | { 15 | var parameters = queryString.Split('&'); 16 | foreach (var param in parameters) 17 | { 18 | var splitParam = param.Split('='); 19 | var valuesToAdd = splitParam[1].Split(',').ToList(); 20 | if (TryGetValue(splitParam[0], out var existingValues)) 21 | { 22 | Remove(splitParam[0]); 23 | valuesToAdd.AddRange(existingValues); 24 | } 25 | Add(splitParam[0], valuesToAdd); 26 | } 27 | } 28 | } 29 | 30 | internal Query(IQueryCollection queryCollection) 31 | { 32 | queryCollection.ToList().ForEach(q => Add(q.Key, q.Value.ToList())); 33 | } 34 | 35 | internal bool Match(Query actualQuery) 36 | { 37 | if (Count != actualQuery.Count) 38 | { 39 | return false; 40 | } 41 | foreach (var expectedParam in this) 42 | { 43 | var existsInActual = actualQuery.TryGetValue(expectedParam.Key, out var actualValues); 44 | if (!existsInActual) 45 | { 46 | return false; 47 | } 48 | if (expectedParam.Value.Count != actualValues.Count) 49 | { 50 | return false; 51 | } 52 | if (string.Join(",", expectedParam.Value) != string.Join(",", actualValues)) 53 | { 54 | return false; 55 | } 56 | } 57 | 58 | return true; 59 | } 60 | 61 | internal string ToQueryString() 62 | { 63 | var queryString = string.Join("&", this.Select(q => q.Key + "=" + string.Join(",", q.Value))); 64 | return queryString; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ComPact/Models/V3/Response.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Exceptions; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Net.Http; 7 | 8 | namespace ComPact.Models.V3 9 | { 10 | internal class Response 11 | { 12 | [JsonProperty("status")] 13 | public int Status { get; set; } = 200; 14 | [JsonProperty("headers")] 15 | public Headers Headers { get; set; } = new Headers(); 16 | [JsonProperty("body")] 17 | public dynamic Body { get; set; } 18 | [JsonProperty("matchingRules")] 19 | internal MatchingRuleCollection MatchingRules { get; set; } 20 | 21 | internal Response() { } 22 | 23 | internal Response(V2.Response responseV2) 24 | { 25 | if (responseV2 == null) 26 | { 27 | throw new System.ArgumentNullException(nameof(responseV2)); 28 | } 29 | 30 | Status = responseV2.Status; 31 | Headers = responseV2.Headers; 32 | Body = responseV2.Body; 33 | if (responseV2.MatchingRules != null) 34 | { 35 | MatchingRules = new MatchingRuleCollection(responseV2.MatchingRules); 36 | } 37 | } 38 | 39 | internal Response(HttpResponseMessage response) 40 | { 41 | if (response == null) 42 | { 43 | throw new ArgumentNullException(nameof(response)); 44 | } 45 | 46 | Status = (int)response?.StatusCode; 47 | Headers = new Headers(response); 48 | try 49 | { 50 | Body = JsonConvert.DeserializeObject(response.Content?.ReadAsStringAsync().Result ?? string.Empty); 51 | } 52 | catch (JsonReaderException) 53 | { 54 | throw new PactException($"Response body could not be deserialized from JSON. Content-Type was {response.Content.Headers.ContentType}"); 55 | } 56 | } 57 | 58 | internal List Match(Response actualResponse) 59 | { 60 | var differences = new List(); 61 | 62 | if (Status != actualResponse.Status) 63 | { 64 | differences.Add($"Expected status {Status}, but was {actualResponse.Status}"); 65 | } 66 | 67 | differences.AddRange(Headers.Match(actualResponse.Headers, MatchingRules)); 68 | 69 | differences.AddRange(Models.BodyMatcher.Match(Body, actualResponse.Body, MatchingRules)); 70 | 71 | return differences; 72 | } 73 | 74 | internal void SetEmptyValuesToNull() 75 | { 76 | Headers = Headers.Any() ? Headers : null; 77 | if (MatchingRules != null) 78 | { 79 | MatchingRules.SetEmptyValuesToNull(); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /ComPact/Verifier/PactVerifierConfig.cs: -------------------------------------------------------------------------------- 1 | using ComPact.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Net.Http; 5 | 6 | namespace ComPact.Verifier 7 | { 8 | public class PactVerifierConfig 9 | { 10 | /// 11 | /// The base url where to call your actual provider service. To set up provider states, {base-url}/provider-states will be called. 12 | /// 13 | public string ProviderBaseUrl { get; set; } 14 | 15 | /// 16 | /// Optional HttpClient for the provider in case you want to supply your own preconfigured instance. 17 | /// 18 | public HttpClient ProviderHttpClient { get; set; } 19 | 20 | /// 21 | /// An action that will be invoked for every provider state in a (message) interaction. Use this to set up any necessary test data. 22 | /// If you cannot handle a specific provider state, throw a PactVerificationException to make the issue show up in the test results 23 | /// that are published to the Pact Broker. 24 | /// 25 | public Action ProviderStateHandler { get; set; } 26 | /// 27 | /// For message interactions, this function will be called with the description defined in the contract as a parameter. 28 | /// It should return the message that your code produces, so the mock consumer can verify it. 29 | /// If a string is returned, the mock consumer will assume that it is a serialized json string and try to deserialize it. 30 | /// If you cannot handle a specific description, throw a PactVerificationException to make the issue show up in the test results 31 | /// that are published to the Pact Broker. 32 | /// 33 | public Func MessageProducer { get; set; } 34 | /// 35 | /// Client that can be used to connect to your Pact Broker to retrieve pacts and (optionally) publish verification results. 36 | /// Should be set up with the correct base URL and if needed any necessary headers. 37 | /// 38 | public HttpClient PactBrokerClient { get; set; } 39 | /// 40 | /// Whether to actually publish the verification results. 41 | /// 42 | public bool PublishVerificationResults { get; set; } 43 | /// 44 | /// The provider version to use when publishing verification results. 45 | /// 46 | public string ProviderVersion { get; set; } 47 | /// 48 | /// Tags to add to this version of the provider. 49 | /// 50 | public IEnumerable ProviderTags { get; set; } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ComPact/Verifier/VerificationResults.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace ComPact.Verifier 8 | { 9 | public class VerificationResults 10 | { 11 | [JsonProperty("providerName")] 12 | public string ProviderName { get; set; } 13 | [JsonProperty("providerApplicationVersion")] 14 | public string ProviderApplicationVersion { get; set; } 15 | [JsonProperty("success")] 16 | public bool Success { get; set; } 17 | [JsonProperty("verificationDate")] 18 | public string VerificationDate { get; set; } 19 | [JsonProperty("testResults")] 20 | public TestResults TestResults { get; set; } 21 | } 22 | 23 | public class TestResults 24 | { 25 | [JsonProperty("summary")] 26 | public Summary Summary { get; set; } 27 | [JsonProperty("tests")] 28 | public List Tests { get; set; } 29 | } 30 | 31 | public class Summary 32 | { 33 | [JsonProperty("testCount")] 34 | public int TestCount { get; set; } 35 | [JsonProperty("failureCount")] 36 | public int FailureCount { get; set; } 37 | } 38 | 39 | public class Test 40 | { 41 | [JsonProperty("testDescription")] 42 | public string Description { get; set; } 43 | [JsonProperty("success")] 44 | public string Status => Issues.Any() ? "failed" : "passed"; 45 | [JsonProperty("issues")] 46 | public List Issues { get; set; } = new List(); 47 | 48 | public string ToTestMessageString() 49 | { 50 | var stringBuilder = new StringBuilder(Description); 51 | stringBuilder.Append($" ({Status})"); 52 | if (Status == "failed") 53 | { 54 | stringBuilder.Append(":"); 55 | foreach (var issues in Issues) 56 | { 57 | stringBuilder.Append(Environment.NewLine); 58 | stringBuilder.Append("- "); 59 | stringBuilder.Append(issues); 60 | } 61 | } 62 | return stringBuilder.ToString(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Bart Schotten 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | --------------------------------------------------------------------------------