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