]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.api/test_Zones.py
ignore a generated file
[thirdparty/pdns.git] / regression-tests.api / test_Zones.py
CommitLineData
e2dba705 1import json
d29d5db7 2import time
e2dba705 3import unittest
ccfabd0d 4from copy import deepcopy
6754ef71
CH
5from pprint import pprint
6from test_helper import ApiTestCase, unique_zone_name, is_auth, is_recursor, get_db_records
7
8
9def get_rrset(data, qname, qtype):
10 for rrset in data['rrsets']:
11 if rrset['name'] == qname and rrset['type'] == qtype:
12 return rrset
13 return None
14
15
16def get_first_rec(data, qname, qtype):
17 rrset = get_rrset(data, qname, qtype)
18 if rrset:
19 return rrset['records'][0]
20 return None
21
22
23def eq_zone_rrsets(rrsets, expected):
24 data_got = {}
25 data_expected = {}
26 for type_, expected_records in expected.iteritems():
27 type_ = str(type_)
28 data_got[type_] = set()
29 data_expected[type_] = set()
30 uses_name = any(['name' in expected_record for expected_record in expected_records])
31 # minify + convert received data
32 for rrset in [rrset for rrset in rrsets if rrset['type'] == type_]:
33 print rrset
34 for r in rrset['records']:
35 data_got[type_].add((rrset['name'] if uses_name else '@', rrset['type'], r['content']))
36 # minify expected data
37 for r in expected_records:
38 data_expected[type_].add((r['name'] if uses_name else '@', type_, r['content']))
39
40 print "eq_zone_rrsets: got:"
41 pprint(data_got)
42 print "eq_zone_rrsets: expected:"
43 pprint(data_expected)
44
45 assert data_got == data_expected, "%r != %r" % (data_got, data_expected)
1a152698
CH
46
47
02945d9a 48class Zones(ApiTestCase):
1a152698 49
c1374bdb 50 def test_list_zones(self):
46d06a12 51 r = self.session.get(self.url("/api/v1/servers/localhost/zones"))
c1374bdb 52 self.assert_success_json(r)
45de6290 53 domains = r.json()
02945d9a 54 example_com = [domain for domain in domains if domain['name'] in ('example.com', 'example.com.')]
1a152698
CH
55 self.assertEquals(len(example_com), 1)
56 example_com = example_com[0]
02945d9a 57 required_fields = ['id', 'url', 'name', 'kind']
c1374bdb 58 if is_auth():
c04b5870 59 required_fields = required_fields + ['masters', 'last_check', 'notified_serial', 'serial', 'account']
c1374bdb 60 elif is_recursor():
02945d9a
CH
61 required_fields = required_fields + ['recursion_desired', 'servers']
62 for field in required_fields:
63 self.assertIn(field, example_com)
64
65
406497f5 66class AuthZonesHelperMixin(object):
284fdfe9 67 def create_zone(self, name=None, **kwargs):
bee2acae
CH
68 if name is None:
69 name = unique_zone_name()
e2dba705 70 payload = {
bee2acae 71 'name': name,
e2dba705 72 'kind': 'Native',
1d6b70f9 73 'nameservers': ['ns1.example.com.', 'ns2.example.com.']
e2dba705 74 }
284fdfe9 75 for k, v in kwargs.items():
4bdff352
CH
76 if v is None:
77 del payload[k]
78 else:
79 payload[k] = v
1d6b70f9 80 print "sending", payload
e2dba705 81 r = self.session.post(
46d06a12 82 self.url("/api/v1/servers/localhost/zones"),
e2dba705
CH
83 data=json.dumps(payload),
84 headers={'content-type': 'application/json'})
c1374bdb 85 self.assert_success_json(r)
64a36f0d 86 self.assertEquals(r.status_code, 201)
1d6b70f9
CH
87 reply = r.json()
88 print "reply", reply
6754ef71 89 return name, payload, reply
bee2acae 90
406497f5
CH
91
92@unittest.skipIf(not is_auth(), "Not applicable")
93class AuthZones(ApiTestCase, AuthZonesHelperMixin):
94
c1374bdb 95 def test_create_zone(self):
b0af9105 96 # soa_edit_api has a default, override with empty for this test
6754ef71 97 name, payload, data = self.create_zone(serial=22, soa_edit_api='')
8ffb7a9b 98 for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'soa_edit_api', 'soa_edit', 'account'):
d29d5db7
CH
99 self.assertIn(k, data)
100 if k in payload:
101 self.assertEquals(data[k], payload[k])
f63168e6 102 # validate generated SOA
6754ef71 103 expected_soa = "a.misconfigured.powerdns.server. hostmaster." + name + " " + \
1d6b70f9 104 str(payload['serial']) + " 10800 3600 604800 3600"
f63168e6 105 self.assertEquals(
6754ef71 106 get_first_rec(data, name, 'SOA')['content'],
1d6b70f9 107 expected_soa
f63168e6 108 )
1d6b70f9 109 # Because we had confusion about dots, check that the DB is without dots.
6754ef71 110 dbrecs = get_db_records(name, 'SOA')
1d6b70f9 111 self.assertEqual(dbrecs[0]['content'], expected_soa.replace('. ', ' '))
d29d5db7 112
c1374bdb 113 def test_create_zone_with_soa_edit_api(self):
f63168e6 114 # soa_edit_api wins over serial
6754ef71 115 name, payload, data = self.create_zone(soa_edit_api='EPOCH', serial=10)
f63168e6 116 for k in ('soa_edit_api', ):
e2dba705
CH
117 self.assertIn(k, data)
118 if k in payload:
119 self.assertEquals(data[k], payload[k])
f63168e6
CH
120 # generated EPOCH serial surely is > fixed serial we passed in
121 print data
122 self.assertGreater(data['serial'], payload['serial'])
6754ef71 123 soa_serial = int(get_first_rec(data, name, 'SOA')['content'].split(' ')[2])
f63168e6
CH
124 self.assertGreater(soa_serial, payload['serial'])
125 self.assertEquals(soa_serial, data['serial'])
6bb25159 126
79532aa7
CH
127 def test_create_zone_with_account(self):
128 # soa_edit_api wins over serial
6754ef71 129 name, payload, data = self.create_zone(account='anaccount', serial=10)
79532aa7
CH
130 print data
131 for k in ('account', ):
132 self.assertIn(k, data)
133 if k in payload:
134 self.assertEquals(data[k], payload[k])
135
9440a9f0
CH
136 def test_create_zone_default_soa_edit_api(self):
137 name, payload, data = self.create_zone()
138 print data
139 self.assertEquals(data['soa_edit_api'], 'DEFAULT')
140
c1374bdb 141 def test_create_zone_with_records(self):
f63168e6 142 name = unique_zone_name()
6754ef71
CH
143 rrset = {
144 "name": name,
145 "type": "A",
146 "ttl": 3600,
147 "records": [{
f63168e6 148 "content": "4.3.2.1",
6754ef71
CH
149 "disabled": False,
150 }],
151 }
152 name, payload, data = self.create_zone(name=name, rrsets=[rrset])
f63168e6 153 # check our record has appeared
6754ef71 154 self.assertEquals(get_rrset(data, name, 'A')['records'], rrset['records'])
f63168e6 155
d0953126
AT
156 def test_create_zone_with_wildcard_records(self):
157 name = unique_zone_name()
6754ef71
CH
158 rrset = {
159 "name": "*."+name,
160 "type": "A",
161 "ttl": 3600,
162 "records": [{
d0953126 163 "content": "4.3.2.1",
6754ef71
CH
164 "disabled": False,
165 }],
166 }
167 name, payload, data = self.create_zone(name=name, rrsets=[rrset])
d0953126 168 # check our record has appeared
6754ef71 169 self.assertEquals(get_rrset(data, rrset['name'], 'A')['records'], rrset['records'])
d0953126 170
c1374bdb 171 def test_create_zone_with_comments(self):
f63168e6 172 name = unique_zone_name()
6754ef71
CH
173 rrset = {
174 "name": name,
175 "type": "soa", # test uppercasing of type, too.
176 "comments": [{
177 "account": "test1",
178 "content": "blah blah",
179 "modified_at": 11112,
180 }],
181 }
182 name, payload, data = self.create_zone(name=name, rrsets=[rrset])
f63168e6 183 # check our comment has appeared
6754ef71 184 self.assertEquals(get_rrset(data, name, 'SOA')['comments'], rrset['comments'])
f63168e6 185
1d6b70f9
CH
186 def test_create_zone_uncanonical_nameservers(self):
187 name = unique_zone_name()
188 payload = {
189 'name': name,
190 'kind': 'Native',
191 'nameservers': ['uncanon.example.com']
192 }
193 print payload
194 r = self.session.post(
195 self.url("/api/v1/servers/localhost/zones"),
196 data=json.dumps(payload),
197 headers={'content-type': 'application/json'})
198 self.assertEquals(r.status_code, 422)
199 self.assertIn('Nameserver is not canonical', r.json()['error'])
200
201 def test_create_auth_zone_no_name(self):
202 name = unique_zone_name()
203 payload = {
204 'name': '',
205 'kind': 'Native',
206 }
207 print payload
208 r = self.session.post(
209 self.url("/api/v1/servers/localhost/zones"),
210 data=json.dumps(payload),
211 headers={'content-type': 'application/json'})
212 self.assertEquals(r.status_code, 422)
213 self.assertIn('is not canonical', r.json()['error'])
214
c1374bdb 215 def test_create_zone_with_custom_soa(self):
f63168e6 216 name = unique_zone_name()
6754ef71
CH
217 content = u"ns1.example.net. testmaster@example.net. 10 10800 3600 604800 3600"
218 rrset = {
219 "name": name,
220 "type": "soa", # test uppercasing of type, too.
221 "ttl": 3600,
222 "records": [{
223 "content": content,
224 "disabled": False,
225 }],
226 }
227 name, payload, data = self.create_zone(name=name, rrsets=[rrset], soa_edit_api='')
228 self.assertEquals(get_rrset(data, name, 'SOA')['records'], rrset['records'])
229 dbrecs = get_db_records(name, 'SOA')
230 self.assertEqual(dbrecs[0]['content'], content.replace('. ', ' '))
1d6b70f9
CH
231
232 def test_create_zone_double_dot(self):
233 name = 'test..' + unique_zone_name()
234 payload = {
235 'name': name,
236 'kind': 'Native',
237 'nameservers': ['ns1.example.com.']
238 }
239 print payload
240 r = self.session.post(
241 self.url("/api/v1/servers/localhost/zones"),
242 data=json.dumps(payload),
243 headers={'content-type': 'application/json'})
244 self.assertEquals(r.status_code, 422)
245 self.assertIn('Unable to parse DNS Name', r.json()['error'])
05776d2f 246
1d6b70f9
CH
247 def test_create_zone_restricted_chars(self):
248 name = 'test:' + unique_zone_name() # : isn't good as a name.
249 payload = {
250 'name': name,
251 'kind': 'Native',
252 'nameservers': ['ns1.example.com']
253 }
254 print payload
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('contains unsupported characters', r.json()['error'])
4ebf78b1 261
c1374bdb 262 def test_create_zone_with_symbols(self):
6754ef71 263 name, payload, data = self.create_zone(name='foo/bar.'+unique_zone_name())
bee2acae 264 name = payload['name']
1d6b70f9 265 expected_id = name.replace('/', '=2F')
00a9b229
CH
266 for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial'):
267 self.assertIn(k, data)
268 if k in payload:
269 self.assertEquals(data[k], payload[k])
bee2acae 270 self.assertEquals(data['id'], expected_id)
1d6b70f9
CH
271 dbrecs = get_db_records(name, 'SOA')
272 self.assertEqual(dbrecs[0]['name'], name.rstrip('.'))
00a9b229 273
c1374bdb 274 def test_create_zone_with_nameservers_non_string(self):
e90b4e38
CH
275 # ensure we don't crash
276 name = unique_zone_name()
277 payload = {
278 'name': name,
279 'kind': 'Native',
280 'nameservers': [{'a': 'ns1.example.com'}] # invalid
281 }
282 print payload
283 r = self.session.post(
46d06a12 284 self.url("/api/v1/servers/localhost/zones"),
e90b4e38
CH
285 data=json.dumps(payload),
286 headers={'content-type': 'application/json'})
287 self.assertEquals(r.status_code, 422)
288
24e11043
CJ
289 def test_create_zone_metadata(self):
290 payload_metadata = {"type": "Metadata", "kind": "AXFR-SOURCE", "metadata": ["127.0.0.2"]}
291 r = self.session.post(self.url("/api/v1/servers/localhost/zones/example.com/metadata"),
292 data=json.dumps(payload_metadata))
293 rdata = r.json()
294 self.assertEquals(r.status_code, 201)
295 self.assertEquals(rdata["metadata"], payload_metadata["metadata"])
296
297 def test_create_zone_metadata_kind(self):
298 payload_metadata = {"metadata": ["127.0.0.2"]}
299 r = self.session.put(self.url("/api/v1/servers/localhost/zones/example.com/metadata/AXFR-SOURCE"),
300 data=json.dumps(payload_metadata))
301 rdata = r.json()
302 self.assertEquals(r.status_code, 200)
303 self.assertEquals(rdata["metadata"], payload_metadata["metadata"])
304
305 def test_create_protected_zone_metadata(self):
306 # test whether it prevents modification of certain kinds
307 for k in ("NSEC3NARROW", "NSEC3PARAM", "PRESIGNED", "LUA-AXFR-SCRIPT"):
308 payload = {"metadata": ["FOO", "BAR"]}
309 r = self.session.put(self.url("/api/v1/servers/localhost/zones/example.com/metadata/%s" % k),
310 data=json.dumps(payload))
311 self.assertEquals(r.status_code, 422)
312
313 def test_retrieve_zone_metadata(self):
314 payload_metadata = {"type": "Metadata", "kind": "AXFR-SOURCE", "metadata": ["127.0.0.2"]}
315 self.session.post(self.url("/api/v1/servers/localhost/zones/example.com/metadata"),
316 data=json.dumps(payload_metadata))
317 r = self.session.get(self.url("/api/v1/servers/localhost/zones/example.com/metadata"))
318 rdata = r.json()
319 self.assertEquals(r.status_code, 200)
320 self.assertIn(payload_metadata, rdata)
321
322 def test_delete_zone_metadata(self):
323 r = self.session.delete(self.url("/api/v1/servers/localhost/zones/example.com/metadata/AXFR-SOURCE"))
324 self.assertEquals(r.status_code, 200)
325 r = self.session.get(self.url("/api/v1/servers/localhost/zones/example.com/metadata/AXFR-SOURCE"))
326 rdata = r.json()
327 self.assertEquals(r.status_code, 200)
328 self.assertEquals(rdata["metadata"], [])
329
4bdff352
CH
330 def test_create_slave_zone(self):
331 # Test that nameservers can be absent for slave zones.
6754ef71 332 name, payload, data = self.create_zone(kind='Slave', nameservers=None, masters=['127.0.0.2'])
4bdff352
CH
333 for k in ('name', 'masters', 'kind'):
334 self.assertIn(k, data)
335 self.assertEquals(data[k], payload[k])
4de11a54
CH
336 print "payload:", payload
337 print "data:", data
338 # Because slave zones don't get a SOA, we need to test that they'll show up in the zone list.
46d06a12 339 r = self.session.get(self.url("/api/v1/servers/localhost/zones"))
4de11a54
CH
340 zonelist = r.json()
341 print "zonelist:", zonelist
342 self.assertIn(payload['name'], [zone['name'] for zone in zonelist])
343 # Also test that fetching the zone works.
46d06a12 344 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + data['id']))
4de11a54
CH
345 data = r.json()
346 print "zone (fetched):", data
347 for k in ('name', 'masters', 'kind'):
348 self.assertIn(k, data)
349 self.assertEquals(data[k], payload[k])
350 self.assertEqual(data['serial'], 0)
6754ef71 351 self.assertEqual(data['rrsets'], [])
4de11a54
CH
352
353 def test_delete_slave_zone(self):
6754ef71 354 name, payload, data = self.create_zone(kind='Slave', nameservers=None, masters=['127.0.0.2'])
46d06a12 355 r = self.session.delete(self.url("/api/v1/servers/localhost/zones/" + data['id']))
4de11a54 356 r.raise_for_status()
4bdff352 357
a426cb89 358 def test_retrieve_slave_zone(self):
6754ef71 359 name, payload, data = self.create_zone(kind='Slave', nameservers=None, masters=['127.0.0.2'])
a426cb89
CH
360 print "payload:", payload
361 print "data:", data
46d06a12 362 r = self.session.put(self.url("/api/v1/servers/localhost/zones/" + data['id'] + "/axfr-retrieve"))
a426cb89
CH
363 data = r.json()
364 print "status for axfr-retrieve:", data
365 self.assertEqual(data['result'], u'Added retrieval request for \'' + payload['name'] +
366 '\' from master 127.0.0.2')
367
368 def test_notify_master_zone(self):
6754ef71 369 name, payload, data = self.create_zone(kind='Master')
a426cb89
CH
370 print "payload:", payload
371 print "data:", data
46d06a12 372 r = self.session.put(self.url("/api/v1/servers/localhost/zones/" + data['id'] + "/notify"))
a426cb89
CH
373 data = r.json()
374 print "status for notify:", data
375 self.assertEqual(data['result'], 'Notification queued')
376
c1374bdb 377 def test_get_zone_with_symbols(self):
6754ef71 378 name, payload, data = self.create_zone(name='foo/bar.'+unique_zone_name())
3c3c006b 379 name = payload['name']
1d6b70f9 380 zone_id = (name.replace('/', '=2F'))
46d06a12 381 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + zone_id))
c1374bdb 382 data = r.json()
6bb25159 383 for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'dnssec'):
3c3c006b
CH
384 self.assertIn(k, data)
385 if k in payload:
386 self.assertEquals(data[k], payload[k])
387
c1374bdb 388 def test_get_zone(self):
46d06a12 389 r = self.session.get(self.url("/api/v1/servers/localhost/zones"))
05776d2f 390 domains = r.json()
1d6b70f9 391 example_com = [domain for domain in domains if domain['name'] == u'example.com.'][0]
46d06a12 392 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + example_com['id']))
c1374bdb 393 self.assert_success_json(r)
05776d2f
CH
394 data = r.json()
395 for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial'):
396 self.assertIn(k, data)
1d6b70f9 397 self.assertEquals(data['name'], 'example.com.')
7c0ba3d2 398
0f0e73fe
MS
399 def test_import_zone_broken(self):
400 payload = {}
401 payload['zone'] = """
402;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58571
403flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
404;; WARNING: recursion requested but not available
405
406;; OPT PSEUDOSECTION:
407; EDNS: version: 0, flags:; udp: 1680
408;; QUESTION SECTION:
409;powerdns.com. IN SOA
410
411;; ANSWER SECTION:
412powerdns-broken.com. 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
413powerdns-broken.com. 3600 IN NS powerdnssec2.ds9a.nl.
414powerdns-broken.com. 3600 IN AAAA 2001:888:2000:1d::2
415powerdns-broken.com. 86400 IN A 82.94.213.34
416powerdns-broken.com. 3600 IN MX 0 xs.powerdns.com.
417powerdns-broken.com. 3600 IN NS powerdnssec1.ds9a.nl.
418powerdns-broken.com. 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
419"""
1d6b70f9 420 payload['name'] = 'powerdns-broken.com.'
0f0e73fe
MS
421 payload['kind'] = 'Master'
422 payload['nameservers'] = []
423 r = self.session.post(
46d06a12 424 self.url("/api/v1/servers/localhost/zones"),
0f0e73fe
MS
425 data=json.dumps(payload),
426 headers={'content-type': 'application/json'})
427 self.assertEquals(r.status_code, 422)
428
1d6b70f9
CH
429 def test_import_zone_axfr_outofzone(self):
430 # Ensure we don't create out-of-zone records
431 name = unique_zone_name()
432 payload = {}
433 payload['zone'] = """
434NAME 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
435NAME 3600 IN NS powerdnssec2.ds9a.nl.
436example.org. 3600 IN AAAA 2001:888:2000:1d::2
437NAME 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
438""".replace('NAME', name)
439 payload['name'] = name
440 payload['kind'] = 'Master'
441 payload['nameservers'] = []
442 r = self.session.post(
443 self.url("/api/v1/servers/localhost/zones"),
444 data=json.dumps(payload),
445 headers={'content-type': 'application/json'})
446 self.assertEquals(r.status_code, 422)
447 self.assertEqual(r.json()['error'], 'RRset example.org. IN AAAA: Name is out of zone')
448
0f0e73fe
MS
449 def test_import_zone_axfr(self):
450 payload = {}
451 payload['zone'] = """
452;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58571
453;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
454;; WARNING: recursion requested but not available
455
456;; OPT PSEUDOSECTION:
457; EDNS: version: 0, flags:; udp: 1680
458;; QUESTION SECTION:
459;powerdns.com. IN SOA
460
461;; ANSWER SECTION:
462powerdns.com. 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
463powerdns.com. 3600 IN NS powerdnssec2.ds9a.nl.
464powerdns.com. 3600 IN AAAA 2001:888:2000:1d::2
465powerdns.com. 86400 IN A 82.94.213.34
466powerdns.com. 3600 IN MX 0 xs.powerdns.com.
467powerdns.com. 3600 IN NS powerdnssec1.ds9a.nl.
468powerdns.com. 86400 IN SOA powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800
469"""
1d6b70f9 470 payload['name'] = 'powerdns.com.'
0f0e73fe
MS
471 payload['kind'] = 'Master'
472 payload['nameservers'] = []
1d6b70f9 473 payload['soa_edit_api'] = '' # turn off so exact SOA comparison works.
0f0e73fe 474 r = self.session.post(
46d06a12 475 self.url("/api/v1/servers/localhost/zones"),
0f0e73fe
MS
476 data=json.dumps(payload),
477 headers={'content-type': 'application/json'})
478 self.assert_success_json(r)
479 data = r.json()
480 self.assertIn('name', data)
0f0e73fe 481
90568eb2
MS
482 expected = {
483 'NS': [
6754ef71
CH
484 {'content': 'powerdnssec1.ds9a.nl.'},
485 {'content': 'powerdnssec2.ds9a.nl.'},
486 ],
90568eb2 487 'SOA': [
6754ef71
CH
488 {'content': 'powerdnssec1.ds9a.nl. ahu.ds9a.nl. 1343746984 10800 3600 604800 10800'},
489 ],
90568eb2 490 'MX': [
6754ef71
CH
491 {'content': '0 xs.powerdns.com.'},
492 ],
90568eb2 493 'A': [
6754ef71
CH
494 {'content': '82.94.213.34', 'name': 'powerdns.com.'},
495 ],
90568eb2 496 'AAAA': [
6754ef71
CH
497 {'content': '2001:888:2000:1d::2', 'name': 'powerdns.com.'},
498 ],
90568eb2 499 }
0f0e73fe 500
6754ef71 501 eq_zone_rrsets(data['rrsets'], expected)
1d6b70f9
CH
502
503 # noDot check
504 dbrecs = get_db_records(payload['name'], 'NS')
505 self.assertEqual(dbrecs[0]['content'], 'powerdnssec2.ds9a.nl')
0f0e73fe
MS
506
507 def test_import_zone_bind(self):
508 payload = {}
509 payload['zone'] = """
510$TTL 86400 ; 24 hours could have been written as 24h or 1d
511; $TTL used for all RRs without explicit TTL value
512$ORIGIN example.org.
513@ 1D IN SOA ns1.example.org. hostmaster.example.org. (
514 2002022401 ; serial
515 3H ; refresh
516 15 ; retry
517 1w ; expire
518 3h ; minimum
519 )
520 IN NS ns1.example.org. ; in the domain
521 IN NS ns2.smokeyjoe.com. ; external to domain
522 IN MX 10 mail.another.com. ; external mail provider
523; server host definitions
1d6b70f9 524ns1 IN A 192.168.0.1 ;name server definition
0f0e73fe
MS
525www IN A 192.168.0.2 ;web server definition
526ftp IN CNAME www.example.org. ;ftp server definition
527; non server domain hosts
528bill IN A 192.168.0.3
1d6b70f9 529fred IN A 192.168.0.4
0f0e73fe 530"""
1d6b70f9 531 payload['name'] = 'example.org.'
0f0e73fe
MS
532 payload['kind'] = 'Master'
533 payload['nameservers'] = []
1d6b70f9 534 payload['soa_edit_api'] = '' # turn off so exact SOA comparison works.
0f0e73fe 535 r = self.session.post(
46d06a12 536 self.url("/api/v1/servers/localhost/zones"),
0f0e73fe
MS
537 data=json.dumps(payload),
538 headers={'content-type': 'application/json'})
539 self.assert_success_json(r)
540 data = r.json()
541 self.assertIn('name', data)
0f0e73fe 542
90568eb2
MS
543 expected = {
544 'NS': [
6754ef71
CH
545 {'content': 'ns1.example.org.'},
546 {'content': 'ns2.smokeyjoe.com.'},
547 ],
90568eb2 548 'SOA': [
6754ef71
CH
549 {'content': 'ns1.example.org. hostmaster.example.org. 2002022401 10800 15 604800 10800'},
550 ],
90568eb2 551 'MX': [
6754ef71
CH
552 {'content': '10 mail.another.com.'},
553 ],
90568eb2 554 'A': [
6754ef71
CH
555 {'content': '192.168.0.1', 'name': 'ns1.example.org.'},
556 {'content': '192.168.0.2', 'name': 'www.example.org.'},
557 {'content': '192.168.0.3', 'name': 'bill.example.org.'},
558 {'content': '192.168.0.4', 'name': 'fred.example.org.'},
559 ],
90568eb2 560 'CNAME': [
6754ef71
CH
561 {'content': 'www.example.org.', 'name': 'ftp.example.org.'},
562 ],
90568eb2 563 }
0f0e73fe 564
6754ef71 565 eq_zone_rrsets(data['rrsets'], expected)
0f0e73fe 566
c1374bdb 567 def test_export_zone_json(self):
6754ef71 568 name, payload, zone = self.create_zone(nameservers=['ns1.foo.com.', 'ns2.foo.com.'], soa_edit_api='')
a83004d3
CH
569 # export it
570 r = self.session.get(
46d06a12 571 self.url("/api/v1/servers/localhost/zones/" + name + "/export"),
a83004d3
CH
572 headers={'accept': 'application/json;q=0.9,*/*;q=0.8'}
573 )
c1374bdb 574 self.assert_success_json(r)
a83004d3
CH
575 data = r.json()
576 self.assertIn('zone', data)
1d6b70f9
CH
577 expected_data = [name + '\t3600\tNS\tns1.foo.com.',
578 name + '\t3600\tNS\tns2.foo.com.',
579 name + '\t3600\tSOA\ta.misconfigured.powerdns.server. hostmaster.' + name +
580 ' 0 10800 3600 604800 3600']
a83004d3
CH
581 self.assertEquals(data['zone'].strip().split('\n'), expected_data)
582
c1374bdb 583 def test_export_zone_text(self):
6754ef71 584 name, payload, zone = self.create_zone(nameservers=['ns1.foo.com.', 'ns2.foo.com.'], soa_edit_api='')
a83004d3
CH
585 # export it
586 r = self.session.get(
46d06a12 587 self.url("/api/v1/servers/localhost/zones/" + name + "/export"),
a83004d3
CH
588 headers={'accept': '*/*'}
589 )
590 data = r.text.strip().split("\n")
1d6b70f9
CH
591 expected_data = [name + '\t3600\tNS\tns1.foo.com.',
592 name + '\t3600\tNS\tns2.foo.com.',
593 name + '\t3600\tSOA\ta.misconfigured.powerdns.server. hostmaster.' + name +
594 ' 0 10800 3600 604800 3600']
a83004d3
CH
595 self.assertEquals(data, expected_data)
596
c1374bdb 597 def test_update_zone(self):
6754ef71 598 name, payload, zone = self.create_zone()
bee2acae 599 name = payload['name']
d29d5db7 600 # update, set as Master and enable SOA-EDIT-API
7c0ba3d2
CH
601 payload = {
602 'kind': 'Master',
c1374bdb 603 'masters': ['192.0.2.1', '192.0.2.2'],
6bb25159
MS
604 'soa_edit_api': 'EPOCH',
605 'soa_edit': 'EPOCH'
7c0ba3d2
CH
606 }
607 r = self.session.put(
46d06a12 608 self.url("/api/v1/servers/localhost/zones/" + name),
7c0ba3d2
CH
609 data=json.dumps(payload),
610 headers={'content-type': 'application/json'})
f0e76cee
CH
611 self.assert_success(r)
612 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
7c0ba3d2
CH
613 for k in payload.keys():
614 self.assertIn(k, data)
615 self.assertEquals(data[k], payload[k])
d29d5db7 616 # update, back to Native and empty(off)
7c0ba3d2 617 payload = {
d29d5db7 618 'kind': 'Native',
6bb25159
MS
619 'soa_edit_api': '',
620 'soa_edit': ''
7c0ba3d2
CH
621 }
622 r = self.session.put(
46d06a12 623 self.url("/api/v1/servers/localhost/zones/" + name),
7c0ba3d2
CH
624 data=json.dumps(payload),
625 headers={'content-type': 'application/json'})
f0e76cee
CH
626 self.assert_success(r)
627 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
7c0ba3d2
CH
628 for k in payload.keys():
629 self.assertIn(k, data)
630 self.assertEquals(data[k], payload[k])
b3905a3d 631
c1374bdb 632 def test_zone_rr_update(self):
6754ef71 633 name, payload, zone = self.create_zone()
b3905a3d 634 # do a replace (= update)
d708640f 635 rrset = {
b3905a3d
CH
636 'changetype': 'replace',
637 'name': name,
8ce0dc75 638 'type': 'ns',
6754ef71 639 'ttl': 3600,
b3905a3d
CH
640 'records': [
641 {
1d6b70f9 642 "content": "ns1.bar.com.",
cea26350
CH
643 "disabled": False
644 },
645 {
1d6b70f9 646 "content": "ns2-disabled.bar.com.",
cea26350 647 "disabled": True
b3905a3d
CH
648 }
649 ]
650 }
d708640f 651 payload = {'rrsets': [rrset]}
b3905a3d 652 r = self.session.patch(
46d06a12 653 self.url("/api/v1/servers/localhost/zones/" + name),
b3905a3d
CH
654 data=json.dumps(payload),
655 headers={'content-type': 'application/json'})
f0e76cee 656 self.assert_success(r)
b3905a3d 657 # verify that (only) the new record is there
f0e76cee 658 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
6754ef71 659 self.assertEquals(get_rrset(data, name, 'NS')['records'], rrset['records'])
b3905a3d 660
c1374bdb 661 def test_zone_rr_update_mx(self):
05cf6a71 662 # Important to test with MX records, as they have a priority field, which must end up in the content field.
6754ef71 663 name, payload, zone = self.create_zone()
41e3b10e 664 # do a replace (= update)
d708640f 665 rrset = {
41e3b10e
CH
666 'changetype': 'replace',
667 'name': name,
668 'type': 'MX',
6754ef71 669 'ttl': 3600,
41e3b10e
CH
670 'records': [
671 {
1d6b70f9 672 "content": "10 mail.example.org.",
41e3b10e
CH
673 "disabled": False
674 }
675 ]
676 }
d708640f 677 payload = {'rrsets': [rrset]}
41e3b10e 678 r = self.session.patch(
46d06a12 679 self.url("/api/v1/servers/localhost/zones/" + name),
41e3b10e
CH
680 data=json.dumps(payload),
681 headers={'content-type': 'application/json'})
f0e76cee 682 self.assert_success(r)
41e3b10e 683 # verify that (only) the new record is there
f0e76cee 684 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
6754ef71 685 self.assertEquals(get_rrset(data, name, 'MX')['records'], rrset['records'])
d708640f 686
c1374bdb 687 def test_zone_rr_update_multiple_rrsets(self):
6754ef71 688 name, payload, zone = self.create_zone()
d708640f
CH
689 rrset1 = {
690 'changetype': 'replace',
691 'name': name,
692 'type': 'NS',
6754ef71 693 'ttl': 3600,
d708640f
CH
694 'records': [
695 {
6754ef71 696
1d6b70f9 697 "content": "ns9999.example.com.",
d708640f
CH
698 "disabled": False
699 }
700 ]
701 }
702 rrset2 = {
703 'changetype': 'replace',
704 'name': name,
705 'type': 'MX',
6754ef71 706 'ttl': 3600,
d708640f
CH
707 'records': [
708 {
1d6b70f9 709 "content": "10 mx444.example.com.",
d708640f
CH
710 "disabled": False
711 }
712 ]
713 }
714 payload = {'rrsets': [rrset1, rrset2]}
715 r = self.session.patch(
46d06a12 716 self.url("/api/v1/servers/localhost/zones/" + name),
d708640f
CH
717 data=json.dumps(payload),
718 headers={'content-type': 'application/json'})
f0e76cee 719 self.assert_success(r)
d708640f 720 # verify that all rrsets have been updated
f0e76cee 721 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
6754ef71
CH
722 self.assertEquals(get_rrset(data, name, 'NS')['records'], rrset1['records'])
723 self.assertEquals(get_rrset(data, name, 'MX')['records'], rrset2['records'])
41e3b10e 724
c1374bdb 725 def test_zone_rr_delete(self):
6754ef71 726 name, payload, zone = self.create_zone()
b3905a3d 727 # do a delete of all NS records (these are created with the zone)
d708640f 728 rrset = {
b3905a3d
CH
729 'changetype': 'delete',
730 'name': name,
731 'type': 'NS'
732 }
d708640f 733 payload = {'rrsets': [rrset]}
b3905a3d 734 r = self.session.patch(
46d06a12 735 self.url("/api/v1/servers/localhost/zones/" + name),
b3905a3d
CH
736 data=json.dumps(payload),
737 headers={'content-type': 'application/json'})
f0e76cee 738 self.assert_success(r)
b3905a3d 739 # verify that the records are gone
f0e76cee 740 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
6754ef71 741 self.assertIsNone(get_rrset(data, name, 'NS'))
cea26350 742
c1374bdb 743 def test_zone_disable_reenable(self):
d29d5db7 744 # This also tests that SOA-EDIT-API works.
6754ef71 745 name, payload, zone = self.create_zone(soa_edit_api='EPOCH')
cea26350 746 # disable zone by disabling SOA
d708640f 747 rrset = {
cea26350
CH
748 'changetype': 'replace',
749 'name': name,
750 'type': 'SOA',
6754ef71 751 'ttl': 3600,
cea26350
CH
752 'records': [
753 {
1d6b70f9 754 "content": "ns1.bar.com. hostmaster.foo.org. 1 1 1 1 1",
cea26350
CH
755 "disabled": True
756 }
757 ]
758 }
d708640f 759 payload = {'rrsets': [rrset]}
cea26350 760 r = self.session.patch(
46d06a12 761 self.url("/api/v1/servers/localhost/zones/" + name),
cea26350
CH
762 data=json.dumps(payload),
763 headers={'content-type': 'application/json'})
f0e76cee 764 self.assert_success(r)
d29d5db7 765 # check SOA serial has been edited
f0e76cee
CH
766 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
767 soa_serial1 = get_first_rec(data, name, 'SOA')['content'].split()[2]
d29d5db7
CH
768 self.assertNotEquals(soa_serial1, '1')
769 # make sure domain is still in zone list (disabled SOA!)
46d06a12 770 r = self.session.get(self.url("/api/v1/servers/localhost/zones"))
cea26350
CH
771 domains = r.json()
772 self.assertEquals(len([domain for domain in domains if domain['name'] == name]), 1)
d29d5db7
CH
773 # sleep 1sec to ensure the EPOCH value changes for the next request
774 time.sleep(1)
cea26350 775 # verify that modifying it still works
d708640f
CH
776 rrset['records'][0]['disabled'] = False
777 payload = {'rrsets': [rrset]}
cea26350 778 r = self.session.patch(
46d06a12 779 self.url("/api/v1/servers/localhost/zones/" + name),
cea26350
CH
780 data=json.dumps(payload),
781 headers={'content-type': 'application/json'})
f0e76cee 782 self.assert_success(r)
d29d5db7 783 # check SOA serial has been edited again
f0e76cee
CH
784 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
785 soa_serial2 = get_first_rec(data, name, 'SOA')['content'].split()[2]
d29d5db7
CH
786 self.assertNotEquals(soa_serial2, '1')
787 self.assertNotEquals(soa_serial2, soa_serial1)
02945d9a 788
c1374bdb 789 def test_zone_rr_update_out_of_zone(self):
6754ef71 790 name, payload, zone = self.create_zone()
35f26cc5 791 # replace with qname mismatch
d708640f 792 rrset = {
35f26cc5 793 'changetype': 'replace',
1d6b70f9 794 'name': 'not-in-zone.',
35f26cc5 795 'type': 'NS',
6754ef71 796 'ttl': 3600,
35f26cc5
CH
797 'records': [
798 {
1d6b70f9 799 "content": "ns1.bar.com.",
35f26cc5
CH
800 "disabled": False
801 }
802 ]
803 }
d708640f 804 payload = {'rrsets': [rrset]}
35f26cc5 805 r = self.session.patch(
46d06a12 806 self.url("/api/v1/servers/localhost/zones/" + name),
35f26cc5
CH
807 data=json.dumps(payload),
808 headers={'content-type': 'application/json'})
809 self.assertEquals(r.status_code, 422)
810 self.assertIn('out of zone', r.json()['error'])
811
1d6b70f9 812 def test_zone_rr_update_restricted_chars(self):
6754ef71 813 name, payload, zone = self.create_zone()
1d6b70f9
CH
814 # replace with qname mismatch
815 rrset = {
816 'changetype': 'replace',
817 'name': 'test:' + name,
818 'type': 'NS',
6754ef71 819 'ttl': 3600,
1d6b70f9
CH
820 'records': [
821 {
1d6b70f9
CH
822 "content": "ns1.bar.com.",
823 "disabled": False
824 }
825 ]
826 }
827 payload = {'rrsets': [rrset]}
828 r = self.session.patch(
829 self.url("/api/v1/servers/localhost/zones/" + name),
830 data=json.dumps(payload),
831 headers={'content-type': 'application/json'})
832 self.assertEquals(r.status_code, 422)
833 self.assertIn('contains unsupported characters', r.json()['error'])
834
24cd86ca 835 def test_rrset_unknown_type(self):
6754ef71 836 name, payload, zone = self.create_zone()
24cd86ca
CH
837 rrset = {
838 'changetype': 'replace',
839 'name': name,
840 'type': 'FAFAFA',
6754ef71 841 'ttl': 3600,
24cd86ca
CH
842 'records': [
843 {
24cd86ca
CH
844 "content": "4.3.2.1",
845 "disabled": False
846 }
847 ]
848 }
849 payload = {'rrsets': [rrset]}
46d06a12 850 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
24cd86ca
CH
851 headers={'content-type': 'application/json'})
852 self.assertEquals(r.status_code, 422)
853 self.assertIn('unknown type', r.json()['error'])
854
1e5b9ab9
CH
855 def test_create_zone_with_leading_space(self):
856 # Actual regression.
6754ef71 857 name, payload, zone = self.create_zone()
1e5b9ab9
CH
858 rrset = {
859 'changetype': 'replace',
860 'name': name,
861 'type': 'A',
6754ef71 862 'ttl': 3600,
1e5b9ab9
CH
863 'records': [
864 {
1e5b9ab9
CH
865 "content": " 4.3.2.1",
866 "disabled": False
867 }
868 ]
869 }
870 payload = {'rrsets': [rrset]}
46d06a12 871 r = self.session.patch(self.url("/api/v1/servers/localhost/zones/" + name), data=json.dumps(payload),
1e5b9ab9
CH
872 headers={'content-type': 'application/json'})
873 self.assertEquals(r.status_code, 422)
874 self.assertIn('Not in expected format', r.json()['error'])
875
c1374bdb 876 def test_zone_rr_delete_out_of_zone(self):
6754ef71 877 name, payload, zone = self.create_zone()
d708640f 878 rrset = {
35f26cc5 879 'changetype': 'delete',
1d6b70f9 880 'name': 'not-in-zone.',
35f26cc5
CH
881 'type': 'NS'
882 }
d708640f 883 payload = {'rrsets': [rrset]}
35f26cc5 884 r = self.session.patch(
46d06a12 885 self.url("/api/v1/servers/localhost/zones/" + name),
35f26cc5
CH
886 data=json.dumps(payload),
887 headers={'content-type': 'application/json'})
34df6ecc 888 print r.content
f0e76cee 889 self.assert_success(r) # succeed so users can fix their wrong, old data
35f26cc5 890
37663c3b 891 def test_zone_delete(self):
6754ef71 892 name, payload, zone = self.create_zone()
46d06a12 893 r = self.session.delete(self.url("/api/v1/servers/localhost/zones/" + name))
37663c3b
CH
894 self.assertEquals(r.status_code, 204)
895 self.assertNotIn('Content-Type', r.headers)
896
c1374bdb 897 def test_zone_comment_create(self):
6754ef71 898 name, payload, zone = self.create_zone()
d708640f 899 rrset = {
6cc98ddf
CH
900 'changetype': 'replace',
901 'name': name,
902 'type': 'NS',
6754ef71 903 'ttl': 3600,
6cc98ddf
CH
904 'comments': [
905 {
906 'account': 'test1',
907 'content': 'blah blah',
908 },
909 {
910 'account': 'test2',
911 'content': 'blah blah bleh',
912 }
913 ]
914 }
d708640f 915 payload = {'rrsets': [rrset]}
6cc98ddf 916 r = self.session.patch(
46d06a12 917 self.url("/api/v1/servers/localhost/zones/" + name),
6cc98ddf
CH
918 data=json.dumps(payload),
919 headers={'content-type': 'application/json'})
f0e76cee 920 self.assert_success(r)
6cc98ddf
CH
921 # make sure the comments have been set, and that the NS
922 # records are still present
f0e76cee
CH
923 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
924 serverset = get_rrset(data, name, 'NS')
6754ef71
CH
925 print serverset
926 self.assertNotEquals(serverset['records'], [])
927 self.assertNotEquals(serverset['comments'], [])
6cc98ddf 928 # verify that modified_at has been set by pdns
6754ef71 929 self.assertNotEquals([c for c in serverset['comments']][0]['modified_at'], 0)
6cc98ddf 930
c1374bdb 931 def test_zone_comment_delete(self):
6cc98ddf 932 # Test: Delete ONLY comments.
6754ef71 933 name, payload, zone = self.create_zone()
d708640f 934 rrset = {
6cc98ddf
CH
935 'changetype': 'replace',
936 'name': name,
937 'type': 'NS',
938 'comments': []
939 }
d708640f 940 payload = {'rrsets': [rrset]}
6cc98ddf 941 r = self.session.patch(
46d06a12 942 self.url("/api/v1/servers/localhost/zones/" + name),
6cc98ddf
CH
943 data=json.dumps(payload),
944 headers={'content-type': 'application/json'})
f0e76cee 945 self.assert_success(r)
6cc98ddf 946 # make sure the NS records are still present
f0e76cee
CH
947 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
948 serverset = get_rrset(data, name, 'NS')
6754ef71
CH
949 print serverset
950 self.assertNotEquals(serverset['records'], [])
951 self.assertEquals(serverset['comments'], [])
6cc98ddf 952
c1374bdb 953 def test_zone_comment_stay_intact(self):
6cc98ddf 954 # Test if comments on an rrset stay intact if the rrset is replaced
6754ef71 955 name, payload, zone = self.create_zone()
6cc98ddf 956 # create a comment
d708640f 957 rrset = {
6cc98ddf
CH
958 'changetype': 'replace',
959 'name': name,
960 'type': 'NS',
961 'comments': [
962 {
963 'account': 'test1',
964 'content': 'oh hi there',
2696eea0 965 'modified_at': 1111
6cc98ddf
CH
966 }
967 ]
968 }
d708640f 969 payload = {'rrsets': [rrset]}
6cc98ddf 970 r = self.session.patch(
46d06a12 971 self.url("/api/v1/servers/localhost/zones/" + name),
6cc98ddf
CH
972 data=json.dumps(payload),
973 headers={'content-type': 'application/json'})
f0e76cee 974 self.assert_success(r)
6cc98ddf 975 # replace rrset records
d708640f 976 rrset2 = {
6cc98ddf
CH
977 'changetype': 'replace',
978 'name': name,
979 'type': 'NS',
6754ef71 980 'ttl': 3600,
6cc98ddf
CH
981 'records': [
982 {
1d6b70f9 983 "content": "ns1.bar.com.",
6cc98ddf
CH
984 "disabled": False
985 }
986 ]
987 }
d708640f 988 payload2 = {'rrsets': [rrset2]}
6cc98ddf 989 r = self.session.patch(
46d06a12 990 self.url("/api/v1/servers/localhost/zones/" + name),
6cc98ddf
CH
991 data=json.dumps(payload2),
992 headers={'content-type': 'application/json'})
f0e76cee 993 self.assert_success(r)
6cc98ddf 994 # make sure the comments still exist
f0e76cee
CH
995 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + name)).json()
996 serverset = get_rrset(data, name, 'NS')
6754ef71
CH
997 print serverset
998 self.assertEquals(serverset['records'], rrset2['records'])
999 self.assertEquals(serverset['comments'], rrset['comments'])
6cc98ddf 1000
3fe7c7d6
CH
1001 def test_zone_auto_ptr_ipv4_create(self):
1002 revzone = '4.2.192.in-addr.arpa.'
1003 _, _, revzonedata = self.create_zone(name=revzone)
1004 name = unique_zone_name()
1005 rrset = {
1006 "name": name,
1007 "type": "A",
1008 "ttl": 3600,
1009 "records": [{
1010 "content": "192.2.4.44",
1011 "disabled": False,
1012 "set-ptr": True,
1013 }],
1014 }
1015 name, payload, data = self.create_zone(name=name, rrsets=[rrset])
1016 del rrset['records'][0]['set-ptr']
1017 self.assertEquals(get_rrset(data, name, 'A')['records'], rrset['records'])
1018 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + revzone)).json()
1019 revsets = [s for s in r['rrsets'] if s['type'] == 'PTR']
1020 print revsets
1021 self.assertEquals(revsets, [{
1022 u'name': u'44.4.2.192.in-addr.arpa.',
1023 u'ttl': 3600,
1024 u'type': u'PTR',
1025 u'comments': [],
1026 u'records': [{
1027 u'content': name,
1028 u'disabled': False,
1029 }],
1030 }])
1031 # with SOA-EDIT-API DEFAULT on the revzone, the serial should now be higher.
1032 self.assertGreater(r['serial'], revzonedata['serial'])
1033
1034 def test_zone_auto_ptr_ipv4_update(self):
1d6b70f9 1035 revzone = '0.2.192.in-addr.arpa.'
a41c038a 1036 _, _, revzonedata = self.create_zone(name=revzone)
6754ef71 1037 name, payload, zone = self.create_zone()
d708640f 1038 rrset = {
d1587ceb
CH
1039 'changetype': 'replace',
1040 'name': name,
1041 'type': 'A',
6754ef71 1042 'ttl': 3600,
d1587ceb
CH
1043 'records': [
1044 {
d1587ceb
CH
1045 "content": '192.2.0.2',
1046 "disabled": False,
1047 "set-ptr": True
1048 }
1049 ]
1050 }
d708640f 1051 payload = {'rrsets': [rrset]}
d1587ceb 1052 r = self.session.patch(
46d06a12 1053 self.url("/api/v1/servers/localhost/zones/" + name),
d1587ceb
CH
1054 data=json.dumps(payload),
1055 headers={'content-type': 'application/json'})
f0e76cee 1056 self.assert_success(r)
a41c038a
CH
1057 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + revzone)).json()
1058 revsets = [s for s in r['rrsets'] if s['type'] == 'PTR']
6754ef71
CH
1059 print revsets
1060 self.assertEquals(revsets, [{
1061 u'name': u'2.0.2.192.in-addr.arpa.',
d1587ceb 1062 u'ttl': 3600,
d1587ceb 1063 u'type': u'PTR',
6754ef71
CH
1064 u'comments': [],
1065 u'records': [{
1066 u'content': name,
1067 u'disabled': False,
1068 }],
d1587ceb 1069 }])
a41c038a
CH
1070 # with SOA-EDIT-API DEFAULT on the revzone, the serial should now be higher.
1071 self.assertGreater(r['serial'], revzonedata['serial'])
d1587ceb 1072
3fe7c7d6 1073 def test_zone_auto_ptr_ipv6_update(self):
d1587ceb 1074 # 2001:DB8::bb:aa
1d6b70f9 1075 revzone = '8.b.d.0.1.0.0.2.ip6.arpa.'
a41c038a 1076 _, _, revzonedata = self.create_zone(name=revzone)
6754ef71 1077 name, payload, zone = self.create_zone()
d708640f 1078 rrset = {
d1587ceb
CH
1079 'changetype': 'replace',
1080 'name': name,
1081 'type': 'AAAA',
6754ef71 1082 'ttl': 3600,
d1587ceb
CH
1083 'records': [
1084 {
d1587ceb
CH
1085 "content": '2001:DB8::bb:aa',
1086 "disabled": False,
1087 "set-ptr": True
1088 }
1089 ]
1090 }
d708640f 1091 payload = {'rrsets': [rrset]}
d1587ceb 1092 r = self.session.patch(
46d06a12 1093 self.url("/api/v1/servers/localhost/zones/" + name),
d1587ceb
CH
1094 data=json.dumps(payload),
1095 headers={'content-type': 'application/json'})
f0e76cee 1096 self.assert_success(r)
a41c038a
CH
1097 r = self.session.get(self.url("/api/v1/servers/localhost/zones/" + revzone)).json()
1098 revsets = [s for s in r['rrsets'] if s['type'] == 'PTR']
6754ef71
CH
1099 print revsets
1100 self.assertEquals(revsets, [{
1101 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 1102 u'ttl': 3600,
d1587ceb 1103 u'type': u'PTR',
6754ef71
CH
1104 u'comments': [],
1105 u'records': [{
1106 u'content': name,
1107 u'disabled': False,
1108 }],
d1587ceb 1109 }])
a41c038a
CH
1110 # with SOA-EDIT-API DEFAULT on the revzone, the serial should now be higher.
1111 self.assertGreater(r['serial'], revzonedata['serial'])
d1587ceb 1112
c1374bdb 1113 def test_search_rr_exact_zone(self):
b1902fab 1114 name = unique_zone_name()
1d6b70f9
CH
1115 self.create_zone(name=name, serial=22, soa_edit_api='')
1116 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=" + name.rstrip('.')))
c1374bdb 1117 self.assert_success_json(r)
b1902fab 1118 print r.json()
1d6b70f9
CH
1119 self.assertEquals(r.json(), [
1120 {u'object_type': u'zone', u'name': name, u'zone_id': name},
1121 {u'content': u'a.misconfigured.powerdns.server. hostmaster.'+name+' 22 10800 3600 604800 3600',
1122 u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
1123 u'ttl': 3600, u'type': u'SOA', u'name': name},
1124 {u'content': u'ns1.example.com.',
1125 u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
1126 u'ttl': 3600, u'type': u'NS', u'name': name},
1127 {u'content': u'ns2.example.com.',
1128 u'zone_id': name, u'zone': name, u'object_type': u'record', u'disabled': False,
1129 u'ttl': 3600, u'type': u'NS', u'name': name},
1130 ])
b1902fab 1131
c1374bdb 1132 def test_search_rr_substring(self):
1d6b70f9 1133 name = 'search-rr-zone.name.'
b1902fab 1134 self.create_zone(name=name)
46d06a12 1135 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=*rr-zone*"))
c1374bdb 1136 self.assert_success_json(r)
b1902fab
CH
1137 print r.json()
1138 # should return zone, SOA, ns1, ns2
60a8e825 1139 self.assertEquals(len(r.json()), 4)
b1902fab 1140
c1374bdb 1141 def test_search_rr_case_insensitive(self):
1d6b70f9 1142 name = 'search-rr-insenszone.name.'
57cb86d8 1143 self.create_zone(name=name)
46d06a12 1144 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=*rr-insensZONE*"))
c1374bdb 1145 self.assert_success_json(r)
57cb86d8
CH
1146 print r.json()
1147 # should return zone, SOA, ns1, ns2
60a8e825 1148 self.assertEquals(len(r.json()), 4)
57cb86d8 1149
02945d9a 1150
406497f5
CH
1151@unittest.skipIf(not is_auth(), "Not applicable")
1152class AuthRootZone(ApiTestCase, AuthZonesHelperMixin):
1153
1154 def setUp(self):
1155 super(AuthRootZone, self).setUp()
1156 # zone name is not unique, so delete the zone before each individual test.
46d06a12 1157 self.session.delete(self.url("/api/v1/servers/localhost/zones/=2E"))
406497f5
CH
1158
1159 def test_create_zone(self):
6754ef71 1160 name, payload, data = self.create_zone(name='.', serial=22, soa_edit_api='')
406497f5
CH
1161 for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'soa_edit_api', 'soa_edit', 'account'):
1162 self.assertIn(k, data)
1163 if k in payload:
1164 self.assertEquals(data[k], payload[k])
406497f5 1165 # validate generated SOA
6754ef71 1166 rec = get_first_rec(data, '.', 'SOA')
406497f5 1167 self.assertEquals(
6754ef71 1168 rec['content'],
1d6b70f9 1169 "a.misconfigured.powerdns.server. hostmaster. " + str(payload['serial']) +
406497f5
CH
1170 " 10800 3600 604800 3600"
1171 )
1172 # Regression test: verify zone list works
46d06a12 1173 zonelist = self.session.get(self.url("/api/v1/servers/localhost/zones")).json()
406497f5
CH
1174 print "zonelist:", zonelist
1175 self.assertIn(payload['name'], [zone['name'] for zone in zonelist])
1176 # Also test that fetching the zone works.
1177 print "id:", data['id']
1178 self.assertEquals(data['id'], '=2E')
46d06a12 1179 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + data['id'])).json()
406497f5
CH
1180 print "zone (fetched):", data
1181 for k in ('name', 'kind'):
1182 self.assertIn(k, data)
1183 self.assertEquals(data[k], payload[k])
6754ef71 1184 self.assertEqual(data['rrsets'][0]['name'], '.')
406497f5
CH
1185
1186 def test_update_zone(self):
6754ef71 1187 name, payload, zone = self.create_zone(name='.')
406497f5
CH
1188 zone_id = '=2E'
1189 # update, set as Master and enable SOA-EDIT-API
1190 payload = {
1191 'kind': 'Master',
1192 'masters': ['192.0.2.1', '192.0.2.2'],
1193 'soa_edit_api': 'EPOCH',
1194 'soa_edit': 'EPOCH'
1195 }
1196 r = self.session.put(
46d06a12 1197 self.url("/api/v1/servers/localhost/zones/" + zone_id),
406497f5
CH
1198 data=json.dumps(payload),
1199 headers={'content-type': 'application/json'})
f0e76cee
CH
1200 self.assert_success(r)
1201 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + zone_id)).json()
406497f5
CH
1202 for k in payload.keys():
1203 self.assertIn(k, data)
1204 self.assertEquals(data[k], payload[k])
1205 # update, back to Native and empty(off)
1206 payload = {
1207 'kind': 'Native',
1208 'soa_edit_api': '',
1209 'soa_edit': ''
1210 }
1211 r = self.session.put(
46d06a12 1212 self.url("/api/v1/servers/localhost/zones/" + zone_id),
406497f5
CH
1213 data=json.dumps(payload),
1214 headers={'content-type': 'application/json'})
f0e76cee
CH
1215 self.assert_success(r)
1216 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + zone_id)).json()
406497f5
CH
1217 for k in payload.keys():
1218 self.assertIn(k, data)
1219 self.assertEquals(data[k], payload[k])
1220
1221
c1374bdb 1222@unittest.skipIf(not is_recursor(), "Not applicable")
02945d9a
CH
1223class RecursorZones(ApiTestCase):
1224
37bc3d01
CH
1225 def create_zone(self, name=None, kind=None, rd=False, servers=None):
1226 if name is None:
1227 name = unique_zone_name()
1228 if servers is None:
1229 servers = []
02945d9a 1230 payload = {
37bc3d01
CH
1231 'name': name,
1232 'kind': kind,
1233 'servers': servers,
1234 'recursion_desired': rd
02945d9a
CH
1235 }
1236 r = self.session.post(
46d06a12 1237 self.url("/api/v1/servers/localhost/zones"),
02945d9a
CH
1238 data=json.dumps(payload),
1239 headers={'content-type': 'application/json'})
c1374bdb
CH
1240 self.assert_success_json(r)
1241 return payload, r.json()
37bc3d01 1242
c1374bdb 1243 def test_create_auth_zone(self):
37bc3d01 1244 payload, data = self.create_zone(kind='Native')
02945d9a
CH
1245 for k in payload.keys():
1246 self.assertEquals(data[k], payload[k])
1247
1d6b70f9
CH
1248 def test_create_zone_no_name(self):
1249 name = unique_zone_name()
1250 payload = {
1251 'name': '',
1252 'kind': 'Native',
1253 'servers': ['8.8.8.8'],
1254 'recursion_desired': False,
1255 }
1256 print payload
1257 r = self.session.post(
1258 self.url("/api/v1/servers/localhost/zones"),
1259 data=json.dumps(payload),
1260 headers={'content-type': 'application/json'})
1261 self.assertEquals(r.status_code, 422)
1262 self.assertIn('is not canonical', r.json()['error'])
1263
c1374bdb 1264 def test_create_forwarded_zone(self):
37bc3d01 1265 payload, data = self.create_zone(kind='Forwarded', rd=False, servers=['8.8.8.8'])
02945d9a
CH
1266 # return values are normalized
1267 payload['servers'][0] += ':53'
02945d9a
CH
1268 for k in payload.keys():
1269 self.assertEquals(data[k], payload[k])
1270
c1374bdb 1271 def test_create_forwarded_rd_zone(self):
1d6b70f9 1272 payload, data = self.create_zone(name='google.com.', kind='Forwarded', rd=True, servers=['8.8.8.8'])
02945d9a
CH
1273 # return values are normalized
1274 payload['servers'][0] += ':53'
02945d9a
CH
1275 for k in payload.keys():
1276 self.assertEquals(data[k], payload[k])
1277
c1374bdb 1278 def test_create_auth_zone_with_symbols(self):
37bc3d01 1279 payload, data = self.create_zone(name='foo/bar.'+unique_zone_name(), kind='Native')
1dbe38ba 1280 expected_id = (payload['name'].replace('/', '=2F'))
02945d9a
CH
1281 for k in payload.keys():
1282 self.assertEquals(data[k], payload[k])
1283 self.assertEquals(data['id'], expected_id)
e2367534 1284
c1374bdb 1285 def test_rename_auth_zone(self):
37bc3d01 1286 payload, data = self.create_zone(kind='Native')
1d6b70f9 1287 name = payload['name']
e2367534
CH
1288 # now rename it
1289 payload = {
1290 'name': 'renamed-'+name,
1291 'kind': 'Native',
1292 'recursion_desired': False
1293 }
1294 r = self.session.put(
46d06a12 1295 self.url("/api/v1/servers/localhost/zones/" + name),
e2367534
CH
1296 data=json.dumps(payload),
1297 headers={'content-type': 'application/json'})
f0e76cee
CH
1298 self.assert_success(r)
1299 data = self.session.get(self.url("/api/v1/servers/localhost/zones/" + payload['name'])).json()
e2367534
CH
1300 for k in payload.keys():
1301 self.assertEquals(data[k], payload[k])
37bc3d01 1302
37663c3b
CH
1303 def test_zone_delete(self):
1304 payload, zone = self.create_zone(kind='Native')
1305 name = payload['name']
46d06a12 1306 r = self.session.delete(self.url("/api/v1/servers/localhost/zones/" + name))
37663c3b
CH
1307 self.assertEquals(r.status_code, 204)
1308 self.assertNotIn('Content-Type', r.headers)
1309
c1374bdb 1310 def test_search_rr_exact_zone(self):
1d6b70f9 1311 name = unique_zone_name()
37bc3d01 1312 self.create_zone(name=name, kind='Native')
46d06a12 1313 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=" + name))
c1374bdb 1314 self.assert_success_json(r)
37bc3d01
CH
1315 print r.json()
1316 self.assertEquals(r.json(), [{u'type': u'zone', u'name': name, u'zone_id': name}])
1317
c1374bdb 1318 def test_search_rr_substring(self):
1d6b70f9 1319 name = 'search-rr-zone.name.'
37bc3d01 1320 self.create_zone(name=name, kind='Native')
46d06a12 1321 r = self.session.get(self.url("/api/v1/servers/localhost/search-data?q=rr-zone"))
c1374bdb 1322 self.assert_success_json(r)
37bc3d01
CH
1323 print r.json()
1324 # should return zone, SOA
1325 self.assertEquals(len(r.json()), 2)
ccfabd0d
CH
1326
1327
1328@unittest.skipIf(not is_auth(), "Not applicable")
1329class AuthZoneKeys(ApiTestCase, AuthZonesHelperMixin):
1330
1331 def test_get_keys(self):
1332 r = self.session.get(
1333 self.url("/api/v1/servers/localhost/zones/powerdnssec.org./cryptokeys"))
1334 self.assert_success_json(r)
1335 keys = r.json()
1336 self.assertGreater(len(keys), 0)
1337
1338 key0 = deepcopy(keys[0])
1339 del key0['dnskey']
b6bd795c 1340 del key0['ds']
ccfabd0d
CH
1341 expected = {
1342 u'active': True,
1343 u'type': u'Cryptokey',
b6bd795c
PL
1344 u'keytype': u'csk',
1345 u'flags': 257,
ccfabd0d
CH
1346 u'id': 1}
1347 self.assertEquals(key0, expected)
1348
1349 keydata = keys[0]['dnskey'].split()
1350 self.assertEqual(len(keydata), 4)