test_rrsets_bulk.py 18 KB

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