mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-19 21:00:15 -05:00
548de62dde
- 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
226 lines
6.6 KiB
Python
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)
|