Browse Source

Complete refactor & rework of how validation is set up

This is with an eye toward expanding it in the future both in terms of what it
checks and to add the ability to ignore things. This commit does not intend to
change any validation. It only reworks the flow and improves the error
messaging.
pull/74/head
Ross McFarland 9 years ago
parent
commit
8323b4c0ea
2 changed files with 895 additions and 262 deletions
  1. +254
    -96
      octodns/record.py
  2. +641
    -166
      tests/test_octodns_record.py

+ 254
- 96
octodns/record.py View File

@ -54,7 +54,14 @@ class Delete(Change):
return 'Delete {}'.format(self.existing) return 'Delete {}'.format(self.existing)
_unescaped_semicolon_re = re.compile(r'\w;')
class ValidationError(Exception):
def __init__(self, fqdn, reasons):
message = 'Invalid record {}\n - {}' \
.format(fqdn, '\n - '.join(reasons))
super(Exception, self).__init__(message)
self.fqdn = fqdn
self.reasons = reasons
class Record(object): class Record(object):
@ -62,13 +69,13 @@ class Record(object):
@classmethod @classmethod
def new(cls, zone, name, data, source=None): def new(cls, zone, name, data, source=None):
fqdn = '{}.{}'.format(name, zone.name) if name else zone.name
try: try:
_type = data['type'] _type = data['type']
except KeyError: except KeyError:
fqdn = '{}.{}'.format(name, zone.name) if name else zone.name
raise Exception('Invalid record {}, missing type'.format(fqdn)) raise Exception('Invalid record {}, missing type'.format(fqdn))
try: try:
_type = {
_class = {
'A': ARecord, 'A': ARecord,
'AAAA': AaaaRecord, 'AAAA': AaaaRecord,
'ALIAS': AliasRecord, 'ALIAS': AliasRecord,
@ -98,7 +105,21 @@ class Record(object):
}[_type] }[_type]
except KeyError: except KeyError:
raise Exception('Unknown record type: "{}"'.format(_type)) raise Exception('Unknown record type: "{}"'.format(_type))
return _type(zone, name, data, source=source)
reasons = _class.validate(name, data)
if reasons:
raise ValidationError(fqdn, reasons)
return _class(zone, name, data, source=source)
@classmethod
def validate(cls, name, data):
reasons = []
try:
ttl = int(data['ttl'])
if ttl < 0:
reasons.append('invalid ttl')
except KeyError:
reasons.append('missing ttl')
return reasons
def __init__(self, zone, name, data, source=None): def __init__(self, zone, name, data, source=None):
self.log.debug('__init__: zone.name=%s, type=%11s, name=%s', zone.name, self.log.debug('__init__: zone.name=%s, type=%11s, name=%s', zone.name,
@ -106,11 +127,8 @@ class Record(object):
self.zone = zone self.zone = zone
# force everything lower-case just to be safe # force everything lower-case just to be safe
self.name = str(name).lower() if name else name self.name = str(name).lower() if name else name
try:
self.ttl = int(data['ttl'])
except KeyError:
raise Exception('Invalid record {}, missing ttl'.format(self.fqdn))
self.source = source self.source = source
self.ttl = int(data['ttl'])
octodns = data.get('octodns', {}) octodns = data.get('octodns', {})
self.ignored = octodns.get('ignored', False) self.ignored = octodns.get('ignored', False)
@ -154,11 +172,17 @@ class GeoValue(object):
geo_re = re.compile(r'^(?P<continent_code>\w\w)(-(?P<country_code>\w\w)' geo_re = re.compile(r'^(?P<continent_code>\w\w)(-(?P<country_code>\w\w)'
r'(-(?P<subdivision_code>\w\w))?)?$') r'(-(?P<subdivision_code>\w\w))?)?$')
def __init__(self, geo, values):
match = self.geo_re.match(geo)
@classmethod
def _validate_geo(cls, code):
reasons = []
match = cls.geo_re.match(code)
if not match: if not match:
raise Exception('Invalid geo "{}"'.format(geo))
reasons.append('invalid geo "{}"'.format(code))
return reasons
def __init__(self, geo, values):
self.code = geo self.code = geo
match = self.geo_re.match(geo)
self.continent_code = match.group('continent_code') self.continent_code = match.group('continent_code')
self.country_code = match.group('country_code') self.country_code = match.group('country_code')
self.subdivision_code = match.group('subdivision_code') self.subdivision_code = match.group('subdivision_code')
@ -185,16 +209,29 @@ class GeoValue(object):
class _ValuesMixin(object): class _ValuesMixin(object):
def __init__(self, zone, name, data, source=None):
super(_ValuesMixin, self).__init__(zone, name, data, source=source)
@classmethod
def validate(cls, name, data):
reasons = super(_ValuesMixin, cls).validate(name, data)
values = []
try: try:
values = data['values'] values = data['values']
except KeyError: except KeyError:
try: try:
values = [data['value']] values = [data['value']]
except KeyError: except KeyError:
raise Exception('Invalid record {}, missing value(s)'
.format(self.fqdn))
reasons.append('missing value(s)')
for value in values:
reasons.extend(cls._validate_value(value))
return reasons
def __init__(self, zone, name, data, source=None):
super(_ValuesMixin, self).__init__(zone, name, data, source=source)
try:
values = data['values']
except KeyError:
values = [data['value']]
self.values = sorted(self._process_values(values)) self.values = sorted(self._process_values(values))
def changes(self, other, target): def changes(self, other, target):
@ -224,6 +261,21 @@ class _GeoMixin(_ValuesMixin):
Must be included before `Record`. Must be included before `Record`.
''' '''
@classmethod
def validate(cls, name, data):
reasons = super(_GeoMixin, cls).validate(name, data)
try:
geo = dict(data['geo'])
# TODO: validate legal codes
for code, values in geo.items():
reasons.extend(GeoValue._validate_geo(code))
for value in values:
reasons.extend(cls._validate_value(value))
except KeyError:
pass
return reasons
# TODO: support 'value' as well
# TODO: move away from "data" hash to strict params, it's kind of leaking # TODO: move away from "data" hash to strict params, it's kind of leaking
# the yaml implementation into here and then forcing it back out into # the yaml implementation into here and then forcing it back out into
# non-yaml providers during input # non-yaml providers during input
@ -233,9 +285,8 @@ class _GeoMixin(_ValuesMixin):
self.geo = dict(data['geo']) self.geo = dict(data['geo'])
except KeyError: except KeyError:
self.geo = {} self.geo = {}
for k, vs in self.geo.items():
vs = sorted(self._process_values(vs))
self.geo[k] = GeoValue(k, vs)
for code, values in self.geo.items():
self.geo[code] = GeoValue(code, values)
def _data(self): def _data(self):
ret = super(_GeoMixin, self)._data() ret = super(_GeoMixin, self)._data()
@ -264,41 +315,52 @@ class _GeoMixin(_ValuesMixin):
class ARecord(_GeoMixin, Record): class ARecord(_GeoMixin, Record):
_type = 'A' _type = 'A'
@classmethod
def _validate_value(self, value):
reasons = []
try:
IPv4Address(unicode(value))
except Exception:
reasons.append('invalid ip address "{}"'.format(value))
return reasons
def _process_values(self, values): def _process_values(self, values):
for ip in values:
try:
IPv4Address(unicode(ip))
except Exception:
raise Exception('Invalid record {}, value {} not a valid ip'
.format(self.fqdn, ip))
return values return values
class AaaaRecord(_GeoMixin, Record): class AaaaRecord(_GeoMixin, Record):
_type = 'AAAA' _type = 'AAAA'
@classmethod
def _validate_value(self, value):
reasons = []
try:
IPv6Address(unicode(value))
except Exception:
reasons.append('invalid ip address "{}"'.format(value))
return reasons
def _process_values(self, values): def _process_values(self, values):
ret = []
for ip in values:
try:
IPv6Address(unicode(ip))
ret.append(ip.lower())
except Exception:
raise Exception('Invalid record {}, value {} not a valid ip'
.format(self.fqdn, ip))
return ret
return values
class _ValueMixin(object): class _ValueMixin(object):
def __init__(self, zone, name, data, source=None):
super(_ValueMixin, self).__init__(zone, name, data, source=source)
@classmethod
def validate(cls, name, data):
reasons = super(_ValueMixin, cls).validate(name, data)
value = None
try: try:
value = data['value'] value = data['value']
except KeyError: except KeyError:
raise Exception('Invalid record {}, missing value'
.format(self.fqdn))
self.value = self._process_value(value)
reasons.append('missing value')
if value:
reasons.extend(cls._validate_value(value))
return reasons
def __init__(self, zone, name, data, source=None):
super(_ValueMixin, self).__init__(zone, name, data, source=source)
self.value = self._process_value(data['value'])
def changes(self, other, target): def changes(self, other, target):
if self.value != other.value: if self.value != other.value:
@ -319,25 +381,42 @@ class _ValueMixin(object):
class AliasRecord(_ValueMixin, Record): class AliasRecord(_ValueMixin, Record):
_type = 'ALIAS' _type = 'ALIAS'
def _process_value(self, value):
@classmethod
def _validate_value(self, value):
reasons = []
if not value.endswith('.'): if not value.endswith('.'):
raise Exception('Invalid record {}, value ({}) missing trailing .'
.format(self.fqdn, value))
reasons.append('missing trailing .')
return reasons
def _process_value(self, value):
return value return value
class CnameRecord(_ValueMixin, Record): class CnameRecord(_ValueMixin, Record):
_type = 'CNAME' _type = 'CNAME'
def _process_value(self, value):
@classmethod
def _validate_value(cls, value):
reasons = []
if not value.endswith('.'): if not value.endswith('.'):
raise Exception('Invalid record {}, value ({}) missing trailing .'
.format(self.fqdn, value))
return value.lower()
reasons.append('missing trailing .')
return reasons
def _process_value(self, value):
return value
class MxValue(object): class MxValue(object):
@classmethod
def _validate_value(cls, value):
reasons = []
if 'priority' not in value:
reasons.append('missing priority')
if 'value' not in value:
reasons.append('missing value')
return reasons
def __init__(self, value): def __init__(self, value):
# TODO: rename preference # TODO: rename preference
self.priority = int(value['priority']) self.priority = int(value['priority'])
@ -363,19 +442,38 @@ class MxValue(object):
class MxRecord(_ValuesMixin, Record): class MxRecord(_ValuesMixin, Record):
_type = 'MX' _type = 'MX'
@classmethod
def _validate_value(cls, value):
return MxValue._validate_value(value)
def _process_values(self, values): def _process_values(self, values):
ret = []
for value in values:
try:
ret.append(MxValue(value))
except KeyError as e:
raise Exception('Invalid value in record {}, missing {}'
.format(self.fqdn, e.args[0]))
return ret
return [MxValue(v) for v in values]
class NaptrValue(object): class NaptrValue(object):
@classmethod
def _validate_value(cls, data):
reasons = []
try:
int(data['order'])
except KeyError:
reasons.append('missing order')
except ValueError:
reasons.append('invalid order "{}"'.format(data['order']))
try:
int(data['preference'])
except KeyError:
reasons.append('missing preference')
except ValueError:
reasons.append('invalid preference "{}"'
.format(data['preference']))
# TODO: validate field data
for k in ('flags', 'service', 'regexp', 'replacement'):
if k not in data:
reasons.append('missing {}'.format(k))
return reasons
def __init__(self, value): def __init__(self, value):
self.order = int(value['order']) self.order = int(value['order'])
self.preference = int(value['preference']) self.preference = int(value['preference'])
@ -420,42 +518,65 @@ class NaptrValue(object):
class NaptrRecord(_ValuesMixin, Record): class NaptrRecord(_ValuesMixin, Record):
_type = 'NAPTR' _type = 'NAPTR'
@classmethod
def _validate_value(cls, value):
return NaptrValue._validate_value(value)
def _process_values(self, values): def _process_values(self, values):
ret = []
for value in values:
try:
ret.append(NaptrValue(value))
except KeyError as e:
raise Exception('Invalid value in record {}, missing {}'
.format(self.fqdn, e.args[0]))
return ret
return [NaptrValue(v) for v in values]
class NsRecord(_ValuesMixin, Record): class NsRecord(_ValuesMixin, Record):
_type = 'NS' _type = 'NS'
@classmethod
def _validate_value(cls, value):
reasons = []
if not value.endswith('.'):
reasons.append('missing trailing .')
return reasons
def _process_values(self, values): def _process_values(self, values):
ret = []
for ns in values:
if not ns.endswith('.'):
raise Exception('Invalid record {}, value {} missing '
'trailing .'.format(self.fqdn, ns))
ret.append(ns.lower())
return ret
return values
class PtrRecord(_ValueMixin, Record): class PtrRecord(_ValueMixin, Record):
_type = 'PTR' _type = 'PTR'
def _process_value(self, value):
@classmethod
def _validate_value(cls, value):
reasons = []
if not value.endswith('.'): if not value.endswith('.'):
raise Exception('Invalid record {}, value ({}) missing trailing .'
.format(self.fqdn, value))
return value.lower()
reasons.append('missing trailing .')
return reasons
def _process_value(self, value):
return value
class SshfpValue(object): class SshfpValue(object):
@classmethod
def _validate_value(cls, value):
reasons = []
# TODO: validate algorithm and fingerprint_type values
try:
int(value['algorithm'])
except KeyError:
reasons.append('missing algorithm')
except ValueError:
reasons.append('invalid algorithm "{}"'.format(value['algorithm']))
try:
int(value['fingerprint_type'])
except KeyError:
reasons.append('missing fingerprint_type')
except ValueError:
reasons.append('invalid fingerprint_type "{}"'
.format(value['fingerprint_type']))
if 'fingerprint' not in value:
reasons.append('missing fingerprint')
return reasons
def __init__(self, value): def __init__(self, value):
self.algorithm = int(value['algorithm']) self.algorithm = int(value['algorithm'])
self.fingerprint_type = int(value['fingerprint_type']) self.fingerprint_type = int(value['fingerprint_type'])
@ -484,26 +605,61 @@ class SshfpValue(object):
class SshfpRecord(_ValuesMixin, Record): class SshfpRecord(_ValuesMixin, Record):
_type = 'SSHFP' _type = 'SSHFP'
@classmethod
def _validate_value(cls, value):
return SshfpValue._validate_value(value)
def _process_values(self, values): def _process_values(self, values):
ret = []
for value in values:
try:
ret.append(SshfpValue(value))
except KeyError as e:
raise Exception('Invalid value in record {}, missing {}'
.format(self.fqdn, e.args[0]))
return ret
return [SshfpValue(v) for v in values]
_unescaped_semicolon_re = re.compile(r'\w;')
class SpfRecord(_ValuesMixin, Record): class SpfRecord(_ValuesMixin, Record):
_type = 'SPF' _type = 'SPF'
@classmethod
def _validate_value(cls, value):
if _unescaped_semicolon_re.search(value):
return ['unescaped ;']
return []
def _process_values(self, values): def _process_values(self, values):
return values return values
class SrvValue(object): class SrvValue(object):
@classmethod
def _validate_value(self, value):
reasons = []
# TODO: validate algorithm and fingerprint_type values
try:
int(value['priority'])
except KeyError:
reasons.append('missing priority')
except ValueError:
reasons.append('invalid priority "{}"'.format(value['priority']))
try:
int(value['weight'])
except KeyError:
reasons.append('missing weight')
except ValueError:
reasons.append('invalid weight "{}"'.format(value['weight']))
try:
int(value['port'])
except KeyError:
reasons.append('missing port')
except ValueError:
reasons.append('invalid port "{}"'.format(value['port']))
try:
if not value['target'].endswith('.'):
reasons.append('missing trailing .')
except KeyError:
reasons.append('missing target')
return reasons
def __init__(self, value): def __init__(self, value):
self.priority = int(value['priority']) self.priority = int(value['priority'])
self.weight = int(value['weight']) self.weight = int(value['weight'])
@ -537,28 +693,30 @@ class SrvRecord(_ValuesMixin, Record):
_type = 'SRV' _type = 'SRV'
_name_re = re.compile(r'^_[^\.]+\.[^\.]+') _name_re = re.compile(r'^_[^\.]+\.[^\.]+')
def __init__(self, zone, name, data, source=None):
if not self._name_re.match(name):
raise Exception('Invalid name {}.{}'.format(name, zone.name))
super(SrvRecord, self).__init__(zone, name, data, source)
@classmethod
def validate(cls, name, data):
reasons = []
if not cls._name_re.match(name):
reasons.append('invalid name')
reasons.extend(super(SrvRecord, cls).validate(name, data))
return reasons
@classmethod
def _validate_value(cls, value):
return SrvValue._validate_value(value)
def _process_values(self, values): def _process_values(self, values):
ret = []
for value in values:
try:
ret.append(SrvValue(value))
except KeyError as e:
raise Exception('Invalid value in record {}, missing {}'
.format(self.fqdn, e.args[0]))
return ret
return [SrvValue(v) for v in values]
class TxtRecord(_ValuesMixin, Record): class TxtRecord(_ValuesMixin, Record):
_type = 'TXT' _type = 'TXT'
@classmethod
def _validate_value(cls, value):
if _unescaped_semicolon_re.search(value):
return ['unescaped ;']
return []
def _process_values(self, values): def _process_values(self, values):
for value in values:
if _unescaped_semicolon_re.search(value):
raise Exception('Invalid record {}, unescaped ;'
.format(self.fqdn))
return values return values

+ 641
- 166
tests/test_octodns_record.py View File

@ -9,7 +9,8 @@ from unittest import TestCase
from octodns.record import ARecord, AaaaRecord, AliasRecord, CnameRecord, \ from octodns.record import ARecord, AaaaRecord, AliasRecord, CnameRecord, \
Create, Delete, GeoValue, MxRecord, NaptrRecord, NaptrValue, NsRecord, \ Create, Delete, GeoValue, MxRecord, NaptrRecord, NaptrValue, NsRecord, \
PtrRecord, Record, SshfpRecord, SpfRecord, SrvRecord, TxtRecord, Update
Record, SshfpRecord, SpfRecord, SrvRecord, TxtRecord, Update, \
ValidationError
from octodns.zone import Zone from octodns.zone import Zone
from helpers import GeoProvider, SimpleProvider from helpers import GeoProvider, SimpleProvider
@ -42,15 +43,6 @@ class TestRecord(TestCase):
self.assertEquals([b_value], b.values) self.assertEquals([b_value], b.values)
self.assertEquals(b_data, b.data) self.assertEquals(b_data, b.data)
# missing ttl
with self.assertRaises(Exception) as ctx:
ARecord(self.zone, None, {'value': '1.1.1.1'})
self.assertTrue('missing ttl' in ctx.exception.message)
# missing values & value
with self.assertRaises(Exception) as ctx:
ARecord(self.zone, None, {'ttl': 42})
self.assertTrue('missing value(s)' in ctx.exception.message)
# top-level # top-level
data = {'ttl': 30, 'value': '4.2.3.4'} data = {'ttl': 30, 'value': '4.2.3.4'}
self.assertEquals(self.zone.name, ARecord(self.zone, '', data).fqdn) self.assertEquals(self.zone.name, ARecord(self.zone, '', data).fqdn)
@ -104,20 +96,6 @@ class TestRecord(TestCase):
DummyRecord().__repr__() DummyRecord().__repr__()
def test_invalid_a(self):
with self.assertRaises(Exception) as ctx:
ARecord(self.zone, 'a', {
'ttl': 30,
'value': 'foo',
})
self.assertTrue('Invalid record' in ctx.exception.message)
with self.assertRaises(Exception) as ctx:
ARecord(self.zone, 'a', {
'ttl': 30,
'values': ['1.2.3.4', 'bar'],
})
self.assertTrue('Invalid record' in ctx.exception.message)
def test_geo(self): def test_geo(self):
geo_data = {'ttl': 42, 'values': ['5.2.3.4', '6.2.3.4'], geo_data = {'ttl': 42, 'values': ['5.2.3.4', '6.2.3.4'],
'geo': {'AF': ['1.1.1.1'], 'geo': {'AF': ['1.1.1.1'],
@ -157,19 +135,6 @@ class TestRecord(TestCase):
# Geo provider does consider lack of geo diffs to be changes # Geo provider does consider lack of geo diffs to be changes
self.assertTrue(geo.changes(other, geo_target)) self.assertTrue(geo.changes(other, geo_target))
# invalid geo code
with self.assertRaises(Exception) as ctx:
ARecord(self.zone, 'geo', {'ttl': 42,
'values': ['5.2.3.4', '6.2.3.4'],
'geo': {'abc': ['1.1.1.1']}})
self.assertEquals('Invalid geo "abc"', ctx.exception.message)
with self.assertRaises(Exception) as ctx:
ARecord(self.zone, 'geo', {'ttl': 42,
'values': ['5.2.3.4', '6.2.3.4'],
'geo': {'NA-US': ['1.1.1']}})
self.assertTrue('not a valid ip' in ctx.exception.message)
# __repr__ doesn't blow up # __repr__ doesn't blow up
geo.__repr__() geo.__repr__()
@ -187,30 +152,12 @@ class TestRecord(TestCase):
self.assertEquals([b_value], b.values) self.assertEquals([b_value], b.values)
self.assertEquals(b_data, b.data) self.assertEquals(b_data, b.data)
# missing values & value
with self.assertRaises(Exception) as ctx:
_type(self.zone, None, {'ttl': 42})
self.assertTrue('missing value(s)' in ctx.exception.message)
def test_aaaa(self): def test_aaaa(self):
a_values = ['2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b', a_values = ['2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b',
'2001:0db8:3c4d:0015:0000:0000:1a2f:1a3b'] '2001:0db8:3c4d:0015:0000:0000:1a2f:1a3b']
b_value = '2001:0db8:3c4d:0015:0000:0000:1a2f:1a4b' b_value = '2001:0db8:3c4d:0015:0000:0000:1a2f:1a4b'
self.assertMultipleValues(AaaaRecord, a_values, b_value) self.assertMultipleValues(AaaaRecord, a_values, b_value)
with self.assertRaises(Exception) as ctx:
AaaaRecord(self.zone, 'a', {
'ttl': 30,
'value': 'foo',
})
self.assertTrue('Invalid record' in ctx.exception.message)
with self.assertRaises(Exception) as ctx:
AaaaRecord(self.zone, 'a', {
'ttl': 30,
'values': [b_value, 'bar'],
})
self.assertTrue('Invalid record' in ctx.exception.message)
def assertSingleValue(self, _type, a_value, b_value): def assertSingleValue(self, _type, a_value, b_value):
a_data = {'ttl': 30, 'value': a_value} a_data = {'ttl': 30, 'value': a_value}
a = _type(self.zone, 'a', a_data) a = _type(self.zone, 'a', a_data)
@ -225,11 +172,6 @@ class TestRecord(TestCase):
self.assertEquals(b_value, b.value) self.assertEquals(b_value, b.value)
self.assertEquals(b_data, b.data) self.assertEquals(b_data, b.data)
# missing value
with self.assertRaises(Exception) as ctx:
_type(self.zone, None, {'ttl': 42})
self.assertTrue('missing value' in ctx.exception.message)
target = SimpleProvider() target = SimpleProvider()
# No changes with self # No changes with self
self.assertFalse(a.changes(a, target)) self.assertFalse(a.changes(a, target))
@ -251,15 +193,6 @@ class TestRecord(TestCase):
self.assertEquals(a_data['value'], a.value) self.assertEquals(a_data['value'], a.value)
self.assertEquals(a_data, a.data) self.assertEquals(a_data, a.data)
# missing value
with self.assertRaises(Exception) as ctx:
AliasRecord(self.zone, None, {'ttl': 0})
self.assertTrue('missing value' in ctx.exception.message)
# bad name
with self.assertRaises(Exception) as ctx:
AliasRecord(self.zone, None, {'ttl': 0, 'value': 'www.unit.tests'})
self.assertTrue('missing trailing .' in ctx.exception.message)
target = SimpleProvider() target = SimpleProvider()
# No changes with self # No changes with self
self.assertFalse(a.changes(a, target)) self.assertFalse(a.changes(a, target))
@ -277,19 +210,6 @@ class TestRecord(TestCase):
self.assertSingleValue(CnameRecord, 'target.foo.com.', self.assertSingleValue(CnameRecord, 'target.foo.com.',
'other.foo.com.') 'other.foo.com.')
with self.assertRaises(Exception) as ctx:
CnameRecord(self.zone, 'a', {
'ttl': 30,
'value': 'foo',
})
self.assertTrue('Invalid record' in ctx.exception.message)
with self.assertRaises(Exception) as ctx:
CnameRecord(self.zone, 'a', {
'ttl': 30,
'values': ['foo.com.', 'bar.com'],
})
self.assertTrue('Invalid record' in ctx.exception.message)
def test_mx(self): def test_mx(self):
a_values = [{ a_values = [{
'priority': 10, 'priority': 10,
@ -319,15 +239,6 @@ class TestRecord(TestCase):
self.assertEquals(b_value['value'], b.values[0].value) self.assertEquals(b_value['value'], b.values[0].value)
self.assertEquals(b_data, b.data) self.assertEquals(b_data, b.data)
# missing value
with self.assertRaises(Exception) as ctx:
MxRecord(self.zone, None, {'ttl': 42})
self.assertTrue('missing value(s)' in ctx.exception.message)
# invalid value
with self.assertRaises(Exception) as ctx:
MxRecord(self.zone, None, {'ttl': 42, 'value': {}})
self.assertTrue('Invalid value' in ctx.exception.message)
target = SimpleProvider() target = SimpleProvider()
# No changes with self # No changes with self
self.assertFalse(a.changes(a, target)) self.assertFalse(a.changes(a, target))
@ -387,15 +298,6 @@ class TestRecord(TestCase):
self.assertEquals(b_value[k], getattr(b.values[0], k)) self.assertEquals(b_value[k], getattr(b.values[0], k))
self.assertEquals(b_data, b.data) self.assertEquals(b_data, b.data)
# missing value
with self.assertRaises(Exception) as ctx:
NaptrRecord(self.zone, None, {'ttl': 42})
self.assertTrue('missing value' in ctx.exception.message)
# invalid value
with self.assertRaises(Exception) as ctx:
NaptrRecord(self.zone, None, {'ttl': 42, 'value': {}})
self.assertTrue('Invalid value' in ctx.exception.message)
target = SimpleProvider() target = SimpleProvider()
# No changes with self # No changes with self
self.assertFalse(a.changes(a, target)) self.assertFalse(a.changes(a, target))
@ -538,33 +440,6 @@ class TestRecord(TestCase):
self.assertEquals([b_value], b.values) self.assertEquals([b_value], b.values)
self.assertEquals(b_data, b.data) self.assertEquals(b_data, b.data)
# missing values & value
with self.assertRaises(Exception) as ctx:
NsRecord(self.zone, None, {'ttl': 42})
self.assertTrue('missing value(s)' in ctx.exception.message)
with self.assertRaises(Exception) as ctx:
NsRecord(self.zone, 'a', {
'ttl': 30,
'value': 'foo',
})
self.assertTrue('Invalid record' in ctx.exception.message)
with self.assertRaises(Exception) as ctx:
NsRecord(self.zone, 'a', {
'ttl': 30,
'values': ['foo.com.', 'bar.com'],
})
self.assertTrue('Invalid record' in ctx.exception.message)
def test_ptr(self):
self.assertSingleValue(PtrRecord, 'foo.bar.com.', 'other.bar.com.')
with self.assertRaises(Exception) as ctx:
PtrRecord(self.zone, 'a', {
'ttl': 30,
'value': 'foo',
})
self.assertTrue('Invalid record' in ctx.exception.message)
def test_sshfp(self): def test_sshfp(self):
a_values = [{ a_values = [{
'algorithm': 10, 'algorithm': 10,
@ -599,15 +474,6 @@ class TestRecord(TestCase):
self.assertEquals(b_value['fingerprint'], b.values[0].fingerprint) self.assertEquals(b_value['fingerprint'], b.values[0].fingerprint)
self.assertEquals(b_data, b.data) self.assertEquals(b_data, b.data)
# missing value
with self.assertRaises(Exception) as ctx:
SshfpRecord(self.zone, None, {'ttl': 42})
self.assertTrue('missing value(s)' in ctx.exception.message)
# invalid value
with self.assertRaises(Exception) as ctx:
SshfpRecord(self.zone, None, {'ttl': 42, 'value': {}})
self.assertTrue('Invalid value' in ctx.exception.message)
target = SimpleProvider() target = SimpleProvider()
# No changes with self # No changes with self
self.assertFalse(a.changes(a, target)) self.assertFalse(a.changes(a, target))
@ -677,21 +543,6 @@ class TestRecord(TestCase):
self.assertEquals(b_value['target'], b.values[0].target) self.assertEquals(b_value['target'], b.values[0].target)
self.assertEquals(b_data, b.data) self.assertEquals(b_data, b.data)
# invalid name
with self.assertRaises(Exception) as ctx:
SrvRecord(self.zone, 'bad', {'ttl': 42})
self.assertEquals('Invalid name bad.unit.tests.',
ctx.exception.message)
# missing value
with self.assertRaises(Exception) as ctx:
SrvRecord(self.zone, '_missing._tcp', {'ttl': 42})
self.assertTrue('missing value(s)' in ctx.exception.message)
# invalid value
with self.assertRaises(Exception) as ctx:
SrvRecord(self.zone, '_missing._udp', {'ttl': 42, 'value': {}})
self.assertTrue('Invalid value' in ctx.exception.message)
target = SimpleProvider() target = SimpleProvider()
# No changes with self # No changes with self
self.assertFalse(a.changes(a, target)) self.assertFalse(a.changes(a, target))
@ -729,21 +580,6 @@ class TestRecord(TestCase):
b_value = 'b other' b_value = 'b other'
self.assertMultipleValues(TxtRecord, a_values, b_value) self.assertMultipleValues(TxtRecord, a_values, b_value)
Record.new(self.zone, 'txt', {
'ttl': 44,
'type': 'TXT',
'value': 'escaped\; foo',
})
with self.assertRaises(Exception) as ctx:
Record.new(self.zone, 'txt', {
'ttl': 44,
'type': 'TXT',
'value': 'un-escaped; foo',
})
self.assertEquals('Invalid record txt.unit.tests., unescaped ;',
ctx.exception.message)
def test_record_new(self): def test_record_new(self):
txt = Record.new(self.zone, 'txt', { txt = Record.new(self.zone, 'txt', {
'ttl': 44, 'ttl': 44,
@ -794,3 +630,642 @@ class TestRecord(TestCase):
self.assertEquals('CA', geo.subdivision_code) self.assertEquals('CA', geo.subdivision_code)
self.assertEquals(values, geo.values) self.assertEquals(values, geo.values)
self.assertEquals(['NA-US', 'NA'], list(geo.parents)) self.assertEquals(['NA-US', 'NA'], list(geo.parents))
class TestRecordValidation(TestCase):
zone = Zone('unit.tests.', [])
def test_base(self):
# no ttl
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'A',
'value': '1.2.3.4',
})
self.assertEquals(['missing ttl'], ctx.exception.reasons)
# invalid ttl
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, 'www', {
'type': 'A',
'ttl': -1,
'value': '1.2.3.4',
})
self.assertEquals('www.unit.tests.', ctx.exception.fqdn)
self.assertEquals(['invalid ttl'], ctx.exception.reasons)
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)
def test_geo(self):
Record.new(self.zone, '', {
'geo': {
'NA': ['1.2.3.5'],
'NA-US': ['1.2.3.5', '1.2.3.6']
},
'type': 'A',
'ttl': 600,
'value': '1.2.3.4',
})
# invalid ip address
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'geo': {
'NA': ['hello'],
'NA-US': ['1.2.3.5', '1.2.3.6']
},
'type': 'A',
'ttl': 600,
'value': '1.2.3.4',
})
self.assertEquals(['invalid ip address "hello"'],
ctx.exception.reasons)
# invalid geo code
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'geo': {
'XYZ': ['1.2.3.4'],
},
'type': 'A',
'ttl': 600,
'value': '1.2.3.4',
})
self.assertEquals(['invalid geo "XYZ"'], ctx.exception.reasons)
# invalid ip address
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'geo': {
'NA': ['hello'],
'NA-US': ['1.2.3.5', 'goodbye']
},
'type': 'A',
'ttl': 600,
'value': '1.2.3.4',
})
self.assertEquals([
'invalid ip address "hello"',
'invalid ip address "goodbye"'
], ctx.exception.reasons)
def test_AAAA(self):
# doesn't blow up
Record.new(self.zone, '', {
'type': 'AAAA',
'ttl': 600,
'value': '2601:644:500:e210:62f8:1dff:feb8:947a',
})
Record.new(self.zone, '', {
'type': 'AAAA',
'ttl': 600,
'values': [
'2601:644:500:e210:62f8:1dff:feb8:947a',
'2601:644:500:e210:62f8:1dff:feb8:947b',
]
})
# invalid ip address
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'AAAA',
'ttl': 600,
'value': 'hello'
})
self.assertEquals(['invalid ip address "hello"'],
ctx.exception.reasons)
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'AAAA',
'ttl': 600,
'value': '1.2.3.4'
})
self.assertEquals(['invalid ip address "1.2.3.4"'],
ctx.exception.reasons)
# invalid ip addresses
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'AAAA',
'ttl': 600,
'values': ['hello', 'goodbye']
})
self.assertEquals([
'invalid ip address "hello"',
'invalid ip address "goodbye"'
], ctx.exception.reasons)
def test_ALIAS_and_value_mixin(self):
# doesn't blow up
Record.new(self.zone, '', {
'type': 'ALIAS',
'ttl': 600,
'value': 'foo.bar.com.',
})
# missing value
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'ALIAS',
'ttl': 600,
})
self.assertEquals(['missing value'], ctx.exception.reasons)
# missing trailing .
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'ALIAS',
'ttl': 600,
'value': 'foo.bar.com',
})
self.assertEquals(['missing trailing .'], ctx.exception.reasons)
def test_CNAME(self):
# doesn't blow up
Record.new(self.zone, '', {
'type': 'CNAME',
'ttl': 600,
'value': 'foo.bar.com.',
})
# missing trailing .
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'CNAME',
'ttl': 600,
'value': 'foo.bar.com',
})
self.assertEquals(['missing trailing .'], ctx.exception.reasons)
def test_MX(self):
# doesn't blow up
Record.new(self.zone, '', {
'type': 'MX',
'ttl': 600,
'value': {
'priority': 10,
'value': 'foo.bar.com.'
}
})
# missing priority
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'MX',
'ttl': 600,
'value': {
'value': 'foo.bar.com.'
}
})
self.assertEquals(['missing priority'], ctx.exception.reasons)
# missing value
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'MX',
'ttl': 600,
'value': {
'priority': 10,
}
})
self.assertEquals(['missing value'], ctx.exception.reasons)
def test_NXPTR(self):
# doesn't blow up
Record.new(self.zone, '', {
'type': 'NAPTR',
'ttl': 600,
'value': {
'order': 10,
'preference': 20,
'flags': 'f',
'service': 'srv',
'regexp': '.*',
'replacement': '.'
}
})
# missing X priority
value = {
'order': 10,
'preference': 20,
'flags': 'f',
'service': 'srv',
'regexp': '.*',
'replacement': '.'
}
for k in ('order', 'preference', 'flags', 'service', 'regexp',
'replacement'):
v = dict(value)
del v[k]
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'NAPTR',
'ttl': 600,
'value': v
})
self.assertEquals(['missing {}'.format(k)], ctx.exception.reasons)
# non-int order
v = dict(value)
v['order'] = 'boo'
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'NAPTR',
'ttl': 600,
'value': v
})
self.assertEquals(['invalid order "boo"'], ctx.exception.reasons)
# non-int preference
v = dict(value)
v['preference'] = 'who'
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'NAPTR',
'ttl': 600,
'value': v
})
self.assertEquals(['invalid preference "who"'], ctx.exception.reasons)
def test_NS(self):
# doesn't blow up
Record.new(self.zone, '', {
'type': 'NS',
'ttl': 600,
'values': [
'foo.bar.com.',
'1.2.3.4.'
]
})
# missing value
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'NS',
'ttl': 600,
})
self.assertEquals(['missing value(s)'], ctx.exception.reasons)
# no trailing .
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'NS',
'ttl': 600,
'value': 'foo.bar',
})
self.assertEquals(['missing trailing .'], ctx.exception.reasons)
def test_PTR(self):
# doesn't blow up (name & zone here don't make any sense, but not
# important)
Record.new(self.zone, '', {
'type': 'PTR',
'ttl': 600,
'value': 'foo.bar.com.',
})
# missing value
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'PTR',
'ttl': 600,
})
self.assertEquals(['missing value'], ctx.exception.reasons)
# no trailing .
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'PTR',
'ttl': 600,
'value': 'foo.bar',
})
self.assertEquals(['missing trailing .'], ctx.exception.reasons)
def test_SSHFP(self):
# doesn't blow up
Record.new(self.zone, '', {
'type': 'SSHFP',
'ttl': 600,
'value': {
'algorithm': 1,
'fingerprint_type': 1,
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
}
})
# missing algorithm
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'SSHFP',
'ttl': 600,
'value': {
'fingerprint_type': 1,
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
}
})
self.assertEquals(['missing algorithm'], ctx.exception.reasons)
# invalid algorithm
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'SSHFP',
'ttl': 600,
'value': {
'algorithm': 'nope',
'fingerprint_type': 1,
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
}
})
self.assertEquals(['invalid algorithm "nope"'], ctx.exception.reasons)
# missing fingerprint_type
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'SSHFP',
'ttl': 600,
'value': {
'algorithm': 1,
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
}
})
self.assertEquals(['missing fingerprint_type'], ctx.exception.reasons)
# invalid fingerprint_type
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'SSHFP',
'ttl': 600,
'value': {
'algorithm': 1,
'fingerprint_type': 'yeeah',
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
}
})
self.assertEquals(['invalid fingerprint_type "yeeah"'],
ctx.exception.reasons)
# missing fingerprint
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'SSHFP',
'ttl': 600,
'value': {
'algorithm': 1,
'fingerprint_type': 1,
}
})
self.assertEquals(['missing fingerprint'], ctx.exception.reasons)
def test_SPF(self):
# doesn't blow up (name & zone here don't make any sense, but not
# important)
Record.new(self.zone, '', {
'type': 'SPF',
'ttl': 600,
'values': [
'v=spf1 ip4:192.168.0.1/16-all',
'v=spf1 ip4:10.1.2.1/24-all',
'this has some\; semi-colons\; in it',
]
})
# missing value
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'SPF',
'ttl': 600,
})
self.assertEquals(['missing value(s)'], ctx.exception.reasons)
# missing escapes
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'SPF',
'ttl': 600,
'value': 'this has some; semi-colons\; in it',
})
self.assertEquals(['unescaped ;'], ctx.exception.reasons)
def test_SRV(self):
# doesn't blow up
Record.new(self.zone, '_srv._tcp', {
'type': 'SRV',
'ttl': 600,
'value': {
'priority': 1,
'weight': 2,
'port': 3,
'target': 'foo.bar.baz.'
}
})
# invalid name
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, 'neup', {
'type': 'SRV',
'ttl': 600,
'value': {
'priority': 1,
'weight': 2,
'port': 3,
'target': 'foo.bar.baz.'
}
})
self.assertEquals(['invalid name'], ctx.exception.reasons)
# missing priority
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '_srv._tcp', {
'type': 'SRV',
'ttl': 600,
'value': {
'weight': 2,
'port': 3,
'target': 'foo.bar.baz.'
}
})
self.assertEquals(['missing priority'], ctx.exception.reasons)
# invalid priority
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '_srv._tcp', {
'type': 'SRV',
'ttl': 600,
'value': {
'priority': 'foo',
'weight': 2,
'port': 3,
'target': 'foo.bar.baz.'
}
})
self.assertEquals(['invalid priority "foo"'], ctx.exception.reasons)
# missing weight
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '_srv._tcp', {
'type': 'SRV',
'ttl': 600,
'value': {
'priority': 1,
'port': 3,
'target': 'foo.bar.baz.'
}
})
self.assertEquals(['missing weight'], ctx.exception.reasons)
# invalid weight
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '_srv._tcp', {
'type': 'SRV',
'ttl': 600,
'value': {
'priority': 1,
'weight': 'foo',
'port': 3,
'target': 'foo.bar.baz.'
}
})
self.assertEquals(['invalid weight "foo"'], ctx.exception.reasons)
# missing port
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '_srv._tcp', {
'type': 'SRV',
'ttl': 600,
'value': {
'priority': 1,
'weight': 2,
'target': 'foo.bar.baz.'
}
})
self.assertEquals(['missing port'], ctx.exception.reasons)
# invalid port
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '_srv._tcp', {
'type': 'SRV',
'ttl': 600,
'value': {
'priority': 1,
'weight': 2,
'port': 'foo',
'target': 'foo.bar.baz.'
}
})
self.assertEquals(['invalid port "foo"'], ctx.exception.reasons)
# missing target
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '_srv._tcp', {
'type': 'SRV',
'ttl': 600,
'value': {
'priority': 1,
'weight': 2,
'port': 3,
}
})
self.assertEquals(['missing target'], ctx.exception.reasons)
# invalid target
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '_srv._tcp', {
'type': 'SRV',
'ttl': 600,
'value': {
'priority': 1,
'weight': 2,
'port': 3,
'target': 'foo.bar.baz'
}
})
self.assertEquals(['missing trailing .'],
ctx.exception.reasons)
def test_TXT(self):
# doesn't blow up (name & zone here don't make any sense, but not
# important)
Record.new(self.zone, '', {
'type': 'TXT',
'ttl': 600,
'values': [
'hello world',
'this has some\; semi-colons\; in it',
]
})
# missing value
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'TXT',
'ttl': 600,
})
self.assertEquals(['missing value(s)'], ctx.exception.reasons)
# missing escapes
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {
'type': 'TXT',
'ttl': 600,
'value': 'this has some; semi-colons\; in it',
})
self.assertEquals(['unescaped ;'], ctx.exception.reasons)

Loading…
Cancel
Save