Maksym Liannoi
09/05/2022, 10:14 PMGET
and POST
methods, we should name the provider states as Task with existing ID is available
and Task with the correct body is created
. But, how should we call the provider state of checking unauthorized scenarios or bad requests (with invalid request body)? And which name of the provider state is better in the point of view of Pact best practices, the first one or post-task-success
and get-task-success
?Boris
09/06/2022, 12:59 AMPOST
may only need a parent resource to exist beforehand, so that's all you need to specify. The same state may work for a GET
or DELETE
of the parent, too.Boris
09/06/2022, 1:08 AMBoris
09/06/2022, 1:08 AMBoris
09/06/2022, 1:10 AMTimothy Jones
09/06/2022, 1:47 AMTimothy Jones
09/06/2022, 1:48 AMMaksym Liannoi
09/06/2022, 6:43 AMMatt (pactflow.io / pact-js / pact-go)
Maksym Liannoi
09/06/2022, 6:48 AMBoris
09/06/2022, 6:51 AMMatt (pactflow.io / pact-js / pact-go)
Boris
09/06/2022, 6:53 AMBoris
09/06/2022, 6:54 AMMatt (pactflow.io / pact-js / pact-go)
4xx
or 5xx
error (auth errors probably warrants a separate error handling to validation or system outages).
That can probably be done outside of Pact though, because it might be hard to produce a 5xx
on the provider sideBoris
09/06/2022, 6:56 AMcurl
)Maksym Liannoi
09/06/2022, 6:59 AMMaksym Liannoi
09/06/2022, 6:59 AMBoris
09/06/2022, 7:03 AMBoris
09/06/2022, 7:04 AMTimothy Jones
09/06/2022, 7:05 AMBoris
09/06/2022, 7:06 AMTimothy Jones
09/06/2022, 7:08 AMTimothy Jones
09/06/2022, 7:09 AMMaksym Liannoi
09/06/2022, 7:18 AMTimothy Jones
09/06/2022, 7:28 AMWe should cover only each case that client sends to our provider;Yes, and all response types (eg both 404 and 200 for
GET /some/item
)
Check unauthorized responses only once per endpoint (e.g. for GET method);Yes, usually - generally you need one response type per client behaviour. For example, if your client treats all 401s the same, then you don't need tests for invalid token vs expired token, etc. But, if the server responds with a reason that prompts the user differently (ie, if the client relies on a specific field value eg
reason: "expired_token"
to switch some behaviour), then you'll want a test for each response
Give names to provider states as generic as we can, but complete them as human-readable as possible;Yes
Cover with Pact case for bad request, where expect a validation error object from a provider - is okayYes. Examples might include
username: " "
, which maybe the user can enter, might return 400: {reason: "Your username must not contain spaces" }
, or maybe username: "bob"
-> {reason: "Your username is too short" }
In a case like that, if the behaviour is "show the reason to the user", then you don't need to test each reason that the provider might reject a username, you just check that the response is 400 { reason: <string> }
Timothy Jones
09/06/2022, 7:29 AMGive names to provider states as generic as we can, but complete them as human-readable as possible;To add to this, keeping the pact states consistent across multiple consumers for the same provider can be annoying - but pact is not a substitute for team to team communication (I'm afraid ๐ )
Timothy Jones
09/06/2022, 7:34 AMMaksym Liannoi
09/06/2022, 7:41 AMTimothy Jones
09/06/2022, 7:42 AMMaksym Liannoi
09/14/2022, 1:59 AMBoris
09/14/2022, 2:29 AMBoris
09/14/2022, 2:30 AMBoris
09/14/2022, 2:31 AMBoris
09/14/2022, 2:33 AMautomatically assign it to this user admin roleMight be fine, but if you have other roles you want to test, then consider being more explicit about it. Newer Pact versions support multiple states, and metadata in states, so you don't have to make unwieldy states.
Boris
09/14/2022, 2:34 AMMaksym Liannoi
09/14/2022, 2:57 AMBoris
09/14/2022, 2:59 AMTimothy Jones
09/14/2022, 4:24 AMMaksym Liannoi
09/14/2022, 4:31 AMTimothy Jones
09/14/2022, 4:37 AMTimothy Jones
09/14/2022, 5:12 AMTimothy Jones
09/14/2022, 5:12 AMTimothy Jones
09/14/2022, 5:15 AMAuthorization: Bearer SOME_TOKEN
and you auto-assign permissions to SOME_TOKEN
, so that they can access the endpoint? That sounds fine to me.Timothy Jones
09/14/2022, 5:17 AMdefault authorized
where SOME_TOKEN
is considered valid, and default unathorized
where SOME_TOKEN
is considered invalid.Timothy Jones
09/14/2022, 5:18 AMMaksym Liannoi
09/14/2022, 5:39 AMdefault
, not "user with ID exists who has task 1 open and task 2 closed"
. Just default
, without a shortcut or, better, an alias for getting a specific user. But, if we rethink that process, the default aligns only for fetching all users or getting one by id, so a provider state with a name default isn't enough. Instead, we should inject a new provider state with a different name (maybe as "user with ID exists who has task 1 open and task 2 closed"
).
If moved back to Authorization, so in our contact tests, we implemented him by adding additional middleware here, such like
services.AddAuthentication(AuthenticationHandler.Schema)
.AddScheme<AuthenticationSchemeOptions, AuthenticationHandler>(AuthenticationHandler.Schema,
_ => { });
and
public sealed class AuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public const string Schema = "Test";
public AuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger,
UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
AuthenticateResult result;
var authorizationHeader = Context.Request.Headers.Authorization;
if (string.IsNullOrWhiteSpace(authorizationHeader))
{
result = AuthenticateResult.Fail(
new UnauthorizedAccessException("To access API, you must provide a Bearer token."));
return Task.FromResult(result);
}
var claims = new[] { new Claim(ClaimTypes.Name, "Test user"), new Claim(ClaimTypes.Role, "admin") };
var identity = new ClaimsIdentity(claims, Schema);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Schema);
result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
Maksym Liannoi
09/14/2022, 5:40 AMTimothy Jones
09/14/2022, 5:41 AMdefault
as it is ambiguous - I would confuse it with "the state that a blank provider would start in". But the concept of "this state actually covers most of the situations I want to test" is pretty helpful.Maksym Liannoi
09/14/2022, 5:45 AMTimothy Jones
09/14/2022, 5:45 AMstate: "user with ID=10 exists who has task 1 open and task 2 closed"
GET /users/10
returns {/* user 10 */}
and
state: "user with ID=10 exists who has task 1 open and task 2 closed"
GET /users/10/tasks
returns [{/* id=1, open */}, {/* id=2, closed */}]
with granular states, you could say:
state: ["user with ID=10 exists"]
GET /users/10
returns {/* user 10 */}
and
state: ["user with ID=10 exists", "user 10 has task ID 1, which is open", "user 10 has task ID 2 which is closed"]
GET /users/10/tasks
returns [{/* id=1, open */}, {/* id=2, closed */}]
this has clear advantages when you want to add new concepts (eg, "task 2 has comments from user 12")Timothy Jones
09/14/2022, 5:46 AMTimothy Jones
09/14/2022, 5:47 AMSOME_TOKEN is an invalid admin auth token
not SOME_TOKEN returns 401
Maksym Liannoi
09/14/2022, 5:49 AMdefault
Maksym Liannoi
09/14/2022, 5:54 AMSOME_TOKEN is an invalid admin auth token
?Maksym Liannoi
09/14/2022, 5:56 AMTimothy Jones
09/14/2022, 6:04 AMTimothy Jones
09/14/2022, 6:04 AMMaksym Liannoi
09/14/2022, 6:15 AMI usually just don't provide any provider state in that caseI am a bit surprised. Is that possible?
Apologies, I don't think I understood your 401 questionSorry, my guilt is related to the general difficulty of our enterprise system. It seems like we can just don't provide any provider state in that case, also if it's possible
Timothy Jones
09/14/2022, 6:18 AMIs that possible?Yep. The point of provider states is to set up some precondition. If you don't have any preconditions, you don't need to use them. If you prefer, a related pattern is to use empty states - like "there are no users", which you implement to do nothing during setup or teardown. This is a neat way to use the states as documentation.
Maksym Liannoi
09/14/2022, 6:24 AM"user with ID=10 does not exist"
is a good one? Whereas, for other methods (GET, PUT and DELETE), we can adapt your granular states exampleTimothy Jones
09/14/2022, 6:33 AM"user with ID=10 does not exist"This is exactly what I do.
Timothy Jones
09/14/2022, 6:35 AMMaksym Liannoi
09/14/2022, 6:43 AMTimothy Jones
09/14/2022, 6:46 AMstart server with mock repos that are empty
"user 10 exists"
setup - mock user repo layer so that user 10 exists
teardown - replace mocked user repo layer with empty mock
Timothy Jones
09/14/2022, 6:47 AM"user 10 exists"
setup - mock user repo layer so that user 10 exists
"there are no users"
setup - mock user repo layer so that it is empty
Maksym Liannoi
09/14/2022, 6:53 AMMaksym Liannoi
09/14/2022, 6:54 AMTimothy Jones
09/14/2022, 7:21 AM