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