test_discover_saved_query_detail.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. import pytest
  2. from django.urls import NoReverseMatch, reverse
  3. from sentry.discover.models import (
  4. DiscoverSavedQuery,
  5. DiscoverSavedQueryProject,
  6. DiscoverSavedQueryTypes,
  7. )
  8. from sentry.testutils.cases import APITestCase, SnubaTestCase
  9. class DiscoverSavedQueryDetailTest(APITestCase, SnubaTestCase):
  10. feature_name = "organizations:discover"
  11. def setUp(self):
  12. super().setUp()
  13. self.login_as(user=self.user)
  14. self.org = self.create_organization(owner=self.user)
  15. self.org_without_access = self.create_organization()
  16. self.project_ids = [
  17. self.create_project(organization=self.org).id,
  18. self.create_project(organization=self.org).id,
  19. ]
  20. query = {"fields": ["test"], "conditions": [], "limit": 10}
  21. model = DiscoverSavedQuery.objects.create(
  22. organization=self.org, created_by_id=self.user.id, name="Test query", query=query
  23. )
  24. model.set_projects(self.project_ids)
  25. self.query_id = model.id
  26. invalid = DiscoverSavedQuery.objects.create(
  27. organization=self.org_without_access, name="Query without access", query=query
  28. )
  29. invalid.set_projects(self.project_ids)
  30. self.query_id_without_access = invalid.id
  31. def setup_no_team_user(self):
  32. # disable Open Membership
  33. self.org.flags.allow_joinleave = False
  34. self.org.save()
  35. # user has no access to the first project
  36. user_no_team = self.create_user(is_superuser=False)
  37. self.create_member(user=user_no_team, organization=self.org, role="member", teams=[])
  38. self.login_as(user_no_team)
  39. def test_invalid_id(self):
  40. with pytest.raises(NoReverseMatch):
  41. reverse("sentry-api-0-discover-saved-query-detail", args=[self.org.slug, "not-an-id"])
  42. def test_get(self):
  43. with self.feature(self.feature_name):
  44. url = reverse(
  45. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, self.query_id]
  46. )
  47. response = self.client.get(url)
  48. assert response.status_code == 200, response.content
  49. assert response.data["id"] == str(self.query_id)
  50. assert set(response.data["projects"]) == set(self.project_ids)
  51. assert response.data["fields"] == ["test"]
  52. assert response.data["conditions"] == []
  53. assert response.data["limit"] == 10
  54. def test_get_discover_query_flag(self):
  55. with self.feature("organizations:discover-query"):
  56. url = reverse(
  57. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, self.query_id]
  58. )
  59. response = self.client.get(url)
  60. assert response.status_code == 200, response.content
  61. assert response.data["id"] == str(self.query_id)
  62. assert set(response.data["projects"]) == set(self.project_ids)
  63. assert response.data["fields"] == ["test"]
  64. assert response.data["conditions"] == []
  65. assert response.data["limit"] == 10
  66. def test_get_version(self):
  67. query = {"fields": ["event_id"], "query": "event.type:error", "limit": 10, "version": 2}
  68. model = DiscoverSavedQuery.objects.create(
  69. organization=self.org, created_by_id=self.user.id, name="v2 query", query=query
  70. )
  71. model.set_projects(self.project_ids)
  72. with self.feature(self.feature_name):
  73. url = reverse(
  74. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, model.id]
  75. )
  76. response = self.client.get(url)
  77. assert response.status_code == 200, response.content
  78. assert response.data["id"] == str(model.id)
  79. assert set(response.data["projects"]) == set(self.project_ids)
  80. assert response.data["fields"] == ["event_id"]
  81. assert response.data["query"] == "event.type:error"
  82. assert response.data["limit"] == 10
  83. assert response.data["version"] == 2
  84. def test_get_org_without_access(self):
  85. with self.feature(self.feature_name):
  86. url = reverse(
  87. "sentry-api-0-discover-saved-query-detail",
  88. args=[self.org_without_access.slug, self.query_id],
  89. )
  90. response = self.client.get(url)
  91. assert response.status_code == 403, response.content
  92. def test_get_homepage_query(self):
  93. query = {"fields": ["event_id"], "query": "event.type:error", "limit": 10, "version": 2}
  94. model = DiscoverSavedQuery.objects.create(
  95. organization=self.org,
  96. created_by_id=self.user.id,
  97. name="v2 query",
  98. query=query,
  99. is_homepage=True,
  100. )
  101. model.set_projects(self.project_ids)
  102. with self.feature(self.feature_name):
  103. url = reverse(
  104. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, model.id]
  105. )
  106. response = self.client.get(url)
  107. assert response.status_code == 404, response.content
  108. def test_get_disallow_when_no_project_access(self):
  109. self.setup_no_team_user()
  110. with self.feature(self.feature_name):
  111. url = reverse(
  112. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, self.query_id]
  113. )
  114. response = self.client.get(url)
  115. assert response.status_code == 403, response.data
  116. assert response.data == {"detail": "You do not have permission to perform this action."}
  117. def test_put(self):
  118. with self.feature(self.feature_name):
  119. url = reverse(
  120. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, self.query_id]
  121. )
  122. response = self.client.put(
  123. url,
  124. {
  125. "name": "New query",
  126. "projects": self.project_ids,
  127. "fields": [],
  128. "range": "24h",
  129. "limit": 20,
  130. "conditions": [],
  131. "aggregations": [],
  132. "orderby": "-time",
  133. },
  134. )
  135. assert response.status_code == 200, response.content
  136. assert response.data["id"] == str(self.query_id)
  137. assert set(response.data["projects"]) == set(self.project_ids)
  138. assert response.data["fields"] == []
  139. assert response.data["conditions"] == []
  140. assert response.data["limit"] == 20
  141. def test_put_dataset(self):
  142. with self.feature(self.feature_name):
  143. url = reverse(
  144. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, self.query_id]
  145. )
  146. response = self.client.put(
  147. url,
  148. {
  149. "name": "New query",
  150. "projects": self.project_ids,
  151. "fields": [],
  152. "range": "24h",
  153. "limit": 20,
  154. "conditions": [],
  155. "aggregations": [],
  156. "orderby": "-time",
  157. "queryDataset": "transaction-like",
  158. },
  159. )
  160. assert response.status_code == 200, response.content
  161. assert response.data["id"] == str(self.query_id)
  162. assert set(response.data["projects"]) == set(self.project_ids)
  163. assert response.data["fields"] == []
  164. assert response.data["conditions"] == []
  165. assert response.data["limit"] == 20
  166. assert response.data["queryDataset"] == "transaction-like"
  167. def test_put_dataset_with_discover_dataset_returns_validation_error(self):
  168. with self.feature(self.feature_name):
  169. url = reverse(
  170. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, self.query_id]
  171. )
  172. with self.feature({"organizations:deprecate-discover-widget-type": True}):
  173. response = self.client.put(
  174. url,
  175. {
  176. "name": "New query",
  177. "projects": self.project_ids,
  178. "fields": [],
  179. "range": "24h",
  180. "limit": 20,
  181. "conditions": [],
  182. "aggregations": [],
  183. "orderby": "-time",
  184. "queryDataset": "discover",
  185. },
  186. )
  187. assert response.status_code == 400, response.content
  188. assert (
  189. "Attribute value `discover` is deprecated. Please use `error-events` or `transaction-like`"
  190. in response.content.decode()
  191. )
  192. def test_dataset_set_to_discover_on_update(self):
  193. query = {"fields": ["event_id"], "query": "event.type:error", "limit": 10, "version": 2}
  194. model = DiscoverSavedQuery.objects.create(
  195. organization=self.org,
  196. created_by_id=self.user.id,
  197. name="query",
  198. query=query,
  199. dataset=DiscoverSavedQueryTypes.TRANSACTION_LIKE,
  200. )
  201. with self.feature(self.feature_name):
  202. url = reverse(
  203. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, model.id]
  204. )
  205. response = self.client.put(
  206. url,
  207. {
  208. "name": "New query",
  209. "projects": self.project_ids,
  210. "fields": [],
  211. "range": "24h",
  212. "limit": 20,
  213. "conditions": [],
  214. "aggregations": [],
  215. "orderby": "-time",
  216. },
  217. )
  218. assert response.status_code == 200, response.content
  219. assert response.data["id"] == str(model.id)
  220. assert response.data["queryDataset"] == "error-events"
  221. assert response.data["datasetSource"] == "unknown"
  222. def test_put_with_interval(self):
  223. with self.feature(self.feature_name):
  224. url = reverse(
  225. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, self.query_id]
  226. )
  227. response = self.client.put(
  228. url,
  229. {
  230. "name": "New query",
  231. "projects": self.project_ids,
  232. "fields": ["transaction", "count()"],
  233. "range": "24h",
  234. "interval": "10m",
  235. "version": 2,
  236. "orderby": "-count",
  237. },
  238. )
  239. assert response.status_code == 200, response.content
  240. assert response.data["fields"] == ["transaction", "count()"]
  241. assert response.data["interval"] == "10m"
  242. def test_put_query_without_access(self):
  243. with self.feature(self.feature_name):
  244. url = reverse(
  245. "sentry-api-0-discover-saved-query-detail",
  246. args=[self.org.slug, self.query_id_without_access],
  247. )
  248. response = self.client.put(
  249. url, {"name": "New query", "projects": self.project_ids, "range": "24h"}
  250. )
  251. assert response.status_code == 404
  252. def test_put_query_with_team(self):
  253. team = self.create_team(organization=self.org, members=[self.user])
  254. project = self.create_project(organization=self.org, teams=[team])
  255. query = DiscoverSavedQuery.objects.create(
  256. organization=self.org,
  257. created_by_id=self.user.id,
  258. name="Test query",
  259. query={"fields": ["test"], "conditions": [], "limit": 10},
  260. )
  261. query.set_projects([project.id])
  262. with self.feature(self.feature_name):
  263. url = reverse(
  264. "sentry-api-0-discover-saved-query-detail",
  265. args=[self.org.slug, query.id],
  266. )
  267. response = self.client.put(url, {"name": "New query", "projects": [], "range": "24h"})
  268. assert response.status_code == 200
  269. def test_put_query_without_team(self):
  270. team = self.create_team(organization=self.org, members=[])
  271. project = self.create_project(organization=self.org, teams=[team])
  272. query = DiscoverSavedQuery.objects.create(
  273. organization=self.org,
  274. created_by_id=self.user.id,
  275. name="Test query",
  276. query={"fields": ["test"], "conditions": [], "limit": 10},
  277. )
  278. query.set_projects([project.id])
  279. with self.feature(self.feature_name):
  280. url = reverse(
  281. "sentry-api-0-discover-saved-query-detail",
  282. args=[self.org.slug, query.id],
  283. )
  284. response = self.client.put(url, {"name": "New query", "projects": [], "range": "24h"})
  285. assert response.status_code == 400
  286. assert "No Projects found, join a Team" == response.data["detail"]
  287. def test_put_homepage_query(self):
  288. query = {"fields": ["event_id"], "query": "event.type:error", "limit": 10, "version": 2}
  289. model = DiscoverSavedQuery.objects.create(
  290. organization=self.org,
  291. created_by_id=self.user.id,
  292. name="v2 query",
  293. query=query,
  294. is_homepage=True,
  295. )
  296. model.set_projects(self.project_ids)
  297. with self.feature(self.feature_name):
  298. url = reverse(
  299. "sentry-api-0-discover-saved-query-detail",
  300. args=[self.org.slug, model.id],
  301. )
  302. response = self.client.put(
  303. url, {"name": "New query", "projects": [], "range": "24h", "fields": []}
  304. )
  305. assert response.status_code == 404, response.content
  306. def test_put_org_without_access(self):
  307. with self.feature(self.feature_name):
  308. url = reverse(
  309. "sentry-api-0-discover-saved-query-detail",
  310. args=[self.org_without_access.slug, self.query_id],
  311. )
  312. response = self.client.put(
  313. url, {"name": "New query", "projects": self.project_ids, "range": "24h"}
  314. )
  315. assert response.status_code == 403, response.content
  316. def test_put_disallow_when_no_project_access(self):
  317. self.setup_no_team_user()
  318. with self.feature(self.feature_name):
  319. url = reverse(
  320. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, self.query_id]
  321. )
  322. response = self.client.put(
  323. url,
  324. {
  325. "name": "New query",
  326. "projects": self.project_ids,
  327. "fields": [],
  328. "range": "24h",
  329. "limit": 20,
  330. "conditions": [],
  331. "aggregations": [],
  332. "orderby": "-time",
  333. },
  334. )
  335. assert response.status_code == 403, response.data
  336. assert response.data == {"detail": "You do not have permission to perform this action."}
  337. def test_delete(self):
  338. with self.feature(self.feature_name):
  339. url = reverse(
  340. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, self.query_id]
  341. )
  342. response = self.client.delete(url)
  343. assert response.status_code == 204
  344. assert self.client.get(url).status_code == 404
  345. def test_delete_removes_projects(self):
  346. with self.feature(self.feature_name):
  347. url = reverse(
  348. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, self.query_id]
  349. )
  350. self.client.delete(url)
  351. projects = list(
  352. DiscoverSavedQueryProject.objects.filter(discover_saved_query=self.query_id)
  353. )
  354. assert projects == []
  355. def test_delete_query_without_access(self):
  356. with self.feature(self.feature_name):
  357. url = reverse(
  358. "sentry-api-0-discover-saved-query-detail",
  359. args=[self.org.slug, self.query_id_without_access],
  360. )
  361. response = self.client.delete(url)
  362. assert response.status_code == 404
  363. def test_delete_org_without_access(self):
  364. with self.feature(self.feature_name):
  365. url = reverse(
  366. "sentry-api-0-discover-saved-query-detail",
  367. args=[self.org_without_access.slug, self.query_id],
  368. )
  369. response = self.client.delete(url)
  370. assert response.status_code == 403, response.content
  371. def test_delete_homepage_query(self):
  372. query = {"fields": ["event_id"], "query": "event.type:error", "limit": 10, "version": 2}
  373. model = DiscoverSavedQuery.objects.create(
  374. organization=self.org,
  375. created_by_id=self.user.id,
  376. name="v2 query",
  377. query=query,
  378. is_homepage=True,
  379. )
  380. model.set_projects(self.project_ids)
  381. with self.feature(self.feature_name):
  382. url = reverse(
  383. "sentry-api-0-discover-saved-query-detail",
  384. args=[self.org.slug, model.id],
  385. )
  386. response = self.client.delete(url)
  387. assert response.status_code == 404, response.content
  388. def test_delete_disallow_when_no_project_access(self):
  389. self.setup_no_team_user()
  390. with self.feature(self.feature_name):
  391. url = reverse(
  392. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, self.query_id]
  393. )
  394. response = self.client.delete(url)
  395. assert response.status_code == 403, response.data
  396. assert response.data == {"detail": "You do not have permission to perform this action."}
  397. def test_disallow_delete_all_projects_savedquery_when_no_open_membership(self):
  398. self.setup_no_team_user()
  399. query = {"fields": ["event_id"], "query": "event.type:error", "limit": 10, "version": 2}
  400. model = DiscoverSavedQuery.objects.create(
  401. organization=self.org,
  402. created_by_id=self.user.id,
  403. name="v2 query",
  404. query=query,
  405. )
  406. assert not model.projects.exists()
  407. with self.feature(self.feature_name):
  408. url = reverse(
  409. "sentry-api-0-discover-saved-query-detail", args=[self.org.slug, model.id]
  410. )
  411. response = self.client.delete(url)
  412. assert response.status_code == 403, response.data
  413. assert response.data == {"detail": "You do not have permission to perform this action."}
  414. class OrganizationDiscoverQueryVisitTest(APITestCase, SnubaTestCase):
  415. def setUp(self):
  416. super().setUp()
  417. self.login_as(user=self.user)
  418. self.org = self.create_organization(owner=self.user)
  419. self.org_without_access = self.create_organization()
  420. self.project_ids = [
  421. self.create_project(organization=self.org).id,
  422. self.create_project(organization=self.org).id,
  423. ]
  424. q = {"fields": ["test"], "conditions": [], "limit": 10}
  425. self.query = DiscoverSavedQuery.objects.create(
  426. organization=self.org, created_by_id=self.user.id, name="Test query", query=q
  427. )
  428. self.query.set_projects(self.project_ids)
  429. def url(self, query_id):
  430. return reverse(
  431. "sentry-api-0-discover-saved-query-visit",
  432. kwargs={"organization_id_or_slug": self.org.slug, "query_id": query_id},
  433. )
  434. def test_visit_query(self):
  435. last_visited = self.query.last_visited
  436. assert last_visited is not None
  437. assert self.query.visits == 1
  438. with self.feature("organizations:discover-query"):
  439. response = self.client.post(self.url(self.query.id))
  440. assert response.status_code == 204
  441. query = DiscoverSavedQuery.objects.get(id=self.query.id)
  442. assert query.visits == 2
  443. assert query.last_visited is not None
  444. assert query.last_visited > last_visited
  445. def test_visit_query_no_access(self):
  446. last_visited = self.query.last_visited
  447. assert self.query.visits == 1
  448. with self.feature({"organizations:discover-query": False}):
  449. response = self.client.post(self.url(self.query.id))
  450. assert response.status_code == 404
  451. query = DiscoverSavedQuery.objects.get(id=self.query.id)
  452. assert query.visits == 1
  453. assert query.last_visited == last_visited