Add issue resolution confirmation workflow
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user