test_plugin.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. from __future__ import annotations
  2. from functools import cached_property
  3. from typing import Any
  4. import orjson
  5. import responses
  6. from django.contrib.auth.models import AnonymousUser
  7. from django.test import RequestFactory
  8. from django.urls import reverse
  9. from sentry.testutils.cases import TestCase
  10. from sentry_plugins.jira.plugin import JiraPlugin
  11. create_meta_response = {
  12. "expand": "projects",
  13. "projects": [
  14. {
  15. "expand": "issuetypes",
  16. "self": "https://getsentry.atlassian.net/rest/api/2/project/10000",
  17. "id": "10000",
  18. "key": "SEN",
  19. "name": "Sentry",
  20. "avatarUrls": {
  21. "48x48": "https://getsentry.atlassian.net/secure/projectavatar?avatarId=10324",
  22. "24x24": "https://getsentry.atlassian.net/secure/projectavatar?size=small&avatarId=10324",
  23. "16x16": "https://getsentry.atlassian.net/secure/projectavatar?size=xsmall&avatarId=10324",
  24. "32x32": "https://getsentry.atlassian.net/secure/projectavatar?size=medium&avatarId=10324",
  25. },
  26. "issuetypes": [
  27. {
  28. "self": "https://getsentry.atlassian.net/rest/api/2/issuetype/10002",
  29. "id": "10002",
  30. "description": "A task that needs to be done.",
  31. "iconUrl": "https://getsentry.atlassian.net/secure/viewavatar?size=xsmall&avatarId=10318&avatarType=issuetype",
  32. "name": "Task",
  33. "subtask": False,
  34. "expand": "fields",
  35. "fields": {
  36. "summary": {
  37. "required": True,
  38. "schema": {"type": "string", "system": "summary"},
  39. "name": "Summary",
  40. "hasDefaultValue": False,
  41. "operations": ["set"],
  42. },
  43. "issuetype": {
  44. "required": True,
  45. "schema": {"type": "issuetype", "system": "issuetype"},
  46. "name": "Issue Type",
  47. "hasDefaultValue": False,
  48. "operations": [],
  49. "allowedValues": [
  50. {
  51. "self": "https://getsentry.atlassian.net/rest/api/2/issuetype/10002",
  52. "id": "10002",
  53. "description": "A task that needs to be done.",
  54. "iconUrl": "https://getsentry.atlassian.net/secure/viewavatar?size=xsmall&avatarId=10318&avatarType=issuetype",
  55. "name": "Task",
  56. "subtask": False,
  57. "avatarId": 10318,
  58. }
  59. ],
  60. },
  61. "components": {
  62. "required": False,
  63. "schema": {
  64. "type": "array",
  65. "items": "component",
  66. "system": "components",
  67. },
  68. "name": "Component/s",
  69. "hasDefaultValue": False,
  70. "operations": ["add", "set", "remove"],
  71. "allowedValues": [],
  72. },
  73. "description": {
  74. "required": False,
  75. "schema": {"type": "string", "system": "description"},
  76. "name": "Description",
  77. "hasDefaultValue": False,
  78. "operations": ["set"],
  79. },
  80. "project": {
  81. "required": True,
  82. "schema": {"type": "project", "system": "project"},
  83. "name": "Project",
  84. "hasDefaultValue": False,
  85. "operations": ["set"],
  86. "allowedValues": [
  87. {
  88. "self": "https://getsentry.atlassian.net/rest/api/2/project/10000",
  89. "id": "10000",
  90. "key": "SEN",
  91. "name": "Sentry",
  92. "avatarUrls": {
  93. "48x48": "https://getsentry.atlassian.net/secure/projectavatar?avatarId=10324",
  94. "24x24": "https://getsentry.atlassian.net/secure/projectavatar?size=small&avatarId=10324",
  95. "16x16": "https://getsentry.atlassian.net/secure/projectavatar?size=xsmall&avatarId=10324",
  96. "32x32": "https://getsentry.atlassian.net/secure/projectavatar?size=medium&avatarId=10324",
  97. },
  98. }
  99. ],
  100. },
  101. "reporter": {
  102. "required": True,
  103. "schema": {"type": "user", "system": "reporter"},
  104. "name": "Reporter",
  105. "autoCompleteUrl": "https://getsentry.atlassian.net/rest/api/latest/user/search?username=",
  106. "hasDefaultValue": False,
  107. "operations": ["set"],
  108. },
  109. "fixVersions": {
  110. "required": False,
  111. "schema": {
  112. "type": "array",
  113. "items": "version",
  114. "system": "fixVersions",
  115. },
  116. "name": "Fix Version/s",
  117. "hasDefaultValue": False,
  118. "operations": ["set", "add", "remove"],
  119. "allowedValues": [],
  120. },
  121. "priority": {
  122. "required": False,
  123. "schema": {"type": "priority", "system": "priority"},
  124. "name": "Priority",
  125. "hasDefaultValue": True,
  126. "operations": ["set"],
  127. "allowedValues": [
  128. {
  129. "self": "https://getsentry.atlassian.net/rest/api/2/priority/1",
  130. "iconUrl": "https://getsentry.atlassian.net/images/icons/priorities/highest.svg",
  131. "name": "Highest",
  132. "id": "1",
  133. }
  134. ],
  135. },
  136. "customfield_10003": {
  137. "required": False,
  138. "schema": {
  139. "type": "array",
  140. "items": "string",
  141. "custom": "com.pyxis.greenhopper.jira:gh-sprint",
  142. "customId": 10003,
  143. },
  144. "name": "Sprint",
  145. "hasDefaultValue": False,
  146. "operations": ["set"],
  147. },
  148. "labels": {
  149. "required": False,
  150. "schema": {"type": "array", "items": "string", "system": "labels"},
  151. "name": "Labels",
  152. "autoCompleteUrl": "https://getsentry.atlassian.net/rest/api/1.0/labels/suggest?query=",
  153. "hasDefaultValue": False,
  154. "operations": ["add", "set", "remove"],
  155. },
  156. "attachment": {
  157. "required": False,
  158. "schema": {
  159. "type": "array",
  160. "items": "attachment",
  161. "system": "attachment",
  162. },
  163. "name": "Attachment",
  164. "hasDefaultValue": False,
  165. "operations": [],
  166. },
  167. "assignee": {
  168. "required": False,
  169. "schema": {"type": "user", "system": "assignee"},
  170. "name": "Assignee",
  171. "autoCompleteUrl": "https://getsentry.atlassian.net/rest/api/latest/user/assignable/search?issueKey=null&username=",
  172. "hasDefaultValue": False,
  173. "operations": ["set"],
  174. },
  175. },
  176. }
  177. ],
  178. }
  179. ],
  180. }
  181. issue_response: dict[str, Any] = {
  182. "key": "SEN-19",
  183. "id": "10708",
  184. "fields": {"summary": "TypeError: 'set' object has no attribute '__getitem__'"},
  185. }
  186. user_search_response: list[dict[str, Any]] = [
  187. {
  188. "self": "https://getsentry.atlassian.net/rest/api/2/user?username=userexample",
  189. "key": "JIRAUSER10100",
  190. "name": "userexample",
  191. "emailAddress": "user@example.com",
  192. "avatarUrls": {
  193. "48x48": "http://www.example.com/jira/secure/useravatar?size=large&ownerId=andrew",
  194. "24x24": "http://www.example.com/jira/secure/useravatar?size=small&ownerId=andrew",
  195. "16x16": "http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=andrew",
  196. "32x32": "http://www.example.com/jira/secure/useravatar?size=medium&ownerId=andrew",
  197. },
  198. "displayName": "User Example",
  199. "active": True,
  200. "deleted": False,
  201. "timeZone": "Europe/Vienna",
  202. "locale": "en_US",
  203. "lastLoginTime": "2024-06-24T12:15:00+0200",
  204. }
  205. ]
  206. class JiraPluginTest(TestCase):
  207. @cached_property
  208. def plugin(self):
  209. return JiraPlugin()
  210. @cached_property
  211. def request(self):
  212. return RequestFactory()
  213. def test_conf_key(self):
  214. assert self.plugin.conf_key == "jira"
  215. def test_get_issue_label(self):
  216. group = self.create_group(message="Hello world", culprit="foo.bar")
  217. assert self.plugin.get_issue_label(group, "SEN-1") == "SEN-1"
  218. def test_get_issue_url(self):
  219. self.plugin.set_option("instance_url", "https://getsentry.atlassian.net", self.project)
  220. group = self.create_group(message="Hello world", culprit="foo.bar")
  221. assert (
  222. self.plugin.get_issue_url(group, "SEN-1")
  223. == "https://getsentry.atlassian.net/browse/SEN-1"
  224. )
  225. def test_is_configured(self):
  226. assert self.plugin.is_configured(self.project) is False
  227. self.plugin.set_option("default_project", "SEN", self.project)
  228. assert self.plugin.is_configured(self.project) is True
  229. @responses.activate
  230. def test_create_issue(self):
  231. responses.add(
  232. responses.GET,
  233. "https://getsentry.atlassian.net/rest/api/2/issue/createmeta",
  234. json=create_meta_response,
  235. )
  236. responses.add(
  237. responses.POST,
  238. "https://getsentry.atlassian.net/rest/api/2/issue",
  239. json={"key": "SEN-1"},
  240. )
  241. self.plugin.set_option("instance_url", "https://getsentry.atlassian.net", self.project)
  242. group = self.create_group(message="Hello world", culprit="foo.bar")
  243. request = self.request.get("/")
  244. request.user = AnonymousUser()
  245. form_data = {
  246. "title": "Hello",
  247. "description": "Fix this.",
  248. "issuetype": "bug",
  249. "project": "SEN",
  250. }
  251. assert self.plugin.create_issue(request, group, form_data) == "SEN-1"
  252. @responses.activate
  253. def test_link_issue(self):
  254. responses.add(
  255. responses.GET,
  256. "https://getsentry.atlassian.net/rest/api/2/issue/SEN-19",
  257. json=issue_response,
  258. )
  259. self.plugin.set_option("instance_url", "https://getsentry.atlassian.net", self.project)
  260. group = self.create_group(message="Hello world", culprit="foo.bar")
  261. request = self.request.get("/")
  262. request.user = AnonymousUser()
  263. form_data = {"issue_id": "SEN-19"}
  264. assert (
  265. self.plugin.link_issue(request, group, form_data)["title"]
  266. == issue_response["fields"]["summary"]
  267. )
  268. def test_no_secrets(self):
  269. self.user = self.create_user("foo@example.com")
  270. self.org = self.create_organization(owner=self.user, name="Rowdy Tiger")
  271. self.team = self.create_team(organization=self.org, name="Mariachi Band")
  272. self.project = self.create_project(organization=self.org, teams=[self.team], name="Bengal")
  273. self.login_as(self.user)
  274. self.plugin.set_option("password", "abcdef", self.project)
  275. url = reverse(
  276. "sentry-api-0-project-plugin-details", args=[self.org.slug, self.project.slug, "jira"]
  277. )
  278. res = self.client.get(url)
  279. config = orjson.loads(res.content)["config"]
  280. password_config = [item for item in config if item["name"] == "password"][0]
  281. assert password_config.get("type") == "secret"
  282. assert password_config.get("value") is None
  283. assert password_config.get("hasSavedValue") is True
  284. assert password_config.get("prefix") == ""
  285. def test_get_formatted_user(self):
  286. assert self.plugin._get_formatted_user(
  287. {"displayName": "Foo Bar", "emailAddress": "foo@sentry.io", "name": "foobar"}
  288. ) == {"text": "Foo Bar - foo@sentry.io (foobar)", "id": "foobar"}
  289. # test weird addon users that don't have email addresses
  290. assert self.plugin._get_formatted_user(
  291. {
  292. "name": "robot",
  293. "avatarUrls": {
  294. "16x16": "https://avatar-cdn.atlassian.com/someid",
  295. "24x24": "https://avatar-cdn.atlassian.com/someotherid",
  296. },
  297. "self": "https://something.atlassian.net/rest/api/2/user?username=someaddon",
  298. }
  299. ) == {"id": "robot", "text": "robot (robot)"}
  300. def _setup_autocomplete_jira(self):
  301. self.plugin.set_option("instance_url", "https://getsentry.atlassian.net", self.project)
  302. self.plugin.set_option("default_project", "SEN", self.project)
  303. self.login_as(user=self.user)
  304. self.group = self.create_group(message="Hello world", culprit="foo.bar")
  305. @responses.activate
  306. def test_autocomplete_issue_id(self):
  307. self._setup_autocomplete_jira()
  308. responses.add(
  309. responses.GET,
  310. "https://getsentry.atlassian.net/rest/api/2/search/",
  311. json={"issues": [issue_response]},
  312. )
  313. url = f"/api/0/issues/{self.group.id}/plugins/jira/autocomplete/?autocomplete_query=SEN&autocomplete_field=issue_id"
  314. response = self.client.get(url)
  315. assert response.json() == {
  316. "issue_id": [
  317. {
  318. "text": "(SEN-19) TypeError: 'set' object has no attribute '__getitem__'",
  319. "id": "SEN-19",
  320. }
  321. ]
  322. }
  323. @responses.activate
  324. def test_autocomplete_jira_url_reporter(self):
  325. self._setup_autocomplete_jira()
  326. responses.add(
  327. responses.GET,
  328. "https://getsentry.atlassian.net/rest/api/2/user/search/?username=user&project=SEN",
  329. json=user_search_response,
  330. )
  331. url = f"/api/0/issues/{self.group.id}/plugins/jira/autocomplete/?autocomplete_query=user&autocomplete_field=reporter&jira_url=https://getsentry.atlassian.net/rest/api/2/user/search/"
  332. response = self.client.get(url)
  333. assert response.json() == {
  334. "reporter": [
  335. {"id": "userexample", "text": "User Example - user@example.com (userexample)"}
  336. ]
  337. }
  338. def test_autocomplete_jira_url_missing(self):
  339. self._setup_autocomplete_jira()
  340. url = f"/api/0/issues/{self.group.id}/plugins/jira/autocomplete/?autocomplete_query=SEN&autocomplete_field=reporter"
  341. response = self.client.get(url)
  342. assert response.json() == {
  343. "error_type": "validation",
  344. "errors": [{"jira_url": "missing required parameter"}],
  345. }
  346. def test_autocomplete_jira_url_mismatch(self):
  347. self._setup_autocomplete_jira()
  348. url = f"/api/0/issues/{self.group.id}/plugins/jira/autocomplete/?autocomplete_query=SEN&autocomplete_field=reporter&jira_url=https://eviljira.com/"
  349. response = self.client.get(url)
  350. assert response.json() == {
  351. "error_type": "validation",
  352. "errors": [{"jira_url": "domain must match"}],
  353. }