test_rrsets_bulk.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. import copy
  2. from django.conf import settings
  3. from rest_framework import status
  4. from desecapi.tests.base import AuthenticatedRRSetBaseTestCase
  5. class AuthenticatedRRSetBulkTestCase(AuthenticatedRRSetBaseTestCase):
  6. @classmethod
  7. def setUpTestDataWithPdns(cls):
  8. super().setUpTestDataWithPdns()
  9. cls.data = [
  10. {'subname': 'my-bulk', 'records': ['1.2.3.4'], 'ttl': 3600, 'type': 'A'},
  11. {'subname': 'my-bulk', 'records': ['desec.io.', 'foobar.example.'], 'ttl': 3600, 'type': 'PTR'},
  12. ]
  13. cls.data_no_records = copy.deepcopy(cls.data)
  14. cls.data_no_records[1].pop('records')
  15. cls.data_empty_records = copy.deepcopy(cls.data)
  16. cls.data_empty_records[1]['records'] = []
  17. cls.data_no_subname = copy.deepcopy(cls.data)
  18. cls.data_no_subname[0].pop('subname')
  19. cls.data_no_ttl = copy.deepcopy(cls.data)
  20. cls.data_no_ttl[0].pop('ttl')
  21. cls.data_no_type = copy.deepcopy(cls.data)
  22. cls.data_no_type[1].pop('type')
  23. cls.data_no_records_no_ttl = copy.deepcopy(cls.data_no_records)
  24. cls.data_no_records_no_ttl[1].pop('ttl')
  25. cls.data_no_subname_empty_records = copy.deepcopy(cls.data_no_subname)
  26. cls.data_no_subname_empty_records[0]['records'] = []
  27. cls.bulk_domain = cls.create_domain(owner=cls.owner)
  28. for data in cls.data:
  29. cls.create_rr_set(cls.bulk_domain, **data)
  30. def test_bulk_post_my_rr_sets(self):
  31. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_empty_domain.name)):
  32. response = self.client.bulk_post_rr_sets(domain_name=self.my_empty_domain.name, payload=self.data)
  33. self.assertStatus(response, status.HTTP_201_CREATED)
  34. response = self.client.get_rr_sets(self.my_empty_domain.name)
  35. self.assertStatus(response, status.HTTP_200_OK)
  36. self.assertRRSetsCount(response.data, self.data)
  37. # Check subname requirement on bulk endpoint (and uniqueness at the same time)
  38. self.assertResponse(
  39. self.client.bulk_post_rr_sets(domain_name=self.my_empty_domain.name, payload=self.data_no_subname),
  40. status.HTTP_400_BAD_REQUEST,
  41. [
  42. {'subname': ['This field is required.']},
  43. {'non_field_errors': ['Another RRset with the same subdomain and type exists for this domain.']}
  44. ]
  45. )
  46. def test_bulk_post_rr_sets_empty_records(self):
  47. expected_response_data = [copy.deepcopy(self.data_empty_records[0]), None]
  48. expected_response_data[0]['domain'] = self.my_empty_domain.name
  49. expected_response_data[0]['name'] = '%s.%s.' % (self.data_empty_records[0]['subname'],
  50. self.my_empty_domain.name)
  51. self.assertResponse(
  52. self.client.bulk_post_rr_sets(domain_name=self.my_empty_domain.name, payload=self.data_empty_records),
  53. status.HTTP_400_BAD_REQUEST,
  54. [
  55. {},
  56. {'records': ['This field must not be empty when using POST.']}
  57. ]
  58. )
  59. def test_bulk_post_existing_rrsets(self):
  60. self.assertResponse(
  61. self.client.bulk_post_rr_sets(
  62. domain_name=self.bulk_domain,
  63. payload=self.data,
  64. ),
  65. status.HTTP_400_BAD_REQUEST,
  66. 2 * [{
  67. 'non_field_errors': ['Another RRset with the same subdomain and type exists for this domain.']
  68. }]
  69. )
  70. def test_bulk_post_duplicates(self):
  71. data = 2 * [self.data[0]] + [self.data[1]]
  72. self.assertResponse(
  73. self.client.bulk_post_rr_sets(domain_name=self.my_empty_domain.name, payload=data),
  74. status.HTTP_400_BAD_REQUEST,
  75. [
  76. {'non_field_errors': ['Same subname and type as in position(s) 1, but must be unique.']},
  77. {'non_field_errors': ['Same subname and type as in position(s) 0, but must be unique.']},
  78. {},
  79. ]
  80. )
  81. data = 2 * [self.data[0]] + [self.data[1]] + [self.data[0]]
  82. self.assertResponse(
  83. self.client.bulk_post_rr_sets(domain_name=self.my_empty_domain.name, payload=data),
  84. status.HTTP_400_BAD_REQUEST,
  85. [
  86. {'non_field_errors': ['Same subname and type as in position(s) 1, 3, but must be unique.']},
  87. {'non_field_errors': ['Same subname and type as in position(s) 0, 3, but must be unique.']},
  88. {},
  89. {'non_field_errors': ['Same subname and type as in position(s) 0, 1, but must be unique.']},
  90. ]
  91. )
  92. def test_bulk_post_missing_fields(self):
  93. self.assertResponse(
  94. self.client.bulk_post_rr_sets(
  95. domain_name=self.my_empty_domain.name,
  96. payload=[
  97. {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 3622},
  98. {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
  99. {'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
  100. {'subname': '', 'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
  101. {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
  102. {'subname': 'd.1', 'ttl': 3650, 'type': 'AAAA'},
  103. {'subname': 'd.1', 'ttl': 3650, 'type': 'SOA',
  104. 'records': ['set.an.example. get.desec.io. 2018034419 10800 3600 604800 60']},
  105. {'subname': 'd.1', 'ttl': 3650, 'type': 'OPT', 'records': ['9999']},
  106. {'subname': 'd.1', 'ttl': 3650, 'type': 'TYPE099', 'records': ['v=spf1 mx -all']},
  107. ]
  108. ),
  109. status.HTTP_400_BAD_REQUEST,
  110. [
  111. {'type': ['This field is required.']},
  112. {'ttl': [f'Ensure this value is greater than or equal to {settings.MINIMUM_TTL_DEFAULT}.']},
  113. {'subname': ['This field is required.']},
  114. {},
  115. {'ttl': ['This field is required.']},
  116. {'records': ['This field is required.']},
  117. {'type': ['You cannot tinker with the SOA RR set. It is managed automatically.']},
  118. {'type': ['You cannot tinker with the OPT RR set. It is managed automatically.']},
  119. {'type': ['Generic type format is not supported.']},
  120. ]
  121. )
  122. def test_bulk_post_accepts_empty_list(self):
  123. self.assertResponse(
  124. self.client.bulk_post_rr_sets(domain_name=self.my_empty_domain.name, payload=[]),
  125. status.HTTP_201_CREATED,
  126. )
  127. def test_bulk_patch_fresh_rrsets_need_records(self):
  128. response = self.client.bulk_patch_rr_sets(self.my_empty_domain.name, payload=self.data_no_records)
  129. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  130. self.assertEqual(response.data, [{}, {'records': ['This field is required.']}])
  131. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(self.my_empty_domain.name)):
  132. response = self.client.bulk_patch_rr_sets(self.my_empty_domain.name, payload=self.data_empty_records)
  133. self.assertStatus(response, status.HTTP_200_OK)
  134. def test_bulk_patch_fresh_rrsets_need_subname(self):
  135. response = self.client.bulk_patch_rr_sets(domain_name=self.my_empty_domain.name, payload=self.data_no_subname)
  136. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  137. def test_bulk_patch_fresh_rrsets_need_ttl(self):
  138. response = self.client.bulk_patch_rr_sets(domain_name=self.my_empty_domain.name, payload=self.data_no_ttl)
  139. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  140. self.assertEqual(response.data, [{'ttl': ['This field is required.']}, {}])
  141. def test_bulk_patch_fresh_rrsets_need_type(self):
  142. response = self.client.bulk_patch_rr_sets(domain_name=self.my_empty_domain.name, payload=self.data_no_type)
  143. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  144. self.assertEqual(response.data, [{}, {'type': ['This field is required.']}])
  145. def test_bulk_patch_does_not_accept_single_objects(self):
  146. response = self.client.bulk_patch_rr_sets(domain_name=self.my_empty_domain.name, payload=self.data[0])
  147. self.assertContains(response, 'Expected a list of items but got dict.', status_code=status.HTTP_400_BAD_REQUEST)
  148. def test_bulk_patch_does_accept_empty_list(self):
  149. response = self.client.bulk_patch_rr_sets(domain_name=self.my_empty_domain.name, payload=[])
  150. self.assertStatus(response, status.HTTP_200_OK)
  151. response = self.client.bulk_patch_rr_sets(domain_name=self.my_rr_set_domain.name, payload=[])
  152. self.assertStatus(response, status.HTTP_200_OK)
  153. def test_bulk_patch_does_not_accept_empty_payload(self):
  154. response = self.client.bulk_patch_rr_sets(domain_name=self.my_empty_domain.name, payload=None)
  155. self.assertContains(response, 'No data provided', status_code=status.HTTP_400_BAD_REQUEST)
  156. def test_bulk_patch_full_on_empty_domain(self):
  157. # Full patch always works
  158. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_empty_domain.name)):
  159. response = self.client.bulk_patch_rr_sets(domain_name=self.my_empty_domain.name, payload=self.data)
  160. self.assertStatus(response, status.HTTP_200_OK)
  161. # Check that RRsets have been created
  162. response = self.client.get_rr_sets(self.my_empty_domain.name)
  163. self.assertStatus(response, status.HTTP_200_OK)
  164. self.assertRRSetsCount(response.data, self.data)
  165. def test_bulk_patch_change_records(self):
  166. data_no_ttl = copy.deepcopy(self.data_no_ttl)
  167. data_no_ttl[0]['records'] = ['4.3.2.1', '8.8.1.2']
  168. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.bulk_domain.name)):
  169. response = self.client.bulk_patch_rr_sets(domain_name=self.bulk_domain.name, payload=data_no_ttl)
  170. self.assertStatus(response, status.HTTP_200_OK)
  171. response = self.client.get_rr_sets(self.bulk_domain.name)
  172. self.assertStatus(response, status.HTTP_200_OK)
  173. self.assertRRSetsCount(response.data, data_no_ttl)
  174. def test_bulk_patch_change_ttl(self):
  175. data_no_records = copy.deepcopy(self.data_no_records)
  176. data_no_records[1]['ttl'] = 3911
  177. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.bulk_domain.name)):
  178. response = self.client.bulk_patch_rr_sets(domain_name=self.bulk_domain.name, payload=data_no_records)
  179. self.assertStatus(response, status.HTTP_200_OK)
  180. response = self.client.get_rr_sets(self.bulk_domain.name)
  181. self.assertStatus(response, status.HTTP_200_OK)
  182. self.assertRRSetsCount(response.data, data_no_records)
  183. def test_bulk_patch_does_not_need_ttl(self):
  184. self.assertResponse(
  185. self.client.bulk_patch_rr_sets(domain_name=self.bulk_domain.name, payload=self.data_no_ttl),
  186. status.HTTP_200_OK,
  187. )
  188. def test_bulk_patch_delete_non_existing_rr_sets(self):
  189. self.assertResponse(
  190. self.client.bulk_patch_rr_sets(
  191. domain_name=self.my_empty_domain.name,
  192. payload=[
  193. {'subname': 'a', 'type': 'A', 'records': [], 'ttl': 3622},
  194. {'subname': 'b', 'type': 'AAAA', 'records': []},
  195. ]),
  196. status.HTTP_200_OK,
  197. [],
  198. )
  199. def test_bulk_patch_missing_invalid_fields_1(self):
  200. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(self.my_empty_domain.name)):
  201. self.client.bulk_post_rr_sets(
  202. domain_name=self.my_empty_domain.name,
  203. payload=[
  204. {'subname': '', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
  205. {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA', 'ttl': 3603},
  206. {'subname': 'd.1', 'ttl': 3650, 'type': 'AAAA', 'records': ['::1', '::2']},
  207. ]
  208. )
  209. self.assertResponse(
  210. self.client.bulk_patch_rr_sets(
  211. domain_name=self.my_empty_domain.name,
  212. payload=[
  213. {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 3622},
  214. {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
  215. {'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
  216. {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
  217. {'subname': 'd.1', 'ttl': 3650, 'type': 'AAAA'},
  218. ]),
  219. status.HTTP_400_BAD_REQUEST,
  220. [
  221. {'type': ['This field is required.']},
  222. {'ttl': [f'Ensure this value is greater than or equal to {settings.MINIMUM_TTL_DEFAULT}.']},
  223. {'subname': ['This field is required.']},
  224. {},
  225. {},
  226. ]
  227. )
  228. def test_bulk_patch_missing_invalid_fields_2(self):
  229. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(self.my_empty_domain.name)):
  230. self.client.bulk_post_rr_sets(
  231. domain_name=self.my_empty_domain.name,
  232. payload=[
  233. {'subname': '', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']}
  234. ]
  235. )
  236. self.assertResponse(
  237. self.client.bulk_patch_rr_sets(
  238. domain_name=self.my_empty_domain.name,
  239. payload=[
  240. {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 3622},
  241. {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
  242. {'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
  243. {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
  244. {'subname': 'd.1', 'ttl': 3650, 'type': 'AAAA'},
  245. ]),
  246. status.HTTP_400_BAD_REQUEST,
  247. [
  248. {'type': ['This field is required.']},
  249. {'ttl': [f'Ensure this value is greater than or equal to {settings.MINIMUM_TTL_DEFAULT}.']},
  250. {'subname': ['This field is required.']},
  251. {'ttl': ['This field is required.']},
  252. {'records': ['This field is required.']},
  253. ]
  254. )
  255. def test_bulk_put_partial(self):
  256. # Need all fields
  257. for domain in [self.my_empty_domain, self.bulk_domain]:
  258. response = self.client.bulk_put_rr_sets(domain_name=domain.name, payload=self.data_no_records)
  259. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  260. self.assertEqual(response.data, [{}, {'records': ['This field is required.']}])
  261. response = self.client.bulk_put_rr_sets(domain_name=domain.name, payload=self.data_no_ttl)
  262. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  263. self.assertEqual(response.data, [{'ttl': ['This field is required.']}, {}])
  264. response = self.client.bulk_put_rr_sets(domain_name=domain.name, payload=self.data_no_records_no_ttl)
  265. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  266. self.assertEqual(response.data, [{},
  267. {'ttl': ['This field is required.'],
  268. 'records': ['This field is required.']}])
  269. response = self.client.bulk_put_rr_sets(domain_name=domain.name, payload=self.data_no_subname)
  270. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  271. self.assertEqual(response.data, [{'subname': ['This field is required.']}, {}])
  272. response = self.client.bulk_put_rr_sets(domain_name=domain.name, payload=self.data_no_subname_empty_records)
  273. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  274. self.assertEqual(response.data, [{'subname': ['This field is required.']}, {}])
  275. response = self.client.bulk_put_rr_sets(domain_name=domain.name, payload=self.data_no_type)
  276. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  277. self.assertEqual(response.data, [{}, {'type': ['This field is required.']}])
  278. def test_bulk_put_does_not_accept_single_objects(self):
  279. response = self.client.bulk_put_rr_sets(domain_name=self.my_empty_domain.name, payload=self.data[0])
  280. self.assertContains(response, 'Expected a list of items but got dict.', status_code=status.HTTP_400_BAD_REQUEST)
  281. def test_bulk_put_does_accept_empty_list(self):
  282. response = self.client.bulk_put_rr_sets(domain_name=self.my_empty_domain.name, payload=[])
  283. self.assertStatus(response, status.HTTP_200_OK)
  284. response = self.client.bulk_put_rr_sets(domain_name=self.my_rr_set_domain.name, payload=[])
  285. self.assertStatus(response, status.HTTP_200_OK)
  286. def test_bulk_put_does_not_accept_empty_payload(self):
  287. response = self.client.bulk_put_rr_sets(domain_name=self.my_empty_domain.name, payload=None)
  288. self.assertContains(response, 'No data provided', status_code=status.HTTP_400_BAD_REQUEST)
  289. def test_bulk_put_does_not_accept_list_of_crap(self):
  290. response = self.client.bulk_put_rr_sets(domain_name=self.my_empty_domain.name, payload=['bla'])
  291. self.assertContains(response, 'Expected a dictionary, but got str.', status_code=status.HTTP_400_BAD_REQUEST)
  292. response = self.client.bulk_put_rr_sets(domain_name=self.my_empty_domain.name, payload=[42])
  293. self.assertContains(response, 'Expected a dictionary, but got int.', status_code=status.HTTP_400_BAD_REQUEST)
  294. def test_bulk_put_full(self):
  295. # Full PUT always works
  296. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_empty_domain.name)):
  297. response = self.client.bulk_put_rr_sets(domain_name=self.my_empty_domain.name, payload=self.data)
  298. self.assertStatus(response, status.HTTP_200_OK)
  299. # Check that RRsets have been created
  300. response = self.client.get_rr_sets(self.my_empty_domain.name)
  301. self.assertStatus(response, status.HTTP_200_OK)
  302. self.assertRRSetsCount(response.data, self.data)
  303. # Do not expect any updates, but successful code when PUT'ing only existing RRsets
  304. response = self.client.bulk_put_rr_sets(domain_name=self.bulk_domain.name, payload=self.data)
  305. self.assertStatus(response, status.HTTP_200_OK)
  306. def test_bulk_put_invalid_records(self):
  307. for records in [
  308. 'asfd',
  309. ['1.1.1.1', '2.2.2.2', 123],
  310. ['1.2.3.4', None],
  311. [True, '1.1.1.1'],
  312. dict(foobar='foobar', asdf='asdf'),
  313. ]:
  314. payload = [{'subname': 'a.2', 'ttl': 3600, 'type': 'MX', 'records': records}]
  315. response = self.client.bulk_put_rr_sets(domain_name=self.my_empty_domain.name, payload=payload)
  316. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  317. self.assertTrue('records' in response.data[0])
  318. def test_bulk_put_empty_records(self):
  319. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.bulk_domain.name)):
  320. self.assertStatus(
  321. self.client.bulk_put_rr_sets(domain_name=self.bulk_domain.name, payload=self.data_empty_records),
  322. status.HTTP_200_OK
  323. )
  324. def test_bulk_duplicate_rrset(self):
  325. data = self.data + self.data
  326. for bulk_request_rr_sets in [
  327. self.client.bulk_patch_rr_sets,
  328. self.client.bulk_put_rr_sets,
  329. self.client.bulk_post_rr_sets,
  330. ]:
  331. response = bulk_request_rr_sets(domain_name=self.my_empty_domain.name, payload=data)
  332. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  333. def test_bulk_patch_or_post_failure_with_single_rrset(self):
  334. for method in [self.client.bulk_patch_rr_sets, self.client.bulk_put_rr_sets]:
  335. response = method(domain_name=self.my_empty_domain.name, payload=self.data[0])
  336. self.assertContains(response, 'Expected a list of items but got dict.',
  337. status_code=status.HTTP_400_BAD_REQUEST)
  338. def test_bulk_delete_rrsets(self):
  339. self.assertStatus(
  340. self.client.delete(
  341. self.reverse('v1:rrsets', name=self.my_empty_domain.name),
  342. data=None,
  343. ),
  344. status.HTTP_405_METHOD_NOT_ALLOWED,
  345. )