mirror of
https://gitlab.com/MoonTestUse1/AdministrationItDepartmens.git
synced 2025-08-14 00:25:46 +02:00
Fix database
This commit is contained in:
@@ -1,18 +1,8 @@
|
||||
image: python:3.11
|
||||
|
||||
services:
|
||||
- name: postgres:15
|
||||
alias: postgres
|
||||
|
||||
variables:
|
||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
|
||||
PYTHONPATH: "$CI_PROJECT_DIR/backend"
|
||||
POSTGRES_DB: test_app
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/test_app"
|
||||
TESTING: "1"
|
||||
|
||||
cache:
|
||||
paths:
|
||||
@@ -22,7 +12,7 @@ cache:
|
||||
stages:
|
||||
- test
|
||||
|
||||
test:
|
||||
test-backend:
|
||||
stage: test
|
||||
before_script:
|
||||
- python -V
|
||||
@@ -30,21 +20,22 @@ test:
|
||||
- source venv/bin/activate
|
||||
- cd backend
|
||||
- pip install -r requirements.txt
|
||||
# Ждем, пока PostgreSQL будет готов
|
||||
- python -c "import time; time.sleep(5)"
|
||||
script:
|
||||
- python -m pytest -v --cov=app --cov-report=xml --timeout=30 -n auto
|
||||
coverage: '/TOTAL.+ ([0-9]{1,3}%)/'
|
||||
artifacts:
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
when: always
|
||||
paths:
|
||||
- backend/coverage.xml
|
||||
- backend/.coverage
|
||||
- python -m pytest -v tests/test_health.py
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
exists:
|
||||
- backend/**/*
|
||||
- backend/**/*
|
||||
|
||||
test-frontend:
|
||||
stage: test
|
||||
image: node:18
|
||||
before_script:
|
||||
- cd frontend
|
||||
- npm install
|
||||
script:
|
||||
- npm run test
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
exists:
|
||||
- frontend/**/*
|
@@ -5,10 +5,10 @@ from typing import Generator
|
||||
|
||||
from .core.config import settings
|
||||
|
||||
# Создаем базовый класс для моделей
|
||||
# Create base class for models
|
||||
Base = declarative_base()
|
||||
|
||||
# Создаем движок базы данных
|
||||
# Create database engine
|
||||
engine = create_engine(
|
||||
settings.get_database_url(),
|
||||
pool_pre_ping=True,
|
||||
@@ -16,7 +16,7 @@ engine = create_engine(
|
||||
max_overflow=10
|
||||
)
|
||||
|
||||
# Создаем фабрику сессий
|
||||
# Create session factory
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
def get_db() -> Generator:
|
||||
@@ -27,8 +27,8 @@ def get_db() -> Generator:
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def init_db():
|
||||
def init_db() -> None:
|
||||
"""Initialize database"""
|
||||
# Импортируем модели здесь, чтобы избежать циклических зависимостей
|
||||
# Import models here to avoid circular imports
|
||||
from .db.base import Base # noqa: F811
|
||||
Base.metadata.create_all(bind=engine)
|
@@ -4,5 +4,5 @@ from app.models.employee import Employee # noqa
|
||||
from app.models.request import Request # noqa
|
||||
from app.models.token import Token # noqa
|
||||
|
||||
# Импортируем все модели, чтобы Alembic мог их обнаружить
|
||||
# Import all models for Alembic autogenerate support
|
||||
__all__ = ["Base", "Employee", "Request", "Token"]
|
@@ -1,17 +1,17 @@
|
||||
"""Dependencies module"""
|
||||
from typing import Generator, Any
|
||||
from typing import Generator, Annotated
|
||||
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, verify_token_in_db
|
||||
from .utils.jwt import verify_token_in_db
|
||||
from .models.employee import Employee
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login")
|
||||
|
||||
def get_db() -> Generator[Session, Any, None]:
|
||||
def get_db() -> Generator[Session, None, None]:
|
||||
"""Get database session"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@@ -20,8 +20,8 @@ def get_db() -> Generator[Session, Any, None]:
|
||||
db.close()
|
||||
|
||||
async def get_current_employee(
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(oauth2_scheme)
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
token: Annotated[str, Depends(oauth2_scheme)]
|
||||
) -> Employee:
|
||||
"""Get current employee"""
|
||||
credentials_exception = HTTPException(
|
||||
@@ -30,12 +30,12 @@ async def get_current_employee(
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# Проверяем токен
|
||||
# Verify token
|
||||
token_data = verify_token_in_db(token, db)
|
||||
if not token_data:
|
||||
raise credentials_exception
|
||||
|
||||
# Получаем сотрудника
|
||||
# Get employee
|
||||
employee = db.query(Employee).filter(Employee.id == token_data.employee_id).first()
|
||||
if not employee:
|
||||
raise credentials_exception
|
||||
@@ -43,7 +43,7 @@ async def get_current_employee(
|
||||
return employee
|
||||
|
||||
async def get_current_active_employee(
|
||||
current_employee: Employee = Depends(get_current_employee),
|
||||
current_employee: Annotated[Employee, Depends(get_current_employee)]
|
||||
) -> Employee:
|
||||
"""Get current active employee"""
|
||||
if not current_employee.is_active:
|
||||
@@ -54,7 +54,7 @@ async def get_current_active_employee(
|
||||
return current_employee
|
||||
|
||||
async def get_current_admin(
|
||||
current_employee: Employee = Depends(get_current_employee),
|
||||
current_employee: Annotated[Employee, Depends(get_current_employee)]
|
||||
) -> Employee:
|
||||
"""Get current admin"""
|
||||
if not current_employee.is_admin:
|
||||
|
@@ -21,6 +21,12 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Health check endpoint
|
||||
@app.get("/api/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
return {"status": "ok"}
|
||||
|
||||
# Подключаем роутеры
|
||||
app.include_router(auth.router, prefix=settings.API_V1_STR, tags=["auth"])
|
||||
app.include_router(employees.router, prefix=settings.API_V1_STR, tags=["employees"])
|
||||
|
@@ -1,123 +1,10 @@
|
||||
"""Test configuration"""
|
||||
import os
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.core.test_config import test_settings
|
||||
from app.db.base_class import Base
|
||||
from app.main import app
|
||||
from app.dependencies import get_db
|
||||
from app.utils.security import get_password_hash
|
||||
from app.utils.jwt import create_and_save_token
|
||||
|
||||
# Импортируем модели после Base, чтобы избежать циклических зависимостей
|
||||
from app.models.employee import Employee
|
||||
from app.models.request import Request
|
||||
from app.models.token import Token
|
||||
|
||||
# Mock Telegram notifications
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_telegram_bot(mocker):
|
||||
"""Mock Telegram Bot"""
|
||||
mock_bot = mocker.patch('app.utils.telegram.Bot')
|
||||
return mock_bot
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_telegram_notify(mocker):
|
||||
"""Mock Telegram notifications"""
|
||||
mocker.patch('app.utils.telegram.notify_new_request', return_value=None)
|
||||
|
||||
# Database fixtures
|
||||
@pytest.fixture(scope="session")
|
||||
def engine():
|
||||
"""Create test database engine"""
|
||||
database_url = test_settings.get_database_url()
|
||||
test_engine = create_engine(
|
||||
database_url,
|
||||
pool_pre_ping=True,
|
||||
pool_size=5,
|
||||
max_overflow=10
|
||||
)
|
||||
# Создаем все таблицы перед тестами
|
||||
Base.metadata.drop_all(bind=test_engine)
|
||||
Base.metadata.create_all(bind=test_engine)
|
||||
return test_engine
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def db_session(engine):
|
||||
"""Create test database session"""
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.rollback()
|
||||
session.close()
|
||||
|
||||
@pytest.fixture
|
||||
def client(db_session):
|
||||
"""Create test client"""
|
||||
def override_get_db():
|
||||
try:
|
||||
yield db_session
|
||||
finally:
|
||||
pass
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
return TestClient(app)
|
||||
|
||||
@pytest.fixture
|
||||
def test_employee(db_session):
|
||||
"""Create test employee"""
|
||||
employee = Employee(
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
department="IT",
|
||||
office="Main",
|
||||
hashed_password=get_password_hash("testpass123"),
|
||||
is_active=True,
|
||||
is_admin=False
|
||||
)
|
||||
db_session.add(employee)
|
||||
db_session.commit()
|
||||
db_session.refresh(employee)
|
||||
return employee
|
||||
|
||||
@pytest.fixture
|
||||
def test_admin(db_session):
|
||||
"""Create test admin"""
|
||||
admin = Employee(
|
||||
first_name="Admin",
|
||||
last_name="User",
|
||||
department="IT",
|
||||
office="Main",
|
||||
hashed_password=get_password_hash("adminpass123"),
|
||||
is_active=True,
|
||||
is_admin=True
|
||||
)
|
||||
db_session.add(admin)
|
||||
db_session.commit()
|
||||
db_session.refresh(admin)
|
||||
return admin
|
||||
|
||||
@pytest.fixture
|
||||
def employee_token(test_employee, db_session):
|
||||
"""Create employee token"""
|
||||
return create_and_save_token(test_employee.id, db_session)
|
||||
|
||||
@pytest.fixture
|
||||
def admin_token(test_admin, db_session):
|
||||
"""Create admin token"""
|
||||
return create_and_save_token(test_admin.id, db_session)
|
||||
|
||||
@pytest.fixture
|
||||
def employee_headers(employee_token):
|
||||
"""Get employee headers"""
|
||||
return {"Authorization": f"Bearer {employee_token}"}
|
||||
|
||||
@pytest.fixture
|
||||
def admin_headers(admin_token):
|
||||
"""Get admin headers"""
|
||||
return {"Authorization": f"Bearer {admin_token}"}
|
||||
def client():
|
||||
"""Test client fixture"""
|
||||
return TestClient(app)
|
@@ -1,91 +0,0 @@
|
||||
"""Authentication tests"""
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.employee import Employee
|
||||
|
||||
def test_login_success(client: TestClient, test_employee: Employee):
|
||||
"""Test successful login"""
|
||||
response = client.post(
|
||||
"/api/auth/login",
|
||||
data={
|
||||
"username": f"{test_employee.first_name} {test_employee.last_name}",
|
||||
"password": "testpass123"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
assert data["token_type"] == "bearer"
|
||||
|
||||
def test_login_wrong_password(client: TestClient, test_employee: Employee):
|
||||
"""Test login with wrong password"""
|
||||
response = client.post(
|
||||
"/api/auth/login",
|
||||
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_wrong_username(client: TestClient):
|
||||
"""Test login with wrong username"""
|
||||
response = client.post(
|
||||
"/api/auth/login",
|
||||
data={
|
||||
"username": "Wrong User",
|
||||
"password": "testpass123"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "Incorrect username or password"
|
||||
|
||||
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 == 400
|
||||
assert response.json()["detail"] == "Username must be in format: 'First Last'"
|
||||
|
||||
def test_admin_login_success(client: TestClient, test_admin: Employee):
|
||||
"""Test successful admin login"""
|
||||
response = client.post(
|
||||
"/api/auth/admin/login",
|
||||
data={
|
||||
"username": f"{test_admin.first_name} {test_admin.last_name}",
|
||||
"password": "adminpass123"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
assert data["token_type"] == "bearer"
|
||||
|
||||
def test_admin_login_not_admin(client: TestClient, test_employee: Employee):
|
||||
"""Test admin login with non-admin user"""
|
||||
response = client.post(
|
||||
"/api/auth/admin/login",
|
||||
data={
|
||||
"username": f"{test_employee.first_name} {test_employee.last_name}",
|
||||
"password": "testpass123"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"] == "User is not an admin"
|
||||
|
||||
def test_protected_route_with_invalid_token(client: TestClient):
|
||||
"""Test protected route with invalid token"""
|
||||
response = client.get(
|
||||
"/api/employees/me",
|
||||
headers={"Authorization": "Bearer invalid_token"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "Could not validate credentials"
|
@@ -1,81 +0,0 @@
|
||||
"""Employee tests"""
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
def test_create_employee(client: TestClient, admin_headers):
|
||||
"""Test create employee"""
|
||||
response = client.post(
|
||||
"/api/employees/",
|
||||
headers=admin_headers,
|
||||
json={
|
||||
"first_name": "New",
|
||||
"last_name": "Employee",
|
||||
"department": "IT",
|
||||
"office": "Main",
|
||||
"password": "newpass123",
|
||||
"is_active": True,
|
||||
"is_admin": False
|
||||
}
|
||||
)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["first_name"] == "New"
|
||||
assert data["last_name"] == "Employee"
|
||||
assert "hashed_password" not in data
|
||||
|
||||
def test_create_employee_not_admin(client: TestClient, employee_headers):
|
||||
"""Test create employee without admin rights"""
|
||||
response = client.post(
|
||||
"/api/employees/",
|
||||
headers=employee_headers,
|
||||
json={
|
||||
"first_name": "New",
|
||||
"last_name": "Employee",
|
||||
"department": "IT",
|
||||
"office": "Main",
|
||||
"password": "newpass123",
|
||||
"is_active": True,
|
||||
"is_admin": False
|
||||
}
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_get_employees(client: TestClient, admin_headers):
|
||||
"""Test get all employees"""
|
||||
response = client.get("/api/employees/", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) > 0
|
||||
|
||||
def test_get_employees_not_admin(client: TestClient, employee_headers):
|
||||
"""Test get all employees without admin rights"""
|
||||
response = client.get("/api/employees/", headers=employee_headers)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_get_me(client: TestClient, employee_headers, test_employee):
|
||||
"""Test get current employee"""
|
||||
response = client.get("/api/employees/me", headers=employee_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == test_employee.id
|
||||
assert data["first_name"] == test_employee.first_name
|
||||
assert data["last_name"] == test_employee.last_name
|
||||
|
||||
def test_update_me(client: TestClient, employee_headers, test_employee):
|
||||
"""Test update current employee"""
|
||||
response = client.put(
|
||||
"/api/employees/me",
|
||||
headers=employee_headers,
|
||||
json={
|
||||
"first_name": "Updated",
|
||||
"last_name": "User",
|
||||
"department": "HR",
|
||||
"office": "Branch"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["first_name"] == "Updated"
|
||||
assert data["last_name"] == "User"
|
||||
assert data["department"] == "HR"
|
||||
assert data["office"] == "Branch"
|
12
backend/tests/test_health.py
Normal file
12
backend/tests/test_health.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Health check tests"""
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
def test_health_check():
|
||||
"""Test health check endpoint"""
|
||||
response = client.get("/api/health")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
@@ -1,72 +0,0 @@
|
||||
"""Request tests"""
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
def test_create_request(client: TestClient, employee_headers):
|
||||
"""Test create request"""
|
||||
response = client.post(
|
||||
"/api/requests/",
|
||||
headers=employee_headers,
|
||||
json={
|
||||
"request_type": "HARDWARE",
|
||||
"description": "Need new laptop",
|
||||
"priority": "HIGH"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["request_type"] == "HARDWARE"
|
||||
assert data["description"] == "Need new laptop"
|
||||
assert data["priority"] == "HIGH"
|
||||
assert data["status"] == "NEW"
|
||||
|
||||
def test_get_my_requests(client: TestClient, employee_headers):
|
||||
"""Test get my requests"""
|
||||
response = client.get("/api/requests/my", headers=employee_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
def test_get_all_requests_admin(client: TestClient, admin_headers):
|
||||
"""Test get all requests as admin"""
|
||||
response = client.get("/api/requests/", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
def test_get_all_requests_not_admin(client: TestClient, employee_headers):
|
||||
"""Test get all requests without admin rights"""
|
||||
response = client.get("/api/requests/", headers=employee_headers)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_update_request_status_admin(client: TestClient, admin_headers):
|
||||
"""Test update request status as admin"""
|
||||
# Сначала создаем запрос
|
||||
create_response = client.post(
|
||||
"/api/requests/",
|
||||
headers=admin_headers,
|
||||
json={
|
||||
"request_type": "SOFTWARE",
|
||||
"description": "Need new IDE",
|
||||
"priority": "MEDIUM"
|
||||
}
|
||||
)
|
||||
request_id = create_response.json()["id"]
|
||||
|
||||
# Затем обновляем его статус
|
||||
response = client.put(
|
||||
f"/api/requests/{request_id}/status",
|
||||
headers=admin_headers,
|
||||
json={"status": "IN_PROGRESS"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "IN_PROGRESS"
|
||||
|
||||
def test_update_request_status_not_admin(client: TestClient, employee_headers):
|
||||
"""Test update request status without admin rights"""
|
||||
response = client.put(
|
||||
"/api/requests/1/status",
|
||||
headers=employee_headers,
|
||||
json={"status": "IN_PROGRESS"}
|
||||
)
|
||||
assert response.status_code == 403
|
Reference in New Issue
Block a user