test_rrsets_bulk.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
  1. from contextlib import nullcontext
  2. import copy
  3. from django.conf import settings
  4. from rest_framework import status
  5. from desecapi.tests.base import AuthenticatedRRSetBaseTestCase
  6. class AuthenticatedRRSetBulkTestCase(AuthenticatedRRSetBaseTestCase):
  7. @classmethod
  8. def setUpTestDataWithPdns(cls):
  9. super().setUpTestDataWithPdns()
  10. cls.data = [
  11. {
  12. "subname": "my-cname",
  13. "records": ["example.com."],
  14. "ttl": 3600,
  15. "type": "CNAME",
  16. },
  17. {
  18. "subname": "my-bulk",
  19. "records": ["desec.io.", "foobar.example."],
  20. "ttl": 3600,
  21. "type": "PTR",
  22. },
  23. ]
  24. cls.data_no_records = copy.deepcopy(cls.data)
  25. cls.data_no_records[1].pop("records")
  26. cls.data_empty_records = copy.deepcopy(cls.data)
  27. cls.data_empty_records[1]["records"] = []
  28. cls.data_no_subname = copy.deepcopy(cls.data)
  29. cls.data_no_subname[0].pop("subname")
  30. cls.data_no_ttl = copy.deepcopy(cls.data)
  31. cls.data_no_ttl[0].pop("ttl")
  32. cls.data_no_type = copy.deepcopy(cls.data)
  33. cls.data_no_type[1].pop("type")
  34. cls.data_no_records_no_ttl = copy.deepcopy(cls.data_no_records)
  35. cls.data_no_records_no_ttl[1].pop("ttl")
  36. cls.data_no_subname_empty_records = copy.deepcopy(cls.data_no_subname)
  37. cls.data_no_subname_empty_records[0]["records"] = []
  38. cls.bulk_domain = cls.create_domain(owner=cls.owner)
  39. for data in cls.data:
  40. cls.create_rr_set(cls.bulk_domain, **data)
  41. def test_bulk_post_my_rr_sets(self):
  42. with self.assertRequests(
  43. self.requests_desec_rr_sets_update(name=self.my_empty_domain.name)
  44. ):
  45. response = self.client.bulk_post_rr_sets(
  46. domain_name=self.my_empty_domain.name, payload=self.data
  47. )
  48. self.assertStatus(response, status.HTTP_201_CREATED)
  49. response = self.client.get_rr_sets(self.my_empty_domain.name)
  50. self.assertStatus(response, status.HTTP_200_OK)
  51. self.assertRRSetsCount(response.data, self.data)
  52. # Check subname requirement on bulk endpoint (and uniqueness at the same time)
  53. self.assertResponse(
  54. self.client.bulk_post_rr_sets(
  55. domain_name=self.my_empty_domain.name, payload=self.data_no_subname
  56. ),
  57. status.HTTP_400_BAD_REQUEST,
  58. [
  59. {"subname": ["This field is required."]},
  60. {
  61. "non_field_errors": [
  62. "Another RRset with the same subdomain and type exists for this domain."
  63. ]
  64. },
  65. ],
  66. )
  67. def test_bulk_post_rr_sets_empty_records(self):
  68. expected_response_data = [copy.deepcopy(self.data_empty_records[0]), None]
  69. expected_response_data[0]["domain"] = self.my_empty_domain.name
  70. expected_response_data[0]["name"] = "%s.%s." % (
  71. self.data_empty_records[0]["subname"],
  72. self.my_empty_domain.name,
  73. )
  74. self.assertResponse(
  75. self.client.bulk_post_rr_sets(
  76. domain_name=self.my_empty_domain.name, payload=self.data_empty_records
  77. ),
  78. status.HTTP_400_BAD_REQUEST,
  79. [{}, {"records": ["This field must not be empty when using POST."]}],
  80. )
  81. def test_bulk_post_existing_rrsets(self):
  82. self.assertResponse(
  83. self.client.bulk_post_rr_sets(
  84. domain_name=self.bulk_domain,
  85. payload=self.data,
  86. ),
  87. status.HTTP_400_BAD_REQUEST,
  88. 2
  89. * [
  90. {
  91. "non_field_errors": [
  92. "Another RRset with the same subdomain and type exists for this domain."
  93. ]
  94. }
  95. ],
  96. )
  97. def test_bulk_post_duplicates(self):
  98. data = 2 * [self.data[0]] + [self.data[1]]
  99. self.assertResponse(
  100. self.client.bulk_post_rr_sets(
  101. domain_name=self.my_empty_domain.name, payload=data
  102. ),
  103. status.HTTP_400_BAD_REQUEST,
  104. [
  105. {
  106. "non_field_errors": [
  107. "Same subname and type as in position(s) 1, but must be unique."
  108. ]
  109. },
  110. {
  111. "non_field_errors": [
  112. "Same subname and type as in position(s) 0, but must be unique."
  113. ]
  114. },
  115. {},
  116. ],
  117. )
  118. data = 2 * [self.data[0]] + [self.data[1]] + [self.data[0]]
  119. self.assertResponse(
  120. self.client.bulk_post_rr_sets(
  121. domain_name=self.my_empty_domain.name, payload=data
  122. ),
  123. status.HTTP_400_BAD_REQUEST,
  124. [
  125. {
  126. "non_field_errors": [
  127. "Same subname and type as in position(s) 1, 3, but must be unique."
  128. ]
  129. },
  130. {
  131. "non_field_errors": [
  132. "Same subname and type as in position(s) 0, 3, but must be unique."
  133. ]
  134. },
  135. {},
  136. {
  137. "non_field_errors": [
  138. "Same subname and type as in position(s) 0, 1, but must be unique."
  139. ]
  140. },
  141. ],
  142. )
  143. def test_bulk_post_missing_fields(self):
  144. self.assertResponse(
  145. self.client.bulk_post_rr_sets(
  146. domain_name=self.my_empty_domain.name,
  147. payload=[
  148. {"subname": "a.1", "records": ["dead::beef"], "ttl": 3622},
  149. {
  150. "subname": "b.1",
  151. "ttl": -50,
  152. "type": "AAAA",
  153. "records": ["dead::beef"],
  154. },
  155. {"ttl": 3640, "type": "TXT", "records": ['"bar"']},
  156. {"subname": "", "ttl": 3640, "type": "TXT", "records": ['"bar"']},
  157. {"subname": "c.1", "records": ["dead::beef"], "type": "AAAA"},
  158. {"subname": "d.1", "ttl": 3650, "type": "AAAA"},
  159. {
  160. "subname": "d.1",
  161. "ttl": 3650,
  162. "type": "SOA",
  163. "records": [
  164. "get.desec.io. get.desec.io. 2018034419 10800 3600 604800 60"
  165. ],
  166. },
  167. {"subname": "d.1", "ttl": 3650, "type": "OPT", "records": ["9999"]},
  168. {
  169. "subname": "d.1",
  170. "ttl": 3650,
  171. "type": "TYPE099",
  172. "records": ["v=spf1 mx -all"],
  173. },
  174. ],
  175. ),
  176. status.HTTP_400_BAD_REQUEST,
  177. [
  178. {"type": ["This field is required."]},
  179. {
  180. "ttl": [
  181. f"Ensure this value is greater than or equal to {self.my_empty_domain.minimum_ttl}."
  182. ]
  183. },
  184. {"subname": ["This field is required."]},
  185. {},
  186. {"ttl": ["This field is required."]},
  187. {"records": ["This field is required."]},
  188. {
  189. "type": [
  190. "You cannot tinker with the SOA RR set. It is managed automatically."
  191. ]
  192. },
  193. {
  194. "type": [
  195. "You cannot tinker with the OPT RR set. It is managed automatically."
  196. ]
  197. },
  198. {"type": ["Generic type format is not supported."]},
  199. ],
  200. )
  201. def test_bulk_patch_cname_exclusivity(self):
  202. response = self.client.bulk_patch_rr_sets(
  203. domain_name=self.my_rr_set_domain.name,
  204. payload=[
  205. {"subname": "test", "type": "A", "ttl": 3600, "records": ["1.2.3.4"]},
  206. {
  207. "subname": "test",
  208. "type": "CNAME",
  209. "ttl": 3600,
  210. "records": ["example.com."],
  211. },
  212. ],
  213. )
  214. self.assertResponse(response, status.HTTP_400_BAD_REQUEST)
  215. self.assertEqual(
  216. response.json(),
  217. [
  218. {
  219. "non_field_errors": [
  220. "RRset with conflicting type present: 1 (CNAME). (No other RRsets are allowed alongside CNAME.)"
  221. ]
  222. },
  223. {
  224. "non_field_errors": [
  225. "RRset with conflicting type present: 0 (A), database (A, TXT). (No other RRsets are allowed alongside CNAME.)"
  226. ]
  227. },
  228. ],
  229. )
  230. def test_bulk_post_accepts_empty_list(self):
  231. self.assertResponse(
  232. self.client.bulk_post_rr_sets(
  233. domain_name=self.my_empty_domain.name, payload=[]
  234. ),
  235. status.HTTP_201_CREATED,
  236. )
  237. def test_bulk_patch_fresh_rrsets_need_records(self):
  238. response = self.client.bulk_patch_rr_sets(
  239. self.my_empty_domain.name, payload=self.data_no_records
  240. )
  241. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  242. self.assertEqual(response.data, [{}, {"records": ["This field is required."]}])
  243. with self.assertRequests(
  244. self.requests_desec_rr_sets_update(self.my_empty_domain.name)
  245. ):
  246. response = self.client.bulk_patch_rr_sets(
  247. self.my_empty_domain.name, payload=self.data_empty_records
  248. )
  249. self.assertStatus(response, status.HTTP_200_OK)
  250. def test_bulk_patch_fresh_rrsets_need_subname(self):
  251. response = self.client.bulk_patch_rr_sets(
  252. domain_name=self.my_empty_domain.name, payload=self.data_no_subname
  253. )
  254. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  255. def test_bulk_patch_fresh_rrsets_need_ttl(self):
  256. response = self.client.bulk_patch_rr_sets(
  257. domain_name=self.my_empty_domain.name, payload=self.data_no_ttl
  258. )
  259. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  260. self.assertEqual(response.data, [{"ttl": ["This field is required."]}, {}])
  261. def test_bulk_patch_fresh_rrsets_need_type(self):
  262. response = self.client.bulk_patch_rr_sets(
  263. domain_name=self.my_empty_domain.name, payload=self.data_no_type
  264. )
  265. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  266. self.assertEqual(response.data, [{}, {"type": ["This field is required."]}])
  267. def test_bulk_patch_does_not_accept_single_objects(self):
  268. response = self.client.bulk_patch_rr_sets(
  269. domain_name=self.my_empty_domain.name, payload=self.data[0]
  270. )
  271. self.assertContains(
  272. response,
  273. "Expected a list of items but got dict.",
  274. status_code=status.HTTP_400_BAD_REQUEST,
  275. )
  276. def test_bulk_patch_does_accept_empty_list(self):
  277. response = self.client.bulk_patch_rr_sets(
  278. domain_name=self.my_empty_domain.name, payload=[]
  279. )
  280. self.assertStatus(response, status.HTTP_200_OK)
  281. response = self.client.bulk_patch_rr_sets(
  282. domain_name=self.my_rr_set_domain.name, payload=[]
  283. )
  284. self.assertStatus(response, status.HTTP_200_OK)
  285. def test_bulk_patch_does_not_accept_empty_payload(self):
  286. response = self.client.bulk_patch_rr_sets(
  287. domain_name=self.my_empty_domain.name, payload=None
  288. )
  289. self.assertContains(
  290. response, "No data provided", status_code=status.HTTP_400_BAD_REQUEST
  291. )
  292. def test_bulk_patch_cname_exclusivity_atomic_rrset_replacement(self):
  293. self.create_rr_set(
  294. self.my_empty_domain,
  295. subname="test",
  296. type="A",
  297. records=["1.2.3.4"],
  298. ttl=3600,
  299. )
  300. with self.assertRequests(
  301. self.requests_desec_rr_sets_update(self.my_empty_domain.name)
  302. ):
  303. response = self.client.bulk_patch_rr_sets(
  304. domain_name=self.my_empty_domain.name,
  305. payload=[
  306. {
  307. "subname": "test",
  308. "type": "CNAME",
  309. "ttl": 3605,
  310. "records": ["example.com."],
  311. },
  312. {"subname": "test", "type": "A", "records": []},
  313. ],
  314. )
  315. self.assertResponse(response, status.HTTP_200_OK)
  316. self.assertEqual(len(response.data), 1)
  317. self.assertEqual(response.data[0]["type"], "CNAME")
  318. self.assertEqual(response.data[0]["records"], ["example.com."])
  319. self.assertEqual(response.data[0]["ttl"], 3605)
  320. with self.assertRequests(
  321. self.requests_desec_rr_sets_update(self.my_empty_domain.name)
  322. ):
  323. response = self.client.bulk_patch_rr_sets(
  324. domain_name=self.my_empty_domain.name,
  325. payload=[
  326. {"subname": "test", "type": "CNAME", "records": []},
  327. {
  328. "subname": "test",
  329. "type": "A",
  330. "ttl": 3600,
  331. "records": ["5.4.2.1"],
  332. },
  333. ],
  334. )
  335. self.assertResponse(response, status.HTTP_200_OK)
  336. self.assertEqual(len(response.data), 1)
  337. self.assertEqual(response.data[0]["type"], "A")
  338. self.assertEqual(response.data[0]["records"], ["5.4.2.1"])
  339. self.assertEqual(response.data[0]["ttl"], 3600)
  340. def test_bulk_patch_full_on_empty_domain(self):
  341. # Full patch always works
  342. with self.assertRequests(
  343. self.requests_desec_rr_sets_update(name=self.my_empty_domain.name)
  344. ):
  345. response = self.client.bulk_patch_rr_sets(
  346. domain_name=self.my_empty_domain.name, payload=self.data
  347. )
  348. self.assertStatus(response, status.HTTP_200_OK)
  349. # Check that RRsets have been created
  350. response = self.client.get_rr_sets(self.my_empty_domain.name)
  351. self.assertStatus(response, status.HTTP_200_OK)
  352. self.assertRRSetsCount(response.data, self.data)
  353. def test_bulk_patch_change_records(self):
  354. data_no_ttl = copy.deepcopy(self.data_no_ttl)
  355. data_no_ttl[0]["records"] = ["example.org."]
  356. with self.assertRequests(
  357. self.requests_desec_rr_sets_update(name=self.bulk_domain.name)
  358. ):
  359. response = self.client.bulk_patch_rr_sets(
  360. domain_name=self.bulk_domain.name, payload=data_no_ttl
  361. )
  362. self.assertStatus(response, status.HTTP_200_OK)
  363. response = self.client.get_rr_sets(self.bulk_domain.name)
  364. self.assertStatus(response, status.HTTP_200_OK)
  365. self.assertRRSetsCount(response.data, data_no_ttl)
  366. def test_bulk_patch_change_ttl(self):
  367. data_no_records = copy.deepcopy(self.data_no_records)
  368. data_no_records[1]["ttl"] = 3911
  369. with self.assertRequests(
  370. self.requests_desec_rr_sets_update(name=self.bulk_domain.name)
  371. ):
  372. response = self.client.bulk_patch_rr_sets(
  373. domain_name=self.bulk_domain.name, payload=data_no_records
  374. )
  375. self.assertStatus(response, status.HTTP_200_OK)
  376. response = self.client.get_rr_sets(self.bulk_domain.name)
  377. self.assertStatus(response, status.HTTP_200_OK)
  378. self.assertRRSetsCount(response.data, data_no_records)
  379. def test_bulk_patch_does_not_need_ttl(self):
  380. self.assertResponse(
  381. self.client.bulk_patch_rr_sets(
  382. domain_name=self.bulk_domain.name, payload=self.data_no_ttl
  383. ),
  384. status.HTTP_200_OK,
  385. )
  386. def test_bulk_patch_delete_non_existing_rr_sets(self):
  387. self.assertResponse(
  388. self.client.bulk_patch_rr_sets(
  389. domain_name=self.my_empty_domain.name,
  390. payload=[
  391. {"subname": "a", "type": "A", "records": [], "ttl": 3622},
  392. {"subname": "b", "type": "AAAA", "records": []},
  393. ],
  394. ),
  395. status.HTTP_200_OK,
  396. [],
  397. )
  398. def test_bulk_patch_missing_invalid_fields_1(self):
  399. with self.assertRequests(
  400. self.requests_desec_rr_sets_update(self.my_empty_domain.name)
  401. ):
  402. self.client.bulk_post_rr_sets(
  403. domain_name=self.my_empty_domain.name,
  404. payload=[
  405. {"subname": "", "ttl": 3650, "type": "TXT", "records": ['"foo"']},
  406. {
  407. "subname": "c.1",
  408. "records": ["dead::beef"],
  409. "type": "AAAA",
  410. "ttl": 3603,
  411. },
  412. {
  413. "subname": "d.1",
  414. "ttl": 3650,
  415. "type": "AAAA",
  416. "records": ["::1", "::2"],
  417. },
  418. ],
  419. )
  420. self.assertResponse(
  421. self.client.bulk_patch_rr_sets(
  422. domain_name=self.my_empty_domain.name,
  423. payload=[
  424. {"subname": "a.1", "records": ["dead::beef"], "ttl": 3622},
  425. {
  426. "subname": "b.1",
  427. "ttl": -50,
  428. "type": "AAAA",
  429. "records": ["dead::beef"],
  430. },
  431. {"ttl": 3640, "type": "TXT", "records": ['"bar"']},
  432. {"subname": "c.1", "records": ["dead::beef"], "type": "AAAA"},
  433. {"subname": "d.1", "ttl": 3650, "type": "AAAA"},
  434. ],
  435. ),
  436. status.HTTP_400_BAD_REQUEST,
  437. [
  438. {"type": ["This field is required."]},
  439. {
  440. "ttl": [
  441. f"Ensure this value is greater than or equal to {settings.MINIMUM_TTL_DEFAULT}."
  442. ]
  443. },
  444. {"subname": ["This field is required."]},
  445. {},
  446. {},
  447. ],
  448. )
  449. def test_bulk_patch_missing_invalid_fields_2(self):
  450. with self.assertRequests(
  451. self.requests_desec_rr_sets_update(self.my_empty_domain.name)
  452. ):
  453. self.client.bulk_post_rr_sets(
  454. domain_name=self.my_empty_domain.name,
  455. payload=[
  456. {"subname": "", "ttl": 3650, "type": "TXT", "records": ['"foo"']}
  457. ],
  458. )
  459. self.assertResponse(
  460. self.client.bulk_patch_rr_sets(
  461. domain_name=self.my_empty_domain.name,
  462. payload=[
  463. {"subname": "a.1", "records": ["dead::beef"], "ttl": 3622},
  464. {
  465. "subname": "b.1",
  466. "ttl": -50,
  467. "type": "AAAA",
  468. "records": ["dead::beef"],
  469. },
  470. {"ttl": 3640, "type": "TXT", "records": ['"bar"']},
  471. {"subname": "c.1", "records": ["dead::beef"], "type": "AAAA"},
  472. {"subname": "d.1", "ttl": 3650, "type": "AAAA"},
  473. ],
  474. ),
  475. status.HTTP_400_BAD_REQUEST,
  476. [
  477. {"type": ["This field is required."]},
  478. {
  479. "ttl": [
  480. f"Ensure this value is greater than or equal to {settings.MINIMUM_TTL_DEFAULT}."
  481. ]
  482. },
  483. {"subname": ["This field is required."]},
  484. {"ttl": ["This field is required."]},
  485. {"records": ["This field is required."]},
  486. ],
  487. )
  488. def test_bulk_put_partial(self):
  489. # Need all fields
  490. for domain in [self.my_empty_domain, self.bulk_domain]:
  491. response = self.client.bulk_put_rr_sets(
  492. domain_name=domain.name, payload=self.data_no_records
  493. )
  494. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  495. self.assertEqual(
  496. response.data, [{}, {"records": ["This field is required."]}]
  497. )
  498. response = self.client.bulk_put_rr_sets(
  499. domain_name=domain.name, payload=self.data_no_ttl
  500. )
  501. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  502. self.assertEqual(response.data, [{"ttl": ["This field is required."]}, {}])
  503. response = self.client.bulk_put_rr_sets(
  504. domain_name=domain.name, payload=self.data_no_records_no_ttl
  505. )
  506. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  507. self.assertEqual(
  508. response.data,
  509. [
  510. {},
  511. {
  512. "ttl": ["This field is required."],
  513. "records": ["This field is required."],
  514. },
  515. ],
  516. )
  517. response = self.client.bulk_put_rr_sets(
  518. domain_name=domain.name, payload=self.data_no_subname
  519. )
  520. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  521. self.assertEqual(
  522. response.data, [{"subname": ["This field is required."]}, {}]
  523. )
  524. response = self.client.bulk_put_rr_sets(
  525. domain_name=domain.name, payload=self.data_no_subname_empty_records
  526. )
  527. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  528. self.assertEqual(
  529. response.data, [{"subname": ["This field is required."]}, {}]
  530. )
  531. response = self.client.bulk_put_rr_sets(
  532. domain_name=domain.name, payload=self.data_no_type
  533. )
  534. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  535. self.assertEqual(response.data, [{}, {"type": ["This field is required."]}])
  536. def test_bulk_put_does_not_accept_single_objects(self):
  537. response = self.client.bulk_put_rr_sets(
  538. domain_name=self.my_empty_domain.name, payload=self.data[0]
  539. )
  540. self.assertContains(
  541. response,
  542. "Expected a list of items but got dict.",
  543. status_code=status.HTTP_400_BAD_REQUEST,
  544. )
  545. def test_bulk_put_does_accept_empty_list(self):
  546. response = self.client.bulk_put_rr_sets(
  547. domain_name=self.my_empty_domain.name, payload=[]
  548. )
  549. self.assertStatus(response, status.HTTP_200_OK)
  550. response = self.client.bulk_put_rr_sets(
  551. domain_name=self.my_rr_set_domain.name, payload=[]
  552. )
  553. self.assertStatus(response, status.HTTP_200_OK)
  554. def test_bulk_put_does_not_accept_empty_payload(self):
  555. response = self.client.bulk_put_rr_sets(
  556. domain_name=self.my_empty_domain.name, payload=None
  557. )
  558. self.assertContains(
  559. response, "No data provided", status_code=status.HTTP_400_BAD_REQUEST
  560. )
  561. def test_bulk_put_does_not_accept_list_of_crap(self):
  562. response = self.client.bulk_put_rr_sets(
  563. domain_name=self.my_empty_domain.name, payload=["bla"]
  564. )
  565. self.assertContains(
  566. response,
  567. "Expected a dictionary, but got str.",
  568. status_code=status.HTTP_400_BAD_REQUEST,
  569. )
  570. response = self.client.bulk_put_rr_sets(
  571. domain_name=self.my_empty_domain.name, payload=[42]
  572. )
  573. self.assertContains(
  574. response,
  575. "Expected a dictionary, but got int.",
  576. status_code=status.HTTP_400_BAD_REQUEST,
  577. )
  578. def test_bulk_put_does_not_accept_rrsets_with_nonstr_subname(self):
  579. payload = [
  580. {"subname": ["foobar"], "type": "A", "ttl": 3600, "records": ["1.2.3.4"]}
  581. ]
  582. response = self.client.bulk_put_rr_sets(
  583. domain_name=self.my_empty_domain.name, payload=payload
  584. )
  585. self.assertContains(
  586. response,
  587. "Expected a string, but got list.",
  588. status_code=status.HTTP_400_BAD_REQUEST,
  589. )
  590. def test_bulk_put_does_not_accept_rrsets_with_nonstr_type(self):
  591. payload = [
  592. {"subname": "foobar", "type": ["A"], "ttl": 3600, "records": ["1.2.3.4"]}
  593. ]
  594. response = self.client.bulk_put_rr_sets(
  595. domain_name=self.my_empty_domain.name, payload=payload
  596. )
  597. self.assertContains(
  598. response,
  599. "Expected a string, but got list.",
  600. status_code=status.HTTP_400_BAD_REQUEST,
  601. )
  602. def test_bulk_put_full(self):
  603. # Full PUT always works
  604. with self.assertRequests(
  605. self.requests_desec_rr_sets_update(name=self.my_empty_domain.name)
  606. ):
  607. response = self.client.bulk_put_rr_sets(
  608. domain_name=self.my_empty_domain.name, payload=self.data
  609. )
  610. self.assertStatus(response, status.HTTP_200_OK)
  611. # Check that RRsets have been created
  612. response = self.client.get_rr_sets(self.my_empty_domain.name)
  613. self.assertStatus(response, status.HTTP_200_OK)
  614. self.assertRRSetsCount(response.data, self.data)
  615. # Do not expect any updates, but successful code when PUT'ing only existing RRsets
  616. response = self.client.bulk_put_rr_sets(
  617. domain_name=self.bulk_domain.name, payload=self.data
  618. )
  619. self.assertStatus(response, status.HTTP_200_OK)
  620. def test_bulk_put_invalid_records(self):
  621. for records in [
  622. "asfd",
  623. ["1.1.1.1", "2.2.2.2", 123],
  624. ["1.2.3.4", None],
  625. [True, "1.1.1.1"],
  626. dict(foobar="foobar", asdf="asdf"),
  627. ]:
  628. payload = [
  629. {"subname": "a.2", "ttl": 3600, "type": "MX", "records": records}
  630. ]
  631. response = self.client.bulk_put_rr_sets(
  632. domain_name=self.my_empty_domain.name, payload=payload
  633. )
  634. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  635. self.assertTrue("records" in response.data[0])
  636. def test_bulk_put_empty_records(self):
  637. with self.assertRequests(
  638. self.requests_desec_rr_sets_update(name=self.bulk_domain.name)
  639. ):
  640. self.assertStatus(
  641. self.client.bulk_put_rr_sets(
  642. domain_name=self.bulk_domain.name, payload=self.data_empty_records
  643. ),
  644. status.HTTP_200_OK,
  645. )
  646. def test_bulk_duplicate_rrset(self):
  647. data = self.data + self.data
  648. for bulk_request_rr_sets in [
  649. self.client.bulk_patch_rr_sets,
  650. self.client.bulk_put_rr_sets,
  651. self.client.bulk_post_rr_sets,
  652. ]:
  653. response = bulk_request_rr_sets(
  654. domain_name=self.my_empty_domain.name, payload=data
  655. )
  656. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  657. def test_bulk_patch_or_post_failure_with_single_rrset(self):
  658. for method in [self.client.bulk_patch_rr_sets, self.client.bulk_put_rr_sets]:
  659. response = method(
  660. domain_name=self.my_empty_domain.name, payload=self.data[0]
  661. )
  662. self.assertContains(
  663. response,
  664. "Expected a list of items but got dict.",
  665. status_code=status.HTTP_400_BAD_REQUEST,
  666. )
  667. def test_bulk_delete_rrsets(self):
  668. self.assertStatus(
  669. self.client.delete(
  670. self.reverse("v1:rrsets", name=self.my_empty_domain.name),
  671. data=None,
  672. ),
  673. status.HTTP_405_METHOD_NOT_ALLOWED,
  674. )
  675. def test_bulk_rrsets_policies(self):
  676. domain = self.my_empty_domain
  677. def assertRequests(*, allowed):
  678. cm = (
  679. self.assertRequests(self.requests_desec_rr_sets_update(domain.name))
  680. if allowed
  681. else nullcontext()
  682. )
  683. data = [
  684. {"subname": "www", "type": "A", "ttl": 3600, "records": ["1.2.3.4"]},
  685. {"subname": "sub", "type": "A", "ttl": 3600, "records": ["1.2.3.4"]},
  686. ]
  687. with cm:
  688. self.assertStatus(
  689. self.client.bulk_post_rr_sets(domain.name, data),
  690. status.HTTP_201_CREATED if allowed else status.HTTP_403_FORBIDDEN,
  691. )
  692. data[0]["records"] = ["4.3.2.1"]
  693. with cm:
  694. self.assertStatus(
  695. self.client.bulk_put_rr_sets(domain.name, data),
  696. status.HTTP_200_OK if allowed else status.HTTP_403_FORBIDDEN,
  697. )
  698. rrset_qs = domain.rrset_set.filter(type="A")
  699. self.assertEqual(rrset_qs.exists(), allowed)
  700. data[0]["records"] = data[1]["records"] = [] # delete
  701. with cm:
  702. self.assertStatus(
  703. self.client.bulk_patch_rr_sets(domain.name, data),
  704. status.HTTP_200_OK if allowed else status.HTTP_403_FORBIDDEN,
  705. )
  706. self.assertStatus(
  707. self.client.bulk_patch_rr_sets(domain.name, data),
  708. status.HTTP_200_OK if allowed else status.HTTP_403_FORBIDDEN,
  709. )
  710. if not allowed:
  711. # Create RRset manually so we cn try manipulating it
  712. for item in data:
  713. item["contents"] = item.pop("records")
  714. self.my_empty_domain.rrset_set.create(**item)
  715. item["records"] = item.pop("contents")
  716. for response in [
  717. self.client.bulk_patch_rr_sets(domain.name, data),
  718. self.client.bulk_put_rr_sets(domain.name, data),
  719. ]:
  720. self.assertStatus(response, status.HTTP_403_FORBIDDEN)
  721. # Clean up
  722. if not allowed:
  723. self.assertTrue(rrset_qs.exists())
  724. rrset_qs.delete()
  725. self.assertFalse(rrset_qs.exists())
  726. assertRequests(allowed=True)
  727. qs = self.token.tokendomainpolicy_set
  728. qs.create(domain=None, subname=None, type=None)
  729. assertRequests(allowed=False)
  730. qs.create(domain=domain, subname=None, type="A", perm_write=True)
  731. assertRequests(allowed=True)
  732. qs.create(domain=domain, subname="www", type="A", perm_write=False)
  733. assertRequests(allowed=False)