]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.api/test_Zones.py
Merge pull request #12565 from jsoref/harmonize-xfr-out-messages
[thirdparty/pdns.git] / regression-tests.api / test_Zones.py
CommitLineData
541bb91b 1from __future__ import print_function
e2dba705 2import json
486210b8 3import operator
d29d5db7 4import time
e2dba705 5import unittest
f626cc48 6import requests.exceptions
ccfabd0d 7from copy import deepcopy
646bcd7d 8from parameterized import parameterized
6754ef71 9from pprint import pprint
168a76b3 10from test_helper import ApiTestCase, unique_zone_name, is_auth, is_auth_lmdb, is_recursor, get_db_records, pdnsutil_rectify, sdig
6754ef71
CH
11
12
13def get_rrset(data, qname, qtype):
14 for rrset in data['rrsets']:
15 if rrset['name'] == qname and rrset['type'] == qtype:
16 return rrset
17 return None
18
19
20def get_first_rec(data, qname, qtype):
21 rrset = get_rrset(data, qname, qtype)
22 if rrset:
23 return rrset['records'][0]
24 return None
25
26
27def eq_zone_rrsets(rrsets, expected):
28 data_got = {}
29 data_expected = {}
541bb91b 30 for type_, expected_records in expected.items():
6754ef71
CH
31 type_ = str(type_)
32 data_got[type_] = set()
33 data_expected[type_] = set()
34 uses_name = any(['name' in expected_record for expected_record in expected_records])
35 # minify + convert received data
36 for rrset in [rrset for rrset in rrsets if rrset['type'] == type_]:
541bb91b 37 print(rrset)
6754ef71
CH
38 for r in rrset['records']:
39 data_got[type_].add((rrset['name'] if uses_name else '@', rrset['type'], r['content']))
40 # minify expected data
41 for r in expected_records:
42 data_expected[type_].add((r['name'] if uses_name else '@', type_, r['content']))
43
541bb91b 44 print("eq_zone_rrsets: got:")
6754ef71 45 pprint(data_got)
541bb91b 46 print("eq_zone_rrsets: expected:")
6754ef71
CH
47 pprint(data_expected)
48
49 assert data_got == data_expected, "%r != %r" % (data_got, data_expected)
1a152698
CH
50
51
02945d9a 52class Zones(ApiTestCase):
1a152698 53
80e9a517
PD
54 def _test_list_zones(self, dnssec=True):
55 path = "/api/v1/servers/localhost/zones"
56 if not dnssec:
57 path = path + "?dnssec=false"
58 r = self.session.get(self.url(path))
c1374bdb 59 self.assert_success_json(r)
45de6290 60 domains = r.json()
02945d9a 61 example_com = [domain for domain in domains if domain['name'] in ('example.com', 'example.com.')]
4bfebc93 62 self.assertEqual(len(example_com), 1)
1a152698 63 example_com = example_com[0]
a21e8566 64 print(example_com)
02945d9a 65 required_fields = ['id', 'url', 'name', 'kind']
c1374bdb 66 if is_auth():
80e9a517
PD
67 required_fields = required_fields + ['masters', 'last_check', 'notified_serial', 'serial', 'account']
68 if dnssec:
69 required_fields = required_fields = ['dnssec', 'edited_serial']
4bfebc93 70 self.assertNotEqual(example_com['serial'], 0)
a25bc5e9
PD
71 if not dnssec:
72 self.assertNotIn('dnssec', example_com)
c1374bdb 73 elif is_recursor():
02945d9a
CH
74 required_fields = required_fields + ['recursion_desired', 'servers']
75 for field in required_fields:
76 self.assertIn(field, example_com)
77
80e9a517 78 def test_list_zones_with_dnssec(self):
a25bc5e9
PD
79 if is_auth():
80 self._test_list_zones(True)
80e9a517
PD
81
82 def test_list_zones_without_dnssec(self):
83 self._test_list_zones(False)
02945d9a 84
406497f5 85class AuthZonesHelperMixin(object):
284fdfe9 86 def create_zone(self, name=None, **kwargs):
bee2acae
CH
87 if name is None:
88 name = unique_zone_name()
e2dba705 89 payload = {
bee2acae 90 'name': name,
e2dba705 91 'kind': 'Native',
1d6b70f9 92 'nameservers': ['ns1.example.com.', 'ns2.example.com.']
e2dba705 93 }
284fdfe9 94 for k, v in kwargs.items():
4bdff352
CH
95 if v is None:
96 del payload[k]
97 else:
98 payload[k] = v
541bb91b 99 print("sending", payload)
e2dba705 100 r = self.session.post(
46d06a12 101 self.url("/api/v1/servers/localhost/zones"),
e2dba705
CH
102 data=json.dumps(payload),
103 headers={'content-type': 'application/json'})
c1374bdb 104 self.assert_success_json(r)
4bfebc93 105 self.assertEqual(r.status_code, 201)
1d6b70f9 106 reply = r.json()
541bb91b 107 print("reply", reply)
6754ef71 108 return name, payload, reply
bee2acae 109
406497f5
CH
110
111@unittest.skipIf(not is_auth(), "Not applicable")
112class AuthZones(ApiTestCase, AuthZonesHelperMixin):
113
c1374bdb 114 def test_create_zone(self):
b0af9105 115 # soa_edit_api has a default, override with empty for this test
6754ef71 116 name, payload, data = self.create_zone(serial=22, soa_edit_api='')
1258fecd 117 for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'edited_serial', 'soa_edit_api', 'soa_edit', 'account'):
d29d5db7
CH
118 self.assertIn(k, data)
119 if k in payload:
4bfebc93 120 self.assertEqual(data[k], payload[k])
f63168e6 121 # validate generated SOA
f527c6ff 122 expected_soa = "a.misconfigured.dns.server.invalid. hostmaster." + name + " " + \
1d6b70f9 123 str(payload['serial']) + " 10800 3600 604800 3600"
4bfebc93 124 self.assertEqual(
6754ef71 125 get_first_rec(data, name, 'SOA')['content'],
1d6b70f9 126 expected_soa
f63168e6 127 )
d1b98434
PD
128
129 if not is_auth_lmdb():
130 # Because we had confusion about dots, check that the DB is without dots.
131 dbrecs = get_db_records(name, 'SOA')
132 self.assertEqual(dbrecs[0]['content'], expected_soa.replace('. ', ' '))
4bfebc93 133 self.assertNotEqual(data['serial'], data['edited_serial'])
d29d5db7 134
c1374bdb 135 def test_create_zone_with_soa_edit_api(self):
f63168e6 136 # soa_edit_api wins over serial
6754ef71 137 name, payload, data = self.create_zone(soa_edit_api='EPOCH', serial=10)
f63168e6 138 for k in ('soa_edit_api', ):
e2dba705
CH
139 self.assertIn(k, data)
140 if k in payload:
4bfebc93 141 self.assertEqual(data[k], payload[k])
f63168e6 142 # generated EPOCH serial surely is > fixed serial we passed in
541bb91b 143 print(data)
f63168e6 144 self.assertGreater(data['serial'], payload['serial'])
6754ef71 145 soa_serial = int(get_first_rec(data, name, 'SOA')['content'].split(' ')[2])
f63168e6 146 self.assertGreater(soa_serial, payload['serial'])
4bfebc93 147 self.assertEqual(soa_serial, data['serial'])
6bb25159 148
a0930e45
KM
149 def test_create_zone_with_catalog(self):
150 # soa_edit_api wins over serial
151 name, payload, data = self.create_zone(catalog='catalog.invalid.', serial=10)
152 print(data)
153 for k in ('catalog', ):
154 self.assertIn(k, data)
155 if k in payload:
156 self.assertEqual(data[k], payload[k])
157
79532aa7
CH
158 def test_create_zone_with_account(self):
159 # soa_edit_api wins over serial
6754ef71 160 name, payload, data = self.create_zone(account='anaccount', serial=10)
541bb91b 161 print(data)
79532aa7
CH
162 for k in ('account', ):
163 self.assertIn(k, data)
164 if k in payload:
4bfebc93 165 self.assertEqual(data[k], payload[k])
79532aa7 166
9440a9f0
CH
167 def test_create_zone_default_soa_edit_api(self):
168 name, payload, data = self.create_zone()
541bb91b 169 print(data)
4bfebc93 170 self.assertEqual(data['soa_edit_api'], 'DEFAULT')
9440a9f0 171
331d3062
CH
172 def test_create_zone_exists(self):
173 name, payload, data = self.create_zone()
174 print(data)
175 payload = {
176 'name': name,
177 'kind': 'Native'
178 }
179 print(payload)
180 r = self.session.post(
181 self.url("/api/v1/servers/localhost/zones"),
182 data=json.dumps(payload),
183 headers={'content-type': 'application/json'})
4bfebc93 184 self.assertEqual(r.status_code, 409) # Conflict - already exists
331d3062 185
01f7df3f
CH
186 def test_create_zone_with_soa_edit(self):
187 name, payload, data = self.create_zone(soa_edit='INCEPTION-INCREMENT', soa_edit_api='SOA-EDIT-INCREASE')
541bb91b 188 print(data)
4bfebc93
CH
189 self.assertEqual(data['soa_edit'], 'INCEPTION-INCREMENT')
190 self.assertEqual(data['soa_edit_api'], 'SOA-EDIT-INCREASE')
01f7df3f
CH
191 soa_serial = get_first_rec(data, name, 'SOA')['content'].split(' ')[2]
192 # These particular settings lead to the first serial set to YYYYMMDD01.
4bfebc93 193 self.assertEqual(soa_serial[-2:], '01')
f613d242
CH
194 rrset = {
195 'changetype': 'replace',
196 'name': name,
197 'type': 'A',
198 'ttl': 3600,
199 'records': [
200 {
201 "content": "127.0.0.1",
202 "disabled": False
203 }
204 ]
205 }
206 payload = {'rrsets': [rrset]}
207 self.session.patch(
208 self.url("/api/v1/servers/localhost/zones/" + data['id']),
209 data=json.dumps(payload),
210 headers={'content-type': 'application/json'})
211 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + data['id']))
212 data = r.json()
213 soa_serial = get_first_rec(data, name, 'SOA')['content'].split(' ')[2]
4bfebc93 214 self.assertEqual(soa_serial[-2:], '02')
01f7df3f 215
c1374bdb 216 def test_create_zone_with_records(self):
f63168e6 217 name = unique_zone_name()
6754ef71
CH
218 rrset = {
219 "name": name,
220 "type": "A",
221 "ttl": 3600,
222 "records": [{
f63168e6 223 "content": "4.3.2.1",
6754ef71
CH
224 "disabled": False,
225 }],
226 }
227 name, payload, data = self.create_zone(name=name, rrsets=[rrset])
f63168e6 228 # check our record has appeared
4bfebc93 229 self.assertEqual(get_rrset(data, name, 'A')['records'], rrset['records'])
f63168e6 230
d0953126
AT
231 def test_create_zone_with_wildcard_records(self):
232 name = unique_zone_name()
6754ef71
CH
233 rrset = {
234 "name": "*."+name,
235 "type": "A",
236 "ttl": 3600,
237 "records": [{
d0953126 238 "content": "4.3.2.1",
6754ef71
CH
239 "disabled": False,
240 }],
241 }
242 name, payload, data = self.create_zone(name=name, rrsets=[rrset])
d0953126 243 # check our record has appeared
4bfebc93 244 self.assertEqual(get_rrset(data, rrset['name'], 'A')['records'], rrset['records'])
d0953126 245
c1374bdb 246 def test_create_zone_with_comments(self):
f63168e6 247 name = unique_zone_name()
f2d6dcc0
RG
248 rrsets = [
249 {
250 "name": name,
251 "type": "soa", # test uppercasing of type, too.
252 "comments": [{
253 "account": "test1",
254 "content": "blah blah",
255 "modified_at": 11112,
256 }],
257 },
258 {
259 "name": name,
260 "type": "AAAA",
261 "ttl": 3600,
262 "records": [{
263 "content": "2001:DB8::1",
264 "disabled": False,
265 }],
266 "comments": [{
267 "account": "test AAAA",
268 "content": "blah blah AAAA",
269 "modified_at": 11112,
270 }],
271 },
272 {
273 "name": name,
274 "type": "TXT",
275 "ttl": 3600,
276 "records": [{
277 "content": "\"test TXT\"",
278 "disabled": False,
279 }],
280 },
281 {
282 "name": name,
283 "type": "A",
284 "ttl": 3600,
285 "records": [{
286 "content": "192.0.2.1",
287 "disabled": False,
288 }],
289 },
290 ]
f626cc48
PD
291
292 if is_auth_lmdb():
293 with self.assertRaises(requests.exceptions.HTTPError): # No comments in LMDB
294 self.create_zone(name=name, rrsets=rrsets)
295 return
296
f2d6dcc0
RG
297 name, payload, data = self.create_zone(name=name, rrsets=rrsets)
298 # NS records have been created
4bfebc93 299 self.assertEqual(len(data['rrsets']), len(rrsets) + 1)
f63168e6 300 # check our comment has appeared
4bfebc93
CH
301 self.assertEqual(get_rrset(data, name, 'SOA')['comments'], rrsets[0]['comments'])
302 self.assertEqual(get_rrset(data, name, 'A')['comments'], [])
303 self.assertEqual(get_rrset(data, name, 'TXT')['comments'], [])
304 self.assertEqual(get_rrset(data, name, 'AAAA')['comments'], rrsets[1]['comments'])
f63168e6 305
1d6b70f9
CH
306 def test_create_zone_uncanonical_nameservers(self):
307 name = unique_zone_name()
308 payload = {
309 'name': name,
310 'kind': 'Native',
311 'nameservers': ['uncanon.example.com']
312 }
541bb91b 313 print(payload)
1d6b70f9
CH
314 r = self.session.post(
315 self.url("/api/v1/servers/localhost/zones"),
316 data=json.dumps(payload),
317 headers={'content-type': 'application/json'})
4bfebc93 318 self.assertEqual(r.status_code, 422)
1d6b70f9
CH
319 self.assertIn('Nameserver is not canonical', r.json()['error'])
320
321 def test_create_auth_zone_no_name(self):
322 name = unique_zone_name()
323 payload = {
324 'name': '',
325 'kind': 'Native',
326 }
541bb91b 327 print(payload)
1d6b70f9
CH
328 r = self.session.post(
329 self.url("/api/v1/servers/localhost/zones"),
330 data=json.dumps(payload),
331 headers={'content-type': 'application/json'})
4bfebc93 332 self.assertEqual(r.status_code, 422)
1d6b70f9
CH
333 self.assertIn('is not canonical', r.json()['error'])
334
c1374bdb 335 def test_create_zone_with_custom_soa(self):
f63168e6 336 name = unique_zone_name()
6754ef71
CH
337 content = u"ns1.example.net. testmaster@example.net. 10 10800 3600 604800 3600"
338 rrset = {
339 "name": name,
340 "type": "soa", # test uppercasing of type, too.
341 "ttl": 3600,
342 "records": [{
343 "content": content,
344 "disabled": False,
345 }],
346 }
347 name, payload, data = self.create_zone(name=name, rrsets=[rrset], soa_edit_api='')
4bfebc93 348 self.assertEqual(get_rrset(data, name, 'SOA')['records'], rrset['records'])
d1b98434
PD
349 if not is_auth_lmdb():
350 dbrecs = get_db_records(name, 'SOA')
351 self.assertEqual(dbrecs[0]['content'], content.replace('. ', ' '))
1d6b70f9
CH
352
353 def test_create_zone_double_dot(self):
354 name = 'test..' + unique_zone_name()
355 payload = {
356 'name': name,
357 'kind': 'Native',
358 'nameservers': ['ns1.example.com.']
359 }
541bb91b 360 print(payload)
1d6b70f9
CH
361 r = self.session.post(
362 self.url("/api/v1/servers/localhost/zones"),
363 data=json.dumps(payload),
364 headers={'content-type': 'application/json'})
4bfebc93 365 self.assertEqual(r.status_code, 422)
1d6b70f9 366 self.assertIn('Unable to parse DNS Name', r.json()['error'])
05776d2f 367
1d6b70f9
CH
368 def test_create_zone_restricted_chars(self):
369 name = 'test:' + unique_zone_name() # : isn't good as a name.
370 payload = {
371 'name': name,
372 'kind': 'Native',
373 'nameservers': ['ns1.example.com']
374 }
541bb91b 375 print(payload)
1d6b70f9
CH
376 r = self.session.post(
377 self.url("/api/v1/servers/localhost/zones"),
378 data=json.dumps(payload),
379 headers={'content-type': 'application/json'})
4bfebc93 380 self.assertEqual(r.status_code, 422)
1d6b70f9 381 self.assertIn('contains unsupported characters', r.json()['error'])
4ebf78b1 382
33e6c3e9
CH
383 def test_create_zone_mixed_nameservers_ns_rrset_zonelevel(self):
384 name = unique_zone_name()
385 rrset = {
386 "name": name,
387 "type": "NS",
388 "ttl": 3600,
389 "records": [{
390 "content": "ns2.example.com.",
391 "disabled": False,
392 }],
393 }
394 payload = {
395 'name': name,
396 'kind': 'Native',
397 'nameservers': ['ns1.example.com.'],
398 'rrsets': [rrset],
399 }
541bb91b 400 print(payload)
33e6c3e9
CH
401 r = self.session.post(
402 self.url("/api/v1/servers/localhost/zones"),
403 data=json.dumps(payload),
404 headers={'content-type': 'application/json'})
4bfebc93 405 self.assertEqual(r.status_code, 422)
33e6c3e9
CH
406 self.assertIn('Nameservers list MUST NOT be mixed with zone-level NS in rrsets', r.json()['error'])
407
408 def test_create_zone_mixed_nameservers_ns_rrset_below_zonelevel(self):
409 name = unique_zone_name()
410 rrset = {
411 "name": 'subzone.'+name,
412 "type": "NS",
413 "ttl": 3600,
414 "records": [{
415 "content": "ns2.example.com.",
416 "disabled": False,
417 }],
418 }
419 payload = {
420 'name': name,
421 'kind': 'Native',
422 'nameservers': ['ns1.example.com.'],
423 'rrsets': [rrset],
424 }
541bb91b 425 print(payload)
33e6c3e9
CH
426 r = self.session.post(
427 self.url("/api/v1/servers/localhost/zones"),
428 data=json.dumps(payload),
429 headers={'content-type': 'application/json'})
430 self.assert_success_json(r)
431
c1374bdb 432 def test_create_zone_with_symbols(self):
6754ef71 433 name, payload, data = self.create_zone(name='foo/bar.'+unique_zone_name())
bee2acae 434 name = payload['name']
1d6b70f9 435 expected_id = name.replace('/', '=2F')
00a9b229
CH
436 for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial'):
437 self.assertIn(k, data)
438 if k in payload:
4bfebc93
CH
439 self.assertEqual(data[k], payload[k])
440 self.assertEqual(data['id'], expected_id)
d1b98434
PD
441 if not is_auth_lmdb():
442 dbrecs = get_db_records(name, 'SOA')
443 self.assertEqual(dbrecs[0]['name'], name.rstrip('.'))
00a9b229 444
c1374bdb 445 def test_create_zone_with_nameservers_non_string(self):
e90b4e38
CH
446 # ensure we don't crash
447 name = unique_zone_name()
448 payload = {
449 'name': name,
450 'kind': 'Native',
451 'nameservers': [{'a': 'ns1.example.com'}] # invalid
452 }
541bb91b 453 print(payload)
e90b4e38 454 r = self.session.post(
46d06a12 455 self.url("/api/v1/servers/localhost/zones"),
e90b4e38
CH
456 data=json.dumps(payload),
457 headers={'content-type': 'application/json'})
4bfebc93 458 self.assertEqual(r.status_code, 422)
e90b4e38 459
986e4858
PL
460 def test_create_zone_with_dnssec(self):
461 """
462 Create a zone with "dnssec" set and see if a key was made.
463 """
464 name = unique_zone_name()
465 name, payload, data = self.create_zone(dnssec=True)
466
467 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name))
468
469 for k in ('dnssec', ):
470 self.assertIn(k, data)
471 if k in payload:
4bfebc93 472 self.assertEqual(data[k], payload[k])
986e4858
PL
473
474 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name + '/cryptokeys'))
475
476 keys = r.json()
477
541bb91b 478 print(keys)
986e4858 479
4bfebc93
CH
480 self.assertEqual(r.status_code, 200)
481 self.assertEqual(len(keys), 1)
482 self.assertEqual(keys[0]['type'], 'Cryptokey')
483 self.assertEqual(keys[0]['active'], True)
484 self.assertEqual(keys[0]['keytype'], 'csk')
986e4858 485
cbe8b186
PL
486 def test_create_zone_with_dnssec_disable_dnssec(self):
487 """
488 Create a zone with "dnssec", then set "dnssec" to false and see if the
489 keys are gone
490 """
491 name = unique_zone_name()
492 name, payload, data = self.create_zone(dnssec=True)
493
494 self.session.put(self.url("/api/v1/servers/localhost/zones/" + name),
495 data=json.dumps({'dnssec': False}))
496 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name))
497
498 zoneinfo = r.json()
499
4bfebc93
CH
500 self.assertEqual(r.status_code, 200)
501 self.assertEqual(zoneinfo['dnssec'], False)
cbe8b186
PL
502
503 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name + '/cryptokeys'))
504
505 keys = r.json()
506
4bfebc93
CH
507 self.assertEqual(r.status_code, 200)
508 self.assertEqual(len(keys), 0)
cbe8b186 509
986e4858
PL
510 def test_create_zone_with_nsec3param(self):
511 """
512 Create a zone with "nsec3param" set and see if the metadata was added.
513 """
514 name = unique_zone_name()
5a5d565f 515 nsec3param = '1 0 100 aabbccddeeff'
986e4858
PL
516 name, payload, data = self.create_zone(dnssec=True, nsec3param=nsec3param)
517
518 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name))
519
520 for k in ('dnssec', 'nsec3param'):
521 self.assertIn(k, data)
522 if k in payload:
4bfebc93 523 self.assertEqual(data[k], payload[k])
986e4858
PL
524
525 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name + '/metadata/NSEC3PARAM'))
526
527 data = r.json()
528
541bb91b 529 print(data)
986e4858 530
4bfebc93
CH
531 self.assertEqual(r.status_code, 200)
532 self.assertEqual(len(data['metadata']), 1)
533 self.assertEqual(data['kind'], 'NSEC3PARAM')
534 self.assertEqual(data['metadata'][0], nsec3param)
986e4858
PL
535
536 def test_create_zone_with_nsec3narrow(self):
537 """
538 Create a zone with "nsec3narrow" set and see if the metadata was added.
539 """
540 name = unique_zone_name()
5a5d565f 541 nsec3param = '1 0 100 aabbccddeeff'
986e4858
PL
542 name, payload, data = self.create_zone(dnssec=True, nsec3param=nsec3param,
543 nsec3narrow=True)
544
545 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name))
546
547 for k in ('dnssec', 'nsec3param', 'nsec3narrow'):
548 self.assertIn(k, data)
549 if k in payload:
4bfebc93 550 self.assertEqual(data[k], payload[k])
986e4858
PL
551
552 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name + '/metadata/NSEC3NARROW'))
553
554 data = r.json()
555
541bb91b 556 print(data)
986e4858 557
4bfebc93
CH
558 self.assertEqual(r.status_code, 200)
559 self.assertEqual(len(data['metadata']), 1)
560 self.assertEqual(data['kind'], 'NSEC3NARROW')
561 self.assertEqual(data['metadata'][0], '1')
986e4858 562
df434f42
PL
563 def test_create_zone_with_nsec3param_switch_to_nsec(self):
564 """
565 Create a zone with "nsec3param", then remove the params
566 """
567 name, payload, data = self.create_zone(dnssec=True,
568 nsec3param='1 0 1 ab')
569 self.session.put(self.url("/api/v1/servers/localhost/zones/" + name),
570 data=json.dumps({'nsec3param': ''}))
571 r = self.session.get(
572 self.url("/api/v1/servers/localhost/zones/" + name))
573 data = r.json()
574
4bfebc93
CH
575 self.assertEqual(r.status_code, 200)
576 self.assertEqual(data['nsec3param'], '')
df434f42 577
efc52697
KM
578 def test_create_zone_without_dnssec_unset_nsec3parm(self):
579 """
580 Create a non dnssec zone and set an empty "nsec3param"
581 """
582 name, payload, data = self.create_zone(dnssec=False)
583 r = self.session.put(self.url("/api/v1/servers/localhost/zones/" + name),
584 data=json.dumps({'nsec3param': ''}))
585
586 self.assertEqual(r.status_code, 204)
587
588 def test_create_zone_without_dnssec_set_nsec3parm(self):
589 """
590 Create a non dnssec zone and set "nsec3param"
591 """
592 name, payload, data = self.create_zone(dnssec=False)
593 r = self.session.put(self.url("/api/v1/servers/localhost/zones/" + name),
594 data=json.dumps({'nsec3param': '1 0 1 ab'}))
595
596 self.assertEqual(r.status_code, 422)
597
a843c67e
KM
598 def test_create_zone_dnssec_serial(self):
599 """
168a76b3 600 Create a zone, then set and unset "dnssec", then check if the serial was increased
a843c67e
KM
601 after every step
602 """
a843c67e
KM
603 name, payload, data = self.create_zone()
604
605 soa_serial = get_first_rec(data, name, 'SOA')['content'].split(' ')[2]
4bfebc93 606 self.assertEqual(soa_serial[-2:], '01')
a843c67e
KM
607
608 self.session.put(self.url("/api/v1/servers/localhost/zones/" + name),
609 data=json.dumps({'dnssec': True}))
610 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name))
611
612 data = r.json()
613 soa_serial = get_first_rec(data, name, 'SOA')['content'].split(' ')[2]
614
4bfebc93
CH
615 self.assertEqual(r.status_code, 200)
616 self.assertEqual(soa_serial[-2:], '02')
a843c67e
KM
617
618 self.session.put(self.url("/api/v1/servers/localhost/zones/" + name),
619 data=json.dumps({'dnssec': False}))
620 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name))
621
622 data = r.json()
623 soa_serial = get_first_rec(data, name, 'SOA')['content'].split(' ')[2]
624
4bfebc93
CH
625 self.assertEqual(r.status_code, 200)
626 self.assertEqual(soa_serial[-2:], '03')
a843c67e 627
16e25450
CH
628 def test_zone_absolute_url(self):
629 name, payload, data = self.create_zone()
630 r = self.session.get(self.url("/api/v1/servers/localhost/zones"))
631 rdata = r.json()
632 print(rdata[0])
633 self.assertTrue(rdata[0]['url'].startswith('/api/v'))
634
24e11043
CJ
635 def test_create_zone_metadata(self):
636 payload_metadata = {"type": "Metadata", "kind": "AXFR-SOURCE", "metadata": ["127.0.0.2"]}
637 r = self.session.post(self.url("/api/v1/servers/localhost/zones/example.com/metadata"),
638 data=json.dumps(payload_metadata))
639 rdata = r.json()
4bfebc93
CH
640 self.assertEqual(r.status_code, 201)
641 self.assertEqual(rdata["metadata"], payload_metadata["metadata"])
24e11043
CJ
642
643 def test_create_zone_metadata_kind(self):
644 payload_metadata = {"metadata": ["127.0.0.2"]}
645 r = self.session.put(self.url("/api/v1/servers/localhost/zones/example.com/metadata/AXFR-SOURCE"),
646 data=json.dumps(payload_metadata))
647 rdata = r.json()
4bfebc93
CH
648 self.assertEqual(r.status_code, 200)
649 self.assertEqual(rdata["metadata"], payload_metadata["metadata"])
24e11043
CJ
650
651 def test_create_protected_zone_metadata(self):
652 # test whether it prevents modification of certain kinds
653 for k in ("NSEC3NARROW", "NSEC3PARAM", "PRESIGNED", "LUA-AXFR-SCRIPT"):
654 payload = {"metadata": ["FOO", "BAR"]}
655 r = self.session.put(self.url("/api/v1/servers/localhost/zones/example.com/metadata/%s" % k),
656 data=json.dumps(payload))
4bfebc93 657 self.assertEqual(r.status_code, 422)
24e11043
CJ
658
659 def test_retrieve_zone_metadata(self):
660 payload_metadata = {"type": "Metadata", "kind": "AXFR-SOURCE", "metadata": ["127.0.0.2"]}
661 self.session.post(self.url("/api/v1/servers/localhost/zones/example.com/metadata"),
662 data=json.dumps(payload_metadata))
663 r = self.session.get(self.url("/api/v1/servers/localhost/zones/example.com/metadata"))
664 rdata = r.json()
4bfebc93 665 self.assertEqual(r.status_code, 200)
24e11043
CJ
666 self.assertIn(payload_metadata, rdata)
667
668 def test_delete_zone_metadata(self):
669 r = self.session.delete(self.url("/api/v1/servers/localhost/zones/example.com/metadata/AXFR-SOURCE"))
4bfebc93 670 self.assertEqual(r.status_code, 200)
24e11043
CJ
671 r = self.session.get(self.url("/api/v1/servers/localhost/zones/example.com/metadata/AXFR-SOURCE"))
672 rdata = r.json()
4bfebc93
CH
673 self.assertEqual(r.status_code, 200)
674 self.assertEqual(rdata["metadata"], [])
24e11043 675
9ac4e6d5
PL
676 def test_create_external_zone_metadata(self):
677 payload_metadata = {"metadata": ["My very important message"]}
678 r = self.session.put(self.url("/api/v1/servers/localhost/zones/example.com/metadata/X-MYMETA"),
679 data=json.dumps(payload_metadata))
4bfebc93 680 self.assertEqual(r.status_code, 200)
9ac4e6d5 681 rdata = r.json()
4bfebc93 682 self.assertEqual(rdata["metadata"], payload_metadata["metadata"])
9ac4e6d5 683
d38e81e6
PL
684 def test_create_metadata_in_non_existent_zone(self):
685 payload_metadata = {"type": "Metadata", "kind": "AXFR-SOURCE", "metadata": ["127.0.0.2"]}
686 r = self.session.post(self.url("/api/v1/servers/localhost/zones/idonotexist.123.456.example./metadata"),
687 data=json.dumps(payload_metadata))
4bfebc93 688 self.assertEqual(r.status_code, 404)
77bfe8de
PL
689 # Note: errors should probably contain json (see #5988)
690 # self.assertIn('Could not find domain ', r.json()['error'])
d38e81e6 691
4bdff352
CH
692 def test_create_slave_zone(self):
693 # Test that nameservers can be absent for slave zones.
6754ef71 694 name, payload, data = self.create_zone(kind='Slave', nameservers=None, masters=['127.0.0.2'])
4bdff352
CH
695 for k in ('name', 'masters', 'kind'):
696 self.assertIn(k, data)
4bfebc93 697 self.assertEqual(data[k], payload[k])
541bb91b
CH
698 print("payload:", payload)
699 print("data:", data)
4de11a54 700 # Because slave zones don't get a SOA, we need to test that they'll show up in the zone list.
46d06a12 701 r = self.session.get(self.url("/api/v1/servers/localhost/zones"))
4de11a54 702 zonelist = r.json()
541bb91b 703 print("zonelist:", zonelist)
4de11a54
CH
704 self.assertIn(payload['name'], [zone['name'] for zone in zonelist])
705 # Also test that fetching the zone works.
46d06a12 706 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + data['id']))
4de11a54 707 data = r.json()
541bb91b 708 print("zone (fetched):", data)
4de11a54
CH
709 for k in ('name', 'masters', 'kind'):
710 self.assertIn(k, data)
4bfebc93 711 self.assertEqual(data[k], payload[k])
4de11a54 712 self.assertEqual(data['serial'], 0)
6754ef71 713 self.assertEqual(data['rrsets'], [])
4de11a54 714
2f27a15f
KM
715 def test_create_consumer_zone(self):
716 # Test that nameservers can be absent for consumer zones.
717 name, payload, data = self.create_zone(kind='Consumer', nameservers=None, masters=['127.0.0.2'])
718 for k in ('name', 'masters', 'kind'):
719 self.assertIn(k, data)
720 self.assertEqual(data[k], payload[k])
721 print("payload:", payload)
722 print("data:", data)
723 # Because consumer zones don't get a SOA, we need to test that they'll show up in the zone list.
724 r = self.session.get(self.url("/api/v1/servers/localhost/zones"))
725 zonelist = r.json()
726 print("zonelist:", zonelist)
727 self.assertIn(payload['name'], [zone['name'] for zone in zonelist])
728 # Also test that fetching the zone works.
729 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + data['id']))
730 data = r.json()
731 print("zone (fetched):", data)
732 for k in ('name', 'masters', 'kind'):
733 self.assertIn(k, data)
734 self.assertEqual(data[k], payload[k])
735 self.assertEqual(data['serial'], 0)
736 self.assertEqual(data['rrsets'], [])
737
e543cc8f
CH
738 def test_find_zone_by_name(self):
739 name = 'foo/' + unique_zone_name()
740 name, payload, data = self.create_zone(name=name)
741 r = self.session.get(self.url("/api/v1/servers/localhost/zones?zone=" + name))
742 data = r.json()
743 print(data)
4bfebc93 744 self.assertEqual(data[0]['name'], name)
e543cc8f 745
4de11a54 746 def test_delete_slave_zone(self):
6754ef71 747 name, payload, data = self.create_zone(kind='Slave', nameservers=None, masters=['127.0.0.2'])
46d06a12 748 r = self.session.delete(self.url("/api/v1/servers/localhost/zones/" + data['id']))
2f27a15f
KM
749 r.raise_for_status()
750
751 def test_delete_consumer_zone(self):
752 name, payload, data = self.create_zone(kind='Consumer', nameservers=None, masters=['127.0.0.2'])
753 r = self.session.delete(self.url("/api/v1/servers/localhost/zones/" + data['id']))
4de11a54 754 r.raise_for_status()
4bdff352 755
a426cb89 756 def test_retrieve_slave_zone(self):
6754ef71 757 name, payload, data = self.create_zone(kind='Slave', nameservers=None, masters=['127.0.0.2'])
541bb91b
CH
758 print("payload:", payload)
759 print("data:", data)
46d06a12 760 r = self.session.put(self.url("/api/v1/servers/localhost/zones/" + data['id'] + "/axfr-retrieve"))
a426cb89 761 data = r.json()
541bb91b 762 print("status for axfr-retrieve:", data)
a426cb89
CH
763 self.assertEqual(data['result'], u'Added retrieval request for \'' + payload['name'] +
764 '\' from master 127.0.0.2')
765
766 def test_notify_master_zone(self):
6754ef71 767 name, payload, data = self.create_zone(kind='Master')
541bb91b
CH
768 print("payload:", payload)
769 print("data:", data)
46d06a12 770 r = self.session.put(self.url("/api/v1/servers/localhost/zones/" + data['id'] + "/notify"))
a426cb89 771 data = r.json()
541bb91b 772 print("status for notify:", data)
a426cb89
CH
773 self.assertEqual(data['result'], 'Notification queued')
774
c1374bdb 775 def test_get_zone_with_symbols(self):
6754ef71 776 name, payload, data = self.create_zone(name='foo/bar.'+unique_zone_name())
3c3c006b 777 name = payload['name']
1d6b70f9 778 zone_id = (name.replace('/', '=2F'))
46d06a12 779 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + zone_id))
c1374bdb 780 data = r.json()
6bb25159 781 for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'dnssec'):
3c3c006b
CH
782 self.assertIn(k, data)
783 if k in payload:
4bfebc93 784 self.assertEqual(data[k], payload[k])
3c3c006b 785
c1374bdb 786 def test_get_zone(self):
46d06a12 787 r = self.session.get(self.url("/api/v1/servers/localhost/zones"))
05776d2f 788 domains = r.json()
1d6b70f9 789 example_com = [domain for domain in domains if domain['name'] == u'example.com.'][0]
46d06a12 790 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + example_com['id']))
c1374bdb 791 self.assert_success_json(r)
05776d2f
CH
792 data = r.json()
793 for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial'):
794 self.assertIn(k, data)
4bfebc93 795 self.assertEqual(data['name'], 'example.com.')
7c0ba3d2 796
486210b8
PD
797 def test_get_zone_rrset(self):
798 rz = self.session.get(self.url("/api/v1/servers/localhost/zones"))
799 domains = rz.json()
800 example_com = [domain for domain in domains if domain['name'] == u'example.com.'][0]
801
802 # verify single record from name that has a single record
803 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + example_com['id'] + "?rrset_name=host-18000.example.com."))
804 self.assert_success_json(r)
805 data = r.json()
806 for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'rrsets'):
807 self.assertIn(k, data)
808 self.assertEqual(data['rrsets'],
809 [
810 {
811 'comments': [],
812 'name': 'host-18000.example.com.',
813 'records':
814 [
815 {
816 'content': '192.168.1.80',
817 'disabled': False
818 }
819 ],
820 'ttl': 120,
821 'type': 'A'
822 }
823 ]
824 )
825
826 # verify two RRsets from a name that has two types with one record each
827 powerdnssec_org = [domain for domain in domains if domain['name'] == u'powerdnssec.org.'][0]
828 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + powerdnssec_org['id'] + "?rrset_name=localhost.powerdnssec.org."))
829 self.assert_success_json(r)
830 data = r.json()
831 for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'rrsets'):
832 self.assertIn(k, data)
833 self.assertEqual(sorted(data['rrsets'], key=operator.itemgetter('type')),
834 [
835 {
836 'comments': [],
837 'name': 'localhost.powerdnssec.org.',
838 'records':
839 [
840 {
841 'content': '127.0.0.1',
842 'disabled': False
843 }
844 ],
845 'ttl': 3600,
846 'type': 'A'
847 },
848 {
849 'comments': [],
850 'name': 'localhost.powerdnssec.org.',
851 'records':
852 [
853 {
854 'content': '::1',
855 'disabled': False
856 }
857 ],
858 'ttl': 3600,
859 'type': 'AAAA'
860 },
861 ]
862 )
863
864 # verify one RRset with one record from a name that has two, then filtered by type
865 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + powerdnssec_org['id'] + "?rrset_name=localhost.powerdnssec.org.&rrset_type=AAAA"))
866 self.assert_success_json(r)
867 data = r.json()
868 for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'rrsets'):
869 self.assertIn(k, data)
870 self.assertEqual(data['rrsets'],
871 [
872 {
873 'comments': [],
874 'name': 'localhost.powerdnssec.org.',
875 'records':
876 [
877 {
878 'content': '::1',
879 'disabled': False
880 }
881 ],
882 'ttl': 3600,
883 'type': 'AAAA'
884 }
885 ]
886 )
887
0f0e73fe 888 def test_import_zone_broken(self):
646bcd7d
CH
889 payload = {
890 'name': 'powerdns-broken.com',
891 'kind': 'Master',
892 'nameservers': [],
893 }
0f0e73fe
MS
894 payload['zone'] = """
895;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58571
896flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
897;; WARNING: recursion requested but not available
898
899;; OPT PSEUDOSECTION:
900; EDNS: version: 0, flags:; udp: 1680
901;; QUESTION SECTION:
902;powerdns.com. IN SOA
903
904;; ANSWER SECTION:
905powerdns-broken.com. 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
906powerdns-broken.com. 3600 IN NS powerdnssec2.ds9a.nl.
907powerdns-broken.com. 3600 IN AAAA 2001:888:2000:1d::2
908powerdns-broken.com. 86400 IN A 82.94.213.34
909powerdns-broken.com. 3600 IN MX 0 xs.powerdns.com.
910powerdns-broken.com. 3600 IN NS powerdnssec1.ds9a.nl.
911powerdns-broken.com. 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
912"""
0f0e73fe 913 r = self.session.post(
46d06a12 914 self.url("/api/v1/servers/localhost/zones"),
0f0e73fe
MS
915 data=json.dumps(payload),
916 headers={'content-type': 'application/json'})
4bfebc93 917 self.assertEqual(r.status_code, 422)
0f0e73fe 918
1d6b70f9
CH
919 def test_import_zone_axfr_outofzone(self):
920 # Ensure we don't create out-of-zone records
646bcd7d
CH
921 payload = {
922 'name': unique_zone_name(),
923 'kind': 'Master',
924 'nameservers': [],
925 }
1d6b70f9 926 payload['zone'] = """
646bcd7d
CH
927%NAME% 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
928%NAME% 3600 IN NS powerdnssec2.ds9a.nl.
1d6b70f9 929example.org. 3600 IN AAAA 2001:888:2000:1d::2
646bcd7d
CH
930%NAME% 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
931""".replace('%NAME%', payload['name'])
1d6b70f9
CH
932 r = self.session.post(
933 self.url("/api/v1/servers/localhost/zones"),
934 data=json.dumps(payload),
935 headers={'content-type': 'application/json'})
4bfebc93 936 self.assertEqual(r.status_code, 422)
1d6b70f9
CH
937 self.assertEqual(r.json()['error'], 'RRset example.org. IN AAAA: Name is out of zone')
938
0f0e73fe 939 def test_import_zone_axfr(self):
646bcd7d
CH
940 payload = {
941 'name': 'powerdns.com.',
942 'kind': 'Master',
943 'nameservers': [],
944 'soa_edit_api': '', # turn off so exact SOA comparison works.
945 }
0f0e73fe
MS
946 payload['zone'] = """
947;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58571
948;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
949;; WARNING: recursion requested but not available
950
951;; OPT PSEUDOSECTION:
952; EDNS: version: 0, flags:; udp: 1680
953;; QUESTION SECTION:
954;powerdns.com. IN SOA
955
956;; ANSWER SECTION:
957powerdns.com. 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
958powerdns.com. 3600 IN NS powerdnssec2.ds9a.nl.
959powerdns.com. 3600 IN AAAA 2001:888:2000:1d::2
960powerdns.com. 86400 IN A 82.94.213.34
961powerdns.com. 3600 IN MX 0 xs.powerdns.com.
962powerdns.com. 3600 IN NS powerdnssec1.ds9a.nl.
963powerdns.com. 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
964"""
0f0e73fe 965 r = self.session.post(
46d06a12 966 self.url("/api/v1/servers/localhost/zones"),
0f0e73fe
MS
967 data=json.dumps(payload),
968 headers={'content-type': 'application/json'})
969 self.assert_success_json(r)
970 data = r.json()
971 self.assertIn('name', data)
0f0e73fe 972
90568eb2
MS
973 expected = {
974 'NS': [
6754ef71
CH
975 {'content': 'powerdnssec1.ds9a.nl.'},
976 {'content': 'powerdnssec2.ds9a.nl.'},
977 ],
90568eb2 978 'SOA': [
6754ef71
CH
979 {'content': 'powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800'},
980 ],
90568eb2 981 'MX': [
6754ef71
CH
982 {'content': '0 xs.powerdns.com.'},
983 ],
90568eb2 984 'A': [
6754ef71
CH
985 {'content': '82.94.213.34', 'name': 'powerdns.com.'},
986 ],
90568eb2 987 'AAAA': [
6754ef71
CH
988 {'content': '2001:888:2000:1d::2', 'name': 'powerdns.com.'},
989 ],
90568eb2 990 }
0f0e73fe 991
6754ef71 992 eq_zone_rrsets(data['rrsets'], expected)
1d6b70f9 993
d1b98434
PD
994 if not is_auth_lmdb():
995 # check content in DB is stored WITHOUT trailing dot.
996 dbrecs = get_db_records(payload['name'], 'NS')
997 dbrec = next((dbrec for dbrec in dbrecs if dbrec['content'].startswith('powerdnssec1')))
998 self.assertEqual(dbrec['content'], 'powerdnssec1.ds9a.nl')
0f0e73fe
MS
999
1000 def test_import_zone_bind(self):
646bcd7d
CH
1001 payload = {
1002 'name': 'example.org.',
1003 'kind': 'Master',
1004 'nameservers': [],
1005 'soa_edit_api': '', # turn off so exact SOA comparison works.
1006 }
0f0e73fe
MS
1007 payload['zone'] = """
1008$TTL 86400 ; 24 hours could have been written as 24h or 1d
1009; $TTL used for all RRs without explicit TTL value
1010$ORIGIN example.org.
1011@ 1D IN SOA ns1.example.org. hostmaster.example.org. (
1012 2002022401 ; serial
1013 3H ; refresh
1014 15 ; retry
1015 1w ; expire
1016 3h ; minimum
1017 )
1018 IN NS ns1.example.org. ; in the domain
1019 IN NS ns2.smokeyjoe.com. ; external to domain
1020 IN MX 10 mail.another.com. ; external mail provider
1021; server host definitions
1d6b70f9 1022ns1 IN A 192.168.0.1 ;name server definition
0f0e73fe
MS
1023www IN A 192.168.0.2 ;web server definition
1024ftp IN CNAME www.example.org. ;ftp server definition
1025; non server domain hosts
1026bill IN A 192.168.0.3
1d6b70f9 1027fred IN A 192.168.0.4
0f0e73fe 1028"""
0f0e73fe 1029 r = self.session.post(
46d06a12 1030 self.url("/api/v1/servers/localhost/zones"),
0f0e73fe
MS
1031 data=json.dumps(payload),
1032 headers={'content-type': 'application/json'})
1033 self.assert_success_json(r)
1034 data = r.json()
1035 self.assertIn('name', data)
0f0e73fe 1036
90568eb2
MS
1037 expected = {
1038 'NS': [
6754ef71
CH
1039 {'content': 'ns1.example.org.'},
1040 {'content': 'ns2.smokeyjoe.com.'},
1041 ],
90568eb2 1042 'SOA': [
6754ef71
CH
1043 {'content': 'ns1.example.org. hostmaster.example.org. 2002022401 10800 15 604800 10800'},
1044 ],
90568eb2 1045 'MX': [
6754ef71
CH
1046 {'content': '10 mail.another.com.'},
1047 ],
90568eb2 1048 'A': [
6754ef71
CH
1049 {'content': '192.168.0.1', 'name': 'ns1.example.org.'},
1050 {'content': '192.168.0.2', 'name': 'www.example.org.'},
1051 {'content': '192.168.0.3', 'name': 'bill.example.org.'},
1052 {'content': '192.168.0.4', 'name': 'fred.example.org.'},
1053 ],
90568eb2 1054 'CNAME': [
6754ef71
CH
1055 {'content': 'www.example.org.', 'name': 'ftp.example.org.'},
1056 ],
90568eb2 1057 }
0f0e73fe 1058
6754ef71 1059 eq_zone_rrsets(data['rrsets'], expected)
0f0e73fe 1060
646bcd7d
CH
1061 def test_import_zone_bind_cname_apex(self):
1062 payload = {
1063 'name': unique_zone_name(),
1064 'kind': 'Master',
1065 'nameservers': [],
1066 }
1067 payload['zone'] = """
1068$ORIGIN %NAME%
1069@ IN SOA ns1.example.org. hostmaster.example.org. (2002022401 3H 15 1W 3H)
1070@ IN NS ns1.example.org.
1071@ IN NS ns2.smokeyjoe.com.
1072@ IN CNAME www.example.org.
1073""".replace('%NAME%', payload['name'])
1074 r = self.session.post(
1075 self.url("/api/v1/servers/localhost/zones"),
1076 data=json.dumps(payload),
1077 headers={'content-type': 'application/json'})
4bfebc93 1078 self.assertEqual(r.status_code, 422)
646bcd7d
CH
1079 self.assertIn('Conflicts with another RRset', r.json()['error'])
1080
c1374bdb 1081 def test_export_zone_json(self):
6754ef71 1082 name, payload, zone = self.create_zone(nameservers=['ns1.foo.com.', 'ns2.foo.com.'], soa_edit_api='')
a83004d3
CH
1083 # export it
1084 r = self.session.get(
46d06a12 1085 self.url("/api/v1/servers/localhost/zones/" + name + "/export"),
a83004d3
CH
1086 headers={'accept': 'application/json;q=0.9,*/*;q=0.8'}
1087 )
c1374bdb 1088 self.assert_success_json(r)
a83004d3
CH
1089 data = r.json()
1090 self.assertIn('zone', data)
ba2a1254
DK
1091 expected_data = [name + '\t3600\tIN\tNS\tns1.foo.com.',
1092 name + '\t3600\tIN\tNS\tns2.foo.com.',
f527c6ff 1093 name + '\t3600\tIN\tSOA\ta.misconfigured.dns.server.invalid. hostmaster.' + name +
1d6b70f9 1094 ' 0 10800 3600 604800 3600']
7386e38f 1095 self.assertCountEqual(data['zone'].strip().split('\n'), expected_data)
a83004d3 1096
c1374bdb 1097 def test_export_zone_text(self):
6754ef71 1098 name, payload, zone = self.create_zone(nameservers=['ns1.foo.com.', 'ns2.foo.com.'], soa_edit_api='')
a83004d3
CH
1099 # export it
1100 r = self.session.get(
46d06a12 1101 self.url("/api/v1/servers/localhost/zones/" + name + "/export"),
a83004d3
CH
1102 headers={'accept': '*/*'}
1103 )
1104 data = r.text.strip().split("\n")
ba2a1254
DK
1105 expected_data = [name + '\t3600\tIN\tNS\tns1.foo.com.',
1106 name + '\t3600\tIN\tNS\tns2.foo.com.',
f527c6ff 1107 name + '\t3600\tIN\tSOA\ta.misconfigured.dns.server.invalid. hostmaster.' + name +
1d6b70f9 1108 ' 0 10800 3600 604800 3600']
7386e38f 1109 self.assertCountEqual(data, expected_data)
a83004d3 1110
c1374bdb 1111 def test_update_zone(self):
6754ef71 1112 name, payload, zone = self.create_zone()
bee2acae 1113 name = payload['name']
d29d5db7 1114 # update, set as Master and enable SOA-EDIT-API
7c0ba3d2
CH
1115 payload = {
1116 'kind': 'Master',
c1374bdb 1117 'masters': ['192.0.2.1', '192.0.2.2'],
a0930e45 1118 'catalog': 'catalog.invalid.',
6bb25159
MS
1119 'soa_edit_api': 'EPOCH',
1120 'soa_edit': 'EPOCH'
7c0ba3d2
CH
1121 }
1122 r = self.session.put(
46d06a12 1123 self.url("/api/v1/servers/localhost/zones/" + name),
7c0ba3d2
CH
1124 data=json.dumps(payload),
1125 headers={'content-type': 'application/json'})
f0e76cee
CH
1126 self.assert_success(r)
1127 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
7c0ba3d2
CH
1128 for k in payload.keys():
1129 self.assertIn(k, data)
4bfebc93 1130 self.assertEqual(data[k], payload[k])
d29d5db7 1131 # update, back to Native and empty(off)
7c0ba3d2 1132 payload = {
d29d5db7 1133 'kind': 'Native',
a0930e45 1134 'catalog': '',
6bb25159
MS
1135 'soa_edit_api': '',
1136 'soa_edit': ''
7c0ba3d2
CH
1137 }
1138 r = self.session.put(
46d06a12 1139 self.url("/api/v1/servers/localhost/zones/" + name),
7c0ba3d2
CH
1140 data=json.dumps(payload),
1141 headers={'content-type': 'application/json'})
f0e76cee
CH
1142 self.assert_success(r)
1143 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
7c0ba3d2
CH
1144 for k in payload.keys():
1145 self.assertIn(k, data)
4bfebc93 1146 self.assertEqual(data[k], payload[k])
b3905a3d 1147
c1374bdb 1148 def test_zone_rr_update(self):
6754ef71 1149 name, payload, zone = self.create_zone()
b3905a3d 1150 # do a replace (= update)
d708640f 1151 rrset = {
b3905a3d
CH
1152 'changetype': 'replace',
1153 'name': name,
8ce0dc75 1154 'type': 'ns',
6754ef71 1155 'ttl': 3600,
b3905a3d
CH
1156 'records': [
1157 {
1d6b70f9 1158 "content": "ns1.bar.com.",
cea26350
CH
1159 "disabled": False
1160 },
1161 {
1d6b70f9 1162 "content": "ns2-disabled.bar.com.",
cea26350 1163 "disabled": True
b3905a3d
CH
1164 }
1165 ]
1166 }
d708640f 1167 payload = {'rrsets': [rrset]}
b3905a3d 1168 r = self.session.patch(
46d06a12 1169 self.url("/api/v1/servers/localhost/zones/" + name),
b3905a3d
CH
1170 data=json.dumps(payload),
1171 headers={'content-type': 'application/json'})
f0e76cee 1172 self.assert_success(r)
b3905a3d 1173 # verify that (only) the new record is there
f0e76cee 1174 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
7386e38f 1175 self.assertCountEqual(get_rrset(data, name, 'NS')['records'], rrset['records'])
b3905a3d 1176
c1374bdb 1177 def test_zone_rr_update_mx(self):
05cf6a71 1178 # Important to test with MX records, as they have a priority field, which must end up in the content field.
6754ef71 1179 name, payload, zone = self.create_zone()
41e3b10e 1180 # do a replace (= update)
d708640f 1181 rrset = {
41e3b10e
CH
1182 'changetype': 'replace',
1183 'name': name,
1184 'type': 'MX',
6754ef71 1185 'ttl': 3600,
41e3b10e
CH
1186 'records': [
1187 {
1d6b70f9 1188 "content": "10 mail.example.org.",
41e3b10e
CH
1189 "disabled": False
1190 }
1191 ]
1192 }
d708640f 1193 payload = {'rrsets': [rrset]}
41e3b10e 1194 r = self.session.patch(
46d06a12 1195 self.url("/api/v1/servers/localhost/zones/" + name),
41e3b10e
CH
1196 data=json.dumps(payload),
1197 headers={'content-type': 'application/json'})
f0e76cee 1198 self.assert_success(r)
41e3b10e 1199 # verify that (only) the new record is there
f0e76cee 1200 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
4bfebc93 1201 self.assertEqual(get_rrset(data, name, 'MX')['records'], rrset['records'])
d708640f 1202
81950930
CHB
1203 def test_zone_rr_update_invalid_mx(self):
1204 name, payload, zone = self.create_zone()
1205 # do a replace (= update)
1206 rrset = {
1207 'changetype': 'replace',
1208 'name': name,
1209 'type': 'MX',
1210 'ttl': 3600,
1211 'records': [
1212 {
1213 "content": "10 mail@mx.example.org.",
1214 "disabled": False
1215 }
1216 ]
1217 }
1218 payload = {'rrsets': [rrset]}
1219 r = self.session.patch(
1220 self.url("/api/v1/servers/localhost/zones/" + name),
1221 data=json.dumps(payload),
1222 headers={'content-type': 'application/json'})
4bfebc93 1223 self.assertEqual(r.status_code, 422)
97c8ea81 1224 self.assertIn('non-hostname content', r.json()['error'])
81950930
CHB
1225 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1226 self.assertIsNone(get_rrset(data, name, 'MX'))
1227
a53b24d0
CHB
1228 def test_zone_rr_update_opt(self):
1229 name, payload, zone = self.create_zone()
1230 # do a replace (= update)
1231 rrset = {
1232 'changetype': 'replace',
1233 'name': name,
1234 'type': 'OPT',
1235 'ttl': 3600,
1236 'records': [
1237 {
1238 "content": "9",
1239 "disabled": False
1240 }
1241 ]
1242 }
1243 payload = {'rrsets': [rrset]}
1244 r = self.session.patch(
1245 self.url("/api/v1/servers/localhost/zones/" + name),
1246 data=json.dumps(payload),
1247 headers={'content-type': 'application/json'})
4bfebc93 1248 self.assertEqual(r.status_code, 422)
a53b24d0
CHB
1249 self.assertIn('OPT: invalid type given', r.json()['error'])
1250
c1374bdb 1251 def test_zone_rr_update_multiple_rrsets(self):
6754ef71 1252 name, payload, zone = self.create_zone()
d708640f
CH
1253 rrset1 = {
1254 'changetype': 'replace',
1255 'name': name,
1256 'type': 'NS',
6754ef71 1257 'ttl': 3600,
d708640f
CH
1258 'records': [
1259 {
6754ef71 1260
1d6b70f9 1261 "content": "ns9999.example.com.",
d708640f
CH
1262 "disabled": False
1263 }
1264 ]
1265 }
1266 rrset2 = {
1267 'changetype': 'replace',
1268 'name': name,
1269 'type': 'MX',
6754ef71 1270 'ttl': 3600,
d708640f
CH
1271 'records': [
1272 {
1d6b70f9 1273 "content": "10 mx444.example.com.",
d708640f
CH
1274 "disabled": False
1275 }
1276 ]
1277 }
1278 payload = {'rrsets': [rrset1, rrset2]}
1279 r = self.session.patch(
46d06a12 1280 self.url("/api/v1/servers/localhost/zones/" + name),
d708640f
CH
1281 data=json.dumps(payload),
1282 headers={'content-type': 'application/json'})
f0e76cee 1283 self.assert_success(r)
d708640f 1284 # verify that all rrsets have been updated
f0e76cee 1285 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
4bfebc93
CH
1286 self.assertEqual(get_rrset(data, name, 'NS')['records'], rrset1['records'])
1287 self.assertEqual(get_rrset(data, name, 'MX')['records'], rrset2['records'])
41e3b10e 1288
e3675a8a
CH
1289 def test_zone_rr_update_duplicate_record(self):
1290 name, payload, zone = self.create_zone()
1291 rrset = {
1292 'changetype': 'replace',
1293 'name': name,
1294 'type': 'NS',
1295 'ttl': 3600,
1296 'records': [
1297 {"content": "ns9999.example.com.", "disabled": False},
1298 {"content": "ns9996.example.com.", "disabled": False},
1299 {"content": "ns9987.example.com.", "disabled": False},
1300 {"content": "ns9988.example.com.", "disabled": False},
1301 {"content": "ns9999.example.com.", "disabled": False},
1302 ]
1303 }
1304 payload = {'rrsets': [rrset]}
1305 r = self.session.patch(
1306 self.url("/api/v1/servers/localhost/zones/" + name),
1307 data=json.dumps(payload),
1308 headers={'content-type': 'application/json'})
4bfebc93 1309 self.assertEqual(r.status_code, 422)
e3675a8a
CH
1310 self.assertIn('Duplicate record in RRset', r.json()['error'])
1311
90904988
PD
1312 def test_zone_rr_update_duplicate_rrset(self):
1313 name, payload, zone = self.create_zone()
1314 rrset1 = {
1315 'changetype': 'replace',
1316 'name': name,
1317 'type': 'NS',
1318 'ttl': 3600,
1319 'records': [
1320 {
1321 "content": "ns9999.example.com.",
1322 "disabled": False
1323 }
1324 ]
1325 }
1326 rrset2 = {
1327 'changetype': 'replace',
1328 'name': name,
1329 'type': 'NS',
1330 'ttl': 3600,
1331 'records': [
1332 {
1333 "content": "ns9998.example.com.",
1334 "disabled": False
1335 }
1336 ]
1337 }
1338 payload = {'rrsets': [rrset1, rrset2]}
1339 r = self.session.patch(
1340 self.url("/api/v1/servers/localhost/zones/" + name),
1341 data=json.dumps(payload),
1342 headers={'content-type': 'application/json'})
4bfebc93 1343 self.assertEqual(r.status_code, 422)
90904988
PD
1344 self.assertIn('Duplicate RRset', r.json()['error'])
1345
c1374bdb 1346 def test_zone_rr_delete(self):
6754ef71 1347 name, payload, zone = self.create_zone()
b3905a3d 1348 # do a delete of all NS records (these are created with the zone)
d708640f 1349 rrset = {
b3905a3d
CH
1350 'changetype': 'delete',
1351 'name': name,
1352 'type': 'NS'
1353 }
d708640f 1354 payload = {'rrsets': [rrset]}
b3905a3d 1355 r = self.session.patch(
46d06a12 1356 self.url("/api/v1/servers/localhost/zones/" + name),
b3905a3d
CH
1357 data=json.dumps(payload),
1358 headers={'content-type': 'application/json'})
f0e76cee 1359 self.assert_success(r)
b3905a3d 1360 # verify that the records are gone
f0e76cee 1361 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
6754ef71 1362 self.assertIsNone(get_rrset(data, name, 'NS'))
cea26350 1363
e732e9cc
JE
1364 def test_zone_rr_update_rrset_combine_replace_and_delete(self):
1365 name, payload, zone = self.create_zone()
1366 rrset1 = {
1367 'changetype': 'delete',
1368 'name': 'sub.' + name,
1369 'type': 'CNAME',
1370 }
1371 rrset2 = {
1372 'changetype': 'replace',
1373 'name': 'sub.' + name,
1374 'type': 'CNAME',
1375 'ttl': 500,
1376 'records': [
1377 {
1378 "content": "www.example.org.",
1379 "disabled": False
1380 }
1381 ]
1382 }
1383 payload = {'rrsets': [rrset1, rrset2]}
1384 r = self.session.patch(
1385 self.url("/api/v1/servers/localhost/zones/" + name),
1386 data=json.dumps(payload),
1387 headers={'content-type': 'application/json'})
1388 self.assert_success(r)
1389 # verify that (only) the new record is there
1390 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
4bfebc93 1391 self.assertEqual(get_rrset(data, 'sub.' + name, 'CNAME')['records'], rrset2['records'])
e732e9cc 1392
c1374bdb 1393 def test_zone_disable_reenable(self):
d29d5db7 1394 # This also tests that SOA-EDIT-API works.
6754ef71 1395 name, payload, zone = self.create_zone(soa_edit_api='EPOCH')
cea26350 1396 # disable zone by disabling SOA
d708640f 1397 rrset = {
cea26350
CH
1398 'changetype': 'replace',
1399 'name': name,
1400 'type': 'SOA',
6754ef71 1401 'ttl': 3600,
cea26350
CH
1402 'records': [
1403 {
1d6b70f9 1404 "content": "ns1.bar.com. hostmaster.foo.org. 1 1 1 1 1",
cea26350
CH
1405 "disabled": True
1406 }
1407 ]
1408 }
d708640f 1409 payload = {'rrsets': [rrset]}
cea26350 1410 r = self.session.patch(
46d06a12 1411 self.url("/api/v1/servers/localhost/zones/" + name),
cea26350
CH
1412 data=json.dumps(payload),
1413 headers={'content-type': 'application/json'})
f0e76cee 1414 self.assert_success(r)
d29d5db7 1415 # check SOA serial has been edited
f0e76cee
CH
1416 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1417 soa_serial1 = get_first_rec(data, name, 'SOA')['content'].split()[2]
4bfebc93 1418 self.assertNotEqual(soa_serial1, '1')
d29d5db7 1419 # make sure domain is still in zone list (disabled SOA!)
46d06a12 1420 r = self.session.get(self.url("/api/v1/servers/localhost/zones"))
cea26350 1421 domains = r.json()
4bfebc93 1422 self.assertEqual(len([domain for domain in domains if domain['name'] == name]), 1)
d29d5db7
CH
1423 # sleep 1sec to ensure the EPOCH value changes for the next request
1424 time.sleep(1)
cea26350 1425 # verify that modifying it still works
d708640f
CH
1426 rrset['records'][0]['disabled'] = False
1427 payload = {'rrsets': [rrset]}
cea26350 1428 r = self.session.patch(
46d06a12 1429 self.url("/api/v1/servers/localhost/zones/" + name),
cea26350
CH
1430 data=json.dumps(payload),
1431 headers={'content-type': 'application/json'})
f0e76cee 1432 self.assert_success(r)
d29d5db7 1433 # check SOA serial has been edited again
f0e76cee
CH
1434 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1435 soa_serial2 = get_first_rec(data, name, 'SOA')['content'].split()[2]
4bfebc93
CH
1436 self.assertNotEqual(soa_serial2, '1')
1437 self.assertNotEqual(soa_serial2, soa_serial1)
02945d9a 1438
c1374bdb 1439 def test_zone_rr_update_out_of_zone(self):
6754ef71 1440 name, payload, zone = self.create_zone()
35f26cc5 1441 # replace with qname mismatch
d708640f 1442 rrset = {
35f26cc5 1443 'changetype': 'replace',
1d6b70f9 1444 'name': 'not-in-zone.',
35f26cc5 1445 'type': 'NS',
6754ef71 1446 'ttl': 3600,
35f26cc5
CH
1447 'records': [
1448 {
1d6b70f9 1449 "content": "ns1.bar.com.",
35f26cc5
CH
1450 "disabled": False
1451 }
1452 ]
1453 }
d708640f 1454 payload = {'rrsets': [rrset]}
35f26cc5 1455 r = self.session.patch(
46d06a12 1456 self.url("/api/v1/servers/localhost/zones/" + name),
35f26cc5
CH
1457 data=json.dumps(payload),
1458 headers={'content-type': 'application/json'})
4bfebc93 1459 self.assertEqual(r.status_code, 422)
35f26cc5
CH
1460 self.assertIn('out of zone', r.json()['error'])
1461
1d6b70f9 1462 def test_zone_rr_update_restricted_chars(self):
6754ef71 1463 name, payload, zone = self.create_zone()
1d6b70f9
CH
1464 # replace with qname mismatch
1465 rrset = {
1466 'changetype': 'replace',
1467 'name': 'test:' + name,
1468 'type': 'NS',
6754ef71 1469 'ttl': 3600,
1d6b70f9
CH
1470 'records': [
1471 {
1d6b70f9
CH
1472 "content": "ns1.bar.com.",
1473 "disabled": False
1474 }
1475 ]
1476 }
1477 payload = {'rrsets': [rrset]}
1478 r = self.session.patch(
1479 self.url("/api/v1/servers/localhost/zones/" + name),
1480 data=json.dumps(payload),
1481 headers={'content-type': 'application/json'})
4bfebc93 1482 self.assertEqual(r.status_code, 422)
1d6b70f9
CH
1483 self.assertIn('contains unsupported characters', r.json()['error'])
1484
24cd86ca 1485 def test_rrset_unknown_type(self):
6754ef71 1486 name, payload, zone = self.create_zone()
24cd86ca
CH
1487 rrset = {
1488 'changetype': 'replace',
1489 'name': name,
1490 'type': 'FAFAFA',
6754ef71 1491 'ttl': 3600,
24cd86ca
CH
1492 'records': [
1493 {
24cd86ca
CH
1494 "content": "4.3.2.1",
1495 "disabled": False
1496 }
1497 ]
1498 }
1499 payload = {'rrsets': [rrset]}
46d06a12 1500 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
24cd86ca 1501 headers={'content-type': 'application/json'})
4bfebc93 1502 self.assertEqual(r.status_code, 422)
24cd86ca
CH
1503 self.assertIn('unknown type', r.json()['error'])
1504
646bcd7d
CH
1505 @parameterized.expand([
1506 ('CNAME', ),
646bcd7d
CH
1507 ])
1508 def test_rrset_exclusive_and_other(self, qtype):
8560f36a
CH
1509 name, payload, zone = self.create_zone()
1510 rrset = {
1511 'changetype': 'replace',
1512 'name': name,
646bcd7d 1513 'type': qtype,
8560f36a
CH
1514 'ttl': 3600,
1515 'records': [
1516 {
1517 "content": "example.org.",
1518 "disabled": False
1519 }
1520 ]
1521 }
1522 payload = {'rrsets': [rrset]}
1523 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1524 headers={'content-type': 'application/json'})
4bfebc93 1525 self.assertEqual(r.status_code, 422)
646bcd7d 1526 self.assertIn('Conflicts with pre-existing RRset', r.json()['error'])
8560f36a 1527
646bcd7d
CH
1528 @parameterized.expand([
1529 ('CNAME', ),
646bcd7d
CH
1530 ])
1531 def test_rrset_other_and_exclusive(self, qtype):
8560f36a
CH
1532 name, payload, zone = self.create_zone()
1533 rrset = {
1534 'changetype': 'replace',
1535 'name': 'sub.'+name,
646bcd7d 1536 'type': qtype,
8560f36a
CH
1537 'ttl': 3600,
1538 'records': [
1539 {
1540 "content": "example.org.",
1541 "disabled": False
1542 }
1543 ]
1544 }
1545 payload = {'rrsets': [rrset]}
1546 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1547 headers={'content-type': 'application/json'})
1548 self.assert_success(r)
1549 rrset = {
1550 'changetype': 'replace',
1551 'name': 'sub.'+name,
1552 'type': 'A',
1553 'ttl': 3600,
1554 'records': [
1555 {
1556 "content": "1.2.3.4",
1557 "disabled": False
1558 }
1559 ]
1560 }
1561 payload = {'rrsets': [rrset]}
1562 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1563 headers={'content-type': 'application/json'})
4bfebc93 1564 self.assertEqual(r.status_code, 422)
646bcd7d
CH
1565 self.assertIn('Conflicts with pre-existing RRset', r.json()['error'])
1566
1567 @parameterized.expand([
2eb206ec
KM
1568 ('', 'SOA', ['ns1.example.org. test@example.org. 10 10800 3600 604800 3600', 'ns2.example.org. test@example.org. 10 10800 3600 604800 3600']),
1569 ('sub.', 'CNAME', ['01.example.org.', '02.example.org.']),
646bcd7d 1570 ])
2eb206ec 1571 def test_rrset_single_qtypes(self, label, qtype, contents):
8b1fa85d
RG
1572 name, payload, zone = self.create_zone()
1573 rrset = {
1574 'changetype': 'replace',
2eb206ec 1575 'name': label + name,
646bcd7d 1576 'type': qtype,
8b1fa85d
RG
1577 'ttl': 3600,
1578 'records': [
1579 {
646bcd7d 1580 "content": contents[0],
8b1fa85d
RG
1581 "disabled": False
1582 },
1583 {
646bcd7d 1584 "content": contents[1],
8b1fa85d
RG
1585 "disabled": False
1586 }
1587 ]
1588 }
1589 payload = {'rrsets': [rrset]}
1590 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1591 headers={'content-type': 'application/json'})
4bfebc93 1592 self.assertEqual(r.status_code, 422)
646bcd7d 1593 self.assertIn('IN ' + qtype + ' has more than one record', r.json()['error'])
8b1fa85d 1594
d2b117ec
AT
1595 def test_rrset_zone_apex(self):
1596 name, payload, zone = self.create_zone()
1597 rrset1 = {
1598 'changetype': 'replace',
1599 'name': name,
1600 'type': 'SOA',
1601 'ttl': 3600,
1602 'records': [
1603 {
1604 "content": 'ns1.example.org. test@example.org. 10 10800 3600 604800 3600',
1605 "disabled": False
1606 },
1607 ]
1608 }
1609 rrset2 = {
1610 'changetype': 'replace',
1611 'name': name,
1612 'type': 'DNAME',
1613 'ttl': 3600,
1614 'records': [
1615 {
1616 "content": 'example.com.',
1617 "disabled": False
1618 },
1619 ]
1620 }
1621
1622 payload = {'rrsets': [rrset1, rrset2]}
1623 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1624 headers={'content-type': 'application/json'})
1625 self.assert_success(r) # user should be able to create DNAME at APEX as per RFC 6672 section 2.3
1626
2eb206ec
KM
1627 @parameterized.expand([
1628 ('SOA', 'ns1.example.org. test@example.org. 10 10800 3600 604800 1800'),
1629 ('DNSKEY', '257 3 8 AwEAAb/+pXOZWYQ8mv9WM5dFva8WU9jcIUdDuEjldbyfnkQ/xlrJC5zAEfhYhrea3SmIPmMTDimLqbh3/4SMTNPTUF+9+U1vpNfIRTFadqsmuU9Fddz3JqCcYwEpWbReg6DJOeyu+9oBoIQkPxFyLtIXEPGlQzrynKubn04Cx83I6NfzDTraJT3jLHKeW5PVc1ifqKzHz5TXdHHTA7NkJAa0sPcZCoNE1LpnJI/wcUpRUiuQhoLFeT1E432GuPuZ7y+agElGj0NnBxEgnHrhrnZWUbULpRa/il+Cr5Taj988HqX9Xdm6FjcP4Lbuds/44U7U8du224Q8jTrZ57Yvj4VDQKc='),
2eb206ec
KM
1630 ])
1631 def test_only_at_apex(self, qtype, content):
1632 name, payload, zone = self.create_zone(soa_edit_api='')
1633 rrset = {
1634 'changetype': 'replace',
1635 'name': name,
1636 'type': qtype,
1637 'ttl': 3600,
1638 'records': [
1639 {
1640 "content": content,
1641 "disabled": False
1642 },
1643 ]
1644 }
1645 payload = {'rrsets': [rrset]}
1646 r = self.session.patch(
1647 self.url("/api/v1/servers/localhost/zones/" + name),
1648 data=json.dumps(payload),
1649 headers={'content-type': 'application/json'})
1650 self.assert_success(r)
1651 # verify that the new record is there
1652 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1653 self.assertEqual(get_rrset(data, name, qtype)['records'], rrset['records'])
1654
1655 rrset = {
1656 'changetype': 'replace',
1657 'name': 'sub.' + name,
1658 'type': qtype,
1659 'ttl': 3600,
1660 'records': [
1661 {
1662 "content": content,
1663 "disabled": False
1664 },
1665 ]
1666 }
1667 payload = {'rrsets': [rrset]}
1668 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1669 headers={'content-type': 'application/json'})
1670 self.assertEqual(r.status_code, 422)
1671 self.assertIn('only allowed at apex', r.json()['error'])
1672 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1673 self.assertIsNone(get_rrset(data, 'sub.' + name, qtype))
1674
1675 @parameterized.expand([
1676 ('DS', '44030 8 2 d4c3d5552b8679faeebc317e5f048b614b2e5f607dc57f1553182d49ab2179f7'),
1677 ])
1678 def test_not_allowed_at_apex(self, qtype, content):
1679 name, payload, zone = self.create_zone()
1680 rrset = {
1681 'changetype': 'replace',
1682 'name': name,
1683 'type': qtype,
1684 'ttl': 3600,
1685 'records': [
1686 {
1687 "content": content,
1688 "disabled": False
1689 },
1690 ]
1691 }
1692 payload = {'rrsets': [rrset]}
1693 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1694 headers={'content-type': 'application/json'})
1695 self.assertEqual(r.status_code, 422)
1696 self.assertIn('not allowed at apex', r.json()['error'])
1697 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1698 self.assertIsNone(get_rrset(data, 'sub.' + name, qtype))
1699
1700 rrset = {
1701 'changetype': 'replace',
1702 'name': 'sub.' + name,
1703 'type': qtype,
1704 'ttl': 3600,
1705 'records': [
1706 {
1707 "content": content,
1708 "disabled": False
1709 },
1710 ]
1711 }
1712 payload = {'rrsets': [rrset]}
1713 r = self.session.patch(
1714 self.url("/api/v1/servers/localhost/zones/" + name),
1715 data=json.dumps(payload),
1716 headers={'content-type': 'application/json'})
1717 self.assert_success(r)
1718 # verify that the new record is there
1719 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1720 self.assertEqual(get_rrset(data, 'sub.' + name, qtype)['records'], rrset['records'])
1721
0ab5d78e
PL
1722 def test_rr_svcb(self):
1723 name, payload, zone = self.create_zone()
1724 rrset = {
1725 'changetype': 'replace',
1726 'name': 'svcb.' + name,
1727 'type': 'SVCB',
1728 'ttl': 3600,
1729 'records': [
1730 {
4f254e34 1731 "content": '40 . mandatory=alpn alpn=h2,h3 ipv4hint=192.0.2.1,192.0.2.2 ech="dG90YWxseSBib2d1cyBlY2hjb25maWcgdmFsdWU="',
0ab5d78e
PL
1732 "disabled": False
1733 },
1734 ]
1735 }
1736 payload = {'rrsets': [rrset]}
1737 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1738 headers={'content-type': 'application/json'})
1739 self.assert_success(r)
1740
d2b117ec
AT
1741 def test_rrset_ns_dname_exclude(self):
1742 name, payload, zone = self.create_zone()
1743 rrset = {
1744 'changetype': 'replace',
1745 'name': 'delegation.'+name,
1746 'type': 'NS',
1747 'ttl': 3600,
1748 'records': [
1749 {
1750 "content": "ns.example.org.",
1751 "disabled": False
1752 }
1753 ]
1754 }
1755 payload = {'rrsets': [rrset]}
1756 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1757 headers={'content-type': 'application/json'})
1758 self.assert_success(r)
1759 rrset = {
1760 'changetype': 'replace',
1761 'name': 'delegation.'+name,
1762 'type': 'DNAME',
1763 'ttl': 3600,
1764 'records': [
1765 {
7bc54205 1766 "content": "example.com.",
d2b117ec
AT
1767 "disabled": False
1768 }
1769 ]
1770 }
1771 payload = {'rrsets': [rrset]}
1772 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1773 headers={'content-type': 'application/json'})
4bfebc93 1774 self.assertEqual(r.status_code, 422)
d2b117ec
AT
1775 self.assertIn('Cannot have both NS and DNAME except in zone apex', r.json()['error'])
1776
7bc54205
AT
1777## FIXME: Enable this when it's time for it
1778# def test_rrset_dname_nothing_under(self):
1779# name, payload, zone = self.create_zone()
1780# rrset = {
1781# 'changetype': 'replace',
1782# 'name': 'delegation.'+name,
1783# 'type': 'DNAME',
1784# 'ttl': 3600,
1785# 'records': [
1786# {
1787# "content": "example.com.",
1788# "disabled": False
1789# }
1790# ]
1791# }
1792# payload = {'rrsets': [rrset]}
1793# r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1794# headers={'content-type': 'application/json'})
1795# self.assert_success(r)
1796# rrset = {
1797# 'changetype': 'replace',
1798# 'name': 'sub.delegation.'+name,
1799# 'type': 'A',
1800# 'ttl': 3600,
1801# 'records': [
1802# {
1803# "content": "1.2.3.4",
1804# "disabled": False
1805# }
1806# ]
1807# }
1808# payload = {'rrsets': [rrset]}
1809# r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1810# headers={'content-type': 'application/json'})
4bfebc93 1811# self.assertEqual(r.status_code, 422)
7bc54205
AT
1812# self.assertIn('You cannot have record(s) under CNAME/DNAME', r.json()['error'])
1813
1e5b9ab9
CH
1814 def test_create_zone_with_leading_space(self):
1815 # Actual regression.
6754ef71 1816 name, payload, zone = self.create_zone()
1e5b9ab9
CH
1817 rrset = {
1818 'changetype': 'replace',
1819 'name': name,
1820 'type': 'A',
6754ef71 1821 'ttl': 3600,
1e5b9ab9
CH
1822 'records': [
1823 {
1e5b9ab9
CH
1824 "content": " 4.3.2.1",
1825 "disabled": False
1826 }
1827 ]
1828 }
1829 payload = {'rrsets': [rrset]}
46d06a12 1830 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1e5b9ab9 1831 headers={'content-type': 'application/json'})
4bfebc93 1832 self.assertEqual(r.status_code, 422)
1e5b9ab9
CH
1833 self.assertIn('Not in expected format', r.json()['error'])
1834
d1b98434 1835 @unittest.skipIf(is_auth_lmdb(), "No out-of-zone storage in LMDB")
c1374bdb 1836 def test_zone_rr_delete_out_of_zone(self):
6754ef71 1837 name, payload, zone = self.create_zone()
d708640f 1838 rrset = {
35f26cc5 1839 'changetype': 'delete',
1d6b70f9 1840 'name': 'not-in-zone.',
35f26cc5
CH
1841 'type': 'NS'
1842 }
d708640f 1843 payload = {'rrsets': [rrset]}
35f26cc5 1844 r = self.session.patch(
46d06a12 1845 self.url("/api/v1/servers/localhost/zones/" + name),
35f26cc5
CH
1846 data=json.dumps(payload),
1847 headers={'content-type': 'application/json'})
541bb91b 1848 print(r.content)
f0e76cee 1849 self.assert_success(r) # succeed so users can fix their wrong, old data
35f26cc5 1850
37663c3b 1851 def test_zone_delete(self):
6754ef71 1852 name, payload, zone = self.create_zone()
46d06a12 1853 r = self.session.delete(self.url("/api/v1/servers/localhost/zones/" + name))
4bfebc93 1854 self.assertEqual(r.status_code, 204)
37663c3b
CH
1855 self.assertNotIn('Content-Type', r.headers)
1856
c1374bdb 1857 def test_zone_comment_create(self):
6754ef71 1858 name, payload, zone = self.create_zone()
d708640f 1859 rrset = {
6cc98ddf
CH
1860 'changetype': 'replace',
1861 'name': name,
1862 'type': 'NS',
6754ef71 1863 'ttl': 3600,
6cc98ddf
CH
1864 'comments': [
1865 {
1866 'account': 'test1',
1867 'content': 'blah blah',
1868 },
1869 {
1870 'account': 'test2',
1871 'content': 'blah blah bleh',
1872 }
1873 ]
1874 }
d708640f 1875 payload = {'rrsets': [rrset]}
6cc98ddf 1876 r = self.session.patch(
46d06a12 1877 self.url("/api/v1/servers/localhost/zones/" + name),
6cc98ddf
CH
1878 data=json.dumps(payload),
1879 headers={'content-type': 'application/json'})
f626cc48
PD
1880 if is_auth_lmdb():
1881 self.assert_error_json(r) # No comments in LMDB
1882 return
1883 else:
1884 self.assert_success(r)
6cc98ddf
CH
1885 # make sure the comments have been set, and that the NS
1886 # records are still present
f0e76cee
CH
1887 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1888 serverset = get_rrset(data, name, 'NS')
541bb91b 1889 print(serverset)
4bfebc93
CH
1890 self.assertNotEqual(serverset['records'], [])
1891 self.assertNotEqual(serverset['comments'], [])
6cc98ddf 1892 # verify that modified_at has been set by pdns
4bfebc93 1893 self.assertNotEqual([c for c in serverset['comments']][0]['modified_at'], 0)
0d7f3c75 1894 # verify that TTL is correct (regression test)
4bfebc93 1895 self.assertEqual(serverset['ttl'], 3600)
6cc98ddf 1896
c1374bdb 1897 def test_zone_comment_delete(self):
6cc98ddf 1898 # Test: Delete ONLY comments.
6754ef71 1899 name, payload, zone = self.create_zone()
d708640f 1900 rrset = {
6cc98ddf
CH
1901 'changetype': 'replace',
1902 'name': name,
1903 'type': 'NS',
1904 'comments': []
1905 }
d708640f 1906 payload = {'rrsets': [rrset]}
6cc98ddf 1907 r = self.session.patch(
46d06a12 1908 self.url("/api/v1/servers/localhost/zones/" + name),
6cc98ddf
CH
1909 data=json.dumps(payload),
1910 headers={'content-type': 'application/json'})
f0e76cee 1911 self.assert_success(r)
6cc98ddf 1912 # make sure the NS records are still present
f0e76cee
CH
1913 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1914 serverset = get_rrset(data, name, 'NS')
541bb91b 1915 print(serverset)
4bfebc93
CH
1916 self.assertNotEqual(serverset['records'], [])
1917 self.assertEqual(serverset['comments'], [])
6cc98ddf 1918
d1b98434 1919 @unittest.skipIf(is_auth_lmdb(), "No comments in LMDB")
1148587f
CH
1920 def test_zone_comment_out_of_range_modified_at(self):
1921 # Test if comments on an rrset stay intact if the rrset is replaced
1922 name, payload, zone = self.create_zone()
1923 rrset = {
1924 'changetype': 'replace',
1925 'name': name,
1926 'type': 'NS',
1927 'comments': [
1928 {
1929 'account': 'test1',
1930 'content': 'oh hi there',
1931 'modified_at': '4294967297'
1932 }
1933 ]
1934 }
1935 payload = {'rrsets': [rrset]}
1936 r = self.session.patch(
1937 self.url("/api/v1/servers/localhost/zones/" + name),
1938 data=json.dumps(payload),
1939 headers={'content-type': 'application/json'})
4bfebc93 1940 self.assertEqual(r.status_code, 422)
1148587f
CH
1941 self.assertIn("Value for key 'modified_at' is out of range", r.json()['error'])
1942
d1b98434 1943 @unittest.skipIf(is_auth_lmdb(), "No comments in LMDB")
c1374bdb 1944 def test_zone_comment_stay_intact(self):
6cc98ddf 1945 # Test if comments on an rrset stay intact if the rrset is replaced
6754ef71 1946 name, payload, zone = self.create_zone()
6cc98ddf 1947 # create a comment
d708640f 1948 rrset = {
6cc98ddf
CH
1949 'changetype': 'replace',
1950 'name': name,
1951 'type': 'NS',
1952 'comments': [
1953 {
1954 'account': 'test1',
1955 'content': 'oh hi there',
2696eea0 1956 'modified_at': 1111
6cc98ddf
CH
1957 }
1958 ]
1959 }
d708640f 1960 payload = {'rrsets': [rrset]}
6cc98ddf 1961 r = self.session.patch(
46d06a12 1962 self.url("/api/v1/servers/localhost/zones/" + name),
6cc98ddf
CH
1963 data=json.dumps(payload),
1964 headers={'content-type': 'application/json'})
f0e76cee 1965 self.assert_success(r)
6cc98ddf 1966 # replace rrset records
d708640f 1967 rrset2 = {
6cc98ddf
CH
1968 'changetype': 'replace',
1969 'name': name,
1970 'type': 'NS',
6754ef71 1971 'ttl': 3600,
6cc98ddf
CH
1972 'records': [
1973 {
1d6b70f9 1974 "content": "ns1.bar.com.",
6cc98ddf
CH
1975 "disabled": False
1976 }
1977 ]
1978 }
d708640f 1979 payload2 = {'rrsets': [rrset2]}
6cc98ddf 1980 r = self.session.patch(
46d06a12 1981 self.url("/api/v1/servers/localhost/zones/" + name),
6cc98ddf
CH
1982 data=json.dumps(payload2),
1983 headers={'content-type': 'application/json'})
f0e76cee 1984 self.assert_success(r)
6cc98ddf 1985 # make sure the comments still exist
f0e76cee
CH
1986 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1987 serverset = get_rrset(data, name, 'NS')
541bb91b 1988 print(serverset)
4bfebc93
CH
1989 self.assertEqual(serverset['records'], rrset2['records'])
1990 self.assertEqual(serverset['comments'], rrset['comments'])
6cc98ddf 1991
d1b98434 1992 @unittest.skipIf(is_auth_lmdb(), "No search in LMDB")
c1374bdb 1993 def test_search_rr_exact_zone(self):
b1902fab 1994 name = unique_zone_name()
1d6b70f9
CH
1995 self.create_zone(name=name, serial=22, soa_edit_api='')
1996 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=" + name.rstrip('.')))
c1374bdb 1997 self.assert_success_json(r)
541bb91b 1998 print(r.json())
7386e38f 1999 self.assertCountEqual(r.json(), [
1d6b70f9 2000 {u'object_type': u'zone', u'name': name, u'zone_id': name},
1d6b70f9
CH
2001 {u'content': u'ns1.example.com.',
2002 u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
2003 u'ttl': 3600, u'type': u'NS', u'name': name},
2004 {u'content': u'ns2.example.com.',
2005 u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
2006 u'ttl': 3600, u'type': u'NS', u'name': name},
f527c6ff 2007 {u'content': u'a.misconfigured.dns.server.invalid. hostmaster.'+name+' 22 10800 3600 604800 3600',
45250285
JE
2008 u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
2009 u'ttl': 3600, u'type': u'SOA', u'name': name},
2010 ])
2011
d1b98434 2012 @unittest.skipIf(is_auth_lmdb(), "No search in LMDB")
45250285
JE
2013 def test_search_rr_exact_zone_filter_type_zone(self):
2014 name = unique_zone_name()
2015 data_type = "zone"
2016 self.create_zone(name=name, serial=22, soa_edit_api='')
0bd91de6 2017 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=" + name.rstrip('.') + "&object_type=" + data_type))
45250285
JE
2018 self.assert_success_json(r)
2019 print(r.json())
4bfebc93 2020 self.assertEqual(r.json(), [
45250285
JE
2021 {u'object_type': u'zone', u'name': name, u'zone_id': name},
2022 ])
2023
d1b98434 2024 @unittest.skipIf(is_auth_lmdb(), "No search in LMDB")
45250285
JE
2025 def test_search_rr_exact_zone_filter_type_record(self):
2026 name = unique_zone_name()
2027 data_type = "record"
2028 self.create_zone(name=name, serial=22, soa_edit_api='')
0bd91de6 2029 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=" + name.rstrip('.') + "&object_type=" + data_type))
45250285
JE
2030 self.assert_success_json(r)
2031 print(r.json())
7386e38f 2032 self.assertCountEqual(r.json(), [
45250285
JE
2033 {u'content': u'ns1.example.com.',
2034 u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
2035 u'ttl': 3600, u'type': u'NS', u'name': name},
2036 {u'content': u'ns2.example.com.',
2037 u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
2038 u'ttl': 3600, u'type': u'NS', u'name': name},
f527c6ff 2039 {u'content': u'a.misconfigured.dns.server.invalid. hostmaster.'+name+' 22 10800 3600 604800 3600',
f2d6dcc0
RG
2040 u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
2041 u'ttl': 3600, u'type': u'SOA', u'name': name},
1d6b70f9 2042 ])
b1902fab 2043
d1b98434 2044 @unittest.skipIf(is_auth_lmdb(), "No search in LMDB")
c1374bdb 2045 def test_search_rr_substring(self):
541bb91b
CH
2046 name = unique_zone_name()
2047 search = name[5:-5]
b1902fab 2048 self.create_zone(name=name)
541bb91b 2049 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=*%s*" % search))
c1374bdb 2050 self.assert_success_json(r)
541bb91b 2051 print(r.json())
b1902fab 2052 # should return zone, SOA, ns1, ns2
4bfebc93 2053 self.assertEqual(len(r.json()), 4)
b1902fab 2054
d1b98434 2055 @unittest.skipIf(is_auth_lmdb(), "No search in LMDB")
c1374bdb 2056 def test_search_rr_case_insensitive(self):
541bb91b 2057 name = unique_zone_name()+'testsuffix.'
57cb86d8 2058 self.create_zone(name=name)
541bb91b 2059 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=*testSUFFIX*"))
c1374bdb 2060 self.assert_success_json(r)
541bb91b 2061 print(r.json())
57cb86d8 2062 # should return zone, SOA, ns1, ns2
4bfebc93 2063 self.assertEqual(len(r.json()), 4)
57cb86d8 2064
478a7402
PL
2065 @unittest.skipIf(is_auth_lmdb(), "No search or comments in LMDB")
2066 def test_search_rr_comment(self):
2067 name = unique_zone_name()
2068 rrsets = [{
2069 "name": name,
2070 "type": "AAAA",
2071 "ttl": 3600,
2072 "records": [{
2073 "content": "2001:DB8::1",
2074 "disabled": False,
2075 }],
2076 "comments": [{
2077 "account": "test AAAA",
2078 "content": "blah",
2079 "modified_at": 11112,
2080 }],
2081 }]
2082 name, payload, data = self.create_zone(name=name, rrsets=rrsets)
2083 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=blah"))
2084 self.assert_success_json(r)
2085 data = r.json()
2086 # should return the AAAA record
4bfebc93 2087 self.assertEqual(len(data), 1)
478a7402
PL
2088 self.assertEqual(data[0]['object_type'], 'comment')
2089 self.assertEqual(data[0]['type'], 'AAAA')
2090 self.assertEqual(data[0]['name'], name)
2091 self.assertEqual(data[0]['content'], rrsets[0]['comments'][0]['content'])
2092
d1b98434 2093 @unittest.skipIf(is_auth_lmdb(), "No search in LMDB")
7cbc5255 2094 def test_search_after_rectify_with_ent(self):
541bb91b
CH
2095 name = unique_zone_name()
2096 search = name.split('.')[0]
7cbc5255
CH
2097 rrset = {
2098 "name": 'sub.sub.' + name,
2099 "type": "A",
2100 "ttl": 3600,
2101 "records": [{
2102 "content": "4.3.2.1",
2103 "disabled": False,
2104 }],
2105 }
2106 self.create_zone(name=name, rrsets=[rrset])
2107 pdnsutil_rectify(name)
541bb91b 2108 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=*%s*" % search))
7cbc5255 2109 self.assert_success_json(r)
541bb91b 2110 print(r.json())
7cbc5255 2111 # should return zone, SOA, ns1, ns2, sub.sub A (but not the ENT)
4bfebc93 2112 self.assertEqual(len(r.json()), 5)
7cbc5255 2113
d1b98434 2114 @unittest.skipIf(is_auth_lmdb(), "No get_db_records for LMDB")
168a76b3 2115 def test_default_api_rectify_dnssec(self):
b8cd24cc 2116 name = unique_zone_name()
b8cd24cc
SH
2117 rrsets = [
2118 {
2119 "name": 'a.' + name,
2120 "type": "AAAA",
2121 "ttl": 3600,
2122 "records": [{
2123 "content": "2001:DB8::1",
2124 "disabled": False,
2125 }],
2126 },
2127 {
2128 "name": 'b.' + name,
2129 "type": "AAAA",
2130 "ttl": 3600,
2131 "records": [{
2132 "content": "2001:DB8::2",
2133 "disabled": False,
2134 }],
2135 },
2136 ]
2137 self.create_zone(name=name, rrsets=rrsets, dnssec=True, nsec3param='1 0 1 ab')
2138 dbrecs = get_db_records(name, 'AAAA')
2139 self.assertIsNotNone(dbrecs[0]['ordername'])
2140
168a76b3
CH
2141 def test_default_api_rectify_nodnssec(self):
2142 """Without any DNSSEC settings, rectify should still add ENTs. Setup the zone
2143 so ENTs are necessary, and check for their existence using sdig.
2144 """
2145 name = unique_zone_name()
2146 rrsets = [
2147 {
2148 "name": 'a.sub.' + name,
2149 "type": "AAAA",
2150 "ttl": 3600,
2151 "records": [{
2152 "content": "2001:DB8::1",
2153 "disabled": False,
2154 }],
2155 },
2156 {
2157 "name": 'b.sub.' + name,
2158 "type": "AAAA",
2159 "ttl": 3600,
2160 "records": [{
2161 "content": "2001:DB8::2",
2162 "disabled": False,
2163 }],
2164 },
2165 ]
2166 self.create_zone(name=name, rrsets=rrsets)
2167 # default-api-rectify is yes (by default). expect rectify to have happened.
2168 assert 'Rcode: 0 ' in sdig('sub.' + name, 'TXT')
2169
d1b98434 2170 @unittest.skipIf(is_auth_lmdb(), "No get_db_records for LMDB")
b8cd24cc
SH
2171 def test_override_api_rectify(self):
2172 name = unique_zone_name()
2173 search = name.split('.')[0]
2174 rrsets = [
2175 {
2176 "name": 'a.' + name,
2177 "type": "AAAA",
2178 "ttl": 3600,
2179 "records": [{
2180 "content": "2001:DB8::1",
2181 "disabled": False,
2182 }],
2183 },
2184 {
2185 "name": 'b.' + name,
2186 "type": "AAAA",
2187 "ttl": 3600,
2188 "records": [{
2189 "content": "2001:DB8::2",
2190 "disabled": False,
2191 }],
2192 },
2193 ]
2194 self.create_zone(name=name, rrsets=rrsets, api_rectify=False, dnssec=True, nsec3param='1 0 1 ab')
2195 dbrecs = get_db_records(name, 'AAAA')
2196 self.assertIsNone(dbrecs[0]['ordername'])
2197
d1b98434 2198 @unittest.skipIf(is_auth_lmdb(), "No get_db_records for LMDB")
a6185e05
CH
2199 def test_explicit_rectify_success(self):
2200 name, _, data = self.create_zone = self.create_zone(api_rectify=False, dnssec=True, nsec3param='1 0 1 ab')
2201 dbrecs = get_db_records(name, 'SOA')
2202 self.assertIsNone(dbrecs[0]['ordername'])
2203 r = self.session.put(self.url("/api/v1/servers/localhost/zones/" + data['id'] + "/rectify"))
4bfebc93 2204 self.assertEqual(r.status_code, 200)
a6185e05
CH
2205 dbrecs = get_db_records(name, 'SOA')
2206 self.assertIsNotNone(dbrecs[0]['ordername'])
2207
a6185e05
CH
2208 def test_explicit_rectify_slave(self):
2209 # Some users want to move a zone to kind=Slave and then rectify, without a re-transfer.
2210 name, _, data = self.create_zone = self.create_zone(api_rectify=False, dnssec=True, nsec3param='1 0 1 ab')
2211 r = self.session.put(self.url("/api/v1/servers/localhost/zones/" + data['id']),
2212 data=json.dumps({'kind': 'Slave'}),
2213 headers={'content-type': 'application/json'})
4bfebc93 2214 self.assertEqual(r.status_code, 204)
a6185e05 2215 r = self.session.put(self.url("/api/v1/servers/localhost/zones/" + data['id'] + "/rectify"))
4bfebc93 2216 self.assertEqual(r.status_code, 200)
d1b98434
PD
2217 if not is_auth_lmdb():
2218 dbrecs = get_db_records(name, 'SOA')
2219 self.assertIsNotNone(dbrecs[0]['ordername'])
a6185e05 2220
03b1cc25 2221 def test_cname_at_ent_place(self):
f04b32e4 2222 name, payload, zone = self.create_zone(dnssec=True, api_rectify=True)
03b1cc25
CH
2223 rrset = {
2224 'changetype': 'replace',
2225 'name': 'sub2.sub1.' + name,
2226 'type': "A",
2227 'ttl': 3600,
2228 'records': [{
2229 'content': "4.3.2.1",
2230 'disabled': False,
2231 }],
2232 }
2233 payload = {'rrsets': [rrset]}
2234 r = self.session.patch(
2235 self.url("/api/v1/servers/localhost/zones/" + zone['id']),
2236 data=json.dumps(payload),
2237 headers={'content-type': 'application/json'})
4bfebc93 2238 self.assertEqual(r.status_code, 204)
03b1cc25
CH
2239 rrset = {
2240 'changetype': 'replace',
2241 'name': 'sub1.' + name,
2242 'type': "CNAME",
2243 'ttl': 3600,
2244 'records': [{
2245 'content': "www.example.org.",
2246 'disabled': False,
2247 }],
2248 }
2249 payload = {'rrsets': [rrset]}
2250 r = self.session.patch(
2251 self.url("/api/v1/servers/localhost/zones/" + zone['id']),
2252 data=json.dumps(payload),
2253 headers={'content-type': 'application/json'})
4bfebc93 2254 self.assertEqual(r.status_code, 204)
03b1cc25 2255
986e4858
PL
2256 def test_rrset_parameter_post_false(self):
2257 name = unique_zone_name()
2258 payload = {
2259 'name': name,
2260 'kind': 'Native',
2261 'nameservers': ['ns1.example.com.', 'ns2.example.com.']
2262 }
2263 r = self.session.post(
2264 self.url("/api/v1/servers/localhost/zones?rrsets=false"),
2265 data=json.dumps(payload),
2266 headers={'content-type': 'application/json'})
541bb91b 2267 print(r.json())
986e4858 2268 self.assert_success_json(r)
4bfebc93
CH
2269 self.assertEqual(r.status_code, 201)
2270 self.assertEqual(r.json().get('rrsets'), None)
986e4858
PL
2271
2272 def test_rrset_false_parameter(self):
2273 name = unique_zone_name()
2274 self.create_zone(name=name, kind='Native')
2275 r = self.session.get(self.url("/api/v1/servers/localhost/zones/"+name+"?rrsets=false"))
2276 self.assert_success_json(r)
541bb91b 2277 print(r.json())
4bfebc93 2278 self.assertEqual(r.json().get('rrsets'), None)
986e4858
PL
2279
2280 def test_rrset_true_parameter(self):
2281 name = unique_zone_name()
2282 self.create_zone(name=name, kind='Native')
2283 r = self.session.get(self.url("/api/v1/servers/localhost/zones/"+name+"?rrsets=true"))
2284 self.assert_success_json(r)
541bb91b 2285 print(r.json())
4bfebc93 2286 self.assertEqual(len(r.json().get('rrsets')), 2)
986e4858
PL
2287
2288 def test_wrong_rrset_parameter(self):
2289 name = unique_zone_name()
2290 self.create_zone(name=name, kind='Native')
2291 r = self.session.get(self.url("/api/v1/servers/localhost/zones/"+name+"?rrsets=foobar"))
4bfebc93 2292 self.assertEqual(r.status_code, 422)
986e4858
PL
2293 self.assertIn("'rrsets' request parameter value 'foobar' is not supported", r.json()['error'])
2294
dc30b8fd
PL
2295 def test_put_master_tsig_key_ids_non_existent(self):
2296 name = unique_zone_name()
2297 keyname = unique_zone_name().split('.')[0]
2298 self.create_zone(name=name, kind='Native')
2299 payload = {
2300 'master_tsig_key_ids': [keyname]
2301 }
2302 r = self.session.put(self.url('/api/v1/servers/localhost/zones/' + name),
2303 data=json.dumps(payload),
2304 headers={'content-type': 'application/json'})
4bfebc93 2305 self.assertEqual(r.status_code, 422)
dc30b8fd
PL
2306 self.assertIn('A TSIG key with the name', r.json()['error'])
2307
2308 def test_put_slave_tsig_key_ids_non_existent(self):
2309 name = unique_zone_name()
2310 keyname = unique_zone_name().split('.')[0]
2311 self.create_zone(name=name, kind='Native')
2312 payload = {
2313 'slave_tsig_key_ids': [keyname]
2314 }
2315 r = self.session.put(self.url('/api/v1/servers/localhost/zones/' + name),
2316 data=json.dumps(payload),
2317 headers={'content-type': 'application/json'})
4bfebc93 2318 self.assertEqual(r.status_code, 422)
dc30b8fd
PL
2319 self.assertIn('A TSIG key with the name', r.json()['error'])
2320
02945d9a 2321
406497f5
CH
2322@unittest.skipIf(not is_auth(), "Not applicable")
2323class AuthRootZone(ApiTestCase, AuthZonesHelperMixin):
2324
2325 def setUp(self):
2326 super(AuthRootZone, self).setUp()
2327 # zone name is not unique, so delete the zone before each individual test.
46d06a12 2328 self.session.delete(self.url("/api/v1/servers/localhost/zones/=2E"))
406497f5
CH
2329
2330 def test_create_zone(self):
6754ef71 2331 name, payload, data = self.create_zone(name='.', serial=22, soa_edit_api='')
406497f5
CH
2332 for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'soa_edit_api', 'soa_edit', 'account'):
2333 self.assertIn(k, data)
2334 if k in payload:
4bfebc93 2335 self.assertEqual(data[k], payload[k])
406497f5 2336 # validate generated SOA
6754ef71 2337 rec = get_first_rec(data, '.', 'SOA')
4bfebc93 2338 self.assertEqual(
6754ef71 2339 rec['content'],
f527c6ff 2340 "a.misconfigured.dns.server.invalid. hostmaster. " + str(payload['serial']) +
406497f5
CH
2341 " 10800 3600 604800 3600"
2342 )
2343 # Regression test: verify zone list works
46d06a12 2344 zonelist = self.session.get(self.url("/api/v1/servers/localhost/zones")).json()
541bb91b 2345 print("zonelist:", zonelist)
406497f5
CH
2346 self.assertIn(payload['name'], [zone['name'] for zone in zonelist])
2347 # Also test that fetching the zone works.
541bb91b 2348 print("id:", data['id'])
4bfebc93 2349 self.assertEqual(data['id'], '=2E')
46d06a12 2350 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + data['id'])).json()
541bb91b 2351 print("zone (fetched):", data)
406497f5
CH
2352 for k in ('name', 'kind'):
2353 self.assertIn(k, data)
4bfebc93 2354 self.assertEqual(data[k], payload[k])
6754ef71 2355 self.assertEqual(data['rrsets'][0]['name'], '.')
406497f5
CH
2356
2357 def test_update_zone(self):
6754ef71 2358 name, payload, zone = self.create_zone(name='.')
406497f5
CH
2359 zone_id = '=2E'
2360 # update, set as Master and enable SOA-EDIT-API
2361 payload = {
2362 'kind': 'Master',
2363 'masters': ['192.0.2.1', '192.0.2.2'],
2364 'soa_edit_api': 'EPOCH',
2365 'soa_edit': 'EPOCH'
2366 }
2367 r = self.session.put(
46d06a12 2368 self.url("/api/v1/servers/localhost/zones/" + zone_id),
406497f5
CH
2369 data=json.dumps(payload),
2370 headers={'content-type': 'application/json'})
f0e76cee
CH
2371 self.assert_success(r)
2372 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + zone_id)).json()
406497f5
CH
2373 for k in payload.keys():
2374 self.assertIn(k, data)
4bfebc93 2375 self.assertEqual(data[k], payload[k])
406497f5
CH
2376 # update, back to Native and empty(off)
2377 payload = {
2378 'kind': 'Native',
2379 'soa_edit_api': '',
2380 'soa_edit': ''
2381 }
2382 r = self.session.put(
46d06a12 2383 self.url("/api/v1/servers/localhost/zones/" + zone_id),
406497f5
CH
2384 data=json.dumps(payload),
2385 headers={'content-type': 'application/json'})
f0e76cee
CH
2386 self.assert_success(r)
2387 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + zone_id)).json()
406497f5
CH
2388 for k in payload.keys():
2389 self.assertIn(k, data)
4bfebc93 2390 self.assertEqual(data[k], payload[k])
406497f5
CH
2391
2392
c1374bdb 2393@unittest.skipIf(not is_recursor(), "Not applicable")
02945d9a
CH
2394class RecursorZones(ApiTestCase):
2395
37bc3d01
CH
2396 def create_zone(self, name=None, kind=None, rd=False, servers=None):
2397 if name is None:
2398 name = unique_zone_name()
2399 if servers is None:
2400 servers = []
02945d9a 2401 payload = {
37bc3d01
CH
2402 'name': name,
2403 'kind': kind,
2404 'servers': servers,
2405 'recursion_desired': rd
02945d9a
CH
2406 }
2407 r = self.session.post(
46d06a12 2408 self.url("/api/v1/servers/localhost/zones"),
02945d9a
CH
2409 data=json.dumps(payload),
2410 headers={'content-type': 'application/json'})
c1374bdb
CH
2411 self.assert_success_json(r)
2412 return payload, r.json()
37bc3d01 2413
c1374bdb 2414 def test_create_auth_zone(self):
37bc3d01 2415 payload, data = self.create_zone(kind='Native')
02945d9a 2416 for k in payload.keys():
4bfebc93 2417 self.assertEqual(data[k], payload[k])
02945d9a 2418
1d6b70f9 2419 def test_create_zone_no_name(self):
1d6b70f9
CH
2420 payload = {
2421 'name': '',
2422 'kind': 'Native',
2423 'servers': ['8.8.8.8'],
2424 'recursion_desired': False,
2425 }
541bb91b 2426 print(payload)
1d6b70f9
CH
2427 r = self.session.post(
2428 self.url("/api/v1/servers/localhost/zones"),
2429 data=json.dumps(payload),
2430 headers={'content-type': 'application/json'})
4bfebc93 2431 self.assertEqual(r.status_code, 422)
1d6b70f9
CH
2432 self.assertIn('is not canonical', r.json()['error'])
2433
c1374bdb 2434 def test_create_forwarded_zone(self):
37bc3d01 2435 payload, data = self.create_zone(kind='Forwarded', rd=False, servers=['8.8.8.8'])
02945d9a
CH
2436 # return values are normalized
2437 payload['servers'][0] += ':53'
02945d9a 2438 for k in payload.keys():
4bfebc93 2439 self.assertEqual(data[k], payload[k])
02945d9a 2440
c1374bdb 2441 def test_create_forwarded_rd_zone(self):
1d6b70f9 2442 payload, data = self.create_zone(name='google.com.', kind='Forwarded', rd=True, servers=['8.8.8.8'])
02945d9a
CH
2443 # return values are normalized
2444 payload['servers'][0] += ':53'
02945d9a 2445 for k in payload.keys():
4bfebc93 2446 self.assertEqual(data[k], payload[k])
02945d9a 2447
c1374bdb 2448 def test_create_auth_zone_with_symbols(self):
37bc3d01 2449 payload, data = self.create_zone(name='foo/bar.'+unique_zone_name(), kind='Native')
1dbe38ba 2450 expected_id = (payload['name'].replace('/', '=2F'))
02945d9a 2451 for k in payload.keys():
4bfebc93
CH
2452 self.assertEqual(data[k], payload[k])
2453 self.assertEqual(data['id'], expected_id)
e2367534 2454
c1374bdb 2455 def test_rename_auth_zone(self):
37bc3d01 2456 payload, data = self.create_zone(kind='Native')
1d6b70f9 2457 name = payload['name']
e2367534
CH
2458 # now rename it
2459 payload = {
2460 'name': 'renamed-'+name,
2461 'kind': 'Native',
2462 'recursion_desired': False
2463 }
2464 r = self.session.put(
46d06a12 2465 self.url("/api/v1/servers/localhost/zones/" + name),
e2367534
CH
2466 data=json.dumps(payload),
2467 headers={'content-type': 'application/json'})
f0e76cee
CH
2468 self.assert_success(r)
2469 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + payload['name'])).json()
e2367534 2470 for k in payload.keys():
4bfebc93 2471 self.assertEqual(data[k], payload[k])
37bc3d01 2472
37663c3b
CH
2473 def test_zone_delete(self):
2474 payload, zone = self.create_zone(kind='Native')
2475 name = payload['name']
46d06a12 2476 r = self.session.delete(self.url("/api/v1/servers/localhost/zones/" + name))
4bfebc93 2477 self.assertEqual(r.status_code, 204)
37663c3b
CH
2478 self.assertNotIn('Content-Type', r.headers)
2479
c1374bdb 2480 def test_search_rr_exact_zone(self):
1d6b70f9 2481 name = unique_zone_name()
37bc3d01 2482 self.create_zone(name=name, kind='Native')
46d06a12 2483 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=" + name))
c1374bdb 2484 self.assert_success_json(r)
541bb91b 2485 print(r.json())
4bfebc93 2486 self.assertEqual(r.json(), [{u'type': u'zone', u'name': name, u'zone_id': name}])
37bc3d01 2487
c1374bdb 2488 def test_search_rr_substring(self):
1d6b70f9 2489 name = 'search-rr-zone.name.'
37bc3d01 2490 self.create_zone(name=name, kind='Native')
46d06a12 2491 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=rr-zone"))
c1374bdb 2492 self.assert_success_json(r)
541bb91b 2493 print(r.json())
37bc3d01 2494 # should return zone, SOA
4bfebc93 2495 self.assertEqual(len(r.json()), 2)
ccfabd0d 2496
ccfabd0d
CH
2497@unittest.skipIf(not is_auth(), "Not applicable")
2498class AuthZoneKeys(ApiTestCase, AuthZonesHelperMixin):
2499
2500 def test_get_keys(self):
2501 r = self.session.get(
2502 self.url("/api/v1/servers/localhost/zones/powerdnssec.org./cryptokeys"))
2503 self.assert_success_json(r)
2504 keys = r.json()
2505 self.assertGreater(len(keys), 0)
2506
2507 key0 = deepcopy(keys[0])
2508 del key0['dnskey']
b6bd795c 2509 del key0['ds']
ccfabd0d 2510 expected = {
5d9c6182
PL
2511 u'algorithm': u'ECDSAP256SHA256',
2512 u'bits': 256,
ccfabd0d
CH
2513 u'active': True,
2514 u'type': u'Cryptokey',
b6bd795c
PL
2515 u'keytype': u'csk',
2516 u'flags': 257,
33918299 2517 u'published': True,
ccfabd0d 2518 u'id': 1}
4bfebc93 2519 self.assertEqual(key0, expected)
ccfabd0d
CH
2520
2521 keydata = keys[0]['dnskey'].split()
2522 self.assertEqual(len(keydata), 4)
17d96af7
PD
2523
2524 def test_get_keys_with_cds(self):
2525 payload_metadata = {"type": "Metadata", "kind": "PUBLISH-CDS", "metadata": ["4"]}
2526 r = self.session.post(self.url("/api/v1/servers/localhost/zones/powerdnssec.org./metadata"),
2527 data=json.dumps(payload_metadata))
2528 rdata = r.json()
4bfebc93
CH
2529 self.assertEqual(r.status_code, 201)
2530 self.assertEqual(rdata["metadata"], payload_metadata["metadata"])
17d96af7
PD
2531
2532 r = self.session.get(
2533 self.url("/api/v1/servers/localhost/zones/powerdnssec.org./cryptokeys"))
2534 self.assert_success_json(r)
2535 keys = r.json()
2536 self.assertGreater(len(keys), 0)
2537
2538 key0 = deepcopy(keys[0])
4bfebc93 2539 self.assertEqual(len(key0['cds']), 1)
17d96af7 2540 self.assertIn(key0['cds'][0], key0['ds'])
4bfebc93 2541 self.assertEqual(key0['cds'][0].split()[2], '4')
17d96af7
PD
2542 del key0['dnskey']
2543 del key0['ds']
2544 del key0['cds']
2545 expected = {
2546 u'algorithm': u'ECDSAP256SHA256',
2547 u'bits': 256,
2548 u'active': True,
2549 u'type': u'Cryptokey',
2550 u'keytype': u'csk',
2551 u'flags': 257,
2552 u'published': True,
2553 u'id': 1}
4bfebc93 2554 self.assertEqual(key0, expected)
17d96af7
PD
2555
2556 keydata = keys[0]['dnskey'].split()
2557 self.assertEqual(len(keydata), 4)
2558
2559 r = self.session.delete(self.url("/api/v1/servers/localhost/zones/powerdnssec.org./metadata/PUBLISH-CDS"))
4bfebc93 2560 self.assertEqual(r.status_code, 200)