fetch_users previously only surfaced users who appeared as ticket owners. Add list_users(limit, offset) to hit the accounts user-list endpoint directly with raw limit/offset params -- the generated OpenAPI wrapper only supports page, which this endpoint silently ignores. fetch_users now paginates that list and hydrates each entry with get_user_detailed_info to keep the existing email/phone/address columns. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
193 lines
6.6 KiB
Python
193 lines
6.6 KiB
Python
from datetime import datetime
|
|
from io import BytesIO
|
|
from unittest.mock import MagicMock, patch
|
|
from uuid import uuid4
|
|
|
|
from django.test import TestCase
|
|
from openpyxl import load_workbook
|
|
from rest_framework import status
|
|
from rest_framework.test import APITestCase
|
|
|
|
from apps.reports.services.data_sources import ReportDataSources
|
|
from utils.clients.gooyal_advertising_client.models.ticket_state_enum import TicketStateEnum
|
|
|
|
|
|
class ReportDataSourcesFetchUsersTests(TestCase):
|
|
@patch("apps.reports.services.data_sources.get_user_detailed_info")
|
|
@patch("apps.reports.services.data_sources.list_users")
|
|
def test_fetch_users_paginates_full_user_list(self, mock_list_users, mock_get_user):
|
|
first_uuid, second_uuid = str(uuid4()), str(uuid4())
|
|
mock_list_users.side_effect = [
|
|
{"results": [{"uuid": first_uuid}], "next": "http://accounts/users/api/users/?limit=1&offset=1"},
|
|
{"results": [{"uuid": second_uuid}], "next": None},
|
|
]
|
|
mock_get_user.side_effect = lambda uuid: MagicMock(uuid=uuid)
|
|
|
|
users = ReportDataSources().fetch_users({})
|
|
|
|
self.assertEqual([user.uuid for user in users], [first_uuid, second_uuid])
|
|
mock_list_users.assert_any_call(limit=100, offset=0)
|
|
mock_list_users.assert_any_call(limit=100, offset=100)
|
|
mock_get_user.assert_any_call(first_uuid)
|
|
mock_get_user.assert_any_call(second_uuid)
|
|
|
|
@patch("apps.reports.services.data_sources.get_user_detailed_info")
|
|
@patch("apps.reports.services.data_sources.list_users")
|
|
def test_fetch_users_skips_user_whose_details_call_fails(self, mock_list_users, mock_get_user):
|
|
ok_uuid, broken_uuid = str(uuid4()), str(uuid4())
|
|
mock_list_users.return_value = {
|
|
"results": [{"uuid": broken_uuid}, {"uuid": ok_uuid}],
|
|
"next": None,
|
|
}
|
|
def get_user(uuid):
|
|
if uuid == broken_uuid:
|
|
raise Exception("boom")
|
|
return MagicMock(uuid=uuid)
|
|
|
|
mock_get_user.side_effect = get_user
|
|
|
|
users = ReportDataSources().fetch_users({})
|
|
|
|
self.assertEqual([user.uuid for user in users], [ok_uuid])
|
|
|
|
|
|
class GenerateReportTests(APITestCase):
|
|
def _build_ticket(self, user_uuid=None):
|
|
ticket = MagicMock()
|
|
ticket.uuid = uuid4()
|
|
ticket.user = user_uuid or uuid4()
|
|
ticket.title = "Login issue"
|
|
ticket.description = "Cannot login"
|
|
ticket.state = TicketStateEnum(10)
|
|
ticket.created_at = datetime(2026, 5, 20, 10, 0)
|
|
ticket.updated_at = datetime(2026, 5, 21, 10, 0)
|
|
return ticket
|
|
|
|
def _build_user(self, user_uuid):
|
|
user = MagicMock()
|
|
user.uuid = user_uuid
|
|
user.username = "alice"
|
|
user.first_name = "Alice"
|
|
user.last_name = "Doe"
|
|
user.email = "alice@example.com"
|
|
user.phone_number = "09120000000"
|
|
return user
|
|
|
|
@patch("apps.reports.services.data_sources.ReportDataSources.fetch_user_metrics")
|
|
@patch("apps.reports.services.data_sources.ReportDataSources.fetch_users")
|
|
def test_generate_user_report_is_public_and_returns_excel(
|
|
self,
|
|
mock_fetch_users,
|
|
mock_fetch_user_metrics,
|
|
):
|
|
user = self._build_user(uuid4())
|
|
mock_fetch_users.return_value = [user]
|
|
mock_fetch_user_metrics.return_value = {str(user.uuid): {"ticket_count": 3}}
|
|
|
|
response = self.client.post(
|
|
"/api/reports/generate/",
|
|
{
|
|
"report_type": "user",
|
|
"filters": {"status": "active"},
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(
|
|
response["Content-Type"],
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
)
|
|
self.assertEqual(
|
|
response["Content-Disposition"],
|
|
'attachment; filename="user-report.xlsx"',
|
|
)
|
|
|
|
workbook = load_workbook(BytesIO(response.content))
|
|
worksheet = workbook.active
|
|
self.assertEqual(worksheet["A1"].value, "User Report")
|
|
self.assertEqual(
|
|
[cell.value for cell in worksheet[2]],
|
|
[
|
|
"User UUID",
|
|
"Username",
|
|
"First Name",
|
|
"Last Name",
|
|
"Email",
|
|
"Phone Number",
|
|
"Ticket Count",
|
|
],
|
|
)
|
|
self.assertEqual(
|
|
[cell.value for cell in worksheet[3]],
|
|
[
|
|
str(user.uuid),
|
|
"alice",
|
|
"Alice",
|
|
"Doe",
|
|
"alice@example.com",
|
|
"09120000000",
|
|
3,
|
|
],
|
|
)
|
|
mock_fetch_users.assert_called_once_with({"status": "active"})
|
|
|
|
@patch("apps.reports.services.data_sources.ReportDataSources.fetch_ticket_metrics")
|
|
@patch("apps.reports.services.data_sources.ReportDataSources.fetch_tickets")
|
|
def test_generate_tickets_report_uses_static_columns(
|
|
self,
|
|
mock_fetch_tickets,
|
|
mock_fetch_ticket_metrics,
|
|
):
|
|
ticket = self._build_ticket()
|
|
mock_fetch_tickets.return_value = [ticket]
|
|
mock_fetch_ticket_metrics.return_value = {}
|
|
|
|
response = self.client.post(
|
|
"/api/reports/generate/",
|
|
{"report_type": "tickets"},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(
|
|
response["Content-Disposition"],
|
|
'attachment; filename="tickets-report.xlsx"',
|
|
)
|
|
|
|
workbook = load_workbook(BytesIO(response.content))
|
|
worksheet = workbook.active
|
|
self.assertEqual(worksheet["A1"].value, "Tickets Report")
|
|
self.assertEqual(
|
|
[cell.value for cell in worksheet[2]],
|
|
[
|
|
"Ticket UUID",
|
|
"User UUID",
|
|
"Title",
|
|
"Description",
|
|
"State",
|
|
"Created At",
|
|
"Updated At",
|
|
],
|
|
)
|
|
self.assertEqual(
|
|
[cell.value for cell in worksheet[3]],
|
|
[
|
|
str(ticket.uuid),
|
|
str(ticket.user),
|
|
"Login issue",
|
|
"Cannot login",
|
|
"CLOSED",
|
|
"2026-05-20 10:00:00",
|
|
"2026-05-21 10:00:00",
|
|
],
|
|
)
|
|
|
|
def test_unknown_report_type_returns_validation_error(self):
|
|
response = self.client.post(
|
|
"/api/reports/generate/",
|
|
{"report_type": "unknown"},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|