From 7cae4948928d52f2575086302a80a0710cae1b87 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Fri, 24 Apr 2026 22:19:36 +0330 Subject: [PATCH] test(time-entries): cover grouped filters and serializer formatting --- apps/time_entries/tests/test_filters.py | 89 +++++++++++++++++++++ apps/time_entries/tests/test_serializers.py | 29 +++++++ apps/time_entries/tests/test_services.py | 47 +++++++++++ apps/time_entries/tests/test_views.py | 51 ++++++++++++ 4 files changed, 216 insertions(+) create mode 100644 apps/time_entries/tests/test_filters.py create mode 100644 apps/time_entries/tests/test_serializers.py create mode 100644 apps/time_entries/tests/test_services.py create mode 100644 apps/time_entries/tests/test_views.py diff --git a/apps/time_entries/tests/test_filters.py b/apps/time_entries/tests/test_filters.py new file mode 100644 index 0000000..719d495 --- /dev/null +++ b/apps/time_entries/tests/test_filters.py @@ -0,0 +1,89 @@ +from datetime import datetime + +from apps.clients.models import Client +from apps.projects.models import Project +from apps.tags.models import Tag +from apps.time_entries.api.filters import TimeEntryFilter +from apps.time_entries.models import TimeEntry +from apps.users.models import User +from apps.workspaces.models import Workspace + + +def make_aware(year, month, day, hour=9, minute=0, second=0): + from django.utils import timezone + + return timezone.make_aware(datetime(year, month, day, hour, minute, second), timezone.get_current_timezone()) + + +def test_time_entry_filter_supports_project_client_tags_and_custom_dates(db): + user = User.objects.create_user(mobile="09124444444", password="secret123") + workspace = Workspace.objects.create(name="Core", owner=user) + client_a = Client.objects.create(workspace=workspace, name="Client A") + client_b = Client.objects.create(workspace=workspace, name="Client B") + project_a = Project.objects.create(workspace=workspace, client=client_a, name="Project A") + project_b = Project.objects.create(workspace=workspace, client=client_b, name="Project B") + tag_backend = Tag.objects.create(workspace=workspace, name="Backend", color="#0EA5E9") + tag_ops = Tag.objects.create(workspace=workspace, name="Ops", color="#10B981") + + entry_a = TimeEntry.objects.create( + workspace=workspace, + user=user, + project=project_a, + description="Backend work", + start_time=make_aware(2026, 4, 10, 10, 0, 0), + end_time=make_aware(2026, 4, 10, 12, 0, 0), + ) + entry_a.tags.set([tag_backend]) + + entry_b = TimeEntry.objects.create( + workspace=workspace, + user=user, + project=project_b, + description="Ops work", + start_time=make_aware(2026, 4, 18, 14, 0, 0), + end_time=make_aware(2026, 4, 18, 15, 30, 0), + ) + entry_b.tags.set([tag_ops]) + + queryset = TimeEntry.objects.filter(workspace=workspace, is_deleted=False) + + filtered = TimeEntryFilter( + data={ + "workspace": str(workspace.id), + "project": str(project_a.id), + "client": str(client_a.id), + "tags": str(tag_backend.id), + "started_after": "2026-04-01", + "started_before": "2026-04-15", + }, + queryset=queryset, + ).qs + + assert list(filtered) == [entry_a] + + +def test_time_entry_filter_supports_status_values(db): + user = User.objects.create_user(mobile="09125555555", password="secret123") + workspace = Workspace.objects.create(name="Core", owner=user) + + ended_entry = TimeEntry.objects.create( + workspace=workspace, + user=user, + description="Ended entry", + start_time=make_aware(2026, 4, 24, 9, 0, 0), + end_time=make_aware(2026, 4, 24, 10, 0, 0), + ) + running_entry = TimeEntry.objects.create( + workspace=workspace, + user=user, + description="Running entry", + start_time=make_aware(2026, 4, 15, 9, 0, 0), + ) + + queryset = TimeEntry.objects.filter(workspace=workspace, is_deleted=False) + + ended = TimeEntryFilter(data={"status": "ended"}, queryset=queryset).qs + running = TimeEntryFilter(data={"status": "running"}, queryset=queryset).qs + + assert list(ended) == [ended_entry] + assert list(running) == [running_entry] diff --git a/apps/time_entries/tests/test_serializers.py b/apps/time_entries/tests/test_serializers.py new file mode 100644 index 0000000..bd9b8c2 --- /dev/null +++ b/apps/time_entries/tests/test_serializers.py @@ -0,0 +1,29 @@ +from datetime import datetime + +from django.utils import timezone + +from apps.time_entries.api.serializers import TimeEntrySerializer +from apps.time_entries.models import TimeEntry +from apps.users.models import User +from apps.workspaces.models import Workspace + + +def test_time_entry_serializer_keeps_seconds(db): + user = User.objects.create_user(mobile="09123333333", password="secret123") + workspace = Workspace.objects.create(name="Core", owner=user) + current_timezone = timezone.get_current_timezone() + + start_time = timezone.make_aware(datetime(2026, 4, 23, 10, 15, 42), current_timezone) + end_time = timezone.make_aware(datetime(2026, 4, 23, 11, 0, 5), current_timezone) + + entry = TimeEntry.objects.create( + workspace=workspace, + user=user, + start_time=start_time, + end_time=end_time, + ) + + data = TimeEntrySerializer(entry).data + + assert data["start_time"] == start_time.strftime("%Y-%m-%d %H:%M:%S") + assert data["end_time"] == end_time.strftime("%Y-%m-%d %H:%M:%S") diff --git a/apps/time_entries/tests/test_services.py b/apps/time_entries/tests/test_services.py new file mode 100644 index 0000000..ddb060f --- /dev/null +++ b/apps/time_entries/tests/test_services.py @@ -0,0 +1,47 @@ +from datetime import timedelta + +import pytest +from django.utils import timezone +from rest_framework.exceptions import ValidationError + +from apps.time_entries.services.time_entries import create_time_entry, stop_time_entry +from apps.users.models import User +from apps.workspaces.models import Workspace + + +@pytest.fixture +def workspace_owner(db): + user = User.objects.create_user(mobile="09121111111", password="secret123") + workspace = Workspace.objects.create(name="Core", owner=user) + return user, workspace + + +def test_create_time_entry_allows_only_one_running_timer_per_workspace(workspace_owner): + user, workspace = workspace_owner + + create_time_entry( + user=user, + workspace_id=workspace.id, + start_time=timezone.now(), + ) + + with pytest.raises(ValidationError): + create_time_entry( + user=user, + workspace_id=workspace.id, + start_time=timezone.now() + timedelta(minutes=5), + ) + + +def test_stop_time_entry_sets_end_time_and_duration(workspace_owner): + user, workspace = workspace_owner + entry = create_time_entry( + user=user, + workspace_id=workspace.id, + start_time=timezone.now() - timedelta(hours=1), + ) + + stopped_entry = stop_time_entry(entry, end_time=timezone.now()) + + assert stopped_entry.end_time is not None + assert stopped_entry.duration is not None diff --git a/apps/time_entries/tests/test_views.py b/apps/time_entries/tests/test_views.py new file mode 100644 index 0000000..afbb5f4 --- /dev/null +++ b/apps/time_entries/tests/test_views.py @@ -0,0 +1,51 @@ +from datetime import datetime + +from django.utils import timezone +from rest_framework.test import APIClient + +from apps.time_entries.models import TimeEntry +from apps.users.models import User +from apps.workspaces.models import Workspace + + +def make_aware(year, month, day, hour=9, minute=0, second=0): + return timezone.make_aware(datetime(year, month, day, hour, minute, second), timezone.get_current_timezone()) + + +def test_time_entry_list_returns_grouped_payload_for_ended_entries(db): + user = User.objects.create_user(mobile="09126666666", password="secret123") + workspace = Workspace.objects.create(name="Core", owner=user) + + first_entry = TimeEntry.objects.create( + workspace=workspace, + user=user, + description="Morning work", + start_time=make_aware(2026, 4, 24, 9, 0, 0), + end_time=make_aware(2026, 4, 24, 10, 30, 0), + ) + TimeEntry.objects.create( + workspace=workspace, + user=user, + description="Running work", + start_time=make_aware(2026, 4, 24, 11, 0, 0), + ) + + client = APIClient() + client.force_authenticate(user=user) + + response = client.get( + "/api/time-entries/", + { + "workspace": str(workspace.id), + "status": "ended", + "limit": 10, + "offset": 0, + }, + ) + + assert response.status_code == 200 + assert response.data["current_page_items_count"] == 1 + assert response.data["has_more"] is False + assert len(response.data["groups"]) == 1 + assert len(response.data["groups"][0]["days"]) == 1 + assert response.data["groups"][0]["days"][0]["entries"][0]["id"] == str(first_entry.id)