Skip to content

Common Mistakes

1. Reusing the shared client for a denial test

Section titled “1. Reusing the shared client for a denial test”

Wrong:

// The shared contract client carries X-User-Permissions: *
var response = await Client.GetAsync("/api/invoices/" + id);
response.ShouldBeForbidden(); // fails: the caller has every permission

Right:

using var request = new HttpRequestMessage(HttpMethod.Get, "/api/invoices/" + id);
request.AsUser("no-perm-user"); // empty permission header overrides the client default
var response = await Client.SendAsync(request);
response.ShouldBeRejected();

Why: HttpClient applies its DefaultRequestHeaders to every request that does not already carry them. Setting identity on the request is what makes an underprivileged caller actually underprivileged. AsUser on a request always writes the permission header — empty when you pass no permission — precisely so the default cannot leak in.


2. Reading ShouldBeRejected as “returned 403”

Section titled “2. Reading ShouldBeRejected as “returned 403””

Wrong assumption: a green _WithoutRequiredPermission_IsRejected proves the authorization filter ran and denied the call.

Right understanding: it proves the caller did not succeed. Any 4xx satisfies it — including a 400 from body binding and a 404 from an entity lookup, both of which happen before authorization on some endpoints.

Why: requiring a literal 403 would fail on endpoints that legitimately reject earlier. When you need the stronger guarantee, assert it explicitly:

response.ShouldHaveStatus(HttpStatusCode.Forbidden);

3. Expecting ShouldNotBeForbidden to mean “succeeded”

Section titled “3. Expecting ShouldNotBeForbidden to mean “succeeded””

Wrong:

var response = await Client.SendAsync(request);
response.ShouldNotBeForbidden();
var dto = await response.Content.ReadFromJsonAsync<GuestDto>(); // may be null: a 500 passes the assertion

Right:

response.ShouldBeOk();

Why: ShouldNotBeForbidden fails only on 401 and 403. A 404 passes — which is intended, since the generated contract uses a random id — but so does a 500. Use it for the authorization question only.


Symptom: every generated test throws

PragmaticContractHost.Client was not set. The consumer must define a collection fixture for the
'PragmaticContractTests' collection that boots the app and assigns it.

Why: the generated classes deliberately have no per-class setup — they read a client the consumer supplies once. Define the fixture as shown in Getting Started.


5. Expecting contract tests for an unimplemented endpoint

Section titled “5. Expecting contract tests for an unimplemented endpoint”

Symptom: an endpoint exists but no contract test was generated for it.

Why: endpoints whose body is still throw Behavior.Pending() are flagged [assembly: PendingContract] by the app’s source generator and skipped — a not-yet-implemented endpoint has no contract to hold it to. Implement the body and the tests appear on the next build.

Also check the endpoint actually requires a permission: authorization contracts are only generated for endpoints that declare one, and rejection tests are skipped for list/search GETs, which data-scope instead of rejecting.


6. Testing more than one application in the same test process

Section titled “6. Testing more than one application in the same test process”

Symptom: tests interfere; the client points at the wrong app.

Why: PragmaticContractHost.Client is a single static, set once by the collection fixture. That is what lets the generated classes avoid per-class wiring, but it means one application under test per test assembly. Split into separate test projects if you need two.