Skip to content

Commit 5369a5a

Browse files
authored
feat: add tags to conversations on removal (#16933)
1 parent e04b7bb commit 5369a5a

File tree

5 files changed

+137
-6
lines changed

5 files changed

+137
-6
lines changed

tests/unit/admin/views/test_malware_reports.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,13 @@ def test_malware_reports_project_verdict_remove_malware(self, db_request):
135135
owner_user = UserFactory.create(is_frozen=False)
136136
project = ProjectFactory.create()
137137
RoleFactory(user=owner_user, project=project, role_name="Owner")
138-
report = ProjectObservationFactory.create(kind="is_malware", related=project)
138+
report = ProjectObservationFactory.create(
139+
kind="is_malware",
140+
related=project,
141+
additional={
142+
"helpscout_conversation_url": "https://example.com/conversation/123"
143+
},
144+
)
139145

140146
db_request.POST["confirm_project_name"] = project.name
141147
db_request.route_path = lambda a: "/admin/malware_reports/"
@@ -236,7 +242,13 @@ def test_detail_remove_malware_for_project(self, db_request):
236242
owner_user = UserFactory.create(is_frozen=False)
237243
project = ProjectFactory.create()
238244
RoleFactory(user=owner_user, project=project, role_name="Owner")
239-
report = ProjectObservationFactory.create(kind="is_malware", related=project)
245+
report = ProjectObservationFactory.create(
246+
kind="is_malware",
247+
related=project,
248+
additional={
249+
"helpscout_conversation_url": "https://example.com/conversation/123"
250+
},
251+
)
240252

241253
db_request.matchdict["observation_id"] = str(report.id)
242254
db_request.POST["confirm_project_name"] = project.name
@@ -254,8 +266,7 @@ def test_detail_remove_malware_for_project(self, db_request):
254266
assert db_request.session.flash.calls == [
255267
pretend.call(f"Deleted the project '{project.name}'", queue="success"),
256268
pretend.call(
257-
f"Malware Project {report.related.name} removed.\n"
258-
"Please update related Help Scout conversations.",
269+
f"Malware Project {report.related.name} removed and report updated",
259270
queue="success",
260271
),
261272
]

tests/unit/helpdesk/test_services.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
# See the License for the specific language governing permissions and
1111
# limitations under the License.
1212

13+
import http
14+
1315
from textwrap import dedent
1416

1517
import pretend
@@ -125,3 +127,70 @@ def test_create_conversation(self):
125127

126128
assert resp == "https://api.helpscout.net/v2/conversations/123"
127129
assert len(responses.calls) == 1
130+
131+
@responses.activate
132+
def test_add_tag(self):
133+
responses.get(
134+
"https://api.helpscout.net/v2/conversations/123",
135+
headers={"Content-Type": "application/hal+json"},
136+
json={"tags": [{"id": 9150, "color": "#929499", "tag": "existing_tag"}]},
137+
)
138+
responses.put(
139+
"https://api.helpscout.net/v2/conversations/123/tags",
140+
match=[
141+
responses.matchers.json_params_matcher(
142+
{"tags": ["existing_tag", "added_tag"]}
143+
)
144+
],
145+
status=http.HTTPStatus.NO_CONTENT,
146+
)
147+
148+
service = HelpScoutService(
149+
session=requests.Session(),
150+
bearer_token="fake token",
151+
mailbox_id="12345",
152+
)
153+
154+
service.add_tag(
155+
conversation_url="https://api.helpscout.net/v2/conversations/123",
156+
tag="added_tag",
157+
)
158+
159+
assert len(responses.calls) == 2
160+
# GET call
161+
get_call = responses.calls[0]
162+
assert get_call.request.url == "https://api.helpscout.net/v2/conversations/123"
163+
assert get_call.request.headers["Authorization"] == "Bearer fake token"
164+
assert get_call.response.json() == {
165+
"tags": [{"id": 9150, "color": "#929499", "tag": "existing_tag"}]
166+
}
167+
# PUT call
168+
put_call = responses.calls[1]
169+
assert (
170+
put_call.request.url
171+
== "https://api.helpscout.net/v2/conversations/123/tags"
172+
)
173+
assert put_call.request.headers["Authorization"] == "Bearer fake token"
174+
assert put_call.response.status_code == http.HTTPStatus.NO_CONTENT
175+
176+
@responses.activate
177+
def test_add_tag_with_duplicate(self):
178+
responses.get(
179+
"https://api.helpscout.net/v2/conversations/123",
180+
headers={"Content-Type": "application/hal+json"},
181+
json={"tags": [{"id": 9150, "color": "#929499", "tag": "existing_tag"}]},
182+
)
183+
184+
service = HelpScoutService(
185+
session=requests.Session(),
186+
bearer_token="fake token",
187+
mailbox_id="12345",
188+
)
189+
190+
service.add_tag(
191+
conversation_url="https://api.helpscout.net/v2/conversations/123",
192+
tag="existing_tag",
193+
)
194+
195+
# No PUT call should be made
196+
assert len(responses.calls) == 1

warehouse/admin/views/malware_reports.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from pyramid.view import view_config
2121

2222
from warehouse.authnz import Permissions
23+
from warehouse.helpdesk.interfaces import IHelpDeskService
2324
from warehouse.observations.models import Observation
2425
from warehouse.utils.project import (
2526
confirm_project,
@@ -180,6 +181,13 @@ def malware_reports_project_verdict_remove_malware(project, request):
180181

181182
# prohibit the project
182183
prohibit_and_remove_project(project, request, comment="malware")
184+
# tell helpdesk to add a tag to all related conversations
185+
helpdesk_service = request.find_service(IHelpDeskService)
186+
for observation in observations:
187+
helpdesk_service.add_tag(
188+
conversation_url=observation.additional["helpscout_conversation_url"],
189+
tag="removed_as_malware_via_admin",
190+
)
183191

184192
request.session.flash(
185193
f"Malware Project {project.name} removed.\n"
@@ -293,9 +301,14 @@ def remove_malware_for_project(request):
293301

294302
prohibit_and_remove_project(project, request, comment="malware")
295303

304+
helpdesk_service = request.find_service(IHelpDeskService)
305+
helpdesk_service.add_tag(
306+
conversation_url=observation.additional["helpscout_conversation_url"],
307+
tag="removed_as_malware_via_admin",
308+
)
309+
296310
request.session.flash(
297-
f"Malware Project {project.name} removed.\n"
298-
"Please update related Help Scout conversations.",
311+
f"Malware Project {project.name} removed and report updated",
299312
queue="success",
300313
)
301314
return HTTPSeeOther(request.route_path("admin.malware_reports.list"))

warehouse/helpdesk/interfaces.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,8 @@ def create_conversation(*, request_json: dict) -> str:
2525
"""
2626
Create a new conversation in the helpdesk service.
2727
"""
28+
29+
def add_tag(*, conversation_url: str, tag: str) -> None:
30+
"""
31+
Add a tag to a conversation.
32+
"""

warehouse/helpdesk/services.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ def create_conversation(self, *, request_json: dict) -> str:
5050
print(dedent(pprint.pformat(request_json)))
5151
return "localhost"
5252

53+
def add_tag(self, *, conversation_url: str, tag: str) -> None:
54+
print(f"Adding tag '{tag}' to conversation '{conversation_url}'")
55+
return
56+
5357

5458
@implementer(IHelpDeskService)
5559
class HelpScoutService:
@@ -120,3 +124,32 @@ def create_conversation(self, *, request_json: dict) -> str:
120124
resp.raise_for_status()
121125
# return the API-friendly location of the conversation
122126
return resp.headers["Location"]
127+
128+
def add_tag(self, *, conversation_url: str, tag: str) -> None:
129+
"""
130+
Add a tag to a conversation in HelpScout
131+
https://developer.helpscout.com/mailbox-api/endpoints/conversations/tags/update/
132+
"""
133+
# Get existing tags and append new one
134+
resp = self.http.get(
135+
conversation_url, headers={"Authorization": f"Bearer {self.bearer_token}"}
136+
)
137+
resp.raise_for_status()
138+
139+
# collect tag strings from response
140+
tags = [tag["tag"] for tag in resp.json()["tags"]]
141+
142+
if tag in tags:
143+
# tag already exists, no need to add it
144+
return
145+
146+
tags.append(tag)
147+
148+
resp = self.http.put(
149+
f"{conversation_url}/tags",
150+
headers={"Authorization": f"Bearer {self.bearer_token}"},
151+
json={"tags": tags},
152+
timeout=10,
153+
)
154+
resp.raise_for_status()
155+
return

0 commit comments

Comments
 (0)