test(backend): convert existing app suites to unittest
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from apps.clients.models import Client
|
||||
from apps.projects.models import Project
|
||||
@@ -12,194 +12,199 @@ from apps.users.models import User
|
||||
from apps.workspaces.models import Workspace, WorkspaceMembership
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def api_client():
|
||||
return APIClient()
|
||||
class ReportViewTests(APITestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.owner = User.objects.create_user(
|
||||
mobile="09128880001",
|
||||
password="secret123",
|
||||
first_name="Owner",
|
||||
)
|
||||
cls.admin = User.objects.create_user(
|
||||
mobile="09128880002",
|
||||
password="secret123",
|
||||
first_name="Admin",
|
||||
)
|
||||
cls.member = User.objects.create_user(
|
||||
mobile="09128880003",
|
||||
password="secret123",
|
||||
first_name="Member",
|
||||
)
|
||||
cls.workspace = Workspace.objects.create(name="Reports", owner=cls.owner)
|
||||
WorkspaceMembership.objects.create(
|
||||
workspace=cls.workspace,
|
||||
user=cls.admin,
|
||||
role=WorkspaceMembership.Role.ADMIN,
|
||||
is_active=True,
|
||||
)
|
||||
WorkspaceMembership.objects.create(
|
||||
workspace=cls.workspace,
|
||||
user=cls.member,
|
||||
role=WorkspaceMembership.Role.MEMBER,
|
||||
is_active=True,
|
||||
)
|
||||
cls.client_obj = Client.objects.create(workspace=cls.workspace, name="Acme")
|
||||
cls.project = Project.objects.create(
|
||||
workspace=cls.workspace,
|
||||
name="Website",
|
||||
client=cls.client_obj,
|
||||
)
|
||||
cls.tag = Tag.objects.create(
|
||||
workspace=cls.workspace,
|
||||
name="Design",
|
||||
color="#ffffff",
|
||||
)
|
||||
|
||||
entry_owner = TimeEntry.objects.create(
|
||||
workspace=cls.workspace,
|
||||
user=cls.owner,
|
||||
project=cls.project,
|
||||
description="Owner work",
|
||||
start_time="2026-04-10T08:00:00+03:30",
|
||||
end_time="2026-04-10T10:00:00+03:30",
|
||||
duration=timedelta(hours=2),
|
||||
is_billable=True,
|
||||
hourly_rate=Decimal("25.00"),
|
||||
currency="USD",
|
||||
)
|
||||
entry_owner.tags.add(cls.tag)
|
||||
|
||||
@pytest.fixture()
|
||||
def owner(db):
|
||||
return User.objects.create_user(mobile="09128880001", password="secret123", first_name="Owner")
|
||||
entry_member = TimeEntry.objects.create(
|
||||
workspace=cls.workspace,
|
||||
user=cls.member,
|
||||
project=cls.project,
|
||||
description="Member work",
|
||||
start_time="2026-04-11T09:00:00+03:30",
|
||||
end_time="2026-04-11T10:00:00+03:30",
|
||||
duration=timedelta(hours=1),
|
||||
is_billable=False,
|
||||
currency="USD",
|
||||
)
|
||||
entry_member.tags.add(cls.tag)
|
||||
|
||||
def test_member_only_sees_own_chart_report(self):
|
||||
self.client.force_authenticate(user=self.member)
|
||||
|
||||
@pytest.fixture()
|
||||
def admin(db):
|
||||
return User.objects.create_user(mobile="09128880002", password="secret123", first_name="Admin")
|
||||
response = self.client.get(
|
||||
"/api/reports/chart/",
|
||||
{"workspace": str(self.workspace.id), "period": "this_month"},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["summary"]["total_duration"], "01:00:00")
|
||||
|
||||
@pytest.fixture()
|
||||
def member(db):
|
||||
return User.objects.create_user(mobile="09128880003", password="secret123", first_name="Member")
|
||||
def test_admin_can_request_combined_table_report(self):
|
||||
self.client.force_authenticate(user=self.admin)
|
||||
|
||||
response = self.client.get(
|
||||
"/api/reports/table/",
|
||||
{"workspace": str(self.workspace.id), "period": "this_month"},
|
||||
)
|
||||
|
||||
@pytest.fixture()
|
||||
def workspace(owner, admin, member):
|
||||
workspace = Workspace.objects.create(name="Reports", owner=owner)
|
||||
WorkspaceMembership.objects.create(workspace=workspace, user=admin, role=WorkspaceMembership.Role.ADMIN, is_active=True)
|
||||
WorkspaceMembership.objects.create(workspace=workspace, user=member, role=WorkspaceMembership.Role.MEMBER, is_active=True)
|
||||
return workspace
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["summary"]["total_duration"], "03:00:00")
|
||||
self.assertEqual(len(response.data["days"]), 2)
|
||||
self.assertIsNone(response.data["days"][0]["latest_hourly_rate"])
|
||||
self.assertIsNone(response.data["days"][1]["latest_hourly_rate"])
|
||||
|
||||
def test_daily_rate_uses_latest_billable_entry_snapshot(self):
|
||||
self.client.force_authenticate(user=self.owner)
|
||||
|
||||
@pytest.fixture()
|
||||
def client(workspace):
|
||||
return Client.objects.create(workspace=workspace, name="Acme")
|
||||
TimeEntry.objects.create(
|
||||
workspace=self.workspace,
|
||||
user=self.owner,
|
||||
project=self.project,
|
||||
description="Morning work",
|
||||
start_time="2026-04-15T08:00:00+03:30",
|
||||
end_time="2026-04-15T09:00:00+03:30",
|
||||
duration=timedelta(hours=1),
|
||||
is_billable=True,
|
||||
hourly_rate=Decimal("20.00"),
|
||||
currency="USD",
|
||||
)
|
||||
TimeEntry.objects.create(
|
||||
workspace=self.workspace,
|
||||
user=self.owner,
|
||||
project=self.project,
|
||||
description="Later work",
|
||||
start_time="2026-04-15T13:00:00+03:30",
|
||||
end_time="2026-04-15T15:00:00+03:30",
|
||||
duration=timedelta(hours=2),
|
||||
is_billable=True,
|
||||
hourly_rate=Decimal("35.00"),
|
||||
currency="USD",
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
"/api/reports/table/",
|
||||
{
|
||||
"workspace": str(self.workspace.id),
|
||||
"period": "this_month",
|
||||
"user": str(self.owner.id),
|
||||
},
|
||||
)
|
||||
|
||||
@pytest.fixture()
|
||||
def project(workspace, client):
|
||||
return Project.objects.create(workspace=workspace, name="Website", client=client)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
target_day = next(day for day in response.data["days"] if day["date"] == "2026-04-15")
|
||||
self.assertEqual(
|
||||
target_day["latest_hourly_rate"],
|
||||
{"amount": "35.00", "currency": "USD"},
|
||||
)
|
||||
|
||||
def test_custom_period_longer_than_31_days_is_rejected(self):
|
||||
self.client.force_authenticate(user=self.owner)
|
||||
|
||||
@pytest.fixture()
|
||||
def tag(workspace):
|
||||
return Tag.objects.create(workspace=workspace, name="Design", color="#ffffff")
|
||||
response = self.client.get(
|
||||
"/api/reports/chart/",
|
||||
{
|
||||
"workspace": str(self.workspace.id),
|
||||
"period": "period",
|
||||
"from_date": "2026-01-01",
|
||||
"to_date": "2026-02-15",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
@pytest.fixture()
|
||||
def time_entries(workspace, owner, member, project, tag):
|
||||
entry_owner = TimeEntry.objects.create(
|
||||
workspace=workspace,
|
||||
user=owner,
|
||||
project=project,
|
||||
description="Owner work",
|
||||
start_time="2026-04-10T08:00:00+03:30",
|
||||
end_time="2026-04-10T10:00:00+03:30",
|
||||
duration=timedelta(hours=2),
|
||||
is_billable=True,
|
||||
hourly_rate=Decimal("25.00"),
|
||||
currency="USD",
|
||||
)
|
||||
entry_owner.tags.add(tag)
|
||||
entry_member = TimeEntry.objects.create(
|
||||
workspace=workspace,
|
||||
user=member,
|
||||
project=project,
|
||||
description="Member work",
|
||||
start_time="2026-04-11T09:00:00+03:30",
|
||||
end_time="2026-04-11T10:00:00+03:30",
|
||||
duration=timedelta(hours=1),
|
||||
is_billable=False,
|
||||
currency="USD",
|
||||
)
|
||||
entry_member.tags.add(tag)
|
||||
return [entry_owner, entry_member]
|
||||
def test_persian_this_month_uses_jalali_month_bounds(self):
|
||||
self.client.force_authenticate(user=self.owner)
|
||||
|
||||
with patch(
|
||||
"apps.reports.services.aggregation.timezone.localdate",
|
||||
return_value=date(2026, 4, 27),
|
||||
):
|
||||
TimeEntry.objects.create(
|
||||
workspace=self.workspace,
|
||||
user=self.owner,
|
||||
project=self.project,
|
||||
description="Previous jalali month",
|
||||
start_time="2026-04-20T08:00:00+03:30",
|
||||
end_time="2026-04-20T09:00:00+03:30",
|
||||
duration=timedelta(hours=1),
|
||||
is_billable=False,
|
||||
currency="USD",
|
||||
)
|
||||
TimeEntry.objects.create(
|
||||
workspace=self.workspace,
|
||||
user=self.owner,
|
||||
project=self.project,
|
||||
description="Current jalali month",
|
||||
start_time="2026-04-21T08:00:00+03:30",
|
||||
end_time="2026-04-21T10:00:00+03:30",
|
||||
duration=timedelta(hours=2),
|
||||
is_billable=False,
|
||||
currency="USD",
|
||||
)
|
||||
|
||||
def test_member_only_sees_own_chart_report(api_client, member, workspace, time_entries):
|
||||
api_client.force_authenticate(user=member)
|
||||
response = self.client.get(
|
||||
"/api/reports/table/",
|
||||
{
|
||||
"workspace": str(self.workspace.id),
|
||||
"period": "this_month",
|
||||
"language": "fa",
|
||||
},
|
||||
)
|
||||
|
||||
response = api_client.get(
|
||||
"/api/reports/chart/",
|
||||
{"workspace": str(workspace.id), "period": "this_month"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data["summary"]["total_duration"] == "01:00:00"
|
||||
|
||||
|
||||
def test_admin_can_request_combined_table_report(api_client, admin, workspace, time_entries):
|
||||
api_client.force_authenticate(user=admin)
|
||||
|
||||
response = api_client.get(
|
||||
"/api/reports/table/",
|
||||
{"workspace": str(workspace.id), "period": "this_month"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data["summary"]["total_duration"] == "03:00:00"
|
||||
assert len(response.data["days"]) == 2
|
||||
assert response.data["days"][0]["latest_hourly_rate"] is None
|
||||
assert response.data["days"][1]["latest_hourly_rate"] is None
|
||||
|
||||
|
||||
def test_daily_rate_uses_latest_billable_entry_snapshot(api_client, owner, workspace, project):
|
||||
api_client.force_authenticate(user=owner)
|
||||
|
||||
TimeEntry.objects.create(
|
||||
workspace=workspace,
|
||||
user=owner,
|
||||
project=project,
|
||||
description="Morning work",
|
||||
start_time="2026-04-15T08:00:00+03:30",
|
||||
end_time="2026-04-15T09:00:00+03:30",
|
||||
duration=timedelta(hours=1),
|
||||
is_billable=True,
|
||||
hourly_rate=Decimal("20.00"),
|
||||
currency="USD",
|
||||
)
|
||||
TimeEntry.objects.create(
|
||||
workspace=workspace,
|
||||
user=owner,
|
||||
project=project,
|
||||
description="Later work",
|
||||
start_time="2026-04-15T13:00:00+03:30",
|
||||
end_time="2026-04-15T15:00:00+03:30",
|
||||
duration=timedelta(hours=2),
|
||||
is_billable=True,
|
||||
hourly_rate=Decimal("35.00"),
|
||||
currency="USD",
|
||||
)
|
||||
|
||||
response = api_client.get(
|
||||
"/api/reports/table/",
|
||||
{"workspace": str(workspace.id), "period": "this_month", "user": str(owner.id)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data["days"][0]["latest_hourly_rate"] == {
|
||||
"amount": "35.00",
|
||||
"currency": "USD",
|
||||
}
|
||||
|
||||
|
||||
def test_custom_period_longer_than_31_days_is_rejected(api_client, owner, workspace):
|
||||
api_client.force_authenticate(user=owner)
|
||||
|
||||
response = api_client.get(
|
||||
"/api/reports/chart/",
|
||||
{
|
||||
"workspace": str(workspace.id),
|
||||
"period": "period",
|
||||
"from_date": "2026-01-01",
|
||||
"to_date": "2026-02-15",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_persian_this_month_uses_jalali_month_bounds(api_client, owner, workspace, project, monkeypatch):
|
||||
api_client.force_authenticate(user=owner)
|
||||
monkeypatch.setattr("apps.reports.services.aggregation.timezone.localdate", lambda: date(2026, 4, 27))
|
||||
|
||||
TimeEntry.objects.create(
|
||||
workspace=workspace,
|
||||
user=owner,
|
||||
project=project,
|
||||
description="Previous jalali month",
|
||||
start_time="2026-04-20T08:00:00+03:30",
|
||||
end_time="2026-04-20T09:00:00+03:30",
|
||||
duration=timedelta(hours=1),
|
||||
is_billable=False,
|
||||
currency="USD",
|
||||
)
|
||||
TimeEntry.objects.create(
|
||||
workspace=workspace,
|
||||
user=owner,
|
||||
project=project,
|
||||
description="Current jalali month",
|
||||
start_time="2026-04-21T08:00:00+03:30",
|
||||
end_time="2026-04-21T10:00:00+03:30",
|
||||
duration=timedelta(hours=2),
|
||||
is_billable=False,
|
||||
currency="USD",
|
||||
)
|
||||
|
||||
response = api_client.get(
|
||||
"/api/reports/table/",
|
||||
{"workspace": str(workspace.id), "period": "this_month", "language": "fa"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data["summary"]["total_duration"] == "02:00:00"
|
||||
assert response.data["scope"]["from_date"] == "2026-04-21"
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["summary"]["total_duration"], "02:00:00")
|
||||
self.assertEqual(response.data["scope"]["from_date"], "2026-04-21")
|
||||
|
||||
Reference in New Issue
Block a user