Merge remote-tracking branch 'origin/master' into zone-shallow-copy

This commit is contained in:
Ross McFarland
2021-08-26 16:01:07 -07:00
8 changed files with 352 additions and 124 deletions
-6
View File
@@ -8,7 +8,6 @@ from __future__ import absolute_import, division, print_function, \
from collections import defaultdict
from requests import Session
from base64 import b64encode
from ipaddress import ip_address
from six import string_types
import hashlib
import hmac
@@ -138,11 +137,6 @@ class ConstellixClient(object):
v['value'] = self._absolutize_value(v['value'],
zone_name)
# compress IPv6 addresses
if record['type'] == 'AAAA':
for i, v in enumerate(value):
value[i] = str(ip_address(v))
return resp
def record_create(self, zone_name, record_type, params):
+50 -3
View File
@@ -80,11 +80,48 @@ class Ns1Client(object):
self._datasource = client.datasource()
self._datafeed = client.datafeed()
self._datasource_id = None
self.reset_caches()
def reset_caches(self):
self._datasource_id = None
self._feeds_for_monitors = None
self._monitors_cache = None
self._notifylists_cache = None
self._zones_cache = {}
self._records_cache = {}
def update_record_cache(func):
def call(self, zone, domain, _type, **params):
if zone in self._zones_cache:
# remove record's zone from cache
del self._zones_cache[zone]
cached = self._records_cache.setdefault(zone, {}) \
.setdefault(domain, {})
if _type in cached:
# remove record from cache
del cached[_type]
# write record to cache if its not a delete
new_record = func(self, zone, domain, _type, **params)
if new_record:
cached[_type] = new_record
return new_record
return call
def read_or_set_record_cache(func):
def call(self, zone, domain, _type):
cached = self._records_cache.setdefault(zone, {}) \
.setdefault(domain, {})
if _type not in cached:
cached[_type] = func(self, zone, domain, _type)
return cached[_type]
return call
@property
def datasource_id(self):
@@ -192,23 +229,30 @@ class Ns1Client(object):
def notifylists_list(self):
return self._try(self._notifylists.list)
@update_record_cache
def records_create(self, zone, domain, _type, **params):
return self._try(self._records.create, zone, domain, _type, **params)
@update_record_cache
def records_delete(self, zone, domain, _type):
return self._try(self._records.delete, zone, domain, _type)
@read_or_set_record_cache
def records_retrieve(self, zone, domain, _type):
return self._try(self._records.retrieve, zone, domain, _type)
@update_record_cache
def records_update(self, zone, domain, _type, **params):
return self._try(self._records.update, zone, domain, _type, **params)
def zones_create(self, name):
return self._try(self._zones.create, name)
self._zones_cache[name] = self._try(self._zones.create, name)
return self._zones_cache[name]
def zones_retrieve(self, name):
return self._try(self._zones.retrieve, name)
if name not in self._zones_cache:
self._zones_cache[name] = self._try(self._zones.retrieve, name)
return self._zones_cache[name]
def _try(self, method, *args, **kwargs):
tries = self.retry_count
@@ -1003,6 +1047,9 @@ class Ns1Provider(BaseProvider):
'regions': self.monitor_regions,
}
if _type == 'AAAA':
ret['config']['ipv6'] = True
if record.healthcheck_protocol != 'TCP':
# IF it's HTTP we need to send the request string
path = record.healthcheck_path
+5 -4
View File
@@ -1088,10 +1088,11 @@ class Route53Provider(BaseProvider):
health_check, value=None):
config = health_check['HealthCheckConfig']
# So interestingly Route53 normalizes IPAddress which will cause us to
# fail to find see things as equivalent. To work around this we'll
# ip_address's returned object for equivalence
# E.g 2001:4860:4860::8842 -> 2001:4860:4860:0:0:0:0:8842
# So interestingly Route53 normalizes IPv6 addresses to a funky, but
# valid, form which will cause us to fail to find see things as
# equivalent. To work around this we'll ip_address's returned objects
# for equivalence.
# E.g 2001:4860:4860:0:0:0:0:8842 -> 2001:4860:4860::8842
if value:
value = ip_address(text_type(value))
config_ip_address = ip_address(text_type(config['IPAddress']))
-3
View File
@@ -1,5 +1,4 @@
from collections import defaultdict
from ipaddress import ip_address
from logging import getLogger
from requests import Session
@@ -196,8 +195,6 @@ class UltraProvider(BaseProvider):
}
def _data_for_AAAA(self, _type, records):
for i, v in enumerate(records['rdata']):
records['rdata'][i] = str(ip_address(v))
return {
'ttl': records['ttl'],
'type': _type,
+7 -2
View File
@@ -749,8 +749,13 @@ class _IpList(object):
@classmethod
def process(cls, values):
# Translating None into '' so that the list will be sortable in python3
return [v if v is not None else '' for v in values]
# Translating None into '' so that the list will be sortable in
# python3, get everything to str first
values = [text_type(v) if v is not None else '' for v in values]
# Now round trip all non-'' through the address type and back to a str
# to normalize the address representation.
return [text_type(cls._address_type(v)) if v != '' else ''
for v in values]
class Ipv4List(_IpList):
-1
View File
@@ -71,7 +71,6 @@ class TestAcmeMangingProcessor(TestCase):
], sorted([r.name for r in got.records]))
managed = None
for record in got.records:
print(record.name)
if record.name.endswith('managed'):
managed = record
break
+277 -102
View File
@@ -197,7 +197,13 @@ class TestNs1Provider(TestCase):
def test_populate(self, zone_retrieve_mock, record_retrieve_mock):
provider = Ns1Provider('test', 'api-key')
def reset():
provider._client.reset_caches()
zone_retrieve_mock.reset_mock()
record_retrieve_mock.reset_mock()
# Bad auth
reset()
zone_retrieve_mock.side_effect = AuthException('unauthorized')
zone = Zone('unit.tests.', [])
with self.assertRaises(AuthException) as ctx:
@@ -205,7 +211,7 @@ class TestNs1Provider(TestCase):
self.assertEquals(zone_retrieve_mock.side_effect, ctx.exception)
# General error
zone_retrieve_mock.reset_mock()
reset()
zone_retrieve_mock.side_effect = ResourceException('boom')
zone = Zone('unit.tests.', [])
with self.assertRaises(ResourceException) as ctx:
@@ -214,7 +220,7 @@ class TestNs1Provider(TestCase):
self.assertEquals(('unit.tests',), zone_retrieve_mock.call_args[0])
# Non-existent zone doesn't populate anything
zone_retrieve_mock.reset_mock()
reset()
zone_retrieve_mock.side_effect = \
ResourceException('server error: zone not found')
zone = Zone('unit.tests.', [])
@@ -224,8 +230,7 @@ class TestNs1Provider(TestCase):
self.assertFalse(exists)
# Existing zone w/o records
zone_retrieve_mock.reset_mock()
record_retrieve_mock.reset_mock()
reset()
ns1_zone = {
'records': [{
"domain": "geo.unit.tests",
@@ -255,8 +260,7 @@ class TestNs1Provider(TestCase):
'geo.unit.tests', 'A')])
# Existing zone w/records
zone_retrieve_mock.reset_mock()
record_retrieve_mock.reset_mock()
reset()
ns1_zone = {
'records': self.ns1_records + [{
"domain": "geo.unit.tests",
@@ -286,8 +290,7 @@ class TestNs1Provider(TestCase):
'geo.unit.tests', 'A')])
# Test skipping unsupported record type
zone_retrieve_mock.reset_mock()
record_retrieve_mock.reset_mock()
reset()
ns1_zone = {
'records': self.ns1_records + [{
'type': 'UNSUPPORTED',
@@ -340,19 +343,21 @@ class TestNs1Provider(TestCase):
self.assertEquals(expected_n, len(plan.changes))
self.assertTrue(plan.exists)
def reset():
provider._client.reset_caches()
record_retrieve_mock.reset_mock()
zone_create_mock.reset_mock()
zone_retrieve_mock.reset_mock()
# Fails, general error
zone_retrieve_mock.reset_mock()
record_retrieve_mock.reset_mock()
zone_create_mock.reset_mock()
reset()
zone_retrieve_mock.side_effect = ResourceException('boom')
with self.assertRaises(ResourceException) as ctx:
provider.apply(plan)
self.assertEquals(zone_retrieve_mock.side_effect, ctx.exception)
# Fails, bad auth
zone_retrieve_mock.reset_mock()
record_retrieve_mock.reset_mock()
zone_create_mock.reset_mock()
reset()
zone_retrieve_mock.side_effect = \
ResourceException('server error: zone not found')
zone_create_mock.side_effect = AuthException('unauthorized')
@@ -361,9 +366,7 @@ class TestNs1Provider(TestCase):
self.assertEquals(zone_create_mock.side_effect, ctx.exception)
# non-existent zone, create
zone_retrieve_mock.reset_mock()
record_retrieve_mock.reset_mock()
zone_create_mock.reset_mock()
reset()
zone_retrieve_mock.side_effect = \
ResourceException('server error: zone not found')
@@ -395,9 +398,7 @@ class TestNs1Provider(TestCase):
])
# Update & delete
zone_retrieve_mock.reset_mock()
record_retrieve_mock.reset_mock()
zone_create_mock.reset_mock()
reset()
ns1_zone = {
'records': self.ns1_records + [{
@@ -607,6 +608,54 @@ class TestNs1ProviderDynamic(TestCase):
'meta': {},
})
def aaaa_record(self):
return Record.new(self.zone, '', {
'dynamic': {
'pools': {
'lhr': {
'fallback': 'iad',
'values': [{
'value': '::ffff:3.4.5.6',
}],
},
'iad': {
'values': [{
'value': '::ffff:1.2.3.4',
}, {
'value': '::ffff:2.3.4.5',
}],
},
},
'rules': [{
'geos': [
'AF',
'EU-GB',
'NA-US-FL'
],
'pool': 'lhr',
}, {
'geos': [
'AF-ZW',
],
'pool': 'iad',
}, {
'pool': 'iad',
}],
},
'octodns': {
'healthcheck': {
'host': 'send.me',
'path': '/_ping',
'port': 80,
'protocol': 'HTTP',
}
},
'ttl': 32,
'type': 'AAAA',
'value': '::ffff:1.2.3.4',
'meta': {},
})
def cname_record(self):
return Record.new(self.zone, 'foo', {
'dynamic': {
@@ -871,6 +920,14 @@ class TestNs1ProviderDynamic(TestCase):
# No http response expected
self.assertFalse('rules' in monitor)
def test_monitor_gen_AAAA(self):
provider = Ns1Provider('test', 'api-key')
value = '::ffff:3.4.5.6'
record = self.aaaa_record()
monitor = provider._monitor_gen(record, value)
self.assertTrue(monitor['config']['ipv6'])
def test_monitor_gen_CNAME(self):
provider = Ns1Provider('test', 'api-key')
@@ -937,11 +994,14 @@ class TestNs1ProviderDynamic(TestCase):
'mon-id': 'feed-id',
}
def reset():
feed_create_mock.reset_mock()
monitor_create_mock.reset_mock()
monitor_gen_mock.reset_mock()
monitors_update_mock.reset_mock()
# No existing monitor
monitor_gen_mock.reset_mock()
monitor_create_mock.reset_mock()
monitors_update_mock.reset_mock()
feed_create_mock.reset_mock()
reset()
monitor_gen_mock.side_effect = [{'key': 'value'}]
monitor_create_mock.side_effect = [('mon-id', 'feed-id')]
value = '1.2.3.4'
@@ -955,10 +1015,7 @@ class TestNs1ProviderDynamic(TestCase):
feed_create_mock.assert_not_called()
# Existing monitor that doesn't need updates
monitor_gen_mock.reset_mock()
monitor_create_mock.reset_mock()
monitors_update_mock.reset_mock()
feed_create_mock.reset_mock()
reset()
monitor = {
'id': 'mon-id',
'key': 'value',
@@ -975,10 +1032,7 @@ class TestNs1ProviderDynamic(TestCase):
feed_create_mock.assert_not_called()
# Existing monitor that doesn't need updates, but is missing its feed
monitor_gen_mock.reset_mock()
monitor_create_mock.reset_mock()
monitors_update_mock.reset_mock()
feed_create_mock.reset_mock()
reset()
monitor = {
'id': 'mon-id2',
'key': 'value',
@@ -996,10 +1050,7 @@ class TestNs1ProviderDynamic(TestCase):
feed_create_mock.assert_has_calls([call(monitor)])
# Existing monitor that needs updates
monitor_gen_mock.reset_mock()
monitor_create_mock.reset_mock()
monitors_update_mock.reset_mock()
feed_create_mock.reset_mock()
reset()
monitor = {
'id': 'mon-id',
'key': 'value',
@@ -1033,11 +1084,14 @@ class TestNs1ProviderDynamic(TestCase):
'mon-id': 'feed-id',
}
def reset():
datafeed_delete_mock.reset_mock()
monitors_delete_mock.reset_mock()
monitors_for_mock.reset_mock()
notifylists_delete_mock.reset_mock()
# No active monitors and no existing, nothing will happen
monitors_for_mock.reset_mock()
datafeed_delete_mock.reset_mock()
monitors_delete_mock.reset_mock()
notifylists_delete_mock.reset_mock()
reset()
monitors_for_mock.side_effect = [{}]
record = self.record()
provider._monitors_gc(record)
@@ -1047,10 +1101,7 @@ class TestNs1ProviderDynamic(TestCase):
notifylists_delete_mock.assert_not_called()
# No active monitors and one existing, delete all the things
monitors_for_mock.reset_mock()
datafeed_delete_mock.reset_mock()
monitors_delete_mock.reset_mock()
notifylists_delete_mock.reset_mock()
reset()
monitors_for_mock.side_effect = [{
'x': {
'id': 'mon-id',
@@ -1070,10 +1121,7 @@ class TestNs1ProviderDynamic(TestCase):
notifylists_delete_mock.assert_has_calls([call('nl-id')])
# Same existing, this time in active list, should be noop
monitors_for_mock.reset_mock()
datafeed_delete_mock.reset_mock()
monitors_delete_mock.reset_mock()
notifylists_delete_mock.reset_mock()
reset()
monitors_for_mock.side_effect = [{
'x': {
'id': 'mon-id',
@@ -1088,10 +1136,7 @@ class TestNs1ProviderDynamic(TestCase):
# Non-active monitor w/o a feed, and another monitor that's left alone
# b/c it's active
monitors_for_mock.reset_mock()
datafeed_delete_mock.reset_mock()
monitors_delete_mock.reset_mock()
notifylists_delete_mock.reset_mock()
reset()
monitors_for_mock.side_effect = [{
'x': {
'id': 'mon-id',
@@ -1120,10 +1165,7 @@ class TestNs1ProviderDynamic(TestCase):
# Non-active monitor w/o a notifylist, generally shouldn't happen, but
# code should handle it just in case someone gets clicky in the UI
monitors_for_mock.reset_mock()
datafeed_delete_mock.reset_mock()
monitors_delete_mock.reset_mock()
notifylists_delete_mock.reset_mock()
reset()
monitors_for_mock.side_effect = [{
'y': {
'id': 'mon-id2',
@@ -1148,11 +1190,8 @@ class TestNs1ProviderDynamic(TestCase):
# Non-active monitor with a shared notifylist, monitor deleted, but
# notifylist is left alone
reset()
provider.shared_notifylist = True
monitors_for_mock.reset_mock()
datafeed_delete_mock.reset_mock()
monitors_delete_mock.reset_mock()
notifylists_delete_mock.reset_mock()
monitors_for_mock.side_effect = [{
'y': {
'id': 'mon-id2',
@@ -1304,6 +1343,7 @@ class TestNs1ProviderDynamic(TestCase):
# provider._params_for_A() calls provider._monitors_for() and
# provider._monitor_sync(). Mock their return values so that we don't
# make NS1 API calls during tests
provider._client.reset_caches()
monitors_for_mock.reset_mock()
monitor_sync_mock.reset_mock()
monitors_for_mock.side_effect = [{
@@ -1934,34 +1974,35 @@ class TestNs1ProviderDynamic(TestCase):
desired = Zone('unit.tests.', [])
def reset():
monitors_for_mock.reset_mock()
provider._client.reset_caches()
records_retrieve_mock.reset_mock()
zones_retrieve_mock.reset_mock()
# Empty zone and no changes
monitors_for_mock.reset_mock()
zones_retrieve_mock.reset_mock()
records_retrieve_mock.reset_mock()
reset()
extra = provider._extra_changes(desired, [])
self.assertFalse(extra)
monitors_for_mock.assert_not_called()
# Non-existent zone. No changes
monitors_for_mock.reset_mock()
reset()
zones_retrieve_mock.side_effect = \
ResourceException('server error: zone not found')
records_retrieve_mock.reset_mock()
extra = provider._extra_changes(desired, [])
self.assertFalse(extra)
# Unexpected exception message
zones_retrieve_mock.reset_mock()
reset()
zones_retrieve_mock.side_effect = ResourceException('boom')
with self.assertRaises(ResourceException) as ctx:
extra = provider._extra_changes(desired, [])
self.assertEquals(zones_retrieve_mock.side_effect, ctx.exception)
# Simple record, ignored, filter update lookups ignored
monitors_for_mock.reset_mock()
zones_retrieve_mock.reset_mock()
records_retrieve_mock.reset_mock()
reset()
zones_retrieve_mock.side_effect = \
ResourceException('server error: zone not found')
@@ -2006,9 +2047,7 @@ class TestNs1ProviderDynamic(TestCase):
desired.add_record(dynamic)
# untouched, but everything in sync so no change needed
monitors_for_mock.reset_mock()
zones_retrieve_mock.reset_mock()
records_retrieve_mock.reset_mock()
reset()
# Generate what we expect to have
gend = provider._monitor_gen(dynamic, '1.2.3.4')
gend.update({
@@ -2026,9 +2065,7 @@ class TestNs1ProviderDynamic(TestCase):
# If we don't have a notify list we're broken and we'll expect to see
# an Update
monitors_for_mock.reset_mock()
zones_retrieve_mock.reset_mock()
records_retrieve_mock.reset_mock()
reset()
del gend['notify_list']
monitors_for_mock.side_effect = [{
'1.2.3.4': gend,
@@ -2042,9 +2079,7 @@ class TestNs1ProviderDynamic(TestCase):
# Add notify_list back and change the healthcheck protocol, we'll still
# expect to see an update
monitors_for_mock.reset_mock()
zones_retrieve_mock.reset_mock()
records_retrieve_mock.reset_mock()
reset()
gend['notify_list'] = 'xyz'
dynamic._octodns['healthcheck']['protocol'] = 'HTTPS'
del gend['notify_list']
@@ -2059,9 +2094,7 @@ class TestNs1ProviderDynamic(TestCase):
monitors_for_mock.assert_has_calls([call(dynamic)])
# If it's in the changed list, it'll be ignored
monitors_for_mock.reset_mock()
zones_retrieve_mock.reset_mock()
records_retrieve_mock.reset_mock()
reset()
extra = provider._extra_changes(desired, [update])
self.assertFalse(extra)
monitors_for_mock.assert_not_called()
@@ -2069,9 +2102,7 @@ class TestNs1ProviderDynamic(TestCase):
# Test changes in filters
# No change in filters
monitors_for_mock.reset_mock()
zones_retrieve_mock.reset_mock()
records_retrieve_mock.reset_mock()
reset()
ns1_zone = {
'records': [{
"domain": "dyn.unit.tests",
@@ -2088,9 +2119,7 @@ class TestNs1ProviderDynamic(TestCase):
self.assertFalse(extra)
# filters need an update
monitors_for_mock.reset_mock()
zones_retrieve_mock.reset_mock()
records_retrieve_mock.reset_mock()
reset()
ns1_zone = {
'records': [{
"domain": "dyn.unit.tests",
@@ -2107,9 +2136,7 @@ class TestNs1ProviderDynamic(TestCase):
self.assertTrue(extra)
# Mixed disabled in filters. Raise Ns1Exception
monitors_for_mock.reset_mock()
zones_retrieve_mock.reset_mock()
records_retrieve_mock.reset_mock()
reset()
ns1_zone = {
'records': [{
"domain": "dyn.unit.tests",
@@ -2234,12 +2261,14 @@ class TestNs1Client(TestCase):
client = Ns1Client('dummy-key')
# No retry required, just calls and is returned
client.reset_caches()
zone_retrieve_mock.reset_mock()
zone_retrieve_mock.side_effect = ['foo']
self.assertEquals('foo', client.zones_retrieve('unit.tests'))
zone_retrieve_mock.assert_has_calls([call('unit.tests')])
# One retry required
client.reset_caches()
zone_retrieve_mock.reset_mock()
zone_retrieve_mock.side_effect = [
RateLimitException('boo', period=0),
@@ -2249,6 +2278,7 @@ class TestNs1Client(TestCase):
zone_retrieve_mock.assert_has_calls([call('unit.tests')])
# Two retries required
client.reset_caches()
zone_retrieve_mock.reset_mock()
zone_retrieve_mock.side_effect = [
RateLimitException('boo', period=0),
@@ -2258,6 +2288,7 @@ class TestNs1Client(TestCase):
zone_retrieve_mock.assert_has_calls([call('unit.tests')])
# Exhaust our retries
client.reset_caches()
zone_retrieve_mock.reset_mock()
zone_retrieve_mock.side_effect = [
RateLimitException('first', period=0),
@@ -2469,9 +2500,12 @@ class TestNs1Client(TestCase):
notifylists_delete_mock):
client = Ns1Client('dummy-key')
notifylists_list_mock.reset_mock()
notifylists_create_mock.reset_mock()
notifylists_delete_mock.reset_mock()
def reset():
notifylists_create_mock.reset_mock()
notifylists_delete_mock.reset_mock()
notifylists_list_mock.reset_mock()
reset()
notifylists_list_mock.side_effect = [{}]
expected = {
'id': 'nl-id',
@@ -2493,9 +2527,7 @@ class TestNs1Client(TestCase):
])
notifylists_delete_mock.assert_not_called()
notifylists_list_mock.reset_mock()
notifylists_create_mock.reset_mock()
notifylists_delete_mock.reset_mock()
reset()
client.notifylists_delete('nlid')
notifylists_list_mock.assert_not_called()
notifylists_create_mock.assert_not_called()
@@ -2503,9 +2535,7 @@ class TestNs1Client(TestCase):
# Delete again, this time with a cache item that needs cleaned out and
# another that needs to be ignored
notifylists_list_mock.reset_mock()
notifylists_create_mock.reset_mock()
notifylists_delete_mock.reset_mock()
reset()
client._notifylists_cache = {
'another': {
'id': 'notid',
@@ -2524,9 +2554,7 @@ class TestNs1Client(TestCase):
# Only another left
self.assertEquals(['another'], list(client._notifylists_cache.keys()))
notifylists_list_mock.reset_mock()
notifylists_create_mock.reset_mock()
notifylists_delete_mock.reset_mock()
reset()
expected = ['one', 'two', 'three']
notifylists_list_mock.side_effect = [expected]
nls = client.notifylists_list()
@@ -2534,3 +2562,150 @@ class TestNs1Client(TestCase):
notifylists_list_mock.assert_has_calls([call()])
notifylists_create_mock.assert_not_called()
notifylists_delete_mock.assert_not_called()
@patch('ns1.rest.records.Records.delete')
@patch('ns1.rest.records.Records.update')
@patch('ns1.rest.records.Records.create')
@patch('ns1.rest.records.Records.retrieve')
@patch('ns1.rest.zones.Zones.create')
@patch('ns1.rest.zones.Zones.delete')
@patch('ns1.rest.zones.Zones.retrieve')
def test_client_caching(self, zone_retrieve_mock, zone_delete_mock,
zone_create_mock, record_retrieve_mock,
record_create_mock, record_update_mock,
record_delete_mock):
client = Ns1Client('dummy-key')
def reset():
zone_retrieve_mock.reset_mock()
zone_delete_mock.reset_mock()
zone_create_mock.reset_mock()
record_retrieve_mock.reset_mock()
record_create_mock.reset_mock()
record_update_mock.reset_mock()
record_delete_mock.reset_mock()
# Testing caches so we don't reset those
# Initial zone get fetches and caches
reset()
zone_retrieve_mock.side_effect = ['foo']
self.assertEquals('foo', client.zones_retrieve('unit.tests'))
zone_retrieve_mock.assert_has_calls([call('unit.tests')])
self.assertEquals({
'unit.tests': 'foo',
}, client._zones_cache)
# Subsequent zone get does not fetch and returns from cache
reset()
self.assertEquals('foo', client.zones_retrieve('unit.tests'))
zone_retrieve_mock.assert_not_called()
# Zone create stores in cache
reset()
zone_create_mock.side_effect = ['bar']
self.assertEquals('bar', client.zones_create('sub.unit.tests'))
zone_create_mock.assert_has_calls([call('sub.unit.tests')])
self.assertEquals({
'sub.unit.tests': 'bar',
'unit.tests': 'foo',
}, client._zones_cache)
# Initial record get fetches and caches
reset()
record_retrieve_mock.side_effect = ['baz']
self.assertEquals('baz', client.records_retrieve('unit.tests',
'a.unit.tests', 'A'))
record_retrieve_mock.assert_has_calls([call('unit.tests',
'a.unit.tests', 'A')])
self.assertEquals({
'unit.tests': {
'a.unit.tests': {
'A': 'baz'
}
}
}, client._records_cache)
# Subsequent record get does not fetch and returns from cache
reset()
self.assertEquals('baz', client.records_retrieve('unit.tests',
'a.unit.tests', 'A'))
record_retrieve_mock.assert_not_called()
# Record create stores in cache
reset()
record_create_mock.side_effect = ['boo']
self.assertEquals('boo', client.records_create('unit.tests',
'aaaa.unit.tests',
'AAAA', key='val'))
record_create_mock.assert_has_calls([call('unit.tests',
'aaaa.unit.tests', 'AAAA',
key='val')])
self.assertEquals({
'unit.tests': {
'a.unit.tests': {
'A': 'baz'
},
'aaaa.unit.tests': {
'AAAA': 'boo'
},
}
}, client._records_cache)
# Record delete removes from cache and removes zone
reset()
record_delete_mock.side_effect = [{}]
self.assertEquals({}, client.records_delete('unit.tests',
'aaaa.unit.tests',
'AAAA'))
record_delete_mock.assert_has_calls([call('unit.tests',
'aaaa.unit.tests', 'AAAA')])
self.assertEquals({
'unit.tests': {
'a.unit.tests': {
'A': 'baz'
},
'aaaa.unit.tests': {},
}
}, client._records_cache)
self.assertEquals({
'sub.unit.tests': 'bar',
}, client._zones_cache)
# Delete the other record, no zone this time, record should still go
# away
reset()
record_delete_mock.side_effect = [{}]
self.assertEquals({}, client.records_delete('unit.tests',
'a.unit.tests', 'A'))
record_delete_mock.assert_has_calls([call('unit.tests', 'a.unit.tests',
'A')])
self.assertEquals({
'unit.tests': {
'a.unit.tests': {},
'aaaa.unit.tests': {},
}
}, client._records_cache)
self.assertEquals({
'sub.unit.tests': 'bar',
}, client._zones_cache)
# Record update removes zone and caches result
record_update_mock.side_effect = ['done']
self.assertEquals('done', client.records_update('sub.unit.tests',
'aaaa.sub.unit.tests',
'AAAA', key='val'))
record_update_mock.assert_has_calls([call('sub.unit.tests',
'aaaa.sub.unit.tests',
'AAAA', key='val')])
self.assertEquals({
'unit.tests': {
'a.unit.tests': {},
'aaaa.unit.tests': {},
},
'sub.unit.tests': {
'aaaa.sub.unit.tests': {
'AAAA': 'done',
},
}
}, client._records_cache)
self.assertEquals({}, client._zones_cache)
+13 -3
View File
@@ -259,11 +259,21 @@ class TestRecord(TestCase):
self.assertEquals(b_data, b.data)
def test_aaaa(self):
a_values = ['2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b',
'2001:0db8:3c4d:0015:0000:0000:1a2f:1a3b']
b_value = '2001:0db8:3c4d:0015:0000:0000:1a2f:1a4b'
a_values = ['2001:db8:3c4d:15::1a2f:1a2b',
'2001:db8:3c4d:15::1a2f:1a3b']
b_value = '2001:db8:3c4d:15::1a2f:1a4b'
self.assertMultipleValues(AaaaRecord, a_values, b_value)
# Specifically validate that we normalize IPv6 addresses
values = ['2001:db8:3c4d:15:0000:0000:1a2f:1a2b',
'2001:0db8:3c4d:0015::1a2f:1a3b']
data = {
'ttl': 30,
'values': values,
}
record = AaaaRecord(self.zone, 'aaaa', data)
self.assertEquals(a_values, record.values)
def assertSingleValue(self, _type, a_value, b_value):
a_data = {'ttl': 30, 'value': a_value}
a = _type(self.zone, 'a', a_data)