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