Working on API

This commit is contained in:
2025-09-25 22:58:31 +02:00
parent 48f5b009ae
commit 1f7ef200e7
16 changed files with 618 additions and 63 deletions

View File

@@ -13,7 +13,7 @@ architecture with Redis for task queuing and MongoDB for data persistence.
- **Backend API**: FastAPI (Python 3.12) - **Backend API**: FastAPI (Python 3.12)
- **Task Processing**: Celery with Redis broker - **Task Processing**: Celery with Redis broker
- **Document Processing**: EasyOCR, PyMuPDF, python-docx, pdfplumber - **Document Processing**: EasyOCR, PyMuPDF, python-docx, pdfplumber
- **Database**: MongoDB - **Database**: MongoDB (pymongo)
- **Frontend**: React - **Frontend**: React
- **Containerization**: Docker & Docker Compose - **Containerization**: Docker & Docker Compose
- **File Monitoring**: Python watchdog library - **File Monitoring**: Python watchdog library
@@ -109,16 +109,18 @@ MyDocManager/
│ │ │ │ └── types.py # PyObjectId and other useful types │ │ │ │ └── types.py # PyObjectId and other useful types
│ │ │ ├── database/ │ │ │ ├── database/
│ │ │ │ ├── __init__.py │ │ │ │ ├── __init__.py
│ │ │ │ ├── connection.py # MongoDB connection │ │ │ │ ├── connection.py # MongoDB connection (pymongo)
│ │ │ │ └── repositories/ │ │ │ │ └── repositories/
│ │ │ │ ├── __init__.py │ │ │ │ ├── __init__.py
│ │ │ │ ├── user_repository.py # User CRUD operations │ │ │ │ ├── user_repository.py # User CRUD operations (synchronous)
│ │ │ │ ── document_repository.py # User CRUD operations │ │ │ │ ── document_repository.py # Document CRUD operations (synchronous)
│ │ │ │ └── job_repository.py # Job CRUD operations (synchronous)
│ │ │ ├── services/ │ │ │ ├── services/
│ │ │ │ ├── __init__.py │ │ │ │ ├── __init__.py
│ │ │ │ ├── auth_service.py # JWT & password logic │ │ │ │ ├── auth_service.py # JWT & password logic (synchronous)
│ │ │ │ ├── user_service.py # User business logic │ │ │ │ ├── user_service.py # User business logic (synchronous)
│ │ │ │ ├── document_service.py # Document business logic │ │ │ │ ├── document_service.py # Document business logic (synchronous)
│ │ │ │ ├── job_service.py # Job processing logic (synchronous)
│ │ │ │ └── init_service.py # Admin creation at startup │ │ │ │ └── init_service.py # Admin creation at startup
│ │ │ ├── api/ │ │ │ ├── api/
│ │ │ │ ├── __init__.py │ │ │ │ ├── __init__.py
@@ -334,13 +336,20 @@ class ProcessingJob(BaseModel):
- **Rationale**: MongoDB is not meant for large files, better performance. Files remain in the file system for easy - **Rationale**: MongoDB is not meant for large files, better performance. Files remain in the file system for easy
access. access.
### Implementation Order #### Repository and Services Implementation
- **Choice**: Synchronous implementation using pymongo
- **Rationale**: Full compatibility with Celery workers and simplified workflow
- **Implementation**: All repositories and services operate synchronously for seamless integration
### Implementation Status
1. ✅ Pydantic models for MongoDB collections 1. ✅ Pydantic models for MongoDB collections
2. UNDER PROGRESS : Repository layer for data access (files + processing_jobs) 2. Repository layer for data access (files + processing_jobs + users + documents) - synchronous
3. TODO : Celery tasks for document processing 3. ✅ Service layer for business logic (auth, user, document, job) - synchronous
4. TODO : Watchdog file monitoring implementation 4. ✅ Celery tasks for document processing
5. TODO : FastAPI integration and startup coordination 5. ✅ Watchdog file monitoring implementation
6. ✅ FastAPI integration and startup coordination
## Job Management Layer ## Job Management Layer
@@ -350,7 +359,7 @@ The job management system follows the repository pattern for clean separation be
#### JobRepository #### JobRepository
Handles direct MongoDB operations for processing jobs: Handles direct MongoDB operations for processing jobs using synchronous pymongo:
**CRUD Operations:** **CRUD Operations:**
- `create_job()` - Create new processing job with automatic `created_at` timestamp - `create_job()` - Create new processing job with automatic `created_at` timestamp
@@ -367,7 +376,7 @@ Handles direct MongoDB operations for processing jobs:
#### JobService #### JobService
Provides business logic layer with strict status transition validation: Provides synchronous business logic layer with strict status transition validation:
**Status Transition Methods:** **Status Transition Methods:**
- `mark_job_as_started()` - PENDING → PROCESSING - `mark_job_as_started()` - PENDING → PROCESSING
@@ -381,7 +390,6 @@ Provides business logic layer with strict status transition validation:
#### Custom Exceptions #### Custom Exceptions
**JobNotFoundError**: Raised when job ID doesn't exist
**InvalidStatusTransitionError**: Raised for invalid status transitions **InvalidStatusTransitionError**: Raised for invalid status transitions
**JobRepositoryError**: Raised for MongoDB operation failures **JobRepositoryError**: Raised for MongoDB operation failures
@@ -400,11 +408,17 @@ All other transitions are forbidden and will raise `InvalidStatusTransitionError
``` ```
src/file-processor/app/ src/file-processor/app/
├── database/repositories/ ├── database/repositories/
── job_repository.py # JobRepository class ── job_repository.py # JobRepository class (synchronous)
│ ├── user_repository.py # UserRepository class (synchronous)
│ ├── document_repository.py # DocumentRepository class (synchronous)
│ └── file_repository.py # FileRepository class (synchronous)
├── services/ ├── services/
── job_service.py # JobService class ── job_service.py # JobService class (synchronous)
│ ├── auth_service.py # AuthService class (synchronous)
│ ├── user_service.py # UserService class (synchronous)
│ └── document_service.py # DocumentService class (synchronous)
└── exceptions/ └── exceptions/
└── job_exceptions.py # Custom exceptions └── job_exceptions.py # Custom exceptions
``` ```
### Processing Pipeline Features ### Processing Pipeline Features
@@ -414,6 +428,7 @@ src/file-processor/app/
- **Status Tracking**: Real-time processing status via `processing_jobs` collection - **Status Tracking**: Real-time processing status via `processing_jobs` collection
- **Extensible Metadata**: Flexible metadata storage per file type - **Extensible Metadata**: Flexible metadata storage per file type
- **Multiple Extraction Methods**: Support for direct text, OCR, and hybrid approaches - **Multiple Extraction Methods**: Support for direct text, OCR, and hybrid approaches
- **Synchronous Operations**: All database operations use pymongo for Celery compatibility
## Key Implementation Notes ## Key Implementation Notes
@@ -436,6 +451,7 @@ src/file-processor/app/
- **Package Manager**: pip (standard) - **Package Manager**: pip (standard)
- **External Dependencies**: Listed in each service's requirements.txt - **External Dependencies**: Listed in each service's requirements.txt
- **Standard Library First**: Prefer standard library when possible - **Standard Library First**: Prefer standard library when possible
- **Database Driver**: pymongo for synchronous MongoDB operations
### Testing Strategy ### Testing Strategy
@@ -460,6 +476,7 @@ src/file-processor/app/
12. **Content in Files Collection**: Extracted content stored with file metadata 12. **Content in Files Collection**: Extracted content stored with file metadata
13. **Direct Task Dispatch**: File watcher directly creates Celery tasks 13. **Direct Task Dispatch**: File watcher directly creates Celery tasks
14. **SHA256 Duplicate Detection**: Prevents reprocessing identical files 14. **SHA256 Duplicate Detection**: Prevents reprocessing identical files
15. **Synchronous Implementation**: All repositories and services use pymongo for Celery compatibility
### Development Process Requirements ### Development Process Requirements
@@ -470,12 +487,13 @@ src/file-processor/app/
### Next Implementation Steps ### Next Implementation Steps
1. **IN PROGRESS**: Implement file processing pipeline => 1. **TODO**: Complete file processing pipeline =>
1. Create Pydantic models for files and processing_jobs collections 1. Create Pydantic models for files and processing_jobs collections
2. Implement repository layer for file and processing job data access 2. Implement repository layer for file and processing job data access (synchronous)
3. Create Celery tasks for document processing (.txt, .pdf, .docx) 3. ✅ Implement service layer for business logic (synchronous)
4. Implement Watchdog file monitoring with dedicated observer 4. ✅ Create Celery tasks for document processing (.txt, .pdf, .docx)
5. Integrate file watcher with FastAPI startup 5. ✅ Implement Watchdog file monitoring with dedicated observer
6. ✅ Integrate file watcher with FastAPI startup
2. Create protected API routes for user management 2. Create protected API routes for user management
3. Build React monitoring interface with authentication 3. Build React monitoring interface with authentication
@@ -566,4 +584,4 @@ docker-compose up --scale worker=3
- **file-processor**: Hot-reload enabled via `--reload` flag - **file-processor**: Hot-reload enabled via `--reload` flag
- Code changes in `src/file-processor/app/` automatically restart FastAPI - Code changes in `src/file-processor/app/` automatically restart FastAPI
- **worker**: No hot-reload (manual restart required for stability) - **worker**: No hot-reload (manual restart required for stability)
- Code changes in `src/worker/tasks/` require: `docker-compose restart worker` - Code changes in `src/worker/tasks/` require: `docker-compose restart worker`

View File

@@ -5,16 +5,22 @@ asgiref==3.9.1
bcrypt==4.3.0 bcrypt==4.3.0
billiard==4.2.1 billiard==4.2.1
celery==5.5.3 celery==5.5.3
certifi==2025.8.3
cffi==2.0.0
click==8.2.1 click==8.2.1
click-didyoumean==0.3.1 click-didyoumean==0.3.1
click-plugins==1.1.1.2 click-plugins==1.1.1.2
click-repl==0.3.0 click-repl==0.3.0
cryptography==46.0.1
dnspython==2.8.0 dnspython==2.8.0
ecdsa==0.19.1
email-validator==2.3.0 email-validator==2.3.0
fastapi==0.116.1 fastapi==0.116.1
h11==0.16.0 h11==0.16.0
hiredis==3.2.1 hiredis==3.2.1
httpcore==1.0.9
httptools==0.6.4 httptools==0.6.4
httpx==0.28.1
idna==3.10 idna==3.10
importlib_metadata==8.7.0 importlib_metadata==8.7.0
iniconfig==2.1.0 iniconfig==2.1.0
@@ -27,6 +33,8 @@ packaging==25.0
pipdeptree==2.28.0 pipdeptree==2.28.0
pluggy==1.6.0 pluggy==1.6.0
prompt_toolkit==3.0.52 prompt_toolkit==3.0.52
pyasn1==0.6.1
pycparser==2.23
pycron==3.2.0 pycron==3.2.0
pydantic==2.11.9 pydantic==2.11.9
pydantic_core==2.33.2 pydantic_core==2.33.2
@@ -42,6 +50,7 @@ python-magic==0.4.27
pytz==2025.2 pytz==2025.2
PyYAML==6.0.2 PyYAML==6.0.2
redis==6.4.0 redis==6.4.0
rsa==4.9.1
sentinels==1.1.1 sentinels==1.1.1
six==1.17.0 six==1.17.0
sniffio==1.3.1 sniffio==1.3.1

View File

View File

@@ -0,0 +1,100 @@
import jwt
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jwt import InvalidTokenError
from starlette import status
from app.config import settings
from app.database.connection import get_database
from app.models.auth import UserRole
from app.models.user import UserInDB
from app.services.auth_service import AuthService
from app.services.user_service import UserService
security = HTTPBearer()
def get_auth_service() -> AuthService:
"""Dependency to get AuthService instance."""
return AuthService()
def get_user_service() -> UserService:
"""Dependency to get UserService instance."""
database = get_database()
return UserService(database)
def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
user_service: UserService = Depends(get_user_service)
) -> UserInDB:
"""
Dependency to get current authenticated user from JWT token.
Args:
credentials: HTTP Bearer credentials
user_service: Auth service instance
Returns:
User: Current authenticated user
Raises:
HTTPException: If token is invalid or user not found
"""
try:
payload = jwt.decode(
credentials.credentials,
settings.get_jwt_secret_key(),
algorithms=[settings.get_jwt_algorithm()]
)
username: str = payload.get("sub")
if username is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
except InvalidTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
user = user_service.get_user_by_username(username)
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
return user
def get_admin_user(current_user: UserInDB = Depends(get_current_user)) -> UserInDB:
"""
Dependency to ensure current user has admin role.
Args:
current_user: Current authenticated user
Returns:
User: Current user if admin
Raises:
HTTPException: If user is not admin
"""
if current_user.role != UserRole.ADMIN:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return current_user

View File

@@ -0,0 +1,80 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from app.api.dependencies import get_auth_service, get_current_user, get_user_service
from app.models.auth import LoginResponse, UserResponse
from app.models.user import UserInDB
from app.services.auth_service import AuthService
from app.services.user_service import UserService
router = APIRouter(tags=["authentication"])
@router.post("/login", response_model=LoginResponse)
def login(
form_data: OAuth2PasswordRequestForm = Depends(),
auth_service: AuthService = Depends(get_auth_service),
user_service: UserService = Depends(get_user_service)
):
"""
Authenticate user and return JWT token.
Args:
form_data: OAuth2 password form data
auth_service: Auth service instance
user_service: User service instance
Returns:
LoginResponse: JWT token and user info
Raises:
HTTPException: If authentication fails
"""
incorrect_username_or_pwd = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
user = user_service.get_user_by_username(form_data.username)
if (not user or
not user.is_active or
not auth_service.verify_user_password(form_data.password, user.hashed_password)):
raise incorrect_username_or_pwd
access_token = auth_service.create_access_token(data={"sub": user.username})
return LoginResponse(
access_token=access_token,
user=UserResponse(
_id=user.id,
username=user.username,
email=user.email,
role=user.role,
is_active=user.is_active,
created_at=user.created_at,
updated_at=user.updated_at
)
)
@router.get("/me", response_model=UserResponse)
def get_current_user_profile(current_user: UserInDB = Depends(get_current_user)):
"""
Get current user profile.
Args:
current_user: Current authenticated user
Returns:
UserResponse: Current user profile without sensitive data
"""
return UserResponse(
_id=current_user.id,
username=current_user.username,
email=current_user.email,
role=current_user.role,
is_active=current_user.is_active,
created_at=current_user.created_at,
updated_at=current_user.updated_at
)

View File

@@ -0,0 +1,172 @@
from fastapi import APIRouter, Depends, HTTPException
from starlette import status
from app.api.dependencies import get_admin_user, get_user_service
from app.models.auth import UserResponse, MessageResponse
from app.models.types import PyObjectId
from app.models.user import UserInDB, UserCreate, UserUpdate
from app.services.user_service import UserService
router = APIRouter(tags=["users"])
@router.get("", response_model=list[UserInDB])
def list_users(
admin_user: UserInDB = Depends(get_admin_user),
user_service: UserService = Depends(get_user_service)
):
"""
List all users (admin only).
Args:
admin_user: Current admin user
user_service: User service instance
Returns:
List[UserResponse]: List of all users without sensitive data
"""
return user_service.list_users()
@router.get("/{user_id}", response_model=UserResponse)
def get_user_by_id(
user_id: PyObjectId,
admin_user: UserInDB = Depends(get_admin_user),
user_service: UserService = Depends(get_user_service)
):
"""
Get specific user by ID (admin only).
Args:
user_id: User ID to retrieve
admin_user: Current admin user
user_service: User service instance
Returns:
UserResponse: User information without sensitive data
Raises:
HTTPException: If user not found
"""
user = user_service.get_user_by_id(str(user_id))
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return user
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
def create_user(
user_data: UserCreate,
admin_user: UserInDB = Depends(get_admin_user),
user_service: UserService = Depends(get_user_service)
):
"""
Create new user (admin only).
Args:
user_data: User creation data
admin_user: Current admin user
user_service: User service instance
Returns:
UserResponse: Created user information without sensitive data
Raises:
HTTPException: If user creation fails
"""
try:
user = user_service.create_user(user_data)
return UserResponse(
_id=user.id,
username=user.username,
email=user.email,
role=user.role,
is_active=user.is_active,
created_at=user.created_at,
updated_at=user.updated_at
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.put("/{user_id}", response_model=UserResponse)
def update_user(
user_id: PyObjectId,
user_data: UserUpdate,
admin_user: UserInDB = Depends(get_admin_user),
user_service: UserService = Depends(get_user_service)
):
"""
Update existing user (admin only).
Args:
user_id: User ID to update
user_data: User update data
admin_user: Current admin user
user_service: User service instance
Returns:
UserResponse: Updated user information without sensitive data
Raises:
HTTPException: If user not found or update fails
"""
try:
user = user_service.update_user(str(user_id), user_data)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return UserResponse(
_id=user.id,
username=user.username,
email=user.email,
role=user.role,
is_active=user.is_active,
created_at=user.created_at,
updated_at=user.updated_at
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.delete("/{user_id}", response_model=MessageResponse)
def delete_user(
user_id: PyObjectId,
admin_user: UserInDB = Depends(get_admin_user),
user_service: UserService = Depends(get_user_service)
):
"""
Delete user by ID (admin only).
Args:
user_id: User ID to delete
admin_user: Current admin user
user_service: User service instance
Returns:
MessageResponse: Success message
Raises:
HTTPException: If user not found or deletion fails
"""
success = user_service.delete_user(str(user_id))
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return MessageResponse(message="User successfully deleted")

View File

@@ -15,6 +15,8 @@ from typing import AsyncGenerator
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from app.api.routes.auth import router as auth_router
from app.api.routes.users import router as users_router
from app.config import settings from app.config import settings
from app.database.connection import get_database from app.database.connection import get_database
from app.file_watcher import create_file_watcher, FileWatcher from app.file_watcher import create_file_watcher, FileWatcher
@@ -23,12 +25,6 @@ from app.services.init_service import InitializationService
from app.services.job_service import JobService from app.services.job_service import JobService
from app.services.user_service import UserService from app.services.user_service import UserService
# from api.routes.auth import router as auth_router
# from api.routes.users import router as users_router
# from api.routes.documents import router as documents_router
# from api.routes.jobs import router as jobs_router
# Configure logging # Configure logging
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -112,10 +108,9 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# Include routers # Include routers
# app.include_router(auth_router, prefix="/auth", tags=["Authentication"]) app.include_router(auth_router, prefix="/auth", tags=["Authentication"])
# app.include_router(users_router, prefix="/users", tags=["User Management"]) app.include_router(users_router, prefix="/users", tags=["User Management"])
# app.include_router(documents_router, prefix="/documents", tags=["Documents"]) # app.include_router(documents_router, prefix="/documents", tags=["Documents"])
# app.include_router(jobs_router, prefix="/jobs", tags=["Processing Jobs"]) # app.include_router(jobs_router, prefix="/jobs", tags=["Processing Jobs"])

View File

@@ -3,12 +3,45 @@ Authentication models and enums for user management.
Contains user roles enumeration and authentication-related Pydantic models. Contains user roles enumeration and authentication-related Pydantic models.
""" """
from datetime import datetime
from enum import Enum from enum import Enum
from pydantic import BaseModel, Field
from app.models.types import PyObjectId
class UserRole(str, Enum): class UserRole(str, Enum):
"""User roles enumeration with string values.""" """User roles enumeration with string values."""
USER = "user" USER = "user"
ADMIN = "admin" ADMIN = "admin"
class UserResponse(BaseModel):
"""Model for user data in API responses (excludes password_hash)."""
id: PyObjectId = Field(alias="_id")
username: str
email: str
role: UserRole
is_active: bool
created_at: datetime
updated_at: datetime
model_config = {
"populate_by_name": True,
"arbitrary_types_allowed": True,
}
class LoginResponse(BaseModel):
"""Response model for successful login."""
access_token: str
token_type: str = "bearer"
user: UserResponse
class MessageResponse(BaseModel):
"""Generic message response."""
message: str

View File

@@ -7,10 +7,10 @@ and API responses with proper validation and type safety.
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any from typing import Optional
from bson import ObjectId from bson import ObjectId
from pydantic import BaseModel, Field, field_validator, EmailStr from pydantic import BaseModel, Field, field_validator, EmailStr
from pydantic_core import core_schema
from app.models.auth import UserRole from app.models.auth import UserRole
from app.models.types import PyObjectId from app.models.types import PyObjectId
@@ -138,21 +138,3 @@ class UserInDB(BaseModel):
"arbitrary_types_allowed": True, "arbitrary_types_allowed": True,
"json_encoders": {ObjectId: str} "json_encoders": {ObjectId: str}
} }
class UserResponse(BaseModel):
"""Model for user data in API responses (excludes password_hash)."""
id: PyObjectId = Field(alias="_id")
username: str
email: str
role: UserRole
is_active: bool
created_at: datetime
updated_at: datetime
model_config = {
"populate_by_name": True,
"arbitrary_types_allowed": True,
"json_encoders": {ObjectId: str}
}

View File

@@ -4,7 +4,11 @@ Authentication service for password hashing and verification.
This module provides authentication-related functionality including This module provides authentication-related functionality including
password hashing, verification, and JWT token management. password hashing, verification, and JWT token management.
""" """
from datetime import datetime, timedelta
import jwt
from app.config import settings
from app.utils.security import hash_password, verify_password from app.utils.security import hash_password, verify_password
@@ -55,4 +59,26 @@ class AuthService:
>>> auth.verify_user_password("wrongpassword", hashed) >>> auth.verify_user_password("wrongpassword", hashed)
False False
""" """
return verify_password(password, hashed_password) return verify_password(password, hashed_password)
@staticmethod
def create_access_token(data=dict) -> str:
"""
Create a JWT access token.
Args:
data (dict): Payload data to include in the token.
Returns:
str: Encoded JWT token.
"""
# Copy data to avoid modifying the original dict
to_encode = data.copy()
# Add expiration time
expire = datetime.now() + timedelta(hours=settings.get_jwt_expire_hours())
to_encode.update({"exp": expire})
# Encode JWT
encoded_jwt = jwt.encode(to_encode, settings.get_jwt_secret_key(), algorithm=settings.get_jwt_algorithm())
return encoded_jwt

View File

@@ -5,8 +5,9 @@ email-validator==2.3.0
fastapi==0.116.1 fastapi==0.116.1
httptools==0.6.4 httptools==0.6.4
motor==3.7.1 motor==3.7.1
pymongo==4.15.0
pydantic==2.11.9 pydantic==2.11.9
PyJWT==2.10.1
pymongo==4.15.0
redis==6.4.0 redis==6.4.0
uvicorn==0.35.0 uvicorn==0.35.0
python-magic==0.4.27 python-magic==0.4.27

View File

@@ -28,8 +28,8 @@ celery_app.conf.update(
timezone="UTC", timezone="UTC",
enable_utc=True, enable_utc=True,
task_track_started=True, task_track_started=True,
task_time_limit=300, # 5 minutes task_time_limit=300, # 5 minutes
task_soft_time_limit=240, # 4 minutes task_soft_time_limit=240, # 4 minutes
) )
if __name__ == "__main__": if __name__ == "__main__":

0
tests/api/__init__.py Normal file
View File

View File

@@ -0,0 +1,139 @@
from unittest.mock import MagicMock
import pytest
from fastapi import status, HTTPException
from fastapi.testclient import TestClient
from app.main import app # Assuming you have FastAPI app defined in app/main.py
from app.models.auth import UserRole
from app.models.types import PyObjectId
from app.models.user import UserInDB
from app.services.auth_service import AuthService
from app.services.user_service import UserService
@pytest.fixture
def client():
return TestClient(app)
@pytest.fixture
def fake_user():
return UserInDB(
_id=PyObjectId(),
username="testuser",
email="test@example.com",
role=UserRole.USER,
is_active=True,
hashed_password="hashed-secret",
created_at="2023-01-01T00:00:00",
updated_at="2023-01-01T00:00:00",
)
def override_auth_service():
mock = MagicMock(spec=AuthService)
mock.verify_user_password.return_value = True
mock.create_access_token.return_value = "fake-jwt-token"
return mock
def override_user_service(fake_user):
mock = MagicMock(spec=UserService)
mock.get_user_by_username.return_value = fake_user
return mock
def override_get_current_user(fake_user):
def _override():
return fake_user
return _override
# ---------------------- TESTS FOR /auth/login ----------------------
class TestLogin:
def test_i_can_login_with_valid_credentials(self, client, fake_user, monkeypatch):
auth_service = override_auth_service()
user_service = override_user_service(fake_user)
monkeypatch.setattr("app.api.routes.auth.get_auth_service", lambda: auth_service)
monkeypatch.setattr("app.api.routes.auth.get_user_service", lambda: user_service)
response = client.post(
"/auth/login",
data={"username": "testuser", "password": "secret"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert "access_token" in data
assert data["user"]["username"] == "testuser"
def test_i_cannot_login_with_invalid_username(self, client, monkeypatch):
auth_service = override_auth_service()
user_service = MagicMock(spec=UserService)
user_service.get_user_by_username.return_value = None
monkeypatch.setattr("app.api.routes.auth.get_auth_service", lambda: auth_service)
monkeypatch.setattr("app.api.routes.auth.get_user_service", lambda: user_service)
response = client.post(
"/auth/login",
data={"username": "unknown", "password": "secret"},
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_i_cannot_login_with_inactive_user(self, client, fake_user, monkeypatch):
fake_user.is_active = False
auth_service = override_auth_service()
user_service = override_user_service(fake_user)
monkeypatch.setattr("app.api.routes.auth.get_auth_service", lambda: auth_service)
monkeypatch.setattr("app.api.routes.auth.get_user_service", lambda: user_service)
response = client.post(
"/auth/login",
data={"username": "testuser", "password": "secret"},
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_i_cannot_login_with_wrong_password(self, client, fake_user, monkeypatch):
auth_service = override_auth_service()
auth_service.verify_user_password.return_value = False
user_service = override_user_service(fake_user)
monkeypatch.setattr("app.api.routes.auth.get_auth_service", lambda: auth_service)
monkeypatch.setattr("app.api.routes.auth.get_user_service", lambda: user_service)
response = client.post(
"/auth/login",
data={"username": "testuser", "password": "wrong"},
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
# ---------------------- TESTS FOR /auth/me ----------------------
class TesteMe:
def test_i_can_get_current_user_profile(self, client, fake_user, monkeypatch):
monkeypatch.setattr("app.api.routes.auth.get_current_user", override_get_current_user(fake_user))
response = client.get("/auth/me")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["username"] == fake_user.username
assert data["email"] == fake_user.email
def test_i_cannot_get_profile_without_authentication(self, client, monkeypatch):
def raise_http_exception():
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
monkeypatch.setattr("app.api.routes.auth.get_current_user", raise_http_exception)
response = client.get("/auth/me")
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View File

@@ -10,8 +10,8 @@ from pydantic import ValidationError
from datetime import datetime from datetime import datetime
from bson import ObjectId from bson import ObjectId
from app.models.user import UserCreate, UserUpdate, UserInDB, UserResponse from app.models.user import UserCreate, UserUpdate, UserInDB
from app.models.auth import UserRole from app.models.auth import UserRole, UserResponse
class TestUserCreateModel: class TestUserCreateModel:
@@ -349,7 +349,7 @@ class TestUserResponseModel:
# Convert to response model (excluding password_hash) # Convert to response model (excluding password_hash)
user_response = UserResponse( user_response = UserResponse(
id=user_in_db.id, _id=user_in_db.id,
username=user_in_db.username, username=user_in_db.username,
email=user_in_db.email, email=user_in_db.email,
role=user_in_db.role, role=user_in_db.role,