test_api.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. import uuid
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.urls import reverse
  4. from django.utils import timezone
  5. from django_rq import get_queue
  6. from django_rq.workers import get_worker
  7. from rest_framework import status
  8. from rq.job import Job as RQ_Job
  9. from rq.job import JobStatus
  10. from rq.registry import FailedJobRegistry, StartedJobRegistry
  11. from users.constants import TOKEN_PREFIX
  12. from users.models import Token
  13. from utilities.testing import APITestCase, APIViewTestCases, GraphQLQueryTest, TestCase
  14. from utilities.testing.mixins import RQQueueTestMixin
  15. from utilities.testing.utils import disable_logging
  16. from ..models import *
  17. class AppTestCase(APITestCase):
  18. def test_root(self):
  19. url = reverse('core-api:api-root')
  20. response = self.client.get('{}?format=api'.format(url), **self.header)
  21. self.assertEqual(response.status_code, 200)
  22. class DataSourceTestCase(APIViewTestCases.APIViewTestCase):
  23. model = DataSource
  24. brief_fields = ['description', 'display', 'id', 'name', 'url']
  25. bulk_update_data = {
  26. 'enabled': False,
  27. 'description': 'foo bar baz',
  28. }
  29. @classmethod
  30. def setUpTestData(cls):
  31. data_sources = (
  32. DataSource(
  33. name='Data Source 1', type='local', source_url='file:///var/tmp/source1/',
  34. parameters={
  35. 'sync_date': '2024-01-01',
  36. 'sync_datetime': '2024-01-01T12:30:00+00:00',
  37. 'sync_time': '12:30:00',
  38. },
  39. ),
  40. DataSource(name='Data Source 2', type='local', source_url='file:///var/tmp/source2/'),
  41. DataSource(name='Data Source 3', type='local', source_url='file:///var/tmp/source3/'),
  42. )
  43. DataSource.objects.bulk_create(data_sources)
  44. cls.graphql_query_tests = (
  45. GraphQLQueryTest(
  46. name='parameters_json_date_lookup',
  47. query=(
  48. '{ data_source_list(filters: {parameters: '
  49. '{path: "sync_date", lookup: {date_lookup: {exact: "2024-01-01"}}}}) '
  50. '{ id } }'
  51. ),
  52. assert_result=cls.assert_only_source_1,
  53. ),
  54. GraphQLQueryTest(
  55. name='parameters_json_datetime_lookup',
  56. query=(
  57. '{ data_source_list(filters: {parameters: '
  58. '{path: "sync_datetime", lookup: {datetime_lookup: {exact: "2024-01-01T12:30:00+00:00"}}}}) '
  59. '{ id } }'
  60. ),
  61. assert_result=cls.assert_only_source_1,
  62. ),
  63. GraphQLQueryTest(
  64. name='parameters_json_time_lookup',
  65. query=(
  66. '{ data_source_list(filters: {parameters: '
  67. '{path: "sync_time", lookup: {time_lookup: {exact: "12:30:00"}}}}) '
  68. '{ id } }'
  69. ),
  70. assert_result=cls.assert_only_source_1,
  71. ),
  72. )
  73. cls.create_data = [
  74. {
  75. 'name': 'Data Source 4',
  76. 'type': 'git',
  77. 'source_url': 'https://example.com/git/source4'
  78. },
  79. {
  80. 'name': 'Data Source 5',
  81. 'type': 'git',
  82. 'source_url': 'https://example.com/git/source5'
  83. },
  84. {
  85. 'name': 'Data Source 6',
  86. 'type': 'git',
  87. 'source_url': 'https://example.com/git/source6'
  88. },
  89. ]
  90. def assert_only_source_1(self, data):
  91. """The JSON lookup returns exactly the source carrying the matching value."""
  92. ids = sorted(result['id'] for result in data['data_source_list'])
  93. self.assertEqual(ids, [str(DataSource.objects.get(name='Data Source 1').pk)])
  94. class DataFileTestCase(
  95. APIViewTestCases.GetObjectViewTestCase,
  96. APIViewTestCases.ListObjectsViewTestCase,
  97. APIViewTestCases.GraphQLTestCase
  98. ):
  99. model = DataFile
  100. brief_fields = ['display', 'id', 'path', 'url']
  101. user_permissions = ('core.view_datasource', )
  102. @classmethod
  103. def setUpTestData(cls):
  104. datasource = DataSource.objects.create(
  105. name='Data Source 1',
  106. type='local',
  107. source_url='file:///var/tmp/source1/'
  108. )
  109. data_files = (
  110. DataFile(
  111. source=datasource,
  112. path='dir1/file1.txt',
  113. last_updated=timezone.now(),
  114. size=1000,
  115. hash='442da078f0111cbdf42f21903724f6597c692535f55bdfbbea758a1ae99ad9e1'
  116. ),
  117. DataFile(
  118. source=datasource,
  119. path='dir1/file2.txt',
  120. last_updated=timezone.now(),
  121. size=2000,
  122. hash='a78168c7c97115bafd96450ed03ea43acec495094c5caa28f0d02e20e3a76cc2'
  123. ),
  124. DataFile(
  125. source=datasource,
  126. path='dir1/file3.txt',
  127. last_updated=timezone.now(),
  128. size=3000,
  129. hash='12b8827a14c4d5a2f30b6c6e2b7983063988612391c6cbe8ee7493b59054827a'
  130. ),
  131. )
  132. DataFile.objects.bulk_create(data_files)
  133. class ObjectTypeTestCase(APITestCase):
  134. model = ObjectType
  135. def test_list_objects(self):
  136. object_type_count = ObjectType.objects.count()
  137. response = self.client.get(reverse('core-api:objecttype-list'), **self.header)
  138. self.assertHttpStatus(response, status.HTTP_200_OK)
  139. self.assertEqual(response.data['count'], object_type_count)
  140. def test_get_object(self):
  141. object_type = ObjectType.objects.first()
  142. url = reverse('core-api:objecttype-detail', kwargs={'pk': object_type.pk})
  143. self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_200_OK)
  144. class JobTestCase(
  145. APIViewTestCases.GetObjectViewTestCase,
  146. APIViewTestCases.ListObjectsViewTestCase,
  147. ):
  148. model = Job
  149. brief_fields = ['completed', 'created', 'status', 'url', 'user']
  150. @classmethod
  151. def setUpTestData(cls):
  152. datasource = DataSource.objects.create(
  153. name='Data Source 1',
  154. type='local',
  155. source_url='file:///var/tmp/source1/',
  156. )
  157. ct = ContentType.objects.get_for_model(DataSource)
  158. Job.objects.bulk_create(
  159. [
  160. Job(
  161. name='Job 1',
  162. object_type=ct,
  163. object_id=datasource.pk,
  164. status='pending',
  165. queue_name='default',
  166. job_id=uuid.uuid4(),
  167. ),
  168. Job(
  169. name='Job 2',
  170. object_type=ct,
  171. object_id=datasource.pk,
  172. status='running',
  173. queue_name='default',
  174. job_id=uuid.uuid4(),
  175. ),
  176. Job(
  177. name='Job 3',
  178. object_type=ct,
  179. object_id=datasource.pk,
  180. status='completed',
  181. queue_name='default',
  182. job_id=uuid.uuid4(),
  183. execution_time=timezone.timedelta(seconds=90),
  184. ),
  185. ]
  186. )
  187. def test_list_objects_by_execution_time(self):
  188. """The Job list endpoint supports filtering and ordering by execution_time."""
  189. self.add_permissions('core.view_job')
  190. url = reverse('core-api:job-list')
  191. # Filter: only the completed job has a (90s) execution_time
  192. response = self.client.get(f'{url}?execution_time__gte=60', **self.header)
  193. self.assertHttpStatus(response, status.HTTP_200_OK)
  194. self.assertEqual(response.data['count'], 1)
  195. # Ordering by execution_time should be accepted (NULLs sort to one end)
  196. response = self.client.get(f'{url}?ordering=execution_time', **self.header)
  197. self.assertHttpStatus(response, status.HTTP_200_OK)
  198. self.assertEqual(response.data['count'], 3)
  199. class BackgroundTaskTestCase(RQQueueTestMixin, TestCase):
  200. user_permissions = ()
  201. @staticmethod
  202. def dummy_job_default():
  203. return "Job finished"
  204. @staticmethod
  205. def dummy_job_failing():
  206. raise Exception("Job failed")
  207. def setUp(self):
  208. super().setUp()
  209. # The base TestCase creates self.user; make it active and create a token for API calls.
  210. self.user.is_active = True
  211. self.user.save()
  212. self.token = Token.objects.create(user=self.user)
  213. self.header = {'HTTP_AUTHORIZATION': f'Bearer {TOKEN_PREFIX}{self.token.key}.{self.token.token}'}
  214. def test_background_queue_list(self):
  215. url = reverse('core-api:rqqueue-list')
  216. # Attempt to load view without permission
  217. response = self.client.get(url, **self.header)
  218. self.assertEqual(response.status_code, 403)
  219. # Load view with permission
  220. self.user.is_superuser = True
  221. self.user.save()
  222. response = self.client.get(url, **self.header)
  223. self.assertEqual(response.status_code, 200)
  224. self.assertIn('default', str(response.content))
  225. self.assertIn('high', str(response.content))
  226. self.assertIn('low', str(response.content))
  227. def test_background_queue(self):
  228. url = reverse('core-api:rqqueue-detail', args=['default'])
  229. # Attempt to load view without permission
  230. response = self.client.get(url, **self.header)
  231. self.assertEqual(response.status_code, 403)
  232. # Load view with permission
  233. self.user.is_superuser = True
  234. self.user.save()
  235. response = self.client.get(url, **self.header)
  236. self.assertEqual(response.status_code, 200)
  237. self.assertIn('default', str(response.content))
  238. self.assertIn('oldest_job_timestamp', str(response.content))
  239. self.assertIn('scheduled_jobs', str(response.content))
  240. def test_background_task_list(self):
  241. queue = get_queue('default')
  242. queue.enqueue(self.dummy_job_default)
  243. url = reverse('core-api:rqtask-list')
  244. # Attempt to load view without permission
  245. response = self.client.get(url, **self.header)
  246. self.assertEqual(response.status_code, 403)
  247. # Load view with permission
  248. self.user.is_superuser = True
  249. self.user.save()
  250. response = self.client.get(url, **self.header)
  251. self.assertEqual(response.status_code, 200)
  252. self.assertIn('origin', str(response.content))
  253. self.assertIn('core.tests.test_api.BackgroundTaskTestCase.dummy_job_default()', str(response.content))
  254. def test_background_task(self):
  255. queue = get_queue('default')
  256. job = queue.enqueue(self.dummy_job_default)
  257. url = reverse('core-api:rqtask-detail', args=[job.id])
  258. # Attempt to load view without permission
  259. response = self.client.get(url, **self.header)
  260. self.assertEqual(response.status_code, 403)
  261. # Load view with permission
  262. self.user.is_superuser = True
  263. self.user.save()
  264. response = self.client.get(url, **self.header)
  265. self.assertEqual(response.status_code, 200)
  266. self.assertIn(str(job.id), str(response.content))
  267. self.assertIn('origin', str(response.content))
  268. self.assertIn('meta', str(response.content))
  269. self.assertIn('kwargs', str(response.content))
  270. def test_background_task_delete(self):
  271. queue = get_queue('default')
  272. job = queue.enqueue(self.dummy_job_default)
  273. url = reverse('core-api:rqtask-delete', args=[job.id])
  274. # Attempt to load view without permission
  275. response = self.client.get(url, **self.header)
  276. self.assertEqual(response.status_code, 403)
  277. # Load view with permission
  278. self.user.is_superuser = True
  279. self.user.save()
  280. response = self.client.post(url, **self.header)
  281. self.assertEqual(response.status_code, 200)
  282. self.assertFalse(RQ_Job.exists(job.id, connection=queue.connection))
  283. queue = get_queue('default')
  284. self.assertNotIn(job.id, queue.job_ids)
  285. def test_background_task_requeue(self):
  286. # Enqueue & run a job that will fail
  287. queue = get_queue('default')
  288. job = queue.enqueue(self.dummy_job_failing)
  289. with disable_logging():
  290. self.run_rq_jobs('default')
  291. self.assertTrue(job.is_failed)
  292. url = reverse('core-api:rqtask-requeue', args=[job.id])
  293. # Attempt to requeue the job without permission
  294. response = self.client.post(url, **self.header)
  295. self.assertEqual(response.status_code, 403)
  296. # Re-enqueue the failed job and check that its status has been reset
  297. self.user.is_superuser = True
  298. self.user.save()
  299. response = self.client.post(url, **self.header)
  300. self.assertEqual(response.status_code, 200)
  301. job = RQ_Job.fetch(job.id, queue.connection)
  302. self.assertFalse(job.is_failed)
  303. def test_background_task_enqueue(self):
  304. # Enqueue some jobs that each depends on its predecessor
  305. queue = get_queue('default')
  306. job = previous_job = None
  307. for _ in range(0, 3):
  308. job = queue.enqueue(self.dummy_job_default, depends_on=previous_job)
  309. previous_job = job
  310. url = reverse('core-api:rqtask-enqueue', args=[job.id])
  311. # Check that the last job to be enqueued has a status of deferred
  312. self.assertIsNotNone(job)
  313. self.assertEqual(job.get_status(), JobStatus.DEFERRED)
  314. self.assertIsNone(job.enqueued_at)
  315. # Attempt to force-enqueue the job without permission
  316. response = self.client.post(url, **self.header)
  317. self.assertEqual(response.status_code, 403)
  318. # Force-enqueue the deferred job
  319. self.user.is_superuser = True
  320. self.user.save()
  321. response = self.client.post(url, **self.header)
  322. self.assertEqual(response.status_code, 200)
  323. # Check that job's status is updated correctly
  324. job = queue.fetch_job(job.id)
  325. self.assertEqual(job.get_status(), JobStatus.QUEUED)
  326. self.assertIsNotNone(job.enqueued_at)
  327. def test_background_task_stop(self):
  328. queue = get_queue('default')
  329. worker = get_worker('default')
  330. job = queue.enqueue(self.dummy_job_default)
  331. worker.prepare_job_execution(job)
  332. url = reverse('core-api:rqtask-stop', args=[job.id])
  333. self.assertEqual(job.get_status(), JobStatus.STARTED)
  334. # Attempt to stop the task without permission
  335. response = self.client.post(url, **self.header)
  336. self.assertEqual(response.status_code, 403)
  337. # Stop the task
  338. self.user.is_superuser = True
  339. self.user.save()
  340. response = self.client.post(url, **self.header)
  341. self.assertEqual(response.status_code, 200)
  342. with disable_logging():
  343. worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
  344. started_job_registry = StartedJobRegistry(queue.name, connection=queue.connection)
  345. self.assertEqual(len(started_job_registry), 0)
  346. # Verify that the task was cancelled
  347. canceled_job_registry = FailedJobRegistry(queue.name, connection=queue.connection)
  348. self.assertEqual(len(canceled_job_registry), 1)
  349. self.assertIn(job.id, canceled_job_registry)
  350. def test_worker_list(self):
  351. worker1 = get_worker('default', name=uuid.uuid4().hex)
  352. worker1.register_birth()
  353. worker2 = get_worker('high')
  354. worker2.register_birth()
  355. url = reverse('core-api:rqworker-list')
  356. # Attempt to fetch the worker list without permission
  357. response = self.client.get(url, **self.header)
  358. self.assertEqual(response.status_code, 403)
  359. # Fetch the worker list
  360. self.user.is_superuser = True
  361. self.user.save()
  362. response = self.client.get(url, **self.header)
  363. self.assertEqual(response.status_code, 200)
  364. self.assertIn(str(worker1.name), str(response.content))
  365. def test_worker(self):
  366. worker1 = get_worker('default', name=uuid.uuid4().hex)
  367. worker1.register_birth()
  368. url = reverse('core-api:rqworker-detail', args=[worker1.name])
  369. # Attempt to fetch a worker without permission
  370. response = self.client.get(url, **self.header)
  371. self.assertEqual(response.status_code, 403)
  372. # Fetch the worker
  373. self.user.is_superuser = True
  374. self.user.save()
  375. response = self.client.get(url, **self.header)
  376. self.assertEqual(response.status_code, 200)
  377. self.assertIn(str(worker1.name), str(response.content))
  378. self.assertIn('birth_date', str(response.content))
  379. self.assertIn('total_working_time', str(response.content))