Why should the test suite for a protected route include a request that carries no credentials at all?
answer
- the happy path never questions the guard
- remove one factor: the credential
- who answered, guard or handler?
- status plus body plus no side effect
basics
~20 sA test that always sends a valid credential passes even when the route is not protected at all. The credential-free request is the case that proves a guard is actually wired on that route and answers before the handler runs.
solid answer
~40 sA happy-path test drives the route with a caller who is allowed through, so the guard is never asked a question whose answer could differ. Delete the guard and that test still goes green. The credential-free request is the control: it is the same route and the same method with one factor removed, so a rejection can only come from the layer that checks identity. Assert more than the status - assert the error body against the shape the service documents, and assert that the handler's collaborator was never invoked, because a status alone can be produced by an unrelated layer such as the framework's unknown-route fallback when the test's path has a typo.
go deeper
Recall the shape of the case: same route, same method, credential removed, and an assertion on the exact status rather than on 'not success'.
Explain why the case isolates the guard - one factor changed - and why the error body and a no-side-effect assertion are needed before the result means anything.
Show how you keep negative coverage from rotting as routes move between groups, and how you detect a shared test client that attaches identity behind your back.
Weigh where this control belongs so it is written once per protected route without becoming ceremony, and how review enforces it on new routes.
## The blind spot in a happy-path test A web-layer test that sends a valid credential and asserts a `200 OK` proves one thing: that a caller who has already been let through gets the documented response. It cannot prove **who is let through**, because the component that makes that decision - the guard, filter, hook, or interceptor that inspects the request before the handler - was never put in a position where it could answer differently. If someone removes the guard from the route, or moves the route out from under the group the guard is attached to, the happy-path test stays green. That is the whole reason interviewers ask this: an authentication requirement that no test can fail is not a requirement, it is a comment. The credential-free request is the **control case**. It is the same path and the same method as the happy path, with exactly one factor removed. When it is rejected, the rejection can only have come from the layer that looks at identity, because nothing else about the request changed. ## What the case actually pins down - **That the guard is attached to this route**, not merely registered somewhere in the application. - **That the guard runs before the handler**, so no work and no side effect happens on behalf of an unidentified caller. - **That the rejection is expressed as a response**, with the status and the body the service contracts for, rather than as a crash or a blank connection. - **That the guard's answer is stable** when the route later moves into or out of a group, because the case fails the moment protection is dropped. ## Assert more than the status A status-only assertion is weak, because several unrelated layers can produce a status in the same family. Three assertions together make the case honest: 1. **Status** - the one the service documents for a request with no identity. 2. **Error body** - the machine-readable code and the field shape the service publishes, so the response is the contracted error and not an accidental one. 3. **No side effect** - the collaborator the handler would have called was not called. With the handler's dependency replaced by a recording stub, this is a direct assertion rather than an inference. | Request the test sends | What a green result proves | What it cannot prove | |---|---|---| | Valid credential | The handler produces the documented success response | That anything is protected | | No credential at all | A guard is wired here and answers before the handler | That credential parsing or verification is correct | | Malformed or expired credential | The parsing and verification branch runs and fails closed | That an unauthenticated caller is refused, since a credential was present | ## How this case is written wrong The most common mistake is asserting only that the status is "not the success status". A typo in the test's path means no route matches, the framework's unknown-route fallback answers, and the assertion still passes - the test is green while the route it names may not even exist. Pin the exact status and the error code from the body. A second mistake is building the request through a shared helper that quietly attaches a credential to everything it sends. The test believes it sent nothing; the client sent an identity. Where a suite has such a helper, the credential-free case must go around it, and it is worth asserting the outbound request as well when the client supports it. A third is treating "no credential" and "bad credential" as the same case. They exercise different branches: one is the absence of any identity to check, the other is the failure of a check that ran. Most frameworks route them to the same rejection status, which is exactly why a test that covers only one of them leaves the other branch unproven. ## Keeping it honest as the route grows When a route later gains a second requirement - say, an additional permission check - the credential-free case keeps its meaning, because it still isolates the earliest decision in the chain. Teams that add only richer positive cases as a feature grows end up with a suite whose negative coverage was written once, at the start, against a much simpler route. The cheap discipline is that every route which gains protection gains its credential-free case in the same change, alongside the positive one, so the two are always written and reviewed together.
- What does a request carrying a malformed credential prove that a credential-free request does not?It exercises the branch where a credential is present and fails its checks - extraction, decoding, signature or expiry. The credential-free case only proves that a request with no identity is refused; it never enters the verification code at all. Both belong in the suite, because a verifier that accepts anything is invisible to the credential-free case.
- How can a credential-free test pass while the route is still unprotected?Two common ways. The test asserts only that the status is not the success status, so a typo in the path lets the unknown-route fallback answer and the assertion holds. Or the shared test client attaches a credential to every request it builds, so the case that claims to send none actually sends one and the handler's own logic produced the status.
saying these in an interview costs you the question
- Thinks a passing authenticated test proves the route is protected
- Only tests valid credentials because rejection is the client's problem
- Asserts a status but never checks the handler stayed out of it
- Treats a missing credential and a malformed one as one case
- Assumes protection wired once at startup cannot be dropped later