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