Add issue resolution confirmation workflow
Magent CI/CD / verify (push) Successful in 10m35s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 12s

This commit is contained in:
2026-09-01 11:40:43 +12:00
parent 393b8c2a88
commit 2adbed7259
14 changed files with 1155 additions and 9 deletions
+118 -1
View File
@@ -23,6 +23,7 @@ from backend.app.routers import site as site_router
from backend.app.routers import status as status_router
from backend.app.security import PASSWORD_POLICY_MESSAGE, validate_password_policy
from backend.app.services import password_reset
from backend.app.services import issue_resolution
from backend.app.services.operation_progress import (
begin_operation,
finish_operation,
@@ -1290,16 +1291,29 @@ class MediaReplacementTests(unittest.IsolatedAsyncioTestCase):
patch.object(requests_router, "build_snapshot", new=AsyncMock(return_value=snapshot)),
patch.object(requests_router, "RadarrClient", return_value=radarr),
patch.object(requests_router, "save_action"),
patch.object(
requests_router,
"get_portal_item",
return_value={
"id": 12,
"kind": "issue",
"external_ref": "/requests/3914",
"created_by_username": "admin",
},
),
patch.object(requests_router, "add_portal_item_activity") as add_activity,
):
result = await requests_router.action_replace_media(
"3914",
{"file_id": 77, "confirmed": True},
{"file_id": 77, "confirmed": True, "issue_id": 12},
{"username": "admin", "role": "admin", "auto_search_enabled": True},
)
self.assertEqual(result["status"], "ok")
radarr.delete_movie_file.assert_awaited_once_with(77)
radarr.search.assert_awaited_once_with(44)
add_activity.assert_called_once()
self.assertEqual(add_activity.call_args.kwargs["event_type"], "replacement_started")
async def test_tv_replacement_options_return_only_safe_file_details(self) -> None:
snapshot = Snapshot(
@@ -1460,3 +1474,106 @@ class PortalWorkflowTests(TempDatabaseMixin, unittest.TestCase):
)
self.assertEqual(len(processing), 1)
self.assertEqual(pending_count, 1)
class IssueResolutionWorkflowTests(TempDatabaseMixin, unittest.IsolatedAsyncioTestCase):
def _create_issue(self, *, status: str = "in_progress") -> dict:
return db.create_portal_item(
kind="issue",
title="Missing content: Test movie",
description="The title is missing.",
created_by_username="reporter",
created_by_id=None,
status=status,
issue_type="missing_content",
)
async def test_marking_issue_fixed_records_contact_and_confirmation(self) -> None:
issue = self._create_issue()
reporter = {"username": "reporter", "email": "reporter@example.com"}
with (
patch.object(issue_resolution, "_workflow_settings", return_value=(2, 3, "days")),
patch.object(issue_resolution, "get_user_by_username", return_value=reporter),
patch.object(issue_resolution, "resolve_user_delivery_email", return_value="reporter@example.com"),
patch.object(issue_resolution, "send_generic_email", new=AsyncMock(return_value=None)) as send_email,
):
waiting = await issue_resolution.begin_issue_confirmation(
int(issue["id"]),
actor_username="admin",
actor_role="admin",
)
self.assertEqual(waiting["status"], "awaiting_confirmation")
state = issue_resolution.issue_resolution_state(waiting)
self.assertEqual(state["attemptsSent"], 1)
self.assertEqual(state["maximumAttempts"], 2)
send_email.assert_awaited_once()
activity = db.list_portal_item_activity(int(issue["id"]))
self.assertEqual(
[event["event_type"] for event in activity],
["resolution_proposed", "confirmation_email_sent"],
)
closed = issue_resolution.respond_to_issue_confirmation(
int(issue["id"]),
resolved=True,
actor_username="reporter",
actor_role="user",
)
self.assertEqual(closed["status"], "closed")
self.assertIsNotNone(closed["issue_resolved_at"])
self.assertEqual(db.list_portal_item_activity(int(issue["id"]))[-1]["event_type"], "resolution_confirmed")
async def test_zero_confirmation_emails_closes_issue_immediately(self) -> None:
issue = self._create_issue()
with patch.object(issue_resolution, "_workflow_settings", return_value=(0, 1, "days")):
closed = await issue_resolution.begin_issue_confirmation(
int(issue["id"]),
actor_username="admin",
actor_role="admin",
)
self.assertEqual(closed["status"], "closed")
self.assertEqual(
[event["event_type"] for event in db.list_portal_item_activity(int(issue["id"]))],
["resolution_proposed", "issue_auto_closed"],
)
async def test_saving_waiting_issue_does_not_restart_confirmation_cycle(self) -> None:
issue = self._create_issue(status="awaiting_confirmation")
user = {"username": "admin", "role": "admin"}
with patch.object(portal_router, "begin_issue_confirmation", new=AsyncMock()) as begin_confirmation:
result = await portal_router.portal_update_item(
int(issue["id"]),
{"title": "Updated title", "status": "awaiting_confirmation"},
user,
)
self.assertEqual(result["item"]["title"], "Updated title")
begin_confirmation.assert_not_awaited()
def test_public_activity_hides_internal_notes_and_admin_identity(self) -> None:
issue = self._create_issue()
db.add_portal_item_activity(
int(issue["id"]),
event_type="internal_note_added",
actor_username="private-admin-name",
actor_role="admin",
message="Internal diagnostic detail",
)
db.add_portal_item_activity(
int(issue["id"]),
event_type="status_changed",
actor_username="private-admin-name",
actor_role="admin",
message="Status changed to in progress.",
metadata_json='{"private":true}',
)
public_activity = portal_router._activity_payload(issue)
self.assertNotIn("Internal diagnostic detail", str(public_activity))
self.assertNotIn("private-admin-name", str(public_activity))
self.assertNotIn("metadata_json", public_activity[-1])
self.assertEqual(public_activity[-1]["actor_username"], "Support team")