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