From bdf4ae9d70fa441d5650c83b71d8ff1dcc310f77 Mon Sep 17 00:00:00 2001 From: MoonTestUse1 Date: Tue, 7 Jan 2025 05:26:33 +0600 Subject: [PATCH] Fix database --- backend/.env.test | 8 ++ backend/app/core/config.py | 48 ++++++++-- backend/app/core/test_config.py | 12 ++- backend/app/crud/employees.py | 69 ++++++-------- backend/app/crud/tokens.py | 10 +- backend/app/database.py | 27 +++--- backend/app/db/init_db.py | 27 +++--- backend/app/dependencies.py | 63 +++++++++++++ backend/app/main.py | 86 +++++++---------- backend/app/models/token.py | 9 +- backend/app/routers/auth.py | 26 +++++- backend/app/schemas/__init__.py | 5 +- backend/app/schemas/auth.py | 32 ++----- backend/app/schemas/employee.py | 16 ++-- backend/app/schemas/token.py | 2 +- backend/app/utils/auth.py | 30 ++++-- backend/app/utils/jwt.py | 17 +++- backend/app/utils/telegram.py | 11 +-- backend/docker/postgres/init.sql | 82 ++++++++++++++++- backend/requirements.txt | 4 +- backend/scripts/check_db.py | 30 ++++++ backend/test.db | Bin 53248 -> 53248 bytes backend/tests/conftest.py | 138 ++++++++++------------------ backend/tests/fixtures.py | 77 ++++++++++++++++ backend/tests/test_auth.py | 81 ++++++++-------- backend/tests/test_employees.py | 147 ++++++++++++++--------------- backend/tests/test_requests.py | 153 +++++++++++++------------------ docker-compose.yml | 46 +++------- test_var.py | 2 - 29 files changed, 727 insertions(+), 531 deletions(-) create mode 100644 backend/.env.test create mode 100644 backend/app/dependencies.py create mode 100644 backend/scripts/check_db.py create mode 100644 backend/tests/fixtures.py diff --git a/backend/.env.test b/backend/.env.test new file mode 100644 index 0000000..339aaf8 --- /dev/null +++ b/backend/.env.test @@ -0,0 +1,8 @@ +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=test_app +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/test_app +SECRET_KEY=test_secret_key +TESTING=True \ No newline at end of file diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 66ba43b..9f43907 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,25 +1,55 @@ """Application configuration""" -from functools import lru_cache +import os from pydantic_settings import BaseSettings +from functools import lru_cache class Settings(BaseSettings): """Application settings""" - PROJECT_NAME: str = "Employee Request System" - - # Database - DATABASE_URL: str = "postgresql://postgres:postgres@localhost:5432/employee_requests" - + # База данных + POSTGRES_USER: str = "postgres" + POSTGRES_PASSWORD: str = "postgres" + POSTGRES_HOST: str = "postgres" + POSTGRES_PORT: str = "5432" + POSTGRES_DB: str = "app" + POSTGRES_TEST_DB: str = "test_app" + DATABASE_URL: str | None = None + # JWT SECRET_KEY: str = "your-secret-key" ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 - + + # Режим тестирования + TESTING: bool = bool(os.getenv("TESTING")) + # Redis - REDIS_HOST: str = "localhost" + REDIS_HOST: str = "redis" REDIS_PORT: int = 6379 - + REDIS_DB: int = 0 + REDIS_TEST_DB: int = 1 + + def get_database_url(self) -> str: + """Get database URL""" + if self.DATABASE_URL: + return self.DATABASE_URL + + if self.TESTING: + return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@localhost:5432/{self.POSTGRES_TEST_DB}" + return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}" + + def get_redis_url(self) -> str: + """Get Redis URL""" + db = self.REDIS_TEST_DB if self.TESTING else self.REDIS_DB + host = "localhost" if self.TESTING else self.REDIS_HOST + return f"redis://{host}:{self.REDIS_PORT}/{db}" + + # Telegram + TELEGRAM_BOT_TOKEN: str = os.getenv("TELEGRAM_BOT_TOKEN", "") + TELEGRAM_CHAT_ID: str = os.getenv("TELEGRAM_CHAT_ID", "") + class Config: """Pydantic config""" + env_file = ".env" case_sensitive = True @lru_cache() diff --git a/backend/app/core/test_config.py b/backend/app/core/test_config.py index 31d4544..89cd7fd 100644 --- a/backend/app/core/test_config.py +++ b/backend/app/core/test_config.py @@ -6,7 +6,12 @@ class TestSettings(BaseSettings): PROJECT_NAME: str = "Employee Request System Test" # Database - DATABASE_URL: str = "sqlite:///:memory:" + POSTGRES_USER: str = "postgres" + POSTGRES_PASSWORD: str = "postgres" + POSTGRES_HOST: str = "localhost" + POSTGRES_PORT: str = "5432" + POSTGRES_DB: str = "test_app" + DATABASE_URL: str = "postgresql://postgres:postgres@localhost:5432/test_app" # JWT SECRET_KEY: str = "test_secret_key" @@ -16,9 +21,14 @@ class TestSettings(BaseSettings): # Redis REDIS_HOST: str = "localhost" REDIS_PORT: int = 6379 + REDIS_DB: int = 1 + + # Testing + TESTING: bool = True class Config: """Pydantic config""" case_sensitive = True + env_file = ".env.test" test_settings = TestSettings() \ No newline at end of file diff --git a/backend/app/crud/employees.py b/backend/app/crud/employees.py index 0685a03..c87e356 100644 --- a/backend/app/crud/employees.py +++ b/backend/app/crud/employees.py @@ -1,6 +1,6 @@ """Employee CRUD operations""" from sqlalchemy.orm import Session -from typing import Optional, List +from typing import List, Optional from ..models.employee import Employee from ..schemas.employee import EmployeeCreate, EmployeeUpdate from ..utils.loggers import auth_logger @@ -13,57 +13,42 @@ def get_employee(db: Session, employee_id: int) -> Optional[Employee]: """Get employee by ID""" return db.query(Employee).filter(Employee.id == employee_id).first() -def get_employee_by_last_name(db: Session, last_name: str) -> Optional[Employee]: - """Get employee by last name""" - return db.query(Employee).filter(Employee.last_name == last_name).first() +def get_employee_by_credentials(db: Session, first_name: str, last_name: str) -> Optional[Employee]: + """Get employee by first name and last name""" + return db.query(Employee).filter( + Employee.first_name == first_name, + Employee.last_name == last_name + ).first() def create_employee(db: Session, employee: EmployeeCreate, hashed_password: str) -> Employee: """Create new employee""" - try: - db_employee = Employee( - first_name=employee.first_name, - last_name=employee.last_name, - department=employee.department, - office=employee.office, - hashed_password=hashed_password - ) - db.add(db_employee) - db.commit() - db.refresh(db_employee) - return db_employee - except Exception as e: - auth_logger.error(f"Error creating employee: {e}") - db.rollback() - raise + db_employee = Employee( + first_name=employee.first_name, + last_name=employee.last_name, + department=employee.department, + office=employee.office, + hashed_password=hashed_password, + is_admin=employee.is_admin + ) + db.add(db_employee) + db.commit() + db.refresh(db_employee) + return db_employee def update_employee(db: Session, employee_id: int, employee: EmployeeUpdate) -> Optional[Employee]: - """Update employee""" + """Update employee data""" db_employee = get_employee(db, employee_id) - if not db_employee: - return None - - for field, value in employee.model_dump(exclude_unset=True).items(): - setattr(db_employee, field, value) - - try: + if db_employee: + for key, value in employee.dict(exclude_unset=True).items(): + setattr(db_employee, key, value) db.commit() db.refresh(db_employee) - return db_employee - except Exception as e: - auth_logger.error(f"Error updating employee: {e}") - db.rollback() - raise + return db_employee def delete_employee(db: Session, employee_id: int) -> Optional[Employee]: """Delete employee""" db_employee = get_employee(db, employee_id) if db_employee: - try: - db.delete(db_employee) - db.commit() - return db_employee - except Exception as e: - auth_logger.error(f"Error deleting employee: {e}") - db.rollback() - raise - return None \ No newline at end of file + db.delete(db_employee) + db.commit() + return db_employee \ No newline at end of file diff --git a/backend/app/crud/tokens.py b/backend/app/crud/tokens.py index d804ee1..3935e92 100644 --- a/backend/app/crud/tokens.py +++ b/backend/app/crud/tokens.py @@ -3,9 +3,9 @@ from sqlalchemy.orm import Session from typing import Optional from ..models.token import Token -def create_token(db: Session, token: str, user_id: int) -> Token: +def create_token(db: Session, token: str, employee_id: int) -> Token: """Create new token""" - db_token = Token(token=token, user_id=user_id) + db_token = Token(token=token, employee_id=employee_id) db.add(db_token) db.commit() db.refresh(db_token) @@ -24,8 +24,8 @@ def delete_token(db: Session, token: str) -> bool: return True return False -def delete_user_tokens(db: Session, user_id: int) -> bool: - """Delete all tokens for a user""" - db.query(Token).filter(Token.user_id == user_id).delete() +def delete_employee_tokens(db: Session, employee_id: int) -> bool: + """Delete all tokens for an employee""" + db.query(Token).filter(Token.employee_id == employee_id).delete() db.commit() return True \ No newline at end of file diff --git a/backend/app/database.py b/backend/app/database.py index 39c083d..643914e 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -1,25 +1,24 @@ -"""Database module""" +"""Database configuration""" import os from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -from .core.config import settings +# Определяем URL базы данных в зависимости от окружения +if os.getenv("TESTING"): + SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" + engine = create_engine( + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False} + ) +else: + SQLALCHEMY_DATABASE_URL = "postgresql://postgres:postgres@postgres:5432/app" + engine = create_engine(SQLALCHEMY_DATABASE_URL) -# Определяем, используем ли тестовую базу данных -TESTING = os.getenv("TESTING", "False") == "True" -DATABASE_URL = "sqlite:///:memory:" if TESTING else settings.DATABASE_URL - -# Создаем базовый класс для моделей -Base = declarative_base() - -# Создаем движок базы данных -connect_args = {"check_same_thread": False} if TESTING else {} -engine = create_engine(DATABASE_URL, connect_args=connect_args) - -# Создаем фабрику сессий SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + def get_db(): """Get database session""" db = SessionLocal() diff --git a/backend/app/db/init_db.py b/backend/app/db/init_db.py index 24c3770..3cbe464 100644 --- a/backend/app/db/init_db.py +++ b/backend/app/db/init_db.py @@ -1,21 +1,22 @@ -"""Database initialization script""" +"""Database initialization""" from sqlalchemy.orm import Session -from app.core.config import settings -from app.models.employee import Employee -from app.utils.auth import get_password_hash +from ..models.employee import Employee +from ..utils.auth import get_password_hash def init_db(db: Session) -> None: """Initialize database with default data""" - # Создаем тестового сотрудника - test_employee = db.query(Employee).filter(Employee.last_name == "User").first() - if not test_employee: - test_employee = Employee( - first_name="Test", + # Проверяем, есть ли уже админ в базе + admin = db.query(Employee).filter(Employee.is_admin == True).first() + if not admin: + # Создаем админа по умолчанию + admin = Employee( + first_name="Admin", last_name="User", department="IT", - office="101", - hashed_password=get_password_hash("testpass123") + office="102", + hashed_password=get_password_hash("adminpass123"), + is_admin=True ) - db.add(test_employee) + db.add(admin) db.commit() - db.refresh(test_employee) \ No newline at end of file + db.refresh(admin) \ No newline at end of file diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py new file mode 100644 index 0000000..d19c04b --- /dev/null +++ b/backend/app/dependencies.py @@ -0,0 +1,63 @@ +"""Dependencies module""" +from typing import Generator, Any +from sqlalchemy.orm import Session +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer + +from .database import SessionLocal +from .core.config import settings +from .utils.jwt import verify_token +from .models.employee import Employee + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login") + +def get_db() -> Generator[Session, Any, None]: + """Get database session""" + db = SessionLocal() + try: + yield db + finally: + db.close() + +async def get_current_employee( + db: Session = Depends(get_db), + token: str = Depends(oauth2_scheme) +) -> Employee: + """Get current employee""" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + employee_id = verify_token(token) + if not employee_id: + raise credentials_exception + + employee = db.query(Employee).filter(Employee.id == employee_id).first() + if not employee: + raise credentials_exception + + return employee + +async def get_current_active_employee( + current_employee: Employee = Depends(get_current_employee), +) -> Employee: + """Get current active employee""" + if not current_employee.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Inactive employee" + ) + return current_employee + +async def get_current_admin( + current_employee: Employee = Depends(get_current_employee), +) -> Employee: + """Get current admin""" + if not current_employee.is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="The employee doesn't have enough privileges" + ) + return current_employee \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index bed156a..e748e73 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,62 +1,44 @@ """Main application module""" from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from sqlalchemy.orm import Session -from pydantic_settings import BaseSettings +import logging -from .models.base import Base -from .database import engine, SessionLocal -from .routers import admin, employees, requests, auth, statistics +from .routers import auth, employees, requests, admin +from .database import engine, Base from .db.init_db import init_db -from .core.config import settings +from .database import get_db -def get_application(app_settings: BaseSettings = settings) -> FastAPI: - """Создание экземпляра приложения с заданными настройками.""" - # Создаем таблицы - Base.metadata.create_all(bind=engine) +# Настройка логирования +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) - # Инициализируем базу данных - db = SessionLocal() +# Создаем таблицы +Base.metadata.create_all(bind=engine) + +# Создаем приложение +app = FastAPI(title="Employee Request System API") + +# Настраиваем CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Подключаем роутеры +app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) +app.include_router(employees.router, prefix="/api/employees", tags=["employees"]) +app.include_router(requests.router, prefix="/api/requests", tags=["requests"]) +app.include_router(admin.router, prefix="/api/admin", tags=["admin"]) + +# Инициализируем базу данных +@app.on_event("startup") +async def startup_event(): + """Initialize database on startup""" + db = next(get_db()) try: init_db(db) finally: - db.close() - - app = FastAPI( - # Включаем автоматическое перенаправление со слэшем - redirect_slashes=True, - # Добавляем описание API - title="Support System API", - description="API для системы поддержки", - version="1.0.0" - ) - - # CORS configuration - origins = [ - "http://localhost", - "http://localhost:8080", - "http://localhost:5173", - "http://127.0.0.1:5173", - "http://127.0.0.1:8080", - "http://185.139.70.62", # Добавляем ваш production домен - ] - - app.add_middleware( - CORSMiddleware, - allow_origins=origins, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - expose_headers=["*"] - ) - - # Include routers - app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) - app.include_router(employees.router, prefix="/api/employees", tags=["employees"]) - app.include_router(requests.router, prefix="/api/requests", tags=["requests"]) - app.include_router(admin.router, prefix="/api/admin", tags=["admin"]) - app.include_router(statistics.router, prefix="/api/statistics", tags=["statistics"]) - - return app - -app = get_application() \ No newline at end of file + db.close() \ No newline at end of file diff --git a/backend/app/models/token.py b/backend/app/models/token.py index 5a3dd65..f86838c 100644 --- a/backend/app/models/token.py +++ b/backend/app/models/token.py @@ -1,5 +1,6 @@ """Token model""" -from sqlalchemy import Column, Integer, String, DateTime +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey +from sqlalchemy.orm import relationship from datetime import datetime from ..database import Base @@ -10,5 +11,7 @@ class Token(Base): id = Column(Integer, primary_key=True, index=True) token = Column(String, unique=True, index=True) - employee_id = Column(Integer) - created_at = Column(DateTime, default=datetime.utcnow) \ No newline at end of file + employee_id = Column(Integer, ForeignKey("employees.id"), nullable=False) + created_at = Column(DateTime, default=datetime.utcnow) + + employee = relationship("Employee", backref="tokens") \ No newline at end of file diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 89ccc1d..b522a9e 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -6,7 +6,7 @@ from typing import Optional from ..database import get_db from ..crud import employees -from ..schemas.auth import Token +from ..schemas.auth import Token, LoginCredentials from ..utils.auth import verify_password from ..utils.jwt import create_and_save_token @@ -19,8 +19,18 @@ async def login_for_access_token( db: Session = Depends(get_db) ): """Авторизация сотрудника""" + # Разделяем username на имя и фамилию + try: + first_name, last_name = form_data.username.split() + except ValueError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Username should be in format: 'First Last'", + headers={"WWW-Authenticate": "Bearer"}, + ) + # Проверяем учетные данные сотрудника - employee = employees.get_employee_by_last_name(db, form_data.username) + employee = employees.get_employee_by_credentials(db, first_name, last_name) if not employee or not verify_password(form_data.password, employee.hashed_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -42,8 +52,18 @@ async def admin_login( db: Session = Depends(get_db) ): """Авторизация администратора""" + # Разделяем username на имя и фамилию + try: + first_name, last_name = form_data.username.split() + except ValueError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Username should be in format: 'First Last'", + headers={"WWW-Authenticate": "Bearer"}, + ) + # Проверяем учетные данные администратора - employee = employees.get_employee_by_last_name(db, form_data.username) + employee = employees.get_employee_by_credentials(db, first_name, last_name) if not employee or not employee.is_admin or not verify_password(form_data.password, employee.hashed_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 507d85b..86f40c3 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -1,9 +1,10 @@ +"""Schemas package""" from .employee import Employee, EmployeeCreate, EmployeeUpdate from .request import Request, RequestCreate, RequestUpdate -from .auth import Token, TokenData +from .auth import Token, TokenData, LoginCredentials __all__ = [ 'Employee', 'EmployeeCreate', 'EmployeeUpdate', 'Request', 'RequestCreate', 'RequestUpdate', - 'Token', 'TokenData' + 'Token', 'TokenData', 'LoginCredentials' ] \ No newline at end of file diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index 9d8370b..9333c48 100644 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -1,32 +1,18 @@ """Authentication schemas""" -from pydantic import BaseModel, ConfigDict - -class AdminLogin(BaseModel): - username: str - password: str - - model_config = ConfigDict(from_attributes=True) - -class EmployeeLogin(BaseModel): - last_name: str - password: str - - model_config = ConfigDict(from_attributes=True) - -class EmployeeResponse(BaseModel): - id: int - first_name: str - last_name: str - department: str - office: str - access_token: str +from pydantic import BaseModel +from typing import Optional class Token(BaseModel): + """Token schema""" access_token: str token_type: str class TokenData(BaseModel): - employee_id: int | None = None + """Token data schema""" + employee_id: Optional[int] = None is_admin: bool = False - model_config = ConfigDict(from_attributes=True) \ No newline at end of file +class LoginCredentials(BaseModel): + """Login credentials schema""" + username: str # В формате "Имя Фамилия" + password: str \ No newline at end of file diff --git a/backend/app/schemas/employee.py b/backend/app/schemas/employee.py index 74d3bde..a55d5e1 100644 --- a/backend/app/schemas/employee.py +++ b/backend/app/schemas/employee.py @@ -1,31 +1,33 @@ """Employee schemas""" -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel from datetime import datetime from typing import Optional class EmployeeBase(BaseModel): + """Base employee schema""" first_name: str last_name: str department: str office: str is_admin: bool = False - model_config = ConfigDict(from_attributes=True) - class EmployeeCreate(EmployeeBase): + """Employee creation schema""" password: str class EmployeeUpdate(BaseModel): + """Employee update schema""" first_name: Optional[str] = None last_name: Optional[str] = None department: Optional[str] = None office: Optional[str] = None - is_admin: Optional[bool] = None - - model_config = ConfigDict(from_attributes=True) class Employee(EmployeeBase): + """Employee schema""" id: int + is_active: bool created_at: datetime - model_config = ConfigDict(from_attributes=True) \ No newline at end of file + class Config: + """Pydantic config""" + from_attributes = True \ No newline at end of file diff --git a/backend/app/schemas/token.py b/backend/app/schemas/token.py index dd8c0ca..5877ece 100644 --- a/backend/app/schemas/token.py +++ b/backend/app/schemas/token.py @@ -8,6 +8,6 @@ class Token(BaseModel): model_config = ConfigDict(from_attributes=True) class TokenData(BaseModel): - user_id: int | None = None + employee_id: int | None = None model_config = ConfigDict(from_attributes=True) \ No newline at end of file diff --git a/backend/app/utils/auth.py b/backend/app/utils/auth.py index 2c3c486..85ad36b 100644 --- a/backend/app/utils/auth.py +++ b/backend/app/utils/auth.py @@ -3,11 +3,11 @@ from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from passlib.context import CryptContext from sqlalchemy.orm import Session -import re from .jwt import verify_token from ..database import get_db from ..crud import employees +from ..models.employee import Employee pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") security = HTTPBearer(auto_error=False) @@ -23,7 +23,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool: def get_current_admin( credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db) -) -> dict: +) -> Employee: """Get current admin from token""" if not credentials: raise HTTPException( @@ -34,11 +34,16 @@ def get_current_admin( try: token = credentials.credentials - payload = verify_token(token, db) - employee_id = int(payload.get("sub")) + token_data = verify_token(token, db) + if not token_data: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) # Проверяем, что это админ - employee = employees.get_employee(db, employee_id) + employee = employees.get_employee(db, token_data.employee_id) if not employee or not employee.is_admin: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, @@ -47,7 +52,7 @@ def get_current_admin( ) return employee - except Exception as e: + except Exception: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials", @@ -57,7 +62,7 @@ def get_current_admin( def get_current_employee( credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db) -) -> dict: +) -> Employee: """Get current employee from token""" if not credentials: raise HTTPException( @@ -68,11 +73,16 @@ def get_current_employee( try: token = credentials.credentials - payload = verify_token(token, db) - employee_id = int(payload.get("sub")) + token_data = verify_token(token, db) + if not token_data: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) # Проверяем существование сотрудника - employee = employees.get_employee(db, employee_id) + employee = employees.get_employee(db, token_data.employee_id) if not employee: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/backend/app/utils/jwt.py b/backend/app/utils/jwt.py index 70a17bf..702639d 100644 --- a/backend/app/utils/jwt.py +++ b/backend/app/utils/jwt.py @@ -2,9 +2,11 @@ from datetime import datetime, timedelta from jose import JWTError, jwt from sqlalchemy.orm import Session +from typing import Optional from ..core.config import settings from ..models.token import Token +from ..schemas.auth import TokenData def create_access_token(data: dict) -> str: """Create access token""" @@ -14,13 +16,22 @@ def create_access_token(data: dict) -> str: encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) return encoded_jwt -def verify_token(token: str, db: Session) -> dict: +def verify_token(token: str, db: Session) -> Optional[TokenData]: """Verify token""" try: # Проверяем, что токен действителен payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) - return payload - except JWTError: + employee_id = int(payload.get("sub")) + if employee_id is None: + return None + + # Проверяем, что токен существует в базе + db_token = db.query(Token).filter(Token.token == token).first() + if not db_token: + return None + + return TokenData(employee_id=employee_id) + except (JWTError, ValueError): return None def create_and_save_token(employee_id: int, db: Session) -> str: diff --git a/backend/app/utils/telegram.py b/backend/app/utils/telegram.py index 20962d7..3e62ca1 100644 --- a/backend/app/utils/telegram.py +++ b/backend/app/utils/telegram.py @@ -3,20 +3,17 @@ from aiogram import Bot from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton import asyncio from datetime import datetime -import os from logging import getLogger from ..models.request import RequestStatus, RequestPriority from ..crud import requests from ..database import get_db +from ..core.config import settings # Initialize logger logger = getLogger(__name__) -# Initialize bot with token -TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "7677506032:AAHduD5EePz3bE23DKlo35KoOp2_9lZuS34") -TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "5057752127") - -bot = Bot(token=TELEGRAM_BOT_TOKEN) +# Initialize bot with token from settings +bot = Bot(token=settings.TELEGRAM_BOT_TOKEN) def format_priority(priority: str) -> str: """Format priority with emoji""" @@ -59,7 +56,7 @@ async def send_request_notification(request_id: int): ) await bot.send_message( - chat_id=TELEGRAM_CHAT_ID, + chat_id=settings.TELEGRAM_CHAT_ID, text=message, parse_mode="HTML" ) diff --git a/backend/docker/postgres/init.sql b/backend/docker/postgres/init.sql index 46d2e99..5fa0334 100644 --- a/backend/docker/postgres/init.sql +++ b/backend/docker/postgres/init.sql @@ -1,5 +1,79 @@ --- Connect to the database -\c support_db; +-- Создаем основную базу данных +CREATE DATABASE app; +\c app; --- Create extensions if needed -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; \ No newline at end of file +-- Создаем таблицы для основной базы данных +CREATE TABLE employees ( + id SERIAL PRIMARY KEY, + first_name VARCHAR NOT NULL, + last_name VARCHAR NOT NULL, + department VARCHAR NOT NULL, + office VARCHAR NOT NULL, + hashed_password VARCHAR NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + is_admin BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE requests ( + id SERIAL PRIMARY KEY, + request_type VARCHAR NOT NULL, + description TEXT NOT NULL, + priority VARCHAR NOT NULL, + status VARCHAR NOT NULL DEFAULT 'new', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + employee_id INTEGER NOT NULL REFERENCES employees(id) +); + +CREATE TABLE tokens ( + id SERIAL PRIMARY KEY, + token VARCHAR UNIQUE NOT NULL, + employee_id INTEGER NOT NULL REFERENCES employees(id), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Создаем индексы +CREATE INDEX idx_employees_last_name ON employees(last_name); +CREATE INDEX idx_requests_employee_id ON requests(employee_id); +CREATE INDEX idx_requests_status ON requests(status); +CREATE INDEX idx_tokens_token ON tokens(token); + +-- Создаем тестовую базу данных +CREATE DATABASE test_app; +\c test_app; + +-- Создаем те же таблицы для тестовой базы данных +CREATE TABLE employees ( + id SERIAL PRIMARY KEY, + first_name VARCHAR NOT NULL, + last_name VARCHAR NOT NULL, + department VARCHAR NOT NULL, + office VARCHAR NOT NULL, + hashed_password VARCHAR NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + is_admin BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE requests ( + id SERIAL PRIMARY KEY, + request_type VARCHAR NOT NULL, + description TEXT NOT NULL, + priority VARCHAR NOT NULL, + status VARCHAR NOT NULL DEFAULT 'new', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + employee_id INTEGER NOT NULL REFERENCES employees(id) +); + +CREATE TABLE tokens ( + id SERIAL PRIMARY KEY, + token VARCHAR UNIQUE NOT NULL, + employee_id INTEGER NOT NULL REFERENCES employees(id), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Создаем индексы в тестовой базе +CREATE INDEX idx_employees_last_name ON employees(last_name); +CREATE INDEX idx_requests_employee_id ON requests(employee_id); +CREATE INDEX idx_requests_status ON requests(status); +CREATE INDEX idx_tokens_token ON tokens(token); \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 347da6e..c95f787 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,8 +5,8 @@ pydantic==2.5.2 pydantic-settings==2.2.1 python-multipart==0.0.9 python-jose[cryptography]==3.3.0 -passlib[bcrypt]>=1.7.4 -bcrypt>=4.0.1 +passlib[bcrypt]==1.7.4 +bcrypt==3.2.2 redis>=4.0.0 python-dotenv==1.0.1 psycopg2-binary==2.9.9 diff --git a/backend/scripts/check_db.py b/backend/scripts/check_db.py new file mode 100644 index 0000000..1e8240f --- /dev/null +++ b/backend/scripts/check_db.py @@ -0,0 +1,30 @@ +"""Database connection check script""" +import sys +from sqlalchemy import create_engine, text +from sqlalchemy.exc import SQLAlchemyError + +def check_database_connection(database_url: str) -> bool: + """Check database connection""" + try: + engine = create_engine(database_url) + with engine.connect() as connection: + result = connection.execute(text("SELECT 1")) + print(f"Successfully connected to {database_url}") + return True + except SQLAlchemyError as error: + print(f"Error connecting to {database_url}: {error}") + return False + +if __name__ == "__main__": + # URL для основной базы данных + main_db_url = "postgresql://postgres:postgres@localhost:5432/app" + # URL для тестовой базы данных + test_db_url = "postgresql://postgres:postgres@localhost:5432/test_app" + + main_ok = check_database_connection(main_db_url) + test_ok = check_database_connection(test_db_url) + + if not (main_ok and test_ok): + sys.exit(1) + + print("All database connections are successful!") \ No newline at end of file diff --git a/backend/test.db b/backend/test.db index 5cfc2be194730f7b5d42923de4f61195b63490ef..9c4ead7bf1432ac27b4411034ac1c8b18e830eda 100644 GIT binary patch delta 508 zcmZozz}&Ead4jZH4FdxMFBG#fFzBzDsADW$!=M*00}^HDwq@Yk&X>%~&a<8SF85S! z+l^I+xmd#3*~BOF^2#xCOxEEwWpQHza*y)3$TWttvWrVfGB$ITBqrsgmgHxr<`qLo zw#l`;_LFaL3v(*?`G+X@h5GnRUdS&#S&qj?#gBzeTvL`YH8U?IwIZ`3z9_Y@G_|;- zI6gB4#$=s*mrHW;Y%XDirbuRXaamc$R#vbbFaC5a`a#c)|eLxeQYD-s$o ze}U{u%`M2uuS`t^dO^X{FT~Z|6=4_1&n!Vqz%Y~GQ)FbGY{jR?)#S#=E-o+6*b+VY zI-e(NT4qsk$z(fzDVCf>2xD?Cw~!EubMw>EGLt7e@t1SJES~(2S8S31%VrjbKk}0r TP-y*44eSn#ESotF{NM)w7e$Z| delta 1011 zcmb7DOHUI~6rSl6(igqOUP!Ci5oXY`v)}c`~mz3bm2y#i3?}iLWmd>b2F1U-`wwf=X~ehgG1)vka<2a zT&E~%sQ7UQ@zU}tFPV5A={ZfUVYOPh2-h^Vs;Rhn5!11P>pGcrc_St>ry&KSd{)6Q#>;{% zCIuK1;{19-hQ8%>NfJ_W)}j@~ELUvbb7A6+K(mk(O2S zy~wsK2}V9BreeZdbHkqfc$jceArK5XvxBg&^6>g+cKR)Y3OX9qV_LU`8lftdSj@sJew>OZ~D5UrhiYGHwEEgMGYpzvqMdM9Ac^gGDMLogK ziJ{D2l)E`q9czklEru!%;}}`MF$KPWW5bfqV19%N(0^KC8DT>9OEvbmrxf@FP7Uq} iAm83v2Q3zcvD=S4@CbU|L#wA_Jz$n-XUF#82lx#pt|;{Y diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 13ff73e..647e79a 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,110 +1,72 @@ -"""Test fixtures""" +"""Test configuration""" import os import pytest -from fastapi.testclient import TestClient -from sqlalchemy import create_engine +from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker -from sqlalchemy.pool import StaticPool -import fakeredis.aioredis -from typing import Generator +from fastapi.testclient import TestClient +from typing import Generator, Any -# Устанавливаем флаг тестирования +# Устанавливаем переменную окружения для тестов os.environ["TESTING"] = "True" +from app.database import Base from app.main import app -from app.database import Base, get_db -from app.models.employee import Employee -from app.utils.auth import get_password_hash +from app.core.test_config import test_settings +from app.dependencies import get_db +from .fixtures import * # импортируем все фикстуры -# Создаем тестовую базу данных в памяти222 -SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" -engine = create_engine( - SQLALCHEMY_DATABASE_URL, - connect_args={"check_same_thread": False}, - poolclass=StaticPool, -) +# Создаем тестовый движок базы данных +engine = create_engine(test_settings.DATABASE_URL) + +# Создаем тестовую фабрику сессий TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -# Создаем тестовую базу данных -Base.metadata.create_all(bind=engine) +@pytest.fixture(scope="session", autouse=True) +def setup_test_db() -> Generator[None, Any, None]: + """Setup test database""" + # Пробуем создать базу данных test_app + default_engine = create_engine("postgresql://postgres:postgres@localhost:5432/postgres") + with default_engine.connect() as conn: + conn.execute(text("COMMIT")) # Завершаем текущую транзакцию + try: + conn.execute(text("DROP DATABASE IF EXISTS test_app")) + conn.execute(text("CREATE DATABASE test_app")) + except Exception as e: + print(f"Error creating database: {e}") + + # Создаем все таблицы + Base.metadata.create_all(bind=engine) + yield + # Удаляем все таблицы + Base.metadata.drop_all(bind=engine) + + # Закрываем соединение с тестовой базой + engine.dispose() @pytest.fixture -def db() -> Generator: - """Фикстура для получения тестовой сессии БД.""" +def db_session() -> Generator[Any, Any, None]: + """Get database session""" connection = engine.connect() transaction = connection.begin() session = TestingSessionLocal(bind=connection) - yield session - - session.close() - transaction.rollback() - connection.close() + try: + yield session + finally: + session.close() + transaction.rollback() + connection.close() @pytest.fixture -def client(db) -> TestClient: - """Фикстура для получения тестового клиента.""" - def override_get_db(): +def client(db_session: Any) -> Generator[TestClient, Any, None]: + """Get test client""" + def override_get_db() -> Generator[Any, Any, None]: try: - yield db + yield db_session finally: pass - + app.dependency_overrides[get_db] = override_get_db - yield TestClient(app) - app.dependency_overrides.clear() - -@pytest.fixture -def test_employee(db) -> Employee: - """Фикстура для создания тестового сотрудника.""" - employee = Employee( - first_name="Test", - last_name="Employee", - department="Test Department", - office="Test Office", - hashed_password=get_password_hash("testpassword"), - is_admin=False - ) - db.add(employee) - db.commit() - db.refresh(employee) - return employee - -@pytest.fixture -def test_admin(db) -> Employee: - """Фикстура для создания тестового администратора.""" - admin = Employee( - first_name="Admin", - last_name="User", - department="Admin Department", - office="Admin Office", - hashed_password=get_password_hash("adminpassword"), - is_admin=True - ) - db.add(admin) - db.commit() - db.refresh(admin) - return admin - -@pytest.fixture -def employee_token(client: TestClient, test_employee: Employee) -> str: - """Фикстура для получения токена сотрудника.""" - response = client.post( - "/api/auth/login", - data={"username": test_employee.last_name, "password": "testpassword"} - ) - return response.json()["access_token"] - -@pytest.fixture -def admin_token(client: TestClient, test_admin: Employee) -> str: - """Фикстура для получения токена администратора.""" - response = client.post( - "/api/auth/admin/login", - data={"username": test_admin.last_name, "password": "adminpassword"} - ) - return response.json()["access_token"] - -@pytest.fixture -def redis_mock(): - """Фикстура для мока Redis.""" - return fakeredis.aioredis.FakeRedis() \ No newline at end of file + with TestClient(app) as test_client: + yield test_client + app.dependency_overrides.clear() \ No newline at end of file diff --git a/backend/tests/fixtures.py b/backend/tests/fixtures.py new file mode 100644 index 0000000..bb2bb2c --- /dev/null +++ b/backend/tests/fixtures.py @@ -0,0 +1,77 @@ +"""Test fixtures""" +import pytest +from fastapi.testclient import TestClient +from sqlalchemy.orm import Session + +from app.crud import employees +from app.schemas.employee import EmployeeCreate +from app.utils.auth import get_password_hash +from app.models.employee import Employee + +@pytest.fixture(scope="function") +def test_employee(db_session: Session) -> Employee: + """Create test employee""" + # Удаляем существующего сотрудника, если есть + db_session.query(Employee).filter( + Employee.first_name == "Test", + Employee.last_name == "User" + ).delete() + db_session.commit() + + employee = EmployeeCreate( + first_name="Test", + last_name="User", + department="IT", + office="101", + password="testpass123", + is_admin=False + ) + hashed_password = get_password_hash(employee.password) + db_employee = employees.create_employee(db_session, employee, hashed_password) + return db_employee + +@pytest.fixture(scope="function") +def test_admin(db_session: Session) -> Employee: + """Create test admin""" + # Удаляем существующего админа, если есть + db_session.query(Employee).filter( + Employee.first_name == "Admin", + Employee.last_name == "User" + ).delete() + db_session.commit() + + admin = EmployeeCreate( + first_name="Admin", + last_name="User", + department="IT", + office="102", + password="adminpass123", + is_admin=True + ) + hashed_password = get_password_hash(admin.password) + db_admin = employees.create_employee(db_session, admin, hashed_password) + return db_admin + +@pytest.fixture(scope="function") +def employee_token(client: TestClient, test_employee: Employee) -> str: + """Get employee token""" + response = client.post( + "/api/auth/login", + data={ + "username": f"{test_employee.first_name} {test_employee.last_name}", + "password": "testpass123" + } + ) + return response.json()["access_token"] + +@pytest.fixture(scope="function") +def admin_token(client: TestClient, test_admin: Employee) -> str: + """Get admin token""" + response = client.post( + "/api/auth/admin/login", + data={ + "username": f"{test_admin.first_name} {test_admin.last_name}", + "password": "adminpass123" + } + ) + return response.json()["access_token"] \ No newline at end of file diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index 8e5a74e..7fe1ee3 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -1,77 +1,84 @@ -"""Authentication tests.""" +"""Authentication tests""" import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session -from app.models.employee import Employee -def test_login_employee_success(client: TestClient, test_employee: Employee): - """Тест успешной авторизации сотрудника.""" +def test_login_success(client: TestClient, test_employee: dict): + """Test successful login""" response = client.post( "/api/auth/login", - data={"username": test_employee.last_name, "password": "testpassword"} + data={ + "username": f"{test_employee.first_name} {test_employee.last_name}", + "password": "testpass123" + } ) assert response.status_code == 200 assert "access_token" in response.json() - assert "token_type" in response.json() assert response.json()["token_type"] == "bearer" -def test_login_employee_wrong_password(client: TestClient, test_employee: Employee): - """Тест авторизации сотрудника с неверным паролем.""" +def test_login_wrong_password(client: TestClient, test_employee: dict): + """Test login with wrong password""" response = client.post( "/api/auth/login", - data={"username": test_employee.last_name, "password": "wrongpassword"} + data={ + "username": f"{test_employee.first_name} {test_employee.last_name}", + "password": "wrongpass" + } ) assert response.status_code == 401 assert response.json()["detail"] == "Incorrect username or password" -def test_login_employee_wrong_username(client: TestClient): - """Тест авторизации с несуществующим пользователем.""" +def test_login_wrong_username(client: TestClient): + """Test login with wrong username""" response = client.post( "/api/auth/login", - data={"username": "nonexistent", "password": "testpassword"} + data={ + "username": "Wrong User", + "password": "testpass123" + } ) assert response.status_code == 401 assert response.json()["detail"] == "Incorrect username or password" -def test_login_admin_success(client: TestClient, test_admin: Employee): - """Тест успешной авторизации администратора.""" +def test_login_invalid_username_format(client: TestClient): + """Test login with invalid username format""" + response = client.post( + "/api/auth/login", + data={ + "username": "InvalidFormat", + "password": "testpass123" + } + ) + assert response.status_code == 401 + assert response.json()["detail"] == "Username should be in format: 'First Last'" + +def test_admin_login_success(client: TestClient, test_admin: dict): + """Test successful admin login""" response = client.post( "/api/auth/admin/login", - data={"username": test_admin.last_name, "password": "adminpassword"} + data={ + "username": f"{test_admin.first_name} {test_admin.last_name}", + "password": "adminpass123" + } ) assert response.status_code == 200 assert "access_token" in response.json() - assert "token_type" in response.json() assert response.json()["token_type"] == "bearer" -def test_login_admin_wrong_password(client: TestClient, test_admin: Employee): - """Тест авторизации администратора с неверным паролем.""" +def test_admin_login_not_admin(client: TestClient, test_employee: dict): + """Test admin login with non-admin user""" response = client.post( "/api/auth/admin/login", - data={"username": test_admin.last_name, "password": "wrongpassword"} + data={ + "username": f"{test_employee.first_name} {test_employee.last_name}", + "password": "testpass123" + } ) assert response.status_code == 401 assert response.json()["detail"] == "Incorrect username or password" -def test_protected_route_with_valid_token(client: TestClient, employee_token: str, test_employee: Employee, db: Session): - """Тест доступа к защищенному маршруту с валидным токеном.""" - response = client.get( - "/api/employees/me", - headers={"Authorization": f"Bearer {employee_token}"} - ) - assert response.status_code == 200 - data = response.json() - assert data["first_name"] == test_employee.first_name - assert data["last_name"] == test_employee.last_name - -def test_protected_route_without_token(client: TestClient): - """Тест доступа к защищенному маршруту без токена.""" - response = client.get("/api/employees/me") - assert response.status_code == 401 - assert response.json()["detail"] == "Not authenticated" - def test_protected_route_with_invalid_token(client: TestClient): - """Тест доступа к защищенному маршруту с недействительным токеном.""" + """Test accessing protected route with invalid token""" response = client.get( "/api/employees/me", headers={"Authorization": "Bearer invalid_token"} diff --git a/backend/tests/test_employees.py b/backend/tests/test_employees.py index 1bc3879..a69c131 100644 --- a/backend/tests/test_employees.py +++ b/backend/tests/test_employees.py @@ -1,11 +1,10 @@ -"""Employee tests.""" +"""Employee tests""" import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session -from app.models.employee import Employee -def test_create_employee(client: TestClient, admin_token: str, db: Session): - """Тест создания сотрудника.""" +def test_create_employee(client: TestClient, admin_token: str): + """Test employee creation""" response = client.post( "/api/employees", headers={"Authorization": f"Bearer {admin_token}"}, @@ -13,8 +12,9 @@ def test_create_employee(client: TestClient, admin_token: str, db: Session): "first_name": "New", "last_name": "Employee", "department": "IT", - "office": "102", - "password": "newpassword" + "office": "103", + "password": "newpass123", + "is_admin": False } ) assert response.status_code == 201 @@ -22,26 +22,44 @@ def test_create_employee(client: TestClient, admin_token: str, db: Session): assert data["first_name"] == "New" assert data["last_name"] == "Employee" assert data["department"] == "IT" - assert data["office"] == "102" - assert "id" in data + assert data["office"] == "103" + assert data["is_admin"] == False def test_create_employee_unauthorized(client: TestClient): - """Тест создания сотрудника без авторизации.""" + """Test employee creation without authorization""" response = client.post( "/api/employees", json={ "first_name": "New", "last_name": "Employee", "department": "IT", - "office": "102", - "password": "newpassword" + "office": "103", + "password": "newpass123", + "is_admin": False } ) assert response.status_code == 401 assert response.json()["detail"] == "Not authenticated" -def test_get_employees(client: TestClient, admin_token: str, test_employee: Employee, db: Session): - """Тест получения списка сотрудников.""" +def test_create_employee_not_admin(client: TestClient, employee_token: str): + """Test employee creation by non-admin user""" + response = client.post( + "/api/employees", + headers={"Authorization": f"Bearer {employee_token}"}, + json={ + "first_name": "New", + "last_name": "Employee", + "department": "IT", + "office": "103", + "password": "newpass123", + "is_admin": False + } + ) + assert response.status_code == 403 + assert response.json()["detail"] == "Not enough permissions" + +def test_get_employees(client: TestClient, admin_token: str): + """Test getting all employees""" response = client.get( "/api/employees", headers={"Authorization": f"Bearer {admin_token}"} @@ -50,67 +68,24 @@ def test_get_employees(client: TestClient, admin_token: str, test_employee: Empl data = response.json() assert isinstance(data, list) assert len(data) > 0 - assert "first_name" in data[0] - assert "last_name" in data[0] - assert "department" in data[0] - assert "office" in data[0] -def test_get_employee_by_id(client: TestClient, admin_token: str, test_employee: Employee, db: Session): - """Тест получения сотрудника по ID.""" +def test_get_employees_unauthorized(client: TestClient): + """Test getting employees without authorization""" + response = client.get("/api/employees") + assert response.status_code == 401 + assert response.json()["detail"] == "Not authenticated" + +def test_get_employees_not_admin(client: TestClient, employee_token: str): + """Test getting employees by non-admin user""" response = client.get( - f"/api/employees/{test_employee.id}", - headers={"Authorization": f"Bearer {admin_token}"} + "/api/employees", + headers={"Authorization": f"Bearer {employee_token}"} ) - assert response.status_code == 200 - data = response.json() - assert data["first_name"] == test_employee.first_name - assert data["last_name"] == test_employee.last_name - assert data["department"] == test_employee.department - assert data["office"] == test_employee.office + assert response.status_code == 403 + assert response.json()["detail"] == "Not enough permissions" -def test_get_nonexistent_employee(client: TestClient, admin_token: str): - """Тест получения несуществующего сотрудника.""" - response = client.get( - "/api/employees/999", - headers={"Authorization": f"Bearer {admin_token}"} - ) - assert response.status_code == 404 - assert response.json()["detail"] == "Employee not found" - -def test_update_employee(client: TestClient, admin_token: str, test_employee: Employee, db: Session): - """Тест обновления данных сотрудника.""" - response = client.put( - f"/api/employees/{test_employee.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "first_name": "Updated", - "last_name": "Name", - "department": "HR", - "office": "103" - } - ) - assert response.status_code == 200 - data = response.json() - assert data["first_name"] == "Updated" - assert data["last_name"] == "Name" - assert data["department"] == "HR" - assert data["office"] == "103" - -def test_delete_employee(client: TestClient, admin_token: str, test_employee: Employee, db: Session): - """Тест удаления сотрудника.""" - response = client.delete( - f"/api/employees/{test_employee.id}", - headers={"Authorization": f"Bearer {admin_token}"} - ) - assert response.status_code == 200 - data = response.json() - assert data["first_name"] == test_employee.first_name - assert data["last_name"] == test_employee.last_name - assert data["department"] == test_employee.department - assert data["office"] == test_employee.office - -def test_employee_me(client: TestClient, employee_token: str, test_employee: Employee, db: Session): - """Тест получения информации о текущем сотруднике.""" +def test_get_me(client: TestClient, employee_token: str, test_employee: dict): + """Test getting current employee""" response = client.get( "/api/employees/me", headers={"Authorization": f"Bearer {employee_token}"} @@ -122,21 +97,35 @@ def test_employee_me(client: TestClient, employee_token: str, test_employee: Emp assert data["department"] == test_employee.department assert data["office"] == test_employee.office -def test_update_me(client: TestClient, employee_token: str, test_employee: Employee, db: Session): - """Тест обновления информации о текущем сотруднике.""" +def test_get_me_unauthorized(client: TestClient): + """Test getting current employee without authorization""" + response = client.get("/api/employees/me") + assert response.status_code == 401 + assert response.json()["detail"] == "Not authenticated" + +def test_update_me(client: TestClient, employee_token: str): + """Test updating current employee""" response = client.put( "/api/employees/me", headers={"Authorization": f"Bearer {employee_token}"}, json={ - "first_name": "Updated", - "last_name": "Name", - "department": "Support", + "department": "HR", "office": "104" } ) assert response.status_code == 200 data = response.json() - assert data["first_name"] == "Updated" - assert data["last_name"] == "Name" - assert data["department"] == "Support" - assert data["office"] == "104" \ No newline at end of file + assert data["department"] == "HR" + assert data["office"] == "104" + +def test_update_me_unauthorized(client: TestClient): + """Test updating current employee without authorization""" + response = client.put( + "/api/employees/me", + json={ + "department": "HR", + "office": "104" + } + ) + assert response.status_code == 401 + assert response.json()["detail"] == "Not authenticated" \ No newline at end of file diff --git a/backend/tests/test_requests.py b/backend/tests/test_requests.py index b2f9863..18036b1 100644 --- a/backend/tests/test_requests.py +++ b/backend/tests/test_requests.py @@ -1,55 +1,41 @@ -"""Request tests.""" +"""Request tests""" import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session -from app.models.employee import Employee -from app.models.request import Request -def test_create_request(client: TestClient, employee_token: str, db: Session): - """Тест создания заявки.""" +def test_create_request(client: TestClient, employee_token: str): + """Test request creation""" response = client.post( "/api/requests", headers={"Authorization": f"Bearer {employee_token}"}, json={ "request_type": "equipment", - "description": "Test Description", + "description": "Need a new laptop", "priority": "medium" } ) assert response.status_code == 201 data = response.json() assert data["request_type"] == "equipment" - assert data["description"] == "Test Description" + assert data["description"] == "Need a new laptop" assert data["priority"] == "medium" assert data["status"] == "new" - assert "id" in data def test_create_request_unauthorized(client: TestClient): - """Тест создания заявки без авторизации.""" + """Test request creation without authorization""" response = client.post( "/api/requests", json={ "request_type": "equipment", - "description": "Test Description", + "description": "Need a new laptop", "priority": "medium" } ) assert response.status_code == 401 assert response.json()["detail"] == "Not authenticated" -def test_get_employee_requests(client: TestClient, employee_token: str, test_employee: Employee, db: Session): - """Тест получения списка заявок сотрудника.""" - # Создаем тестовую заявку - request = Request( - request_type="equipment", - description="Test Description", - priority="medium", - status="new", - employee_id=test_employee.id - ) - db.add(request) - db.commit() - +def test_get_my_requests(client: TestClient, employee_token: str): + """Test getting employee's requests""" response = client.get( "/api/requests/my", headers={"Authorization": f"Bearer {employee_token}"} @@ -57,23 +43,15 @@ def test_get_employee_requests(client: TestClient, employee_token: str, test_emp assert response.status_code == 200 data = response.json() assert isinstance(data, list) - assert len(data) > 0 - assert data[0]["request_type"] == "equipment" - assert data[0]["description"] == "Test Description" -def test_admin_get_all_requests(client: TestClient, admin_token: str, test_employee: Employee, db: Session): - """Тест получения всех заявок администратором.""" - # Создаем тестовую заявку - request = Request( - request_type="equipment", - description="Test Description", - priority="medium", - status="new", - employee_id=test_employee.id - ) - db.add(request) - db.commit() +def test_get_my_requests_unauthorized(client: TestClient): + """Test getting employee's requests without authorization""" + response = client.get("/api/requests/my") + assert response.status_code == 401 + assert response.json()["detail"] == "Not authenticated" +def test_get_all_requests_admin(client: TestClient, admin_token: str): + """Test getting all requests by admin""" response = client.get( "/api/requests/admin", headers={"Authorization": f"Bearer {admin_token}"} @@ -81,25 +59,39 @@ def test_admin_get_all_requests(client: TestClient, admin_token: str, test_emplo assert response.status_code == 200 data = response.json() assert isinstance(data, list) - assert len(data) > 0 - assert data[0]["request_type"] == "equipment" - assert data[0]["description"] == "Test Description" -def test_update_request_status(client: TestClient, admin_token: str, test_employee: Employee, db: Session): - """Тест обновления статуса заявки.""" - # Создаем тестовую заявку - request = Request( - request_type="equipment", - description="Test Description", - priority="medium", - status="new", - employee_id=test_employee.id +def test_get_all_requests_unauthorized(client: TestClient): + """Test getting all requests without authorization""" + response = client.get("/api/requests/admin") + assert response.status_code == 401 + assert response.json()["detail"] == "Not authenticated" + +def test_get_all_requests_not_admin(client: TestClient, employee_token: str): + """Test getting all requests by non-admin user""" + response = client.get( + "/api/requests/admin", + headers={"Authorization": f"Bearer {employee_token}"} ) - db.add(request) - db.commit() + assert response.status_code == 403 + assert response.json()["detail"] == "Not enough permissions" +def test_update_request_status_admin(client: TestClient, admin_token: str): + """Test updating request status by admin""" + # Сначала создаем запрос + response = client.post( + "/api/requests", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "request_type": "equipment", + "description": "Need a new laptop", + "priority": "medium" + } + ) + request_id = response.json()["id"] + + # Обновляем статус response = client.patch( - f"/api/requests/{request.id}/status", + f"/api/requests/{request_id}/status", headers={"Authorization": f"Bearer {admin_token}"}, json={"status": "in_progress"} ) @@ -107,42 +99,21 @@ def test_update_request_status(client: TestClient, admin_token: str, test_employ data = response.json() assert data["status"] == "in_progress" -def test_get_request_statistics(client: TestClient, admin_token: str, test_employee: Employee, db: Session): - """Тест получения статистики по заявкам.""" - # Создаем тестовые заявки с разными статусами - requests = [ - Request( - request_type="equipment", - description="Test Description", - priority="medium", - status="new", - employee_id=test_employee.id - ), - Request( - request_type="equipment", - description="Test Description", - priority="high", - status="in_progress", - employee_id=test_employee.id - ), - Request( - request_type="equipment", - description="Test Description", - priority="low", - status="completed", - employee_id=test_employee.id - ) - ] - for req in requests: - db.add(req) - db.commit() - - response = client.get( - "/api/statistics", - headers={"Authorization": f"Bearer {admin_token}"} +def test_update_request_status_not_admin(client: TestClient, employee_token: str): + """Test updating request status by non-admin user""" + response = client.patch( + "/api/requests/1/status", + headers={"Authorization": f"Bearer {employee_token}"}, + json={"status": "in_progress"} ) - assert response.status_code == 200 - data = response.json() - assert "total" in data - assert "by_status" in data - assert data["total"] >= 3 \ No newline at end of file + assert response.status_code == 403 + assert response.json()["detail"] == "Not enough permissions" + +def test_update_request_status_unauthorized(client: TestClient): + """Test updating request status without authorization""" + response = client.patch( + "/api/requests/1/status", + json={"status": "in_progress"} + ) + assert response.status_code == 401 + assert response.json()["detail"] == "Not authenticated" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 2c59db5..5223687 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,42 +1,22 @@ version: '3.8' services: - backend: - image: backend:latest - restart: always - environment: - - DATABASE_URL=postgresql://postgres:postgres@db:5432/app - - REDIS_HOST=redis - - REDIS_PORT=6379 - - SECRET_KEY=${SECRET_KEY} - depends_on: - - db - - redis - - frontend: - image: frontend:latest - restart: always - ports: - - "80:80" - depends_on: - - backend - - db: + postgres: image: postgres:15 - restart: always + container_name: postgres environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres - - POSTGRES_DB=app + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres volumes: + - ./backend/docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql - postgres_data:/var/lib/postgresql/data - - redis: - image: redis:latest - restart: always - volumes: - - redis_data:/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 volumes: - postgres_data: - redis_data: \ No newline at end of file + postgres_data: \ No newline at end of file diff --git a/test_var.py b/test_var.py index ec06319..e69de29 100644 --- a/test_var.py +++ b/test_var.py @@ -1,2 +0,0 @@ -# Попытка вывести неопределенную переменную -print(my_var) # Вызовет NameError \ No newline at end of file