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