1
0
mirror of https://github.com/github/octodns.git synced 2024-05-11 05:55:00 +00:00
Files
github-octodns/tests/test_octodns_provider_ns1.py
2018-01-08 12:28:25 -05:00

457 lines
15 KiB
Python

#
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from mock import Mock, call, patch
from nsone.rest.errors import AuthException, RateLimitException, \
ResourceException
from unittest import TestCase
from octodns.record import Delete, Record, Update
from octodns.provider.ns1 import Ns1Provider
from octodns.zone import Zone
class DummyZone(object):
def __init__(self, records):
self.data = {
'records': records
}
class TestNs1Provider(TestCase):
zone = Zone('unit.tests.', [])
expected = set()
expected.add(Record.new(zone, '', {
'ttl': 32,
'type': 'A',
'value': '1.2.3.4',
'meta': {},
}))
expected.add(Record.new(zone, 'foo', {
'ttl': 33,
'type': 'A',
'values': ['1.2.3.4', '1.2.3.5'],
'meta': {},
}))
expected.add(Record.new(zone, 'geo', {
'ttl': 34,
'type': 'A',
'values': ['101.102.103.104', '101.102.103.105'],
'geo': {'NA-US-NY': ['201.202.203.204']},
'meta': {},
}))
expected.add(Record.new(zone, 'cname', {
'ttl': 34,
'type': 'CNAME',
'value': 'foo.unit.tests.',
}))
expected.add(Record.new(zone, '', {
'ttl': 35,
'type': 'MX',
'values': [{
'preference': 10,
'exchange': 'mx1.unit.tests.',
}, {
'preference': 20,
'exchange': 'mx2.unit.tests.',
}]
}))
expected.add(Record.new(zone, 'naptr', {
'ttl': 36,
'type': 'NAPTR',
'values': [{
'flags': 'U',
'order': 100,
'preference': 100,
'regexp': '!^.*$!sip:info@bar.example.com!',
'replacement': '.',
'service': 'SIP+D2U',
}, {
'flags': 'S',
'order': 10,
'preference': 100,
'regexp': '!^.*$!sip:info@bar.example.com!',
'replacement': '.',
'service': 'SIP+D2U',
}]
}))
expected.add(Record.new(zone, '', {
'ttl': 37,
'type': 'NS',
'values': ['ns1.unit.tests.', 'ns2.unit.tests.'],
}))
expected.add(Record.new(zone, '_srv._tcp', {
'ttl': 38,
'type': 'SRV',
'values': [{
'priority': 10,
'weight': 20,
'port': 30,
'target': 'foo-1.unit.tests.',
}, {
'priority': 12,
'weight': 30,
'port': 30,
'target': 'foo-2.unit.tests.',
}]
}))
expected.add(Record.new(zone, 'sub', {
'ttl': 39,
'type': 'NS',
'values': ['ns3.unit.tests.', 'ns4.unit.tests.'],
}))
expected.add(Record.new(zone, '', {
'ttl': 40,
'type': 'CAA',
'value': {
'flags': 0,
'tag': 'issue',
'value': 'ca.unit.tests',
},
}))
nsone_records = [{
'type': 'A',
'ttl': 32,
'short_answers': ['1.2.3.4'],
'domain': 'unit.tests.',
}, {
'type': 'A',
'ttl': 33,
'short_answers': ['1.2.3.4', '1.2.3.5'],
'domain': 'foo.unit.tests.',
}, {
'type': 'A',
'ttl': 34,
'short_answers': ['101.102.103.104', '101.102.103.105'],
'domain': 'geo.unit.tests',
}, {
'type': 'CNAME',
'ttl': 34,
'short_answers': ['foo.unit.tests.'],
'domain': 'cname.unit.tests.',
}, {
'type': 'MX',
'ttl': 35,
'short_answers': ['10 mx1.unit.tests.', '20 mx2.unit.tests.'],
'domain': 'unit.tests.',
}, {
'type': 'NAPTR',
'ttl': 36,
'short_answers': [
'10 100 S SIP+D2U !^.*$!sip:info@bar.example.com! .',
'100 100 U SIP+D2U !^.*$!sip:info@bar.example.com! .'
],
'domain': 'naptr.unit.tests.',
}, {
'type': 'NS',
'ttl': 37,
'short_answers': ['ns1.unit.tests.', 'ns2.unit.tests.'],
'domain': 'unit.tests.',
}, {
'type': 'SRV',
'ttl': 38,
'short_answers': ['12 30 30 foo-2.unit.tests.',
'10 20 30 foo-1.unit.tests.'],
'domain': '_srv._tcp.unit.tests.',
}, {
'type': 'NS',
'ttl': 39,
'short_answers': ['ns3.unit.tests.', 'ns4.unit.tests.'],
'domain': 'sub.unit.tests.',
}, {
'type': 'CAA',
'ttl': 40,
'short_answers': ['0 issue ca.unit.tests'],
'domain': 'unit.tests.',
}]
@patch('nsone.NSONE.loadZone')
def test_populate(self, load_mock):
provider = Ns1Provider('test', 'api-key')
# Bad auth
load_mock.side_effect = AuthException('unauthorized')
zone = Zone('unit.tests.', [])
with self.assertRaises(AuthException) as ctx:
provider.populate(zone)
self.assertEquals(load_mock.side_effect, ctx.exception)
# General error
load_mock.reset_mock()
load_mock.side_effect = ResourceException('boom')
zone = Zone('unit.tests.', [])
with self.assertRaises(ResourceException) as ctx:
provider.populate(zone)
self.assertEquals(load_mock.side_effect, ctx.exception)
self.assertEquals(('unit.tests',), load_mock.call_args[0])
# Non-existant zone doesn't populate anything
load_mock.reset_mock()
load_mock.side_effect = \
ResourceException('server error: zone not found')
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(set(), zone.records)
self.assertEquals(('unit.tests',), load_mock.call_args[0])
# Existing zone w/o records
load_mock.reset_mock()
nsone_zone = DummyZone([])
load_mock.side_effect = [nsone_zone]
zone_search = Mock()
zone_search.return_value = [
{
"domain": "geo.unit.tests",
"zone": "unit.tests",
"type": "A",
"answers": [
{'answer': ['1.1.1.1'], 'meta': {}},
{'answer': ['1.2.3.4'],
'meta': {'ca_province': ['ON']}},
{'answer': ['2.3.4.5'], 'meta': {'us_state': ['NY']}},
{'answer': ['3.4.5.6'], 'meta': {'country': ['US']}},
{'answer': ['4.5.6.7'],
'meta': {'iso_region_code': ['NA-US-WA']}},
],
'ttl': 34,
},
]
nsone_zone.search = zone_search
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(1, len(zone.records))
self.assertEquals(('unit.tests',), load_mock.call_args[0])
# Existing zone w/records
load_mock.reset_mock()
nsone_zone = DummyZone(self.nsone_records)
load_mock.side_effect = [nsone_zone]
zone_search = Mock()
zone_search.return_value = [
{
"domain": "geo.unit.tests",
"zone": "unit.tests",
"type": "A",
"answers": [
{'answer': ['1.1.1.1'], 'meta': {}},
{'answer': ['1.2.3.4'],
'meta': {'ca_province': ['ON']}},
{'answer': ['2.3.4.5'], 'meta': {'us_state': ['NY']}},
{'answer': ['3.4.5.6'], 'meta': {'country': ['US']}},
{'answer': ['4.5.6.7'],
'meta': {'iso_region_code': ['NA-US-WA']}},
],
'ttl': 34,
},
]
nsone_zone.search = zone_search
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(self.expected, zone.records)
self.assertEquals(('unit.tests',), load_mock.call_args[0])
@patch('nsone.NSONE.createZone')
@patch('nsone.NSONE.loadZone')
def test_sync(self, load_mock, create_mock):
provider = Ns1Provider('test', 'api-key')
desired = Zone('unit.tests.', [])
for r in self.expected:
desired.add_record(r)
plan = provider.plan(desired)
# everything except the root NS
expected_n = len(self.expected) - 1
self.assertEquals(expected_n, len(plan.changes))
# Fails, general error
load_mock.reset_mock()
create_mock.reset_mock()
load_mock.side_effect = ResourceException('boom')
with self.assertRaises(ResourceException) as ctx:
provider.apply(plan)
self.assertEquals(load_mock.side_effect, ctx.exception)
# Fails, bad auth
load_mock.reset_mock()
create_mock.reset_mock()
load_mock.side_effect = \
ResourceException('server error: zone not found')
create_mock.side_effect = AuthException('unauthorized')
with self.assertRaises(AuthException) as ctx:
provider.apply(plan)
self.assertEquals(create_mock.side_effect, ctx.exception)
# non-existant zone, create
load_mock.reset_mock()
create_mock.reset_mock()
load_mock.side_effect = \
ResourceException('server error: zone not found')
# ugh, need a mock zone with a mock prop since we're using getattr, we
# can actually control side effects on `meth` with that.
mock_zone = Mock()
mock_zone.add_SRV = Mock()
mock_zone.add_SRV.side_effect = [
RateLimitException('boo', period=0),
None,
]
create_mock.side_effect = [mock_zone]
got_n = provider.apply(plan)
self.assertEquals(expected_n, got_n)
# Update & delete
load_mock.reset_mock()
create_mock.reset_mock()
nsone_zone = DummyZone(self.nsone_records + [{
'type': 'A',
'ttl': 42,
'short_answers': ['9.9.9.9'],
'domain': 'delete-me.unit.tests.',
}])
nsone_zone.data['records'][0]['short_answers'][0] = '2.2.2.2'
nsone_zone.loadRecord = Mock()
zone_search = Mock()
zone_search.return_value = [
{
"domain": "geo.unit.tests",
"zone": "unit.tests",
"type": "A",
"answers": [
{'answer': ['1.1.1.1'], 'meta': {}},
{'answer': ['1.2.3.4'],
'meta': {'ca_province': ['ON']}},
{'answer': ['2.3.4.5'], 'meta': {'us_state': ['NY']}},
{'answer': ['3.4.5.6'], 'meta': {'country': ['US']}},
{'answer': ['4.5.6.7'],
'meta': {'iso_region_code': ['NA-US-WA']}},
],
'ttl': 34,
},
]
nsone_zone.search = zone_search
load_mock.side_effect = [nsone_zone, nsone_zone]
plan = provider.plan(desired)
self.assertEquals(3, len(plan.changes))
self.assertIsInstance(plan.changes[0], Update)
self.assertIsInstance(plan.changes[2], Delete)
# ugh, we need a mock record that can be returned from loadRecord for
# the update and delete targets, we can add our side effects to that to
# trigger rate limit handling
mock_record = Mock()
mock_record.update.side_effect = [
RateLimitException('one', period=0),
None,
None,
]
mock_record.delete.side_effect = [
RateLimitException('two', period=0),
None,
None,
]
nsone_zone.loadRecord.side_effect = [mock_record, mock_record,
mock_record]
got_n = provider.apply(plan)
self.assertEquals(3, got_n)
nsone_zone.loadRecord.assert_has_calls([
call('unit.tests', u'A'),
call('geo', u'A'),
call('delete-me', u'A'),
])
mock_record.assert_has_calls([
call.update(answers=[{'answer': [u'1.2.3.4'], 'meta': {}}],
filters=[],
ttl=32),
call.update(answers=[{u'answer': [u'1.2.3.4'], u'meta': {}}],
filters=[],
ttl=32),
call.update(
answers=[
{u'answer': [u'101.102.103.104'], u'meta': {}},
{u'answer': [u'101.102.103.105'], u'meta': {}},
{
u'answer': [u'201.202.203.204'],
u'meta': {
u'iso_region_code': [u'NA-US-NY']
},
},
],
filters=[
{u'filter': u'shuffle', u'config': {}},
{u'filter': u'geotarget_country', u'config': {}},
{u'filter': u'select_first_n', u'config': {u'N': 1}},
],
ttl=34),
call.delete(),
call.delete()
])
def test_escaping(self):
provider = Ns1Provider('test', 'api-key')
record = {
'ttl': 31,
'short_answers': ['foo; bar baz; blip']
}
self.assertEquals(['foo\; bar baz\; blip'],
provider._data_for_SPF('SPF', record)['values'])
record = {
'ttl': 31,
'short_answers': ['no', 'foo; bar baz; blip', 'yes']
}
self.assertEquals(['no', 'foo\; bar baz\; blip', 'yes'],
provider._data_for_TXT('TXT', record)['values'])
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'spf', {
'ttl': 34,
'type': 'SPF',
'value': 'foo\; bar baz\; blip'
})
self.assertEquals(['foo; bar baz; blip'],
provider._params_for_SPF(record)['answers'])
record = Record.new(zone, 'txt', {
'ttl': 35,
'type': 'TXT',
'value': 'foo\; bar baz\; blip'
})
self.assertEquals(['foo; bar baz; blip'],
provider._params_for_TXT(record)['answers'])
def test_data_for_CNAME(self):
provider = Ns1Provider('test', 'api-key')
# answers from nsone
a_record = {
'ttl': 31,
'type': 'CNAME',
'short_answers': ['foo.unit.tests.']
}
a_expected = {
'ttl': 31,
'type': 'CNAME',
'value': 'foo.unit.tests.'
}
self.assertEqual(a_expected,
provider._data_for_CNAME(a_record['type'], a_record))
# no answers from nsone
b_record = {
'ttl': 32,
'type': 'CNAME',
'short_answers': []
}
b_expected = {
'ttl': 32,
'type': 'CNAME',
'value': None
}
self.assertEqual(b_expected,
provider._data_for_CNAME(b_record['type'], b_record))