mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-06 03:30:25 -06:00
916 lines
27 KiB
Python
916 lines
27 KiB
Python
"""
|
|
Pytest configuration and shared fixtures for TimeTracker tests.
|
|
This file contains common fixtures and test configuration used across all test modules.
|
|
"""
|
|
|
|
import pytest
|
|
import os
|
|
import tempfile
|
|
import uuid
|
|
from datetime import datetime, timedelta
|
|
from decimal import Decimal
|
|
from sqlalchemy.pool import NullPool
|
|
|
|
from app import create_app, db
|
|
|
|
# Import all models to ensure their tables are created by db.create_all()
|
|
from app.models import (
|
|
User,
|
|
Project,
|
|
TimeEntry,
|
|
Client,
|
|
Settings,
|
|
Invoice,
|
|
InvoiceItem,
|
|
Task,
|
|
TaskActivity,
|
|
Comment,
|
|
ExpenseCategory,
|
|
Mileage,
|
|
PerDiem,
|
|
PerDiemRate,
|
|
ExtraGood,
|
|
FocusSession,
|
|
RecurringBlock,
|
|
RateOverride,
|
|
SavedFilter,
|
|
ProjectCost,
|
|
KanbanColumn,
|
|
TimeEntryTemplate,
|
|
Activity,
|
|
UserFavoriteProject,
|
|
ClientNote,
|
|
WeeklyTimeGoal,
|
|
Expense,
|
|
Permission,
|
|
Role,
|
|
ApiToken,
|
|
CalendarEvent,
|
|
BudgetAlert,
|
|
DataImport,
|
|
DataExport,
|
|
InvoicePDFTemplate,
|
|
ClientPrepaidConsumption,
|
|
AuditLog,
|
|
RecurringInvoice,
|
|
InvoiceEmail,
|
|
Webhook,
|
|
WebhookDelivery,
|
|
InvoiceTemplate,
|
|
Currency,
|
|
ExchangeRate,
|
|
TaxRule,
|
|
Payment,
|
|
CreditNote,
|
|
InvoiceReminderSchedule,
|
|
SavedReportView,
|
|
ReportEmailSchedule,
|
|
Warehouse,
|
|
StockItem,
|
|
WarehouseStock,
|
|
StockMovement,
|
|
StockReservation,
|
|
ProjectStockAllocation,
|
|
Quote,
|
|
QuoteItem,
|
|
)
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Time control helpers (freezegun)
|
|
# ----------------------------------------------------------------------------
|
|
@pytest.fixture
|
|
def time_freezer():
|
|
"""
|
|
Utility fixture to freeze time during a test.
|
|
|
|
Usage:
|
|
freezer = time_freezer() # freezes at default "2024-01-01 09:00:00"
|
|
# ... run code ...
|
|
freezer.stop()
|
|
|
|
# or with a custom timestamp:
|
|
f = time_freezer("2024-06-15 12:30:00")
|
|
# ... run code ...
|
|
f.stop()
|
|
"""
|
|
from freezegun import freeze_time as _freeze_time
|
|
|
|
_active = []
|
|
|
|
def _start(at: str = "2024-01-01 09:00:00"):
|
|
f = _freeze_time(at)
|
|
f.start()
|
|
_active.append(f)
|
|
return f
|
|
|
|
try:
|
|
yield _start
|
|
finally:
|
|
while _active:
|
|
f = _active.pop()
|
|
try:
|
|
f.stop()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
# ============================================================================
|
|
# Application Fixtures
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def app_config():
|
|
"""Base test configuration."""
|
|
return {
|
|
"TESTING": True,
|
|
# Use file-based SQLite to ensure consistent connections across contexts/threads
|
|
"SQLALCHEMY_DATABASE_URI": "sqlite:///pytest_main.sqlite",
|
|
# Mitigate SQLite 'database is locked' by increasing busy timeout and enabling pre-ping
|
|
"SQLALCHEMY_ENGINE_OPTIONS": {
|
|
"pool_pre_ping": True,
|
|
"connect_args": {"timeout": 30},
|
|
"poolclass": NullPool,
|
|
},
|
|
"FLASK_ENV": "testing",
|
|
"SQLALCHEMY_TRACK_MODIFICATIONS": False,
|
|
"WTF_CSRF_ENABLED": False,
|
|
"SECRET_KEY": "test-secret-key-do-not-use-in-production",
|
|
"SERVER_NAME": "localhost:5000",
|
|
"APPLICATION_ROOT": "/",
|
|
"PREFERRED_URL_SCHEME": "http",
|
|
"SESSION_COOKIE_HTTPONLY": True,
|
|
# Ensure a stable locale for Babel-dependent formatting in tests
|
|
"BABEL_DEFAULT_LOCALE": "en",
|
|
}
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def app(app_config):
|
|
"""Create application for testing with function scope."""
|
|
# Use a unique SQLite file per test function to avoid Windows file locking
|
|
unique_db_path = os.path.join(tempfile.gettempdir(), f"pytest_{uuid.uuid4().hex}.sqlite")
|
|
config = dict(app_config)
|
|
config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{unique_db_path}"
|
|
app = create_app(config)
|
|
|
|
with app.app_context():
|
|
# Import all models AFTER app creation but BEFORE db.create_all()
|
|
# This ensures they're registered with SQLAlchemy's metadata
|
|
# Import all models explicitly to ensure their tables are created
|
|
from app.models import (
|
|
User,
|
|
Project,
|
|
TimeEntry,
|
|
Client,
|
|
Settings,
|
|
Invoice,
|
|
InvoiceItem,
|
|
Task,
|
|
TaskActivity,
|
|
Comment,
|
|
ExpenseCategory,
|
|
Mileage,
|
|
PerDiem,
|
|
PerDiemRate,
|
|
ExtraGood,
|
|
FocusSession,
|
|
RecurringBlock,
|
|
RateOverride,
|
|
SavedFilter,
|
|
ProjectCost,
|
|
KanbanColumn,
|
|
TimeEntryTemplate,
|
|
Activity,
|
|
UserFavoriteProject,
|
|
ClientNote,
|
|
WeeklyTimeGoal,
|
|
Expense,
|
|
Permission,
|
|
Role,
|
|
ApiToken,
|
|
CalendarEvent,
|
|
BudgetAlert,
|
|
DataImport,
|
|
DataExport,
|
|
InvoicePDFTemplate,
|
|
ClientPrepaidConsumption,
|
|
AuditLog,
|
|
RecurringInvoice,
|
|
InvoiceEmail,
|
|
Webhook,
|
|
WebhookDelivery,
|
|
InvoiceTemplate,
|
|
Currency,
|
|
ExchangeRate,
|
|
TaxRule,
|
|
Payment,
|
|
CreditNote,
|
|
InvoiceReminderSchedule,
|
|
SavedReportView,
|
|
ReportEmailSchedule,
|
|
)
|
|
|
|
# Ensure any lingering connections are closed to avoid SQLite file locks (Windows)
|
|
try:
|
|
db.engine.dispose()
|
|
except Exception:
|
|
pass
|
|
# Drop all tables first to ensure clean state
|
|
try:
|
|
db.drop_all()
|
|
except Exception:
|
|
pass # Ignore errors if tables don't exist
|
|
|
|
# Create all tables, handling index creation errors gracefully
|
|
# We need to create tables even if some indexes already exist
|
|
# SQLAlchemy's create_all() stops on first error, so we need to handle this carefully
|
|
try:
|
|
db.create_all()
|
|
except Exception as e:
|
|
# SQLite may raise OperationalError if indexes already exist
|
|
# This can happen if db.create_all() is called multiple times
|
|
error_msg = str(e).lower()
|
|
if "index" in error_msg and ("already exists" in error_msg or "duplicate" in error_msg):
|
|
# Index already exists - this is okay, but we need to ensure all tables are created
|
|
# Create tables individually to work around the issue
|
|
from sqlalchemy import inspect
|
|
|
|
inspector = inspect(db.engine)
|
|
existing_tables = set(inspector.get_table_names())
|
|
|
|
# Create missing tables explicitly
|
|
for table_name, table in db.metadata.tables.items():
|
|
if table_name not in existing_tables:
|
|
try:
|
|
table.create(db.engine, checkfirst=True)
|
|
except Exception as table_error:
|
|
# Ignore errors for individual tables (might be index issues)
|
|
pass
|
|
else:
|
|
# Log other errors but try to continue
|
|
import logging
|
|
import traceback
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger.warning(f"Error during db.create_all(): {e}")
|
|
logger.warning(traceback.format_exc())
|
|
|
|
# Verify critical tables were created and create any missing ones
|
|
from sqlalchemy import inspect
|
|
|
|
inspector = inspect(db.engine)
|
|
created_tables = set(inspector.get_table_names())
|
|
required_tables = ["time_entries", "tasks", "users", "projects"]
|
|
missing_tables = [t for t in required_tables if t not in created_tables]
|
|
|
|
if missing_tables:
|
|
# Try to create missing tables explicitly
|
|
for table_name in missing_tables:
|
|
if table_name in db.metadata.tables:
|
|
try:
|
|
db.metadata.tables[table_name].create(db.engine, checkfirst=True)
|
|
except Exception as e:
|
|
# Ignore errors - table might already exist or have dependency issues
|
|
pass
|
|
|
|
# Create default settings
|
|
settings = Settings()
|
|
db.session.add(settings)
|
|
db.session.commit()
|
|
|
|
yield app
|
|
|
|
db.session.remove()
|
|
try:
|
|
db.drop_all()
|
|
except Exception:
|
|
pass # Ignore errors during cleanup
|
|
try:
|
|
db.engine.dispose()
|
|
except Exception:
|
|
pass
|
|
# Remove the per-test database file
|
|
try:
|
|
if os.path.exists(unique_db_path):
|
|
os.remove(unique_db_path)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def client(app):
|
|
"""Create test client."""
|
|
return app.test_client()
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def runner(app):
|
|
"""Create test CLI runner."""
|
|
return app.test_cli_runner()
|
|
|
|
|
|
# ============================================================================
|
|
# Database Fixtures
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def db_session(app):
|
|
"""Create a database session for tests."""
|
|
with app.app_context():
|
|
yield db.session
|
|
|
|
|
|
# ============================================================================
|
|
# User Fixtures
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def user(app):
|
|
"""Create a regular test user."""
|
|
# Idempotent: return existing test user if already present (PostgreSQL CI)
|
|
try:
|
|
existing = User.query.filter_by(username="testuser").first()
|
|
if existing:
|
|
if not existing.is_active:
|
|
existing.is_active = True
|
|
db.session.commit()
|
|
# Ensure password is set for login endpoint
|
|
if not existing.check_password("password123"):
|
|
existing.set_password("password123")
|
|
db.session.commit()
|
|
db.session.refresh(existing)
|
|
return existing
|
|
except Exception:
|
|
# Tables don't exist yet or other DB error, rollback and proceed to create user
|
|
db.session.rollback()
|
|
|
|
try:
|
|
user = User(username="testuser", role="user", email="testuser@example.com")
|
|
user.is_active = True # Set after creation
|
|
user.set_password("password123") # Set password for login endpoint
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
|
|
# Refresh to ensure all relationships are loaded and object stays in session
|
|
db.session.refresh(user)
|
|
return user
|
|
except Exception:
|
|
# If tables still don't exist, try to create them
|
|
db.session.rollback()
|
|
db.create_all()
|
|
|
|
# Try again after creating tables
|
|
user = User(username="testuser", role="user", email="testuser@example.com")
|
|
user.is_active = True # Set after creation
|
|
user.set_password("password123") # Set password for login endpoint
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(user)
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_user(app):
|
|
"""Create an admin test user."""
|
|
# Idempotent: return existing admin user if already present (PostgreSQL CI)
|
|
try:
|
|
existing = User.query.filter_by(username="admin").first()
|
|
if existing:
|
|
if existing.role != "admin":
|
|
existing.role = "admin"
|
|
existing.is_active = True
|
|
db.session.commit()
|
|
# Ensure password is set for login endpoint
|
|
if not existing.check_password("password123"):
|
|
existing.set_password("password123")
|
|
db.session.commit()
|
|
db.session.refresh(existing)
|
|
return existing
|
|
except Exception:
|
|
# Tables don't exist yet or other DB error, rollback and proceed to create admin
|
|
db.session.rollback()
|
|
|
|
try:
|
|
admin = User(username="admin", role="admin", email="admin@example.com")
|
|
admin.is_active = True # Set after creation
|
|
admin.set_password("password123") # Set password for login endpoint
|
|
db.session.add(admin)
|
|
db.session.commit()
|
|
|
|
# Refresh to ensure all relationships are loaded and object stays in session
|
|
db.session.refresh(admin)
|
|
return admin
|
|
except Exception:
|
|
# If tables still don't exist, try to create them
|
|
db.session.rollback()
|
|
db.create_all()
|
|
|
|
# Try again after creating tables
|
|
admin = User(username="admin", role="admin", email="admin@example.com")
|
|
admin.is_active = True # Set after creation
|
|
admin.set_password("password123") # Set password for login endpoint
|
|
db.session.add(admin)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(admin)
|
|
return admin
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_user(user):
|
|
"""Alias for user fixture (for backward compatibility with older tests)."""
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def multiple_users(app):
|
|
"""Create multiple test users."""
|
|
users = []
|
|
for i in range(1, 4):
|
|
user = User(username=f"user{i}", role="user", email=f"user{i}@example.com")
|
|
user.is_active = True # Set after creation
|
|
users.append(user)
|
|
db.session.add_all(users)
|
|
db.session.commit()
|
|
|
|
for user in users:
|
|
db.session.refresh(user)
|
|
|
|
return users
|
|
|
|
|
|
# ============================================================================
|
|
# Client Fixtures
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def test_client(app, user):
|
|
"""Create a test client (business client, not test client)."""
|
|
client_model = Client(
|
|
name="Test Client Corp",
|
|
description="Test client for integration tests",
|
|
contact_person="John Doe",
|
|
email="john@testclient.com",
|
|
phone="+1 (555) 123-4567",
|
|
address="123 Test Street, Test City, TC 12345",
|
|
default_hourly_rate=Decimal("85.00"),
|
|
)
|
|
client_model.status = "active" # Set after creation
|
|
db.session.add(client_model)
|
|
# Flush to assign primary key before commit to avoid expired attribute reloads
|
|
db.session.flush()
|
|
client_id = client_model.id
|
|
db.session.commit()
|
|
# Re-query to ensure we return a persistent instance without relying on refresh
|
|
persisted_client = Client.query.get(client_id) or Client.query.filter_by(id=client_id).first()
|
|
# Fallback to the original instance if re-query unexpectedly returns None
|
|
return persisted_client or client_model
|
|
|
|
|
|
@pytest.fixture
|
|
def multiple_clients(app, user):
|
|
"""Create multiple test clients."""
|
|
clients = []
|
|
for i in range(1, 4):
|
|
client = Client(
|
|
name=f"Client {i}", email=f"client{i}@example.com", default_hourly_rate=Decimal("75.00") + Decimal(i * 10)
|
|
)
|
|
client.status = "active" # Set after creation
|
|
clients.append(client)
|
|
db.session.add_all(clients)
|
|
db.session.commit()
|
|
|
|
for client in clients:
|
|
db.session.refresh(client)
|
|
|
|
return clients
|
|
|
|
|
|
# ============================================================================
|
|
# Project Fixtures
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def project(app, test_client):
|
|
"""Create a test project."""
|
|
# Resolve client_id robustly to avoid issues with expired/detached instances
|
|
try:
|
|
cid = getattr(test_client, "id", None)
|
|
except Exception:
|
|
cid = None
|
|
if not cid:
|
|
existing = Client.query.filter_by(name="Test Client Corp").first() or Client.query.first()
|
|
if existing:
|
|
cid = existing.id
|
|
else:
|
|
fallback = Client(
|
|
name="Test Client Corp", email="john@testclient.com", default_hourly_rate=Decimal("85.00")
|
|
)
|
|
fallback.status = "active"
|
|
db.session.add(fallback)
|
|
db.session.flush()
|
|
cid = fallback.id
|
|
|
|
project = Project(
|
|
name="Test Project",
|
|
client_id=cid,
|
|
description="Test project description",
|
|
billable=True,
|
|
hourly_rate=Decimal("75.00"),
|
|
)
|
|
project.status = "active" # Set after creation
|
|
db.session.add(project)
|
|
# Flush to assign ID before commit and return the same instance to avoid re-query issues
|
|
db.session.flush()
|
|
db.session.commit()
|
|
return project
|
|
|
|
|
|
@pytest.fixture
|
|
def multiple_projects(app, test_client):
|
|
"""Create multiple test projects."""
|
|
# Resolve client_id robustly
|
|
try:
|
|
cid = getattr(test_client, "id", None)
|
|
except Exception:
|
|
cid = None
|
|
if not cid:
|
|
existing = Client.query.filter_by(name="Test Client Corp").first() or Client.query.first()
|
|
if existing:
|
|
cid = existing.id
|
|
else:
|
|
fallback = Client(
|
|
name="Test Client Corp", email="john@testclient.com", default_hourly_rate=Decimal("85.00")
|
|
)
|
|
fallback.status = "active"
|
|
db.session.add(fallback)
|
|
db.session.flush()
|
|
cid = fallback.id
|
|
|
|
projects = []
|
|
for i in range(1, 4):
|
|
project = Project(
|
|
name=f"Project {i}",
|
|
client_id=cid,
|
|
description=f"Test project {i}",
|
|
billable=True,
|
|
hourly_rate=Decimal("75.00"),
|
|
)
|
|
project.status = "active" # Set after creation
|
|
projects.append(project)
|
|
db.session.add_all(projects)
|
|
db.session.commit()
|
|
|
|
for proj in projects:
|
|
db.session.refresh(proj)
|
|
|
|
return projects
|
|
|
|
|
|
# ============================================================================
|
|
# Time Entry Fixtures
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def time_entry(app, user, project):
|
|
"""Create a single time entry."""
|
|
start_time = datetime.utcnow() - timedelta(hours=2)
|
|
end_time = datetime.utcnow()
|
|
|
|
entry = TimeEntry(
|
|
user_id=user.id,
|
|
project_id=project.id,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
notes="Test time entry",
|
|
tags="test,development",
|
|
source="manual",
|
|
billable=True,
|
|
)
|
|
db.session.add(entry)
|
|
db.session.commit()
|
|
|
|
# Refresh entry, but handle case where related objects might be deleted
|
|
try:
|
|
db.session.refresh(entry)
|
|
except Exception:
|
|
# If refresh fails, just return the entry as-is
|
|
# This can happen if user/project are deleted before this fixture is used
|
|
pass
|
|
return entry
|
|
|
|
|
|
@pytest.fixture
|
|
def multiple_time_entries(app, user, project):
|
|
"""Create multiple time entries."""
|
|
base_time = datetime.utcnow() - timedelta(days=7)
|
|
entries = []
|
|
|
|
for i in range(5):
|
|
start = base_time + timedelta(days=i, hours=9)
|
|
end = base_time + timedelta(days=i, hours=17)
|
|
|
|
entry = TimeEntry(
|
|
user_id=user.id,
|
|
project_id=project.id,
|
|
start_time=start,
|
|
end_time=end,
|
|
notes=f"Work day {i+1}",
|
|
tags="development,testing",
|
|
source="manual",
|
|
billable=True,
|
|
)
|
|
entries.append(entry)
|
|
|
|
db.session.add_all(entries)
|
|
db.session.commit()
|
|
|
|
for entry in entries:
|
|
db.session.refresh(entry)
|
|
|
|
return entries
|
|
|
|
|
|
@pytest.fixture
|
|
def active_timer(app, user, project):
|
|
"""Create an active timer (time entry without end time)."""
|
|
timer = TimeEntry(
|
|
user_id=user.id,
|
|
project_id=project.id,
|
|
start_time=datetime.utcnow(),
|
|
notes="Active timer",
|
|
source="auto",
|
|
billable=True,
|
|
)
|
|
db.session.add(timer)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(timer)
|
|
return timer
|
|
|
|
|
|
# ============================================================================
|
|
# Task Fixtures
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def task(app, project, user):
|
|
"""Create a test task."""
|
|
task = Task(
|
|
name="Test Task",
|
|
description="Test task description",
|
|
project_id=project.id,
|
|
priority="medium",
|
|
created_by=user.id,
|
|
)
|
|
task.status = "todo" # Set after creation
|
|
db.session.add(task)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(task)
|
|
return task
|
|
|
|
|
|
# ============================================================================
|
|
# Invoice Fixtures
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def invoice(app, user, project, test_client):
|
|
"""Create a test invoice."""
|
|
from datetime import date
|
|
from factories import InvoiceFactory
|
|
|
|
invoice = InvoiceFactory(
|
|
invoice_number=Invoice.generate_invoice_number(),
|
|
project_id=project.id,
|
|
client_id=test_client.id,
|
|
client_name=test_client.name,
|
|
due_date=date.today() + timedelta(days=30),
|
|
created_by=user.id,
|
|
tax_rate=Decimal("20.00"),
|
|
status="draft",
|
|
)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(invoice)
|
|
return invoice
|
|
|
|
|
|
@pytest.fixture
|
|
def invoice_with_items(app, invoice):
|
|
"""Create an invoice with items."""
|
|
from factories import InvoiceItemFactory
|
|
|
|
items = [
|
|
InvoiceItemFactory(
|
|
invoice_id=invoice.id,
|
|
description="Development work",
|
|
quantity=Decimal("10.00"),
|
|
unit_price=Decimal("75.00"),
|
|
),
|
|
InvoiceItemFactory(
|
|
invoice_id=invoice.id, description="Testing work", quantity=Decimal("5.00"), unit_price=Decimal("60.00")
|
|
),
|
|
]
|
|
db.session.commit()
|
|
|
|
invoice.calculate_totals()
|
|
db.session.commit()
|
|
|
|
db.session.refresh(invoice)
|
|
for item in items:
|
|
db.session.refresh(item)
|
|
|
|
return invoice, items
|
|
|
|
|
|
# ============================================================================
|
|
# Authentication Fixtures
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def authenticated_client(client, user):
|
|
"""Create an authenticated test client."""
|
|
# Use the actual login endpoint to properly authenticate
|
|
# If CSRF is enabled, fetch a token and include it in the form submit
|
|
try:
|
|
from flask import current_app
|
|
|
|
csrf_enabled = bool(current_app.config.get("WTF_CSRF_ENABLED"))
|
|
except Exception:
|
|
csrf_enabled = False
|
|
|
|
login_data = {"username": user.username, "password": "password123"}
|
|
headers = {}
|
|
|
|
if csrf_enabled:
|
|
try:
|
|
resp = client.get("/auth/csrf-token")
|
|
token = ""
|
|
if resp.is_json:
|
|
token = (resp.get_json() or {}).get("csrf_token") or ""
|
|
login_data["csrf_token"] = token
|
|
headers["X-CSRFToken"] = token
|
|
except Exception:
|
|
pass
|
|
|
|
client.post("/login", data=login_data, headers=headers or None, follow_redirects=True)
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_authenticated_client(client, admin_user):
|
|
"""Create an authenticated admin test client."""
|
|
# Use the actual login endpoint to properly authenticate (same as authenticated_client)
|
|
# If CSRF is enabled, fetch a token and include it in the form submit
|
|
try:
|
|
from flask import current_app
|
|
|
|
csrf_enabled = bool(current_app.config.get("WTF_CSRF_ENABLED"))
|
|
except Exception:
|
|
csrf_enabled = False
|
|
|
|
login_data = {"username": admin_user.username, "password": "password123"}
|
|
headers = {}
|
|
|
|
if csrf_enabled:
|
|
try:
|
|
resp = client.get("/auth/csrf-token")
|
|
token = ""
|
|
if resp.is_json:
|
|
token = (resp.get_json() or {}).get("csrf_token") or ""
|
|
login_data["csrf_token"] = token
|
|
headers["X-CSRFToken"] = token
|
|
except Exception:
|
|
pass
|
|
|
|
client.post("/login", data=login_data, headers=headers or None, follow_redirects=True)
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_headers(user):
|
|
"""Create authentication headers for API tests (session-based)."""
|
|
# Note: For tests that use headers, they should use authenticated_client instead
|
|
# This fixture is here for backward compatibility
|
|
return {}
|
|
|
|
|
|
@pytest.fixture
|
|
def regular_user(user):
|
|
"""Alias for user fixture (regular non-admin user)."""
|
|
return user
|
|
|
|
|
|
# ============================================================================
|
|
# Utility Fixtures
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_file():
|
|
"""Create a temporary file for testing."""
|
|
fd, path = tempfile.mkstemp()
|
|
yield path
|
|
os.close(fd)
|
|
os.unlink(path)
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_dir():
|
|
"""Create a temporary directory for testing."""
|
|
dirpath = tempfile.mkdtemp()
|
|
yield dirpath
|
|
import shutil
|
|
|
|
shutil.rmtree(dirpath)
|
|
|
|
|
|
# ============================================================================
|
|
# Alias Fixtures (for compatibility with different test naming conventions)
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def test_client_obj(test_client):
|
|
"""Alias for test_client to avoid naming conflicts"""
|
|
return test_client
|
|
|
|
|
|
@pytest.fixture
|
|
def test_user(user):
|
|
"""Alias for user fixture"""
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_user(user):
|
|
"""Alias for user fixture"""
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def test_project(project):
|
|
"""Alias for project fixture"""
|
|
return project
|
|
|
|
|
|
@pytest.fixture
|
|
def test_task(task):
|
|
"""Alias for task fixture"""
|
|
return task
|
|
|
|
|
|
# ============================================================================
|
|
# Installation Config Fixture
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def installation_config(temp_dir):
|
|
"""Create a temporary installation config for testing"""
|
|
from app.utils.installation import InstallationConfig
|
|
|
|
# Override the config directory to use temp directory
|
|
original_config_dir = InstallationConfig.CONFIG_DIR
|
|
InstallationConfig.CONFIG_DIR = temp_dir
|
|
|
|
# Create the config instance
|
|
config = InstallationConfig()
|
|
|
|
yield config
|
|
|
|
# Restore original config directory
|
|
InstallationConfig.CONFIG_DIR = original_config_dir
|
|
|
|
|
|
# ============================================================================
|
|
# Pytest Markers
|
|
# ============================================================================
|
|
|
|
|
|
def pytest_configure(config):
|
|
"""Configure custom pytest markers."""
|
|
config.addinivalue_line("markers", "smoke: Quick smoke tests")
|
|
config.addinivalue_line("markers", "unit: Unit tests")
|
|
config.addinivalue_line("markers", "integration: Integration tests")
|
|
config.addinivalue_line("markers", "api: API endpoint tests")
|
|
config.addinivalue_line("markers", "database: Database-related tests")
|
|
config.addinivalue_line("markers", "models: Model tests")
|
|
config.addinivalue_line("markers", "routes: Route tests")
|
|
config.addinivalue_line("markers", "security: Security tests")
|
|
config.addinivalue_line("markers", "performance: Performance tests")
|
|
config.addinivalue_line("markers", "slow: Slow running tests")
|