class TestFastdnsProvider(TestCase): expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) # Our test suite differs a bit, add our NS and remove the simple one expected.add_record( Record.new( expected, 'under', { 'ttl': 3600, 'type': 'NS', 'values': [ 'ns1.unit.tests.', 'ns2.unit.tests.', ] })) for record in list(expected.records): if record.name == 'sub' and record._type == 'NS': expected._remove_record(record) break def test_populate(self): provider = AkamaiProvider("test", "secret", "akam.com", "atok", "ctok") # Bad Auth with requests_mock() as mock: mock.get(ANY, status_code=401, text='{"message": "Unauthorized"}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(401, ctx.exception.response.status_code) # general error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existant zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=404, text='{"message": "Domain `foo.bar` not found"}') zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # No diffs == no changes with requests_mock() as mock: with open('tests/fixtures/fastdns-records.json') as fh: mock.get(ANY, text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(16, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(1, len(changes)) # 2nd populate makes no network calls/all from cache again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(16, len(again.records)) # bust the cache del provider._zone_records[zone.name] def test_apply(self): provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok", "cid", "gid") # tests create update delete through previous state config json with requests_mock() as mock: with open('tests/fixtures/fastdns-records-prev.json') as fh: mock.get(ANY, text=fh.read()) plan = provider.plan(self.expected) mock.post(ANY, status_code=201) mock.put(ANY, status_code=200) mock.delete(ANY, status_code=204) changes = provider.apply(plan) self.assertEquals(29, changes) # Test against a zone that doesn't exist yet with requests_mock() as mock: with open('tests/fixtures/fastdns-records-prev-other.json') as fh: mock.get(ANY, status_code=404) plan = provider.plan(self.expected) mock.post(ANY, status_code=201) mock.put(ANY, status_code=200) mock.delete(ANY, status_code=204) changes = provider.apply(plan) self.assertEquals(14, changes) # Test against a zone that doesn't exist yet, but gid not provided with requests_mock() as mock: with open('tests/fixtures/fastdns-records-prev-other.json') as fh: mock.get(ANY, status_code=404) provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok", "cid") plan = provider.plan(self.expected) mock.post(ANY, status_code=201) mock.put(ANY, status_code=200) mock.delete(ANY, status_code=204) changes = provider.apply(plan) self.assertEquals(14, changes) # Test against a zone that doesn't exist, but cid not provided with requests_mock() as mock: mock.get(ANY, status_code=404) provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok") plan = provider.plan(self.expected) mock.post(ANY, status_code=201) mock.put(ANY, status_code=200) mock.delete(ANY, status_code=204) try: changes = provider.apply(plan) except NameError as e: expected = "contractId not specified to create zone" self.assertEquals(text_type(e), expected)
def test_apply(self): provider = CloudflareProvider('test', 'email', 'token') provider._request = Mock() provider._request.side_effect = [ self.empty, # no zones { 'result': { 'id': 42, } }, # zone create ] + [None] * 20 # individual record creates # non-existent zone, create everything plan = provider.plan(self.expected) self.assertEquals(12, len(plan.changes)) self.assertEquals(12, provider.apply(plan)) self.assertFalse(plan.exists) provider._request.assert_has_calls( [ # created the domain call('POST', '/zones', data={ 'jump_start': False, 'name': 'unit.tests' }), # created at least one of the record with expected data call('POST', '/zones/42/dns_records', data={ 'content': 'ns1.unit.tests.', 'type': 'NS', 'name': 'under.unit.tests', 'ttl': 3600 }), # make sure semicolons are not escaped when sending data call('POST', '/zones/42/dns_records', data={ 'content': 'v=DKIM1;k=rsa;s=email;h=sha256;' 'p=A/kinda+of/long/string+with+numb3rs', 'type': 'TXT', 'name': 'txt.unit.tests', 'ttl': 600 }), ], True) # expected number of total calls self.assertEquals(22, provider._request.call_count) provider._request.reset_mock() provider.zone_records = Mock(return_value=[ { "id": "fc12ab34cd5611334422ab3322997653", "type": "A", "name": "www.unit.tests", "content": "1.2.3.4", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997654", "type": "A", "name": "www.unit.tests", "content": "2.2.3.4", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997655", "type": "A", "name": "nc.unit.tests", "content": "3.2.3.4", "proxiable": True, "proxied": False, "ttl": 120, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997655", "type": "A", "name": "ttl.unit.tests", "content": "4.2.3.4", "proxiable": True, "proxied": False, "ttl": 600, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, ]) # we don't care about the POST/create return values provider._request.return_value = {} provider._request.side_effect = None wanted = Zone('unit.tests.', []) wanted.add_record( Record.new( wanted, 'nc', { 'ttl': 60, # TTL is below their min 'type': 'A', 'value': '3.2.3.4' })) wanted.add_record( Record.new( wanted, 'ttl', { 'ttl': 300, # TTL change 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) # only see the delete & ttl update, below min-ttl is filtered out self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) self.assertTrue(plan.exists) # creates a the new value and then deletes all the old provider._request.assert_has_calls([ call('PUT', '/zones/42/dns_records/' 'fc12ab34cd5611334422ab3322997655', data={ 'content': '3.2.3.4', 'type': 'A', 'name': 'ttl.unit.tests', 'proxied': False, 'ttl': 300 }), call( 'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' 'dns_records/fc12ab34cd5611334422ab3322997653'), call( 'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' 'dns_records/fc12ab34cd5611334422ab3322997654') ])
def test_update_add_swap(self): provider = CloudflareProvider('test', 'email', 'token') provider.zone_records = Mock(return_value=[ { "id": "fc12ab34cd5611334422ab3322997653", "type": "A", "name": "a.unit.tests", "content": "1.1.1.1", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997654", "type": "A", "name": "a.unit.tests", "content": "2.2.2.2", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, ]) provider._request = Mock() provider._request.side_effect = [ self.empty, # no zones { 'result': { 'id': 42, } }, # zone create None, None, None, None, ] # Add something and delete something zone = Zone('unit.tests.', []) existing = Record.new( zone, 'a', { 'ttl': 300, 'type': 'A', # This matches the zone data above, one to swap, one to leave 'values': ['1.1.1.1', '2.2.2.2'], }) new = Record.new( zone, 'a', { 'ttl': 300, 'type': 'A', # This leaves one, swaps ones, and adds one 'values': ['2.2.2.2', '3.3.3.3', '4.4.4.4'], }) change = Update(existing, new) plan = Plan(zone, zone, [change], True) provider._apply(plan) # get the list of zones, create a zone, add some records, update # something, and delete something provider._request.assert_has_calls([ call('GET', '/zones', params={'page': 1}), call('POST', '/zones', data={ 'jump_start': False, 'name': 'unit.tests' }), call('POST', '/zones/42/dns_records', data={ 'content': '4.4.4.4', 'type': 'A', 'name': 'a.unit.tests', 'proxied': False, 'ttl': 300 }), call('PUT', '/zones/42/dns_records/' 'fc12ab34cd5611334422ab3322997654', data={ 'content': '2.2.2.2', 'type': 'A', 'name': 'a.unit.tests', 'proxied': False, 'ttl': 300 }), call('PUT', '/zones/42/dns_records/' 'fc12ab34cd5611334422ab3322997653', data={ 'content': '3.3.3.3', 'type': 'A', 'name': 'a.unit.tests', 'proxied': False, 'ttl': 300 }), ])
def test_sync(self, execute_mock): provider = DynProvider('test', 'cust', 'user', 'pass') # Test Zone create execute_mock.side_effect = [ # No such zone, during populate DynectGetError('foo'), # No such zone, during sync DynectGetError('foo'), # get empty Zone { 'data': {} }, # get zone we can modify & delete with { 'data': { # A top-level to delete 'a_records': [{ 'fqdn': 'unit.tests', 'rdata': { 'address': '1.2.3.4' }, 'record_id': 1, 'record_type': 'A', 'ttl': 30, 'zone': 'unit.tests', }, { 'fqdn': 'a.unit.tests', 'rdata': { 'address': '2.3.4.5' }, 'record_id': 2, 'record_type': 'A', 'ttl': 30, 'zone': 'unit.tests', }], # A node to delete 'cname_records': [{ 'fqdn': 'cname.unit.tests', 'rdata': { 'cname': 'unit.tests.' }, 'record_id': 3, 'record_type': 'CNAME', 'ttl': 30, 'zone': 'unit.tests', }], # A record to leave alone 'ptr_records': [{ 'fqdn': 'ptr.unit.tests', 'rdata': { 'ptrdname': 'xx.unit.tests.' }, 'record_id': 4, 'record_type': 'PTR', 'ttl': 30, 'zone': 'unit.tests', }], # A record to modify 'srv_records': [{ 'fqdn': '_srv._tcp.unit.tests', 'rdata': { 'port': 10, 'priority': 11, 'target': 'foo-1.unit.tests.', 'weight': 12 }, 'record_id': 5, 'record_type': 'SRV', 'ttl': 30, 'zone': 'unit.tests', }, { 'fqdn': '_srv._tcp.unit.tests', 'rdata': { 'port': 20, 'priority': 21, 'target': 'foo-2.unit.tests.', 'weight': 22 }, 'record_id': 6, 'record_type': 'SRV', 'ttl': 30, 'zone': 'unit.tests', }], } } ] # No existing records, create all with patch('dyn.tm.zones.Zone.add_record') as add_mock: with patch('dyn.tm.zones.Zone._update') as update_mock: plan = provider.plan(self.expected) update_mock.assert_not_called() provider.apply(plan) update_mock.assert_called() add_mock.assert_called() # Once for each dyn record (8 Records, 2 of which have dual values) self.assertEquals(14, len(add_mock.call_args_list)) execute_mock.assert_has_calls([ call('/Zone/unit.tests/', 'GET', {}), call('/Zone/unit.tests/', 'GET', {}) ]) self.assertEquals(9, len(plan.changes)) execute_mock.reset_mock() # Delete one and modify another new = Zone('unit.tests.', []) for name, data in (('a', { 'type': 'A', 'ttl': 30, 'value': '2.3.4.5' }), ('ptr', { 'type': 'PTR', 'ttl': 30, 'value': 'xx.unit.tests.' }), ('_srv._tcp', { 'type': 'SRV', 'ttl': 30, 'values': [{ 'priority': 31, 'weight': 12, 'port': 10, 'target': 'foo-1.unit.tests.' }, { 'priority': 21, 'weight': 22, 'port': 20, 'target': 'foo-2.unit.tests.' }] })): new.add_record(Record.new(new, name, data)) with patch('dyn.tm.zones.Zone.add_record') as add_mock: with patch('dyn.tm.records.DNSRecord.delete') as delete_mock: with patch('dyn.tm.zones.Zone._update') as update_mock: plan = provider.plan(new) provider.apply(plan) update_mock.assert_called() # we expect 4 deletes, 2 from actual deletes and 2 from # updates which delete and recreate self.assertEquals(4, len(delete_mock.call_args_list)) # the 2 (re)creates self.assertEquals(2, len(add_mock.call_args_list)) execute_mock.assert_has_calls([ call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}) ]) self.assertEquals(3, len(plan.changes))
def test_base_provider(self): with self.assertRaises(NotImplementedError) as ctx: BaseProvider('base') self.assertEquals('Abstract base class, log property missing', text_type(ctx.exception)) class HasLog(BaseProvider): log = getLogger('HasLog') with self.assertRaises(NotImplementedError) as ctx: HasLog('haslog') self.assertEquals('Abstract base class, SUPPORTS_GEO property missing', text_type(ctx.exception)) class HasSupportsGeo(HasLog): SUPPORTS_GEO = False zone = Zone('unit.tests.', ['sub']) with self.assertRaises(NotImplementedError) as ctx: HasSupportsGeo('hassupportsgeo').populate(zone) self.assertEquals('Abstract base class, SUPPORTS property missing', text_type(ctx.exception)) class HasSupports(HasSupportsGeo): SUPPORTS = set(('A', )) with self.assertRaises(NotImplementedError) as ctx: HasSupports('hassupports').populate(zone) self.assertEquals('Abstract base class, populate method missing', text_type(ctx.exception)) # SUPPORTS_DYNAMIC has a default/fallback self.assertFalse(HasSupports('hassupports').SUPPORTS_DYNAMIC) # But can be overridden class HasSupportsDyanmic(HasSupports): SUPPORTS_DYNAMIC = True self.assertTrue( HasSupportsDyanmic('hassupportsdynamic').SUPPORTS_DYNAMIC) class HasPopulate(HasSupports): def populate(self, zone, target=False, lenient=False): zone.add_record(Record.new(zone, '', { 'ttl': 60, 'type': 'A', 'value': '2.3.4.5' }), lenient=lenient) zone.add_record(Record.new(zone, 'going', { 'ttl': 60, 'type': 'A', 'value': '3.4.5.6' }), lenient=lenient) zone.add_record(Record.new(zone, 'foo.sub', { 'ttl': 61, 'type': 'A', 'value': '4.5.6.7' }), lenient=lenient) zone.add_record( Record.new(zone, '', { 'ttl': 60, 'type': 'A', 'value': '1.2.3.4' })) self.assertTrue( HasSupports('hassupportsgeo').supports(list(zone.records)[0])) plan = HasPopulate('haspopulate').plan(zone) self.assertEquals(3, len(plan.changes)) with self.assertRaises(NotImplementedError) as ctx: HasPopulate('haspopulate').apply(plan) self.assertEquals('Abstract base class, _apply method missing', text_type(ctx.exception))
class TestNs1Provider(TestCase): zone = Zone('unit.tests.', []) expected = set() expected.add( Record.new(zone, '', { 'ttl': 32, 'type': 'A', 'value': '1.2.3.4', })) expected.add( Record.new(zone, 'foo', { 'ttl': 33, 'type': 'A', 'values': ['1.2.3.4', '1.2.3.5'], })) 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:[email protected]!', 'replacement': '.', 'service': 'SIP+D2U', }, { 'flags': 'S', 'order': 10, 'preference': 100, 'regexp': '!^.*$!sip:[email protected]!', '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.'], })) 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': '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:[email protected]! .', '100 100 U SIP+D2U !^.*$!sip:[email protected]! .' ], '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.', }] @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 = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), 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 = 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.', []) desired.records.update(self.expected) 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() load_mock.side_effect = [nsone_zone, nsone_zone] plan = provider.plan(desired) self.assertEquals(2, len(plan.changes)) self.assertIsInstance(plan.changes[0], Update) self.assertIsInstance(plan.changes[1], 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, ] mock_record.delete.side_effect = [ RateLimitException('two', period=0), None, ] nsone_zone.loadRecord.side_effect = [mock_record, mock_record] got_n = provider.apply(plan) self.assertEquals(2, got_n) nsone_zone.loadRecord.assert_has_calls([ call('unit.tests', u'A'), call('delete-me', u'A'), ]) mock_record.assert_has_calls( [call.update(answers=[u'1.2.3.4'], ttl=32), 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'])
class TestDynProviderAlias(TestCase): expected = Zone('unit.tests.', []) for name, data in (('', { 'type': 'ALIAS', 'ttl': 300, 'value': 'www.unit.tests.' }), ('www', { 'type': 'A', 'ttl': 300, 'values': ['1.2.3.4'] })): expected.add_record(Record.new(expected, name, data)) def setUp(self): # Flush our zone to ensure we start fresh _CachingDynZone.flush_zone(self.expected.name[:-1]) @patch('dyn.core.SessionEngine.execute') def test_populate(self, execute_mock): provider = DynProvider('test', 'cust', 'user', 'pass') # Test Zone create execute_mock.side_effect = [ # get Zone { 'data': {} }, # get_all_records { 'data': { 'a_records': [{ 'fqdn': 'www.unit.tests', 'rdata': { 'address': '1.2.3.4' }, 'record_id': 1, 'record_type': 'A', 'ttl': 300, 'zone': 'unit.tests', }], 'alias_records': [{ 'fqdn': 'unit.tests', 'rdata': { 'alias': 'www.unit.tests.' }, 'record_id': 2, 'record_type': 'ALIAS', 'ttl': 300, 'zone': 'unit.tests', }], } } ] got = Zone('unit.tests.', []) provider.populate(got) execute_mock.assert_has_calls([ call('/Zone/unit.tests/', 'GET', {}), call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}) ]) changes = self.expected.changes(got, SimpleProvider()) self.assertEquals([], changes) @patch('dyn.core.SessionEngine.execute') def test_sync(self, execute_mock): provider = DynProvider('test', 'cust', 'user', 'pass') # Test Zone create execute_mock.side_effect = [ # No such zone, during populate DynectGetError('foo'), # No such zone, during sync DynectGetError('foo'), # get empty Zone { 'data': {} }, # get zone we can modify & delete with { 'data': { # A top-level to delete 'a_records': [{ 'fqdn': 'www.unit.tests', 'rdata': { 'address': '1.2.3.4' }, 'record_id': 1, 'record_type': 'A', 'ttl': 300, 'zone': 'unit.tests', }], # A node to delete 'alias_records': [{ 'fqdn': 'unit.tests', 'rdata': { 'alias': 'www.unit.tests.' }, 'record_id': 2, 'record_type': 'ALIAS', 'ttl': 300, 'zone': 'unit.tests', }], } } ] # No existing records, create all with patch('dyn.tm.zones.Zone.add_record') as add_mock: with patch('dyn.tm.zones.Zone._update') as update_mock: plan = provider.plan(self.expected) update_mock.assert_not_called() provider.apply(plan) update_mock.assert_called() add_mock.assert_called() # Once for each dyn record self.assertEquals(2, len(add_mock.call_args_list)) execute_mock.assert_has_calls([ call('/Zone/unit.tests/', 'GET', {}), call('/Zone/unit.tests/', 'GET', {}) ]) self.assertEquals(2, len(plan.changes))
class TestGandiProvider(TestCase): expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) # We remove this record from the test zone as Gandi API reject it # (rightfully). expected._remove_record( Record.new(expected, 'sub', { 'ttl': 1800, 'type': 'NS', 'values': ['6.2.3.4.', '7.2.3.4.'] })) def test_populate(self): provider = GandiProvider('test_id', 'token') # 400 - Bad Request. with requests_mock() as mock: mock.get(ANY, status_code=400, text='{"status": "error", "errors": [{"location": ' '"body", "name": "items", "description": ' '"\'6.2.3.4.\': invalid hostname (param: ' '{\'rrset_type\': u\'NS\', \'rrset_ttl\': 3600, ' '\'rrset_name\': u\'sub\', \'rrset_values\': ' '[u\'6.2.3.4.\', u\'7.2.3.4.\']})"}, {"location": ' '"body", "name": "items", "description": ' '"\'7.2.3.4.\': invalid hostname (param: ' '{\'rrset_type\': u\'NS\', \'rrset_ttl\': 3600, ' '\'rrset_name\': u\'sub\', \'rrset_values\': ' '[u\'6.2.3.4.\', u\'7.2.3.4.\']})"}]}') with self.assertRaises(GandiClientBadRequest) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertIn('"status": "error"', str(ctx.exception)) # 401 - Unauthorized. with requests_mock() as mock: mock.get(ANY, status_code=401, text='{"code":401,"message":"The server could not verify ' 'that you authorized to access the document you ' 'requested. Either you supplied the wrong ' 'credentials (e.g., bad api key), or your access ' 'token has expired","object":"HTTPUnauthorized",' '"cause":"Unauthorized"}') with self.assertRaises(GandiClientUnauthorized) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertIn('"cause":"Unauthorized"', str(ctx.exception)) # 403 - Forbidden. with requests_mock() as mock: mock.get(ANY, status_code=403, text='{"code":403,"message":"Access was denied to this ' 'resource.","object":"HTTPForbidden","cause":' '"Forbidden"}') with self.assertRaises(GandiClientForbidden) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertIn('"cause":"Forbidden"', str(ctx.exception)) # 404 - Not Found. with requests_mock() as mock: mock.get(ANY, status_code=404, text='{"code": 404, "message": "The resource could not ' 'be found.", "object": "HTTPNotFound", "cause": ' '"Not Found"}') with self.assertRaises(GandiClientNotFound) as ctx: zone = Zone('unit.tests.', []) provider._client.zone(zone) self.assertIn('"cause": "Not Found"', str(ctx.exception)) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # No diffs == no changes with requests_mock() as mock: base = 'https://api.gandi.net/v5/livedns/domains/unit.tests' \ '/records' with open('tests/fixtures/gandi-no-changes.json') as fh: mock.get(base, text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(16, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(0, len(changes)) del provider._zone_records[zone.name] # Default Gandi zone file. with requests_mock() as mock: base = 'https://api.gandi.net/v5/livedns/domains/unit.tests' \ '/records' with open('tests/fixtures/gandi-records.json') as fh: mock.get(base, text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(11, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(24, len(changes)) # 2nd populate makes no network calls/all from cache again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(11, len(again.records)) # bust the cache del provider._zone_records[zone.name] def test_apply(self): provider = GandiProvider('test_id', 'token') # Zone does not exists but can be created. with requests_mock() as mock: mock.get(ANY, status_code=404, text='{"code": 404, "message": "The resource could not ' 'be found.", "object": "HTTPNotFound", "cause": ' '"Not Found"}') mock.post(ANY, status_code=201, text='{"message": "Domain Created"}') plan = provider.plan(self.expected) provider.apply(plan) # Zone does not exists and can't be created. with requests_mock() as mock: mock.get(ANY, status_code=404, text='{"code": 404, "message": "The resource could not ' 'be found.", "object": "HTTPNotFound", "cause": ' '"Not Found"}') mock.post(ANY, status_code=404, text='{"code": 404, "message": "The resource could not ' 'be found.", "object": "HTTPNotFound", "cause": ' '"Not Found"}') with self.assertRaises( (GandiClientNotFound, GandiClientUnknownDomainName)) as ctx: plan = provider.plan(self.expected) provider.apply(plan) self.assertIn('This domain is not registered at Gandi.', str(ctx.exception)) resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) with open('tests/fixtures/gandi-zone.json') as fh: zone = fh.read() # non-existent domain resp.json.side_effect = [ GandiClientNotFound(resp), # no zone in populate GandiClientNotFound(resp), # no domain during apply zone ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no LOC n = len(self.expected.records) - 6 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) self.assertFalse(plan.exists) provider._client._request.assert_has_calls([ call('GET', '/livedns/domains/unit.tests/records'), call('GET', '/livedns/domains/unit.tests'), call('POST', '/livedns/domains', data={ 'fqdn': 'unit.tests', 'zone': {} }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'www.sub', 'rrset_ttl': 300, 'rrset_type': 'A', 'rrset_values': ['2.2.3.6'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'www', 'rrset_ttl': 300, 'rrset_type': 'A', 'rrset_values': ['2.2.3.6'] }), call( 'POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'txt', 'rrset_ttl': 600, 'rrset_type': 'TXT', 'rrset_values': [ 'Bah bah black sheep', 'have you any wool.', 'v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string' '+with+numb3rs' ] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'spf', 'rrset_ttl': 600, 'rrset_type': 'SPF', 'rrset_values': ['v=spf1 ip4:192.168.0.1/16-all'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'ptr', 'rrset_ttl': 300, 'rrset_type': 'PTR', 'rrset_values': ['foo.bar.com.'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'mx', 'rrset_ttl': 300, 'rrset_type': 'MX', 'rrset_values': [ '10 smtp-4.unit.tests.', '20 smtp-2.unit.tests.', '30 smtp-3.unit.tests.', '40 smtp-1.unit.tests.' ] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'excluded', 'rrset_ttl': 3600, 'rrset_type': 'CNAME', 'rrset_values': ['unit.tests.'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'dname', 'rrset_ttl': 300, 'rrset_type': 'DNAME', 'rrset_values': ['unit.tests.'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'cname', 'rrset_ttl': 300, 'rrset_type': 'CNAME', 'rrset_values': ['unit.tests.'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'aaaa', 'rrset_ttl': 600, 'rrset_type': 'AAAA', 'rrset_values': ['2601:644:500:e210:62f8:1dff:feb8:947a'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': '_srv._tcp', 'rrset_ttl': 600, 'rrset_type': 'SRV', 'rrset_values': [ '10 20 30 foo-1.unit.tests.', '12 20 30 foo-2.unit.tests.' ] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': '_pop3._tcp', 'rrset_ttl': 600, 'rrset_type': 'SRV', 'rrset_values': [ '0 0 0 .', ] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': '_imap._tcp', 'rrset_ttl': 600, 'rrset_type': 'SRV', 'rrset_values': [ '0 0 0 .', ] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': '@', 'rrset_ttl': 3600, 'rrset_type': 'SSHFP', 'rrset_values': [ '1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49', '1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73' ] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': '@', 'rrset_ttl': 3600, 'rrset_type': 'CAA', 'rrset_values': ['0 issue "ca.unit.tests"'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': '@', 'rrset_ttl': 300, 'rrset_type': 'A', 'rrset_values': ['1.2.3.4', '1.2.3.5'] }) ]) # expected number of total calls self.assertEquals(19, provider._client._request.call_count) provider._client._request.reset_mock() # delete 1 and update 1 provider._client.zone_records = Mock( return_value=[{ 'rrset_name': 'www', 'rrset_ttl': 300, 'rrset_type': 'A', 'rrset_values': ['1.2.3.4'] }, { 'rrset_name': 'www', 'rrset_ttl': 300, 'rrset_type': 'A', 'rrset_values': ['2.2.3.4'] }, { 'rrset_name': 'ttl', 'rrset_ttl': 600, 'rrset_type': 'A', 'rrset_values': ['3.2.3.4'] }]) # Domain exists, we don't care about return resp.json.side_effect = ['{}'] wanted = Zone('unit.tests.', []) wanted.add_record( Record.new(wanted, 'ttl', { 'ttl': 300, 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) self.assertTrue(plan.exists) self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._client._request.assert_has_calls([ call('DELETE', '/livedns/domains/unit.tests/records/www/A'), call('DELETE', '/livedns/domains/unit.tests/records/ttl/A'), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'ttl', 'rrset_ttl': 300, 'rrset_type': 'A', 'rrset_values': ['3.2.3.4'] }) ], any_order=True)
def test_SSHFP(self): # doesn't blow up Record.new( self.zone, '', { 'type': 'SSHFP', 'ttl': 600, 'value': { 'algorithm': 1, 'fingerprint_type': 1, 'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73' } }) # missing algorithm with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, '', { 'type': 'SSHFP', 'ttl': 600, 'value': { 'fingerprint_type': 1, 'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73' } }) self.assertEquals(['missing algorithm'], ctx.exception.reasons) # invalid algorithm with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, '', { 'type': 'SSHFP', 'ttl': 600, 'value': { 'algorithm': 'nope', 'fingerprint_type': 1, 'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73' } }) self.assertEquals(['invalid algorithm "nope"'], ctx.exception.reasons) # missing fingerprint_type with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, '', { 'type': 'SSHFP', 'ttl': 600, 'value': { 'algorithm': 1, 'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73' } }) self.assertEquals(['missing fingerprint_type'], ctx.exception.reasons) # invalid fingerprint_type with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, '', { 'type': 'SSHFP', 'ttl': 600, 'value': { 'algorithm': 1, 'fingerprint_type': 'yeeah', 'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73' } }) self.assertEquals(['invalid fingerprint_type "yeeah"'], ctx.exception.reasons) # missing fingerprint with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, '', { 'type': 'SSHFP', 'ttl': 600, 'value': { 'algorithm': 1, 'fingerprint_type': 1, } }) self.assertEquals(['missing fingerprint'], ctx.exception.reasons)
class TestCloudflareProvider(TestCase): expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) # Our test suite differs a bit, add our NS and remove the simple one expected.add_record( Record.new( expected, 'under', { 'ttl': 3600, 'type': 'NS', 'values': [ 'ns1.unit.tests.', 'ns2.unit.tests.', ] })) for record in list(expected.records): if record.name == 'sub' and record._type == 'NS': expected.records.remove(record) break empty = {'result': [], 'result_info': {'count': 0, 'per_page': 0}} def test_populate(self): provider = CloudflareProvider('test', 'email', 'token') # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=403, text='{"success":false,"errors":[{"code":9103,' '"message":"Unknown X-Auth-Key or X-Auth-Email"}],' '"messages":[],"result":null}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Unknown X-Auth-Key or X-Auth-Email', ctx.exception.message) # Bad auth, unknown resp with requests_mock() as mock: mock.get(ANY, status_code=403, text='{}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Authentication error', ctx.exception.message) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existant zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=200, json=self.empty) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # re-populating the same non-existant zone uses cache and makes no # calls again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(set(), again.records) # bust zone cache provider._zones = None # existing zone with data with requests_mock() as mock: base = 'https://api.cloudflare.com/client/v4/zones' # zones with open('tests/fixtures/cloudflare-zones-page-1.json') as fh: mock.get('{}?page=1'.format(base), status_code=200, text=fh.read()) with open('tests/fixtures/cloudflare-zones-page-2.json') as fh: mock.get('{}?page=2'.format(base), status_code=200, text=fh.read()) mock.get('{}?page=3'.format(base), status_code=200, json={ 'result': [], 'result_info': { 'count': 0, 'per_page': 0 } }) # records base = '{}/234234243423aaabb334342aaa343435/dns_records' \ .format(base) with open('tests/fixtures/cloudflare-dns_records-' 'page-1.json') as fh: mock.get('{}?page=1'.format(base), status_code=200, text=fh.read()) with open('tests/fixtures/cloudflare-dns_records-' 'page-2.json') as fh: mock.get('{}?page=2'.format(base), status_code=200, text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(9, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(0, len(changes)) # re-populating the same zone/records comes out of cache, no calls again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(9, len(again.records)) def test_apply(self): provider = CloudflareProvider('test', 'email', 'token') provider._request = Mock() provider._request.side_effect = [ self.empty, # no zones { 'result': { 'id': 42, } }, # zone create ] + [None] * 15 # individual record creates # non-existant zone, create everything plan = provider.plan(self.expected) self.assertEquals(9, len(plan.changes)) self.assertEquals(9, provider.apply(plan)) provider._request.assert_has_calls([ # created the domain call('POST', '/zones', data={ 'jump_start': False, 'name': 'unit.tests' }), # created at least one of the record with expected data call('POST', '/zones/42/dns_records', data={ 'content': 'ns1.unit.tests.', 'type': 'NS', 'name': 'under.unit.tests', 'ttl': 3600 }), ]) # expected number of total calls self.assertEquals(17, provider._request.call_count) provider._request.reset_mock() provider.zone_records = Mock(return_value=[ { "id": "fc12ab34cd5611334422ab3322997653", "type": "A", "name": "www.unit.tests", "content": "1.2.3.4", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997654", "type": "A", "name": "www.unit.tests", "content": "2.2.3.4", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997655", "type": "A", "name": "nc.unit.tests", "content": "3.2.3.4", "proxiable": True, "proxied": False, "ttl": 120, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997655", "type": "A", "name": "ttl.unit.tests", "content": "4.2.3.4", "proxiable": True, "proxied": False, "ttl": 600, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, ]) # we don't care about the POST/create return values provider._request.return_value = {} provider._request.side_effect = None wanted = Zone('unit.tests.', []) wanted.add_record( Record.new( wanted, 'nc', { 'ttl': 60, # TTL is below their min 'type': 'A', 'value': '3.2.3.4' })) wanted.add_record( Record.new( wanted, 'ttl', { 'ttl': 300, # TTL change 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) # only see the delete & ttl update, below min-ttl is filtered out self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._request.assert_has_calls([ call('POST', '/zones/42/dns_records', data={ 'content': '3.2.3.4', 'type': 'A', 'name': 'ttl.unit.tests', 'ttl': 300 }), call( 'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' 'dns_records/fc12ab34cd5611334422ab3322997655'), call( 'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' 'dns_records/fc12ab34cd5611334422ab3322997653'), call( 'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' 'dns_records/fc12ab34cd5611334422ab3322997654') ])
def test_apply(self): provider = GandiProvider('test_id', 'token') # Zone does not exists but can be created. with requests_mock() as mock: mock.get(ANY, status_code=404, text='{"code": 404, "message": "The resource could not ' 'be found.", "object": "HTTPNotFound", "cause": ' '"Not Found"}') mock.post(ANY, status_code=201, text='{"message": "Domain Created"}') plan = provider.plan(self.expected) provider.apply(plan) # Zone does not exists and can't be created. with requests_mock() as mock: mock.get(ANY, status_code=404, text='{"code": 404, "message": "The resource could not ' 'be found.", "object": "HTTPNotFound", "cause": ' '"Not Found"}') mock.post(ANY, status_code=404, text='{"code": 404, "message": "The resource could not ' 'be found.", "object": "HTTPNotFound", "cause": ' '"Not Found"}') with self.assertRaises( (GandiClientNotFound, GandiClientUnknownDomainName)) as ctx: plan = provider.plan(self.expected) provider.apply(plan) self.assertIn('This domain is not registered at Gandi.', str(ctx.exception)) resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) with open('tests/fixtures/gandi-zone.json') as fh: zone = fh.read() # non-existent domain resp.json.side_effect = [ GandiClientNotFound(resp), # no zone in populate GandiClientNotFound(resp), # no domain during apply zone ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no LOC n = len(self.expected.records) - 6 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) self.assertFalse(plan.exists) provider._client._request.assert_has_calls([ call('GET', '/livedns/domains/unit.tests/records'), call('GET', '/livedns/domains/unit.tests'), call('POST', '/livedns/domains', data={ 'fqdn': 'unit.tests', 'zone': {} }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'www.sub', 'rrset_ttl': 300, 'rrset_type': 'A', 'rrset_values': ['2.2.3.6'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'www', 'rrset_ttl': 300, 'rrset_type': 'A', 'rrset_values': ['2.2.3.6'] }), call( 'POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'txt', 'rrset_ttl': 600, 'rrset_type': 'TXT', 'rrset_values': [ 'Bah bah black sheep', 'have you any wool.', 'v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string' '+with+numb3rs' ] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'spf', 'rrset_ttl': 600, 'rrset_type': 'SPF', 'rrset_values': ['v=spf1 ip4:192.168.0.1/16-all'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'ptr', 'rrset_ttl': 300, 'rrset_type': 'PTR', 'rrset_values': ['foo.bar.com.'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'mx', 'rrset_ttl': 300, 'rrset_type': 'MX', 'rrset_values': [ '10 smtp-4.unit.tests.', '20 smtp-2.unit.tests.', '30 smtp-3.unit.tests.', '40 smtp-1.unit.tests.' ] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'excluded', 'rrset_ttl': 3600, 'rrset_type': 'CNAME', 'rrset_values': ['unit.tests.'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'dname', 'rrset_ttl': 300, 'rrset_type': 'DNAME', 'rrset_values': ['unit.tests.'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'cname', 'rrset_ttl': 300, 'rrset_type': 'CNAME', 'rrset_values': ['unit.tests.'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'aaaa', 'rrset_ttl': 600, 'rrset_type': 'AAAA', 'rrset_values': ['2601:644:500:e210:62f8:1dff:feb8:947a'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': '_srv._tcp', 'rrset_ttl': 600, 'rrset_type': 'SRV', 'rrset_values': [ '10 20 30 foo-1.unit.tests.', '12 20 30 foo-2.unit.tests.' ] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': '_pop3._tcp', 'rrset_ttl': 600, 'rrset_type': 'SRV', 'rrset_values': [ '0 0 0 .', ] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': '_imap._tcp', 'rrset_ttl': 600, 'rrset_type': 'SRV', 'rrset_values': [ '0 0 0 .', ] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': '@', 'rrset_ttl': 3600, 'rrset_type': 'SSHFP', 'rrset_values': [ '1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49', '1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73' ] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': '@', 'rrset_ttl': 3600, 'rrset_type': 'CAA', 'rrset_values': ['0 issue "ca.unit.tests"'] }), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': '@', 'rrset_ttl': 300, 'rrset_type': 'A', 'rrset_values': ['1.2.3.4', '1.2.3.5'] }) ]) # expected number of total calls self.assertEquals(19, provider._client._request.call_count) provider._client._request.reset_mock() # delete 1 and update 1 provider._client.zone_records = Mock( return_value=[{ 'rrset_name': 'www', 'rrset_ttl': 300, 'rrset_type': 'A', 'rrset_values': ['1.2.3.4'] }, { 'rrset_name': 'www', 'rrset_ttl': 300, 'rrset_type': 'A', 'rrset_values': ['2.2.3.4'] }, { 'rrset_name': 'ttl', 'rrset_ttl': 600, 'rrset_type': 'A', 'rrset_values': ['3.2.3.4'] }]) # Domain exists, we don't care about return resp.json.side_effect = ['{}'] wanted = Zone('unit.tests.', []) wanted.add_record( Record.new(wanted, 'ttl', { 'ttl': 300, 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) self.assertTrue(plan.exists) self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._client._request.assert_has_calls([ call('DELETE', '/livedns/domains/unit.tests/records/www/A'), call('DELETE', '/livedns/domains/unit.tests/records/ttl/A'), call('POST', '/livedns/domains/unit.tests/records', data={ 'rrset_name': 'ttl', 'rrset_ttl': 300, 'rrset_type': 'A', 'rrset_values': ['3.2.3.4'] }) ], any_order=True)
def test_apply(self): provider = MythicBeastsProvider('test', {'unit.tests.': 'mypassword'}) zone = Zone('unit.tests.', []) # Create blank zone with requests_mock() as mock: mock.post(ANY, status_code=200, text='') provider.populate(zone) self.assertEquals(0, len(zone.records)) # Record change failed with requests_mock() as mock: mock.post(ANY, status_code=200, text='') provider.populate(zone) zone.add_record( Record.new(zone, 'prawf', { 'ttl': 300, 'type': 'TXT', 'value': 'prawf', })) plan = provider.plan(zone) with requests_mock() as mock: mock.post(ANY, status_code=400, text='NADD 300 TXT prawf') with self.assertRaises(Exception) as err: provider.apply(plan) self.assertEquals( 'Mythic Beasts could not action command: unit.tests ' 'ADD prawf.unit.tests 300 TXT prawf', err.exception.message) # Check deleting and adding/changing test record existing = 'prawf 300 TXT prawf prawf prawf\ndileu 300 TXT dileu' with requests_mock() as mock: mock.post(ANY, status_code=200, text=existing) # Mash up a new zone with records so a plan # is generated with changes and applied. For some reason # passing self.expected, or just changing each record's zone # doesn't work. Nor does this without a single add_record after wanted = Zone('unit.tests.', []) for record in list(self.expected.records): data = {'type': record._type} data.update(record.data) wanted.add_record(Record.new(wanted, record.name, data)) wanted.add_record( Record.new(wanted, 'prawf', { 'ttl': 60, 'type': 'TXT', 'value': 'prawf yw e', })) plan = provider.plan(wanted) # Octo ignores NS records (15-1) self.assertEquals( 1, len([c for c in plan.changes if isinstance(c, Update)])) self.assertEquals( 1, len([c for c in plan.changes if isinstance(c, Delete)])) self.assertEquals( 14, len([c for c in plan.changes if isinstance(c, Create)])) self.assertEquals(16, provider.apply(plan)) self.assertTrue(plan.exists)
def test_command_generation(self): zone = Zone('unit.tests.', []) zone.add_record( Record.new(zone, 'prawf-alias', { 'ttl': 60, 'type': 'ALIAS', 'value': 'alias.unit.tests.', })) zone.add_record( Record.new( zone, 'prawf-ns', { 'ttl': 300, 'type': 'NS', 'values': [ 'alias.unit.tests.', 'alias2.unit.tests.', ], })) zone.add_record( Record.new(zone, 'prawf-a', { 'ttl': 60, 'type': 'A', 'values': [ '1.2.3.4', '5.6.7.8', ], })) zone.add_record( Record.new( zone, 'prawf-aaaa', { 'ttl': 60, 'type': 'AAAA', 'values': [ 'a:a::a', 'b:b::b', 'c:c::c:c', ], })) zone.add_record( Record.new(zone, 'prawf-txt', { 'ttl': 60, 'type': 'TXT', 'value': 'prawf prawf dyma prawf', })) zone.add_record( Record.new(zone, 'prawf-txt2', { 'ttl': 60, 'type': 'TXT', 'value': 'v=DKIM1\\; k=rsa\\; p=prawf', })) with requests_mock() as mock: mock.post(ANY, status_code=200, text='') provider = MythicBeastsProvider('test', {'unit.tests.': 'mypassword'}) plan = provider.plan(zone) changes = plan.changes generated_commands = [] for change in changes: generated_commands.extend( provider._compile_commands('ADD', change.new)) expected_commands = [ 'ADD prawf-alias.unit.tests 60 ANAME alias.unit.tests.', 'ADD prawf-ns.unit.tests 300 NS alias.unit.tests.', 'ADD prawf-ns.unit.tests 300 NS alias2.unit.tests.', 'ADD prawf-a.unit.tests 60 A 1.2.3.4', 'ADD prawf-a.unit.tests 60 A 5.6.7.8', 'ADD prawf-aaaa.unit.tests 60 AAAA a:a::a', 'ADD prawf-aaaa.unit.tests 60 AAAA b:b::b', 'ADD prawf-aaaa.unit.tests 60 AAAA c:c::c:c', 'ADD prawf-txt.unit.tests 60 TXT prawf prawf dyma prawf', 'ADD prawf-txt2.unit.tests 60 TXT v=DKIM1; k=rsa; p=prawf', ] generated_commands.sort() expected_commands.sort() self.assertEquals(generated_commands, expected_commands) # Now test deletion existing = 'prawf-txt 300 TXT prawf prawf dyma prawf\n' \ 'prawf-txt2 300 TXT v=DKIM1; k=rsa; p=prawf\n' \ 'prawf-a 60 A 1.2.3.4' with requests_mock() as mock: mock.post(ANY, status_code=200, text=existing) wanted = Zone('unit.tests.', []) plan = provider.plan(wanted) changes = plan.changes generated_commands = [] for change in changes: generated_commands.extend( provider._compile_commands('DELETE', change.existing)) expected_commands = [ 'DELETE prawf-a.unit.tests 60 A 1.2.3.4', 'DELETE prawf-txt.unit.tests 300 TXT prawf prawf dyma prawf', 'DELETE prawf-txt2.unit.tests 300 TXT v=DKIM1; k=rsa; p=prawf', ] generated_commands.sort() expected_commands.sort() self.assertEquals(generated_commands, expected_commands)
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:[email protected]!', 'replacement': '.', 'service': 'SIP+D2U', }, { 'flags': 'S', 'order': 10, 'preference': 100, 'regexp': '!^.*$!sip:[email protected]!', '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', }, })) ns1_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:[email protected]! .', '100 100 U SIP+D2U !^.*$!sip:[email protected]! .' ], '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('ns1.rest.zones.Zones.retrieve') def test_populate(self, zone_retrieve_mock): provider = Ns1Provider('test', 'api-key') # Bad auth zone_retrieve_mock.side_effect = AuthException('unauthorized') zone = Zone('unit.tests.', []) with self.assertRaises(AuthException) as ctx: provider.populate(zone) self.assertEquals(zone_retrieve_mock.side_effect, ctx.exception) # General error zone_retrieve_mock.reset_mock() zone_retrieve_mock.side_effect = ResourceException('boom') zone = Zone('unit.tests.', []) with self.assertRaises(ResourceException) as ctx: provider.populate(zone) self.assertEquals(zone_retrieve_mock.side_effect, ctx.exception) self.assertEquals(('unit.tests', ), zone_retrieve_mock.call_args[0]) # Non-existent zone doesn't populate anything zone_retrieve_mock.reset_mock() zone_retrieve_mock.side_effect = \ ResourceException('server error: zone not found') zone = Zone('unit.tests.', []) exists = provider.populate(zone) self.assertEquals(set(), zone.records) self.assertEquals(('unit.tests', ), zone_retrieve_mock.call_args[0]) self.assertFalse(exists) # Existing zone w/o records zone_retrieve_mock.reset_mock() ns1_zone = { 'records': [{ "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, }], } zone_retrieve_mock.side_effect = [ns1_zone] zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(1, len(zone.records)) self.assertEquals(('unit.tests', ), zone_retrieve_mock.call_args[0]) # Existing zone w/records zone_retrieve_mock.reset_mock() ns1_zone = { 'records': self.ns1_records + [{ "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, }], } zone_retrieve_mock.side_effect = [ns1_zone] zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(self.expected, zone.records) self.assertEquals(('unit.tests', ), zone_retrieve_mock.call_args[0]) # Test skipping unsupported record type zone_retrieve_mock.reset_mock() ns1_zone = { 'records': self.ns1_records + [{ 'type': 'UNSUPPORTED', 'ttl': 42, 'short_answers': ['unsupported'], 'domain': 'unsupported.unit.tests.', }, { "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, }], } zone_retrieve_mock.side_effect = [ns1_zone] zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(self.expected, zone.records) self.assertEquals(('unit.tests', ), zone_retrieve_mock.call_args[0]) @patch('ns1.rest.records.Records.delete') @patch('ns1.rest.records.Records.update') @patch('ns1.rest.records.Records.create') @patch('ns1.rest.records.Records.retrieve') @patch('ns1.rest.zones.Zones.create') @patch('ns1.rest.zones.Zones.retrieve') def test_sync(self, zone_retrieve_mock, zone_create_mock, record_retrieve_mock, record_create_mock, record_update_mock, record_delete_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)) self.assertTrue(plan.exists) # Fails, general error zone_retrieve_mock.reset_mock() zone_create_mock.reset_mock() zone_retrieve_mock.side_effect = ResourceException('boom') with self.assertRaises(ResourceException) as ctx: provider.apply(plan) self.assertEquals(zone_retrieve_mock.side_effect, ctx.exception) # Fails, bad auth zone_retrieve_mock.reset_mock() zone_create_mock.reset_mock() zone_retrieve_mock.side_effect = \ ResourceException('server error: zone not found') zone_create_mock.side_effect = AuthException('unauthorized') with self.assertRaises(AuthException) as ctx: provider.apply(plan) self.assertEquals(zone_create_mock.side_effect, ctx.exception) # non-existent zone, create zone_retrieve_mock.reset_mock() zone_create_mock.reset_mock() zone_retrieve_mock.side_effect = \ ResourceException('server error: zone not found') zone_create_mock.side_effect = ['foo'] # Test out the create rate-limit handling, then 9 successes record_create_mock.side_effect = [ RateLimitException('boo', period=0), ] + ([None] * 9) got_n = provider.apply(plan) self.assertEquals(expected_n, got_n) # Zone was created zone_create_mock.assert_has_calls([call('unit.tests')]) # Checking that we got some of the expected records too record_create_mock.assert_has_calls([ call('unit.tests', 'unit.tests', 'A', answers=[{ 'answer': ['1.2.3.4'], 'meta': {} }], filters=[], ttl=32), call('unit.tests', 'unit.tests', 'CAA', answers=[(0, 'issue', 'ca.unit.tests')], ttl=40), call('unit.tests', 'unit.tests', 'MX', answers=[(10, 'mx1.unit.tests.'), (20, 'mx2.unit.tests.')], ttl=35), ]) # Update & delete zone_retrieve_mock.reset_mock() zone_create_mock.reset_mock() ns1_zone = { 'records': self.ns1_records + [ { 'type': 'A', 'ttl': 42, 'short_answers': ['9.9.9.9'], 'domain': 'delete-me.unit.tests.', }, { "domain": "geo.unit.tests", "zone": "unit.tests", "type": "A", "short_answers": [ '1.1.1.1', '1.2.3.4', '2.3.4.5', '3.4.5.6', '4.5.6.7', ], 'tier': 3, # This flags it as advacned, full load required 'ttl': 34, } ], } ns1_zone['records'][0]['short_answers'][0] = '2.2.2.2' record_retrieve_mock.side_effect = [{ "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'] } }, ], 'tier': 3, 'ttl': 34, }] zone_retrieve_mock.side_effect = [ns1_zone, ns1_zone] plan = provider.plan(desired) self.assertEquals(3, len(plan.changes)) # Shouldn't rely on order so just count classes classes = defaultdict(lambda: 0) for change in plan.changes: classes[change.__class__] += 1 self.assertEquals(1, classes[Delete]) self.assertEquals(2, classes[Update]) record_update_mock.side_effect = [ RateLimitException('one', period=0), None, None, ] record_delete_mock.side_effect = [ RateLimitException('two', period=0), None, None, ] got_n = provider.apply(plan) self.assertEquals(3, got_n) record_update_mock.assert_has_calls([ call('unit.tests', 'unit.tests', 'A', answers=[{ 'answer': ['1.2.3.4'], 'meta': {} }], filters=[], ttl=32), call('unit.tests', 'unit.tests', 'A', answers=[{ 'answer': ['1.2.3.4'], 'meta': {} }], filters=[], ttl=32), call('unit.tests', 'geo.unit.tests', 'A', answers=[{ 'answer': ['101.102.103.104'], 'meta': {} }, { 'answer': ['101.102.103.105'], 'meta': {} }, { 'answer': ['201.202.203.204'], 'meta': { 'iso_region_code': ['NA-US-NY'] } }], filters=[{ 'filter': 'shuffle', 'config': {} }, { 'filter': 'geotarget_country', 'config': {} }, { 'filter': 'select_first_n', 'config': { 'N': 1 } }], ttl=34) ]) 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 ns1 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 ns1 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))
class TestDigitalOceanProvider(TestCase): expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) # Our test suite differs a bit, add our NS and remove the simple one expected.add_record( Record.new( expected, 'under', { 'ttl': 3600, 'type': 'NS', 'values': [ 'ns1.unit.tests.', 'ns2.unit.tests.', ] })) for record in list(expected.records): if record.name == 'sub' and record._type == 'NS': expected._remove_record(record) break def test_populate(self): provider = DigitalOceanProvider('test', 'token') # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=401, text='{"id":"unauthorized",' '"message":"Unable to authenticate you."}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Unauthorized', text_type(ctx.exception)) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existent zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=404, text='{"id":"not_found","message":"The resource you ' 'were accessing could not be found."}') zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # No diffs == no changes with requests_mock() as mock: base = 'https://api.digitalocean.com/v2/domains/unit.tests/' \ 'records?page=' with open('tests/fixtures/digitalocean-page-1.json') as fh: mock.get('{}{}'.format(base, 1), text=fh.read()) with open('tests/fixtures/digitalocean-page-2.json') as fh: mock.get('{}{}'.format(base, 2), text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(12, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(0, len(changes)) # 2nd populate makes no network calls/all from cache again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(12, len(again.records)) # bust the cache del provider._zone_records[zone.name] def test_apply(self): provider = DigitalOceanProvider('test', 'token') resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) domain_after_creation = { "domain_records": [{ "id": 11189874, "type": "NS", "name": "@", "data": "ns1.digitalocean.com", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }, { "id": 11189875, "type": "NS", "name": "@", "data": "ns2.digitalocean.com", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }, { "id": 11189876, "type": "NS", "name": "@", "data": "ns3.digitalocean.com", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }, { "id": 11189877, "type": "A", "name": "@", "data": "192.0.2.1", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }], "links": {}, "meta": { "total": 4 } } # non-existent domain, create everything resp.json.side_effect = [ DigitalOceanClientNotFound, # no zone in populate DigitalOceanClientNotFound, # no domain during apply domain_after_creation ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no unsupported n = len(self.expected.records) - 8 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) self.assertFalse(plan.exists) provider._client._request.assert_has_calls([ # created the domain call('POST', '/domains', data={ 'ip_address': '192.0.2.1', 'name': 'unit.tests' }), # get all records in newly created zone call('GET', '/domains/unit.tests/records', {'page': 1}), # delete the initial A record call('DELETE', '/domains/unit.tests/records/11189877'), # created at least some of the record with expected data call('POST', '/domains/unit.tests/records', data={ 'data': '1.2.3.4', 'name': '@', 'ttl': 300, 'type': 'A' }), call('POST', '/domains/unit.tests/records', data={ 'data': '1.2.3.5', 'name': '@', 'ttl': 300, 'type': 'A' }), call('POST', '/domains/unit.tests/records', data={ 'data': 'ca.unit.tests.', 'flags': 0, 'name': '@', 'tag': 'issue', 'ttl': 3600, 'type': 'CAA' }), call('POST', '/domains/unit.tests/records', data={ 'name': '_srv._tcp', 'weight': 20, 'data': 'foo-1.unit.tests.', 'priority': 10, 'ttl': 600, 'type': 'SRV', 'port': 30 }), ]) self.assertEquals(24, provider._client._request.call_count) provider._client._request.reset_mock() # delete 1 and update 1 provider._client.records = Mock(return_value=[{ 'id': 11189897, 'name': 'www', 'data': '1.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189898, 'name': 'www', 'data': '2.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189899, 'name': 'ttl', 'data': '3.2.3.4', 'ttl': 600, 'type': 'A', }]) # Domain exists, we don't care about return resp.json.side_effect = ['{}'] wanted = Zone('unit.tests.', []) wanted.add_record( Record.new(wanted, 'ttl', { 'ttl': 300, 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) self.assertTrue(plan.exists) self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) # recreate for update, and delete for the 2 parts of the other provider._client._request.assert_has_calls([ call('POST', '/domains/unit.tests/records', data={ 'data': '3.2.3.4', 'type': 'A', 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/domains/unit.tests/records/11189899'), call('DELETE', '/domains/unit.tests/records/11189897'), call('DELETE', '/domains/unit.tests/records/11189898') ], any_order=True)
def test_SRV(self): # doesn't blow up Record.new( self.zone, '_srv._tcp', { 'type': 'SRV', 'ttl': 600, 'value': { 'priority': 1, 'weight': 2, 'port': 3, 'target': 'foo.bar.baz.' } }) # invalid name with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'neup', { 'type': 'SRV', 'ttl': 600, 'value': { 'priority': 1, 'weight': 2, 'port': 3, 'target': 'foo.bar.baz.' } }) self.assertEquals(['invalid name'], ctx.exception.reasons) # missing priority with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, '_srv._tcp', { 'type': 'SRV', 'ttl': 600, 'value': { 'weight': 2, 'port': 3, 'target': 'foo.bar.baz.' } }) self.assertEquals(['missing priority'], ctx.exception.reasons) # invalid priority with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, '_srv._tcp', { 'type': 'SRV', 'ttl': 600, 'value': { 'priority': 'foo', 'weight': 2, 'port': 3, 'target': 'foo.bar.baz.' } }) self.assertEquals(['invalid priority "foo"'], ctx.exception.reasons) # missing weight with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, '_srv._tcp', { 'type': 'SRV', 'ttl': 600, 'value': { 'priority': 1, 'port': 3, 'target': 'foo.bar.baz.' } }) self.assertEquals(['missing weight'], ctx.exception.reasons) # invalid weight with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, '_srv._tcp', { 'type': 'SRV', 'ttl': 600, 'value': { 'priority': 1, 'weight': 'foo', 'port': 3, 'target': 'foo.bar.baz.' } }) self.assertEquals(['invalid weight "foo"'], ctx.exception.reasons) # missing port with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, '_srv._tcp', { 'type': 'SRV', 'ttl': 600, 'value': { 'priority': 1, 'weight': 2, 'target': 'foo.bar.baz.' } }) self.assertEquals(['missing port'], ctx.exception.reasons) # invalid port with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, '_srv._tcp', { 'type': 'SRV', 'ttl': 600, 'value': { 'priority': 1, 'weight': 2, 'port': 'foo', 'target': 'foo.bar.baz.' } }) self.assertEquals(['invalid port "foo"'], ctx.exception.reasons) # missing target with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, '_srv._tcp', { 'type': 'SRV', 'ttl': 600, 'value': { 'priority': 1, 'weight': 2, 'port': 3, } }) self.assertEquals(['missing target'], ctx.exception.reasons) # invalid target with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, '_srv._tcp', { 'type': 'SRV', 'ttl': 600, 'value': { 'priority': 1, 'weight': 2, 'port': 3, 'target': 'foo.bar.baz' } }) self.assertEquals(['missing trailing .'], ctx.exception.reasons)
def test_existing_nameservers(self): ns_values = ['8.8.8.8.', '9.9.9.9.'] provider = PowerDnsProvider('test', 'non.existent', 'api-key', nameserver_values=ns_values) expected = Zone('unit.tests.', []) ns_record = Record.new(expected, '', { 'type': 'NS', 'ttl': 600, 'values': ns_values }) expected.add_record(ns_record) # no changes with requests_mock() as mock: data = { 'rrsets': [{ 'comments': [], 'name': 'unit.tests.', 'records': [{ 'content': '8.8.8.8.', 'disabled': False }, { 'content': '9.9.9.9.', 'disabled': False }], 'ttl': 600, 'type': 'NS' }, { 'comments': [], 'name': 'unit.tests.', 'records': [{ 'content': '1.2.3.4', 'disabled': False, }], 'ttl': 60, 'type': 'A' }] } mock.get(ANY, status_code=200, json=data) unrelated_record = Record.new(expected, '', { 'type': 'A', 'ttl': 60, 'value': '1.2.3.4' }) expected.add_record(unrelated_record) plan = provider.plan(expected) self.assertFalse(plan) # remove it now that we don't need the unrelated change any longer expected._remove_record(unrelated_record) # ttl diff with requests_mock() as mock: data = { 'rrsets': [{ 'comments': [], 'name': 'unit.tests.', 'records': [ { 'content': '8.8.8.8.', 'disabled': False }, { 'content': '9.9.9.9.', 'disabled': False }, ], 'ttl': 3600, 'type': 'NS' }] } mock.get(ANY, status_code=200, json=data) plan = provider.plan(expected) self.assertEquals(1, len(plan.changes)) # create with requests_mock() as mock: data = {'rrsets': []} mock.get(ANY, status_code=200, json=data) plan = provider.plan(expected) self.assertEquals(1, len(plan.changes))
def test_A_and_values_mixin(self): # doesn't blow up Record.new(self.zone, '', { 'type': 'A', 'ttl': 600, 'value': '1.2.3.4', }) Record.new(self.zone, '', { 'type': 'A', 'ttl': 600, 'values': [ '1.2.3.4', '1.2.3.5', ] }) # missing value(s) with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, '', { 'type': 'A', 'ttl': 600, }) self.assertEquals(['missing value(s)'], ctx.exception.reasons) # missing value(s) & ttl with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, '', { 'type': 'A', }) self.assertEquals(['missing ttl', 'missing value(s)'], ctx.exception.reasons) # invalid ip address with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, '', { 'type': 'A', 'ttl': 600, 'value': 'hello' }) self.assertEquals(['invalid ip address "hello"'], ctx.exception.reasons) # invalid ip addresses with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, '', { 'type': 'A', 'ttl': 600, 'values': ['hello', 'goodbye'] }) self.assertEquals( ['invalid ip address "hello"', 'invalid ip address "goodbye"'], ctx.exception.reasons) # invalid & valid ip addresses, no ttl with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, '', { 'type': 'A', 'values': ['1.2.3.4', 'hello', '5.6.7.8'] }) self.assertEquals([ 'missing ttl', 'invalid ip address "hello"', ], ctx.exception.reasons)
from octodns.provider.base import Plan from azure.mgmt.dns.models import ARecord, AaaaRecord, CaaRecord, \ CnameRecord, MxRecord, SrvRecord, NsRecord, PtrRecord, TxtRecord, \ RecordSet, SoaRecord, Zone as AzureZone from msrestazure.azure_exceptions import CloudError from unittest import TestCase from mock import Mock, patch zone = Zone(name='unit.tests.', sub_zones=[]) octo_records = [] octo_records.append( Record.new(zone, '', { 'ttl': 0, 'type': 'A', 'values': ['1.2.3.4', '10.10.10.10'] })) octo_records.append( Record.new(zone, 'a', { 'ttl': 1, 'type': 'A', 'values': ['1.2.3.4', '1.1.1.1'] })) octo_records.append( Record.new(zone, 'aa', { 'ttl': 9001, 'type': 'A', 'values': ['1.2.4.3'] })) octo_records.append(
def test_apply(self): provider = DnsimpleProvider('test', 'token', 42) resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) # non-existant domain, create everything resp.json.side_effect = [ DnsimpleClientNotFound, # no zone in populate DnsimpleClientNotFound, # no domain during apply ] plan = provider.plan(self.expected) # No root NS, no ignored n = len(self.expected.records) - 2 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) provider._client._request.assert_has_calls([ # created the domain call('POST', '/domains', data={'name': 'unit.tests'}), # created at least one of the record with expected data call('POST', '/zones/unit.tests/records', data={ 'content': '20 30 foo-1.unit.tests.', 'priority': 10, 'type': 'SRV', 'name': '_srv._tcp', 'ttl': 600 }), ]) # expected number of total calls self.assertEquals(26, provider._client._request.call_count) provider._client._request.reset_mock() # delete 1 and update 1 provider._client.records = Mock(return_value=[{ 'id': 11189897, 'name': 'www', 'content': '1.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189898, 'name': 'www', 'content': '2.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189899, 'name': 'ttl', 'content': '3.2.3.4', 'ttl': 600, 'type': 'A', }]) # Domain exists, we don't care about return resp.json.side_effect = ['{}'] wanted = Zone('unit.tests.', []) wanted.add_record( Record.new(wanted, 'ttl', { 'ttl': 300, 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._client._request.assert_has_calls([ call('POST', '/zones/unit.tests/records', data={ 'content': '3.2.3.4', 'type': 'A', 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/zones/unit.tests/records/11189899'), call('DELETE', '/zones/unit.tests/records/11189897'), call('DELETE', '/zones/unit.tests/records/11189898') ], any_order=True)
class TestDynProvider(TestCase): expected = Zone('unit.tests.', []) for name, data in (('', { 'type': 'A', 'ttl': 300, 'values': ['1.2.3.4'] }), ('cname', { 'type': 'CNAME', 'ttl': 301, 'value': 'unit.tests.' }), ('', { 'type': 'MX', 'ttl': 302, 'values': [{ 'preference': 10, 'exchange': 'smtp-1.unit.tests.' }, { 'preference': 20, 'exchange': 'smtp-2.unit.tests.' }] }), ('naptr', { 'type': 'NAPTR', 'ttl': 303, 'values': [{ 'order': 100, 'preference': 101, 'flags': 'U', 'service': 'SIP+D2U', 'regexp': '!^.*$!sip:[email protected]!', 'replacement': '.', }, { 'order': 200, 'preference': 201, 'flags': 'U', 'service': 'SIP+D2U', 'regexp': '!^.*$!sip:[email protected]!', 'replacement': '.', }] }), ('sub', { 'type': 'NS', 'ttl': 3600, 'values': ['ns3.p10.dynect.net.', 'ns3.p10.dynect.net.'], }), ('ptr', { 'type': 'PTR', 'ttl': 304, 'value': 'xx.unit.tests.' }), ('spf', { 'type': 'SPF', 'ttl': 305, 'values': ['v=spf1 ip4:192.168.0.1/16-all', 'v=spf1 -all'], }), ('', { 'type': 'SSHFP', 'ttl': 306, 'value': { 'algorithm': 1, 'fingerprint_type': 1, 'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73', } }), ('_srv._tcp', { 'type': 'SRV', 'ttl': 307, 'values': [{ 'priority': 11, 'weight': 12, 'port': 10, 'target': 'foo-1.unit.tests.' }, { 'priority': 21, 'weight': 22, 'port': 20, 'target': 'foo-2.unit.tests.' }] })): expected.add_record(Record.new(expected, name, data)) @classmethod def setUpClass(self): # Get the DynectSession creation out of the way so that tests can # ignore it with patch('dyn.core.SessionEngine.execute', return_value={'status': 'success'}): provider = DynProvider('test', 'cust', 'user', 'pass') provider._check_dyn_sess() def setUp(self): # Flush our zone to ensure we start fresh _CachingDynZone.flush_zone(self.expected.name[:-1]) @patch('dyn.core.SessionEngine.execute') def test_populate_non_existent(self, execute_mock): provider = DynProvider('test', 'cust', 'user', 'pass') # Test Zone create execute_mock.side_effect = [ DynectGetError('foo'), ] got = Zone('unit.tests.', []) provider.populate(got) execute_mock.assert_has_calls([ call('/Zone/unit.tests/', 'GET', {}), ]) self.assertEquals(set(), got.records) @patch('dyn.core.SessionEngine.execute') def test_populate(self, execute_mock): provider = DynProvider('test', 'cust', 'user', 'pass') # Test Zone create execute_mock.side_effect = [ # get Zone { 'data': {} }, # get_all_records { 'data': { 'a_records': [{ 'fqdn': 'unit.tests', 'rdata': { 'address': '1.2.3.4' }, 'record_id': 1, 'record_type': 'A', 'ttl': 300, 'zone': 'unit.tests', }], 'cname_records': [{ 'fqdn': 'cname.unit.tests', 'rdata': { 'cname': 'unit.tests.' }, 'record_id': 2, 'record_type': 'CNAME', 'ttl': 301, 'zone': 'unit.tests', }], 'ns_records': [{ 'fqdn': 'unit.tests', 'rdata': { 'nsdname': 'ns1.p10.dynect.net.' }, 'record_id': 254597562, 'record_type': 'NS', 'service_class': '', 'ttl': 3600, 'zone': 'unit.tests' }, { 'fqdn': 'unit.tests', 'rdata': { 'nsdname': 'ns2.p10.dynect.net.' }, 'record_id': 254597563, 'record_type': 'NS', 'service_class': '', 'ttl': 3600, 'zone': 'unit.tests' }, { 'fqdn': 'unit.tests', 'rdata': { 'nsdname': 'ns3.p10.dynect.net.' }, 'record_id': 254597564, 'record_type': 'NS', 'service_class': '', 'ttl': 3600, 'zone': 'unit.tests' }, { 'fqdn': 'unit.tests', 'rdata': { 'nsdname': 'ns4.p10.dynect.net.' }, 'record_id': 254597565, 'record_type': 'NS', 'service_class': '', 'ttl': 3600, 'zone': 'unit.tests' }, { 'fqdn': 'sub.unit.tests', 'rdata': { 'nsdname': 'ns3.p10.dynect.net.' }, 'record_id': 254597564, 'record_type': 'NS', 'service_class': '', 'ttl': 3600, 'zone': 'unit.tests' }, { 'fqdn': 'sub.unit.tests', 'rdata': { 'nsdname': 'ns3.p10.dynect.net.' }, 'record_id': 254597564, 'record_type': 'NS', 'service_class': '', 'ttl': 3600, 'zone': 'unit.tests' }], 'mx_records': [{ 'fqdn': 'unit.tests', 'rdata': { 'exchange': 'smtp-1.unit.tests.', 'preference': 10 }, 'record_id': 3, 'record_type': 'MX', 'ttl': 302, 'zone': 'unit.tests', }, { 'fqdn': 'unit.tests', 'rdata': { 'exchange': 'smtp-2.unit.tests.', 'preference': 20 }, 'record_id': 4, 'record_type': 'MX', 'ttl': 302, 'zone': 'unit.tests', }], 'naptr_records': [{ 'fqdn': 'naptr.unit.tests', 'rdata': { 'flags': 'U', 'order': 100, 'preference': 101, 'regexp': '!^.*$!sip:[email protected]!', 'replacement': '.', 'services': 'SIP+D2U' }, 'record_id': 5, 'record_type': 'MX', 'ttl': 303, 'zone': 'unit.tests', }, { 'fqdn': 'naptr.unit.tests', 'rdata': { 'flags': 'U', 'order': 200, 'preference': 201, 'regexp': '!^.*$!sip:[email protected]!', 'replacement': '.', 'services': 'SIP+D2U' }, 'record_id': 6, 'record_type': 'MX', 'ttl': 303, 'zone': 'unit.tests', }], 'ptr_records': [{ 'fqdn': 'ptr.unit.tests', 'rdata': { 'ptrdname': 'xx.unit.tests.' }, 'record_id': 7, 'record_type': 'PTR', 'ttl': 304, 'zone': 'unit.tests', }], 'soa_records': [{ 'fqdn': 'unit.tests', 'rdata': { 'txtdata': 'ns1.p16.dynect.net. ' 'hostmaster.unit.tests. 4 3600 600 604800 1800' }, 'record_id': 99, 'record_type': 'SOA', 'ttl': 299, 'zone': 'unit.tests', }], 'spf_records': [{ 'fqdn': 'spf.unit.tests', 'rdata': { 'txtdata': 'v=spf1 ip4:192.168.0.1/16-all' }, 'record_id': 8, 'record_type': 'SPF', 'ttl': 305, 'zone': 'unit.tests', }, { 'fqdn': 'spf.unit.tests', 'rdata': { 'txtdata': 'v=spf1 -all' }, 'record_id': 8, 'record_type': 'SPF', 'ttl': 305, 'zone': 'unit.tests', }], 'sshfp_records': [{ 'fqdn': 'unit.tests', 'rdata': { 'algorithm': 1, 'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73', 'fptype': 1 }, 'record_id': 9, 'record_type': 'SSHFP', 'ttl': 306, 'zone': 'unit.tests', }], 'srv_records': [{ 'fqdn': '_srv._tcp.unit.tests', 'rdata': { 'port': 10, 'priority': 11, 'target': 'foo-1.unit.tests.', 'weight': 12 }, 'record_id': 10, 'record_type': 'SRV', 'ttl': 307, 'zone': 'unit.tests', }, { 'fqdn': '_srv._tcp.unit.tests', 'rdata': { 'port': 20, 'priority': 21, 'target': 'foo-2.unit.tests.', 'weight': 22 }, 'record_id': 11, 'record_type': 'SRV', 'ttl': 307, 'zone': 'unit.tests', }], } } ] got = Zone('unit.tests.', []) provider.populate(got) execute_mock.assert_has_calls([ call('/Zone/unit.tests/', 'GET', {}), call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}) ]) changes = self.expected.changes(got, SimpleProvider()) self.assertEquals([], changes) @patch('dyn.core.SessionEngine.execute') def test_sync(self, execute_mock): provider = DynProvider('test', 'cust', 'user', 'pass') # Test Zone create execute_mock.side_effect = [ # No such zone, during populate DynectGetError('foo'), # No such zone, during sync DynectGetError('foo'), # get empty Zone { 'data': {} }, # get zone we can modify & delete with { 'data': { # A top-level to delete 'a_records': [{ 'fqdn': 'unit.tests', 'rdata': { 'address': '1.2.3.4' }, 'record_id': 1, 'record_type': 'A', 'ttl': 30, 'zone': 'unit.tests', }, { 'fqdn': 'a.unit.tests', 'rdata': { 'address': '2.3.4.5' }, 'record_id': 2, 'record_type': 'A', 'ttl': 30, 'zone': 'unit.tests', }], # A node to delete 'cname_records': [{ 'fqdn': 'cname.unit.tests', 'rdata': { 'cname': 'unit.tests.' }, 'record_id': 3, 'record_type': 'CNAME', 'ttl': 30, 'zone': 'unit.tests', }], # A record to leave alone 'ptr_records': [{ 'fqdn': 'ptr.unit.tests', 'rdata': { 'ptrdname': 'xx.unit.tests.' }, 'record_id': 4, 'record_type': 'PTR', 'ttl': 30, 'zone': 'unit.tests', }], # A record to modify 'srv_records': [{ 'fqdn': '_srv._tcp.unit.tests', 'rdata': { 'port': 10, 'priority': 11, 'target': 'foo-1.unit.tests.', 'weight': 12 }, 'record_id': 5, 'record_type': 'SRV', 'ttl': 30, 'zone': 'unit.tests', }, { 'fqdn': '_srv._tcp.unit.tests', 'rdata': { 'port': 20, 'priority': 21, 'target': 'foo-2.unit.tests.', 'weight': 22 }, 'record_id': 6, 'record_type': 'SRV', 'ttl': 30, 'zone': 'unit.tests', }], } } ] # No existing records, create all with patch('dyn.tm.zones.Zone.add_record') as add_mock: with patch('dyn.tm.zones.Zone._update') as update_mock: plan = provider.plan(self.expected) update_mock.assert_not_called() provider.apply(plan) update_mock.assert_called() add_mock.assert_called() # Once for each dyn record (8 Records, 2 of which have dual values) self.assertEquals(14, len(add_mock.call_args_list)) execute_mock.assert_has_calls([ call('/Zone/unit.tests/', 'GET', {}), call('/Zone/unit.tests/', 'GET', {}) ]) self.assertEquals(9, len(plan.changes)) execute_mock.reset_mock() # Delete one and modify another new = Zone('unit.tests.', []) for name, data in (('a', { 'type': 'A', 'ttl': 30, 'value': '2.3.4.5' }), ('ptr', { 'type': 'PTR', 'ttl': 30, 'value': 'xx.unit.tests.' }), ('_srv._tcp', { 'type': 'SRV', 'ttl': 30, 'values': [{ 'priority': 31, 'weight': 12, 'port': 10, 'target': 'foo-1.unit.tests.' }, { 'priority': 21, 'weight': 22, 'port': 20, 'target': 'foo-2.unit.tests.' }] })): new.add_record(Record.new(new, name, data)) with patch('dyn.tm.zones.Zone.add_record') as add_mock: with patch('dyn.tm.records.DNSRecord.delete') as delete_mock: with patch('dyn.tm.zones.Zone._update') as update_mock: plan = provider.plan(new) provider.apply(plan) update_mock.assert_called() # we expect 4 deletes, 2 from actual deletes and 2 from # updates which delete and recreate self.assertEquals(4, len(delete_mock.call_args_list)) # the 2 (re)creates self.assertEquals(2, len(add_mock.call_args_list)) execute_mock.assert_has_calls([ call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}) ]) self.assertEquals(3, len(plan.changes))
class TestDnsimpleProvider(TestCase): expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) # Our test suite differs a bit, add our NS and remove the simple one expected.add_record( Record.new( expected, 'under', { 'ttl': 3600, 'type': 'NS', 'values': [ 'ns1.unit.tests.', 'ns2.unit.tests.', ] })) for record in list(expected.records): if record.name == 'sub' and record._type == 'NS': expected._remove_record(record) break def test_populate(self): provider = DnsimpleProvider('test', 'token', 42) # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=401, text='{"message": "Authentication failed"}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Unauthorized', ctx.exception.message) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existant zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=404, text='{"message": "Domain `foo.bar` not found"}') zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # No diffs == no changes with requests_mock() as mock: base = 'https://api.dnsimple.com/v2/42/zones/unit.tests/' \ 'records?page=' with open('tests/fixtures/dnsimple-page-1.json') as fh: mock.get('{}{}'.format(base, 1), text=fh.read()) with open('tests/fixtures/dnsimple-page-2.json') as fh: mock.get('{}{}'.format(base, 2), text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(14, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(0, len(changes)) # 2nd populate makes no network calls/all from cache again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(14, len(again.records)) # bust the cache del provider._zone_records[zone.name] # test handling of invalid content with requests_mock() as mock: with open('tests/fixtures/dnsimple-invalid-content.json') as fh: mock.get(ANY, text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals( set([ Record.new(zone, '', { 'ttl': 3600, 'type': 'SSHFP', 'values': [] }), Record.new(zone, '_srv._tcp', { 'ttl': 600, 'type': 'SRV', 'values': [] }), Record.new(zone, 'naptr', { 'ttl': 600, 'type': 'NAPTR', 'values': [] }), ]), zone.records) def test_apply(self): provider = DnsimpleProvider('test', 'token', 42) resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) # non-existant domain, create everything resp.json.side_effect = [ DnsimpleClientNotFound, # no zone in populate DnsimpleClientNotFound, # no domain during apply ] plan = provider.plan(self.expected) # No root NS, no ignored n = len(self.expected.records) - 2 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) provider._client._request.assert_has_calls([ # created the domain call('POST', '/domains', data={'name': 'unit.tests'}), # created at least one of the record with expected data call('POST', '/zones/unit.tests/records', data={ 'content': '20 30 foo-1.unit.tests.', 'priority': 10, 'type': 'SRV', 'name': '_srv._tcp', 'ttl': 600 }), ]) # expected number of total calls self.assertEquals(26, provider._client._request.call_count) provider._client._request.reset_mock() # delete 1 and update 1 provider._client.records = Mock(return_value=[{ 'id': 11189897, 'name': 'www', 'content': '1.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189898, 'name': 'www', 'content': '2.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189899, 'name': 'ttl', 'content': '3.2.3.4', 'ttl': 600, 'type': 'A', }]) # Domain exists, we don't care about return resp.json.side_effect = ['{}'] wanted = Zone('unit.tests.', []) wanted.add_record( Record.new(wanted, 'ttl', { 'ttl': 300, 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._client._request.assert_has_calls([ call('POST', '/zones/unit.tests/records', data={ 'content': '3.2.3.4', 'type': 'A', 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/zones/unit.tests/records/11189899'), call('DELETE', '/zones/unit.tests/records/11189897'), call('DELETE', '/zones/unit.tests/records/11189898') ], any_order=True)
class TestDynProviderGeo(TestCase): with open('./tests/fixtures/dyn-traffic-director-get.json') as fh: traffic_director_response = loads(fh.read()) @property def traffic_directors_reponse(self): return { 'data': [{ 'active': 'Y', 'label': 'unit.tests.:A', 'nodes': [], 'notifiers': [], 'pending_change': '', 'rulesets': [], 'service_id': '2ERWXQNsb_IKG2YZgYqkPvk0PBM', 'ttl': '300' }, { 'active': 'Y', 'label': 'some.other.:A', 'nodes': [], 'notifiers': [], 'pending_change': '', 'rulesets': [], 'service_id': '3ERWXQNsb_IKG2YZgYqkPvk0PBM', 'ttl': '300' }, { 'active': 'Y', 'label': 'other format', 'nodes': [], 'notifiers': [], 'pending_change': '', 'rulesets': [], 'service_id': '4ERWXQNsb_IKG2YZgYqkPvk0PBM', 'ttl': '300' }] } # Doing this as a property so that we get a fresh copy each time, dyn's # client lib messes with the return value and prevents it from working on # subsequent uses otherwise @property def records_response(self): return { 'data': { 'a_records': [{ 'fqdn': 'unit.tests', 'rdata': { 'address': '1.2.3.4' }, 'record_id': 1, 'record_type': 'A', 'ttl': 301, 'zone': 'unit.tests', }], } } monitor_id = '42a' monitors_response = { 'data': [{ 'active': 'Y', 'dsf_monitor_id': monitor_id, 'endpoints': [], 'label': 'unit.tests.', 'notifier': '', 'options': { 'expected': '', 'header': 'User-Agent: Dyn Monitor', 'host': 'unit.tests', 'path': '/_dns', 'port': '443', 'timeout': '10' }, 'probe_interval': '60', 'protocol': 'HTTPS', 'response_count': '2', 'retries': '2' }], 'job_id': 3376281406, 'msgs': [{ 'ERR_CD': None, 'INFO': 'DSFMonitor_get: Here are your monitors', 'LVL': 'INFO', 'SOURCE': 'BLL' }], 'status': 'success' } expected_geo = Zone('unit.tests.', []) geo_record = Record.new( expected_geo, '', { 'geo': { 'AF': ['2.2.3.4', '2.2.3.5'], 'AS-JP': ['3.2.3.4', '3.2.3.5'], 'NA-US': ['4.2.3.4', '4.2.3.5'], 'NA-US-CA': ['5.2.3.4', '5.2.3.5'] }, 'ttl': 300, 'type': 'A', 'values': ['1.2.3.4', '1.2.3.5'], }) expected_geo.add_record(geo_record) expected_regular = Zone('unit.tests.', []) regular_record = Record.new(expected_regular, '', { 'ttl': 301, 'type': 'A', 'value': '1.2.3.4', }) expected_regular.add_record(regular_record) def setUp(self): # Flush our zone to ensure we start fresh _CachingDynZone.flush_zone('unit.tests') @patch('dyn.core.SessionEngine.execute') def test_traffic_directors(self, mock): provider = DynProvider('test', 'cust', 'user', 'pass', True) # short-circuit session checking provider._dyn_sess = True # no tds mock.side_effect = [{'data': []}] self.assertEquals({}, provider.traffic_directors) # a supported td and an ingored one response = { 'data': [{ 'active': 'Y', 'label': 'unit.tests.:A', 'nodes': [], 'notifiers': [], 'pending_change': '', 'rulesets': [], 'service_id': '2ERWXQNsb_IKG2YZgYqkPvk0PBM', 'ttl': '300' }, { 'active': 'Y', 'label': 'geo.unit.tests.:A', 'nodes': [], 'notifiers': [], 'pending_change': '', 'rulesets': [], 'service_id': '3ERWXQNsb_IKG2YZgYqkPvk0PBM', 'ttl': '300' }, { 'active': 'Y', 'label': 'something else', 'nodes': [], 'notifiers': [], 'pending_change': '', 'rulesets': [], 'service_id': '4ERWXQNsb_IKG2YZgYqkPvk0PBM', 'ttl': '300' }], 'job_id': 3376164583, 'status': 'success' } mock.side_effect = [response] # first make sure that we get the empty version from cache self.assertEquals({}, provider.traffic_directors) # reach in and bust the cache provider._traffic_directors = None tds = provider.traffic_directors self.assertEquals(set(['unit.tests.', 'geo.unit.tests.']), set(tds.keys())) self.assertEquals(['A'], tds['unit.tests.'].keys()) self.assertEquals(['A'], tds['geo.unit.tests.'].keys()) @patch('dyn.core.SessionEngine.execute') def test_traffic_director_monitor(self, mock): provider = DynProvider('test', 'cust', 'user', 'pass', True) # short-circuit session checking provider._dyn_sess = True # no monitors, will try and create geo_monitor_id = '42x' mock.side_effect = [ self.monitors_response, { 'data': { 'active': 'Y', 'dsf_monitor_id': geo_monitor_id, 'endpoints': [], 'label': 'geo.unit.tests.', 'notifier': '', 'options': { 'expected': '', 'header': 'User-Agent: Dyn Monitor', 'host': 'geo.unit.tests.', 'path': '/_dns', 'port': '443', 'timeout': '10' }, 'probe_interval': '60', 'protocol': 'HTTPS', 'response_count': '2', 'retries': '2' }, 'job_id': 3376259461, 'msgs': [{ 'ERR_CD': None, 'INFO': 'add: Here is the new monitor', 'LVL': 'INFO', 'SOURCE': 'BLL' }], 'status': 'success' } ] # ask for a monitor that doesn't exist monitor = provider._traffic_director_monitor('geo.unit.tests.') self.assertEquals(geo_monitor_id, monitor.dsf_monitor_id) # should see a request for the list and a create mock.assert_has_calls([ call('/DSFMonitor/', 'GET', {'detail': 'Y'}), call( '/DSFMonitor/', 'POST', { 'retries': 2, 'protocol': 'HTTPS', 'response_count': 2, 'label': 'geo.unit.tests.', 'probe_interval': 60, 'active': 'Y', 'options': { 'path': '/_dns', 'host': 'geo.unit.tests', 'header': 'User-Agent: Dyn Monitor', 'port': 443, 'timeout': 10 } }) ]) # created monitor is now cached self.assertTrue( 'geo.unit.tests.' in provider._traffic_director_monitors) # pre-existing one is there too self.assertTrue('unit.tests.' in provider._traffic_director_monitors) # now ask for a monitor that does exist mock.reset_mock() monitor = provider._traffic_director_monitor('unit.tests.') self.assertEquals(self.monitor_id, monitor.dsf_monitor_id) # should have resulted in no calls b/c exists & we've cached the list mock.assert_not_called() @patch('dyn.core.SessionEngine.execute') def test_populate_traffic_directors_empty(self, mock): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) # empty all around mock.side_effect = [ # get traffic directors { 'data': [] }, # get zone { 'data': {} }, # get records { 'data': {} }, ] got = Zone('unit.tests.', []) provider.populate(got) self.assertEquals(0, len(got.records)) mock.assert_has_calls([ call('/DSF/', 'GET', {'detail': 'Y'}), call('/Zone/unit.tests/', 'GET', {}), call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}), ]) @patch('dyn.core.SessionEngine.execute') def test_populate_traffic_directors_td(self, mock): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) # only traffic director mock.side_effect = [ # get traffic directors self.traffic_directors_reponse, # get traffic director self.traffic_director_response, # get zone { 'data': {} }, # get records { 'data': {} }, ] got = Zone('unit.tests.', []) provider.populate(got) self.assertEquals(1, len(got.records)) self.assertFalse(self.expected_geo.changes(got, provider)) mock.assert_has_calls([ call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET', {'pending_changes': 'Y'}), call('/Zone/unit.tests/', 'GET', {}), call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}), ]) @patch('dyn.core.SessionEngine.execute') def test_populate_traffic_directors_regular(self, mock): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) # only regular mock.side_effect = [ # get traffic directors { 'data': [] }, # get zone { 'data': {} }, # get records self.records_response ] got = Zone('unit.tests.', []) provider.populate(got) self.assertEquals(1, len(got.records)) self.assertFalse(self.expected_regular.changes(got, provider)) mock.assert_has_calls([ call('/DSF/', 'GET', {'detail': 'Y'}), call('/Zone/unit.tests/', 'GET', {}), call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}), ]) @patch('dyn.core.SessionEngine.execute') def test_populate_traffic_directors_both(self, mock): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) # both traffic director and regular, regular is ignored mock.side_effect = [ # get traffic directors self.traffic_directors_reponse, # get traffic director self.traffic_director_response, # get zone { 'data': {} }, # get records self.records_response ] got = Zone('unit.tests.', []) provider.populate(got) self.assertEquals(1, len(got.records)) self.assertFalse(self.expected_geo.changes(got, provider)) mock.assert_has_calls([ call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET', {'pending_changes': 'Y'}), call('/Zone/unit.tests/', 'GET', {}), call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}), ]) @patch('dyn.core.SessionEngine.execute') def test_populate_traffic_director_busted(self, mock): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) busted_traffic_director_response = { "status": "success", "data": { "notifiers": [], "rulesets": [], "ttl": "300", "active": "Y", "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", "nodes": [{ "fqdn": "unit.tests", "zone": "unit.tests" }], "pending_change": "", "label": "unit.tests.:A" }, "job_id": 3376642606, "msgs": [{ "INFO": "detail: Here is your service", "LVL": "INFO", "ERR_CD": None, "SOURCE": "BLL" }] } # busted traffic director mock.side_effect = [ # get traffic directors self.traffic_directors_reponse, # get traffic director busted_traffic_director_response, # get zone { 'data': {} }, # get records { 'data': {} }, ] got = Zone('unit.tests.', []) provider.populate(got) self.assertEquals(1, len(got.records)) # we expect a change here for the record, the values aren't important, # so just compare set contents (which does name and type) self.assertEquals(self.expected_geo.records, got.records) mock.assert_has_calls([ call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET', {'pending_changes': 'Y'}), call('/Zone/unit.tests/', 'GET', {}), call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}), ]) @patch('dyn.core.SessionEngine.execute') def test_apply_traffic_director(self, mock): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) # stubbing these out to avoid a lot of messy mocking, they'll be tested # individually, we'll check for expected calls provider._mod_geo_Create = MagicMock() provider._mod_geo_Update = MagicMock() provider._mod_geo_Delete = MagicMock() provider._mod_Create = MagicMock() provider._mod_Update = MagicMock() provider._mod_Delete = MagicMock() # busted traffic director mock.side_effect = [ # get zone { 'data': {} }, # accept publish { 'data': {} }, ] desired = Zone('unit.tests.', []) geo = self.geo_record regular = self.regular_record changes = [ Create(geo), Create(regular), Update(geo, geo), Update(regular, regular), Delete(geo), Delete(regular), ] plan = Plan(None, desired, changes) provider._apply(plan) mock.assert_has_calls([ call('/Zone/unit.tests/', 'GET', {}), call('/Zone/unit.tests/', 'PUT', {'publish': True}) ]) # should have seen 1 call to each provider._mod_geo_Create.assert_called_once() provider._mod_geo_Update.assert_called_once() provider._mod_geo_Delete.assert_called_once() provider._mod_Create.assert_called_once() provider._mod_Update.assert_called_once() provider._mod_Delete.assert_called_once() @patch('dyn.core.SessionEngine.execute') def test_mod_geo_create(self, mock): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) # will be tested seperately provider._mod_rulesets = MagicMock() mock.side_effect = [ # create traffic director self.traffic_director_response, # get traffic directors self.traffic_directors_reponse ] provider._mod_geo_Create(None, Create(self.geo_record)) # td now lives in cache self.assertTrue('A' in provider.traffic_directors['unit.tests.']) # should have seen 1 gen call provider._mod_rulesets.assert_called_once() def test_mod_geo_update_geo_geo(self): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) # update of an existing td # pre-populate the cache with our mock td provider._traffic_directors = { 'unit.tests.': { 'A': 42, } } # mock _mod_rulesets provider._mod_rulesets = MagicMock() geo = self.geo_record change = Update(geo, geo) provider._mod_geo_Update(None, change) # still in cache self.assertTrue('A' in provider.traffic_directors['unit.tests.']) # should have seen 1 gen call provider._mod_rulesets.assert_called_once_with(42, change) @patch('dyn.core.SessionEngine.execute') def test_mod_geo_update_geo_regular(self, _): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) # convert a td to a regular record provider._mod_Create = MagicMock() provider._mod_geo_Delete = MagicMock() change = Update(self.geo_record, self.regular_record) provider._mod_geo_Update(42, change) # should have seen a call to create the new regular record provider._mod_Create.assert_called_once_with(42, change) # should have seen a call to delete the old td record provider._mod_geo_Delete.assert_called_once_with(42, change) @patch('dyn.core.SessionEngine.execute') def test_mod_geo_update_regular_geo(self, _): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) # convert a regular record to a td provider._mod_geo_Create = MagicMock() provider._mod_Delete = MagicMock() change = Update(self.regular_record, self.geo_record) provider._mod_geo_Update(42, change) # should have seen a call to create the new geo record provider._mod_geo_Create.assert_called_once_with(42, change) # should have seen a call to delete the old regular record provider._mod_Delete.assert_called_once_with(42, change) @patch('dyn.core.SessionEngine.execute') def test_mod_geo_delete(self, mock): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) td_mock = MagicMock() provider._traffic_directors = { 'unit.tests.': { 'A': td_mock, } } provider._mod_geo_Delete(None, Delete(self.geo_record)) # delete called td_mock.delete.assert_called_once() # removed from cache self.assertFalse('A' in provider.traffic_directors['unit.tests.']) @patch('dyn.tm.services.DSFResponsePool.create') def test_find_or_create_pool(self, mock): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) td = 42 # no candidates cache miss, so create values = ['1.2.3.4', '1.2.3.5'] pool = provider._find_or_create_pool(td, [], 'default', 'A', values) self.assertIsInstance(pool, DSFResponsePool) self.assertEquals(1, len(pool.rs_chains)) records = pool.rs_chains[0].record_sets[0].records self.assertEquals(values, [r.address for r in records]) mock.assert_called_once_with(td) # cache hit, use the one we just created mock.reset_mock() pools = [pool] cached = provider._find_or_create_pool(td, pools, 'default', 'A', values) self.assertEquals(pool, cached) mock.assert_not_called() # cache miss, non-matching label mock.reset_mock() miss = provider._find_or_create_pool(td, pools, 'NA-US-CA', 'A', values) self.assertNotEquals(pool, miss) self.assertEquals('NA-US-CA', miss.label) mock.assert_called_once_with(td) # cache miss, non-matching label mock.reset_mock() values = ['2.2.3.4.', '2.2.3.5'] miss = provider._find_or_create_pool(td, pools, 'default', 'A', values) self.assertNotEquals(pool, miss) mock.assert_called_once_with(td) @patch('dyn.tm.services.DSFRuleset.add_response_pool') @patch('dyn.tm.services.DSFRuleset.create') # just lets us ignore the pool.create calls @patch('dyn.tm.services.DSFResponsePool.create') def test_mod_rulesets_create(self, _, ruleset_create_mock, add_response_pool_mock): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) td_mock = MagicMock() td_mock._rulesets = [] provider._traffic_director_monitor = MagicMock() provider._find_or_create_pool = MagicMock() td_mock.all_response_pools = [] provider._find_or_create_pool.side_effect = [ _DummyPool('default'), _DummyPool(1), _DummyPool(2), _DummyPool(3), _DummyPool(4), ] change = Create(self.geo_record) provider._mod_rulesets(td_mock, change) ruleset_create_mock.assert_has_calls(( call(td_mock, index=0), call(td_mock, index=0), call(td_mock, index=0), call(td_mock, index=0), call(td_mock, index=0), )) add_response_pool_mock.assert_has_calls(( # default call('default'), # first geo and it's fallback call(1), call('default', index=999), # 2nd geo and it's fallback call(2), call('default', index=999), # 3nd geo and it's fallback call(3), call('default', index=999), # 4th geo and it's 2 levels of fallback call(4), call(3, index=999), call('default', index=999), )) # have to patch the place it's imported into, not where it lives @patch('octodns.provider.dyn.get_response_pool') @patch('dyn.tm.services.DSFRuleset.add_response_pool') @patch('dyn.tm.services.DSFRuleset.create') # just lets us ignore the pool.create calls @patch('dyn.tm.services.DSFResponsePool.create') def test_mod_rulesets_existing(self, _, ruleset_create_mock, add_response_pool_mock, get_response_pool_mock): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) ruleset_mock = MagicMock() ruleset_mock.response_pools = [_DummyPool(3)] td_mock = MagicMock() td_mock._rulesets = [ ruleset_mock, ] provider._traffic_director_monitor = MagicMock() provider._find_or_create_pool = MagicMock() unused_pool = _DummyPool('unused') td_mock.all_response_pools = \ ruleset_mock.response_pools + [unused_pool] get_response_pool_mock.return_value = unused_pool provider._find_or_create_pool.side_effect = [ _DummyPool('default'), _DummyPool(1), _DummyPool(2), ruleset_mock.response_pools[0], _DummyPool(4), ] change = Create(self.geo_record) provider._mod_rulesets(td_mock, change) ruleset_create_mock.assert_has_calls(( call(td_mock, index=0), call(td_mock, index=0), call(td_mock, index=0), call(td_mock, index=0), call(td_mock, index=0), )) add_response_pool_mock.assert_has_calls(( # default call('default'), # first geo and it's fallback call(1), call('default', index=999), # 2nd geo and it's fallback call(2), call('default', index=999), # 3nd geo, from existing, and it's fallback call(3), call('default', index=999), # 4th geo and it's 2 levels of fallback call(4), call(3, index=999), call('default', index=999), )) # unused poll should have been deleted self.assertTrue(unused_pool.deleted) # old ruleset ruleset should be deleted, it's pool will have been # reused ruleset_mock.delete.assert_called_once()
def test_populate(self): provider = DnsimpleProvider('test', 'token', 42) # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=401, text='{"message": "Authentication failed"}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Unauthorized', ctx.exception.message) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existant zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=404, text='{"message": "Domain `foo.bar` not found"}') zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # No diffs == no changes with requests_mock() as mock: base = 'https://api.dnsimple.com/v2/42/zones/unit.tests/' \ 'records?page=' with open('tests/fixtures/dnsimple-page-1.json') as fh: mock.get('{}{}'.format(base, 1), text=fh.read()) with open('tests/fixtures/dnsimple-page-2.json') as fh: mock.get('{}{}'.format(base, 2), text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(14, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(0, len(changes)) # 2nd populate makes no network calls/all from cache again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(14, len(again.records)) # bust the cache del provider._zone_records[zone.name] # test handling of invalid content with requests_mock() as mock: with open('tests/fixtures/dnsimple-invalid-content.json') as fh: mock.get(ANY, text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals( set([ Record.new(zone, '', { 'ttl': 3600, 'type': 'SSHFP', 'values': [] }), Record.new(zone, '_srv._tcp', { 'ttl': 600, 'type': 'SRV', 'values': [] }), Record.new(zone, 'naptr', { 'ttl': 600, 'type': 'NAPTR', 'values': [] }), ]), zone.records)
class TestSelectelProvider(TestCase): API_URL = 'https://api.selectel.ru/domains/v1' api_record = [] zone = Zone('unit.tests.', []) expected = set() domain = [{"name": "unit.tests", "id": 100000}] # A, subdomain='' api_record.append({ 'type': 'A', 'ttl': 100, 'content': '1.2.3.4', 'name': 'unit.tests', 'id': 1 }) expected.add( Record.new(zone, '', { 'ttl': 100, 'type': 'A', 'value': '1.2.3.4', })) # A, subdomain='sub' api_record.append({ 'type': 'A', 'ttl': 200, 'content': '1.2.3.4', 'name': 'sub.unit.tests', 'id': 2 }) expected.add( Record.new(zone, 'sub', { 'ttl': 200, 'type': 'A', 'value': '1.2.3.4', })) # CNAME api_record.append({ 'type': 'CNAME', 'ttl': 300, 'content': 'unit.tests', 'name': 'www2.unit.tests', 'id': 3 }) expected.add( Record.new(zone, 'www2', { 'ttl': 300, 'type': 'CNAME', 'value': 'unit.tests.', })) # MX api_record.append({ 'type': 'MX', 'ttl': 400, 'content': 'mx1.unit.tests', 'priority': 10, 'name': 'unit.tests', 'id': 4 }) expected.add( Record.new( zone, '', { 'ttl': 400, 'type': 'MX', 'values': [{ 'preference': 10, 'exchange': 'mx1.unit.tests.', }] })) # NS api_record.append({ 'type': 'NS', 'ttl': 600, 'content': 'ns1.unit.tests', 'name': 'unit.tests.', 'id': 6 }) api_record.append({ 'type': 'NS', 'ttl': 600, 'content': 'ns2.unit.tests', 'name': 'unit.tests', 'id': 7 }) expected.add( Record.new( zone, '', { 'ttl': 600, 'type': 'NS', 'values': ['ns1.unit.tests.', 'ns2.unit.tests.'], })) # NS with sub api_record.append({ 'type': 'NS', 'ttl': 700, 'content': 'ns3.unit.tests', 'name': 'www3.unit.tests', 'id': 8 }) api_record.append({ 'type': 'NS', 'ttl': 700, 'content': 'ns4.unit.tests', 'name': 'www3.unit.tests', 'id': 9 }) expected.add( Record.new( zone, 'www3', { 'ttl': 700, 'type': 'NS', 'values': ['ns3.unit.tests.', 'ns4.unit.tests.'], })) # SRV api_record.append({ 'type': 'SRV', 'ttl': 800, 'target': 'foo-1.unit.tests', 'weight': 20, 'priority': 10, 'port': 30, 'id': 10, 'name': '_srv._tcp.unit.tests' }) api_record.append({ 'type': 'SRV', 'ttl': 800, 'target': 'foo-2.unit.tests', 'name': '_srv._tcp.unit.tests', 'weight': 50, 'priority': 40, 'port': 60, 'id': 11 }) expected.add( Record.new( zone, '_srv._tcp', { 'ttl': 800, 'type': 'SRV', 'values': [{ 'priority': 10, 'weight': 20, 'port': 30, 'target': 'foo-1.unit.tests.', }, { 'priority': 40, 'weight': 50, 'port': 60, 'target': 'foo-2.unit.tests.', }] })) # AAAA aaaa_record = { 'type': 'AAAA', 'ttl': 200, 'content': '1:1ec:1::1', 'name': 'unit.tests', 'id': 15 } api_record.append(aaaa_record) expected.add( Record.new(zone, '', { 'ttl': 200, 'type': 'AAAA', 'value': '1:1ec:1::1', })) # TXT api_record.append({ 'type': 'TXT', 'ttl': 300, 'content': 'little text', 'name': 'text.unit.tests', 'id': 16 }) expected.add( Record.new(zone, 'text', { 'ttl': 200, 'type': 'TXT', 'value': 'little text', })) @requests_mock.Mocker() def test_populate(self, fake_http): zone = Zone('unit.tests.', []) fake_http.get(f'{self.API_URL}/unit.tests/records/', json=self.api_record) fake_http.get(f'{self.API_URL}/', json=self.domain) fake_http.head(f'{self.API_URL}/unit.tests/records/', headers={'X-Total-Count': str(len(self.api_record))}) fake_http.head(f'{self.API_URL}/', headers={'X-Total-Count': str(len(self.domain))}) provider = SelectelProvider(123, 'secret_token') provider.populate(zone) self.assertEquals(self.expected, zone.records) @requests_mock.Mocker() def test_populate_invalid_record(self, fake_http): more_record = self.api_record more_record.append({ "name": "unit.tests", "id": 100001, "content": "support.unit.tests.", "ttl": 300, "ns": "ns1.unit.tests", "type": "SOA", "email": "*****@*****.**" }) zone = Zone('unit.tests.', []) fake_http.get(f'{self.API_URL}/unit.tests/records/', json=more_record) fake_http.get(f'{self.API_URL}/', json=self.domain) fake_http.head(f'{self.API_URL}/unit.tests/records/', headers={'X-Total-Count': str(len(self.api_record))}) fake_http.head(f'{self.API_URL}/', headers={'X-Total-Count': str(len(self.domain))}) zone.add_record( Record.new( self.zone, 'unsup', { 'ttl': 200, 'type': 'NAPTR', 'value': { 'order': 40, 'preference': 70, 'flags': 'U', 'service': 'SIP+D2U', 'regexp': '!^.*$!sip:[email protected]!', 'replacement': '.', } })) provider = SelectelProvider(123, 'secret_token') provider.populate(zone) self.assertNotEqual(self.expected, zone.records) @requests_mock.Mocker() def test_apply(self, fake_http): fake_http.get(f'{self.API_URL}/unit.tests/records/', json=list()) fake_http.get(f'{self.API_URL}/', json=self.domain) fake_http.head(f'{self.API_URL}/unit.tests/records/', headers={'X-Total-Count': '0'}) fake_http.head(f'{self.API_URL}/', headers={'X-Total-Count': str(len(self.domain))}) fake_http.post(f'{self.API_URL}/100000/records/', json=list()) provider = SelectelProvider(123, 'test_token') zone = Zone('unit.tests.', []) for record in self.expected: zone.add_record(record) plan = provider.plan(zone) self.assertEquals(8, len(plan.changes)) self.assertEquals(8, provider.apply(plan)) @requests_mock.Mocker() def test_domain_list(self, fake_http): fake_http.get(f'{self.API_URL}/', json=self.domain) fake_http.head(f'{self.API_URL}/', headers={'X-Total-Count': str(len(self.domain))}) expected = {'unit.tests': self.domain[0]} provider = SelectelProvider(123, 'test_token') result = provider.domain_list() self.assertEquals(result, expected) @requests_mock.Mocker() def test_authentication_fail(self, fake_http): fake_http.get(f'{self.API_URL}/', status_code=401) fake_http.head(f'{self.API_URL}/', headers={'X-Total-Count': str(len(self.domain))}) with self.assertRaises(Exception) as ctx: SelectelProvider(123, 'fail_token') self.assertEquals(text_type(ctx.exception), 'Authorization failed. Invalid or empty token.') @requests_mock.Mocker() def test_not_exist_domain(self, fake_http): fake_http.get(f'{self.API_URL}/', status_code=404, json='') fake_http.head(f'{self.API_URL}/', headers={'X-Total-Count': str(len(self.domain))}) fake_http.post(f'{self.API_URL}/', json={ "name": "unit.tests", "create_date": 1507154178, "id": 100000 }) fake_http.get(f'{self.API_URL}/unit.tests/records/', json=list()) fake_http.head(f'{self.API_URL}/unit.tests/records/', headers={'X-Total-Count': str(len(self.api_record))}) fake_http.post(f'{self.API_URL}/100000/records/', json=list()) provider = SelectelProvider(123, 'test_token') zone = Zone('unit.tests.', []) for record in self.expected: zone.add_record(record) plan = provider.plan(zone) self.assertEquals(8, len(plan.changes)) self.assertEquals(8, provider.apply(plan)) @requests_mock.Mocker() def test_delete_no_exist_record(self, fake_http): fake_http.get(f'{self.API_URL}/', json=self.domain) fake_http.get(f'{self.API_URL}/100000/records/', json=list()) fake_http.head(f'{self.API_URL}/', headers={'X-Total-Count': str(len(self.domain))}) fake_http.head(f'{self.API_URL}/unit.tests/records/', headers={'X-Total-Count': '0'}) provider = SelectelProvider(123, 'test_token') zone = Zone('unit.tests.', []) provider.delete_record('unit.tests', 'NS', zone) @requests_mock.Mocker() def test_change_record(self, fake_http): exist_record = [ self.aaaa_record, { "content": "6.6.5.7", "ttl": 100, "type": "A", "id": 100001, "name": "delete.unit.tests" }, { "content": "9.8.2.1", "ttl": 100, "type": "A", "id": 100002, "name": "unit.tests" } ] # exist fake_http.get(f'{self.API_URL}/unit.tests/records/', json=exist_record) fake_http.get(f'{self.API_URL}/', json=self.domain) fake_http.get(f'{self.API_URL}/100000/records/', json=exist_record) fake_http.head(f'{self.API_URL}/unit.tests/records/', headers={'X-Total-Count': str(len(exist_record))}) fake_http.head(f'{self.API_URL}/', headers={'X-Total-Count': str(len(self.domain))}) fake_http.head(f'{self.API_URL}/100000/records/', headers={'X-Total-Count': str(len(exist_record))}) fake_http.post(f'{self.API_URL}/100000/records/', json=list()) fake_http.delete(f'{self.API_URL}/100000/records/100001', text="") fake_http.delete(f'{self.API_URL}/100000/records/100002', text="") provider = SelectelProvider(123, 'test_token') zone = Zone('unit.tests.', []) for record in self.expected: zone.add_record(record) plan = provider.plan(zone) self.assertEquals(8, len(plan.changes)) self.assertEquals(8, provider.apply(plan)) @requests_mock.Mocker() def test_include_change_returns_false(self, fake_http): fake_http.get(f'{self.API_URL}/', json=self.domain) fake_http.head(f'{self.API_URL}/', headers={'X-Total-Count': str(len(self.domain))}) provider = SelectelProvider(123, 'test_token') zone = Zone('unit.tests.', []) exist_record = Record.new(zone, '', { 'ttl': 60, 'type': 'A', 'values': ['1.1.1.1', '2.2.2.2'] }) new = Record.new(zone, '', { 'ttl': 10, 'type': 'A', 'values': ['1.1.1.1', '2.2.2.2'] }) change = Update(exist_record, new) include_change = provider._include_change(change) self.assertFalse(include_change)
def test_apply(self): provider = EasyDNSProvider('test', 'token', 'apikey') resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) domain_after_creation = { "tm": 1000000000, "data": [{ "id": "12341001", "domain": "unit.tests", "host": "@", "ttl": "0", "prio": "0", "type": "SOA", "rdata": "dns1.easydns.com. zone.easydns.com. 2020010101" " 3600 600 604800 0", "geozone_id": "0", "last_mod": "2020-01-01 01:01:01" }, { "id": "12341002", "domain": "unit.tests", "host": "@", "ttl": "0", "prio": "0", "type": "NS", "rdata": "LOCAL.", "geozone_id": "0", "last_mod": "2020-01-01 01:01:01" }, { "id": "12341003", "domain": "unit.tests", "host": "@", "ttl": "0", "prio": "0", "type": "MX", "rdata": "LOCAL.", "geozone_id": "0", "last_mod": "2020-01-01 01:01:01" }], "count": 3, "total": 3, "start": 0, "max": 1000, "status": 200 } # non-existent domain, create everything resp.json.side_effect = [ EasyDNSClientNotFound, # no zone in populate domain_after_creation ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no unsupported n = len(self.expected.records) - 9 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) self.assertFalse(plan.exists) self.assertEquals(25, provider._client._request.call_count) provider._client._request.reset_mock() # delete 1 and update 1 provider._client.records = Mock( return_value=[{ "id": "12342001", "domain": "unit.tests", "host": "www", "ttl": "300", "prio": "0", "type": "A", "rdata": "2.2.3.9", "geozone_id": "0", "last_mod": "2020-01-01 01:01:01" }, { "id": "12342002", "domain": "unit.tests", "host": "www", "ttl": "300", "prio": "0", "type": "A", "rdata": "2.2.3.8", "geozone_id": "0", "last_mod": "2020-01-01 01:01:01" }, { "id": "12342003", "domain": "unit.tests", "host": "test1", "ttl": "3600", "prio": "0", "type": "A", "rdata": "1.2.3.4", "geozone_id": "0", "last_mod": "2020-01-01 01:01:01" }]) # Domain exists, we don't care about return resp.json.side_effect = ['{}'] wanted = Zone('unit.tests.', []) wanted.add_record( Record.new(wanted, 'test1', { "name": "test1", "ttl": 300, "type": "A", "value": "1.2.3.4", })) plan = provider.plan(wanted) self.assertTrue(plan.exists) self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) # recreate for update, and delete for the 2 parts of the other provider._client._request.assert_has_calls([ call('PUT', '/zones/records/add/unit.tests/A', data={ 'rdata': '1.2.3.4', 'name': 'test1', 'ttl': 300, 'type': 'A', 'host': 'test1', }), call('DELETE', '/zones/records/unit.tests/12342001'), call('DELETE', '/zones/records/unit.tests/12342002'), call('DELETE', '/zones/records/unit.tests/12342003') ], any_order=True)
class TestCloudflareProvider(TestCase): expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) # Our test suite differs a bit, add our NS and remove the simple one expected.add_record( Record.new( expected, 'under', { 'ttl': 3600, 'type': 'NS', 'values': [ 'ns1.unit.tests.', 'ns2.unit.tests.', ] })) for record in list(expected.records): if record.name == 'sub' and record._type == 'NS': expected._remove_record(record) break empty = {'result': [], 'result_info': {'count': 0, 'per_page': 0}} def test_populate(self): provider = CloudflareProvider('test', 'email', 'token') # Bad requests with requests_mock() as mock: mock.get(ANY, status_code=400, text='{"success":false,"errors":[{"code":1101,' '"message":"request was invalid"}],' '"messages":[],"result":null}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('CloudflareError', type(ctx.exception).__name__) self.assertEquals('request was invalid', ctx.exception.message) # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=403, text='{"success":false,"errors":[{"code":9103,' '"message":"Unknown X-Auth-Key or X-Auth-Email"}],' '"messages":[],"result":null}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('CloudflareAuthenticationError', type(ctx.exception).__name__) self.assertEquals('Unknown X-Auth-Key or X-Auth-Email', ctx.exception.message) # Bad auth, unknown resp with requests_mock() as mock: mock.get(ANY, status_code=403, text='{}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('CloudflareAuthenticationError', type(ctx.exception).__name__) self.assertEquals('Cloudflare error', ctx.exception.message) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existent zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=200, json=self.empty) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # re-populating the same non-existent zone uses cache and makes no # calls again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(set(), again.records) # bust zone cache provider._zones = None # existing zone with data with requests_mock() as mock: base = 'https://api.cloudflare.com/client/v4/zones' # zones with open('tests/fixtures/cloudflare-zones-page-1.json') as fh: mock.get('{}?page=1'.format(base), status_code=200, text=fh.read()) with open('tests/fixtures/cloudflare-zones-page-2.json') as fh: mock.get('{}?page=2'.format(base), status_code=200, text=fh.read()) mock.get('{}?page=3'.format(base), status_code=200, json={ 'result': [], 'result_info': { 'count': 0, 'per_page': 0 } }) # records base = '{}/234234243423aaabb334342aaa343435/dns_records' \ .format(base) with open('tests/fixtures/cloudflare-dns_records-' 'page-1.json') as fh: mock.get('{}?page=1'.format(base), status_code=200, text=fh.read()) with open('tests/fixtures/cloudflare-dns_records-' 'page-2.json') as fh: mock.get('{}?page=2'.format(base), status_code=200, text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(12, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(0, len(changes)) # re-populating the same zone/records comes out of cache, no calls again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(12, len(again.records)) def test_apply(self): provider = CloudflareProvider('test', 'email', 'token') provider._request = Mock() provider._request.side_effect = [ self.empty, # no zones { 'result': { 'id': 42, } }, # zone create ] + [None] * 20 # individual record creates # non-existent zone, create everything plan = provider.plan(self.expected) self.assertEquals(12, len(plan.changes)) self.assertEquals(12, provider.apply(plan)) self.assertFalse(plan.exists) provider._request.assert_has_calls( [ # created the domain call('POST', '/zones', data={ 'jump_start': False, 'name': 'unit.tests' }), # created at least one of the record with expected data call('POST', '/zones/42/dns_records', data={ 'content': 'ns1.unit.tests.', 'type': 'NS', 'name': 'under.unit.tests', 'ttl': 3600 }), # make sure semicolons are not escaped when sending data call('POST', '/zones/42/dns_records', data={ 'content': 'v=DKIM1;k=rsa;s=email;h=sha256;' 'p=A/kinda+of/long/string+with+numb3rs', 'type': 'TXT', 'name': 'txt.unit.tests', 'ttl': 600 }), ], True) # expected number of total calls self.assertEquals(22, provider._request.call_count) provider._request.reset_mock() provider.zone_records = Mock(return_value=[ { "id": "fc12ab34cd5611334422ab3322997653", "type": "A", "name": "www.unit.tests", "content": "1.2.3.4", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997654", "type": "A", "name": "www.unit.tests", "content": "2.2.3.4", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997655", "type": "A", "name": "nc.unit.tests", "content": "3.2.3.4", "proxiable": True, "proxied": False, "ttl": 120, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997655", "type": "A", "name": "ttl.unit.tests", "content": "4.2.3.4", "proxiable": True, "proxied": False, "ttl": 600, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, ]) # we don't care about the POST/create return values provider._request.return_value = {} provider._request.side_effect = None wanted = Zone('unit.tests.', []) wanted.add_record( Record.new( wanted, 'nc', { 'ttl': 60, # TTL is below their min 'type': 'A', 'value': '3.2.3.4' })) wanted.add_record( Record.new( wanted, 'ttl', { 'ttl': 300, # TTL change 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) # only see the delete & ttl update, below min-ttl is filtered out self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) self.assertTrue(plan.exists) # creates a the new value and then deletes all the old provider._request.assert_has_calls([ call('PUT', '/zones/42/dns_records/' 'fc12ab34cd5611334422ab3322997655', data={ 'content': '3.2.3.4', 'type': 'A', 'name': 'ttl.unit.tests', 'proxied': False, 'ttl': 300 }), call( 'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' 'dns_records/fc12ab34cd5611334422ab3322997653'), call( 'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' 'dns_records/fc12ab34cd5611334422ab3322997654') ]) def test_update_add_swap(self): provider = CloudflareProvider('test', 'email', 'token') provider.zone_records = Mock(return_value=[ { "id": "fc12ab34cd5611334422ab3322997653", "type": "A", "name": "a.unit.tests", "content": "1.1.1.1", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997654", "type": "A", "name": "a.unit.tests", "content": "2.2.2.2", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, ]) provider._request = Mock() provider._request.side_effect = [ self.empty, # no zones { 'result': { 'id': 42, } }, # zone create None, None, None, None, ] # Add something and delete something zone = Zone('unit.tests.', []) existing = Record.new( zone, 'a', { 'ttl': 300, 'type': 'A', # This matches the zone data above, one to swap, one to leave 'values': ['1.1.1.1', '2.2.2.2'], }) new = Record.new( zone, 'a', { 'ttl': 300, 'type': 'A', # This leaves one, swaps ones, and adds one 'values': ['2.2.2.2', '3.3.3.3', '4.4.4.4'], }) change = Update(existing, new) plan = Plan(zone, zone, [change], True) provider._apply(plan) # get the list of zones, create a zone, add some records, update # something, and delete something provider._request.assert_has_calls([ call('GET', '/zones', params={'page': 1}), call('POST', '/zones', data={ 'jump_start': False, 'name': 'unit.tests' }), call('POST', '/zones/42/dns_records', data={ 'content': '4.4.4.4', 'type': 'A', 'name': 'a.unit.tests', 'proxied': False, 'ttl': 300 }), call('PUT', '/zones/42/dns_records/' 'fc12ab34cd5611334422ab3322997654', data={ 'content': '2.2.2.2', 'type': 'A', 'name': 'a.unit.tests', 'proxied': False, 'ttl': 300 }), call('PUT', '/zones/42/dns_records/' 'fc12ab34cd5611334422ab3322997653', data={ 'content': '3.3.3.3', 'type': 'A', 'name': 'a.unit.tests', 'proxied': False, 'ttl': 300 }), ]) def test_update_delete(self): # We need another run so that we can delete, we can't both add and # delete in one go b/c of swaps provider = CloudflareProvider('test', 'email', 'token') provider.zone_records = Mock(return_value=[ { "id": "fc12ab34cd5611334422ab3322997653", "type": "NS", "name": "unit.tests", "content": "ns1.foo.bar", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997654", "type": "NS", "name": "unit.tests", "content": "ns2.foo.bar", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, ]) provider._request = Mock() provider._request.side_effect = [ self.empty, # no zones { 'result': { 'id': 42, } }, # zone create None, None, ] # Add something and delete something zone = Zone('unit.tests.', []) existing = Record.new( zone, '', { 'ttl': 300, 'type': 'NS', # This matches the zone data above, one to delete, one to leave 'values': ['ns1.foo.bar.', 'ns2.foo.bar.'], }) new = Record.new( zone, '', { 'ttl': 300, 'type': 'NS', # This leaves one and deletes one 'value': 'ns2.foo.bar.', }) change = Update(existing, new) plan = Plan(zone, zone, [change], True) provider._apply(plan) # Get zones, create zone, create a record, delete a record provider._request.assert_has_calls([ call('GET', '/zones', params={'page': 1}), call('POST', '/zones', data={ 'jump_start': False, 'name': 'unit.tests' }), call('PUT', '/zones/42/dns_records/' 'fc12ab34cd5611334422ab3322997654', data={ 'content': 'ns2.foo.bar.', 'type': 'NS', 'name': 'unit.tests', 'ttl': 300 }), call('DELETE', '/zones/42/dns_records/' 'fc12ab34cd5611334422ab3322997653') ]) def test_srv(self): provider = CloudflareProvider('test', 'email', 'token') zone = Zone('unit.tests.', []) # SRV record not under a sub-domain srv_record = Record.new( zone, '_example._tcp', { 'ttl': 300, 'type': 'SRV', 'value': { 'port': 1234, 'priority': 0, 'target': 'nc.unit.tests.', 'weight': 5 } }) # SRV record under a sub-domain srv_record_with_sub = Record.new( zone, '_example._tcp.sub', { 'ttl': 300, 'type': 'SRV', 'value': { 'port': 1234, 'priority': 0, 'target': 'nc.unit.tests.', 'weight': 5 } }) srv_record_contents = provider._gen_data(srv_record) srv_record_with_sub_contents = provider._gen_data(srv_record_with_sub) self.assertEquals( { 'name': '_example._tcp.unit.tests', 'ttl': 300, 'type': 'SRV', 'data': { 'service': '_example', 'proto': '_tcp', 'name': 'unit.tests.', 'priority': 0, 'weight': 5, 'port': 1234, 'target': 'nc.unit.tests' } }, list(srv_record_contents)[0]) self.assertEquals( { 'name': '_example._tcp.sub.unit.tests', 'ttl': 300, 'type': 'SRV', 'data': { 'service': '_example', 'proto': '_tcp', 'name': 'sub', 'priority': 0, 'weight': 5, 'port': 1234, 'target': 'nc.unit.tests' } }, list(srv_record_with_sub_contents)[0]) def test_alias(self): provider = CloudflareProvider('test', 'email', 'token') # A CNAME for us to transform to ALIAS provider.zone_records = Mock(return_value=[ { "id": "fc12ab34cd5611334422ab3322997642", "type": "CNAME", "name": "unit.tests", "content": "www.unit.tests", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, ]) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(1, len(zone.records)) record = list(zone.records)[0] self.assertEquals('', record.name) self.assertEquals('unit.tests.', record.fqdn) self.assertEquals('ALIAS', record._type) self.assertEquals('www.unit.tests.', record.value) # Make sure we transform back to CNAME going the other way contents = provider._gen_data(record) self.assertEquals( { 'content': 'www.unit.tests.', 'name': 'unit.tests', 'proxied': False, 'ttl': 300, 'type': 'CNAME' }, list(contents)[0]) def test_gen_key(self): provider = CloudflareProvider('test', 'email', 'token') for expected, data in ( ('foo.bar.com.', { 'content': 'foo.bar.com.', 'type': 'CNAME', }), ('10 foo.bar.com.', { 'content': 'foo.bar.com.', 'priority': 10, 'type': 'MX', }), ('0 tag some-value', { 'data': { 'flags': 0, 'tag': 'tag', 'value': 'some-value', }, 'type': 'CAA', }), ('42 100 thing-were-pointed.at 101', { 'data': { 'port': 42, 'priority': 100, 'target': 'thing-were-pointed.at', 'weight': 101, }, 'type': 'SRV', }), ): self.assertEqual(expected, provider._gen_key(data)) def test_cdn(self): provider = CloudflareProvider('test', 'email', 'token', True) # A CNAME for us to transform to ALIAS provider.zone_records = Mock(return_value=[ { "id": "fc12ab34cd5611334422ab3322997642", "type": "CNAME", "name": "cname.unit.tests", "content": "www.unit.tests", "proxiable": True, "proxied": True, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997642", "type": "A", "name": "a.unit.tests", "content": "1.1.1.1", "proxiable": True, "proxied": True, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997642", "type": "A", "name": "a.unit.tests", "content": "1.1.1.2", "proxiable": True, "proxied": True, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997642", "type": "A", "name": "multi.unit.tests", "content": "1.1.1.3", "proxiable": True, "proxied": True, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997642", "type": "AAAA", "name": "multi.unit.tests", "content": "::1", "proxiable": True, "proxied": True, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, ]) zone = Zone('unit.tests.', []) provider.populate(zone) # the two A records get merged into one CNAME record pointing to # the CDN. self.assertEquals(3, len(zone.records)) record = list(zone.records)[0] self.assertEquals('multi', record.name) self.assertEquals('multi.unit.tests.', record.fqdn) self.assertEquals('CNAME', record._type) self.assertEquals('multi.unit.tests.cdn.cloudflare.net.', record.value) record = list(zone.records)[1] self.assertEquals('cname', record.name) self.assertEquals('cname.unit.tests.', record.fqdn) self.assertEquals('CNAME', record._type) self.assertEquals('cname.unit.tests.cdn.cloudflare.net.', record.value) record = list(zone.records)[2] self.assertEquals('a', record.name) self.assertEquals('a.unit.tests.', record.fqdn) self.assertEquals('CNAME', record._type) self.assertEquals('a.unit.tests.cdn.cloudflare.net.', record.value) # CDN enabled records can't be updated, we don't know the real values # never point a Cloudflare record to itself. wanted = Zone('unit.tests.', []) wanted.add_record( Record.new( wanted, 'cname', { 'ttl': 300, 'type': 'CNAME', 'value': 'change.unit.tests.cdn.cloudflare.net.' })) wanted.add_record( Record.new( wanted, 'new', { 'ttl': 300, 'type': 'CNAME', 'value': 'new.unit.tests.cdn.cloudflare.net.' })) wanted.add_record( Record.new(wanted, 'created', { 'ttl': 300, 'type': 'CNAME', 'value': 'www.unit.tests.' })) plan = provider.plan(wanted) self.assertEquals(1, len(plan.changes)) def test_cdn_alias(self): provider = CloudflareProvider('test', 'email', 'token', True) # A CNAME for us to transform to ALIAS provider.zone_records = Mock(return_value=[ { "id": "fc12ab34cd5611334422ab3322997642", "type": "CNAME", "name": "unit.tests", "content": "www.unit.tests", "proxiable": True, "proxied": True, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, ]) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(1, len(zone.records)) record = list(zone.records)[0] self.assertEquals('', record.name) self.assertEquals('unit.tests.', record.fqdn) self.assertEquals('ALIAS', record._type) self.assertEquals('unit.tests.cdn.cloudflare.net.', record.value) # CDN enabled records can't be updated, we don't know the real values # never point a Cloudflare record to itself. wanted = Zone('unit.tests.', []) wanted.add_record( Record.new( wanted, '', { 'ttl': 300, 'type': 'ALIAS', 'value': 'change.unit.tests.cdn.cloudflare.net.' })) plan = provider.plan(wanted) self.assertEquals(False, hasattr(plan, 'changes')) def test_unproxiabletype_recordfor_returnsrecordwithnocloudflare(self): provider = CloudflareProvider('test', 'email', 'token') name = "unit.tests" _type = "NS" zone_records = [{ "id": "fc12ab34cd5611334422ab3322997654", "type": _type, "name": name, "content": "ns2.foo.bar", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }] provider.zone_records = Mock(return_value=zone_records) zone = Zone('unit.tests.', []) provider.populate(zone) record = provider._record_for(zone, name, _type, zone_records, False) self.assertFalse('cloudflare' in record._octodns) def test_proxiabletype_recordfor_retrecordwithcloudflareunproxied(self): provider = CloudflareProvider('test', 'email', 'token') name = "multi.unit.tests" _type = "AAAA" zone_records = [{ "id": "fc12ab34cd5611334422ab3322997642", "type": _type, "name": name, "content": "::1", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }] provider.zone_records = Mock(return_value=zone_records) zone = Zone('unit.tests.', []) provider.populate(zone) record = provider._record_for(zone, name, _type, zone_records, False) self.assertFalse(record._octodns['cloudflare']['proxied']) def test_proxiabletype_recordfor_returnsrecordwithcloudflareproxied(self): provider = CloudflareProvider('test', 'email', 'token') name = "multi.unit.tests" _type = "AAAA" zone_records = [{ "id": "fc12ab34cd5611334422ab3322997642", "type": _type, "name": name, "content": "::1", "proxiable": True, "proxied": True, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }] provider.zone_records = Mock(return_value=zone_records) zone = Zone('unit.tests.', []) provider.populate(zone) record = provider._record_for(zone, name, _type, zone_records, False) self.assertTrue(record._octodns['cloudflare']['proxied']) def test_proxiedrecordandnewttl_includechange_returnsfalse(self): provider = CloudflareProvider('test', 'email', 'token') zone = Zone('unit.tests.', []) existing = set_record_proxied_flag( Record.new(zone, 'a', { 'ttl': 1, 'type': 'A', 'values': ['1.1.1.1', '2.2.2.2'] }), True) new = Record.new(zone, 'a', { 'ttl': 300, 'type': 'A', 'values': ['1.1.1.1', '2.2.2.2'] }) change = Update(existing, new) include_change = provider._include_change(change) self.assertFalse(include_change) def test_unproxiabletype_gendata_returnsnoproxied(self): provider = CloudflareProvider('test', 'email', 'token') zone = Zone('unit.tests.', []) record = Record.new(zone, 'a', { 'ttl': 3600, 'type': 'NS', 'value': 'ns1.unit.tests.' }) data = provider._gen_data(record).next() self.assertFalse('proxied' in data) def test_proxiabletype_gendata_returnsunproxied(self): provider = CloudflareProvider('test', 'email', 'token') zone = Zone('unit.tests.', []) record = set_record_proxied_flag( Record.new(zone, 'a', { 'ttl': 300, 'type': 'A', 'value': '1.2.3.4' }), False) data = provider._gen_data(record).next() self.assertFalse(data['proxied']) def test_proxiabletype_gendata_returnsproxied(self): provider = CloudflareProvider('test', 'email', 'token') zone = Zone('unit.tests.', []) record = set_record_proxied_flag( Record.new(zone, 'a', { 'ttl': 300, 'type': 'A', 'value': '1.2.3.4' }), True) data = provider._gen_data(record).next() self.assertTrue(data['proxied']) def test_createrecord_extrachanges_returnsemptylist(self): provider = CloudflareProvider('test', 'email', 'token') provider.zone_records = Mock(return_value=[]) existing = Zone('unit.tests.', []) provider.populate(existing) provider.zone_records = Mock(return_value=[{ "id": "fc12ab34cd5611334422ab3322997642", "type": "CNAME", "name": "a.unit.tests", "content": "www.unit.tests", "proxiable": True, "proxied": True, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }]) desired = Zone('unit.tests.', []) provider.populate(desired) changes = existing.changes(desired, provider) extra_changes = provider._extra_changes(existing, desired, changes) self.assertFalse(extra_changes) def test_updaterecord_extrachanges_returnsemptylist(self): provider = CloudflareProvider('test', 'email', 'token') provider.zone_records = Mock(return_value=[{ "id": "fc12ab34cd5611334422ab3322997642", "type": "CNAME", "name": "a.unit.tests", "content": "www.unit.tests", "proxiable": True, "proxied": True, "ttl": 120, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }]) existing = Zone('unit.tests.', []) provider.populate(existing) provider.zone_records = Mock(return_value=[{ "id": "fc12ab34cd5611334422ab3322997642", "type": "CNAME", "name": "a.unit.tests", "content": "www.unit.tests", "proxiable": True, "proxied": True, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }]) desired = Zone('unit.tests.', []) provider.populate(desired) changes = existing.changes(desired, provider) extra_changes = provider._extra_changes(existing, desired, changes) self.assertFalse(extra_changes) def test_deleterecord_extrachanges_returnsemptylist(self): provider = CloudflareProvider('test', 'email', 'token') provider.zone_records = Mock(return_value=[{ "id": "fc12ab34cd5611334422ab3322997642", "type": "CNAME", "name": "a.unit.tests", "content": "www.unit.tests", "proxiable": True, "proxied": True, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }]) existing = Zone('unit.tests.', []) provider.populate(existing) provider.zone_records = Mock(return_value=[]) desired = Zone('unit.tests.', []) provider.populate(desired) changes = existing.changes(desired, provider) extra_changes = provider._extra_changes(existing, desired, changes) self.assertFalse(extra_changes) def test_proxify_extrachanges_returnsupdatelist(self): provider = CloudflareProvider('test', 'email', 'token') provider.zone_records = Mock(return_value=[{ "id": "fc12ab34cd5611334422ab3322997642", "type": "CNAME", "name": "a.unit.tests", "content": "www.unit.tests", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }]) existing = Zone('unit.tests.', []) provider.populate(existing) provider.zone_records = Mock(return_value=[{ "id": "fc12ab34cd5611334422ab3322997642", "type": "CNAME", "name": "a.unit.tests", "content": "www.unit.tests", "proxiable": True, "proxied": True, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }]) desired = Zone('unit.tests.', []) provider.populate(desired) changes = existing.changes(desired, provider) extra_changes = provider._extra_changes(existing, desired, changes) self.assertEquals(1, len(extra_changes)) self.assertFalse( extra_changes[0].existing._octodns['cloudflare']['proxied']) self.assertTrue(extra_changes[0].new._octodns['cloudflare']['proxied']) def test_unproxify_extrachanges_returnsupdatelist(self): provider = CloudflareProvider('test', 'email', 'token') provider.zone_records = Mock(return_value=[{ "id": "fc12ab34cd5611334422ab3322997642", "type": "CNAME", "name": "a.unit.tests", "content": "www.unit.tests", "proxiable": True, "proxied": True, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }]) existing = Zone('unit.tests.', []) provider.populate(existing) provider.zone_records = Mock(return_value=[{ "id": "fc12ab34cd5611334422ab3322997642", "type": "CNAME", "name": "a.unit.tests", "content": "www.unit.tests", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }]) desired = Zone('unit.tests.', []) provider.populate(desired) changes = existing.changes(desired, provider) extra_changes = provider._extra_changes(existing, desired, changes) self.assertEquals(1, len(extra_changes)) self.assertTrue( extra_changes[0].existing._octodns['cloudflare']['proxied']) self.assertFalse( extra_changes[0].new._octodns['cloudflare']['proxied'])
def test_apply(self): provider = DigitalOceanProvider('test', 'token') resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) domain_after_creation = { "domain_records": [{ "id": 11189874, "type": "NS", "name": "@", "data": "ns1.digitalocean.com", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }, { "id": 11189875, "type": "NS", "name": "@", "data": "ns2.digitalocean.com", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }, { "id": 11189876, "type": "NS", "name": "@", "data": "ns3.digitalocean.com", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }, { "id": 11189877, "type": "A", "name": "@", "data": "192.0.2.1", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }], "links": {}, "meta": { "total": 4 } } # non-existent domain, create everything resp.json.side_effect = [ DigitalOceanClientNotFound, # no zone in populate DigitalOceanClientNotFound, # no domain during apply domain_after_creation ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no unsupported n = len(self.expected.records) - 8 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) self.assertFalse(plan.exists) provider._client._request.assert_has_calls([ # created the domain call('POST', '/domains', data={ 'ip_address': '192.0.2.1', 'name': 'unit.tests' }), # get all records in newly created zone call('GET', '/domains/unit.tests/records', {'page': 1}), # delete the initial A record call('DELETE', '/domains/unit.tests/records/11189877'), # created at least some of the record with expected data call('POST', '/domains/unit.tests/records', data={ 'data': '1.2.3.4', 'name': '@', 'ttl': 300, 'type': 'A' }), call('POST', '/domains/unit.tests/records', data={ 'data': '1.2.3.5', 'name': '@', 'ttl': 300, 'type': 'A' }), call('POST', '/domains/unit.tests/records', data={ 'data': 'ca.unit.tests.', 'flags': 0, 'name': '@', 'tag': 'issue', 'ttl': 3600, 'type': 'CAA' }), call('POST', '/domains/unit.tests/records', data={ 'name': '_srv._tcp', 'weight': 20, 'data': 'foo-1.unit.tests.', 'priority': 10, 'ttl': 600, 'type': 'SRV', 'port': 30 }), ]) self.assertEquals(24, provider._client._request.call_count) provider._client._request.reset_mock() # delete 1 and update 1 provider._client.records = Mock(return_value=[{ 'id': 11189897, 'name': 'www', 'data': '1.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189898, 'name': 'www', 'data': '2.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189899, 'name': 'ttl', 'data': '3.2.3.4', 'ttl': 600, 'type': 'A', }]) # Domain exists, we don't care about return resp.json.side_effect = ['{}'] wanted = Zone('unit.tests.', []) wanted.add_record( Record.new(wanted, 'ttl', { 'ttl': 300, 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) self.assertTrue(plan.exists) self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) # recreate for update, and delete for the 2 parts of the other provider._client._request.assert_has_calls([ call('POST', '/domains/unit.tests/records', data={ 'data': '3.2.3.4', 'type': 'A', 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/domains/unit.tests/records/11189899'), call('DELETE', '/domains/unit.tests/records/11189897'), call('DELETE', '/domains/unit.tests/records/11189898') ], any_order=True)
def test_update_delete(self): # We need another run so that we can delete, we can't both add and # delete in one go b/c of swaps provider = CloudflareProvider('test', 'email', 'token') provider.zone_records = Mock(return_value=[ { "id": "fc12ab34cd5611334422ab3322997653", "type": "NS", "name": "unit.tests", "content": "ns1.foo.bar", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997654", "type": "NS", "name": "unit.tests", "content": "ns2.foo.bar", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, ]) provider._request = Mock() provider._request.side_effect = [ self.empty, # no zones { 'result': { 'id': 42, } }, # zone create None, None, ] # Add something and delete something zone = Zone('unit.tests.', []) existing = Record.new( zone, '', { 'ttl': 300, 'type': 'NS', # This matches the zone data above, one to delete, one to leave 'values': ['ns1.foo.bar.', 'ns2.foo.bar.'], }) new = Record.new( zone, '', { 'ttl': 300, 'type': 'NS', # This leaves one and deletes one 'value': 'ns2.foo.bar.', }) change = Update(existing, new) plan = Plan(zone, zone, [change], True) provider._apply(plan) # Get zones, create zone, create a record, delete a record provider._request.assert_has_calls([ call('GET', '/zones', params={'page': 1}), call('POST', '/zones', data={ 'jump_start': False, 'name': 'unit.tests' }), call('PUT', '/zones/42/dns_records/' 'fc12ab34cd5611334422ab3322997654', data={ 'content': 'ns2.foo.bar.', 'type': 'NS', 'name': 'unit.tests', 'ttl': 300 }), call('DELETE', '/zones/42/dns_records/' 'fc12ab34cd5611334422ab3322997653') ])
def test_gen_data(self): provider = _get_provider() zone = Zone('unit.tests.', []) for name, _type, expected_path, expected_payload, expected_record in ( # A ('a', 'A', '/v2/zones/unit.tests./rrsets/A/a.unit.tests.', { 'ttl': 60, 'rdata': ['1.2.3.4'] }, Record.new(zone, 'a', { 'ttl': 60, 'type': 'A', 'values': ['1.2.3.4'] })), ('a', 'A', '/v2/zones/unit.tests./rrsets/A/a.unit.tests.', { 'ttl': 60, 'rdata': ['1.2.3.4', '5.6.7.8'], 'profile': { '@context': 'http://schemas.ultradns.com/RDPool.jsonschema', 'order': 'FIXED', 'description': 'a.unit.tests.' } }, Record.new(zone, 'a', { 'ttl': 60, 'type': 'A', 'values': ['1.2.3.4', '5.6.7.8'] })), # AAAA ('aaaa', 'AAAA', '/v2/zones/unit.tests./rrsets/AAAA/aaaa.unit.tests.', { 'ttl': 60, 'rdata': ['::1'] }, Record.new(zone, 'aaaa', { 'ttl': 60, 'type': 'AAAA', 'values': ['::1'] })), ('aaaa', 'AAAA', '/v2/zones/unit.tests./rrsets/AAAA/aaaa.unit.tests.', { 'ttl': 60, 'rdata': ['::1', '::2'], 'profile': { '@context': 'http://schemas.ultradns.com/RDPool.jsonschema', 'order': 'FIXED', 'description': 'aaaa.unit.tests.' } }, Record.new(zone, 'aaaa', { 'ttl': 60, 'type': 'AAAA', 'values': ['::1', '::2'] })), # CAA ('caa', 'CAA', '/v2/zones/unit.tests./rrsets/CAA/caa.unit.tests.', { 'ttl': 60, 'rdata': ['0 issue foo.com'] }, Record.new( zone, 'caa', { 'ttl': 60, 'type': 'CAA', 'values': [{ 'flags': 0, 'tag': 'issue', 'value': 'foo.com' }] })), # CNAME ('cname', 'CNAME', '/v2/zones/unit.tests./rrsets/CNAME/cname.unit.tests.', { 'ttl': 60, 'rdata': ['netflix.com.'] }, Record.new(zone, 'cname', { 'ttl': 60, 'type': 'CNAME', 'value': 'netflix.com.' })), # MX ('mx', 'MX', '/v2/zones/unit.tests./rrsets/MX/mx.unit.tests.', { 'ttl': 60, 'rdata': ['1 mx1.unit.tests.', '1 mx2.unit.tests.'] }, Record.new( zone, 'mx', { 'ttl': 60, 'type': 'MX', 'values': [{ 'preference': 1, 'exchange': 'mx1.unit.tests.' }, { 'preference': 1, 'exchange': 'mx2.unit.tests.' }] })), # NS ('ns', 'NS', '/v2/zones/unit.tests./rrsets/NS/ns.unit.tests.', { 'ttl': 60, 'rdata': ['ns1.unit.tests.', 'ns2.unit.tests.'] }, Record.new( zone, 'ns', { 'ttl': 60, 'type': 'NS', 'values': ['ns1.unit.tests.', 'ns2.unit.tests.'] })), # PTR ('ptr', 'PTR', '/v2/zones/unit.tests./rrsets/PTR/ptr.unit.tests.', { 'ttl': 60, 'rdata': ['a.unit.tests.'] }, Record.new(zone, 'ptr', { 'ttl': 60, 'type': 'PTR', 'value': 'a.unit.tests.' })), # SPF ('spf', 'SPF', '/v2/zones/unit.tests./rrsets/SPF/spf.unit.tests.', { 'ttl': 60, 'rdata': ['v=spf1 -all'] }, Record.new(zone, 'spf', { 'ttl': 60, 'type': 'SPF', 'values': ['v=spf1 -all'] })), # SRV ('_srv._tcp', 'SRV', '/v2/zones/unit.tests./rrsets/SRV/_srv._tcp.unit.tests.', { 'ttl': 60, 'rdata': ['10 20 443 target.unit.tests.'] }, Record.new( zone, '_srv._tcp', { 'ttl': 60, 'type': 'SRV', 'values': [{ 'priority': 10, 'weight': 20, 'port': 443, 'target': 'target.unit.tests.' }] })), # TXT ('txt', 'TXT', '/v2/zones/unit.tests./rrsets/TXT/txt.unit.tests.', { 'ttl': 60, 'rdata': ['abc', 'def'] }, Record.new(zone, 'txt', { 'ttl': 60, 'type': 'TXT', 'values': ['abc', 'def'] })), # ALIAS ('', 'ALIAS', '/v2/zones/unit.tests./rrsets/APEXALIAS/unit.tests.', { 'ttl': 60, 'rdata': ['target.unit.tests.'] }, Record.new(zone, '', { 'ttl': 60, 'type': 'ALIAS', 'value': 'target.unit.tests.' })), ): # Validate path and payload based on record meet expectations path, payload = provider._gen_data(expected_record) self.assertEqual(expected_path, path) self.assertEqual(expected_payload, payload) # Use generator for record and confirm the output matches rec = provider._record_for(zone, name, _type, expected_payload, False) path, payload = provider._gen_data(rec) self.assertEqual(expected_path, path) self.assertEqual(expected_payload, payload)