Prakhar Roy
11/06/2023, 1:10 PMNuno Domingues
11/10/2023, 12:25 AMBas Dijkstra
11/10/2023, 5:25 AMNuno Domingues
11/10/2023, 10:48 AMNuno Domingues
11/10/2023, 10:56 AMnamespace providertests
{
[TestFixture]
public class VerifyPact
{
private readonly string _url = "<http://127.0.0.1/9001>";
private readonly Uri _Uri = new Uri("<http://127.0.0.1/9001>");
public VerifyPact() { }
[Test]
public void Verify_That_Address_Service_Honours_Pacts()
{
var config = new PactVerifierConfig
{
LogLevel = PactNet.PactLogLevel.Debug
};
using (var _webHost = WebHost.CreateDefaultBuilder().UseUrls(_url).Build())
{
_webHost.Start();
//Assert
IPactVerifier pactVerifier = new PactVerifier(config);
var pactFile = new FileInfo(Path.Join("..", "..", "..", "..", "consumertests", "pacts", "consumer-provider.json"));
pactVerifier
.ServiceProvider("provider", _Uri)
.WithFileSource(pactFile)
.Verify();
}
}
}
}
This is the code, that i'm using to verify the provider with the pact file generatedNuno Domingues
11/10/2023, 10:58 AMNuno Domingues
11/10/2023, 10:59 AMusing provider.model;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at <https://aka.ms/aspnetcore/swashbuckle>
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
List<Jokes> jokesStorage = new List<Jokes>
{
new Jokes (0,"Why do programmers prefer dark mode? -Because the light attracts bugs!","Jonas" ),
new Jokes (1,"Why did the programmer break up with their keyboard? -They had too many arguments.","Peter" ),
};
app.MapGet("/jokes", () =>
{
var allJokes = jokesStorage.ToArray();
if (allJokes.Count() > 0)
return Results.Ok(jokesStorage);
else
return Results.NoContent();
})
.WithName("GetJokes")
.Produces<Jokes>(StatusCodes.Status200OK)
.Produces<string>(StatusCodes.Status204NoContent, contentType: "text/plain");
app.MapGet("/jokes/{id}", (string id) =>
{
int _id = 0;
if (!(int.TryParse(id, out _id)))
{
return Results.BadRequest("BadRequest!");
}
Jokes joke = jokesStorage.FirstOrDefault(j => j.Id == _id);
if (joke != null)
{
return Results.Ok(joke);
}
return Results.NotFound("Joke with the id provided not found!");
})
.WithName("GetJokesId")
.Produces<Jokes>(StatusCodes.Status200OK, contentType: "application/json")
.Produces<string>(StatusCodes.Status404NotFound, contentType: "text/plain")
.Produces<string>(StatusCodes.Status400BadRequest, contentType: "text/plain");
app.MapPost("/jokes", (Jokes entity) =>
{
Console.WriteLine(entity);
if (entity is Jokes joke && entity.Id == 0 && entity.Joke != null && entity.Bywho != null)
{
var lastid = GetLastJokesId();
joke.Id = (lastid != -1) ? ++lastid : 0;
jokesStorage.Add(joke);
return Results.Created($"/jokes/{joke.Id}", joke);
}
return Results.BadRequest("BadRequest!");
})
.WithName("PostJoke")
.Accepts<Jokes>("application/json")
.Produces<Jokes>(StatusCodes.Status201Created)
.Produces<string>(StatusCodes.Status400BadRequest, contentType: "text/plain")
.Produces<string>(StatusCodes.Status415UnsupportedMediaType, contentType: "text/plain");
app.MapDelete("/jokes/{id}", (string id) =>
{
int _id = 0;
if (!(int.TryParse(id, out _id)))
{
return Results.BadRequest("BadRequest!");
}
Jokes joke = jokesStorage.FirstOrDefault(j => j.Id == _id);
if (joke != null)
{
return Results.Ok(joke);
}
return Results.NotFound("Joke with the id provided not found!");
})
.WithName("DeleteJoke")
.Produces<Jokes>(StatusCodes.Status200OK, contentType: "application/json")
.Produces<string>(StatusCodes.Status404NotFound, contentType: "text/plain")
.Produces<string>(StatusCodes.Status400BadRequest, contentType: "text/plain");
app.Run();
int GetLastJokesId()
{
if (jokesStorage.Count > 0)
{
Jokes lastJoke = jokesStorage[jokesStorage.Count - 1];
return lastJoke.Id;
}
else
{
return -1;
}
}
Bas Dijkstra
11/10/2023, 9:43 PMBas Dijkstra
11/10/2023, 9:43 PMNuno Domingues
11/10/2023, 11:24 PMNuno Domingues
11/10/2023, 11:25 PMChandra Muddam
11/11/2023, 7:41 AMNuno Domingues
11/15/2023, 11:13 AMSteve Blomeley
11/15/2023, 3:34 PMGitHub
11/16/2023, 3:32 PMconst provider = new Pact({
consumer: "consumer-js-v2",
provider: "provider-js-v2",
host: "127.0.0.1",
port: 9999
});By comparison, I cannot specify those same properties in Pact-Net...
var config = new PactConfig
{
PactDir = PactDir,
Outputters = new List { new XunitOutput(output), new ConsoleOutput() },
LogLevel = PactLogLevel.Trace
};
_pact = PactNet.Pact.V3(_consumerName, _providerName, config).WithHttpInteractions();pact-foundation/pact-net
Prakhar Roy
11/17/2023, 8:27 AMThere is no verified pact between version 1.0.0 of Consumer and the latest version of Producer (c8169874d39b17ac0ca748dcd2fd)
Wanted to know how we can incorporate git sh in our code.
pactVerifier
.ServiceProvider("City_Provider_API", new Uri(_providerServiceUri))
.WithUriSource(new Uri(pactUri), options =>
{
options.TokenAuthentication("Uiv1OaU4O1wOXX0GoIfLAA");
options.PublishResults(true, "1.0.0", results => // Version -> CI_COMMIT_SH (something of this manner instead of 1.0.0)
{
results.ProviderBranch("Development");
});
})
boden winkel
11/21/2023, 10:29 AMGitHub
11/22/2023, 2:39 PMRafaela Azevedo
11/27/2023, 10:00 AMRafaela Azevedo
11/27/2023, 10:00 AMRafaela Azevedo
11/27/2023, 10:01 AMgiphy
11/27/2023, 10:01 AMRafaela Azevedo
11/27/2023, 10:01 AMDivya Parappanavar
11/27/2023, 1:28 PMAdam Schaff
11/28/2023, 4:38 AMLukasz
11/28/2023, 10:59 AMGET /api/document/{id}
. We would like to have contract tests for this as it happened in the past that the contact was broken. ID is guid
.
The question is how should we provide ID? Should it be the one existing in Service A or should it be any valid guid?
If the first one, the tests are becoming white box, right? Provider knows about the details & the data in Service A. But service A can take the guid from the contract, act on it and return the data. In this case, there is a temptation to check also the data returned, so you can achieve both functional tests and contract at once.
How should this work? How should you provide id
s for the data you would like to use? How should provider check it? On real data?
The same will also apply for resources with availability. Assuming you there is an endpoint Delete api/document/{id}
. Should you check it in contract tests? If yes, how? Same problem with guid, but also additional problem. If you execute it on any side of tests then the document with {id} will be removed, so you won’t be able to run this for the 2nd time.
We really love the tool but after some time we have many questions to ask, as we are not sure how to use it properly 👼
is there any expert in the house that could help us? 🤔Bas Dijkstra
11/28/2023, 3:46 PM[Test]
public async Task PostAddress_AddressIdIsValid()
{
Address address = new Address
{
Id = new Guid(addressIdNonexistent),
AddressType = "delivery",
Street = "Main Street",
Number = 123,
City = "Beverly Hills",
ZipCode = 90210,
State = "California",
Country = "United States"
};
pact.UponReceiving("A request to create an address by ID")
.Given("an address with ID {id} does not exist", new Dictionary<string, string> { ["id"] = addressIdNonexistent })
.WithRequest(HttpMethod.Post, $"/address")
.WithHeader("Content-Type", "application/json; charset=utf-8")
.WithJsonBody(address)
.WillRespond()
.WithStatus(HttpStatusCode.Created);
await pact.VerifyAsync(async ctx => {
var response = await client.PostAddress(address);
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Created));
});
}
This is my client implementation:
public async Task<HttpResponseMessage> PostAddress(Address address)
{
using (var client = new HttpClient { BaseAddress = baseUri })
{
try
{
var response = await client.PostAsync($"/address",
new StringContent(JsonSerializer.Serialize(address), Encoding.UTF8, "application/json"));
return response;
}
catch (Exception ex)
{
throw new Exception("There was a problem connecting to the AddressProvider API.", ex);
}
}
}
The actual request, as captured by Fiddler:
POST <http://localhost:9876/address> HTTP/1.1
Host: localhost:9876
Content-Type: application/json; charset=utf-8
Content-Length: 192
{"Id":"3514466e-3e58-48b3-ab35-e553b91aa2b3","AddressType":"delivery","Street":"Main Street","Number":123,"City":"Beverly Hills","ZipCode":90210,"State":"California","Country":"United States"}
And the response from the Pact mock provider:
HTTP/1.1 500 Internal Server Error
access-control-allow-origin: *
content-type: application/json; charset=utf-8
x-pact: Request-Mismatch
content-length: 747
date: Tue, 28 Nov 2023 15:53:22 GMT
{"error":"Request-Mismatch : HttpRequest { method: \"POST\", path: \"/address\", query: None, headers: Some({\"content-length\": [\"192\"], \"host\": [\"localhost:9876\"], \"content-type\": [\"application/json; charset=utf-8\"]}), body: Present(b\"{\\\"Id\\\":\\\"3514466e-3e58-48b3-ab35-e553b91aa2b3\\\",\\\"AddressType\\\":\\\"delivery\\\",\\\"Street\\\":\\\"Main Street\\\",\\\"Number\\\":123,\\\"City\\\":\\\"Beverly Hills\\\",\\\"ZipCode\\\":90210,\\\"State\\\":\\\"California\\\",\\\"Country\\\":\\\"United States\\\"}\", Some(ContentType { main_type: \"application\", sub_type: \"json\", attributes: {\"charset\": \"utf-8\"}, suffix: None }), None), matching_rules: MatchingRules { rules: {} }, generators: Generators { categories: {} } }"}
And the log output:
2023-11-28T15:53:22.876142Z INFO tokio-runtime-worker pact_mock_server::hyper_server: Received request HTTP Request ( method: POST, path: /address, query: None, headers: Some({"content-length": ["192"], "host": ["localhost:9876"], "content-type": ["application/json; charset=utf-8"]}), body: Present(192 bytes, application/json;charset=utf-8) )
2023-11-28T15:53:22.876349Z INFO tokio-runtime-worker pact_matching: comparing to expected HTTP Request ( method: POST, path: /address, query: None, headers: Some({"Content-Type": ["application/json; charset=utf-8"]}), body: Present(192 bytes, application/json) )
The only real difference I'm seeing is the capitalization of the Content-Type
header name, but Pact isn't that fussy, hopefully?
EDIT: it doesn't seem like it is, when I change the capitalization in the test to content-type
it still fails, here's the updated logging:
2023-11-28T16:11:08.276151Z INFO tokio-runtime-worker pact_mock_server::hyper_server: Received request HTTP Request ( method: POST, path: /address, query: None, headers: Some({"content-length": ["192"], "content-type": ["application/json; charset=utf-8"], "host": ["localhost:9876"]}), body: Present(192 bytes, application/json;charset=utf-8) )
2023-11-28T16:11:08.276441Z INFO tokio-runtime-worker pact_matching: comparing to expected HTTP Request ( method: POST, path: /address, query: None, headers: Some({"content-type": ["application/json; charset=utf-8"]}), body: Present(192 bytes, application/json) )
GitHub
11/29/2023, 2:51 AMHTTP/1.1 500 Internal Server Error
access-control-allow-origin: *
content-type: application/json; charset=utf-8
x-pact: Request-Mismatch
content-length: 747
date: Tue, 28 Nov 2023 15:53:22 GMT
{"error":"Request-Mismatch : HttpRequest { method: \"POST\", path: \"/address\", query: None, headers: Some({\"content-length\": [\"192\"], \"host\": [\"localhost:9876\"], \"content-type\": [\"application/json; charset=utf-8\"]}), body: Present(b\"{\\\"Id\\\":\\\"3514466e-3e58-48b3-ab35-e553b91aa2b3\\\",\\\"AddressType\\\":\\\"delivery\\\",\\\"Street\\\":\\\"Main Street\\\",\\\"Number\\\":123,\\\"City\\\":\\\"Beverly Hills\\\",\\\"ZipCode\\\":90210,\\\"State\\\":\\\"California\\\",\\\"Country\\\":\\\"United States\\\"}\", Some(ContentType { main_type: \"application\", sub_type: \"json\", attributes: {\"charset\": \"utf-8\"}, suffix: None }), None), matching_rules: MatchingRules { rules: {} }, generators: Generators { categories: {} } }"}
Ideally, this is pretty printed and logged to console (or whatever logging framework is configured). For example, in Pact JS it would be something like this:
Mock server failed with the following mismatches:
0) The following request was incorrect:
POST /test
1.0 $: Failed to parse the expected body as a MIME multipart body: 'no boundary in content-type'
pact-foundation/pact-netShiva Kumar
11/30/2023, 9:39 AM{
"providerName": "DocumentArtifact",
"providerApplicationVersion": "1",
"success": false,
"verificationDate": "2023-11-30T09:31:21+00:00",
"testResults": [
{
"exceptions": [
{
"message": "error sending request for url (<http://localhost:9223/api/v1/api/v1/artifact/d4ebdf49-fff0-4968-a6b0-e7bc27072c64>): error trying to connect: tcp connect error: No connection could be made because the target machine actively refused it. (os error 10061)"
}
],
"interactionId": "f7ba0fa997611c33ada03b17e453118d3dfbac56",
"success": false
}
],
"verifiedBy": {
"implementation": "Pact-Rust",
"version": "1.0.4"
},
Eddie Stanley
11/30/2023, 8:58 PM.Verify
if contract verification fails then a PactFailureException
is thrown. Will results still be published to the broker in this case, though? Is it safe to swallow this exception?