DEV Community

Cover image for Write a JWT Login Test Using Cypress
حذيفة
حذيفة

Posted on

Write a JWT Login Test Using Cypress

hodaifa article thumbnail

Testing JWT Authentication in a React + Laravel Clothes Store with Cypress

After spending two weeks trying to create dashboard tests for our React and Laravel e-commerce application, I hit a major roadblock: authentication. Since our application uses stateless API communication, JWT (JSON Web Tokens) with Laravel Sanctum handles authentication. Here's how I successfully implemented Cypress tests for this setup.

Understanding the Authentication Flow

The login functionality comprises:

  • Backend: Laravel Sanctum for JWT generation
  • Frontend: Axios interceptors + React Context for token management
  • Protection: Dashboard pages wrapped in an authentication context

Backend: Laravel Auth Controller

The key authentication endpoints:

class AuthController extends Controller
{
    public function login(LoginRequest $request): JsonResponse
    {
        if (!Auth::attempt($request->only('email', 'password'))) {
            return $this->error(null, 'Invalid credentials', 401);
        }

        $user = $request->user();
        $token = $user->createToken('auth_token')->plainTextToken;

        return $this->success([
            'user' => $user,
            'token' => $token
        ], 'User logged in successfully.');
    }
}
Enter fullscreen mode Exit fullscreen mode
Route::prefix('auth')->group(function () {
    Route::post('login', [AuthController::class, 'login']);
    Route::get('me', [AuthController::class, 'me']);
});
Enter fullscreen mode Exit fullscreen mode

Frontend: React Authentication Context

The AuthContext manages user state and token storage:

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  const bootstrapAuth = useCallback(async () => {
    const token = localStorage.getItem("token");
    if (!token) {
      setLoading(false);
      return;
    }

    try {
      const { data } = await authApi.me();
      setUser(data.data);
    } catch {
      localStorage.removeItem("token");
      setUser(null);
    } finally {
      setLoading(false);
    }
  }, []);

  async function login(credentials) {
    const { data } = await authApi.login(credentials);
    localStorage.setItem("token", data.data.token);
    setUser(data.data.user);
  }

  if (loading) return <ClothesLoader />;

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Axios Interceptors for Token Management

The interceptor automatically attaches tokens to protected requests:

export const privateClient = axios.create({
  baseURL: import.meta.env.VITE_LARAVEL_APP_API_URL,
  headers: { "Content-Type": "application/json" },
});

privateClient.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});
Enter fullscreen mode Exit fullscreen mode

Dashboard Route Protection

Protected routes check authentication before loading:

export const Route = createFileRoute("/_dashboard")({
  beforeLoad: ({ context }) => {
    if (!context.auth?.user) {
      throw redirect({ to: "/login" });
    }
  },
  component: DashboardLayout,
});
Enter fullscreen mode Exit fullscreen mode

Implementing Cypress Login Command

The key insight: create a custom Cypress command that mimics the exact authentication flow. This command uses cy.session() to cache login state across tests:

Cypress.Commands.add("login", () => {
  cy.session("admin-session", () => {
    cy.request("POST", `${Cypress.env('apiUrl')}/auth/login`, {
      email: Cypress.env("email"),
      password: Cypress.env("password"),
    }).then((response) => {
      const token = response.body.data.token;
      const user = response.body.data.user;

      cy.window().then((win) => {
        win.localStorage.setItem("token", token);
        win.localStorage.setItem("user", JSON.stringify(user));
      });

      cy.intercept("GET", `${Cypress.env('apiUrl')}/auth/me`, {
        statusCode: 200,
        body: { data: user, message: "User fetched successfully." }
      }).as("getMe");

      cy.visit("/");
      cy.wait("@getMe");
    });
  }, {
    cacheAcrossSpecs: true,
    validate: () => {
      cy.window().then((win) => {
        expect(win.localStorage.getItem("token")).to.exist;
      });
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

Configuration

Set up environment variables in cypress.config.js:

module.exports = defineConfig({
  env: {
    email: 'admin@example.com',
    password: 'securePassword123',
    apiUrl: 'http://clothes-store.test/api/v1'
  },
  e2e: {
    baseUrl: 'http://localhost:5173',
  },
})
Enter fullscreen mode Exit fullscreen mode

Using the Login Command in Tests

Now you can easily authenticate in any test:

describe("Add Product Page", () => {
  beforeEach(() => {
    cy.login();
    cy.visit("/dashboard/products/add");
    cy.contains("Add New Product").should("be.visible");
  });

  it("successfully creates a new product", () => {
    // Test implementation...
  });
});
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Understand the authentication flow before writing tests
  2. Use cy.session() to cache login state and speed up tests
  3. Mock API responses that occur during authentication bootstrap
  4. Set up environment variables for sensitive credentials
  5. Create reusable commands for common authentication patterns

This approach reduced my test execution time by 60% and made tests more reliable by eliminating flaky login processes.

Resources

Understanding both frontend and backend authentication implementation is crucial for writing effective Cypress tests. The cy.session() command combined with proper API mocking creates a robust testing foundation for JWT-protected applications.

Top comments (0)