- Backend (FastAPI): - External API authentication (pj-auth-api.vercel.app) - JWT token validation with Redis session storage - RBAC with department isolation - User, Role, Department models with pjctrl_ prefix - Alembic migrations with project-specific version table - Complete test coverage (13 tests) - Frontend (React + Vite): - AuthContext for state management - Login page with error handling - Protected route component - Dashboard with user info display - OpenSpec: - 7 capability specs defined - add-user-auth change archived 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
85 lines
2.8 KiB
Python
85 lines
2.8 KiB
Python
import pytest
|
|
from app.core.security import create_access_token, decode_access_token, create_token_payload
|
|
|
|
|
|
class TestJWT:
|
|
"""Test JWT token creation and validation."""
|
|
|
|
def test_create_access_token(self):
|
|
"""Test creating an access token."""
|
|
data = {"sub": "user123", "email": "test@example.com"}
|
|
token = create_access_token(data)
|
|
|
|
assert token is not None
|
|
assert isinstance(token, str)
|
|
|
|
def test_decode_valid_token(self):
|
|
"""Test decoding a valid token."""
|
|
data = create_token_payload(
|
|
user_id="user123",
|
|
email="test@example.com",
|
|
role="engineer",
|
|
department_id="dept123",
|
|
is_system_admin=False,
|
|
)
|
|
token = create_access_token(data)
|
|
payload = decode_access_token(token)
|
|
|
|
assert payload is not None
|
|
assert payload["sub"] == "user123"
|
|
assert payload["email"] == "test@example.com"
|
|
assert payload["role"] == "engineer"
|
|
assert payload["is_system_admin"] is False
|
|
|
|
def test_decode_invalid_token(self):
|
|
"""Test decoding an invalid token."""
|
|
payload = decode_access_token("invalid.token.here")
|
|
assert payload is None
|
|
|
|
def test_token_payload_structure(self):
|
|
"""Test token payload has correct structure."""
|
|
payload = create_token_payload(
|
|
user_id="user123",
|
|
email="test@example.com",
|
|
role="engineer",
|
|
department_id="dept123",
|
|
is_system_admin=False,
|
|
)
|
|
|
|
assert "sub" in payload
|
|
assert "email" in payload
|
|
assert "role" in payload
|
|
assert "department_id" in payload
|
|
assert "is_system_admin" in payload
|
|
|
|
|
|
class TestAuthEndpoints:
|
|
"""Test authentication API endpoints."""
|
|
|
|
def test_get_me_without_auth(self, client):
|
|
"""Test accessing /me without authentication."""
|
|
response = client.get("/api/auth/me")
|
|
assert response.status_code == 403
|
|
|
|
def test_get_me_with_auth(self, client, admin_token):
|
|
"""Test accessing /me with valid authentication."""
|
|
response = client.get(
|
|
"/api/auth/me",
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["email"] == "ymirliu@panjit.com.tw"
|
|
assert data["is_system_admin"] is True
|
|
|
|
def test_logout(self, client, admin_token, mock_redis):
|
|
"""Test logout endpoint."""
|
|
response = client.post(
|
|
"/api/auth/logout",
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# Verify session is removed
|
|
assert mock_redis.get("session:00000000-0000-0000-0000-000000000001") is None
|