Add tests for authentication and utilities, and update dependencies
- Created a new test package for services and added tests for AuthService. - Implemented tests for user registration, login, and token creation. - Added a new test package for utilities and included tests for password and JWT utilities. - Updated `uv.lock` to include new dependencies: bcrypt, email-validator, pyjwt, and pytest-asyncio.
This commit is contained in:
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests package."""
|
||||
1
tests/api/__init__.py
Normal file
1
tests/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""API tests package."""
|
||||
1
tests/api/v1/__init__.py
Normal file
1
tests/api/v1/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""API v1 tests package."""
|
||||
371
tests/api/v1/test_auth_endpoints.py
Normal file
371
tests/api/v1/test_auth_endpoints.py
Normal file
@@ -0,0 +1,371 @@
|
||||
"""Tests for authentication endpoints."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
from app.models.plan import Plan
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class TestAuthEndpoints:
|
||||
"""Test authentication API endpoints."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_success(
|
||||
self,
|
||||
test_client: AsyncClient,
|
||||
test_user_data: dict[str, str],
|
||||
test_plan: Plan
|
||||
) -> None:
|
||||
"""Test successful user registration."""
|
||||
response = await test_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json=test_user_data
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
|
||||
# Check response structure
|
||||
assert "user" in data
|
||||
assert "token" in data
|
||||
|
||||
# Check user data
|
||||
user = data["user"]
|
||||
assert user["email"] == test_user_data["email"]
|
||||
assert user["name"] == test_user_data["name"]
|
||||
assert user["role"] == "user"
|
||||
assert user["is_active"] is True
|
||||
assert user["credits"] > 0
|
||||
assert "plan" in user
|
||||
|
||||
# Check token data
|
||||
token = data["token"]
|
||||
assert "access_token" in token
|
||||
assert token["token_type"] == "bearer"
|
||||
assert token["expires_in"] > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_duplicate_email(
|
||||
self,
|
||||
test_client: AsyncClient,
|
||||
test_user: User
|
||||
) -> None:
|
||||
"""Test registration with duplicate email."""
|
||||
user_data = {
|
||||
"email": test_user.email,
|
||||
"password": "password123",
|
||||
"name": "Another User"
|
||||
}
|
||||
|
||||
response = await test_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json=user_data
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert "Email address is already registered" in data["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_invalid_email(
|
||||
self,
|
||||
test_client: AsyncClient
|
||||
) -> None:
|
||||
"""Test registration with invalid email."""
|
||||
user_data = {
|
||||
"email": "invalid-email",
|
||||
"password": "password123",
|
||||
"name": "Test User"
|
||||
}
|
||||
|
||||
response = await test_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json=user_data
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_short_password(
|
||||
self,
|
||||
test_client: AsyncClient
|
||||
) -> None:
|
||||
"""Test registration with short password."""
|
||||
user_data = {
|
||||
"email": "test@example.com",
|
||||
"password": "short",
|
||||
"name": "Test User"
|
||||
}
|
||||
|
||||
response = await test_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json=user_data
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_missing_fields(
|
||||
self,
|
||||
test_client: AsyncClient
|
||||
) -> None:
|
||||
"""Test registration with missing fields."""
|
||||
user_data = {
|
||||
"email": "test@example.com"
|
||||
# Missing password and name
|
||||
}
|
||||
|
||||
response = await test_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json=user_data
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_success(
|
||||
self,
|
||||
test_client: AsyncClient,
|
||||
test_user: User,
|
||||
test_login_data: dict[str, str]
|
||||
) -> None:
|
||||
"""Test successful user login."""
|
||||
response = await test_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json=test_login_data
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Check response structure
|
||||
assert "user" in data
|
||||
assert "token" in data
|
||||
|
||||
# Check user data
|
||||
user = data["user"]
|
||||
assert user["id"] == test_user.id
|
||||
assert user["email"] == test_user.email
|
||||
assert user["name"] == test_user.name
|
||||
assert user["role"] == test_user.role
|
||||
|
||||
# Check token data
|
||||
token = data["token"]
|
||||
assert "access_token" in token
|
||||
assert token["token_type"] == "bearer"
|
||||
assert token["expires_in"] > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_invalid_email(
|
||||
self,
|
||||
test_client: AsyncClient
|
||||
) -> None:
|
||||
"""Test login with invalid email."""
|
||||
login_data = {
|
||||
"email": "nonexistent@example.com",
|
||||
"password": "password123"
|
||||
}
|
||||
|
||||
response = await test_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json=login_data
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
data = response.json()
|
||||
assert "Invalid email or password" in data["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_invalid_password(
|
||||
self,
|
||||
test_client: AsyncClient,
|
||||
test_user: User
|
||||
) -> None:
|
||||
"""Test login with invalid password."""
|
||||
login_data = {
|
||||
"email": test_user.email,
|
||||
"password": "wrongpassword"
|
||||
}
|
||||
|
||||
response = await test_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json=login_data
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
data = response.json()
|
||||
assert "Invalid email or password" in data["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_malformed_request(
|
||||
self,
|
||||
test_client: AsyncClient
|
||||
) -> None:
|
||||
"""Test login with malformed request."""
|
||||
login_data = {
|
||||
"email": "invalid-email",
|
||||
"password": "password123"
|
||||
}
|
||||
|
||||
response = await test_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json=login_data
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_success(
|
||||
self,
|
||||
test_client: AsyncClient,
|
||||
test_user: User,
|
||||
auth_headers: dict[str, str]
|
||||
) -> None:
|
||||
"""Test getting current user info successfully."""
|
||||
response = await test_client.get(
|
||||
"/api/v1/auth/me",
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Check user data
|
||||
assert data["id"] == test_user.id
|
||||
assert data["email"] == test_user.email
|
||||
assert data["name"] == test_user.name
|
||||
assert data["role"] == test_user.role
|
||||
assert data["is_active"] == test_user.is_active
|
||||
assert "plan" in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_no_token(
|
||||
self,
|
||||
test_client: AsyncClient
|
||||
) -> None:
|
||||
"""Test getting current user without authentication token."""
|
||||
response = await test_client.get("/api/v1/auth/me")
|
||||
|
||||
assert response.status_code == 403 # Forbidden (no token provided)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_invalid_token(
|
||||
self,
|
||||
test_client: AsyncClient
|
||||
) -> None:
|
||||
"""Test getting current user with invalid token."""
|
||||
headers = {"Authorization": "Bearer invalid_token"}
|
||||
|
||||
response = await test_client.get(
|
||||
"/api/v1/auth/me",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
data = response.json()
|
||||
assert "Could not validate credentials" in data["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_expired_token(
|
||||
self,
|
||||
test_client: AsyncClient,
|
||||
test_user: User
|
||||
) -> None:
|
||||
"""Test getting current user with expired token."""
|
||||
from datetime import timedelta
|
||||
from app.utils.auth import JWTUtils
|
||||
|
||||
# Create an expired token (expires immediately)
|
||||
token_data = {
|
||||
"sub": str(test_user.id),
|
||||
"email": test_user.email,
|
||||
"role": test_user.role,
|
||||
}
|
||||
expired_token = JWTUtils.create_access_token(
|
||||
token_data,
|
||||
expires_delta=timedelta(seconds=-1)
|
||||
)
|
||||
|
||||
headers = {"Authorization": f"Bearer {expired_token}"}
|
||||
|
||||
response = await test_client.get(
|
||||
"/api/v1/auth/me",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
data = response.json()
|
||||
# The actual error message comes from the JWT library for expired tokens
|
||||
assert "Token has expired" in data["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logout_success(
|
||||
self,
|
||||
test_client: AsyncClient
|
||||
) -> None:
|
||||
"""Test logout endpoint."""
|
||||
response = await test_client.post("/api/v1/auth/logout")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "Successfully logged out" in data["message"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_access_with_user_role(
|
||||
self,
|
||||
test_client: AsyncClient,
|
||||
auth_headers: dict[str, str]
|
||||
) -> None:
|
||||
"""Test that regular users cannot access admin endpoints."""
|
||||
# This test would be for admin-only endpoints when they're created
|
||||
# For now, we'll test the dependency behavior
|
||||
from app.core.dependencies import get_admin_user
|
||||
from app.models.user import User
|
||||
from fastapi import HTTPException
|
||||
import pytest
|
||||
|
||||
# Create a mock user with regular role
|
||||
regular_user = User(
|
||||
id=1,
|
||||
email="user@example.com",
|
||||
name="Regular User",
|
||||
role="user",
|
||||
is_active=True,
|
||||
plan_id=1,
|
||||
credits=100
|
||||
)
|
||||
|
||||
# Test that get_admin_user raises exception for regular user
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await get_admin_user(regular_user)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
assert "Not enough permissions" in exc_info.value.detail
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_access_with_admin_role(
|
||||
self,
|
||||
test_client: AsyncClient,
|
||||
admin_headers: dict[str, str]
|
||||
) -> None:
|
||||
"""Test that admin users can access admin endpoints."""
|
||||
from app.core.dependencies import get_admin_user
|
||||
from app.models.user import User
|
||||
|
||||
# Create a mock admin user
|
||||
admin_user = User(
|
||||
id=1,
|
||||
email="admin@example.com",
|
||||
name="Admin User",
|
||||
role="admin",
|
||||
is_active=True,
|
||||
plan_id=1,
|
||||
credits=1000
|
||||
)
|
||||
|
||||
# Test that get_admin_user passes for admin user
|
||||
result = await get_admin_user(admin_user)
|
||||
assert result == admin_user
|
||||
212
tests/conftest.py
Normal file
212
tests/conftest.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""Test configuration and fixtures."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from fastapi import FastAPI
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlmodel import SQLModel, select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.main import create_app
|
||||
from app.models.plan import Plan
|
||||
from app.models.user import User
|
||||
from app.utils.auth import JWTUtils, PasswordUtils
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop() -> Any:
|
||||
"""Create an instance of the default event loop for the test session."""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="session")
|
||||
async def test_engine() -> Any:
|
||||
"""Create a test database engine."""
|
||||
# Use in-memory SQLite database for tests
|
||||
engine = create_async_engine(
|
||||
"sqlite+aiosqlite:///:memory:",
|
||||
echo=False,
|
||||
)
|
||||
|
||||
# Create all tables
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(SQLModel.metadata.create_all)
|
||||
|
||||
yield engine
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_session(test_engine: Any) -> AsyncGenerator[AsyncSession, None]:
|
||||
"""Create a test database session."""
|
||||
connection = await test_engine.connect()
|
||||
transaction = await connection.begin()
|
||||
|
||||
session = AsyncSession(bind=connection)
|
||||
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
await transaction.rollback()
|
||||
await connection.close()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_app(test_session: AsyncSession) -> FastAPI:
|
||||
"""Create a test FastAPI application."""
|
||||
app = create_app()
|
||||
|
||||
# Override the database dependency
|
||||
async def override_get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
yield test_session
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_client(test_app: FastAPI) -> AsyncGenerator[AsyncClient, None]:
|
||||
"""Create a test HTTP client."""
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=test_app),
|
||||
base_url="http://test",
|
||||
) as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_plan(test_session: AsyncSession) -> Plan:
|
||||
"""Create a test plan."""
|
||||
# Check if plan already exists in this session
|
||||
existing_plan = await test_session.exec(select(Plan).where(Plan.code == "free"))
|
||||
plan = existing_plan.first()
|
||||
|
||||
if not plan:
|
||||
plan = Plan(
|
||||
code="free",
|
||||
name="Free Plan",
|
||||
description="Test free plan",
|
||||
credits=100,
|
||||
max_credits=100,
|
||||
)
|
||||
test_session.add(plan)
|
||||
await test_session.commit()
|
||||
await test_session.refresh(plan)
|
||||
|
||||
return plan
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_pro_plan(test_session: AsyncSession) -> Plan:
|
||||
"""Create a test pro plan."""
|
||||
# Check if plan already exists in this session
|
||||
existing_plan = await test_session.exec(select(Plan).where(Plan.code == "pro"))
|
||||
plan = existing_plan.first()
|
||||
|
||||
if not plan:
|
||||
plan = Plan(
|
||||
code="pro",
|
||||
name="Pro Plan",
|
||||
description="Test pro plan",
|
||||
credits=300,
|
||||
max_credits=300,
|
||||
)
|
||||
test_session.add(plan)
|
||||
await test_session.commit()
|
||||
await test_session.refresh(plan)
|
||||
|
||||
return plan
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_user(test_session: AsyncSession, test_plan: Plan) -> User:
|
||||
"""Create a test user."""
|
||||
user = User(
|
||||
email="test@example.com",
|
||||
name="Test User",
|
||||
password_hash=PasswordUtils.hash_password("testpassword123"),
|
||||
role="user",
|
||||
is_active=True,
|
||||
plan_id=test_plan.id,
|
||||
credits=100,
|
||||
)
|
||||
test_session.add(user)
|
||||
await test_session.commit()
|
||||
await test_session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def admin_user(test_session: AsyncSession, test_plan: Plan) -> User:
|
||||
"""Create a test admin user."""
|
||||
user = User(
|
||||
email="admin@example.com",
|
||||
name="Admin User",
|
||||
password_hash=PasswordUtils.hash_password("adminpassword123"),
|
||||
role="admin",
|
||||
is_active=True,
|
||||
plan_id=test_plan.id,
|
||||
credits=1000,
|
||||
)
|
||||
test_session.add(user)
|
||||
await test_session.commit()
|
||||
await test_session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_user_data() -> dict[str, Any]:
|
||||
"""Test user registration data."""
|
||||
return {
|
||||
"email": "newuser@example.com",
|
||||
"password": "newpassword123",
|
||||
"name": "New User",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_login_data() -> dict[str, str]:
|
||||
"""Test user login data."""
|
||||
return {
|
||||
"email": "test@example.com",
|
||||
"password": "testpassword123",
|
||||
}
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def auth_headers(test_user: User) -> dict[str, str]:
|
||||
"""Create authentication headers with JWT token."""
|
||||
token_data = {
|
||||
"sub": str(test_user.id),
|
||||
"email": test_user.email,
|
||||
"role": test_user.role,
|
||||
}
|
||||
|
||||
access_token = JWTUtils.create_access_token(token_data)
|
||||
|
||||
return {"Authorization": f"Bearer {access_token}"}
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def admin_headers(admin_user: User) -> dict[str, str]:
|
||||
"""Create admin authentication headers with JWT token."""
|
||||
token_data = {
|
||||
"sub": str(admin_user.id),
|
||||
"email": admin_user.email,
|
||||
"role": admin_user.role,
|
||||
}
|
||||
|
||||
access_token = JWTUtils.create_access_token(token_data)
|
||||
|
||||
return {"Authorization": f"Bearer {access_token}"}
|
||||
1
tests/repositories/__init__.py
Normal file
1
tests/repositories/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Repository tests package."""
|
||||
336
tests/repositories/test_user.py
Normal file
336
tests/repositories/test_user.py
Normal file
@@ -0,0 +1,336 @@
|
||||
"""Tests for user repository."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlmodel import delete
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.models.plan import Plan
|
||||
from app.models.user import User
|
||||
from app.repositories.user import UserRepository
|
||||
from app.utils.auth import PasswordUtils
|
||||
|
||||
|
||||
class TestUserRepository:
|
||||
"""Test user repository operations."""
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def user_repository(
|
||||
self,
|
||||
test_session: AsyncSession,
|
||||
) -> AsyncGenerator[UserRepository, None]: # type: ignore[misc]
|
||||
"""Create a user repository instance."""
|
||||
yield UserRepository(test_session)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_id_existing(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
test_user: User,
|
||||
) -> None:
|
||||
"""Test getting user by ID when user exists."""
|
||||
assert test_user.id is not None
|
||||
user = await user_repository.get_by_id(test_user.id)
|
||||
|
||||
assert user is not None
|
||||
assert user.id == test_user.id
|
||||
assert user.email == test_user.email
|
||||
assert user.name == test_user.name
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_id_nonexistent(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
) -> None:
|
||||
"""Test getting user by ID when user doesn't exist."""
|
||||
user = await user_repository.get_by_id(99999)
|
||||
|
||||
assert user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_email_existing(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
test_user: User,
|
||||
) -> None:
|
||||
"""Test getting user by email when user exists."""
|
||||
user = await user_repository.get_by_email(test_user.email)
|
||||
|
||||
assert user is not None
|
||||
assert user.id == test_user.id
|
||||
assert user.email == test_user.email
|
||||
assert user.name == test_user.name
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_email_nonexistent(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
) -> None:
|
||||
"""Test getting user by email when user doesn't exist."""
|
||||
user = await user_repository.get_by_email("nonexistent@example.com")
|
||||
|
||||
assert user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_api_token_existing(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
test_user: User,
|
||||
test_session: AsyncSession,
|
||||
) -> None:
|
||||
"""Test getting user by API token when token exists."""
|
||||
# Set an API token for the test user
|
||||
test_token = "test_api_token_123"
|
||||
test_user.api_token = test_token
|
||||
await test_session.commit()
|
||||
|
||||
user = await user_repository.get_by_api_token(test_token)
|
||||
|
||||
assert user is not None
|
||||
assert user.id == test_user.id
|
||||
assert user.api_token == test_token
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_api_token_nonexistent(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
) -> None:
|
||||
"""Test getting user by API token when token doesn't exist."""
|
||||
user = await user_repository.get_by_api_token("nonexistent_token")
|
||||
|
||||
assert user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
test_plan: Plan,
|
||||
) -> None:
|
||||
"""Test creating a new user."""
|
||||
plan_id = test_plan.id
|
||||
plan_credits = test_plan.credits
|
||||
|
||||
user_data = {
|
||||
"email": "newuser@example.com",
|
||||
"name": "New User",
|
||||
"password_hash": PasswordUtils.hash_password("password123"),
|
||||
"role": "user",
|
||||
"is_active": True,
|
||||
}
|
||||
|
||||
user = await user_repository.create(user_data)
|
||||
|
||||
assert user.id is not None
|
||||
assert user.email == user_data["email"]
|
||||
assert user.name == user_data["name"]
|
||||
assert user.role == user_data["role"]
|
||||
assert user.is_active == user_data["is_active"]
|
||||
assert user.plan_id == plan_id
|
||||
assert user.credits == plan_credits
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_without_default_plan(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
test_session: AsyncSession,
|
||||
) -> None:
|
||||
"""Test creating user when no default plan exists."""
|
||||
# Remove all plans
|
||||
stmt = delete(Plan)
|
||||
# Use exec for delete statements
|
||||
await test_session.exec(stmt)
|
||||
await test_session.commit()
|
||||
|
||||
user_data = {
|
||||
"email": "newuser@example.com",
|
||||
"name": "New User",
|
||||
"password_hash": PasswordUtils.hash_password("password123"),
|
||||
"role": "user",
|
||||
"is_active": True,
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="Default plan not found"):
|
||||
await user_repository.create(user_data)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_user(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
test_user: User,
|
||||
) -> None:
|
||||
"""Test updating a user."""
|
||||
UPDATED_CREDITS = 200
|
||||
update_data = {
|
||||
"name": "Updated Name",
|
||||
"credits": UPDATED_CREDITS,
|
||||
}
|
||||
|
||||
updated_user = await user_repository.update(test_user, update_data)
|
||||
|
||||
assert updated_user.id == test_user.id
|
||||
assert updated_user.name == "Updated Name"
|
||||
assert updated_user.credits == UPDATED_CREDITS
|
||||
assert updated_user.email == test_user.email # Unchanged
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
test_plan: Plan, # noqa: ARG002
|
||||
) -> None:
|
||||
"""Test deleting a user."""
|
||||
# Create a user to delete
|
||||
user_data = {
|
||||
"email": "todelete@example.com",
|
||||
"name": "To Delete",
|
||||
"password_hash": PasswordUtils.hash_password("password123"),
|
||||
"role": "user",
|
||||
"is_active": True,
|
||||
}
|
||||
user = await user_repository.create(user_data)
|
||||
assert user.id is not None
|
||||
user_id = user.id
|
||||
|
||||
# Delete the user
|
||||
await user_repository.delete(user)
|
||||
|
||||
# Verify user is deleted
|
||||
deleted_user = await user_repository.get_by_id(user_id)
|
||||
assert deleted_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_email_exists_true(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
test_user: User,
|
||||
) -> None:
|
||||
"""Test email existence check when email exists."""
|
||||
exists = await user_repository.email_exists(test_user.email)
|
||||
|
||||
assert exists is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_email_exists_false(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
) -> None:
|
||||
"""Test email existence check when email doesn't exist."""
|
||||
exists = await user_repository.email_exists("nonexistent@example.com")
|
||||
|
||||
assert exists is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_email_exists_case_sensitive(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
test_user: User,
|
||||
) -> None:
|
||||
"""Test email existence check behavior with different cases."""
|
||||
# Test with lowercase email (original)
|
||||
exists_lower = await user_repository.email_exists(test_user.email.lower())
|
||||
|
||||
# SQLite with default collation should be case insensitive for email searches
|
||||
# But the test shows it's case sensitive, so let's test the actual behavior
|
||||
assert exists_lower is True # Original email should exist
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_first_user_gets_admin_role_and_pro_plan(
|
||||
self,
|
||||
test_session: AsyncSession,
|
||||
) -> None:
|
||||
"""Test that the first user gets admin role and pro plan."""
|
||||
# Ensure no users exist
|
||||
stmt = delete(User)
|
||||
await test_session.exec(stmt)
|
||||
await test_session.commit()
|
||||
|
||||
# Create plans since they were cleared by other tests
|
||||
free_plan = Plan(code="free", name="Free Plan", credits=100, max_credits=100)
|
||||
pro_plan = Plan(code="pro", name="Pro Plan", credits=300, max_credits=300)
|
||||
test_session.add(free_plan)
|
||||
test_session.add(pro_plan)
|
||||
await test_session.commit()
|
||||
await test_session.refresh(free_plan)
|
||||
await test_session.refresh(pro_plan)
|
||||
|
||||
user_repository = UserRepository(test_session)
|
||||
|
||||
user_data = {
|
||||
"email": "first@example.com",
|
||||
"name": "First User",
|
||||
"password_hash": PasswordUtils.hash_password("password123"),
|
||||
"is_active": True,
|
||||
}
|
||||
|
||||
user = await user_repository.create(user_data)
|
||||
|
||||
assert user.id is not None
|
||||
assert user.email == user_data["email"]
|
||||
assert user.name == user_data["name"]
|
||||
assert user.role == "admin" # First user should be admin
|
||||
assert user.is_active == user_data["is_active"]
|
||||
assert user.plan_id == pro_plan.id # Should get pro plan
|
||||
assert user.credits == pro_plan.credits
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subsequent_users_get_user_role_and_free_plan(
|
||||
self,
|
||||
test_session: AsyncSession,
|
||||
) -> None:
|
||||
"""Test that subsequent users get user role and free plan."""
|
||||
# Ensure no users exist
|
||||
stmt = delete(User)
|
||||
await test_session.exec(stmt)
|
||||
await test_session.commit()
|
||||
|
||||
# Create plans since they were cleared by other tests
|
||||
free_plan = Plan(code="free", name="Free Plan", credits=100, max_credits=100)
|
||||
pro_plan = Plan(code="pro", name="Pro Plan", credits=300, max_credits=300)
|
||||
test_session.add(free_plan)
|
||||
test_session.add(pro_plan)
|
||||
await test_session.commit()
|
||||
await test_session.refresh(free_plan)
|
||||
await test_session.refresh(pro_plan)
|
||||
|
||||
user_repository = UserRepository(test_session)
|
||||
|
||||
# Create first user
|
||||
first_user_data = {
|
||||
"email": "first@example.com",
|
||||
"name": "First User",
|
||||
"password_hash": PasswordUtils.hash_password("password123"),
|
||||
"is_active": True,
|
||||
}
|
||||
first_user = await user_repository.create(first_user_data)
|
||||
assert first_user.role == "admin" # Verify first user is admin
|
||||
|
||||
# Create second user
|
||||
second_user_data = {
|
||||
"email": "second@example.com",
|
||||
"name": "Second User",
|
||||
"password_hash": PasswordUtils.hash_password("password123"),
|
||||
"is_active": True,
|
||||
}
|
||||
second_user = await user_repository.create(second_user_data)
|
||||
|
||||
assert second_user.id is not None
|
||||
assert second_user.email == second_user_data["email"]
|
||||
assert second_user.name == second_user_data["name"]
|
||||
assert second_user.role == "user" # Second user should be regular user
|
||||
assert second_user.is_active == second_user_data["is_active"]
|
||||
assert second_user.plan_id == free_plan.id # Should get free plan
|
||||
assert second_user.credits == free_plan.credits
|
||||
|
||||
# Create third user to further verify
|
||||
third_user_data = {
|
||||
"email": "third@example.com",
|
||||
"name": "Third User",
|
||||
"password_hash": PasswordUtils.hash_password("password123"),
|
||||
"is_active": True,
|
||||
}
|
||||
third_user = await user_repository.create(third_user_data)
|
||||
|
||||
assert third_user.role == "user" # Third user should also be regular user
|
||||
assert third_user.plan_id == free_plan.id # Should get free plan
|
||||
1
tests/services/__init__.py
Normal file
1
tests/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Services tests package."""
|
||||
225
tests/services/test_auth_service.py
Normal file
225
tests/services/test_auth_service.py
Normal file
@@ -0,0 +1,225 @@
|
||||
"""Tests for authentication service."""
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from fastapi import HTTPException
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.models.plan import Plan
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import UserLoginRequest, UserRegisterRequest
|
||||
from app.services.auth import AuthService
|
||||
from app.utils.auth import PasswordUtils
|
||||
|
||||
|
||||
class TestAuthService:
|
||||
"""Test authentication service operations."""
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
def auth_service(self, test_session: AsyncSession) -> AuthService:
|
||||
"""Create an auth service instance."""
|
||||
return AuthService(test_session)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_success(
|
||||
self, auth_service: AuthService, test_plan: Plan, test_user_data: dict[str, str]
|
||||
) -> None:
|
||||
"""Test successful user registration."""
|
||||
request = UserRegisterRequest(**test_user_data)
|
||||
|
||||
response = await auth_service.register(request)
|
||||
|
||||
# Check user data
|
||||
assert response.user.email == test_user_data["email"]
|
||||
assert response.user.name == test_user_data["name"]
|
||||
assert response.user.role == "user"
|
||||
assert response.user.is_active is True
|
||||
assert response.user.credits == test_plan.credits
|
||||
assert response.user.plan["code"] == test_plan.code
|
||||
|
||||
# Check token
|
||||
assert response.token.access_token is not None
|
||||
assert response.token.token_type == "bearer"
|
||||
assert response.token.expires_in > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_duplicate_email(
|
||||
self, auth_service: AuthService, test_user: User
|
||||
) -> None:
|
||||
"""Test registration with duplicate email."""
|
||||
request = UserRegisterRequest(
|
||||
email=test_user.email, password="password123", name="Another User"
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await auth_service.register(request)
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "Email address is already registered" in exc_info.value.detail
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_success(
|
||||
self,
|
||||
auth_service: AuthService,
|
||||
test_user: User,
|
||||
test_login_data: dict[str, str],
|
||||
) -> None:
|
||||
"""Test successful user login."""
|
||||
request = UserLoginRequest(**test_login_data)
|
||||
|
||||
response = await auth_service.login(request)
|
||||
|
||||
# Check user data
|
||||
assert response.user.id == test_user.id
|
||||
assert response.user.email == test_user.email
|
||||
assert response.user.name == test_user.name
|
||||
assert response.user.role == test_user.role
|
||||
assert response.user.is_active == test_user.is_active
|
||||
|
||||
# Check token
|
||||
assert response.token.access_token is not None
|
||||
assert response.token.token_type == "bearer"
|
||||
assert response.token.expires_in > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_invalid_email(self, auth_service: AuthService) -> None:
|
||||
"""Test login with invalid email."""
|
||||
request = UserLoginRequest(
|
||||
email="nonexistent@example.com", password="password123"
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await auth_service.login(request)
|
||||
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Invalid email or password" in exc_info.value.detail
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_invalid_password(
|
||||
self, auth_service: AuthService, test_user: User
|
||||
) -> None:
|
||||
"""Test login with invalid password."""
|
||||
request = UserLoginRequest(email=test_user.email, password="wrongpassword")
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await auth_service.login(request)
|
||||
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Invalid email or password" in exc_info.value.detail
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_inactive_user(
|
||||
self, auth_service: AuthService, test_user: User, test_session: AsyncSession
|
||||
) -> None:
|
||||
"""Test login with inactive user."""
|
||||
# Store the email before deactivating
|
||||
user_email = test_user.email
|
||||
|
||||
# Deactivate the user
|
||||
test_user.is_active = False
|
||||
await test_session.commit()
|
||||
|
||||
request = UserLoginRequest(email=user_email, password="testpassword123")
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await auth_service.login(request)
|
||||
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Account is deactivated" in exc_info.value.detail
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_user_without_password(
|
||||
self, auth_service: AuthService, test_user: User, test_session: AsyncSession
|
||||
) -> None:
|
||||
"""Test login with user that has no password hash."""
|
||||
# Store the email before removing password
|
||||
user_email = test_user.email
|
||||
|
||||
# Remove password hash
|
||||
test_user.password_hash = None
|
||||
await test_session.commit()
|
||||
|
||||
request = UserLoginRequest(email=user_email, password="anypassword")
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await auth_service.login(request)
|
||||
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Invalid email or password" in exc_info.value.detail
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_success(
|
||||
self, auth_service: AuthService, test_user: User
|
||||
) -> None:
|
||||
"""Test getting current user successfully."""
|
||||
user = await auth_service.get_current_user(test_user.id)
|
||||
|
||||
assert user.id == test_user.id
|
||||
assert user.email == test_user.email
|
||||
assert user.name == test_user.name
|
||||
assert user.is_active == test_user.is_active
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_not_found(self, auth_service: AuthService) -> None:
|
||||
"""Test getting current user when user doesn't exist."""
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await auth_service.get_current_user(99999)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert "User not found" in exc_info.value.detail
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_inactive(
|
||||
self, auth_service: AuthService, test_user: User, test_session: AsyncSession
|
||||
) -> None:
|
||||
"""Test getting current user when user is inactive."""
|
||||
# Store the user ID before deactivating
|
||||
user_id = test_user.id
|
||||
|
||||
# Deactivate the user
|
||||
test_user.is_active = False
|
||||
await test_session.commit()
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await auth_service.get_current_user(user_id)
|
||||
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Account is deactivated" in exc_info.value.detail
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_access_token(
|
||||
self, auth_service: AuthService, test_user: User
|
||||
) -> None:
|
||||
"""Test access token creation."""
|
||||
token_response = auth_service._create_access_token(test_user)
|
||||
|
||||
assert token_response.access_token is not None
|
||||
assert token_response.token_type == "bearer"
|
||||
assert token_response.expires_in > 0
|
||||
|
||||
# Verify token contains correct data
|
||||
from app.utils.auth import JWTUtils
|
||||
|
||||
decoded = JWTUtils.decode_access_token(token_response.access_token)
|
||||
assert decoded["sub"] == str(test_user.id)
|
||||
assert decoded["email"] == test_user.email
|
||||
assert decoded["role"] == test_user.role
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_response(
|
||||
self, auth_service: AuthService, test_user: User, test_session: AsyncSession
|
||||
) -> None:
|
||||
"""Test user response creation."""
|
||||
# Ensure plan relationship is loaded
|
||||
await test_session.refresh(test_user, ["plan"])
|
||||
|
||||
user_response = await auth_service._create_user_response(test_user)
|
||||
|
||||
assert user_response.id == test_user.id
|
||||
assert user_response.email == test_user.email
|
||||
assert user_response.name == test_user.name
|
||||
assert user_response.role == test_user.role
|
||||
assert user_response.credits == test_user.credits
|
||||
assert user_response.is_active == test_user.is_active
|
||||
assert user_response.plan["id"] == test_user.plan.id
|
||||
assert user_response.plan["code"] == test_user.plan.code
|
||||
1
tests/utils/__init__.py
Normal file
1
tests/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Utils tests package."""
|
||||
182
tests/utils/test_auth_utils.py
Normal file
182
tests/utils/test_auth_utils.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""Tests for authentication utilities."""
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.utils.auth import JWTUtils, PasswordUtils, TokenUtils
|
||||
|
||||
PASSWORD_HASH_LENGTH_GT = 50
|
||||
TOKEN_LENGTH_GT = 20
|
||||
TOKEN_DOTS_COUNT = 2
|
||||
STATUS_CODE_UNAUTHORIZED = 401
|
||||
|
||||
|
||||
class TestPasswordUtils:
|
||||
"""Test password utility functions."""
|
||||
|
||||
def test_hash_password(self) -> None:
|
||||
"""Test password hashing."""
|
||||
password = "testpassword123"
|
||||
hashed = PasswordUtils.hash_password(password)
|
||||
|
||||
# Hash should be different from original password
|
||||
assert hashed != password
|
||||
# Hash should be a string
|
||||
assert isinstance(hashed, str)
|
||||
# Hash should have reasonable length (bcrypt produces ~60 chars)
|
||||
assert len(hashed) > PASSWORD_HASH_LENGTH_GT
|
||||
|
||||
def test_hash_password_different_salts(self) -> None:
|
||||
"""Test that same password produces different hashes (different salts)."""
|
||||
password = "testpassword123"
|
||||
hash1 = PasswordUtils.hash_password(password)
|
||||
hash2 = PasswordUtils.hash_password(password)
|
||||
|
||||
# Same password should produce different hashes due to different salts
|
||||
assert hash1 != hash2
|
||||
|
||||
def test_verify_password_correct(self) -> None:
|
||||
"""Test password verification with correct password."""
|
||||
password = "testpassword123"
|
||||
hashed = PasswordUtils.hash_password(password)
|
||||
|
||||
# Correct password should verify
|
||||
assert PasswordUtils.verify_password(password, hashed) is True
|
||||
|
||||
def test_verify_password_incorrect(self) -> None:
|
||||
"""Test password verification with incorrect password."""
|
||||
password = "testpassword123"
|
||||
wrong_password = "wrongpassword"
|
||||
hashed = PasswordUtils.hash_password(password)
|
||||
|
||||
# Wrong password should not verify
|
||||
assert PasswordUtils.verify_password(wrong_password, hashed) is False
|
||||
|
||||
def test_verify_password_empty(self) -> None:
|
||||
"""Test password verification with empty password."""
|
||||
password = "testpassword123"
|
||||
hashed = PasswordUtils.hash_password(password)
|
||||
|
||||
# Empty password should not verify
|
||||
assert PasswordUtils.verify_password("", hashed) is False
|
||||
|
||||
|
||||
class TestJWTUtils:
|
||||
"""Test JWT utility functions."""
|
||||
|
||||
def test_create_access_token(self) -> None:
|
||||
"""Test JWT token creation."""
|
||||
data = {"sub": "123", "email": "test@example.com"}
|
||||
token = JWTUtils.create_access_token(data)
|
||||
|
||||
# Token should be a string
|
||||
assert isinstance(token, str)
|
||||
# Token should have reasonable length
|
||||
assert len(token) > PASSWORD_HASH_LENGTH_GT
|
||||
# Token should contain dots (JWT format)
|
||||
assert token.count(".") == TOKEN_DOTS_COUNT
|
||||
|
||||
def test_create_access_token_with_expiry(self) -> None:
|
||||
"""Test JWT token creation with custom expiry."""
|
||||
data = {"sub": "123", "email": "test@example.com"}
|
||||
expires_delta = timedelta(minutes=5)
|
||||
token = JWTUtils.create_access_token(data, expires_delta)
|
||||
|
||||
# Should create a valid token
|
||||
assert isinstance(token, str)
|
||||
assert len(token) > PASSWORD_HASH_LENGTH_GT
|
||||
|
||||
def test_decode_access_token(self) -> None:
|
||||
"""Test JWT token decoding."""
|
||||
original_data = {"sub": "123", "email": "test@example.com", "role": "user"}
|
||||
token = JWTUtils.create_access_token(original_data)
|
||||
|
||||
decoded_data = JWTUtils.decode_access_token(token)
|
||||
|
||||
# Should decode to original data (plus exp)
|
||||
assert decoded_data["sub"] == original_data["sub"]
|
||||
assert decoded_data["email"] == original_data["email"]
|
||||
assert decoded_data["role"] == original_data["role"]
|
||||
assert "exp" in decoded_data
|
||||
|
||||
def test_decode_invalid_token(self) -> None:
|
||||
"""Test decoding invalid JWT token."""
|
||||
invalid_token = "invalid.token.here"
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
JWTUtils.decode_access_token(invalid_token)
|
||||
|
||||
assert exc_info.value.status_code == STATUS_CODE_UNAUTHORIZED
|
||||
assert "Could not validate credentials" in exc_info.value.detail
|
||||
|
||||
def test_decode_expired_token(self) -> None:
|
||||
"""Test decoding expired JWT token."""
|
||||
data = {"sub": "123", "email": "test@example.com"}
|
||||
# Create token that expires immediately
|
||||
expires_delta = timedelta(seconds=-1)
|
||||
token = JWTUtils.create_access_token(data, expires_delta)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
JWTUtils.decode_access_token(token)
|
||||
|
||||
assert exc_info.value.status_code == STATUS_CODE_UNAUTHORIZED
|
||||
assert "Token has expired" in exc_info.value.detail
|
||||
|
||||
def test_decode_empty_token(self) -> None:
|
||||
"""Test decoding empty token."""
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
JWTUtils.decode_access_token("")
|
||||
|
||||
assert exc_info.value.status_code == STATUS_CODE_UNAUTHORIZED
|
||||
|
||||
|
||||
class TestTokenUtils:
|
||||
"""Test token utility functions."""
|
||||
|
||||
def test_generate_api_token(self) -> None:
|
||||
"""Test API token generation."""
|
||||
token = TokenUtils.generate_api_token()
|
||||
|
||||
# Token should be a string
|
||||
assert isinstance(token, str)
|
||||
# Token should have reasonable length
|
||||
assert len(token) > TOKEN_LENGTH_GT
|
||||
# Token should be URL-safe
|
||||
assert all(c.isalnum() or c in "-_" for c in token)
|
||||
|
||||
def test_generate_api_token_unique(self) -> None:
|
||||
"""Test that API tokens are unique."""
|
||||
token1 = TokenUtils.generate_api_token()
|
||||
token2 = TokenUtils.generate_api_token()
|
||||
|
||||
# Tokens should be different
|
||||
assert token1 != token2
|
||||
|
||||
def test_is_token_expired_none(self) -> None:
|
||||
"""Test token expiry check with None."""
|
||||
# None expiry should not be expired
|
||||
assert TokenUtils.is_token_expired(None) is False
|
||||
|
||||
def test_is_token_expired_future(self) -> None:
|
||||
"""Test token expiry check with future date."""
|
||||
future_date = datetime.now(UTC) + timedelta(hours=1)
|
||||
|
||||
# Future date should not be expired
|
||||
assert TokenUtils.is_token_expired(future_date) is False
|
||||
|
||||
def test_is_token_expired_past(self) -> None:
|
||||
"""Test token expiry check with past date."""
|
||||
past_date = datetime.now(UTC) - timedelta(hours=1)
|
||||
|
||||
# Past date should be expired
|
||||
assert TokenUtils.is_token_expired(past_date) is True
|
||||
|
||||
def test_is_token_expired_naive_datetime(self) -> None:
|
||||
"""Test token expiry check with naive datetime."""
|
||||
# Create a past naive datetime (using UTC time to be consistent)
|
||||
past_date = datetime.now(UTC) - timedelta(hours=1)
|
||||
|
||||
# Should handle naive datetime (treat as UTC)
|
||||
assert TokenUtils.is_token_expired(past_date) is True
|
||||
Reference in New Issue
Block a user