To test auth middleware and protected routes, visit each protected route without a session and assert a redirect to login, then visit it with a valid session and assert the page loads, and repeat per role to prove role-based guards enforce access. A guard that stops guarding fails open and silently: the nav link disappears so the route looks protected, but the page still serves to anyone with the URL. The only proof the guard is working is a test that visits the route both ways.
The protected route that stopped protecting did not announce itself. The link disappeared from the nav, so every manual tester clicked around the app, saw no way to reach the admin dashboard, and called it fine. Meanwhile, anyone who had bookmarked the URL or knew the path could load the page without a session. No redirect. No error. Just the dashboard, fully rendered, for an unauthenticated request.
That is the incident pattern the wrapper that quietly disappeared documents in detail: an AI coding agent refactors a component or file, drops the auth wrapper in the process, and the guard is gone while the UI still hides the link. The taxonomy and root causes live in that article. This article is the concrete counterpart: given that incident pattern, here is exactly how to write the assertions that would have caught it.
What Auth Middleware Does (and How It Silently Breaks)
Middleware intercepts the request before the protected route renders. It reads the session token from the cookie or authorization header, checks whether a valid authenticated session is present (and, for role-based routes, whether the session holds the required role), and either lets the request through or redirects to the login page. The redirect happens before any route handler or page component runs. The route never renders.
The silent failure is that the nav link disappears independently of the middleware guard. A UI conditional hides the link for unauthenticated users, so the route looks unreachable. But if the middleware guard is removed, the route is reachable. Anyone who navigates directly to the URL bypasses the non-existent guard and gets the page. No log entry, no error response. For a deeper explanation of why this happens and the specific patterns that cause guards to drop, see the wrapper that quietly disappeared.
The practical point: you cannot tell whether the guard is working by checking whether the nav link is visible. You have to request the route without a session and verify the redirect actually happens.
Testing the Unauthenticated Redirect
The same route, visited twice. Pass one proves the guard exists by asserting the redirect; pass two proves the guard admits a legitimate session by asserting the page loads.
The assertion is simple: visit the protected route with no session, confirm the browser ends up on the login page.
In Playwright, a fresh browser context has no cookies by default. Navigate to the protected route and assert that page.url() matches your login path after navigation resolves. The exact pattern: expect(page).toHaveURL(//login/) after a page.goto('/dashboard') with no storageState loaded. If the middleware is working, the navigation resolves at the login page. If the middleware guard was dropped, the navigation resolves at the dashboard, and the assertion fails.
For server-side redirects, you can also check the response status directly. A 302 or 307 on the initial request confirms the server-side redirect rather than a client-side one. Playwright follows redirects by default, so asserting the final URL is usually more reliable than asserting the status code on the original request. But if you want to confirm the redirect is server-side (not client-side JavaScript after the page loads), intercept the initial response and check the status there.
API routes and route handlers need a separate check. A middleware redirect protects the page, but if your API route is also meant to be protected, test it directly. Send a fetch with no credentials to the API route. Assert the response status is 401 or 403. This matters because some architectures protect the UI through middleware but leave the underlying API endpoints accessible without a session. Testing the page redirect alone misses that.
For teams setting up Playwright session fixtures, Playwright authentication testing covers storageState setup in detail, including how to generate and reuse session files across parallel workers.
Testing the Authenticated Path and Roles
Once you have confirmed the unauthenticated redirect, test the other side: a valid authenticated session reaches the route and sees the expected content.
Load your storageState fixture before navigating. Navigate to the protected route. Assert that the URL is still the protected route (not the login page) and that content expected only on the authenticated page is visible. A heading, a data table, a user-specific element: something that proves the page rendered rather than redirecting. This test fails if the middleware is over-blocking (redirecting authenticated users) or if the page fails to load content for a valid session.
Assert every cell in the role matrix. A guard that lets a regular user into an admin route only surfaces when you test the disallowed-role path explicitly.
Role-based access adds more passes. Create one authenticated session per role. For each role, navigate to each role-restricted route and assert the right outcome. An admin navigating to /admin should see the page. A regular user navigating to /admin should land on the login page (or a 403 page, depending on your implementation). Assert both: that allowed roles reach the page, and that disallowed roles are blocked. A role-based guard that blocks regular users but lets them in as an edge case (for example, because the role check compares a string case-insensitively in one place and case-sensitively in another) will only surface if you run the disallowed-role test.
For how to obtain and structure the authenticated session you load in these tests, test cases for the login page covers the session setup patterns, including how to create test accounts per role and what to assert during the login flow itself.
Why This Is the Test a Dropped Wrapper Fails
UI tests and unit tests stay green after a middleware guard is removed. The unit test for the dashboard component does not make an unauthenticated HTTP request to the route. The Storybook story does not go through the middleware stack. Even an integration test that mounts the component in isolation does not exercise the middleware. They all pass, because the component itself is fine. The guard was somewhere else.
The unauthenticated-visit assertion is the only test that actually proves the guard is in place. It visits the route as a real browser with no session, and asserts the redirect happens. Remove the middleware guard and that assertion fails immediately, before any user encounters the unprotected route. That is the regression net the incident pattern requires.
Autonoma runs this assertion as part of how our agents approach protected-route coverage. The Executor agent visits each protected route twice: once with no session, once with a valid authenticated session. It asserts the redirect in the first pass and the page load in the second. The Diffs Agent monitors changes to middleware files, route configurations, and auth wrapper components on every PR. If a change removes or bypasses a guard, the protected-route test goes red at code review rather than in production. For teams shipping auth-gated dashboards, admin panels, or multi-tenant SaaS applications, that is the difference between a silent hole and a caught regression.
Autonoma turns that unauthenticated-visit check into maintained PR coverage: Planner keeps the route expectations current, Executor runs both session states on the live preview, Reviewer classifies the failure, and the Diffs Agent updates coverage when auth middleware changes.
FAQ
Send a request to a route the middleware protects, without including any session cookie or authorization header. Assert that the response is a redirect (status 302 or 307) with a Location header pointing to your login page. Then repeat the request with a valid authenticated session and assert the route responds normally. If the middleware is a Next.js middleware.ts file, Playwright end-to-end tests are the most reliable way to cover it because they drive a real browser through the full request lifecycle.
Test a protected route in two passes. First, navigate to it with no session and assert the browser lands on the login page (check the final URL matches /login or equivalent). Second, load a valid authenticated session via storageState or a login fixture, navigate to the same route, and assert the expected page content is present rather than a redirect. Both assertions are required: the first proves the guard exists, the second proves it does not over-block legitimate users.
Create an authenticated session for each role your app supports (e.g., admin, user, read-only). For each role, navigate to every role-restricted route and assert the correct outcome: allowed roles should reach the page and see expected content, disallowed roles should be redirected or receive a 403 response. Generate one storageState fixture per role and load the appropriate fixture for each test. Keep per-role fixture files separate so test workers can load them in parallel without interfering.
Navigate to the protected route in a browser context with no cookies and no authorization headers. Assert that the final URL after navigation matches your login path (for example, assert page.url() includes /login). For API routes and route handlers, send a fetch request with no credentials and assert the response status is 401 or 403. Both checks are worth running: the page-level test proves the middleware redirect, the API-level test proves the route handler itself rejects unauthenticated requests rather than relying on the middleware alone.
The most common cause is a middleware guard being removed or the route being moved outside the middleware matcher's path pattern. When a route stops appearing in the nav (because a UI check hides the link), it can look protected while the actual route still serves to anyone with the URL. AI coding agents making refactors are a frequent source of this: they rewrite a wrapper component or middleware file and silently drop the guard. The fix is a test that visits the route without a session and asserts a redirect. That test fails the moment the guard is gone, before the change ships.




