| @ -0,0 +1,181 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.a import ARecord | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.zone import Zone | |||||
| from helpers import SimpleProvider | |||||
| class TestRecordA(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def test_a_and_record(self): | |||||
| a_values = ['1.2.3.4', '2.2.3.4'] | |||||
| a_data = {'ttl': 30, 'values': a_values} | |||||
| a = ARecord(self.zone, 'a', a_data) | |||||
| self.assertEqual('a', a.name) | |||||
| self.assertEqual('a.unit.tests.', a.fqdn) | |||||
| self.assertEqual(30, a.ttl) | |||||
| self.assertEqual(a_values, a.values) | |||||
| self.assertEqual(a_data, a.data) | |||||
| b_value = '3.2.3.4' | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = ARecord(self.zone, 'b', b_data) | |||||
| self.assertEqual([b_value], b.values) | |||||
| self.assertEqual(b_data, b.data) | |||||
| # top-level | |||||
| data = {'ttl': 30, 'value': '4.2.3.4'} | |||||
| self.assertEqual(self.zone.name, ARecord(self.zone, '', data).fqdn) | |||||
| self.assertEqual(self.zone.name, ARecord(self.zone, None, data).fqdn) | |||||
| # ARecord equate with itself | |||||
| self.assertTrue(a == a) | |||||
| # Records with differing names and same type don't equate | |||||
| self.assertFalse(a == b) | |||||
| # Records with same name & type equate even if ttl is different | |||||
| self.assertTrue( | |||||
| a == ARecord(self.zone, 'a', {'ttl': 31, 'values': a_values}) | |||||
| ) | |||||
| # Records with same name & type equate even if values are different | |||||
| self.assertTrue( | |||||
| a == ARecord(self.zone, 'a', {'ttl': 30, 'value': b_value}) | |||||
| ) | |||||
| target = SimpleProvider() | |||||
| # no changes if self | |||||
| self.assertFalse(a.changes(a, target)) | |||||
| # no changes if clone | |||||
| other = ARecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| self.assertFalse(a.changes(other, target)) | |||||
| # changes if ttl modified | |||||
| other.ttl = 31 | |||||
| update = a.changes(other, target) | |||||
| self.assertEqual(a, update.existing) | |||||
| self.assertEqual(other, update.new) | |||||
| # changes if values modified | |||||
| other.ttl = a.ttl | |||||
| other.values = ['4.4.4.4'] | |||||
| update = a.changes(other, target) | |||||
| self.assertEqual(a, update.existing) | |||||
| self.assertEqual(other, update.new) | |||||
| # Hashing | |||||
| records = set() | |||||
| records.add(a) | |||||
| self.assertTrue(a in records) | |||||
| self.assertFalse(b in records) | |||||
| records.add(b) | |||||
| self.assertTrue(b in records) | |||||
| # __repr__ doesn't blow up | |||||
| a.__repr__() | |||||
| # Record.__repr__ does | |||||
| with self.assertRaises(NotImplementedError): | |||||
| class DummyRecord(Record): | |||||
| def __init__(self): | |||||
| pass | |||||
| DummyRecord().__repr__() | |||||
| def test_validation_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']} | |||||
| ) | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| {'type': 'A', 'ttl': 600, 'values': ['1.2.3.4', '1.2.3.5']}, | |||||
| ) | |||||
| # missing value(s), no value or value | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new(self.zone, '', {'type': 'A', 'ttl': 600}) | |||||
| self.assertEqual(['missing value(s)'], ctx.exception.reasons) | |||||
| # missing value(s), empty values | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, 'www', {'type': 'A', 'ttl': 600, 'values': []} | |||||
| ) | |||||
| self.assertEqual(['missing value(s)'], ctx.exception.reasons) | |||||
| # missing value(s), None values | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, 'www', {'type': 'A', 'ttl': 600, 'values': None} | |||||
| ) | |||||
| self.assertEqual(['missing value(s)'], ctx.exception.reasons) | |||||
| # missing value(s) and empty value | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| 'www', | |||||
| {'type': 'A', 'ttl': 600, 'values': [None, '']}, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['missing value(s)', 'empty value'], ctx.exception.reasons | |||||
| ) | |||||
| # missing value(s), None value | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, 'www', {'type': 'A', 'ttl': 600, 'value': None} | |||||
| ) | |||||
| self.assertEqual(['missing value(s)'], ctx.exception.reasons) | |||||
| # empty value, empty string value | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new(self.zone, 'www', {'type': 'A', 'ttl': 600, 'value': ''}) | |||||
| self.assertEqual(['empty value'], ctx.exception.reasons) | |||||
| # missing value(s) & ttl | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new(self.zone, '', {'type': 'A'}) | |||||
| self.assertEqual( | |||||
| ['missing ttl', 'missing value(s)'], ctx.exception.reasons | |||||
| ) | |||||
| # invalid ipv4 address | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, '', {'type': 'A', 'ttl': 600, 'value': 'hello'} | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid IPv4 address "hello"'], ctx.exception.reasons | |||||
| ) | |||||
| # invalid ipv4 addresses | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| {'type': 'A', 'ttl': 600, 'values': ['hello', 'goodbye']}, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid IPv4 address "hello"', 'invalid IPv4 address "goodbye"'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| # invalid & valid ipv4 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.assertEqual( | |||||
| ['missing ttl', 'invalid IPv4 address "hello"'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| @ -0,0 +1,227 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.aaaa import AaaaRecord | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.zone import Zone | |||||
| class TestRecordAaaa(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def assertMultipleValues(self, _type, a_values, b_value): | |||||
| a_data = {'ttl': 30, 'values': a_values} | |||||
| a = _type(self.zone, 'a', a_data) | |||||
| self.assertEqual('a', a.name) | |||||
| self.assertEqual('a.unit.tests.', a.fqdn) | |||||
| self.assertEqual(30, a.ttl) | |||||
| self.assertEqual(a_values, a.values) | |||||
| self.assertEqual(a_data, a.data) | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = _type(self.zone, 'b', b_data) | |||||
| self.assertEqual([b_value], b.values) | |||||
| self.assertEqual(b_data, b.data) | |||||
| def test_aaaa(self): | |||||
| 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.assertEqual(a_values, record.values) | |||||
| def test_validation(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'], | |||||
| }, | |||||
| ) | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'AAAA', | |||||
| 'ttl': 600, | |||||
| 'values': [ | |||||
| '2601:644:500:e210:62f8:1dff:feb8:947a', | |||||
| '2601:642:500:e210:62f8:1dff:feb8:947a', | |||||
| ], | |||||
| }, | |||||
| ) | |||||
| # missing value(s), no value or value | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new(self.zone, '', {'type': 'AAAA', 'ttl': 600}) | |||||
| self.assertEqual(['missing value(s)'], ctx.exception.reasons) | |||||
| # missing value(s), empty values | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, 'www', {'type': 'AAAA', 'ttl': 600, 'values': []} | |||||
| ) | |||||
| self.assertEqual(['missing value(s)'], ctx.exception.reasons) | |||||
| # missing value(s), None values | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, 'www', {'type': 'AAAA', 'ttl': 600, 'values': None} | |||||
| ) | |||||
| self.assertEqual(['missing value(s)'], ctx.exception.reasons) | |||||
| # missing value(s) and empty value | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| 'www', | |||||
| {'type': 'AAAA', 'ttl': 600, 'values': [None, '']}, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['missing value(s)', 'empty value'], ctx.exception.reasons | |||||
| ) | |||||
| # missing value(s), None value | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, 'www', {'type': 'AAAA', 'ttl': 600, 'value': None} | |||||
| ) | |||||
| self.assertEqual(['missing value(s)'], ctx.exception.reasons) | |||||
| # empty value, empty string value | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, 'www', {'type': 'AAAA', 'ttl': 600, 'value': ''} | |||||
| ) | |||||
| self.assertEqual(['empty value'], ctx.exception.reasons) | |||||
| # missing value(s) & ttl | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new(self.zone, '', {'type': 'AAAA'}) | |||||
| self.assertEqual( | |||||
| ['missing ttl', 'missing value(s)'], ctx.exception.reasons | |||||
| ) | |||||
| # invalid IPv6 address | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, '', {'type': 'AAAA', 'ttl': 600, 'value': 'hello'} | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid IPv6 address "hello"'], ctx.exception.reasons | |||||
| ) | |||||
| # invalid IPv6 addresses | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| {'type': 'AAAA', 'ttl': 600, 'values': ['hello', 'goodbye']}, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid IPv6 address "hello"', 'invalid IPv6 address "goodbye"'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| # invalid & valid IPv6 addresses, no ttl | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'AAAA', | |||||
| 'values': [ | |||||
| '2601:644:500:e210:62f8:1dff:feb8:947a', | |||||
| 'hello', | |||||
| '2601:642:500:e210:62f8:1dff:feb8:947a', | |||||
| ], | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['missing ttl', 'invalid IPv6 address "hello"'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| def test_more_validation(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.assertEqual( | |||||
| ['invalid IPv6 address "hello"'], ctx.exception.reasons | |||||
| ) | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| {'type': 'AAAA', 'ttl': 600, 'values': ['1.2.3.4', '2.3.4.5']}, | |||||
| ) | |||||
| self.assertEqual( | |||||
| [ | |||||
| 'invalid IPv6 address "1.2.3.4"', | |||||
| 'invalid IPv6 address "2.3.4.5"', | |||||
| ], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| # invalid ip addresses | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| {'type': 'AAAA', 'ttl': 600, 'values': ['hello', 'goodbye']}, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid IPv6 address "hello"', 'invalid IPv6 address "goodbye"'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| @ -0,0 +1,108 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.alias import AliasRecord | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.zone import Zone | |||||
| from helpers import SimpleProvider | |||||
| class TestRecordAlias(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def test_alias(self): | |||||
| a_data = {'ttl': 0, 'value': 'www.unit.tests.'} | |||||
| a = AliasRecord(self.zone, '', a_data) | |||||
| self.assertEqual('', a.name) | |||||
| self.assertEqual('unit.tests.', a.fqdn) | |||||
| self.assertEqual(0, a.ttl) | |||||
| self.assertEqual(a_data['value'], a.value) | |||||
| self.assertEqual(a_data, a.data) | |||||
| target = SimpleProvider() | |||||
| # No changes with self | |||||
| self.assertFalse(a.changes(a, target)) | |||||
| # Diff in value causes change | |||||
| other = AliasRecord(self.zone, 'a', a_data) | |||||
| other.value = 'foo.unit.tests.' | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # __repr__ doesn't blow up | |||||
| a.__repr__() | |||||
| def test_alias_lowering_value(self): | |||||
| upper_record = AliasRecord( | |||||
| self.zone, | |||||
| 'aliasUppwerValue', | |||||
| {'ttl': 30, 'type': 'ALIAS', 'value': 'GITHUB.COM'}, | |||||
| ) | |||||
| lower_record = AliasRecord( | |||||
| self.zone, | |||||
| 'aliasLowerValue', | |||||
| {'ttl': 30, 'type': 'ALIAS', 'value': 'github.com'}, | |||||
| ) | |||||
| self.assertEqual(upper_record.value, lower_record.value) | |||||
| def test_validation_and_value_mixin(self): | |||||
| # doesn't blow up | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| {'type': 'ALIAS', 'ttl': 600, 'value': 'foo.bar.com.'}, | |||||
| ) | |||||
| # root only | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| 'nope', | |||||
| {'type': 'ALIAS', 'ttl': 600, 'value': 'foo.bar.com.'}, | |||||
| ) | |||||
| self.assertEqual(['non-root ALIAS not allowed'], ctx.exception.reasons) | |||||
| # missing value | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new(self.zone, '', {'type': 'ALIAS', 'ttl': 600}) | |||||
| self.assertEqual(['missing value'], ctx.exception.reasons) | |||||
| # missing value | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, '', {'type': 'ALIAS', 'ttl': 600, 'value': None} | |||||
| ) | |||||
| self.assertEqual(['missing value'], ctx.exception.reasons) | |||||
| # empty value | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, '', {'type': 'ALIAS', 'ttl': 600, 'value': ''} | |||||
| ) | |||||
| self.assertEqual(['empty value'], ctx.exception.reasons) | |||||
| # not a valid FQDN | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, '', {'type': 'ALIAS', 'ttl': 600, 'value': '__.'} | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['ALIAS value "__." is not a valid FQDN'], ctx.exception.reasons | |||||
| ) | |||||
| # missing trailing . | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| {'type': 'ALIAS', 'ttl': 600, 'value': 'foo.bar.com'}, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['ALIAS value "foo.bar.com" missing trailing .'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| @ -0,0 +1,273 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.caa import CaaRecord, CaaValue | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.record.rr import RrParseError | |||||
| from octodns.zone import Zone | |||||
| from helpers import SimpleProvider | |||||
| class TestRecordCaa(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def test_caa(self): | |||||
| a_values = [ | |||||
| CaaValue({'flags': 0, 'tag': 'issue', 'value': 'ca.example.net'}), | |||||
| CaaValue( | |||||
| { | |||||
| 'flags': 128, | |||||
| 'tag': 'iodef', | |||||
| 'value': 'mailto:security@example.com', | |||||
| } | |||||
| ), | |||||
| ] | |||||
| a_data = {'ttl': 30, 'values': a_values} | |||||
| a = CaaRecord(self.zone, 'a', a_data) | |||||
| self.assertEqual('a', a.name) | |||||
| self.assertEqual('a.unit.tests.', a.fqdn) | |||||
| self.assertEqual(30, a.ttl) | |||||
| self.assertEqual(a_values[0]['flags'], a.values[0].flags) | |||||
| self.assertEqual(a_values[0]['tag'], a.values[0].tag) | |||||
| self.assertEqual(a_values[0]['value'], a.values[0].value) | |||||
| self.assertEqual(a_values[1]['flags'], a.values[1].flags) | |||||
| self.assertEqual(a_values[1]['tag'], a.values[1].tag) | |||||
| self.assertEqual(a_values[1]['value'], a.values[1].value) | |||||
| self.assertEqual(a_data, a.data) | |||||
| b_value = CaaValue( | |||||
| {'tag': 'iodef', 'value': 'http://iodef.example.com/'} | |||||
| ) | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = CaaRecord(self.zone, 'b', b_data) | |||||
| self.assertEqual(0, b.values[0].flags) | |||||
| self.assertEqual(b_value['tag'], b.values[0].tag) | |||||
| self.assertEqual(b_value['value'], b.values[0].value) | |||||
| b_data['value']['flags'] = 0 | |||||
| self.assertEqual(b_data, b.data) | |||||
| target = SimpleProvider() | |||||
| # No changes with self | |||||
| self.assertFalse(a.changes(a, target)) | |||||
| # Diff in flags causes change | |||||
| other = CaaRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].flags = 128 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in tag causes change | |||||
| other.values[0].flags = a.values[0].flags | |||||
| other.values[0].tag = 'foo' | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in value causes change | |||||
| other.values[0].tag = a.values[0].tag | |||||
| other.values[0].value = 'bar' | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # __repr__ doesn't blow up | |||||
| a.__repr__() | |||||
| def test_caa_value_rdata_text(self): | |||||
| # empty string won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| CaaValue.parse_rdata_text('') | |||||
| # single word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| CaaValue.parse_rdata_text('nope') | |||||
| # 2nd word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| CaaValue.parse_rdata_text('0 tag') | |||||
| # 4th word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| CaaValue.parse_rdata_text('1 tag value another') | |||||
| # flags not an int, will parse | |||||
| self.assertEqual( | |||||
| {'flags': 'one', 'tag': 'tag', 'value': 'value'}, | |||||
| CaaValue.parse_rdata_text('one tag value'), | |||||
| ) | |||||
| # valid | |||||
| self.assertEqual( | |||||
| {'flags': 0, 'tag': 'tag', 'value': '99148c81'}, | |||||
| CaaValue.parse_rdata_text('0 tag 99148c81'), | |||||
| ) | |||||
| zone = Zone('unit.tests.', []) | |||||
| a = CaaRecord( | |||||
| zone, | |||||
| 'caa', | |||||
| { | |||||
| 'ttl': 32, | |||||
| 'values': [ | |||||
| {'flags': 1, 'tag': 'tag1', 'value': '99148c81'}, | |||||
| {'flags': 2, 'tag': 'tag2', 'value': '99148c44'}, | |||||
| ], | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(1, a.values[0].flags) | |||||
| self.assertEqual('tag1', a.values[0].tag) | |||||
| self.assertEqual('99148c81', a.values[0].value) | |||||
| self.assertEqual('1 tag1 99148c81', a.values[0].rdata_text) | |||||
| self.assertEqual(2, a.values[1].flags) | |||||
| self.assertEqual('tag2', a.values[1].tag) | |||||
| self.assertEqual('99148c44', a.values[1].value) | |||||
| self.assertEqual('2 tag2 99148c44', a.values[1].rdata_text) | |||||
| def test_caa_value(self): | |||||
| a = CaaValue({'flags': 0, 'tag': 'a', 'value': 'v'}) | |||||
| b = CaaValue({'flags': 1, 'tag': 'a', 'value': 'v'}) | |||||
| c = CaaValue({'flags': 0, 'tag': 'c', 'value': 'v'}) | |||||
| d = CaaValue({'flags': 0, 'tag': 'a', 'value': 'z'}) | |||||
| self.assertEqual(a, a) | |||||
| self.assertEqual(b, b) | |||||
| self.assertEqual(c, c) | |||||
| self.assertEqual(d, d) | |||||
| self.assertNotEqual(a, b) | |||||
| self.assertNotEqual(a, c) | |||||
| self.assertNotEqual(a, d) | |||||
| self.assertNotEqual(b, a) | |||||
| self.assertNotEqual(b, c) | |||||
| self.assertNotEqual(b, d) | |||||
| self.assertNotEqual(c, a) | |||||
| self.assertNotEqual(c, b) | |||||
| self.assertNotEqual(c, d) | |||||
| self.assertTrue(a < b) | |||||
| self.assertTrue(a < c) | |||||
| self.assertTrue(a < d) | |||||
| self.assertTrue(b > a) | |||||
| self.assertTrue(b > c) | |||||
| self.assertTrue(b > d) | |||||
| self.assertTrue(c > a) | |||||
| self.assertTrue(c < b) | |||||
| self.assertTrue(c > d) | |||||
| self.assertTrue(d > a) | |||||
| self.assertTrue(d < b) | |||||
| self.assertTrue(d < c) | |||||
| self.assertTrue(a <= b) | |||||
| self.assertTrue(a <= c) | |||||
| self.assertTrue(a <= d) | |||||
| self.assertTrue(a <= a) | |||||
| self.assertTrue(a >= a) | |||||
| self.assertTrue(b >= a) | |||||
| self.assertTrue(b >= c) | |||||
| self.assertTrue(b >= d) | |||||
| self.assertTrue(b >= b) | |||||
| self.assertTrue(b <= b) | |||||
| self.assertTrue(c >= a) | |||||
| self.assertTrue(c <= b) | |||||
| self.assertTrue(c >= d) | |||||
| self.assertTrue(c >= c) | |||||
| self.assertTrue(c <= c) | |||||
| self.assertTrue(d >= a) | |||||
| self.assertTrue(d <= b) | |||||
| self.assertTrue(d <= c) | |||||
| self.assertTrue(d >= d) | |||||
| self.assertTrue(d <= d) | |||||
| def test_validation(self): | |||||
| # doesn't blow up | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'CAA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'flags': 128, | |||||
| 'tag': 'iodef', | |||||
| 'value': 'http://foo.bar.com/', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| # invalid flags | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'CAA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'flags': -42, | |||||
| 'tag': 'iodef', | |||||
| 'value': 'http://foo.bar.com/', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['invalid flags "-42"'], ctx.exception.reasons) | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'CAA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'flags': 442, | |||||
| 'tag': 'iodef', | |||||
| 'value': 'http://foo.bar.com/', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['invalid flags "442"'], ctx.exception.reasons) | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'CAA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'flags': 'nope', | |||||
| 'tag': 'iodef', | |||||
| 'value': 'http://foo.bar.com/', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['invalid flags "nope"'], ctx.exception.reasons) | |||||
| # missing tag | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'CAA', | |||||
| 'ttl': 600, | |||||
| 'value': {'value': 'http://foo.bar.com/'}, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['missing tag'], ctx.exception.reasons) | |||||
| # missing value | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| {'type': 'CAA', 'ttl': 600, 'value': {'tag': 'iodef'}}, | |||||
| ) | |||||
| self.assertEqual(['missing value'], ctx.exception.reasons) | |||||
| @ -0,0 +1,92 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.change import Create, Delete, Update | |||||
| from octodns.zone import Zone | |||||
| class TestChanges(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| record_a_1 = Record.new( | |||||
| zone, '1', {'type': 'A', 'ttl': 30, 'value': '1.2.3.4'} | |||||
| ) | |||||
| record_a_2 = Record.new( | |||||
| zone, '2', {'type': 'A', 'ttl': 30, 'value': '1.2.3.4'} | |||||
| ) | |||||
| record_aaaa_1 = Record.new( | |||||
| zone, | |||||
| '1', | |||||
| { | |||||
| 'type': 'AAAA', | |||||
| 'ttl': 30, | |||||
| 'value': '2601:644:500:e210:62f8:1dff:feb8:947a', | |||||
| }, | |||||
| ) | |||||
| record_aaaa_2 = Record.new( | |||||
| zone, | |||||
| '2', | |||||
| { | |||||
| 'type': 'AAAA', | |||||
| 'ttl': 30, | |||||
| 'value': '2601:644:500:e210:62f8:1dff:feb8:947a', | |||||
| }, | |||||
| ) | |||||
| def test_sort_same_change_type(self): | |||||
| # expect things to be ordered by name and type since all the change | |||||
| # types are the same it doesn't matter | |||||
| changes = [ | |||||
| Create(self.record_aaaa_1), | |||||
| Create(self.record_a_2), | |||||
| Create(self.record_a_1), | |||||
| Create(self.record_aaaa_2), | |||||
| ] | |||||
| self.assertEqual( | |||||
| [ | |||||
| Create(self.record_a_1), | |||||
| Create(self.record_aaaa_1), | |||||
| Create(self.record_a_2), | |||||
| Create(self.record_aaaa_2), | |||||
| ], | |||||
| sorted(changes), | |||||
| ) | |||||
| def test_sort_same_different_type(self): | |||||
| # this time the change type is the deciding factor, deletes come before | |||||
| # creates, and then updates. Things of the same type, go down the line | |||||
| # and sort by name, and then type | |||||
| changes = [ | |||||
| Delete(self.record_aaaa_1), | |||||
| Create(self.record_aaaa_1), | |||||
| Update(self.record_aaaa_1, self.record_aaaa_1), | |||||
| Update(self.record_a_1, self.record_a_1), | |||||
| Create(self.record_a_1), | |||||
| Delete(self.record_a_1), | |||||
| Delete(self.record_aaaa_2), | |||||
| Create(self.record_aaaa_2), | |||||
| Update(self.record_aaaa_2, self.record_aaaa_2), | |||||
| Update(self.record_a_2, self.record_a_2), | |||||
| Create(self.record_a_2), | |||||
| Delete(self.record_a_2), | |||||
| ] | |||||
| self.assertEqual( | |||||
| [ | |||||
| Delete(self.record_a_1), | |||||
| Delete(self.record_aaaa_1), | |||||
| Delete(self.record_a_2), | |||||
| Delete(self.record_aaaa_2), | |||||
| Create(self.record_a_1), | |||||
| Create(self.record_aaaa_1), | |||||
| Create(self.record_a_2), | |||||
| Create(self.record_aaaa_2), | |||||
| Update(self.record_a_1, self.record_a_1), | |||||
| Update(self.record_aaaa_1, self.record_aaaa_1), | |||||
| Update(self.record_a_2, self.record_a_2), | |||||
| Update(self.record_aaaa_2, self.record_aaaa_2), | |||||
| ], | |||||
| sorted(changes), | |||||
| ) | |||||
| @ -0,0 +1,37 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record.chunked import _ChunkedValue | |||||
| from octodns.record.spf import SpfRecord | |||||
| from octodns.zone import Zone | |||||
| class TestRecordChunked(TestCase): | |||||
| def test_chunked_value_rdata_text(self): | |||||
| for s in ( | |||||
| None, | |||||
| '', | |||||
| 'word', | |||||
| 42, | |||||
| 42.43, | |||||
| '1.2.3', | |||||
| 'some.words.that.here', | |||||
| '1.2.word.4', | |||||
| '1.2.3.4', | |||||
| ): | |||||
| self.assertEqual(s, _ChunkedValue.parse_rdata_text(s)) | |||||
| # semi-colons are escaped | |||||
| self.assertEqual( | |||||
| 'Hello\\; World!', _ChunkedValue.parse_rdata_text('Hello; World!') | |||||
| ) | |||||
| # since we're always a string validate and __init__ don't | |||||
| # parse_rdata_text | |||||
| zone = Zone('unit.tests.', []) | |||||
| a = SpfRecord(zone, 'a', {'ttl': 42, 'value': 'some.target.'}) | |||||
| self.assertEqual('some.target.', a.values[0].rdata_text) | |||||
| @ -0,0 +1,136 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.cname import CnameRecord | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.zone import Zone | |||||
| from helpers import SimpleProvider | |||||
| class TestRecordCname(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def assertSingleValue(self, _type, a_value, b_value): | |||||
| a_data = {'ttl': 30, 'value': a_value} | |||||
| a = _type(self.zone, 'a', a_data) | |||||
| self.assertEqual('a', a.name) | |||||
| self.assertEqual('a.unit.tests.', a.fqdn) | |||||
| self.assertEqual(30, a.ttl) | |||||
| self.assertEqual(a_value, a.value) | |||||
| self.assertEqual(a_data, a.data) | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = _type(self.zone, 'b', b_data) | |||||
| self.assertEqual(b_value, b.value) | |||||
| self.assertEqual(b_data, b.data) | |||||
| target = SimpleProvider() | |||||
| # No changes with self | |||||
| self.assertFalse(a.changes(a, target)) | |||||
| # Diff in value causes change | |||||
| other = _type(self.zone, 'a', {'ttl': 30, 'value': b_value}) | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # __repr__ doesn't blow up | |||||
| a.__repr__() | |||||
| def test_cname(self): | |||||
| self.assertSingleValue(CnameRecord, 'target.foo.com.', 'other.foo.com.') | |||||
| def test_cname_lowering_value(self): | |||||
| upper_record = CnameRecord( | |||||
| self.zone, | |||||
| 'CnameUppwerValue', | |||||
| {'ttl': 30, 'type': 'CNAME', 'value': 'GITHUB.COM'}, | |||||
| ) | |||||
| lower_record = CnameRecord( | |||||
| self.zone, | |||||
| 'CnameLowerValue', | |||||
| {'ttl': 30, 'type': 'CNAME', 'value': 'github.com'}, | |||||
| ) | |||||
| self.assertEqual(upper_record.value, lower_record.value) | |||||
| def test_validation(self): | |||||
| # doesn't blow up | |||||
| Record.new( | |||||
| self.zone, | |||||
| 'www', | |||||
| {'type': 'CNAME', 'ttl': 600, 'value': 'foo.bar.com.'}, | |||||
| ) | |||||
| # root cname is a no-no | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| {'type': 'CNAME', 'ttl': 600, 'value': 'foo.bar.com.'}, | |||||
| ) | |||||
| self.assertEqual(['root CNAME not allowed'], ctx.exception.reasons) | |||||
| # not a valid FQDN | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, 'www', {'type': 'CNAME', 'ttl': 600, 'value': '___.'} | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['CNAME value "___." is not a valid FQDN'], ctx.exception.reasons | |||||
| ) | |||||
| # missing trailing . | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| 'www', | |||||
| {'type': 'CNAME', 'ttl': 600, 'value': 'foo.bar.com'}, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['CNAME value "foo.bar.com" missing trailing .'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| # doesn't allow urls | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| 'www', | |||||
| {'type': 'CNAME', 'ttl': 600, 'value': 'https://google.com'}, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['CNAME value "https://google.com" is not a valid FQDN'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| # doesn't allow urls with paths | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| 'www', | |||||
| { | |||||
| 'type': 'CNAME', | |||||
| 'ttl': 600, | |||||
| 'value': 'https://google.com/a/b/c', | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['CNAME value "https://google.com/a/b/c" is not a valid FQDN'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| # doesn't allow paths | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| 'www', | |||||
| {'type': 'CNAME', 'ttl': 600, 'value': 'google.com/some/path'}, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['CNAME value "google.com/some/path" is not a valid FQDN'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| @ -0,0 +1,94 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.dname import DnameRecord | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.zone import Zone | |||||
| from helpers import SimpleProvider | |||||
| class TestRecordDname(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def assertSingleValue(self, _type, a_value, b_value): | |||||
| a_data = {'ttl': 30, 'value': a_value} | |||||
| a = _type(self.zone, 'a', a_data) | |||||
| self.assertEqual('a', a.name) | |||||
| self.assertEqual('a.unit.tests.', a.fqdn) | |||||
| self.assertEqual(30, a.ttl) | |||||
| self.assertEqual(a_value, a.value) | |||||
| self.assertEqual(a_data, a.data) | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = _type(self.zone, 'b', b_data) | |||||
| self.assertEqual(b_value, b.value) | |||||
| self.assertEqual(b_data, b.data) | |||||
| target = SimpleProvider() | |||||
| # No changes with self | |||||
| self.assertFalse(a.changes(a, target)) | |||||
| # Diff in value causes change | |||||
| other = _type(self.zone, 'a', {'ttl': 30, 'value': b_value}) | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # __repr__ doesn't blow up | |||||
| a.__repr__() | |||||
| def test_dname(self): | |||||
| self.assertSingleValue(DnameRecord, 'target.foo.com.', 'other.foo.com.') | |||||
| def test_dname_lowering_value(self): | |||||
| upper_record = DnameRecord( | |||||
| self.zone, | |||||
| 'DnameUppwerValue', | |||||
| {'ttl': 30, 'type': 'DNAME', 'value': 'GITHUB.COM'}, | |||||
| ) | |||||
| lower_record = DnameRecord( | |||||
| self.zone, | |||||
| 'DnameLowerValue', | |||||
| {'ttl': 30, 'type': 'DNAME', 'value': 'github.com'}, | |||||
| ) | |||||
| self.assertEqual(upper_record.value, lower_record.value) | |||||
| def test_validation(self): | |||||
| # A valid DNAME record. | |||||
| Record.new( | |||||
| self.zone, | |||||
| 'sub', | |||||
| {'type': 'DNAME', 'ttl': 600, 'value': 'foo.bar.com.'}, | |||||
| ) | |||||
| # A DNAME record can be present at the zone APEX. | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| {'type': 'DNAME', 'ttl': 600, 'value': 'foo.bar.com.'}, | |||||
| ) | |||||
| # not a valid FQDN | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, 'www', {'type': 'DNAME', 'ttl': 600, 'value': '.'} | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['DNAME value "." is not a valid FQDN'], ctx.exception.reasons | |||||
| ) | |||||
| # missing trailing . | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| 'www', | |||||
| {'type': 'DNAME', 'ttl': 600, 'value': 'foo.bar.com'}, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['DNAME value "foo.bar.com" missing trailing .'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| @ -0,0 +1,206 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record.ds import DsRecord, DsValue | |||||
| from octodns.record.rr import RrParseError | |||||
| from octodns.zone import Zone | |||||
| class TestRecordDs(TestCase): | |||||
| def test_ds(self): | |||||
| for a, b in ( | |||||
| # diff flags | |||||
| ( | |||||
| { | |||||
| 'flags': 0, | |||||
| 'protocol': 1, | |||||
| 'algorithm': 2, | |||||
| 'public_key': 'abcdef0123456', | |||||
| }, | |||||
| { | |||||
| 'flags': 1, | |||||
| 'protocol': 1, | |||||
| 'algorithm': 2, | |||||
| 'public_key': 'abcdef0123456', | |||||
| }, | |||||
| ), | |||||
| # diff protocol | |||||
| ( | |||||
| { | |||||
| 'flags': 0, | |||||
| 'protocol': 1, | |||||
| 'algorithm': 2, | |||||
| 'public_key': 'abcdef0123456', | |||||
| }, | |||||
| { | |||||
| 'flags': 0, | |||||
| 'protocol': 2, | |||||
| 'algorithm': 2, | |||||
| 'public_key': 'abcdef0123456', | |||||
| }, | |||||
| ), | |||||
| # diff algorithm | |||||
| ( | |||||
| { | |||||
| 'flags': 0, | |||||
| 'protocol': 1, | |||||
| 'algorithm': 2, | |||||
| 'public_key': 'abcdef0123456', | |||||
| }, | |||||
| { | |||||
| 'flags': 0, | |||||
| 'protocol': 1, | |||||
| 'algorithm': 3, | |||||
| 'public_key': 'abcdef0123456', | |||||
| }, | |||||
| ), | |||||
| # diff public_key | |||||
| ( | |||||
| { | |||||
| 'flags': 0, | |||||
| 'protocol': 1, | |||||
| 'algorithm': 2, | |||||
| 'public_key': 'abcdef0123456', | |||||
| }, | |||||
| { | |||||
| 'flags': 0, | |||||
| 'protocol': 1, | |||||
| 'algorithm': 2, | |||||
| 'public_key': 'bcdef0123456a', | |||||
| }, | |||||
| ), | |||||
| ): | |||||
| a = DsValue(a) | |||||
| self.assertEqual(a, a) | |||||
| b = DsValue(b) | |||||
| self.assertEqual(b, b) | |||||
| self.assertNotEqual(a, b) | |||||
| self.assertNotEqual(b, a) | |||||
| self.assertTrue(a < b) | |||||
| # empty string won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| DsValue.parse_rdata_text('') | |||||
| # single word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| DsValue.parse_rdata_text('nope') | |||||
| # 2nd word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| DsValue.parse_rdata_text('0 1') | |||||
| # 3rd word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| DsValue.parse_rdata_text('0 1 2') | |||||
| # 5th word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| DsValue.parse_rdata_text('0 1 2 key blah') | |||||
| # things ints, will parse | |||||
| self.assertEqual( | |||||
| { | |||||
| 'flags': 'one', | |||||
| 'protocol': 'two', | |||||
| 'algorithm': 'three', | |||||
| 'public_key': 'key', | |||||
| }, | |||||
| DsValue.parse_rdata_text('one two three key'), | |||||
| ) | |||||
| # valid | |||||
| data = { | |||||
| 'flags': 0, | |||||
| 'protocol': 1, | |||||
| 'algorithm': 2, | |||||
| 'public_key': '99148c81', | |||||
| } | |||||
| self.assertEqual(data, DsValue.parse_rdata_text('0 1 2 99148c81')) | |||||
| self.assertEqual([], DsValue.validate(data, 'DS')) | |||||
| # missing flags | |||||
| data = {'protocol': 1, 'algorithm': 2, 'public_key': '99148c81'} | |||||
| self.assertEqual(['missing flags'], DsValue.validate(data, 'DS')) | |||||
| # invalid flags | |||||
| data = { | |||||
| 'flags': 'a', | |||||
| 'protocol': 1, | |||||
| 'algorithm': 2, | |||||
| 'public_key': '99148c81', | |||||
| } | |||||
| self.assertEqual(['invalid flags "a"'], DsValue.validate(data, 'DS')) | |||||
| # missing protocol | |||||
| data = {'flags': 1, 'algorithm': 2, 'public_key': '99148c81'} | |||||
| self.assertEqual(['missing protocol'], DsValue.validate(data, 'DS')) | |||||
| # invalid protocol | |||||
| data = { | |||||
| 'flags': 1, | |||||
| 'protocol': 'a', | |||||
| 'algorithm': 2, | |||||
| 'public_key': '99148c81', | |||||
| } | |||||
| self.assertEqual(['invalid protocol "a"'], DsValue.validate(data, 'DS')) | |||||
| # missing algorithm | |||||
| data = {'flags': 1, 'protocol': 2, 'public_key': '99148c81'} | |||||
| self.assertEqual(['missing algorithm'], DsValue.validate(data, 'DS')) | |||||
| # invalid algorithm | |||||
| data = { | |||||
| 'flags': 1, | |||||
| 'protocol': 2, | |||||
| 'algorithm': 'a', | |||||
| 'public_key': '99148c81', | |||||
| } | |||||
| self.assertEqual( | |||||
| ['invalid algorithm "a"'], DsValue.validate(data, 'DS') | |||||
| ) | |||||
| # missing algorithm (list) | |||||
| data = {'flags': 1, 'protocol': 2, 'algorithm': 3} | |||||
| self.assertEqual(['missing public_key'], DsValue.validate([data], 'DS')) | |||||
| zone = Zone('unit.tests.', []) | |||||
| values = [ | |||||
| { | |||||
| 'flags': 0, | |||||
| 'protocol': 1, | |||||
| 'algorithm': 2, | |||||
| 'public_key': '99148c81', | |||||
| }, | |||||
| { | |||||
| 'flags': 1, | |||||
| 'protocol': 2, | |||||
| 'algorithm': 3, | |||||
| 'public_key': '99148c44', | |||||
| }, | |||||
| ] | |||||
| a = DsRecord(zone, 'ds', {'ttl': 32, 'values': values}) | |||||
| self.assertEqual(0, a.values[0].flags) | |||||
| a.values[0].flags += 1 | |||||
| self.assertEqual(1, a.values[0].flags) | |||||
| self.assertEqual(1, a.values[0].protocol) | |||||
| a.values[0].protocol += 1 | |||||
| self.assertEqual(2, a.values[0].protocol) | |||||
| self.assertEqual(2, a.values[0].algorithm) | |||||
| a.values[0].algorithm += 1 | |||||
| self.assertEqual(3, a.values[0].algorithm) | |||||
| self.assertEqual('99148c81', a.values[0].public_key) | |||||
| a.values[0].public_key = '99148c42' | |||||
| self.assertEqual('99148c42', a.values[0].public_key) | |||||
| self.assertEqual(1, a.values[1].flags) | |||||
| self.assertEqual(2, a.values[1].protocol) | |||||
| self.assertEqual(3, a.values[1].algorithm) | |||||
| self.assertEqual('99148c44', a.values[1].public_key) | |||||
| self.assertEqual(DsValue(values[1]), a.values[1].data) | |||||
| self.assertEqual('1 2 3 99148c44', a.values[1].rdata_text) | |||||
| self.assertEqual('1 2 3 99148c44', a.values[1].__repr__()) | |||||
| @ -0,0 +1,30 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record.a import ARecord, Ipv4Value | |||||
| from octodns.zone import Zone | |||||
| class TestRecordIp(TestCase): | |||||
| def test_ipv4_value_rdata_text(self): | |||||
| # anything goes, we're a noop | |||||
| for s in ( | |||||
| None, | |||||
| '', | |||||
| 'word', | |||||
| 42, | |||||
| 42.43, | |||||
| '1.2.3', | |||||
| 'some.words.that.here', | |||||
| '1.2.word.4', | |||||
| '1.2.3.4', | |||||
| ): | |||||
| self.assertEqual(s, Ipv4Value.parse_rdata_text(s)) | |||||
| zone = Zone('unit.tests.', []) | |||||
| a = ARecord(zone, 'a', {'ttl': 42, 'value': '1.2.3.4'}) | |||||
| self.assertEqual('1.2.3.4', a.values[0].rdata_text) | |||||
| @ -0,0 +1,697 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.loc import LocRecord, LocValue | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.record.rr import RrParseError | |||||
| from octodns.zone import Zone | |||||
| from helpers import SimpleProvider | |||||
| class TestRecordLoc(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def test_loc(self): | |||||
| a_values = [ | |||||
| LocValue( | |||||
| { | |||||
| 'lat_degrees': 31, | |||||
| 'lat_minutes': 58, | |||||
| 'lat_seconds': 52.1, | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 20, | |||||
| 'size': 10, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| } | |||||
| ) | |||||
| ] | |||||
| a_data = {'ttl': 30, 'values': a_values} | |||||
| a = LocRecord(self.zone, 'a', a_data) | |||||
| self.assertEqual('a', a.name) | |||||
| self.assertEqual('a.unit.tests.', a.fqdn) | |||||
| self.assertEqual(30, a.ttl) | |||||
| self.assertEqual(a_values[0]['lat_degrees'], a.values[0].lat_degrees) | |||||
| self.assertEqual(a_values[0]['lat_minutes'], a.values[0].lat_minutes) | |||||
| self.assertEqual(a_values[0]['lat_seconds'], a.values[0].lat_seconds) | |||||
| self.assertEqual( | |||||
| a_values[0]['lat_direction'], a.values[0].lat_direction | |||||
| ) | |||||
| self.assertEqual(a_values[0]['long_degrees'], a.values[0].long_degrees) | |||||
| self.assertEqual(a_values[0]['long_minutes'], a.values[0].long_minutes) | |||||
| self.assertEqual(a_values[0]['long_seconds'], a.values[0].long_seconds) | |||||
| self.assertEqual( | |||||
| a_values[0]['long_direction'], a.values[0].long_direction | |||||
| ) | |||||
| self.assertEqual(a_values[0]['altitude'], a.values[0].altitude) | |||||
| self.assertEqual(a_values[0]['size'], a.values[0].size) | |||||
| self.assertEqual( | |||||
| a_values[0]['precision_horz'], a.values[0].precision_horz | |||||
| ) | |||||
| self.assertEqual( | |||||
| a_values[0]['precision_vert'], a.values[0].precision_vert | |||||
| ) | |||||
| b_value = LocValue( | |||||
| { | |||||
| 'lat_degrees': 32, | |||||
| 'lat_minutes': 7, | |||||
| 'lat_seconds': 19, | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 116, | |||||
| 'long_minutes': 2, | |||||
| 'long_seconds': 25, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 10, | |||||
| 'size': 1, | |||||
| 'precision_horz': 10000, | |||||
| 'precision_vert': 10, | |||||
| } | |||||
| ) | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = LocRecord(self.zone, 'b', b_data) | |||||
| self.assertEqual(b_value['lat_degrees'], b.values[0].lat_degrees) | |||||
| self.assertEqual(b_value['lat_minutes'], b.values[0].lat_minutes) | |||||
| self.assertEqual(b_value['lat_seconds'], b.values[0].lat_seconds) | |||||
| self.assertEqual(b_value['lat_direction'], b.values[0].lat_direction) | |||||
| self.assertEqual(b_value['long_degrees'], b.values[0].long_degrees) | |||||
| self.assertEqual(b_value['long_minutes'], b.values[0].long_minutes) | |||||
| self.assertEqual(b_value['long_seconds'], b.values[0].long_seconds) | |||||
| self.assertEqual(b_value['long_direction'], b.values[0].long_direction) | |||||
| self.assertEqual(b_value['altitude'], b.values[0].altitude) | |||||
| self.assertEqual(b_value['size'], b.values[0].size) | |||||
| self.assertEqual(b_value['precision_horz'], b.values[0].precision_horz) | |||||
| self.assertEqual(b_value['precision_vert'], b.values[0].precision_vert) | |||||
| self.assertEqual(b_data, b.data) | |||||
| target = SimpleProvider() | |||||
| # No changes with self | |||||
| self.assertFalse(a.changes(a, target)) | |||||
| # Diff in lat_direction causes change | |||||
| other = LocRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].lat_direction = 'N' | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in altitude causes change | |||||
| other.values[0].altitude = a.values[0].altitude | |||||
| other.values[0].altitude = -10 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # __repr__ doesn't blow up | |||||
| a.__repr__() | |||||
| def test_loc_value_rdata_text(self): | |||||
| # only the exact correct number of words is allowed | |||||
| for i in tuple(range(0, 12)) + (13,): | |||||
| s = ''.join(['word'] * i) | |||||
| with self.assertRaises(RrParseError): | |||||
| LocValue.parse_rdata_text(s) | |||||
| # type conversions are best effort | |||||
| self.assertEqual( | |||||
| { | |||||
| 'altitude': 'six', | |||||
| 'lat_degrees': 'zero', | |||||
| 'lat_direction': 'S', | |||||
| 'lat_minutes': 'one', | |||||
| 'lat_seconds': 'two', | |||||
| 'long_degrees': 'three', | |||||
| 'long_direction': 'W', | |||||
| 'long_minutes': 'four', | |||||
| 'long_seconds': 'five', | |||||
| 'precision_horz': 'eight', | |||||
| 'precision_vert': 'nine', | |||||
| 'size': 'seven', | |||||
| }, | |||||
| LocValue.parse_rdata_text( | |||||
| 'zero one two S three four five W six seven eight nine' | |||||
| ), | |||||
| ) | |||||
| # valid | |||||
| s = '0 1 2.2 N 3 4 5.5 E 6.6m 7.7m 8.8m 9.9m' | |||||
| self.assertEqual( | |||||
| { | |||||
| 'altitude': 6.6, | |||||
| 'lat_degrees': 0, | |||||
| 'lat_direction': 'N', | |||||
| 'lat_minutes': 1, | |||||
| 'lat_seconds': 2.2, | |||||
| 'long_degrees': 3, | |||||
| 'long_direction': 'E', | |||||
| 'long_minutes': 4, | |||||
| 'long_seconds': 5.5, | |||||
| 'precision_horz': 8.8, | |||||
| 'precision_vert': 9.9, | |||||
| 'size': 7.7, | |||||
| }, | |||||
| LocValue.parse_rdata_text(s), | |||||
| ) | |||||
| # make sure that the cstor is using parse_rdata_text | |||||
| zone = Zone('unit.tests.', []) | |||||
| a = LocRecord( | |||||
| zone, | |||||
| 'mx', | |||||
| { | |||||
| 'type': 'LOC', | |||||
| 'ttl': 42, | |||||
| 'value': { | |||||
| 'altitude': 6.6, | |||||
| 'lat_degrees': 0, | |||||
| 'lat_direction': 'N', | |||||
| 'lat_minutes': 1, | |||||
| 'lat_seconds': 2.2, | |||||
| 'long_degrees': 3, | |||||
| 'long_direction': 'E', | |||||
| 'long_minutes': 4, | |||||
| 'long_seconds': 5.5, | |||||
| 'precision_horz': 8.8, | |||||
| 'precision_vert': 9.9, | |||||
| 'size': 7.7, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(0, a.values[0].lat_degrees) | |||||
| self.assertEqual(1, a.values[0].lat_minutes) | |||||
| self.assertEqual(2.2, a.values[0].lat_seconds) | |||||
| self.assertEqual('N', a.values[0].lat_direction) | |||||
| self.assertEqual(3, a.values[0].long_degrees) | |||||
| self.assertEqual(4, a.values[0].long_minutes) | |||||
| self.assertEqual(5.5, a.values[0].long_seconds) | |||||
| self.assertEqual('E', a.values[0].long_direction) | |||||
| self.assertEqual(6.6, a.values[0].altitude) | |||||
| self.assertEqual(7.7, a.values[0].size) | |||||
| self.assertEqual(8.8, a.values[0].precision_horz) | |||||
| self.assertEqual(9.9, a.values[0].precision_vert) | |||||
| self.assertEqual(s, a.values[0].rdata_text) | |||||
| def test_loc_value(self): | |||||
| a = LocValue( | |||||
| { | |||||
| 'lat_degrees': 31, | |||||
| 'lat_minutes': 58, | |||||
| 'lat_seconds': 52.1, | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 20, | |||||
| 'size': 10, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| } | |||||
| ) | |||||
| b = LocValue( | |||||
| { | |||||
| 'lat_degrees': 32, | |||||
| 'lat_minutes': 7, | |||||
| 'lat_seconds': 19, | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 116, | |||||
| 'long_minutes': 2, | |||||
| 'long_seconds': 25, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 10, | |||||
| 'size': 1, | |||||
| 'precision_horz': 10000, | |||||
| 'precision_vert': 10, | |||||
| } | |||||
| ) | |||||
| c = LocValue( | |||||
| { | |||||
| 'lat_degrees': 53, | |||||
| 'lat_minutes': 14, | |||||
| 'lat_seconds': 10, | |||||
| 'lat_direction': 'N', | |||||
| 'long_degrees': 2, | |||||
| 'long_minutes': 18, | |||||
| 'long_seconds': 26, | |||||
| 'long_direction': 'W', | |||||
| 'altitude': 10, | |||||
| 'size': 1, | |||||
| 'precision_horz': 1000, | |||||
| 'precision_vert': 10, | |||||
| } | |||||
| ) | |||||
| self.assertEqual(a, a) | |||||
| self.assertEqual(b, b) | |||||
| self.assertEqual(c, c) | |||||
| self.assertNotEqual(a, b) | |||||
| self.assertNotEqual(a, c) | |||||
| self.assertNotEqual(b, a) | |||||
| self.assertNotEqual(b, c) | |||||
| self.assertNotEqual(c, a) | |||||
| self.assertNotEqual(c, b) | |||||
| self.assertTrue(a < b) | |||||
| self.assertTrue(a < c) | |||||
| self.assertTrue(b > a) | |||||
| self.assertTrue(b < c) | |||||
| self.assertTrue(c > a) | |||||
| self.assertTrue(c > b) | |||||
| self.assertTrue(a <= b) | |||||
| self.assertTrue(a <= c) | |||||
| self.assertTrue(a <= a) | |||||
| self.assertTrue(a >= a) | |||||
| self.assertTrue(b >= a) | |||||
| self.assertTrue(b <= c) | |||||
| self.assertTrue(b >= b) | |||||
| self.assertTrue(b <= b) | |||||
| self.assertTrue(c >= a) | |||||
| self.assertTrue(c >= b) | |||||
| self.assertTrue(c >= c) | |||||
| self.assertTrue(c <= c) | |||||
| self.assertEqual(31, a.lat_degrees) | |||||
| a.lat_degrees = a.lat_degrees + 1 | |||||
| self.assertEqual(32, a.lat_degrees) | |||||
| self.assertEqual(58, a.lat_minutes) | |||||
| a.lat_minutes = a.lat_minutes + 1 | |||||
| self.assertEqual(59, a.lat_minutes) | |||||
| self.assertEqual(52.1, a.lat_seconds) | |||||
| a.lat_seconds = a.lat_seconds + 1 | |||||
| self.assertEqual(53.1, a.lat_seconds) | |||||
| self.assertEqual('S', a.lat_direction) | |||||
| a.lat_direction = 'N' | |||||
| self.assertEqual('N', a.lat_direction) | |||||
| self.assertEqual(115, a.long_degrees) | |||||
| a.long_degrees = a.long_degrees + 1 | |||||
| self.assertEqual(116, a.long_degrees) | |||||
| self.assertEqual(49, a.long_minutes) | |||||
| a.long_minutes = a.long_minutes + 1 | |||||
| self.assertEqual(50, a.long_minutes) | |||||
| self.assertEqual(11.7, a.long_seconds) | |||||
| a.long_seconds = a.long_seconds + 1 | |||||
| self.assertEqual(12.7, a.long_seconds) | |||||
| self.assertEqual('E', a.long_direction) | |||||
| a.long_direction = 'W' | |||||
| self.assertEqual('W', a.long_direction) | |||||
| self.assertEqual(20, a.altitude) | |||||
| a.altitude = a.altitude + 1 | |||||
| self.assertEqual(21, a.altitude) | |||||
| self.assertEqual(10, a.size) | |||||
| a.size = a.size + 1 | |||||
| self.assertEqual(11, a.size) | |||||
| self.assertEqual(10, a.precision_horz) | |||||
| a.precision_horz = a.precision_horz + 1 | |||||
| self.assertEqual(11, a.precision_horz) | |||||
| self.assertEqual(2, a.precision_vert) | |||||
| a.precision_vert = a.precision_vert + 1 | |||||
| self.assertEqual(3, a.precision_vert) | |||||
| # Hash | |||||
| values = set() | |||||
| values.add(a) | |||||
| self.assertTrue(a in values) | |||||
| self.assertFalse(b in values) | |||||
| values.add(b) | |||||
| self.assertTrue(b in values) | |||||
| def test_validation(self): | |||||
| # doesn't blow up | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'LOC', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'lat_degrees': 31, | |||||
| 'lat_minutes': 58, | |||||
| 'lat_seconds': 52.1, | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 20, | |||||
| 'size': 10, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| # missing int key | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'LOC', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'lat_minutes': 58, | |||||
| 'lat_seconds': 52.1, | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 20, | |||||
| 'size': 10, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['missing lat_degrees'], ctx.exception.reasons) | |||||
| # missing float key | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'LOC', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'lat_degrees': 31, | |||||
| 'lat_minutes': 58, | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 20, | |||||
| 'size': 10, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['missing lat_seconds'], ctx.exception.reasons) | |||||
| # missing text key | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'LOC', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'lat_degrees': 31, | |||||
| 'lat_minutes': 58, | |||||
| 'lat_seconds': 52.1, | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 20, | |||||
| 'size': 10, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['missing lat_direction'], ctx.exception.reasons) | |||||
| # invalid direction | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'LOC', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'lat_degrees': 31, | |||||
| 'lat_minutes': 58, | |||||
| 'lat_seconds': 52.1, | |||||
| 'lat_direction': 'U', | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 20, | |||||
| 'size': 10, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid direction for lat_direction "U"'], ctx.exception.reasons | |||||
| ) | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'LOC', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'lat_degrees': 31, | |||||
| 'lat_minutes': 58, | |||||
| 'lat_seconds': 52.1, | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'N', | |||||
| 'altitude': 20, | |||||
| 'size': 10, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid direction for long_direction "N"'], ctx.exception.reasons | |||||
| ) | |||||
| # invalid degrees | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'LOC', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'lat_degrees': 360, | |||||
| 'lat_minutes': 58, | |||||
| 'lat_seconds': 52.1, | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 20, | |||||
| 'size': 10, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid value for lat_degrees "360"'], ctx.exception.reasons | |||||
| ) | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'LOC', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'lat_degrees': 'nope', | |||||
| 'lat_minutes': 58, | |||||
| 'lat_seconds': 52.1, | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 20, | |||||
| 'size': 10, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['invalid lat_degrees "nope"'], ctx.exception.reasons) | |||||
| # invalid minutes | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'LOC', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'lat_degrees': 31, | |||||
| 'lat_minutes': 60, | |||||
| 'lat_seconds': 52.1, | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 20, | |||||
| 'size': 10, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid value for lat_minutes "60"'], ctx.exception.reasons | |||||
| ) | |||||
| # invalid seconds | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'LOC', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'lat_degrees': 31, | |||||
| 'lat_minutes': 58, | |||||
| 'lat_seconds': 60, | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 20, | |||||
| 'size': 10, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid value for lat_seconds "60"'], ctx.exception.reasons | |||||
| ) | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'LOC', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'lat_degrees': 31, | |||||
| 'lat_minutes': 58, | |||||
| 'lat_seconds': 'nope', | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 20, | |||||
| 'size': 10, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['invalid lat_seconds "nope"'], ctx.exception.reasons) | |||||
| # invalid altitude | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'LOC', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'lat_degrees': 31, | |||||
| 'lat_minutes': 58, | |||||
| 'lat_seconds': 52.1, | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': -666666, | |||||
| 'size': 10, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid value for altitude "-666666"'], ctx.exception.reasons | |||||
| ) | |||||
| # invalid size | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'LOC', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'lat_degrees': 31, | |||||
| 'lat_minutes': 58, | |||||
| 'lat_seconds': 52.1, | |||||
| 'lat_direction': 'S', | |||||
| 'long_degrees': 115, | |||||
| 'long_minutes': 49, | |||||
| 'long_seconds': 11.7, | |||||
| 'long_direction': 'E', | |||||
| 'altitude': 20, | |||||
| 'size': 99999999.99, | |||||
| 'precision_horz': 10, | |||||
| 'precision_vert': 2, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid value for size "99999999.99"'], ctx.exception.reasons | |||||
| ) | |||||
| @ -0,0 +1,265 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.mx import MxRecord, MxValue | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.record.rr import RrParseError | |||||
| from octodns.zone import Zone | |||||
| from helpers import SimpleProvider | |||||
| class TestRecordMx(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def test_mx(self): | |||||
| a_values = [ | |||||
| MxValue({'preference': 10, 'exchange': 'smtp1.'}), | |||||
| MxValue({'priority': 20, 'value': 'smtp2.'}), | |||||
| ] | |||||
| a_data = {'ttl': 30, 'values': a_values} | |||||
| a = MxRecord(self.zone, 'a', a_data) | |||||
| self.assertEqual('a', a.name) | |||||
| self.assertEqual('a.unit.tests.', a.fqdn) | |||||
| self.assertEqual(30, a.ttl) | |||||
| self.assertEqual(a_values[0]['preference'], a.values[0].preference) | |||||
| self.assertEqual(a_values[0]['exchange'], a.values[0].exchange) | |||||
| self.assertEqual(a_values[1]['preference'], a.values[1].preference) | |||||
| self.assertEqual(a_values[1]['exchange'], a.values[1].exchange) | |||||
| a_data['values'][1] = MxValue({'preference': 20, 'exchange': 'smtp2.'}) | |||||
| self.assertEqual(a_data, a.data) | |||||
| b_value = MxValue({'preference': 0, 'exchange': 'smtp3.'}) | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = MxRecord(self.zone, 'b', b_data) | |||||
| self.assertEqual(b_value['preference'], b.values[0].preference) | |||||
| self.assertEqual(b_value['exchange'], b.values[0].exchange) | |||||
| self.assertEqual(b_data, b.data) | |||||
| a_upper_values = [ | |||||
| {'preference': 10, 'exchange': 'SMTP1.'}, | |||||
| {'priority': 20, 'value': 'SMTP2.'}, | |||||
| ] | |||||
| a_upper_data = {'ttl': 30, 'values': a_upper_values} | |||||
| a_upper = MxRecord(self.zone, 'a', a_upper_data) | |||||
| self.assertEqual(a_upper.data, a.data) | |||||
| target = SimpleProvider() | |||||
| # No changes with self | |||||
| self.assertFalse(a.changes(a, target)) | |||||
| # Diff in preference causes change | |||||
| other = MxRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].preference = 22 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in value causes change | |||||
| other.values[0].preference = a.values[0].preference | |||||
| other.values[0].exchange = 'smtpX' | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # __repr__ doesn't blow up | |||||
| a.__repr__() | |||||
| def test_mx_value_rdata_text(self): | |||||
| # empty string won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| MxValue.parse_rdata_text('') | |||||
| # single word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| MxValue.parse_rdata_text('nope') | |||||
| # 3rd word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| MxValue.parse_rdata_text('10 mx.unit.tests. another') | |||||
| # preference not an int | |||||
| self.assertEqual( | |||||
| {'preference': 'abc', 'exchange': 'mx.unit.tests.'}, | |||||
| MxValue.parse_rdata_text('abc mx.unit.tests.'), | |||||
| ) | |||||
| # valid | |||||
| self.assertEqual( | |||||
| {'preference': 10, 'exchange': 'mx.unit.tests.'}, | |||||
| MxValue.parse_rdata_text('10 mx.unit.tests.'), | |||||
| ) | |||||
| zone = Zone('unit.tests.', []) | |||||
| a = MxRecord( | |||||
| zone, | |||||
| 'mx', | |||||
| { | |||||
| 'ttl': 32, | |||||
| 'values': [ | |||||
| {'preference': 11, 'exchange': 'mail1.unit.tests.'}, | |||||
| {'preference': 12, 'exchange': 'mail2.unit.tests.'}, | |||||
| ], | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(11, a.values[0].preference) | |||||
| self.assertEqual('mail1.unit.tests.', a.values[0].exchange) | |||||
| self.assertEqual('11 mail1.unit.tests.', a.values[0].rdata_text) | |||||
| self.assertEqual(12, a.values[1].preference) | |||||
| self.assertEqual('mail2.unit.tests.', a.values[1].exchange) | |||||
| self.assertEqual('12 mail2.unit.tests.', a.values[1].rdata_text) | |||||
| def test_mx_value(self): | |||||
| a = MxValue( | |||||
| {'preference': 0, 'priority': 'a', 'exchange': 'v', 'value': '1'} | |||||
| ) | |||||
| b = MxValue( | |||||
| {'preference': 10, 'priority': 'a', 'exchange': 'v', 'value': '2'} | |||||
| ) | |||||
| c = MxValue( | |||||
| {'preference': 0, 'priority': 'b', 'exchange': 'z', 'value': '3'} | |||||
| ) | |||||
| self.assertEqual(a, a) | |||||
| self.assertEqual(b, b) | |||||
| self.assertEqual(c, c) | |||||
| self.assertNotEqual(a, b) | |||||
| self.assertNotEqual(a, c) | |||||
| self.assertNotEqual(b, a) | |||||
| self.assertNotEqual(b, c) | |||||
| self.assertNotEqual(c, a) | |||||
| self.assertNotEqual(c, b) | |||||
| self.assertTrue(a < b) | |||||
| self.assertTrue(a < c) | |||||
| self.assertTrue(b > a) | |||||
| self.assertTrue(b > c) | |||||
| self.assertTrue(c > a) | |||||
| self.assertTrue(c < b) | |||||
| self.assertTrue(a <= b) | |||||
| self.assertTrue(a <= c) | |||||
| self.assertTrue(a <= a) | |||||
| self.assertTrue(a >= a) | |||||
| self.assertTrue(b >= a) | |||||
| self.assertTrue(b >= c) | |||||
| self.assertTrue(b >= b) | |||||
| self.assertTrue(b <= b) | |||||
| self.assertTrue(c >= a) | |||||
| self.assertTrue(c <= b) | |||||
| self.assertTrue(c >= c) | |||||
| self.assertTrue(c <= c) | |||||
| self.assertEqual(a.__hash__(), a.__hash__()) | |||||
| self.assertNotEqual(a.__hash__(), b.__hash__()) | |||||
| def test_validation(self): | |||||
| # doesn't blow up | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'MX', | |||||
| 'ttl': 600, | |||||
| 'value': {'preference': 10, 'exchange': 'foo.bar.com.'}, | |||||
| }, | |||||
| ) | |||||
| # missing preference | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'MX', | |||||
| 'ttl': 600, | |||||
| 'value': {'exchange': 'foo.bar.com.'}, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['missing preference'], ctx.exception.reasons) | |||||
| # invalid preference | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'MX', | |||||
| 'ttl': 600, | |||||
| 'value': {'preference': 'nope', 'exchange': 'foo.bar.com.'}, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['invalid preference "nope"'], ctx.exception.reasons) | |||||
| # missing exchange | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| {'type': 'MX', 'ttl': 600, 'value': {'preference': 10}}, | |||||
| ) | |||||
| self.assertEqual(['missing exchange'], ctx.exception.reasons) | |||||
| # missing trailing . | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'MX', | |||||
| 'ttl': 600, | |||||
| 'value': {'preference': 10, 'exchange': 'foo.bar.com'}, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['MX value "foo.bar.com" missing trailing .'], ctx.exception.reasons | |||||
| ) | |||||
| # exchange must be a valid FQDN | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'MX', | |||||
| 'ttl': 600, | |||||
| 'value': {'preference': 10, 'exchange': '100 foo.bar.com.'}, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['Invalid MX exchange "100 foo.bar.com." is not a valid FQDN.'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| # if exchange doesn't exist value can not be None/falsey | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'MX', | |||||
| 'ttl': 600, | |||||
| 'value': {'preference': 10, 'value': ''}, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['missing exchange'], ctx.exception.reasons) | |||||
| # exchange can be a single `.` | |||||
| record = Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'MX', | |||||
| 'ttl': 600, | |||||
| 'value': {'preference': 0, 'exchange': '.'}, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual('.', record.values[0].exchange) | |||||
| @ -0,0 +1,438 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.naptr import NaptrRecord, NaptrValue | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.record.rr import RrParseError | |||||
| from octodns.zone import Zone | |||||
| from helpers import SimpleProvider | |||||
| class TestRecordNaptr(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def test_naptr(self): | |||||
| a_values = [ | |||||
| NaptrValue( | |||||
| { | |||||
| 'order': 10, | |||||
| 'preference': 11, | |||||
| 'flags': 'X', | |||||
| 'service': 'Y', | |||||
| 'regexp': 'Z', | |||||
| 'replacement': '.', | |||||
| } | |||||
| ), | |||||
| NaptrValue( | |||||
| { | |||||
| 'order': 20, | |||||
| 'preference': 21, | |||||
| 'flags': 'A', | |||||
| 'service': 'B', | |||||
| 'regexp': 'C', | |||||
| 'replacement': 'foo.com', | |||||
| } | |||||
| ), | |||||
| ] | |||||
| a_data = {'ttl': 30, 'values': a_values} | |||||
| a = NaptrRecord(self.zone, 'a', a_data) | |||||
| self.assertEqual('a', a.name) | |||||
| self.assertEqual('a.unit.tests.', a.fqdn) | |||||
| self.assertEqual(30, a.ttl) | |||||
| for i in (0, 1): | |||||
| for k in a_values[0].keys(): | |||||
| self.assertEqual(a_values[i][k], getattr(a.values[i], k)) | |||||
| self.assertEqual(a_data, a.data) | |||||
| b_value = NaptrValue( | |||||
| { | |||||
| 'order': 30, | |||||
| 'preference': 31, | |||||
| 'flags': 'M', | |||||
| 'service': 'N', | |||||
| 'regexp': 'O', | |||||
| 'replacement': 'x', | |||||
| } | |||||
| ) | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = NaptrRecord(self.zone, 'b', b_data) | |||||
| for k in a_values[0].keys(): | |||||
| self.assertEqual(b_value[k], getattr(b.values[0], k)) | |||||
| self.assertEqual(b_data, b.data) | |||||
| target = SimpleProvider() | |||||
| # No changes with self | |||||
| self.assertFalse(a.changes(a, target)) | |||||
| # Diff in priority causes change | |||||
| other = NaptrRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].order = 22 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in replacement causes change | |||||
| other.values[0].order = a.values[0].order | |||||
| other.values[0].replacement = 'smtpX' | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # full sorting | |||||
| # equivalent | |||||
| b_naptr_value = b.values[0] | |||||
| self.assertTrue(b_naptr_value == b_naptr_value) | |||||
| self.assertFalse(b_naptr_value != b_naptr_value) | |||||
| self.assertTrue(b_naptr_value <= b_naptr_value) | |||||
| self.assertTrue(b_naptr_value >= b_naptr_value) | |||||
| # by order | |||||
| self.assertTrue( | |||||
| b_naptr_value | |||||
| > NaptrValue( | |||||
| { | |||||
| 'order': 10, | |||||
| 'preference': 31, | |||||
| 'flags': 'M', | |||||
| 'service': 'N', | |||||
| 'regexp': 'O', | |||||
| 'replacement': 'x', | |||||
| } | |||||
| ) | |||||
| ) | |||||
| self.assertTrue( | |||||
| b_naptr_value | |||||
| < NaptrValue( | |||||
| { | |||||
| 'order': 40, | |||||
| 'preference': 31, | |||||
| 'flags': 'M', | |||||
| 'service': 'N', | |||||
| 'regexp': 'O', | |||||
| 'replacement': 'x', | |||||
| } | |||||
| ) | |||||
| ) | |||||
| # by preference | |||||
| self.assertTrue( | |||||
| b_naptr_value | |||||
| > NaptrValue( | |||||
| { | |||||
| 'order': 30, | |||||
| 'preference': 10, | |||||
| 'flags': 'M', | |||||
| 'service': 'N', | |||||
| 'regexp': 'O', | |||||
| 'replacement': 'x', | |||||
| } | |||||
| ) | |||||
| ) | |||||
| self.assertTrue( | |||||
| b_naptr_value | |||||
| < NaptrValue( | |||||
| { | |||||
| 'order': 30, | |||||
| 'preference': 40, | |||||
| 'flags': 'M', | |||||
| 'service': 'N', | |||||
| 'regexp': 'O', | |||||
| 'replacement': 'x', | |||||
| } | |||||
| ) | |||||
| ) | |||||
| # by flags | |||||
| self.assertTrue( | |||||
| b_naptr_value | |||||
| > NaptrValue( | |||||
| { | |||||
| 'order': 30, | |||||
| 'preference': 31, | |||||
| 'flags': 'A', | |||||
| 'service': 'N', | |||||
| 'regexp': 'O', | |||||
| 'replacement': 'x', | |||||
| } | |||||
| ) | |||||
| ) | |||||
| self.assertTrue( | |||||
| b_naptr_value | |||||
| < NaptrValue( | |||||
| { | |||||
| 'order': 30, | |||||
| 'preference': 31, | |||||
| 'flags': 'Z', | |||||
| 'service': 'N', | |||||
| 'regexp': 'O', | |||||
| 'replacement': 'x', | |||||
| } | |||||
| ) | |||||
| ) | |||||
| # by service | |||||
| self.assertTrue( | |||||
| b_naptr_value | |||||
| > NaptrValue( | |||||
| { | |||||
| 'order': 30, | |||||
| 'preference': 31, | |||||
| 'flags': 'M', | |||||
| 'service': 'A', | |||||
| 'regexp': 'O', | |||||
| 'replacement': 'x', | |||||
| } | |||||
| ) | |||||
| ) | |||||
| self.assertTrue( | |||||
| b_naptr_value | |||||
| < NaptrValue( | |||||
| { | |||||
| 'order': 30, | |||||
| 'preference': 31, | |||||
| 'flags': 'M', | |||||
| 'service': 'Z', | |||||
| 'regexp': 'O', | |||||
| 'replacement': 'x', | |||||
| } | |||||
| ) | |||||
| ) | |||||
| # by regexp | |||||
| self.assertTrue( | |||||
| b_naptr_value | |||||
| > NaptrValue( | |||||
| { | |||||
| 'order': 30, | |||||
| 'preference': 31, | |||||
| 'flags': 'M', | |||||
| 'service': 'N', | |||||
| 'regexp': 'A', | |||||
| 'replacement': 'x', | |||||
| } | |||||
| ) | |||||
| ) | |||||
| self.assertTrue( | |||||
| b_naptr_value | |||||
| < NaptrValue( | |||||
| { | |||||
| 'order': 30, | |||||
| 'preference': 31, | |||||
| 'flags': 'M', | |||||
| 'service': 'N', | |||||
| 'regexp': 'Z', | |||||
| 'replacement': 'x', | |||||
| } | |||||
| ) | |||||
| ) | |||||
| # by replacement | |||||
| self.assertTrue( | |||||
| b_naptr_value | |||||
| > NaptrValue( | |||||
| { | |||||
| 'order': 30, | |||||
| 'preference': 31, | |||||
| 'flags': 'M', | |||||
| 'service': 'N', | |||||
| 'regexp': 'O', | |||||
| 'replacement': 'a', | |||||
| } | |||||
| ) | |||||
| ) | |||||
| self.assertTrue( | |||||
| b_naptr_value | |||||
| < NaptrValue( | |||||
| { | |||||
| 'order': 30, | |||||
| 'preference': 31, | |||||
| 'flags': 'M', | |||||
| 'service': 'N', | |||||
| 'regexp': 'O', | |||||
| 'replacement': 'z', | |||||
| } | |||||
| ) | |||||
| ) | |||||
| # __repr__ doesn't blow up | |||||
| a.__repr__() | |||||
| # Hash | |||||
| v = NaptrValue( | |||||
| { | |||||
| 'order': 30, | |||||
| 'preference': 31, | |||||
| 'flags': 'M', | |||||
| 'service': 'N', | |||||
| 'regexp': 'O', | |||||
| 'replacement': 'z', | |||||
| } | |||||
| ) | |||||
| o = NaptrValue( | |||||
| { | |||||
| 'order': 30, | |||||
| 'preference': 32, | |||||
| 'flags': 'M', | |||||
| 'service': 'N', | |||||
| 'regexp': 'O', | |||||
| 'replacement': 'z', | |||||
| } | |||||
| ) | |||||
| values = set() | |||||
| values.add(v) | |||||
| self.assertTrue(v in values) | |||||
| self.assertFalse(o in values) | |||||
| values.add(o) | |||||
| self.assertTrue(o in values) | |||||
| self.assertEqual(30, o.order) | |||||
| o.order = o.order + 1 | |||||
| self.assertEqual(31, o.order) | |||||
| self.assertEqual(32, o.preference) | |||||
| o.preference = o.preference + 1 | |||||
| self.assertEqual(33, o.preference) | |||||
| self.assertEqual('M', o.flags) | |||||
| o.flags = 'P' | |||||
| self.assertEqual('P', o.flags) | |||||
| self.assertEqual('N', o.service) | |||||
| o.service = 'Q' | |||||
| self.assertEqual('Q', o.service) | |||||
| self.assertEqual('O', o.regexp) | |||||
| o.regexp = 'R' | |||||
| self.assertEqual('R', o.regexp) | |||||
| self.assertEqual('z', o.replacement) | |||||
| o.replacement = '1' | |||||
| self.assertEqual('1', o.replacement) | |||||
| def test_naptr_value_rdata_text(self): | |||||
| # things with the wrong number of words won't parse | |||||
| for v in ( | |||||
| '', | |||||
| 'one', | |||||
| 'one two', | |||||
| 'one two three', | |||||
| 'one two three four', | |||||
| 'one two three four five', | |||||
| 'one two three four five six seven', | |||||
| ): | |||||
| with self.assertRaises(RrParseError): | |||||
| NaptrValue.parse_rdata_text(v) | |||||
| # we don't care if the types of things are correct when parsing rr text | |||||
| self.assertEqual( | |||||
| { | |||||
| 'order': 'one', | |||||
| 'preference': 'two', | |||||
| 'flags': 'three', | |||||
| 'service': 'four', | |||||
| 'regexp': 'five', | |||||
| 'replacement': 'six', | |||||
| }, | |||||
| NaptrValue.parse_rdata_text('one two three four five six'), | |||||
| ) | |||||
| # order and preference will be converted to int's when possible | |||||
| self.assertEqual( | |||||
| { | |||||
| 'order': 1, | |||||
| 'preference': 2, | |||||
| 'flags': 'three', | |||||
| 'service': 'four', | |||||
| 'regexp': 'five', | |||||
| 'replacement': 'six', | |||||
| }, | |||||
| NaptrValue.parse_rdata_text('1 2 three four five six'), | |||||
| ) | |||||
| # make sure that the cstor is using parse_rdata_text | |||||
| zone = Zone('unit.tests.', []) | |||||
| a = NaptrRecord( | |||||
| zone, | |||||
| 'naptr', | |||||
| { | |||||
| 'ttl': 32, | |||||
| 'value': { | |||||
| 'order': 1, | |||||
| 'preference': 2, | |||||
| 'flags': 'S', | |||||
| 'service': 'service', | |||||
| 'regexp': 'regexp', | |||||
| 'replacement': 'replacement', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(1, a.values[0].order) | |||||
| self.assertEqual(2, a.values[0].preference) | |||||
| self.assertEqual('S', a.values[0].flags) | |||||
| self.assertEqual('service', a.values[0].service) | |||||
| self.assertEqual('regexp', a.values[0].regexp) | |||||
| self.assertEqual('replacement', a.values[0].replacement) | |||||
| s = '1 2 S service regexp replacement' | |||||
| self.assertEqual(s, a.values[0].rdata_text) | |||||
| def test_validation(self): | |||||
| # doesn't blow up | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'NAPTR', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'order': 10, | |||||
| 'preference': 20, | |||||
| 'flags': 'S', | |||||
| 'service': 'srv', | |||||
| 'regexp': '.*', | |||||
| 'replacement': '.', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| # missing X priority | |||||
| value = { | |||||
| 'order': 10, | |||||
| 'preference': 20, | |||||
| 'flags': 'S', | |||||
| '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.assertEqual([f'missing {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.assertEqual(['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.assertEqual(['invalid preference "who"'], ctx.exception.reasons) | |||||
| # unrecognized flags | |||||
| v = dict(value) | |||||
| v['flags'] = 'X' | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new(self.zone, '', {'type': 'NAPTR', 'ttl': 600, 'value': v}) | |||||
| self.assertEqual(['unrecognized flags "X"'], ctx.exception.reasons) | |||||
| @ -0,0 +1,83 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.ns import NsRecord, NsValue | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.zone import Zone | |||||
| class TestRecordNs(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def test_ns(self): | |||||
| a_values = ['5.6.7.8.', '6.7.8.9.', '7.8.9.0.'] | |||||
| a_data = {'ttl': 30, 'values': a_values} | |||||
| a = NsRecord(self.zone, 'a', a_data) | |||||
| self.assertEqual('a', a.name) | |||||
| self.assertEqual('a.unit.tests.', a.fqdn) | |||||
| self.assertEqual(30, a.ttl) | |||||
| self.assertEqual(a_values, a.values) | |||||
| self.assertEqual(a_data, a.data) | |||||
| b_value = '9.8.7.6.' | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = NsRecord(self.zone, 'b', b_data) | |||||
| self.assertEqual([b_value], b.values) | |||||
| self.assertEqual(b_data, b.data) | |||||
| def test_ns_value_rdata_text(self): | |||||
| # anything goes, we're a noop | |||||
| for s in ( | |||||
| None, | |||||
| '', | |||||
| 'word', | |||||
| 42, | |||||
| 42.43, | |||||
| '1.2.3', | |||||
| 'some.words.that.here', | |||||
| '1.2.word.4', | |||||
| '1.2.3.4', | |||||
| ): | |||||
| self.assertEqual(s, NsValue.parse_rdata_text(s)) | |||||
| zone = Zone('unit.tests.', []) | |||||
| a = NsRecord(zone, 'a', {'ttl': 42, 'value': 'some.target.'}) | |||||
| self.assertEqual('some.target.', a.values[0].rdata_text) | |||||
| def test_validation(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.assertEqual(['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.assertEqual( | |||||
| ['NS value "foo.bar" missing trailing .'], ctx.exception.reasons | |||||
| ) | |||||
| # exchange must be a valid FQDN | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| {'type': 'NS', 'ttl': 600, 'value': '100 foo.bar.com.'}, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['Invalid NS value "100 foo.bar.com." is not a valid FQDN.'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| @ -0,0 +1,89 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.ptr import PtrRecord, PtrValue | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.zone import Zone | |||||
| class TestRecordPtr(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def test_ptr_lowering_value(self): | |||||
| upper_record = PtrRecord( | |||||
| self.zone, | |||||
| 'PtrUppwerValue', | |||||
| {'ttl': 30, 'type': 'PTR', 'value': 'GITHUB.COM.'}, | |||||
| ) | |||||
| lower_record = PtrRecord( | |||||
| self.zone, | |||||
| 'PtrLowerValue', | |||||
| {'ttl': 30, 'type': 'PTR', 'value': 'github.com.'}, | |||||
| ) | |||||
| self.assertEqual(upper_record.value, lower_record.value) | |||||
| 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.assertEqual(['missing value(s)'], ctx.exception.reasons) | |||||
| # empty value | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new(self.zone, '', {'type': 'PTR', 'ttl': 600, 'value': ''}) | |||||
| self.assertEqual(['missing value(s)'], ctx.exception.reasons) | |||||
| # not a valid FQDN | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, '', {'type': 'PTR', 'ttl': 600, 'value': '_.'} | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['Invalid PTR value "_." is not a valid FQDN.'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| # no trailing . | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, '', {'type': 'PTR', 'ttl': 600, 'value': 'foo.bar'} | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['PTR value "foo.bar" missing trailing .'], ctx.exception.reasons | |||||
| ) | |||||
| def test_ptr_rdata_text(self): | |||||
| # anything goes, we're a noop | |||||
| for s in ( | |||||
| None, | |||||
| '', | |||||
| 'word', | |||||
| 42, | |||||
| 42.43, | |||||
| '1.2.3', | |||||
| 'some.words.that.here', | |||||
| '1.2.word.4', | |||||
| '1.2.3.4', | |||||
| ): | |||||
| self.assertEqual(s, PtrValue.parse_rdata_text(s)) | |||||
| zone = Zone('unit.tests.', []) | |||||
| a = PtrRecord(zone, 'a', {'ttl': 42, 'value': 'some.target.'}) | |||||
| self.assertEqual('some.target.', a.values[0].rdata_text) | |||||
| a = PtrRecord( | |||||
| zone, 'a', {'ttl': 42, 'values': ['some.target.', 'second.target.']} | |||||
| ) | |||||
| self.assertEqual('second.target.', a.values[0].rdata_text) | |||||
| self.assertEqual('some.target.', a.values[1].rdata_text) | |||||
| @ -0,0 +1,71 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.spf import SpfRecord | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.zone import Zone | |||||
| class TestRecordSpf(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def assertMultipleValues(self, _type, a_values, b_value): | |||||
| a_data = {'ttl': 30, 'values': a_values} | |||||
| a = _type(self.zone, 'a', a_data) | |||||
| self.assertEqual('a', a.name) | |||||
| self.assertEqual('a.unit.tests.', a.fqdn) | |||||
| self.assertEqual(30, a.ttl) | |||||
| self.assertEqual(a_values, a.values) | |||||
| self.assertEqual(a_data, a.data) | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = _type(self.zone, 'b', b_data) | |||||
| self.assertEqual([b_value], b.values) | |||||
| self.assertEqual(b_data, b.data) | |||||
| def test_spf(self): | |||||
| a_values = ['spf1 -all', 'spf1 -hrm'] | |||||
| b_value = 'spf1 -other' | |||||
| self.assertMultipleValues(SpfRecord, a_values, b_value) | |||||
| def test_validation(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.assertEqual(['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.assertEqual( | |||||
| ['unescaped ; in "this has some; semi-colons\\; in it"'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| @ -0,0 +1,432 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.srv import SrvRecord, SrvValue | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.record.rr import RrParseError | |||||
| from octodns.zone import Zone | |||||
| from helpers import SimpleProvider | |||||
| class TestRecordSrv(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def test_srv(self): | |||||
| a_values = [ | |||||
| SrvValue( | |||||
| {'priority': 10, 'weight': 11, 'port': 12, 'target': 'server1'} | |||||
| ), | |||||
| SrvValue( | |||||
| {'priority': 20, 'weight': 21, 'port': 22, 'target': 'server2'} | |||||
| ), | |||||
| ] | |||||
| a_data = {'ttl': 30, 'values': a_values} | |||||
| a = SrvRecord(self.zone, '_a._tcp', a_data) | |||||
| self.assertEqual('_a._tcp', a.name) | |||||
| self.assertEqual('_a._tcp.unit.tests.', a.fqdn) | |||||
| self.assertEqual(30, a.ttl) | |||||
| self.assertEqual(a_values[0]['priority'], a.values[0].priority) | |||||
| self.assertEqual(a_values[0]['weight'], a.values[0].weight) | |||||
| self.assertEqual(a_values[0]['port'], a.values[0].port) | |||||
| self.assertEqual(a_values[0]['target'], a.values[0].target) | |||||
| self.assertEqual(a_data, a.data) | |||||
| b_value = SrvValue( | |||||
| {'priority': 30, 'weight': 31, 'port': 32, 'target': 'server3'} | |||||
| ) | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = SrvRecord(self.zone, '_b._tcp', b_data) | |||||
| self.assertEqual(b_value['priority'], b.values[0].priority) | |||||
| self.assertEqual(b_value['weight'], b.values[0].weight) | |||||
| self.assertEqual(b_value['port'], b.values[0].port) | |||||
| self.assertEqual(b_value['target'], b.values[0].target) | |||||
| self.assertEqual(b_data, b.data) | |||||
| target = SimpleProvider() | |||||
| # No changes with self | |||||
| self.assertFalse(a.changes(a, target)) | |||||
| # Diff in priority causes change | |||||
| other = SrvRecord( | |||||
| self.zone, '_a._icmp', {'ttl': 30, 'values': a_values} | |||||
| ) | |||||
| other.values[0].priority = 22 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in weight causes change | |||||
| other.values[0].priority = a.values[0].priority | |||||
| other.values[0].weight = 33 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in port causes change | |||||
| other.values[0].weight = a.values[0].weight | |||||
| other.values[0].port = 44 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in target causes change | |||||
| other.values[0].port = a.values[0].port | |||||
| other.values[0].target = 'serverX' | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # __repr__ doesn't blow up | |||||
| a.__repr__() | |||||
| def test_srv_value_rdata_text(self): | |||||
| # empty string won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| SrvValue.parse_rdata_text('') | |||||
| # single word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| SrvValue.parse_rdata_text('nope') | |||||
| # 2nd word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| SrvValue.parse_rdata_text('1 2') | |||||
| # 3rd word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| SrvValue.parse_rdata_text('1 2 3') | |||||
| # 5th word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| SrvValue.parse_rdata_text('1 2 3 4 5') | |||||
| # priority weight and port not ints | |||||
| self.assertEqual( | |||||
| { | |||||
| 'priority': 'one', | |||||
| 'weight': 'two', | |||||
| 'port': 'three', | |||||
| 'target': 'srv.unit.tests.', | |||||
| }, | |||||
| SrvValue.parse_rdata_text('one two three srv.unit.tests.'), | |||||
| ) | |||||
| # valid | |||||
| self.assertEqual( | |||||
| { | |||||
| 'priority': 1, | |||||
| 'weight': 2, | |||||
| 'port': 3, | |||||
| 'target': 'srv.unit.tests.', | |||||
| }, | |||||
| SrvValue.parse_rdata_text('1 2 3 srv.unit.tests.'), | |||||
| ) | |||||
| zone = Zone('unit.tests.', []) | |||||
| a = SrvRecord( | |||||
| zone, | |||||
| '_srv._tcp', | |||||
| { | |||||
| 'ttl': 32, | |||||
| 'value': { | |||||
| 'priority': 1, | |||||
| 'weight': 2, | |||||
| 'port': 3, | |||||
| 'target': 'srv.unit.tests.', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(1, a.values[0].priority) | |||||
| self.assertEqual(2, a.values[0].weight) | |||||
| self.assertEqual(3, a.values[0].port) | |||||
| self.assertEqual('srv.unit.tests.', a.values[0].target) | |||||
| def test_srv_value(self): | |||||
| a = SrvValue({'priority': 0, 'weight': 0, 'port': 0, 'target': 'foo.'}) | |||||
| b = SrvValue({'priority': 1, 'weight': 0, 'port': 0, 'target': 'foo.'}) | |||||
| c = SrvValue({'priority': 0, 'weight': 2, 'port': 0, 'target': 'foo.'}) | |||||
| d = SrvValue({'priority': 0, 'weight': 0, 'port': 3, 'target': 'foo.'}) | |||||
| e = SrvValue({'priority': 0, 'weight': 0, 'port': 0, 'target': 'mmm.'}) | |||||
| self.assertEqual(a, a) | |||||
| self.assertEqual(b, b) | |||||
| self.assertEqual(c, c) | |||||
| self.assertEqual(d, d) | |||||
| self.assertEqual(e, e) | |||||
| self.assertNotEqual(a, b) | |||||
| self.assertNotEqual(a, c) | |||||
| self.assertNotEqual(a, d) | |||||
| self.assertNotEqual(a, e) | |||||
| self.assertNotEqual(b, a) | |||||
| self.assertNotEqual(b, c) | |||||
| self.assertNotEqual(b, d) | |||||
| self.assertNotEqual(b, e) | |||||
| self.assertNotEqual(c, a) | |||||
| self.assertNotEqual(c, b) | |||||
| self.assertNotEqual(c, d) | |||||
| self.assertNotEqual(c, e) | |||||
| self.assertNotEqual(d, a) | |||||
| self.assertNotEqual(d, b) | |||||
| self.assertNotEqual(d, c) | |||||
| self.assertNotEqual(d, e) | |||||
| self.assertNotEqual(e, a) | |||||
| self.assertNotEqual(e, b) | |||||
| self.assertNotEqual(e, c) | |||||
| self.assertNotEqual(e, d) | |||||
| self.assertTrue(a < b) | |||||
| self.assertTrue(a < c) | |||||
| self.assertTrue(b > a) | |||||
| self.assertTrue(b > c) | |||||
| self.assertTrue(c > a) | |||||
| self.assertTrue(c < b) | |||||
| self.assertTrue(a <= b) | |||||
| self.assertTrue(a <= c) | |||||
| self.assertTrue(a <= a) | |||||
| self.assertTrue(a >= a) | |||||
| self.assertTrue(b >= a) | |||||
| self.assertTrue(b >= c) | |||||
| self.assertTrue(b >= b) | |||||
| self.assertTrue(b <= b) | |||||
| self.assertTrue(c >= a) | |||||
| self.assertTrue(c <= b) | |||||
| self.assertTrue(c >= c) | |||||
| self.assertTrue(c <= c) | |||||
| # Hash | |||||
| values = set() | |||||
| values.add(a) | |||||
| self.assertTrue(a in values) | |||||
| self.assertFalse(b in values) | |||||
| values.add(b) | |||||
| self.assertTrue(b in values) | |||||
| def test_valiation(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.', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| # permit wildcard entries | |||||
| Record.new( | |||||
| self.zone, | |||||
| '*._tcp', | |||||
| { | |||||
| 'type': 'SRV', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'priority': 1, | |||||
| 'weight': 2, | |||||
| 'port': 3, | |||||
| 'target': 'food.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.assertEqual(['invalid name for SRV record'], 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.assertEqual(['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.assertEqual(['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.assertEqual(['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.assertEqual(['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.assertEqual(['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.assertEqual(['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.assertEqual(['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.assertEqual( | |||||
| ['SRV value "foo.bar.baz" missing trailing .'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| # falsey 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': '', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['missing target'], ctx.exception.reasons) | |||||
| # target must be a valid FQDN | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '_srv._tcp', | |||||
| { | |||||
| 'type': 'SRV', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'priority': 1, | |||||
| 'weight': 2, | |||||
| 'port': 3, | |||||
| 'target': '100 foo.bar.com.', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['Invalid SRV target "100 foo.bar.com." is not a valid FQDN.'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| @ -0,0 +1,330 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record.base import Record | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.record.sshfp import SshfpRecord, SshfpValue | |||||
| from octodns.record.rr import RrParseError | |||||
| from octodns.zone import Zone | |||||
| from helpers import SimpleProvider | |||||
| class TestRecordSshfp(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def test_sshfp(self): | |||||
| a_values = [ | |||||
| SshfpValue( | |||||
| { | |||||
| 'algorithm': 10, | |||||
| 'fingerprint_type': 11, | |||||
| 'fingerprint': 'abc123', | |||||
| } | |||||
| ), | |||||
| SshfpValue( | |||||
| { | |||||
| 'algorithm': 20, | |||||
| 'fingerprint_type': 21, | |||||
| 'fingerprint': 'def456', | |||||
| } | |||||
| ), | |||||
| ] | |||||
| a_data = {'ttl': 30, 'values': a_values} | |||||
| a = SshfpRecord(self.zone, 'a', a_data) | |||||
| self.assertEqual('a', a.name) | |||||
| self.assertEqual('a.unit.tests.', a.fqdn) | |||||
| self.assertEqual(30, a.ttl) | |||||
| self.assertEqual(a_values[0]['algorithm'], a.values[0].algorithm) | |||||
| self.assertEqual( | |||||
| a_values[0]['fingerprint_type'], a.values[0].fingerprint_type | |||||
| ) | |||||
| self.assertEqual(a_values[0]['fingerprint'], a.values[0].fingerprint) | |||||
| self.assertEqual(a_data, a.data) | |||||
| b_value = SshfpValue( | |||||
| {'algorithm': 30, 'fingerprint_type': 31, 'fingerprint': 'ghi789'} | |||||
| ) | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = SshfpRecord(self.zone, 'b', b_data) | |||||
| self.assertEqual(b_value['algorithm'], b.values[0].algorithm) | |||||
| self.assertEqual( | |||||
| b_value['fingerprint_type'], b.values[0].fingerprint_type | |||||
| ) | |||||
| self.assertEqual(b_value['fingerprint'], b.values[0].fingerprint) | |||||
| self.assertEqual(b_data, b.data) | |||||
| target = SimpleProvider() | |||||
| # No changes with self | |||||
| self.assertFalse(a.changes(a, target)) | |||||
| # Diff in algorithm causes change | |||||
| other = SshfpRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].algorithm = 22 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in fingerprint_type causes change | |||||
| other = SshfpRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].algorithm = a.values[0].algorithm | |||||
| other.values[0].fingerprint_type = 22 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in fingerprint causes change | |||||
| other = SshfpRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].fingerprint_type = a.values[0].fingerprint_type | |||||
| other.values[0].fingerprint = 22 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # __repr__ doesn't blow up | |||||
| a.__repr__() | |||||
| def test_sshfp_value_rdata_text(self): | |||||
| # empty string won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| SshfpValue.parse_rdata_text('') | |||||
| # single word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| SshfpValue.parse_rdata_text('nope') | |||||
| # 3rd word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| SshfpValue.parse_rdata_text('0 1 00479b27 another') | |||||
| # algorithm and fingerprint_type not ints | |||||
| self.assertEqual( | |||||
| { | |||||
| 'algorithm': 'one', | |||||
| 'fingerprint_type': 'two', | |||||
| 'fingerprint': '00479b27', | |||||
| }, | |||||
| SshfpValue.parse_rdata_text('one two 00479b27'), | |||||
| ) | |||||
| # valid | |||||
| self.assertEqual( | |||||
| {'algorithm': 1, 'fingerprint_type': 2, 'fingerprint': '00479b27'}, | |||||
| SshfpValue.parse_rdata_text('1 2 00479b27'), | |||||
| ) | |||||
| zone = Zone('unit.tests.', []) | |||||
| a = SshfpRecord( | |||||
| zone, | |||||
| 'sshfp', | |||||
| { | |||||
| 'ttl': 32, | |||||
| 'value': { | |||||
| 'algorithm': 1, | |||||
| 'fingerprint_type': 2, | |||||
| 'fingerprint': '00479b27', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(1, a.values[0].algorithm) | |||||
| self.assertEqual(2, a.values[0].fingerprint_type) | |||||
| self.assertEqual('00479b27', a.values[0].fingerprint) | |||||
| self.assertEqual('1 2 00479b27', a.values[0].rdata_text) | |||||
| def test_sshfp_value(self): | |||||
| a = SshfpValue( | |||||
| {'algorithm': 0, 'fingerprint_type': 0, 'fingerprint': 'abcd'} | |||||
| ) | |||||
| b = SshfpValue( | |||||
| {'algorithm': 1, 'fingerprint_type': 0, 'fingerprint': 'abcd'} | |||||
| ) | |||||
| c = SshfpValue( | |||||
| {'algorithm': 0, 'fingerprint_type': 1, 'fingerprint': 'abcd'} | |||||
| ) | |||||
| d = SshfpValue( | |||||
| {'algorithm': 0, 'fingerprint_type': 0, 'fingerprint': 'bcde'} | |||||
| ) | |||||
| self.assertEqual(a, a) | |||||
| self.assertEqual(b, b) | |||||
| self.assertEqual(c, c) | |||||
| self.assertEqual(d, d) | |||||
| self.assertNotEqual(a, b) | |||||
| self.assertNotEqual(a, c) | |||||
| self.assertNotEqual(a, d) | |||||
| self.assertNotEqual(b, a) | |||||
| self.assertNotEqual(b, c) | |||||
| self.assertNotEqual(b, d) | |||||
| self.assertNotEqual(c, a) | |||||
| self.assertNotEqual(c, b) | |||||
| self.assertNotEqual(c, d) | |||||
| self.assertNotEqual(d, a) | |||||
| self.assertNotEqual(d, b) | |||||
| self.assertNotEqual(d, c) | |||||
| self.assertTrue(a < b) | |||||
| self.assertTrue(a < c) | |||||
| self.assertTrue(b > a) | |||||
| self.assertTrue(b > c) | |||||
| self.assertTrue(c > a) | |||||
| self.assertTrue(c < b) | |||||
| self.assertTrue(a <= b) | |||||
| self.assertTrue(a <= c) | |||||
| self.assertTrue(a <= a) | |||||
| self.assertTrue(a >= a) | |||||
| self.assertTrue(b >= a) | |||||
| self.assertTrue(b >= c) | |||||
| self.assertTrue(b >= b) | |||||
| self.assertTrue(b <= b) | |||||
| self.assertTrue(c >= a) | |||||
| self.assertTrue(c <= b) | |||||
| self.assertTrue(c >= c) | |||||
| self.assertTrue(c <= c) | |||||
| # Hash | |||||
| values = set() | |||||
| values.add(a) | |||||
| self.assertTrue(a in values) | |||||
| self.assertFalse(b in values) | |||||
| values.add(b) | |||||
| self.assertTrue(b in values) | |||||
| def test_validation(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.assertEqual(['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': 2, | |||||
| 'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['invalid algorithm "nope"'], ctx.exception.reasons) | |||||
| # unrecognized algorithm | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'SSHFP', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'algorithm': 42, | |||||
| 'fingerprint_type': 1, | |||||
| 'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['unrecognized algorithm "42"'], ctx.exception.reasons) | |||||
| # missing fingerprint_type | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'SSHFP', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'algorithm': 2, | |||||
| 'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['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': 3, | |||||
| 'fingerprint_type': 'yeeah', | |||||
| 'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid fingerprint_type "yeeah"'], ctx.exception.reasons | |||||
| ) | |||||
| # unrecognized fingerprint_type | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'SSHFP', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'algorithm': 1, | |||||
| 'fingerprint_type': 42, | |||||
| 'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['unrecognized fingerprint_type "42"'], 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.assertEqual(['missing fingerprint'], ctx.exception.reasons) | |||||
| @ -0,0 +1,31 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record.alias import AliasRecord | |||||
| from octodns.record.target import _TargetValue | |||||
| from octodns.zone import Zone | |||||
| class TestRecordTarget(TestCase): | |||||
| def test_target_rdata_text(self): | |||||
| # anything goes, we're a noop | |||||
| for s in ( | |||||
| None, | |||||
| '', | |||||
| 'word', | |||||
| 42, | |||||
| 42.43, | |||||
| '1.2.3', | |||||
| 'some.words.that.here', | |||||
| '1.2.word.4', | |||||
| '1.2.3.4', | |||||
| ): | |||||
| self.assertEqual(s, _TargetValue.parse_rdata_text(s)) | |||||
| zone = Zone('unit.tests.', []) | |||||
| a = AliasRecord(zone, 'a', {'ttl': 42, 'value': 'some.target.'}) | |||||
| self.assertEqual('some.target.', a.value.rdata_text) | |||||
| @ -0,0 +1,421 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.tlsa import TlsaRecord, TlsaValue | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.record.rr import RrParseError | |||||
| from octodns.zone import Zone | |||||
| from helpers import SimpleProvider | |||||
| class TestRecordTlsa(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def test_tlsa(self): | |||||
| a_values = [ | |||||
| TlsaValue( | |||||
| { | |||||
| 'certificate_usage': 1, | |||||
| 'selector': 1, | |||||
| 'matching_type': 1, | |||||
| 'certificate_association_data': 'ABABABABABABABABAB', | |||||
| } | |||||
| ), | |||||
| TlsaValue( | |||||
| { | |||||
| 'certificate_usage': 2, | |||||
| 'selector': 0, | |||||
| 'matching_type': 2, | |||||
| 'certificate_association_data': 'ABABABABABABABABAC', | |||||
| } | |||||
| ), | |||||
| ] | |||||
| a_data = {'ttl': 30, 'values': a_values} | |||||
| a = TlsaRecord(self.zone, 'a', a_data) | |||||
| self.assertEqual('a.unit.tests.', a.fqdn) | |||||
| self.assertEqual('a', a.name) | |||||
| self.assertEqual(30, a.ttl) | |||||
| self.assertEqual( | |||||
| a_values[0]['certificate_usage'], a.values[0].certificate_usage | |||||
| ) | |||||
| self.assertEqual(a_values[0]['selector'], a.values[0].selector) | |||||
| self.assertEqual( | |||||
| a_values[0]['matching_type'], a.values[0].matching_type | |||||
| ) | |||||
| self.assertEqual( | |||||
| a_values[0]['certificate_association_data'], | |||||
| a.values[0].certificate_association_data, | |||||
| ) | |||||
| self.assertEqual( | |||||
| a_values[1]['certificate_usage'], a.values[1].certificate_usage | |||||
| ) | |||||
| self.assertEqual(a_values[1]['selector'], a.values[1].selector) | |||||
| self.assertEqual( | |||||
| a_values[1]['matching_type'], a.values[1].matching_type | |||||
| ) | |||||
| self.assertEqual( | |||||
| a_values[1]['certificate_association_data'], | |||||
| a.values[1].certificate_association_data, | |||||
| ) | |||||
| self.assertEqual(a_data, a.data) | |||||
| b_value = TlsaValue( | |||||
| { | |||||
| 'certificate_usage': 0, | |||||
| 'selector': 0, | |||||
| 'matching_type': 0, | |||||
| 'certificate_association_data': 'AAAAAAAAAAAAAAA', | |||||
| } | |||||
| ) | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = TlsaRecord(self.zone, 'b', b_data) | |||||
| self.assertEqual( | |||||
| b_value['certificate_usage'], b.values[0].certificate_usage | |||||
| ) | |||||
| self.assertEqual(b_value['selector'], b.values[0].selector) | |||||
| self.assertEqual(b_value['matching_type'], b.values[0].matching_type) | |||||
| self.assertEqual( | |||||
| b_value['certificate_association_data'], | |||||
| b.values[0].certificate_association_data, | |||||
| ) | |||||
| self.assertEqual(b_data, b.data) | |||||
| target = SimpleProvider() | |||||
| # No changes with self | |||||
| self.assertFalse(a.changes(a, target)) | |||||
| # Diff in certificate_usage causes change | |||||
| other = TlsaRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].certificate_usage = 0 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in selector causes change | |||||
| other = TlsaRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].selector = 0 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in matching_type causes change | |||||
| other = TlsaRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].matching_type = 0 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in certificate_association_data causes change | |||||
| other = TlsaRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].certificate_association_data = 'AAAAAAAAAAAAA' | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # __repr__ doesn't blow up | |||||
| a.__repr__() | |||||
| def test_tsla_value_rdata_text(self): | |||||
| # empty string won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| TlsaValue.parse_rdata_text('') | |||||
| # single word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| TlsaValue.parse_rdata_text('nope') | |||||
| # 2nd word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| TlsaValue.parse_rdata_text('1 2') | |||||
| # 3rd word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| TlsaValue.parse_rdata_text('1 2 3') | |||||
| # 5th word won't parse | |||||
| with self.assertRaises(RrParseError): | |||||
| TlsaValue.parse_rdata_text('1 2 3 abcd another') | |||||
| # non-ints | |||||
| self.assertEqual( | |||||
| { | |||||
| 'certificate_usage': 'one', | |||||
| 'selector': 'two', | |||||
| 'matching_type': 'three', | |||||
| 'certificate_association_data': 'abcd', | |||||
| }, | |||||
| TlsaValue.parse_rdata_text('one two three abcd'), | |||||
| ) | |||||
| # valid | |||||
| self.assertEqual( | |||||
| { | |||||
| 'certificate_usage': 1, | |||||
| 'selector': 2, | |||||
| 'matching_type': 3, | |||||
| 'certificate_association_data': 'abcd', | |||||
| }, | |||||
| TlsaValue.parse_rdata_text('1 2 3 abcd'), | |||||
| ) | |||||
| zone = Zone('unit.tests.', []) | |||||
| a = TlsaRecord( | |||||
| zone, | |||||
| 'tlsa', | |||||
| { | |||||
| 'ttl': 32, | |||||
| 'value': { | |||||
| 'certificate_usage': 2, | |||||
| 'selector': 1, | |||||
| 'matching_type': 0, | |||||
| 'certificate_association_data': 'abcd', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(2, a.values[0].certificate_usage) | |||||
| self.assertEqual(1, a.values[0].selector) | |||||
| self.assertEqual(0, a.values[0].matching_type) | |||||
| self.assertEqual('abcd', a.values[0].certificate_association_data) | |||||
| self.assertEqual('2 1 0 abcd', a.values[0].rdata_text) | |||||
| def test_validation(self): | |||||
| # doesn't blow up | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'TLSA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'certificate_usage': 0, | |||||
| 'selector': 0, | |||||
| 'matching_type': 0, | |||||
| 'certificate_association_data': 'AAAAAAAAAAAAA', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| # Multi value, second missing certificate usage | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'TLSA', | |||||
| 'ttl': 600, | |||||
| 'values': [ | |||||
| { | |||||
| 'certificate_usage': 0, | |||||
| 'selector': 0, | |||||
| 'matching_type': 0, | |||||
| 'certificate_association_data': 'AAAAAAAAAAAAA', | |||||
| }, | |||||
| { | |||||
| 'selector': 0, | |||||
| 'matching_type': 0, | |||||
| 'certificate_association_data': 'AAAAAAAAAAAAA', | |||||
| }, | |||||
| ], | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['missing certificate_usage'], ctx.exception.reasons | |||||
| ) | |||||
| # missing certificate_association_data | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'TLSA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'certificate_usage': 0, | |||||
| 'selector': 0, | |||||
| 'matching_type': 0, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['missing certificate_association_data'], ctx.exception.reasons | |||||
| ) | |||||
| # missing certificate_usage | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'TLSA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'selector': 0, | |||||
| 'matching_type': 0, | |||||
| 'certificate_association_data': 'AAAAAAAAAAAAA', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['missing certificate_usage'], ctx.exception.reasons | |||||
| ) | |||||
| # False certificate_usage | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'TLSA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'certificate_usage': 4, | |||||
| 'selector': 0, | |||||
| 'matching_type': 0, | |||||
| 'certificate_association_data': 'AAAAAAAAAAAAA', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| 'invalid certificate_usage "{value["certificate_usage"]}"', | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| # Invalid certificate_usage | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'TLSA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'certificate_usage': 'XYZ', | |||||
| 'selector': 0, | |||||
| 'matching_type': 0, | |||||
| 'certificate_association_data': 'AAAAAAAAAAAAA', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| 'invalid certificate_usage "{value["certificate_usage"]}"', | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| # missing selector | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'TLSA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'certificate_usage': 0, | |||||
| 'matching_type': 0, | |||||
| 'certificate_association_data': 'AAAAAAAAAAAAA', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['missing selector'], ctx.exception.reasons) | |||||
| # False selector | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'TLSA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'certificate_usage': 0, | |||||
| 'selector': 4, | |||||
| 'matching_type': 0, | |||||
| 'certificate_association_data': 'AAAAAAAAAAAAA', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| 'invalid selector "{value["selector"]}"', ctx.exception.reasons | |||||
| ) | |||||
| # Invalid selector | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'TLSA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'certificate_usage': 0, | |||||
| 'selector': 'XYZ', | |||||
| 'matching_type': 0, | |||||
| 'certificate_association_data': 'AAAAAAAAAAAAA', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| 'invalid selector "{value["selector"]}"', ctx.exception.reasons | |||||
| ) | |||||
| # missing matching_type | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'TLSA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'certificate_usage': 0, | |||||
| 'selector': 0, | |||||
| 'certificate_association_data': 'AAAAAAAAAAAAA', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['missing matching_type'], ctx.exception.reasons) | |||||
| # False matching_type | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'TLSA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'certificate_usage': 0, | |||||
| 'selector': 1, | |||||
| 'matching_type': 3, | |||||
| 'certificate_association_data': 'AAAAAAAAAAAAA', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| 'invalid matching_type "{value["matching_type"]}"', | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| # Invalid matching_type | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'TLSA', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'certificate_usage': 0, | |||||
| 'selector': 1, | |||||
| 'matching_type': 'XYZ', | |||||
| 'certificate_association_data': 'AAAAAAAAAAAAA', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| 'invalid matching_type "{value["matching_type"]}"', | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| @ -0,0 +1,144 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.txt import TxtRecord | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.zone import Zone | |||||
| class TestRecordTxt(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def assertMultipleValues(self, _type, a_values, b_value): | |||||
| a_data = {'ttl': 30, 'values': a_values} | |||||
| a = _type(self.zone, 'a', a_data) | |||||
| self.assertEqual('a', a.name) | |||||
| self.assertEqual('a.unit.tests.', a.fqdn) | |||||
| self.assertEqual(30, a.ttl) | |||||
| self.assertEqual(a_values, a.values) | |||||
| self.assertEqual(a_data, a.data) | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = _type(self.zone, 'b', b_data) | |||||
| self.assertEqual([b_value], b.values) | |||||
| self.assertEqual(b_data, b.data) | |||||
| def test_txt(self): | |||||
| a_values = ['a one', 'a two'] | |||||
| b_value = 'b other' | |||||
| self.assertMultipleValues(TxtRecord, a_values, b_value) | |||||
| def test_validation(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.assertEqual(['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.assertEqual( | |||||
| ['unescaped ; in "this has some; semi-colons\\; in it"'], | |||||
| ctx.exception.reasons, | |||||
| ) | |||||
| def test_long_value_chunking(self): | |||||
| expected = ( | |||||
| '"Lorem ipsum dolor sit amet, consectetur adipiscing ' | |||||
| 'elit, sed do eiusmod tempor incididunt ut labore et dolore ' | |||||
| 'magna aliqua. Ut enim ad minim veniam, quis nostrud ' | |||||
| 'exercitation ullamco laboris nisi ut aliquip ex ea commodo ' | |||||
| 'consequat. Duis aute irure dolor i" "n reprehenderit in ' | |||||
| 'voluptate velit esse cillum dolore eu fugiat nulla pariatur. ' | |||||
| 'Excepteur sint occaecat cupidatat non proident, sunt in culpa ' | |||||
| 'qui officia deserunt mollit anim id est laborum."' | |||||
| ) | |||||
| long_value = ( | |||||
| 'Lorem ipsum dolor sit amet, consectetur adipiscing ' | |||||
| 'elit, sed do eiusmod tempor incididunt ut labore et dolore ' | |||||
| 'magna aliqua. Ut enim ad minim veniam, quis nostrud ' | |||||
| 'exercitation ullamco laboris nisi ut aliquip ex ea commodo ' | |||||
| 'consequat. Duis aute irure dolor in reprehenderit in ' | |||||
| 'voluptate velit esse cillum dolore eu fugiat nulla ' | |||||
| 'pariatur. Excepteur sint occaecat cupidatat non proident, ' | |||||
| 'sunt in culpa qui officia deserunt mollit anim id est ' | |||||
| 'laborum.' | |||||
| ) | |||||
| # Single string | |||||
| single = Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'TXT', | |||||
| 'ttl': 600, | |||||
| 'values': [ | |||||
| 'hello world', | |||||
| long_value, | |||||
| 'this has some\\; semi-colons\\; in it', | |||||
| ], | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(3, len(single.values)) | |||||
| self.assertEqual(3, len(single.chunked_values)) | |||||
| # Note we are checking that this normalizes the chunking, not that we | |||||
| # get out what we put in. | |||||
| self.assertEqual(expected, single.chunked_values[0]) | |||||
| long_split_value = ( | |||||
| '"Lorem ipsum dolor sit amet, consectetur ' | |||||
| 'adipiscing elit, sed do eiusmod tempor incididunt ut ' | |||||
| 'labore et dolore magna aliqua. Ut enim ad minim veniam, ' | |||||
| 'quis nostrud exercitation ullamco laboris nisi ut aliquip ' | |||||
| 'ex" " ea commodo consequat. Duis aute irure dolor in ' | |||||
| 'reprehenderit in voluptate velit esse cillum dolore eu ' | |||||
| 'fugiat nulla pariatur. Excepteur sint occaecat cupidatat ' | |||||
| 'non proident, sunt in culpa qui officia deserunt mollit ' | |||||
| 'anim id est laborum."' | |||||
| ) | |||||
| # Chunked | |||||
| chunked = Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'TXT', | |||||
| 'ttl': 600, | |||||
| 'values': [ | |||||
| '"hello world"', | |||||
| long_split_value, | |||||
| '"this has some\\; semi-colons\\; in it"', | |||||
| ], | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(expected, chunked.chunked_values[0]) | |||||
| # should be single values, no quoting | |||||
| self.assertEqual(single.values, chunked.values) | |||||
| # should be chunked values, with quoting | |||||
| self.assertEqual(single.chunked_values, chunked.chunked_values) | |||||
| @ -0,0 +1,391 @@ | |||||
| # | |||||
| # | |||||
| # | |||||
| from unittest import TestCase | |||||
| from octodns.record import Record | |||||
| from octodns.record.urlfwd import UrlfwdRecord, UrlfwdValue | |||||
| from octodns.record.exception import ValidationError | |||||
| from octodns.zone import Zone | |||||
| from helpers import SimpleProvider | |||||
| class TestRecordUrlfwd(TestCase): | |||||
| zone = Zone('unit.tests.', []) | |||||
| def test_urlfwd(self): | |||||
| a_values = [ | |||||
| UrlfwdValue( | |||||
| { | |||||
| 'path': '/', | |||||
| 'target': 'http://foo', | |||||
| 'code': 301, | |||||
| 'masking': 2, | |||||
| 'query': 0, | |||||
| } | |||||
| ), | |||||
| UrlfwdValue( | |||||
| { | |||||
| 'path': '/target', | |||||
| 'target': 'http://target', | |||||
| 'code': 302, | |||||
| 'masking': 2, | |||||
| 'query': 0, | |||||
| } | |||||
| ), | |||||
| ] | |||||
| a_data = {'ttl': 30, 'values': a_values} | |||||
| a = UrlfwdRecord(self.zone, 'a', a_data) | |||||
| self.assertEqual('a', a.name) | |||||
| self.assertEqual('a.unit.tests.', a.fqdn) | |||||
| self.assertEqual(30, a.ttl) | |||||
| self.assertEqual(a_values[0]['path'], a.values[0].path) | |||||
| self.assertEqual(a_values[0]['target'], a.values[0].target) | |||||
| self.assertEqual(a_values[0]['code'], a.values[0].code) | |||||
| self.assertEqual(a_values[0]['masking'], a.values[0].masking) | |||||
| self.assertEqual(a_values[0]['query'], a.values[0].query) | |||||
| self.assertEqual(a_values[1]['path'], a.values[1].path) | |||||
| self.assertEqual(a_values[1]['target'], a.values[1].target) | |||||
| self.assertEqual(a_values[1]['code'], a.values[1].code) | |||||
| self.assertEqual(a_values[1]['masking'], a.values[1].masking) | |||||
| self.assertEqual(a_values[1]['query'], a.values[1].query) | |||||
| self.assertEqual(a_data, a.data) | |||||
| b_value = UrlfwdValue( | |||||
| { | |||||
| 'path': '/', | |||||
| 'target': 'http://location', | |||||
| 'code': 301, | |||||
| 'masking': 2, | |||||
| 'query': 0, | |||||
| } | |||||
| ) | |||||
| b_data = {'ttl': 30, 'value': b_value} | |||||
| b = UrlfwdRecord(self.zone, 'b', b_data) | |||||
| self.assertEqual(b_value['path'], b.values[0].path) | |||||
| self.assertEqual(b_value['target'], b.values[0].target) | |||||
| self.assertEqual(b_value['code'], b.values[0].code) | |||||
| self.assertEqual(b_value['masking'], b.values[0].masking) | |||||
| self.assertEqual(b_value['query'], b.values[0].query) | |||||
| self.assertEqual(b_data, b.data) | |||||
| target = SimpleProvider() | |||||
| # No changes with self | |||||
| self.assertFalse(a.changes(a, target)) | |||||
| # Diff in path causes change | |||||
| other = UrlfwdRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].path = '/change' | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in target causes change | |||||
| other = UrlfwdRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].target = 'http://target' | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in code causes change | |||||
| other = UrlfwdRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].code = 302 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in masking causes change | |||||
| other = UrlfwdRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].masking = 0 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # Diff in query causes change | |||||
| other = UrlfwdRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) | |||||
| other.values[0].query = 1 | |||||
| change = a.changes(other, target) | |||||
| self.assertEqual(change.existing, a) | |||||
| self.assertEqual(change.new, other) | |||||
| # hash | |||||
| v = UrlfwdValue( | |||||
| { | |||||
| 'path': '/', | |||||
| 'target': 'http://place', | |||||
| 'code': 301, | |||||
| 'masking': 2, | |||||
| 'query': 0, | |||||
| } | |||||
| ) | |||||
| o = UrlfwdValue( | |||||
| { | |||||
| 'path': '/location', | |||||
| 'target': 'http://redirect', | |||||
| 'code': 302, | |||||
| 'masking': 2, | |||||
| 'query': 0, | |||||
| } | |||||
| ) | |||||
| values = set() | |||||
| values.add(v) | |||||
| self.assertTrue(v in values) | |||||
| self.assertFalse(o in values) | |||||
| values.add(o) | |||||
| self.assertTrue(o in values) | |||||
| # __repr__ doesn't blow up | |||||
| a.__repr__() | |||||
| def test_validation(self): | |||||
| # doesn't blow up | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'URLFWD', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'path': '/', | |||||
| 'target': 'http://foo', | |||||
| 'code': 301, | |||||
| 'masking': 2, | |||||
| 'query': 0, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'URLFWD', | |||||
| 'ttl': 600, | |||||
| 'values': [ | |||||
| { | |||||
| 'path': '/', | |||||
| 'target': 'http://foo', | |||||
| 'code': 301, | |||||
| 'masking': 2, | |||||
| 'query': 0, | |||||
| }, | |||||
| { | |||||
| 'path': '/target', | |||||
| 'target': 'http://target', | |||||
| 'code': 302, | |||||
| 'masking': 2, | |||||
| 'query': 0, | |||||
| }, | |||||
| ], | |||||
| }, | |||||
| ) | |||||
| # missing path | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'URLFWD', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'target': 'http://foo', | |||||
| 'code': 301, | |||||
| 'masking': 2, | |||||
| 'query': 0, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['missing path'], ctx.exception.reasons) | |||||
| # missing target | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'URLFWD', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'path': '/', | |||||
| 'code': 301, | |||||
| 'masking': 2, | |||||
| 'query': 0, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['missing target'], ctx.exception.reasons) | |||||
| # missing code | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'URLFWD', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'path': '/', | |||||
| 'target': 'http://foo', | |||||
| 'masking': 2, | |||||
| 'query': 0, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['missing code'], ctx.exception.reasons) | |||||
| # invalid code | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'URLFWD', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'path': '/', | |||||
| 'target': 'http://foo', | |||||
| 'code': 'nope', | |||||
| 'masking': 2, | |||||
| 'query': 0, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['invalid return code "nope"'], ctx.exception.reasons) | |||||
| # unrecognized code | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'URLFWD', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'path': '/', | |||||
| 'target': 'http://foo', | |||||
| 'code': 3, | |||||
| 'masking': 2, | |||||
| 'query': 0, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['unrecognized return code "3"'], ctx.exception.reasons | |||||
| ) | |||||
| # missing masking | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'URLFWD', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'path': '/', | |||||
| 'target': 'http://foo', | |||||
| 'code': 301, | |||||
| 'query': 0, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['missing masking'], ctx.exception.reasons) | |||||
| # invalid masking | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'URLFWD', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'path': '/', | |||||
| 'target': 'http://foo', | |||||
| 'code': 301, | |||||
| 'masking': 'nope', | |||||
| 'query': 0, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid masking setting "nope"'], ctx.exception.reasons | |||||
| ) | |||||
| # unrecognized masking | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'URLFWD', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'path': '/', | |||||
| 'target': 'http://foo', | |||||
| 'code': 301, | |||||
| 'masking': 3, | |||||
| 'query': 0, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['unrecognized masking setting "3"'], ctx.exception.reasons | |||||
| ) | |||||
| # missing query | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'URLFWD', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'path': '/', | |||||
| 'target': 'http://foo', | |||||
| 'code': 301, | |||||
| 'masking': 2, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual(['missing query'], ctx.exception.reasons) | |||||
| # invalid query | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'URLFWD', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'path': '/', | |||||
| 'target': 'http://foo', | |||||
| 'code': 301, | |||||
| 'masking': 2, | |||||
| 'query': 'nope', | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['invalid query setting "nope"'], ctx.exception.reasons | |||||
| ) | |||||
| # unrecognized query | |||||
| with self.assertRaises(ValidationError) as ctx: | |||||
| Record.new( | |||||
| self.zone, | |||||
| '', | |||||
| { | |||||
| 'type': 'URLFWD', | |||||
| 'ttl': 600, | |||||
| 'value': { | |||||
| 'path': '/', | |||||
| 'target': 'http://foo', | |||||
| 'code': 301, | |||||
| 'masking': 2, | |||||
| 'query': 3, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| self.assertEqual( | |||||
| ['unrecognized query setting "3"'], ctx.exception.reasons | |||||
| ) | |||||