Authentication Testing

Test coverage and patterns for the authentication system including browser flow, JWT, and frontend tests

Authentication Testing

Test coverage and patterns for the authentication system.

Test Files

FileCoverage
tests/account/test_headless_browser_csrf.pyBrowser CSRF flow, session auth
tests/account/test_jwt_auth.pyJWT tokens, tenant claims
tests/account/test_password_reset.pyPassword reset flow

Running Tests

# All account tests
uv run pytest tests/account/ -v

# Specific test file
uv run pytest tests/account/test_headless_browser_csrf.py -v

# With coverage
uv run pytest tests/account/ --cov=apps.accounts

Test Fixtures

User with Verified Email and Tenant

@pytest.fixture
def user_with_verified_email_and_tenant():
    from allauth.account.models import EmailAddress

    user = UserFactory(password=TEST_PASSWORD)
    EmailAddress.objects.create(
        user=user,
        email=user.email,
        verified=True,
        primary=True,
    )
    tenant = TenantFactory(owner=user)
    tenant.users.add(user)
    return user, tenant

Browser Flow Tests

def test_browser_config_sets_csrf_cookie():
    """GET /_allauth/browser/v1/config must set csrftoken cookie."""
    client = Client(enforce_csrf_checks=True)
    resp = client.get("/_allauth/browser/v1/config")
    assert resp.status_code == 200
    assert "csrftoken" in resp.cookies

Login with CSRF

def test_browser_login_requires_csrf_cookie_then_succeeds(user_with_verified_email_and_tenant):
    user, _tenant = user_with_verified_email_and_tenant
    client = Client(enforce_csrf_checks=True)

    # Step 1: Get CSRF token
    cfg = client.get("/_allauth/browser/v1/config")
    csrf_token = client.cookies["csrftoken"].value

    # Step 2: Login with CSRF header
    login = client.post(
        "/_allauth/browser/v1/auth/login",
        data=json.dumps({"email": user.email, "password": TEST_PASSWORD}),
        content_type="application/json",
        HTTP_X_CSRFTOKEN=csrf_token,
    )

    assert login.status_code == 200
    payload = login.json()
    assert payload["meta"]["is_authenticated"] is True
    assert payload["data"]["user"]["email"] == user.email

Session Auth for API

def test_ninja_api_accepts_django_session_cookie(user_with_verified_email_and_tenant):
    """After browser login, Ninja API should authenticate via session."""
    user, _tenant = user_with_verified_email_and_tenant
    client = Client(enforce_csrf_checks=True)
    
    # Login
    client.get("/_allauth/browser/v1/config")
    csrf_token = client.cookies["csrftoken"].value
    client.post(
        "/_allauth/browser/v1/auth/login",
        data=json.dumps({"email": user.email, "password": TEST_PASSWORD}),
        content_type="application/json",
        HTTP_X_CSRFTOKEN=csrf_token,
    )

    # Access protected API endpoint
    profile = client.get("/api/v1/users/profile/")
    assert profile.status_code == 200
    assert profile.json()["email"] == user.email

JWT Flow Tests

JWT tests are skipped unless JWT strategy is enabled:

if "jwt" not in getattr(settings, "HEADLESS_TOKEN_STRATEGY", "").lower():
    pytest.skip("JWT strategy not enabled", allow_module_level=True)

Login Returns JWT Tokens

class TestJWTLogin:
    def test_login_returns_jwt_tokens(self, dj_client, user_with_tenant):
        user, tenant = user_with_tenant
        response = dj_client.post(
            "/_allauth/app/v1/auth/login",
            data=json.dumps({"email": user.email, "password": TEST_PASSWORD}),
            content_type="application/json",
        )

        assert response.status_code == 200
        data = response.json()
        assert "access_token" in data["meta"]
        assert "refresh_token" in data["meta"]
        assert data["data"]["user"]["email"] == user.email

Token Contains Tenant Claims

def test_token_includes_tenant_id_claim(self, dj_client, user_with_tenant):
    import jwt
    user, tenant = user_with_tenant

    response = dj_client.post(
        "/_allauth/app/v1/auth/login",
        data=json.dumps({"email": user.email, "password": TEST_PASSWORD}),
        content_type="application/json",
    )

    access_token = response.json()["meta"]["access_token"]
    claims = jwt.decode(access_token, options={"verify_signature": False})
    
    assert "tenant_id" in claims or "current_tenant_id" in claims
    tenant_claim = claims.get("tenant_id") or claims.get("current_tenant_id")
    assert tenant_claim == str(tenant.id)

API Request with JWT

def test_api_request_with_valid_jwt_succeeds(self, dj_client, user_with_tenant):
    user, tenant = user_with_tenant

    # Get tokens
    login_response = dj_client.post(
        "/_allauth/app/v1/auth/login",
        data=json.dumps({"email": user.email, "password": TEST_PASSWORD}),
        content_type="application/json",
    )
    access_token = login_response.json()["meta"]["access_token"]

    # Make authenticated request
    response = dj_client.get(
        "/api/v1/users/profile/",
        HTTP_AUTHORIZATION=f"Bearer {access_token}",
    )

    assert response.status_code == 200
    assert response.json()["email"] == user.email

Token Refresh

def test_refresh_token_returns_new_tokens(self, dj_client, user_with_tenant):
    user, _ = user_with_tenant

    # Login
    login_response = dj_client.post(
        "/_allauth/app/v1/auth/login",
        data=json.dumps({"email": user.email, "password": TEST_PASSWORD}),
        content_type="application/json",
    )
    refresh_token = login_response.json()["meta"]["refresh_token"]

    # Refresh
    response = dj_client.post(
        "/_allauth/app/v1/auth/token/refresh",
        data=json.dumps({"refresh": refresh_token}),
        content_type="application/json",
    )

    assert response.status_code == 200
    assert "access_token" in response.json()["meta"]
    assert "refresh_token" in response.json()["meta"]

Frontend Testing

// hooks/use-auth.test.tsx
import { renderHook, act, waitFor } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { useAuth, useAuthStore } from "./use-auth";

const server = setupServer(
  http.get("http://localhost:8080/_allauth/browser/v1/config", () => {
    return HttpResponse.json({
      data: {
        account: { authentication_method: "email" },
        socialaccount: { providers: [] },
      },
    });
  }),
  http.post("http://localhost:8080/_allauth/browser/v1/auth/login", () => {
    return HttpResponse.json({
      meta: { is_authenticated: true },
      data: { user: { id: 1, email: "test@example.com" } },
    });
  })
);

beforeAll(() => server.listen());
afterEach(() => {
  server.resetHandlers();
  useAuthStore.getState().logout();
});
afterAll(() => server.close());

describe("useAuth", () => {
  it("should login and set user", async () => {
    const { result } = renderHook(() => useAuth());

    await act(async () => {
      await result.current.login("test@example.com", "password");
    });

    await waitFor(() => {
      expect(result.current.isAuthenticated).toBe(true);
      expect(result.current.user?.email).toBe("test@example.com");
    });
  });
});

Test Password

Use a consistent test password that meets validation requirements:

TEST_PASSWORD = "ComPL@S6sxPdfsdword1"

This password meets:

  • Minimum length (12+ chars)
  • Mixed case
  • Numbers
  • Special characters

Last updated on

On this page