Testing authentication, authorization, and permission failures
coreintermediateDRF ships `APIClient`, which is Django's test client plus content-type handling and three ways to authenticate: `force_authenticate(user)` on a request factory, `client.force_authenticate(user)` to skip credential checking entirely, and `client.credentials(HTTP_AUTHORIZATION=...)` to send a real header when the scheme itself is what you are testing. The tests that matter most are the negative ones. For every protected endpoint, assert the anonymous case (401 or 403 — and which one depends on the first authentication class), the wrong-user case, and the wrong-role case. And test the **list** endpoint separately from the detail endpoint, because DRF never calls `has_object_permission()` for a list, so a permission class that looks correct can still return every row in the table.
Think of it as
Authorization tests are the ones a suite most often lacks, because the happy path is what gets written first and the negative paths only exist if someone deliberately writes them. The useful discipline is to treat every protected endpoint as having a small fixed matrix — anonymous, authenticated-but-not-yours, authenticated-with-the-wrong-role, authenticated-and-allowed — and to notice that three of those four rows are failures. Parameterizing that matrix is what makes it affordable to have on every endpoint rather than on the two people remembered. The other half is where the leak actually happens. Object-level permissions only run when `get_object()` runs, so `GET /orders/57/` on someone else's order is protected by a permission class while `GET /orders/` is protected only by `get_queryset()` — two different mechanisms, and a test of the first proves nothing about the second. That is why the list-scoping test deserves its own name and its own assertion: create data belonging to another user, request the collection, and assert it is absent. Finally, prefer `force_authenticate` for tests about *authorization* and real credentials for tests about *authentication* — mixing them means a token bug hides behind a helper that never checks tokens.
What we're doing: Cover the whole matrix with one parametrized test, plus the list-scoping test that the matrix cannot express.
- 8
- Asserting 401 rather than 403 is a claim about the stack: DRF only returns 401 when the first authentication class can supply a `WWW-Authenticate` header. Pin whichever your project actually produces.
- 9
- 404 rather than 403 for another customer's order, because the queryset is filtered — the response deliberately cannot be used to confirm the order exists.
- 22–28
- The test that the parametrized matrix cannot express. Object-level permissions never run for a list, so this asserts on `get_queryset()` and nothing else.
- 32–33
- Real credentials, not `force_authenticate` — this test is about the authentication scheme, and the helper would bypass exactly the code under test.
- 40–43
- Role separation: a user who legitimately passes every earlier check must still be refused this one action.
Why this works: The matrix covers who may reach an object, the list test covers what a collection returns, and the token test covers the scheme itself — three different mechanisms that a single happy-path test would leave entirely unverified.
Testing only the detail route and assuming the list is covered
Wrong
Better
What you see: Every permission test passes, and the collection endpoint returns other customers' orders. It is found by a customer noticing unfamiliar data, not by the suite.
Why: DRF calls `has_object_permission()` from `get_object()`, which a list response never invokes — so the detail route and the list route are protected by two different mechanisms. A permission class implementing only the object-level hook secures the first and does nothing for the second, and no amount of detail-route testing can detect that. The list needs its own test asserting a specific foreign row is absent.
- 401 / 403 from permission_classes: detail route (/orders/57/), anonymous — has_permission() runs before anything is fetched
- 401 / 403 — same class, same check: collection route (/orders/), anonymous — the collection is denied the same way when unauthenticated
- 403 or 404 from has_object_permission(): detail route (/orders/57/), authenticated, not yours — runs from get_object(); 404 if you filtered the queryset instead
- 200 with the row leaked: collection route (/orders/), authenticated, not yours — has_object_permission is NEVER called for list — only get_queryset() protects this
The matrix every protected endpoint needs
Together
Remember: Use `force_authenticate` for authorization tests and real `credentials()` headers for authentication tests — mixing them means a token bug hides behind a helper that never checks tokens. Give every protected endpoint the four-row matrix (anonymous, not-yours, wrong-role, allowed), and parameterize it so it is affordable everywhere. Above all, test the list endpoint separately: DRF never calls `has_object_permission()` for a collection, so the leak that matters is only visible in a test that asserts a specific foreign row is absent.
See also: testing contracts status codes and collections · custom basepermission and object level checks · authentication order anonymous users and failures

