Files
TimeTracker/tests/factories.py
T
Dries Peeters 548de62dde test: extend fixtures and add scope, auth, recurring, reports tests
- Extend conftest and factories for API and scope tests
- Add test_auth, test_reports_scope, test_timer_scope
- Add test_recurring_invoice_service, test_scope_filter
- Add test_admin_dashboard_charts, test_api_contract, test_reports_task_report
- Update test_invoices, test_project_archiving_models, test_project_costs, test_time_entry_repository, test_utils
2026-03-15 09:37:15 +01:00

226 lines
6.6 KiB
Python

"""
Reusable model factories for tests.
Requires factory_boy and Faker (declared in requirements-test.txt).
"""
import datetime as _dt
from decimal import Decimal
import factory
from factory.alchemy import SQLAlchemyModelFactory
from app import db
from app.models import (
User,
Client,
Project,
TimeEntry,
Invoice,
InvoiceItem,
Expense,
Task,
Payment,
ExpenseCategory,
ApiToken,
RecurringInvoice,
)
class _SessionFactory(SQLAlchemyModelFactory):
"""Base factory wired to Flask-SQLAlchemy session."""
class Meta:
abstract = True
sqlalchemy_session = db.session
sqlalchemy_session_persistence = "flush"
class UserFactory(_SessionFactory):
class Meta:
model = User
username = factory.Sequence(lambda n: f"user{n}")
role = "user"
email = factory.LazyAttribute(lambda o: f"{o.username}@example.com")
class ClientFactory(_SessionFactory):
class Meta:
model = Client
name = factory.Sequence(lambda n: f"Client {n}")
email = factory.LazyAttribute(lambda o: f"{o.name.lower().replace(' ', '')}@example.com")
default_hourly_rate = Decimal("80.00")
class ProjectFactory(_SessionFactory):
class Meta:
model = Project
name = factory.Sequence(lambda n: f"Project {n}")
@factory.lazy_attribute
def client_id(self):
client = ClientFactory()
# Ensure id is populated
db.session.flush()
return client.id
description = factory.Faker("sentence")
billable = True
hourly_rate = Decimal("75.00")
status = "active"
class UserTaskFactory(_SessionFactory):
class Meta:
model = Task
name = factory.Sequence(lambda n: f"Task {n}")
description = factory.Faker("sentence")
project = factory.SubFactory(ProjectFactory)
created_by = factory.SubFactory(UserFactory)
priority = "medium"
class TimeEntryFactory(_SessionFactory):
class Meta:
model = TimeEntry
user_fk = factory.SubFactory(UserFactory)
project_fk = factory.SubFactory(ProjectFactory)
user_id = factory.SelfAttribute("user_fk.id")
project_id = factory.SelfAttribute("project_fk.id")
start_time = factory.LazyFunction(lambda: _dt.datetime.now() - _dt.timedelta(hours=2))
end_time = factory.LazyFunction(lambda: _dt.datetime.now())
notes = factory.Faker("sentence")
tags = "test,automation"
source = "manual"
billable = True
class InvoiceFactory(_SessionFactory):
class Meta:
model = Invoice
project_fk = factory.SubFactory(ProjectFactory)
invoice_number = factory.LazyFunction(
lambda: (
Invoice.generate_invoice_number()
if hasattr(Invoice, "generate_invoice_number")
else f"INV-{_dt.datetime.utcnow().strftime('%Y%m%d')}-001"
)
)
project_id = factory.SelfAttribute("project_fk.id")
client_id = factory.SelfAttribute("project_fk.client_id")
client_name = factory.LazyAttribute(lambda o: db.session.get(Client, o.client_id).name if o.client_id else "Client")
created_by = factory.LazyAttribute(lambda o: UserFactory().id)
tax_rate = Decimal("20.00")
issue_date = factory.LazyFunction(lambda: _dt.date.today())
due_date = factory.LazyFunction(lambda: _dt.date.today() + _dt.timedelta(days=30))
status = "draft"
class InvoiceItemFactory(_SessionFactory):
class Meta:
model = InvoiceItem
# By default, create a backing invoice; tests may override invoice_id explicitly.
invoice_id = factory.LazyAttribute(lambda o: InvoiceFactory().id)
description = factory.Faker("sentence")
quantity = Decimal("1.00")
unit_price = Decimal("50.00")
class ExpenseFactory(_SessionFactory):
class Meta:
model = Expense
user_fk = factory.SubFactory(UserFactory)
project_fk = factory.SubFactory(ProjectFactory)
user_id = factory.SelfAttribute("user_fk.id")
project_id = factory.SelfAttribute("project_fk.id")
client_id = factory.SelfAttribute("project_fk.client_id")
title = factory.Faker("sentence", nb_words=3)
category = "other"
amount = Decimal("10.00")
expense_date = factory.LazyFunction(lambda: _dt.date.today())
billable = False
reimbursable = True
class PaymentFactory(_SessionFactory):
class Meta:
model = Payment
# Ensure an invoice exists by default; tests can override invoice_id explicitly.
invoice_id = factory.LazyAttribute(lambda _: InvoiceFactory().id)
amount = Decimal("100.00")
currency = "EUR"
payment_date = factory.LazyFunction(lambda: _dt.date.today())
method = "bank_transfer"
reference = factory.Sequence(lambda n: f"PAY-REF-{n:04d}")
status = "completed"
received_by = factory.LazyAttribute(lambda _: UserFactory().id)
class ExpenseCategoryFactory(_SessionFactory):
class Meta:
model = ExpenseCategory
name = factory.Sequence(lambda n: f"Category {n}")
code = factory.Sequence(lambda n: f"C{n:03d}")
monthly_budget = Decimal("5000")
quarterly_budget = Decimal("15000")
yearly_budget = Decimal("60000")
budget_threshold_percent = 80
requires_receipt = True
requires_approval = True
is_active = True
class ApiTokenFactory:
"""
Factory for API tokens. ApiToken must be created via create_token() for correct hashing.
Returns (token_model, plain_token). Usage:
token, plain = ApiTokenFactory.create(user_id=user.id, scopes="read:projects")
"""
@classmethod
def create(
cls,
user_id,
name="Test API Token",
description="",
scopes="read:projects,write:projects,read:time_entries,write:time_entries",
expires_days=30,
):
token, plain = ApiToken.create_token(
user_id=user_id,
name=name,
description=description,
scopes=scopes,
expires_days=expires_days,
)
db.session.add(token)
db.session.flush()
return token, plain
class RecurringInvoiceFactory(_SessionFactory):
class Meta:
model = RecurringInvoice
name = factory.Sequence(lambda n: f"Recurring {n}")
project_fk = factory.SubFactory(ProjectFactory)
project_id = factory.SelfAttribute("project_fk.id")
client_id = factory.SelfAttribute("project_fk.client_id")
client_name = factory.LazyAttribute(lambda o: f"Client {o.client_id}")
frequency = "monthly"
interval = 1
next_run_date = factory.LazyFunction(lambda: _dt.date.today() + _dt.timedelta(days=1))
due_date_days = 30
tax_rate = Decimal("20.00")
currency_code = "EUR"
is_active = True
created_by = factory.LazyAttribute(lambda _: UserFactory().id)