1.Testing authentication, authorization, and permission failures
DRF 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.
Key features
- `APIClient` is Django's test client with content-type handling: `client.post(url, data, format="json")` rather than manual encoding
- `client.force_authenticate(user)` bypasses the whole authentication layer — right for authorization tests, wrong for testing the scheme itself
- `client.credentials(HTTP_AUTHORIZATION="Bearer …")` sends a real header, which is what actually exercises the authentication classes
- Anonymous denial is 401 or 403 depending on whether the *first* authentication class can supply a `WWW-Authenticate` header — assert the one your stack produces
- Test the list endpoint separately: DRF never calls `has_object_permission()` for `list`, so scoping there is `get_queryset()`'s job alone
- `client.force_authenticate(user=None)` resets to anonymous — necessary when one test method makes several calls as different users
The matrix every protected endpoint needs
- 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
Command
client = APIClient()
client.force_authenticate(user) # authorization tests
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}") # authentication testsUse `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.