]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.api/test_Zones.py
API: Remove ENTs when "replacing" new records
[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)
1d6b70f9
CH
831 expected_data = [name + '\t3600\tNS\tns1.foo.com.',
832 name + '\t3600\tNS\tns2.foo.com.',
833 name + '\t3600\tSOA\ta.misconfigured.powerdns.server. hostmaster.' + name +
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")
1d6b70f9
CH
845 expected_data = [name + '\t3600\tNS\tns1.foo.com.',
846 name + '\t3600\tNS\tns2.foo.com.',
847 name + '\t3600\tSOA\ta.misconfigured.powerdns.server. hostmaster.' + name +
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
c1374bdb 1025 def test_zone_rr_delete(self):
6754ef71 1026 name, payload, zone = self.create_zone()
b3905a3d 1027 # do a delete of all NS records (these are created with the zone)
d708640f 1028 rrset = {
b3905a3d
CH
1029 'changetype': 'delete',
1030 'name': name,
1031 'type': 'NS'
1032 }
d708640f 1033 payload = {'rrsets': [rrset]}
b3905a3d 1034 r = self.session.patch(
46d06a12 1035 self.url("/api/v1/servers/localhost/zones/" + name),
b3905a3d
CH
1036 data=json.dumps(payload),
1037 headers={'content-type': 'application/json'})
f0e76cee 1038 self.assert_success(r)
b3905a3d 1039 # verify that the records are gone
f0e76cee 1040 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
6754ef71 1041 self.assertIsNone(get_rrset(data, name, 'NS'))
cea26350 1042
c1374bdb 1043 def test_zone_disable_reenable(self):
d29d5db7 1044 # This also tests that SOA-EDIT-API works.
6754ef71 1045 name, payload, zone = self.create_zone(soa_edit_api='EPOCH')
cea26350 1046 # disable zone by disabling SOA
d708640f 1047 rrset = {
cea26350
CH
1048 'changetype': 'replace',
1049 'name': name,
1050 'type': 'SOA',
6754ef71 1051 'ttl': 3600,
cea26350
CH
1052 'records': [
1053 {
1d6b70f9 1054 "content": "ns1.bar.com. hostmaster.foo.org. 1 1 1 1 1",
cea26350
CH
1055 "disabled": True
1056 }
1057 ]
1058 }
d708640f 1059 payload = {'rrsets': [rrset]}
cea26350 1060 r = self.session.patch(
46d06a12 1061 self.url("/api/v1/servers/localhost/zones/" + name),
cea26350
CH
1062 data=json.dumps(payload),
1063 headers={'content-type': 'application/json'})
f0e76cee 1064 self.assert_success(r)
d29d5db7 1065 # check SOA serial has been edited
f0e76cee
CH
1066 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1067 soa_serial1 = get_first_rec(data, name, 'SOA')['content'].split()[2]
d29d5db7
CH
1068 self.assertNotEquals(soa_serial1, '1')
1069 # make sure domain is still in zone list (disabled SOA!)
46d06a12 1070 r = self.session.get(self.url("/api/v1/servers/localhost/zones"))
cea26350
CH
1071 domains = r.json()
1072 self.assertEquals(len([domain for domain in domains if domain['name'] == name]), 1)
d29d5db7
CH
1073 # sleep 1sec to ensure the EPOCH value changes for the next request
1074 time.sleep(1)
cea26350 1075 # verify that modifying it still works
d708640f
CH
1076 rrset['records'][0]['disabled'] = False
1077 payload = {'rrsets': [rrset]}
cea26350 1078 r = self.session.patch(
46d06a12 1079 self.url("/api/v1/servers/localhost/zones/" + name),
cea26350
CH
1080 data=json.dumps(payload),
1081 headers={'content-type': 'application/json'})
f0e76cee 1082 self.assert_success(r)
d29d5db7 1083 # check SOA serial has been edited again
f0e76cee
CH
1084 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1085 soa_serial2 = get_first_rec(data, name, 'SOA')['content'].split()[2]
d29d5db7
CH
1086 self.assertNotEquals(soa_serial2, '1')
1087 self.assertNotEquals(soa_serial2, soa_serial1)
02945d9a 1088
c1374bdb 1089 def test_zone_rr_update_out_of_zone(self):
6754ef71 1090 name, payload, zone = self.create_zone()
35f26cc5 1091 # replace with qname mismatch
d708640f 1092 rrset = {
35f26cc5 1093 'changetype': 'replace',
1d6b70f9 1094 'name': 'not-in-zone.',
35f26cc5 1095 'type': 'NS',
6754ef71 1096 'ttl': 3600,
35f26cc5
CH
1097 'records': [
1098 {
1d6b70f9 1099 "content": "ns1.bar.com.",
35f26cc5
CH
1100 "disabled": False
1101 }
1102 ]
1103 }
d708640f 1104 payload = {'rrsets': [rrset]}
35f26cc5 1105 r = self.session.patch(
46d06a12 1106 self.url("/api/v1/servers/localhost/zones/" + name),
35f26cc5
CH
1107 data=json.dumps(payload),
1108 headers={'content-type': 'application/json'})
1109 self.assertEquals(r.status_code, 422)
1110 self.assertIn('out of zone', r.json()['error'])
1111
1d6b70f9 1112 def test_zone_rr_update_restricted_chars(self):
6754ef71 1113 name, payload, zone = self.create_zone()
1d6b70f9
CH
1114 # replace with qname mismatch
1115 rrset = {
1116 'changetype': 'replace',
1117 'name': 'test:' + name,
1118 'type': 'NS',
6754ef71 1119 'ttl': 3600,
1d6b70f9
CH
1120 'records': [
1121 {
1d6b70f9
CH
1122 "content": "ns1.bar.com.",
1123 "disabled": False
1124 }
1125 ]
1126 }
1127 payload = {'rrsets': [rrset]}
1128 r = self.session.patch(
1129 self.url("/api/v1/servers/localhost/zones/" + name),
1130 data=json.dumps(payload),
1131 headers={'content-type': 'application/json'})
1132 self.assertEquals(r.status_code, 422)
1133 self.assertIn('contains unsupported characters', r.json()['error'])
1134
24cd86ca 1135 def test_rrset_unknown_type(self):
6754ef71 1136 name, payload, zone = self.create_zone()
24cd86ca
CH
1137 rrset = {
1138 'changetype': 'replace',
1139 'name': name,
1140 'type': 'FAFAFA',
6754ef71 1141 'ttl': 3600,
24cd86ca
CH
1142 'records': [
1143 {
24cd86ca
CH
1144 "content": "4.3.2.1",
1145 "disabled": False
1146 }
1147 ]
1148 }
1149 payload = {'rrsets': [rrset]}
46d06a12 1150 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
24cd86ca
CH
1151 headers={'content-type': 'application/json'})
1152 self.assertEquals(r.status_code, 422)
1153 self.assertIn('unknown type', r.json()['error'])
1154
8560f36a
CH
1155 def test_rrset_cname_and_other(self):
1156 name, payload, zone = self.create_zone()
1157 rrset = {
1158 'changetype': 'replace',
1159 'name': name,
1160 'type': 'CNAME',
1161 'ttl': 3600,
1162 'records': [
1163 {
1164 "content": "example.org.",
1165 "disabled": False
1166 }
1167 ]
1168 }
1169 payload = {'rrsets': [rrset]}
1170 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1171 headers={'content-type': 'application/json'})
1172 self.assertEquals(r.status_code, 422)
1173 self.assertIn('Conflicts with pre-existing non-CNAME RRset', r.json()['error'])
1174
1175 def test_rrset_other_and_cname(self):
1176 name, payload, zone = self.create_zone()
1177 rrset = {
1178 'changetype': 'replace',
1179 'name': 'sub.'+name,
1180 'type': 'CNAME',
1181 'ttl': 3600,
1182 'records': [
1183 {
1184 "content": "example.org.",
1185 "disabled": False
1186 }
1187 ]
1188 }
1189 payload = {'rrsets': [rrset]}
1190 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1191 headers={'content-type': 'application/json'})
1192 self.assert_success(r)
1193 rrset = {
1194 'changetype': 'replace',
1195 'name': 'sub.'+name,
1196 'type': 'A',
1197 'ttl': 3600,
1198 'records': [
1199 {
1200 "content": "1.2.3.4",
1201 "disabled": False
1202 }
1203 ]
1204 }
1205 payload = {'rrsets': [rrset]}
1206 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1207 headers={'content-type': 'application/json'})
1208 self.assertEquals(r.status_code, 422)
1209 self.assertIn('Conflicts with pre-existing CNAME RRset', r.json()['error'])
1210
1e5b9ab9
CH
1211 def test_create_zone_with_leading_space(self):
1212 # Actual regression.
6754ef71 1213 name, payload, zone = self.create_zone()
1e5b9ab9
CH
1214 rrset = {
1215 'changetype': 'replace',
1216 'name': name,
1217 'type': 'A',
6754ef71 1218 'ttl': 3600,
1e5b9ab9
CH
1219 'records': [
1220 {
1e5b9ab9
CH
1221 "content": " 4.3.2.1",
1222 "disabled": False
1223 }
1224 ]
1225 }
1226 payload = {'rrsets': [rrset]}
46d06a12 1227 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1e5b9ab9
CH
1228 headers={'content-type': 'application/json'})
1229 self.assertEquals(r.status_code, 422)
1230 self.assertIn('Not in expected format', r.json()['error'])
1231
c1374bdb 1232 def test_zone_rr_delete_out_of_zone(self):
6754ef71 1233 name, payload, zone = self.create_zone()
d708640f 1234 rrset = {
35f26cc5 1235 'changetype': 'delete',
1d6b70f9 1236 'name': 'not-in-zone.',
35f26cc5
CH
1237 'type': 'NS'
1238 }
d708640f 1239 payload = {'rrsets': [rrset]}
35f26cc5 1240 r = self.session.patch(
46d06a12 1241 self.url("/api/v1/servers/localhost/zones/" + name),
35f26cc5
CH
1242 data=json.dumps(payload),
1243 headers={'content-type': 'application/json'})
541bb91b 1244 print(r.content)
f0e76cee 1245 self.assert_success(r) # succeed so users can fix their wrong, old data
35f26cc5 1246
37663c3b 1247 def test_zone_delete(self):
6754ef71 1248 name, payload, zone = self.create_zone()
46d06a12 1249 r = self.session.delete(self.url("/api/v1/servers/localhost/zones/" + name))
37663c3b
CH
1250 self.assertEquals(r.status_code, 204)
1251 self.assertNotIn('Content-Type', r.headers)
1252
c1374bdb 1253 def test_zone_comment_create(self):
6754ef71 1254 name, payload, zone = self.create_zone()
d708640f 1255 rrset = {
6cc98ddf
CH
1256 'changetype': 'replace',
1257 'name': name,
1258 'type': 'NS',
6754ef71 1259 'ttl': 3600,
6cc98ddf
CH
1260 'comments': [
1261 {
1262 'account': 'test1',
1263 'content': 'blah blah',
1264 },
1265 {
1266 'account': 'test2',
1267 'content': 'blah blah bleh',
1268 }
1269 ]
1270 }
d708640f 1271 payload = {'rrsets': [rrset]}
6cc98ddf 1272 r = self.session.patch(
46d06a12 1273 self.url("/api/v1/servers/localhost/zones/" + name),
6cc98ddf
CH
1274 data=json.dumps(payload),
1275 headers={'content-type': 'application/json'})
f0e76cee 1276 self.assert_success(r)
6cc98ddf
CH
1277 # make sure the comments have been set, and that the NS
1278 # records are still present
f0e76cee
CH
1279 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1280 serverset = get_rrset(data, name, 'NS')
541bb91b 1281 print(serverset)
6754ef71
CH
1282 self.assertNotEquals(serverset['records'], [])
1283 self.assertNotEquals(serverset['comments'], [])
6cc98ddf 1284 # verify that modified_at has been set by pdns
6754ef71 1285 self.assertNotEquals([c for c in serverset['comments']][0]['modified_at'], 0)
0d7f3c75
CH
1286 # verify that TTL is correct (regression test)
1287 self.assertEquals(serverset['ttl'], 3600)
6cc98ddf 1288
c1374bdb 1289 def test_zone_comment_delete(self):
6cc98ddf 1290 # Test: Delete ONLY comments.
6754ef71 1291 name, payload, zone = self.create_zone()
d708640f 1292 rrset = {
6cc98ddf
CH
1293 'changetype': 'replace',
1294 'name': name,
1295 'type': 'NS',
1296 'comments': []
1297 }
d708640f 1298 payload = {'rrsets': [rrset]}
6cc98ddf 1299 r = self.session.patch(
46d06a12 1300 self.url("/api/v1/servers/localhost/zones/" + name),
6cc98ddf
CH
1301 data=json.dumps(payload),
1302 headers={'content-type': 'application/json'})
f0e76cee 1303 self.assert_success(r)
6cc98ddf 1304 # make sure the NS records are still present
f0e76cee
CH
1305 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1306 serverset = get_rrset(data, name, 'NS')
541bb91b 1307 print(serverset)
6754ef71
CH
1308 self.assertNotEquals(serverset['records'], [])
1309 self.assertEquals(serverset['comments'], [])
6cc98ddf 1310
c1374bdb 1311 def test_zone_comment_stay_intact(self):
6cc98ddf 1312 # Test if comments on an rrset stay intact if the rrset is replaced
6754ef71 1313 name, payload, zone = self.create_zone()
6cc98ddf 1314 # create a comment
d708640f 1315 rrset = {
6cc98ddf
CH
1316 'changetype': 'replace',
1317 'name': name,
1318 'type': 'NS',
1319 'comments': [
1320 {
1321 'account': 'test1',
1322 'content': 'oh hi there',
2696eea0 1323 'modified_at': 1111
6cc98ddf
CH
1324 }
1325 ]
1326 }
d708640f 1327 payload = {'rrsets': [rrset]}
6cc98ddf 1328 r = self.session.patch(
46d06a12 1329 self.url("/api/v1/servers/localhost/zones/" + name),
6cc98ddf
CH
1330 data=json.dumps(payload),
1331 headers={'content-type': 'application/json'})
f0e76cee 1332 self.assert_success(r)
6cc98ddf 1333 # replace rrset records
d708640f 1334 rrset2 = {
6cc98ddf
CH
1335 'changetype': 'replace',
1336 'name': name,
1337 'type': 'NS',
6754ef71 1338 'ttl': 3600,
6cc98ddf
CH
1339 'records': [
1340 {
1d6b70f9 1341 "content": "ns1.bar.com.",
6cc98ddf
CH
1342 "disabled": False
1343 }
1344 ]
1345 }
d708640f 1346 payload2 = {'rrsets': [rrset2]}
6cc98ddf 1347 r = self.session.patch(
46d06a12 1348 self.url("/api/v1/servers/localhost/zones/" + name),
6cc98ddf
CH
1349 data=json.dumps(payload2),
1350 headers={'content-type': 'application/json'})
f0e76cee 1351 self.assert_success(r)
6cc98ddf 1352 # make sure the comments still exist
f0e76cee
CH
1353 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
1354 serverset = get_rrset(data, name, 'NS')
541bb91b 1355 print(serverset)
6754ef71
CH
1356 self.assertEquals(serverset['records'], rrset2['records'])
1357 self.assertEquals(serverset['comments'], rrset['comments'])
6cc98ddf 1358
3fe7c7d6
CH
1359 def test_zone_auto_ptr_ipv4_create(self):
1360 revzone = '4.2.192.in-addr.arpa.'
1361 _, _, revzonedata = self.create_zone(name=revzone)
1362 name = unique_zone_name()
1363 rrset = {
1364 "name": name,
1365 "type": "A",
1366 "ttl": 3600,
1367 "records": [{
1368 "content": "192.2.4.44",
1369 "disabled": False,
1370 "set-ptr": True,
1371 }],
1372 }
1373 name, payload, data = self.create_zone(name=name, rrsets=[rrset])
1374 del rrset['records'][0]['set-ptr']
1375 self.assertEquals(get_rrset(data, name, 'A')['records'], rrset['records'])
1376 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + revzone)).json()
1377 revsets = [s for s in r['rrsets'] if s['type'] == 'PTR']
541bb91b 1378 print(revsets)
3fe7c7d6
CH
1379 self.assertEquals(revsets, [{
1380 u'name': u'44.4.2.192.in-addr.arpa.',
1381 u'ttl': 3600,
1382 u'type': u'PTR',
1383 u'comments': [],
1384 u'records': [{
1385 u'content': name,
1386 u'disabled': False,
1387 }],
1388 }])
1389 # with SOA-EDIT-API DEFAULT on the revzone, the serial should now be higher.
1390 self.assertGreater(r['serial'], revzonedata['serial'])
1391
1392 def test_zone_auto_ptr_ipv4_update(self):
1d6b70f9 1393 revzone = '0.2.192.in-addr.arpa.'
a41c038a 1394 _, _, revzonedata = self.create_zone(name=revzone)
6754ef71 1395 name, payload, zone = self.create_zone()
d708640f 1396 rrset = {
d1587ceb
CH
1397 'changetype': 'replace',
1398 'name': name,
1399 'type': 'A',
6754ef71 1400 'ttl': 3600,
d1587ceb
CH
1401 'records': [
1402 {
d1587ceb
CH
1403 "content": '192.2.0.2',
1404 "disabled": False,
1405 "set-ptr": True
1406 }
1407 ]
1408 }
d708640f 1409 payload = {'rrsets': [rrset]}
d1587ceb 1410 r = self.session.patch(
46d06a12 1411 self.url("/api/v1/servers/localhost/zones/" + name),
d1587ceb
CH
1412 data=json.dumps(payload),
1413 headers={'content-type': 'application/json'})
f0e76cee 1414 self.assert_success(r)
a41c038a
CH
1415 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + revzone)).json()
1416 revsets = [s for s in r['rrsets'] if s['type'] == 'PTR']
541bb91b 1417 print(revsets)
6754ef71
CH
1418 self.assertEquals(revsets, [{
1419 u'name': u'2.0.2.192.in-addr.arpa.',
d1587ceb 1420 u'ttl': 3600,
d1587ceb 1421 u'type': u'PTR',
6754ef71
CH
1422 u'comments': [],
1423 u'records': [{
1424 u'content': name,
1425 u'disabled': False,
1426 }],
d1587ceb 1427 }])
a41c038a
CH
1428 # with SOA-EDIT-API DEFAULT on the revzone, the serial should now be higher.
1429 self.assertGreater(r['serial'], revzonedata['serial'])
d1587ceb 1430
3fe7c7d6 1431 def test_zone_auto_ptr_ipv6_update(self):
d1587ceb 1432 # 2001:DB8::bb:aa
1d6b70f9 1433 revzone = '8.b.d.0.1.0.0.2.ip6.arpa.'
a41c038a 1434 _, _, revzonedata = self.create_zone(name=revzone)
6754ef71 1435 name, payload, zone = self.create_zone()
d708640f 1436 rrset = {
d1587ceb
CH
1437 'changetype': 'replace',
1438 'name': name,
1439 'type': 'AAAA',
6754ef71 1440 'ttl': 3600,
d1587ceb
CH
1441 'records': [
1442 {
d1587ceb
CH
1443 "content": '2001:DB8::bb:aa',
1444 "disabled": False,
1445 "set-ptr": True
1446 }
1447 ]
1448 }
d708640f 1449 payload = {'rrsets': [rrset]}
d1587ceb 1450 r = self.session.patch(
46d06a12 1451 self.url("/api/v1/servers/localhost/zones/" + name),
d1587ceb
CH
1452 data=json.dumps(payload),
1453 headers={'content-type': 'application/json'})
f0e76cee 1454 self.assert_success(r)
a41c038a
CH
1455 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + revzone)).json()
1456 revsets = [s for s in r['rrsets'] if s['type'] == 'PTR']
541bb91b 1457 print(revsets)
6754ef71
CH
1458 self.assertEquals(revsets, [{
1459 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 1460 u'ttl': 3600,
d1587ceb 1461 u'type': u'PTR',
6754ef71
CH
1462 u'comments': [],
1463 u'records': [{
1464 u'content': name,
1465 u'disabled': False,
1466 }],
d1587ceb 1467 }])
a41c038a
CH
1468 # with SOA-EDIT-API DEFAULT on the revzone, the serial should now be higher.
1469 self.assertGreater(r['serial'], revzonedata['serial'])
d1587ceb 1470
c1374bdb 1471 def test_search_rr_exact_zone(self):
b1902fab 1472 name = unique_zone_name()
1d6b70f9
CH
1473 self.create_zone(name=name, serial=22, soa_edit_api='')
1474 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=" + name.rstrip('.')))
c1374bdb 1475 self.assert_success_json(r)
541bb91b 1476 print(r.json())
1d6b70f9
CH
1477 self.assertEquals(r.json(), [
1478 {u'object_type': u'zone', u'name': name, u'zone_id': name},
1479 {u'content': u'a.misconfigured.powerdns.server. hostmaster.'+name+' 22 10800 3600 604800 3600',
1480 u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
1481 u'ttl': 3600, u'type': u'SOA', u'name': name},
1482 {u'content': u'ns1.example.com.',
1483 u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
1484 u'ttl': 3600, u'type': u'NS', u'name': name},
1485 {u'content': u'ns2.example.com.',
1486 u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
1487 u'ttl': 3600, u'type': u'NS', u'name': name},
1488 ])
b1902fab 1489
c1374bdb 1490 def test_search_rr_substring(self):
541bb91b
CH
1491 name = unique_zone_name()
1492 search = name[5:-5]
b1902fab 1493 self.create_zone(name=name)
541bb91b 1494 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=*%s*" % search))
c1374bdb 1495 self.assert_success_json(r)
541bb91b 1496 print(r.json())
b1902fab 1497 # should return zone, SOA, ns1, ns2
60a8e825 1498 self.assertEquals(len(r.json()), 4)
b1902fab 1499
c1374bdb 1500 def test_search_rr_case_insensitive(self):
541bb91b 1501 name = unique_zone_name()+'testsuffix.'
57cb86d8 1502 self.create_zone(name=name)
541bb91b 1503 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=*testSUFFIX*"))
c1374bdb 1504 self.assert_success_json(r)
541bb91b 1505 print(r.json())
57cb86d8 1506 # should return zone, SOA, ns1, ns2
60a8e825 1507 self.assertEquals(len(r.json()), 4)
57cb86d8 1508
7cbc5255 1509 def test_search_after_rectify_with_ent(self):
541bb91b
CH
1510 name = unique_zone_name()
1511 search = name.split('.')[0]
7cbc5255
CH
1512 rrset = {
1513 "name": 'sub.sub.' + name,
1514 "type": "A",
1515 "ttl": 3600,
1516 "records": [{
1517 "content": "4.3.2.1",
1518 "disabled": False,
1519 }],
1520 }
1521 self.create_zone(name=name, rrsets=[rrset])
1522 pdnsutil_rectify(name)
541bb91b 1523 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=*%s*" % search))
7cbc5255 1524 self.assert_success_json(r)
541bb91b 1525 print(r.json())
7cbc5255
CH
1526 # should return zone, SOA, ns1, ns2, sub.sub A (but not the ENT)
1527 self.assertEquals(len(r.json()), 5)
1528
03b1cc25
CH
1529 def test_cname_at_ent_place(self):
1530 name, payload, zone = self.create_zone(api_rectify=True)
1531 rrset = {
1532 'changetype': 'replace',
1533 'name': 'sub2.sub1.' + name,
1534 'type': "A",
1535 'ttl': 3600,
1536 'records': [{
1537 'content': "4.3.2.1",
1538 'disabled': False,
1539 }],
1540 }
1541 payload = {'rrsets': [rrset]}
1542 r = self.session.patch(
1543 self.url("/api/v1/servers/localhost/zones/" + zone['id']),
1544 data=json.dumps(payload),
1545 headers={'content-type': 'application/json'})
1546 self.assertEquals(r.status_code, 204)
1547 rrset = {
1548 'changetype': 'replace',
1549 'name': 'sub1.' + name,
1550 'type': "CNAME",
1551 'ttl': 3600,
1552 'records': [{
1553 'content': "www.example.org.",
1554 'disabled': False,
1555 }],
1556 }
1557 payload = {'rrsets': [rrset]}
1558 r = self.session.patch(
1559 self.url("/api/v1/servers/localhost/zones/" + zone['id']),
1560 data=json.dumps(payload),
1561 headers={'content-type': 'application/json'})
1562 self.assertEquals(r.status_code, 204)
1563
986e4858
PL
1564 def test_rrset_parameter_post_false(self):
1565 name = unique_zone_name()
1566 payload = {
1567 'name': name,
1568 'kind': 'Native',
1569 'nameservers': ['ns1.example.com.', 'ns2.example.com.']
1570 }
1571 r = self.session.post(
1572 self.url("/api/v1/servers/localhost/zones?rrsets=false"),
1573 data=json.dumps(payload),
1574 headers={'content-type': 'application/json'})
541bb91b 1575 print(r.json())
986e4858
PL
1576 self.assert_success_json(r)
1577 self.assertEquals(r.status_code, 201)
1578 self.assertEquals(r.json().get('rrsets'), None)
1579
1580 def test_rrset_false_parameter(self):
1581 name = unique_zone_name()
1582 self.create_zone(name=name, kind='Native')
1583 r = self.session.get(self.url("/api/v1/servers/localhost/zones/"+name+"?rrsets=false"))
1584 self.assert_success_json(r)
541bb91b 1585 print(r.json())
986e4858
PL
1586 self.assertEquals(r.json().get('rrsets'), None)
1587
1588 def test_rrset_true_parameter(self):
1589 name = unique_zone_name()
1590 self.create_zone(name=name, kind='Native')
1591 r = self.session.get(self.url("/api/v1/servers/localhost/zones/"+name+"?rrsets=true"))
1592 self.assert_success_json(r)
541bb91b 1593 print(r.json())
986e4858
PL
1594 self.assertEquals(len(r.json().get('rrsets')), 2)
1595
1596 def test_wrong_rrset_parameter(self):
1597 name = unique_zone_name()
1598 self.create_zone(name=name, kind='Native')
1599 r = self.session.get(self.url("/api/v1/servers/localhost/zones/"+name+"?rrsets=foobar"))
1600 self.assertEquals(r.status_code, 422)
1601 self.assertIn("'rrsets' request parameter value 'foobar' is not supported", r.json()['error'])
1602
02945d9a 1603
406497f5
CH
1604@unittest.skipIf(not is_auth(), "Not applicable")
1605class AuthRootZone(ApiTestCase, AuthZonesHelperMixin):
1606
1607 def setUp(self):
1608 super(AuthRootZone, self).setUp()
1609 # zone name is not unique, so delete the zone before each individual test.
46d06a12 1610 self.session.delete(self.url("/api/v1/servers/localhost/zones/=2E"))
406497f5
CH
1611
1612 def test_create_zone(self):
6754ef71 1613 name, payload, data = self.create_zone(name='.', serial=22, soa_edit_api='')
406497f5
CH
1614 for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'soa_edit_api', 'soa_edit', 'account'):
1615 self.assertIn(k, data)
1616 if k in payload:
1617 self.assertEquals(data[k], payload[k])
406497f5 1618 # validate generated SOA
6754ef71 1619 rec = get_first_rec(data, '.', 'SOA')
406497f5 1620 self.assertEquals(
6754ef71 1621 rec['content'],
1d6b70f9 1622 "a.misconfigured.powerdns.server. hostmaster. " + str(payload['serial']) +
406497f5
CH
1623 " 10800 3600 604800 3600"
1624 )
1625 # Regression test: verify zone list works
46d06a12 1626 zonelist = self.session.get(self.url("/api/v1/servers/localhost/zones")).json()
541bb91b 1627 print("zonelist:", zonelist)
406497f5
CH
1628 self.assertIn(payload['name'], [zone['name'] for zone in zonelist])
1629 # Also test that fetching the zone works.
541bb91b 1630 print("id:", data['id'])
406497f5 1631 self.assertEquals(data['id'], '=2E')
46d06a12 1632 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + data['id'])).json()
541bb91b 1633 print("zone (fetched):", data)
406497f5
CH
1634 for k in ('name', 'kind'):
1635 self.assertIn(k, data)
1636 self.assertEquals(data[k], payload[k])
6754ef71 1637 self.assertEqual(data['rrsets'][0]['name'], '.')
406497f5
CH
1638
1639 def test_update_zone(self):
6754ef71 1640 name, payload, zone = self.create_zone(name='.')
406497f5
CH
1641 zone_id = '=2E'
1642 # update, set as Master and enable SOA-EDIT-API
1643 payload = {
1644 'kind': 'Master',
1645 'masters': ['192.0.2.1', '192.0.2.2'],
1646 'soa_edit_api': 'EPOCH',
1647 'soa_edit': 'EPOCH'
1648 }
1649 r = self.session.put(
46d06a12 1650 self.url("/api/v1/servers/localhost/zones/" + zone_id),
406497f5
CH
1651 data=json.dumps(payload),
1652 headers={'content-type': 'application/json'})
f0e76cee
CH
1653 self.assert_success(r)
1654 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + zone_id)).json()
406497f5
CH
1655 for k in payload.keys():
1656 self.assertIn(k, data)
1657 self.assertEquals(data[k], payload[k])
1658 # update, back to Native and empty(off)
1659 payload = {
1660 'kind': 'Native',
1661 'soa_edit_api': '',
1662 'soa_edit': ''
1663 }
1664 r = self.session.put(
46d06a12 1665 self.url("/api/v1/servers/localhost/zones/" + zone_id),
406497f5
CH
1666 data=json.dumps(payload),
1667 headers={'content-type': 'application/json'})
f0e76cee
CH
1668 self.assert_success(r)
1669 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + zone_id)).json()
406497f5
CH
1670 for k in payload.keys():
1671 self.assertIn(k, data)
1672 self.assertEquals(data[k], payload[k])
1673
1674
c1374bdb 1675@unittest.skipIf(not is_recursor(), "Not applicable")
02945d9a
CH
1676class RecursorZones(ApiTestCase):
1677
37bc3d01
CH
1678 def create_zone(self, name=None, kind=None, rd=False, servers=None):
1679 if name is None:
1680 name = unique_zone_name()
1681 if servers is None:
1682 servers = []
02945d9a 1683 payload = {
37bc3d01
CH
1684 'name': name,
1685 'kind': kind,
1686 'servers': servers,
1687 'recursion_desired': rd
02945d9a
CH
1688 }
1689 r = self.session.post(
46d06a12 1690 self.url("/api/v1/servers/localhost/zones"),
02945d9a
CH
1691 data=json.dumps(payload),
1692 headers={'content-type': 'application/json'})
c1374bdb
CH
1693 self.assert_success_json(r)
1694 return payload, r.json()
37bc3d01 1695
c1374bdb 1696 def test_create_auth_zone(self):
37bc3d01 1697 payload, data = self.create_zone(kind='Native')
02945d9a
CH
1698 for k in payload.keys():
1699 self.assertEquals(data[k], payload[k])
1700
1d6b70f9 1701 def test_create_zone_no_name(self):
1d6b70f9
CH
1702 payload = {
1703 'name': '',
1704 'kind': 'Native',
1705 'servers': ['8.8.8.8'],
1706 'recursion_desired': False,
1707 }
541bb91b 1708 print(payload)
1d6b70f9
CH
1709 r = self.session.post(
1710 self.url("/api/v1/servers/localhost/zones"),
1711 data=json.dumps(payload),
1712 headers={'content-type': 'application/json'})
1713 self.assertEquals(r.status_code, 422)
1714 self.assertIn('is not canonical', r.json()['error'])
1715
c1374bdb 1716 def test_create_forwarded_zone(self):
37bc3d01 1717 payload, data = self.create_zone(kind='Forwarded', rd=False, servers=['8.8.8.8'])
02945d9a
CH
1718 # return values are normalized
1719 payload['servers'][0] += ':53'
02945d9a
CH
1720 for k in payload.keys():
1721 self.assertEquals(data[k], payload[k])
1722
c1374bdb 1723 def test_create_forwarded_rd_zone(self):
1d6b70f9 1724 payload, data = self.create_zone(name='google.com.', kind='Forwarded', rd=True, servers=['8.8.8.8'])
02945d9a
CH
1725 # return values are normalized
1726 payload['servers'][0] += ':53'
02945d9a
CH
1727 for k in payload.keys():
1728 self.assertEquals(data[k], payload[k])
1729
c1374bdb 1730 def test_create_auth_zone_with_symbols(self):
37bc3d01 1731 payload, data = self.create_zone(name='foo/bar.'+unique_zone_name(), kind='Native')
1dbe38ba 1732 expected_id = (payload['name'].replace('/', '=2F'))
02945d9a
CH
1733 for k in payload.keys():
1734 self.assertEquals(data[k], payload[k])
1735 self.assertEquals(data['id'], expected_id)
e2367534 1736
c1374bdb 1737 def test_rename_auth_zone(self):
37bc3d01 1738 payload, data = self.create_zone(kind='Native')
1d6b70f9 1739 name = payload['name']
e2367534
CH
1740 # now rename it
1741 payload = {
1742 'name': 'renamed-'+name,
1743 'kind': 'Native',
1744 'recursion_desired': False
1745 }
1746 r = self.session.put(
46d06a12 1747 self.url("/api/v1/servers/localhost/zones/" + name),
e2367534
CH
1748 data=json.dumps(payload),
1749 headers={'content-type': 'application/json'})
f0e76cee
CH
1750 self.assert_success(r)
1751 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + payload['name'])).json()
e2367534
CH
1752 for k in payload.keys():
1753 self.assertEquals(data[k], payload[k])
37bc3d01 1754
37663c3b
CH
1755 def test_zone_delete(self):
1756 payload, zone = self.create_zone(kind='Native')
1757 name = payload['name']
46d06a12 1758 r = self.session.delete(self.url("/api/v1/servers/localhost/zones/" + name))
37663c3b
CH
1759 self.assertEquals(r.status_code, 204)
1760 self.assertNotIn('Content-Type', r.headers)
1761
c1374bdb 1762 def test_search_rr_exact_zone(self):
1d6b70f9 1763 name = unique_zone_name()
37bc3d01 1764 self.create_zone(name=name, kind='Native')
46d06a12 1765 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=" + name))
c1374bdb 1766 self.assert_success_json(r)
541bb91b 1767 print(r.json())
37bc3d01
CH
1768 self.assertEquals(r.json(), [{u'type': u'zone', u'name': name, u'zone_id': name}])
1769
c1374bdb 1770 def test_search_rr_substring(self):
1d6b70f9 1771 name = 'search-rr-zone.name.'
37bc3d01 1772 self.create_zone(name=name, kind='Native')
46d06a12 1773 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=rr-zone"))
c1374bdb 1774 self.assert_success_json(r)
541bb91b 1775 print(r.json())
37bc3d01
CH
1776 # should return zone, SOA
1777 self.assertEquals(len(r.json()), 2)
ccfabd0d 1778
ccfabd0d
CH
1779@unittest.skipIf(not is_auth(), "Not applicable")
1780class AuthZoneKeys(ApiTestCase, AuthZonesHelperMixin):
1781
1782 def test_get_keys(self):
1783 r = self.session.get(
1784 self.url("/api/v1/servers/localhost/zones/powerdnssec.org./cryptokeys"))
1785 self.assert_success_json(r)
1786 keys = r.json()
1787 self.assertGreater(len(keys), 0)
1788
1789 key0 = deepcopy(keys[0])
1790 del key0['dnskey']
b6bd795c 1791 del key0['ds']
ccfabd0d 1792 expected = {
5d9c6182
PL
1793 u'algorithm': u'ECDSAP256SHA256',
1794 u'bits': 256,
ccfabd0d
CH
1795 u'active': True,
1796 u'type': u'Cryptokey',
b6bd795c
PL
1797 u'keytype': u'csk',
1798 u'flags': 257,
ccfabd0d
CH
1799 u'id': 1}
1800 self.assertEquals(key0, expected)
1801
1802 keydata = keys[0]['dnskey'].split()
1803 self.assertEqual(len(keydata), 4)