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)
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.'], })) expected.add( Record.new( zone, '', { 'ttl': 40, 'type': 'CAA', 'value': { 'flags': 0, 'tag': 'issue', 'value': 'ca.unit.tests', }, })) nsone_records = [{ 'type': 'A', 'ttl': 32, 'short_answers': ['1.2.3.4'], 'domain': 'unit.tests.', }, { 'type': 'A', 'ttl': 33, 'short_answers': ['1.2.3.4', '1.2.3.5'], 'domain': 'foo.unit.tests.', }, { 'type': '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('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.', []) for r in self.expected: desired.add_record(r) plan = provider.plan(desired) # everything except the root NS expected_n = len(self.expected) - 1 self.assertEquals(expected_n, len(plan.changes)) # Fails, general error load_mock.reset_mock() create_mock.reset_mock() load_mock.side_effect = ResourceException('boom') with self.assertRaises(ResourceException) as ctx: provider.apply(plan) self.assertEquals(load_mock.side_effect, ctx.exception) # Fails, bad auth load_mock.reset_mock() create_mock.reset_mock() load_mock.side_effect = \ ResourceException('server error: zone not found') create_mock.side_effect = AuthException('unauthorized') with self.assertRaises(AuthException) as ctx: provider.apply(plan) self.assertEquals(create_mock.side_effect, ctx.exception) # non-existant zone, create load_mock.reset_mock() create_mock.reset_mock() load_mock.side_effect = \ ResourceException('server error: zone not found') # ugh, need a mock zone with a mock prop since we're using getattr, we # can actually control side effects on `meth` with that. mock_zone = Mock() mock_zone.add_SRV = Mock() mock_zone.add_SRV.side_effect = [ RateLimitException('boo', period=0), None, ] create_mock.side_effect = [mock_zone] got_n = provider.apply(plan) self.assertEquals(expected_n, got_n) # Update & delete load_mock.reset_mock() create_mock.reset_mock() nsone_zone = DummyZone(self.nsone_records + [{ 'type': 'A', 'ttl': 42, 'short_answers': ['9.9.9.9'], 'domain': 'delete-me.unit.tests.', }]) nsone_zone.data['records'][0]['short_answers'][0] = '2.2.2.2' nsone_zone.loadRecord = Mock() 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'])
def test_apply(self): # Create provider with sandbox enabled provider = DnsMadeEasyProvider('test', 'api', 'secret', True) resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) with open('tests/fixtures/dnsmadeeasy-domains.json') as fh: domains = json.load(fh) # non-existent domain, create everything resp.json.side_effect = [ DnsMadeEasyClientNotFound, # no zone in populate DnsMadeEasyClientNotFound, # no domain during apply domains ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no unsupported n = len(self.expected.records) - 10 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) provider._client._request.assert_has_calls([ # created the domain call('POST', '/', data={'name': 'unit.tests'}), # get all domains to build the cache call('GET', '/'), # created at least some of the record with expected data call('POST', '/123123/records', data={ 'type': 'A', 'name': '', 'value': '1.2.3.4', 'ttl': 300}), call('POST', '/123123/records', data={ 'type': 'A', 'name': '', 'value': '1.2.3.5', 'ttl': 300}), call('POST', '/123123/records', data={ 'type': 'ANAME', 'name': '', 'value': 'aname.unit.tests.', 'ttl': 1800}), call('POST', '/123123/records', data={ 'name': '', 'value': 'ca.unit.tests', 'issuerCritical': 0, 'caaType': 'issue', 'ttl': 3600, 'type': 'CAA'}), call('POST', '/123123/records', data={ 'name': '_srv._tcp', 'weight': 20, 'value': 'foo-1.unit.tests.', 'priority': 10, 'ttl': 600, 'type': 'SRV', 'port': 30 }), ]) 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', 'value': '1.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189898, 'name': 'www', 'value': '2.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189899, 'name': 'ttl', 'value': '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', '/123123/records', data={ 'value': '3.2.3.4', 'type': 'A', 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/123123/records/11189899'), call('DELETE', '/123123/records/11189897'), call('DELETE', '/123123/records/11189898') ], any_order=True)
def test_apply(self): provider = HetznerProvider('test', 'token') resp = Mock() resp.json = Mock() provider._client._do = Mock(return_value=resp) domain_after_creation = {'zone': { 'id': 'unit.tests', 'name': 'unit.tests', 'ttl': 3600, }} # non-existent domain, create everything resp.json.side_effect = [ HetznerClientNotFound, # no zone in populate HetznerClientNotFound, # no zone during apply 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) provider._client._do.assert_has_calls([ # created the zone call('POST', '/zones', None, { 'name': 'unit.tests', 'ttl': None, }), # created all the records with their expected data call('POST', '/records', data={ 'name': '@', 'ttl': 300, 'type': 'A', 'value': '1.2.3.4', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': '@', 'ttl': 300, 'type': 'A', 'value': '1.2.3.5', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': '@', 'ttl': 3600, 'type': 'CAA', 'value': '0 issue "ca.unit.tests"', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': '_imap._tcp', 'ttl': 600, 'type': 'SRV', 'value': '0 0 0 .', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': '_pop3._tcp', 'ttl': 600, 'type': 'SRV', 'value': '0 0 0 .', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': '_srv._tcp', 'ttl': 600, 'type': 'SRV', 'value': '10 20 30 foo-1.unit.tests.', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': '_srv._tcp', 'ttl': 600, 'type': 'SRV', 'value': '12 20 30 foo-2.unit.tests.', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': 'aaaa', 'ttl': 600, 'type': 'AAAA', 'value': '2601:644:500:e210:62f8:1dff:feb8:947a', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': 'cname', 'ttl': 300, 'type': 'CNAME', 'value': 'unit.tests.', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': 'included', 'ttl': 3600, 'type': 'CNAME', 'value': 'unit.tests.', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': 'mx', 'ttl': 300, 'type': 'MX', 'value': '10 smtp-4.unit.tests.', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': 'mx', 'ttl': 300, 'type': 'MX', 'value': '20 smtp-2.unit.tests.', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': 'mx', 'ttl': 300, 'type': 'MX', 'value': '30 smtp-3.unit.tests.', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': 'mx', 'ttl': 300, 'type': 'MX', 'value': '40 smtp-1.unit.tests.', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': 'sub', 'ttl': 3600, 'type': 'NS', 'value': '6.2.3.4.', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': 'sub', 'ttl': 3600, 'type': 'NS', 'value': '7.2.3.4.', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': 'txt', 'ttl': 600, 'type': 'TXT', 'value': 'Bah bah black sheep', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': 'txt', 'ttl': 600, 'type': 'TXT', 'value': 'have you any wool.', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': 'txt', 'ttl': 600, 'type': 'TXT', 'value': 'v=DKIM1;k=rsa;s=email;h=sha256;' 'p=A/kinda+of/long/string+with+numb3rs', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': 'www', 'ttl': 300, 'type': 'A', 'value': '2.2.3.6', 'zone_id': 'unit.tests', }), call('POST', '/records', data={ 'name': 'www.sub', 'ttl': 300, 'type': 'A', 'value': '2.2.3.6', 'zone_id': 'unit.tests', }), ]) self.assertEquals(24, provider._client._do.call_count) provider._client._do.reset_mock() # delete 1 and update 1 provider._client.zone_get = Mock(return_value={ 'id': 'unit.tests', 'name': 'unit.tests', 'ttl': 3600, }) provider._client.zone_records_get = Mock(return_value=[ { 'type': 'A', 'id': 'one', 'created': '0000-00-00T00:00:00Z', 'modified': '0000-00-00T00:00:00Z', 'zone_id': 'unit.tests', 'name': 'www', 'value': '1.2.3.4', 'ttl': 300, }, { 'type': 'A', 'id': 'two', 'created': '0000-00-00T00:00:00Z', 'modified': '0000-00-00T00:00:00Z', 'zone_id': 'unit.tests', 'name': 'www', 'value': '2.2.3.4', 'ttl': 300, }, { 'type': 'A', 'id': 'three', 'created': '0000-00-00T00:00:00Z', 'modified': '0000-00-00T00:00:00Z', 'zone_id': 'unit.tests', 'name': 'ttl', 'value': '3.2.3.4', 'ttl': 600, }, ]) # 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._do.assert_has_calls([ call('POST', '/records', data={ 'name': 'ttl', 'ttl': 300, 'type': 'A', 'value': '3.2.3.4', 'zone_id': 'unit.tests', }), call('DELETE', '/records/one'), call('DELETE', '/records/two'), call('DELETE', '/records/three'), ], any_order=True)
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(16, 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(16, 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, lenient=True) self.assertEquals(set([ Record.new(zone, '', { 'ttl': 3600, 'type': 'SSHFP', 'values': [] }, lenient=True), Record.new(zone, '_srv._tcp', { 'ttl': 600, 'type': 'SRV', 'values': [] }, lenient=True), Record.new(zone, 'naptr', { 'ttl': 600, 'type': 'NAPTR', 'values': [] }, lenient=True), ]), 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, no excluded n = len(self.expected.records) - 3 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={'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(28, 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.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('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)
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_apply(self): provider = ConstellixProvider('test', 'api', 'secret') resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) # non-existent domain, create everything resp.json.side_effect = [ [], # no domains returned during populate [{ 'id': 123123, 'name': 'unit.tests' }], # domain created in apply ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no unsupported n = len(self.expected.records) - 6 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) provider._client._request.assert_has_calls([ # get all domains to build the cache call('GET', ''), # created the domain call('POST', '/', data={'names': ['unit.tests']}) ]) # These two checks are broken up so that ordering doesn't break things. # Python3 doesn't make the calls in a consistent order so different # things follow the GET / on different runs provider._client._request.assert_has_calls([ call('POST', '/123123/records/SRV', data={ 'roundRobin': [{ 'priority': 10, 'weight': 20, 'value': 'foo-1.unit.tests.', 'port': 30 }, { 'priority': 12, 'weight': 20, 'value': 'foo-2.unit.tests.', 'port': 30 }], 'name': '_srv._tcp', 'ttl': 600, }), ]) self.assertEquals(18, provider._client._request.call_count) provider._client._request.reset_mock() provider._client.records = Mock(return_value=[ { 'id': 11189897, 'type': 'A', 'name': 'www', 'ttl': 300, 'value': [ '1.2.3.4', '2.2.3.4', ] }, { 'id': 11189898, 'type': 'A', 'name': 'ttl', 'ttl': 600, 'value': [ '3.2.3.4' ] }, { 'id': 11189899, 'type': 'ALIAS', 'name': 'alias', 'ttl': 600, 'value': [{ 'value': 'aname.unit.tests.' }] } ]) # 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(3, len(plan.changes)) self.assertEquals(3, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._client._request.assert_has_calls([ call('POST', '/123123/records/A', data={ 'roundRobin': [{ 'value': '3.2.3.4' }], 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/123123/records/A/11189897'), call('DELETE', '/123123/records/A/11189898'), call('DELETE', '/123123/records/ANAME/11189899') ], any_order=True)
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 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))
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))
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(
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_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_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)
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_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_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])
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_normal(self): got = Zone('example.com.', []) self.source.populate(got) self.assertEquals(11, len(got.records)) expected = Zone('example.com.', []) for name, data in ( ('', { 'type': 'A', 'ttl': 30, 'values': ['10.2.3.4', '10.2.3.5'], }), ('sub', { 'type': 'NS', 'ttl': 30, 'values': ['ns1.ns.com.', 'ns2.ns.com.'], }), ('www', { 'type': 'A', 'ttl': 3600, 'value': '10.2.3.6', }), ('cname', { 'type': 'CNAME', 'ttl': 3600, 'value': 'www.example.com.', }), ('some-host-abc123', { 'type': 'A', 'ttl': 1800, 'value': '10.2.3.7', }), ('has-dup-def123', { 'type': 'A', 'ttl': 3600, 'value': '10.2.3.8', }), ('www.sub', { 'type': 'A', 'ttl': 3600, 'value': '1.2.3.4', }), ('has-dup-def456', { 'type': 'A', 'ttl': 3600, 'value': '10.2.3.8', }), ('', { 'type': 'MX', 'ttl': 3600, 'values': [{ 'priority': 10, 'value': 'smtp-1-host.example.com.', }, { 'priority': 20, 'value': 'smtp-2-host.example.com.', }] }), ('smtp', { 'type': 'MX', 'ttl': 1800, 'values': [{ 'priority': 30, 'value': 'smtp-1-host.example.com.', }, { 'priority': 40, 'value': 'smtp-2-host.example.com.', }] }), ): record = Record.new(expected, name, data) expected.add_record(record) changes = expected.changes(got, SimpleProvider()) self.assertEquals([], 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 TestConstellixProvider(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.', ] })) # Add some ALIAS records expected.add_record(Record.new(expected, '', { 'ttl': 1800, 'type': 'ALIAS', 'value': 'aname.unit.tests.' })) expected.add_record(Record.new(expected, 'sub', { 'ttl': 1800, 'type': 'ALIAS', 'value': 'aname.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 = ConstellixProvider('test', 'api', 'secret') # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=401, text='{"errors": ["Unable to authenticate token"]}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Unauthorized', text_type(ctx.exception)) # Bad request with requests_mock() as mock: mock.get(ANY, status_code=400, text='{"errors": ["\\"unittests\\" is not ' 'a valid domain name"]}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('\n - "unittests" is not a valid domain name', 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='<html><head></head><body></body></html>') zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # No diffs == no changes with requests_mock() as mock: base = 'https://api.dns.constellix.com/v1/domains' with open('tests/fixtures/constellix-domains.json') as fh: mock.get('{}{}'.format(base, ''), text=fh.read()) with open('tests/fixtures/constellix-records.json') as fh: mock.get('{}{}'.format(base, '/123123/records'), text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(15, 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(15, len(again.records)) # bust the cache del provider._zone_records[zone.name] def test_apply(self): provider = ConstellixProvider('test', 'api', 'secret') resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) # non-existent domain, create everything resp.json.side_effect = [ [], # no domains returned during populate [{ 'id': 123123, 'name': 'unit.tests' }], # domain created in apply ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no unsupported n = len(self.expected.records) - 6 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) provider._client._request.assert_has_calls([ # get all domains to build the cache call('GET', ''), # created the domain call('POST', '/', data={'names': ['unit.tests']}) ]) # These two checks are broken up so that ordering doesn't break things. # Python3 doesn't make the calls in a consistent order so different # things follow the GET / on different runs provider._client._request.assert_has_calls([ call('POST', '/123123/records/SRV', data={ 'roundRobin': [{ 'priority': 10, 'weight': 20, 'value': 'foo-1.unit.tests.', 'port': 30 }, { 'priority': 12, 'weight': 20, 'value': 'foo-2.unit.tests.', 'port': 30 }], 'name': '_srv._tcp', 'ttl': 600, }), ]) self.assertEquals(18, provider._client._request.call_count) provider._client._request.reset_mock() provider._client.records = Mock(return_value=[ { 'id': 11189897, 'type': 'A', 'name': 'www', 'ttl': 300, 'value': [ '1.2.3.4', '2.2.3.4', ] }, { 'id': 11189898, 'type': 'A', 'name': 'ttl', 'ttl': 600, 'value': [ '3.2.3.4' ] }, { 'id': 11189899, 'type': 'ALIAS', 'name': 'alias', 'ttl': 600, 'value': [{ 'value': 'aname.unit.tests.' }] } ]) # 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(3, len(plan.changes)) self.assertEquals(3, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._client._request.assert_has_calls([ call('POST', '/123123/records/A', data={ 'roundRobin': [{ 'value': '3.2.3.4' }], 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/123123/records/A/11189897'), call('DELETE', '/123123/records/A/11189898'), call('DELETE', '/123123/records/ANAME/11189899') ], any_order=True)
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 = 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, no excluded n = len(self.expected.records) - 3 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={'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(28, 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.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('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)
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_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(16, 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(16, 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, lenient=True) self.assertEquals(set([ Record.new(zone, '', { 'ttl': 3600, 'type': 'SSHFP', 'values': [] }, lenient=True), Record.new(zone, '_srv._tcp', { 'ttl': 600, 'type': 'SRV', 'values': [] }, lenient=True), Record.new(zone, 'naptr', { 'ttl': 600, 'type': 'NAPTR', 'values': [] }, lenient=True), ]), zone.records)
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_populate_normal(self): got = Zone('example.com.', []) self.source.populate(got) self.assertEquals(17, len(got.records)) expected = Zone('example.com.', []) for name, data in ( ('', { 'type': 'A', 'ttl': 30, 'values': ['10.2.3.4', '10.2.3.5'], }), ('', { 'type': 'NS', 'ttl': 3600, 'values': ['ns1.ns.com.', 'ns2.ns.com.'], }), ('sub', { 'type': 'NS', 'ttl': 30, 'values': ['ns1.ns.com.', 'ns2.ns.com.'], }), ('www', { 'type': 'A', 'ttl': 3600, 'value': '10.2.3.6', }), ('cname', { 'type': 'CNAME', 'ttl': 3600, 'value': 'www.example.com.', }), ('some-host-abc123', { 'type': 'A', 'ttl': 1800, 'value': '10.2.3.7', }), ('has-dup-def123', { 'type': 'A', 'ttl': 3600, 'value': '10.2.3.8', }), ('www.sub', { 'type': 'A', 'ttl': 3600, 'value': '1.2.3.4', }), ('has-dup-def456', { 'type': 'A', 'ttl': 3600, 'value': '10.2.3.8', }), ('', { 'type': 'MX', 'ttl': 3600, 'values': [{ 'preference': 10, 'exchange': 'smtp-1-host.example.com.', }, { 'preference': 20, 'exchange': 'smtp-2-host.example.com.', }] }), ('smtp', { 'type': 'MX', 'ttl': 1800, 'values': [{ 'preference': 30, 'exchange': 'smtp-1-host.example.com.', }, { 'preference': 40, 'exchange': 'smtp-2-host.example.com.', }] }), ('', { 'type': 'TXT', 'ttl': 300, 'value': 'test TXT', }), ('colon', { 'type': 'TXT', 'ttl': 300, 'value': 'test : TXT', }), ('nottl', { 'type': 'TXT', 'ttl': 3600, 'value': 'nottl test TXT', }), ('ipv6-3', { 'type': 'AAAA', 'ttl': 300, 'value': '2a02:1348:017c:d5d0:0024:19ff:fef3:5742', }), ('ipv6-6', { 'type': 'AAAA', 'ttl': 3600, 'value': '2a02:1348:017c:d5d0:0024:19ff:fef3:5743', }), ('semicolon', { 'type': 'TXT', 'ttl': 300, 'value': 'v=DKIM1\\; k=rsa\\; p=blah', }), ): record = Record.new(expected, name, data) expected.add_record(record) changes = expected.changes(got, SimpleProvider()) self.assertEquals([], changes)
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 }), ])
class TestDnsMadeEasyProvider(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.', ] })) # Add some ALIAS records expected.add_record(Record.new(expected, '', { 'ttl': 1800, 'type': 'ALIAS', 'value': 'aname.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 = DnsMadeEasyProvider('test', 'api', 'secret') # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=401, text='{"error": ["API key not found"]}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Unauthorized', text_type(ctx.exception)) # Bad request with requests_mock() as mock: mock.get(ANY, status_code=400, text='{"error": ["Rate limit exceeded"]}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('\n - Rate limit exceeded', 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='<html><head></head><body></body></html>') zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # No diffs == no changes with requests_mock() as mock: base = 'https://api.dnsmadeeasy.com/V2.0/dns/managed' with open('tests/fixtures/dnsmadeeasy-domains.json') as fh: mock.get('{}{}'.format(base, '/'), text=fh.read()) with open('tests/fixtures/dnsmadeeasy-records.json') as fh: mock.get('{}{}'.format(base, '/123123/records'), 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] def test_apply(self): # Create provider with sandbox enabled provider = DnsMadeEasyProvider('test', 'api', 'secret', True) resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) with open('tests/fixtures/dnsmadeeasy-domains.json') as fh: domains = json.load(fh) # non-existent domain, create everything resp.json.side_effect = [ DnsMadeEasyClientNotFound, # no zone in populate DnsMadeEasyClientNotFound, # no domain during apply domains ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no unsupported n = len(self.expected.records) - 10 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) provider._client._request.assert_has_calls([ # created the domain call('POST', '/', data={'name': 'unit.tests'}), # get all domains to build the cache call('GET', '/'), # created at least some of the record with expected data call('POST', '/123123/records', data={ 'type': 'A', 'name': '', 'value': '1.2.3.4', 'ttl': 300}), call('POST', '/123123/records', data={ 'type': 'A', 'name': '', 'value': '1.2.3.5', 'ttl': 300}), call('POST', '/123123/records', data={ 'type': 'ANAME', 'name': '', 'value': 'aname.unit.tests.', 'ttl': 1800}), call('POST', '/123123/records', data={ 'name': '', 'value': 'ca.unit.tests', 'issuerCritical': 0, 'caaType': 'issue', 'ttl': 3600, 'type': 'CAA'}), call('POST', '/123123/records', data={ 'name': '_srv._tcp', 'weight': 20, 'value': 'foo-1.unit.tests.', 'priority': 10, 'ttl': 600, 'type': 'SRV', 'port': 30 }), ]) 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', 'value': '1.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189898, 'name': 'www', 'value': '2.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189899, 'name': 'ttl', 'value': '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', '/123123/records', data={ 'value': '3.2.3.4', 'type': 'A', 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/123123/records/11189899'), call('DELETE', '/123123/records/11189897'), call('DELETE', '/123123/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)