# # # from unittest import TestCase from helpers import SimpleProvider from octodns.record import Record from octodns.record.exception import ValidationError from octodns.record.rr import RrParseError from octodns.record.svcb import SvcbRecord, SvcbValue from octodns.zone import Zone class TestRecordSvcb(TestCase): zone = Zone('unit.tests.', []) def test_svcb(self): aliasmode_value = SvcbValue( {'svcpriority': 0, 'targetname': 'foo.example.com.'} ) aliasmode_data = {'ttl': 300, 'value': aliasmode_value} a = SvcbRecord(self.zone, 'alias', aliasmode_data) self.assertEqual('alias', a.name) self.assertEqual('alias.unit.tests.', a.fqdn) self.assertEqual(300, a.ttl) self.assertEqual( aliasmode_value['svcpriority'], a.values[0].svcpriority ) self.assertEqual(aliasmode_value['targetname'], a.values[0].targetname) self.assertEqual(aliasmode_value['svcparams'], a.values[0].svcparams) self.assertEqual(aliasmode_data, a.data) servicemode_values = [ SvcbValue( { 'svcpriority': 1, 'targetname': 'foo.example.com.', 'svcparams': {'port': 8002}, } ), SvcbValue( { 'svcpriority': 2, 'targetname': 'foo.example.net.', 'svcparams': {'port': 8080}, } ), ] servicemode_data = {'ttl': 300, 'values': servicemode_values} b = SvcbRecord(self.zone, 'service', servicemode_data) self.assertEqual('service', b.name) self.assertEqual('service.unit.tests.', b.fqdn) self.assertEqual(300, b.ttl) self.assertEqual( servicemode_values[0]['svcpriority'], b.values[0].svcpriority ) self.assertEqual( servicemode_values[0]['targetname'], b.values[0].targetname ) self.assertEqual( servicemode_values[0]['svcparams'], b.values[0].svcparams ) self.assertEqual( servicemode_values[1]['svcpriority'], b.values[1].svcpriority ) self.assertEqual( servicemode_values[1]['targetname'], b.values[1].targetname ) self.assertEqual( servicemode_values[1]['svcparams'], b.values[1].svcparams ) self.assertEqual(servicemode_data, b.data) target = SimpleProvider() # No changes with self self.assertFalse(b.changes(b, target)) # Diff in priority causes change other = SvcbRecord( self.zone, 'service2', {'ttl': 30, 'values': servicemode_values} ) other.values[0].svcpriority = 22 change = b.changes(other, target) self.assertEqual(change.existing, b) self.assertEqual(change.new, other) # Diff in target causes change other.values[0].svcpriority = b.values[0].svcpriority other.values[0].targetname = 'blabla.example.com.' change = b.changes(other, target) self.assertEqual(change.existing, b) self.assertEqual(change.new, other) # Diff in params causes change other.values[0].targetname = b.values[0].targetname other.values[0].svcparams = {'port': '8888'} change = b.changes(other, target) self.assertEqual(change.existing, b) self.assertEqual(change.new, other) # __repr__ doesn't blow up a.__repr__() b.__repr__() def test_svcb_value_rdata_text(self): # empty string won't parse with self.assertRaises(RrParseError): SvcbValue.parse_rdata_text('') # single word won't parse with self.assertRaises(RrParseError): SvcbValue.parse_rdata_text('nope') # Double keys are not allowed with self.assertRaises(RrParseError): SvcbValue.parse_rdata_text('1 foo.example.com port=8080 port=8084') # priority not int self.assertEqual( { 'svcpriority': 'one', 'targetname': 'foo.example.com', 'svcparams': dict(), }, SvcbValue.parse_rdata_text('one foo.example.com'), ) # valid with params self.assertEqual( { 'svcpriority': 1, 'targetname': 'svcb.unit.tests.', 'svcparams': {'port': '8080', 'no-default-alpn': None}, }, SvcbValue.parse_rdata_text( '1 svcb.unit.tests. port=8080 no-default-alpn' ), ) # quoted target self.assertEqual( { 'svcpriority': 1, 'targetname': 'svcb.unit.tests.', 'svcparams': dict(), }, SvcbValue.parse_rdata_text('1 "svcb.unit.tests."'), ) zone = Zone('unit.tests.', []) a = SvcbRecord( zone, 'svc', { 'ttl': 32, 'value': { 'svcpriority': 1, 'targetname': 'svcb.unit.tests.', 'svcparams': {'port': '8080'}, }, }, ) self.assertEqual(1, a.values[0].svcpriority) self.assertEqual('svcb.unit.tests.', a.values[0].targetname) self.assertEqual({'port': '8080'}, a.values[0].svcparams) # both directions should match rdata = '1 svcb.unit.tests. no-default-alpn port=8080 ipv4hint=192.0.2.2,192.0.2.53 key3333=foobar' record = SvcbRecord( zone, 'svc', {'ttl': 32, 'value': SvcbValue.parse_rdata_text(rdata)} ) self.assertEqual(rdata, record.values[0].rdata_text) # both directions should match rdata = '0 svcb.unit.tests.' record = SvcbRecord( zone, 'svc', {'ttl': 32, 'value': SvcbValue.parse_rdata_text(rdata)} ) self.assertEqual(rdata, record.values[0].rdata_text) # quoted params need to be correctly handled rdata = '1 svcb.unit.tests. no-default-alpn port=8080 ipv4hint="192.0.2.2,192.0.2.53" key3333="foobar"' record = SvcbRecord( zone, 'svc', {'ttl': 32, 'value': SvcbValue.parse_rdata_text(rdata)} ) self.assertEqual(rdata.replace('"', ''), record.values[0].rdata_text) def test_svcb_value(self): a = SvcbValue( {'svcpriority': 0, 'targetname': 'foo.', 'svcparams': dict()} ) b = SvcbValue( {'svcpriority': 1, 'targetname': 'foo.', 'svcparams': dict()} ) c = SvcbValue( { 'svcpriority': 0, 'targetname': 'foo.', 'svcparams': {'port': 8080, 'no-default-alpn': None}, } ) d = SvcbValue( { 'svcpriority': 0, 'targetname': 'foo.', 'svcparams': {'alpn': ['h2', 'h3'], 'port': 8080}, } ) e = SvcbValue( { 'svcpriority': 0, 'targetname': 'mmm.', 'svcparams': {'ipv4hint': ['192.0.2.1']}, } ) 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_validation(self): # doesn't blow up Record.new( self.zone, 'svcb', { 'type': 'SVCB', 'ttl': 600, 'value': {'svcpriority': 1, 'targetname': 'foo.bar.baz.'}, }, ) # Wildcards are fine Record.new( self.zone, '*', { 'type': 'SVCB', 'ttl': 600, 'value': {'svcpriority': 1, 'targetname': 'foo.bar.baz.'}, }, ) # missing priority with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': {'targetname': 'foo.bar.baz.'}, }, ) self.assertEqual(['missing svcpriority'], ctx.exception.reasons) # invalid priority with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 'foo', 'targetname': 'foo.bar.baz.', }, }, ) self.assertEqual(['invalid priority "foo"'], ctx.exception.reasons) # invalid priority (out of range) with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 100000, 'targetname': 'foo.bar.baz.', }, }, ) self.assertEqual(['invalid priority "100000"'], ctx.exception.reasons) # missing target with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', {'type': 'SVCB', 'ttl': 600, 'value': {'svcpriority': 1}}, ) self.assertEqual(['missing targetname'], ctx.exception.reasons) # invalid target with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': {'svcpriority': 1, 'targetname': 'foo.bar.baz'}, }, ) self.assertEqual( ['SVCB value "foo.bar.baz" missing trailing .'], ctx.exception.reasons, ) # falsey target with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': {'svcpriority': 1, 'targetname': ''}, }, ) self.assertEqual(['missing targetname'], ctx.exception.reasons) # target must be a valid FQDN with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 1, 'targetname': 'bla foo.bar.com.', }, }, ) self.assertEqual( ['Invalid SVCB target "bla foo.bar.com." is not a valid FQDN.'], ctx.exception.reasons, ) # target can be root label Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': {'svcpriority': 1, 'targetname': '.'}, }, ) # Params can't be set for AliasMode with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 0, 'targetname': 'foo.bar.com.', 'svcparams': {'port': '8000'}, }, }, ) self.assertEqual( ['svcparams set on AliasMode SVCB record'], ctx.exception.reasons ) # Unknown param with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 1, 'targetname': 'foo.bar.com.', 'svcparams': {'blablabla': '222'}, }, }, ) self.assertEqual(['Unknown SvcParam blablabla'], ctx.exception.reasons) # Port number invalid with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 1, 'targetname': 'foo.bar.com.', 'svcparams': {'port': 100000}, }, }, ) self.assertEqual( ['port 100000 is not a valid number'], ctx.exception.reasons ) # Port number not an int with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 1, 'targetname': 'foo.bar.com.', 'svcparams': {'port': 'foo'}, }, }, ) self.assertEqual(['port is not a number'], ctx.exception.reasons) # no-default-alpn set Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 1, 'targetname': 'foo.bar.com.', 'svcparams': {'no-default-alpn': None}, }, }, ) # no-default-alpn has value with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 1, 'targetname': 'foo.bar.com.', 'svcparams': {'no-default-alpn': 'foobar'}, }, }, ) self.assertEqual( ['SvcParam no-default-alpn has value when it should not'], ctx.exception.reasons, ) # alpn is broken with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 1, 'targetname': 'foo.bar.com.', 'svcparams': {'alpn': ['h2', '😅']}, }, }, ) self.assertEqual(['non ASCII character in "😅"'], ctx.exception.reasons) # svcbvaluelist that is not a list with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 1, 'targetname': 'foo.bar.com.', 'svcparams': { 'ipv4hint': '192.0.2.1,192.0.2.2', 'ipv6hint': '2001:db8::1', 'mandatory': 'ipv6hint', 'alpn': 'h2,h3', }, }, }, ) self.assertEqual( [ 'ipv4hint is not a list', 'ipv6hint is not a list', 'mandatory is not a list', 'alpn is not a list', ], ctx.exception.reasons, ) # ipv4hint with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 1, 'targetname': 'foo.bar.com.', 'svcparams': { 'ipv4hint': ['192.0.2.0', '500.500.30.30'] }, }, }, ) self.assertEqual( ['ipv4hint "500.500.30.30" is not a valid IPv4 address'], ctx.exception.reasons, ) # ipv6hint with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 1, 'targetname': 'foo.bar.com.', 'svcparams': { 'ipv6hint': ['2001:db8:43::1', 'notanip'] }, }, }, ) self.assertEqual( ['ipv6hint "notanip" is not a valid IPv6 address'], ctx.exception.reasons, ) # mandatory with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 1, 'targetname': 'foo.bar.com.', 'svcparams': { 'mandatory': ['ipv4hint', 'unknown', 'key4444'] }, }, }, ) self.assertEqual( ['unsupported SvcParam "unknown" in mandatory'], ctx.exception.reasons, ) # ech with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 1, 'targetname': 'foo.bar.com.', 'svcparams': {'ech': ' dG90YWxseUZha2VFQ0hPcHRpb24'}, }, }, ) self.assertEqual( ['ech SvcParam is invalid Base64'], ctx.exception.reasons ) # broken keyNNNN format with self.assertRaises(ValidationError) as ctx: Record.new( self.zone, 'foo', { 'type': 'SVCB', 'ttl': 600, 'value': { 'svcpriority': 1, 'targetname': 'foo.bar.com.', 'svcparams': { 'key100000': 'foo', 'key3333': 'bar', 'keyXXX': 'foo', }, }, }, ) self.assertEqual( [ 'SvcParam key "key100000" has wrong key number', 'SvcParam key "keyXXX" has wrong format', ], ctx.exception.reasons, ) class TestSrvValue(TestCase): def test_template(self): value = SvcbValue({'svcpriority': 0, 'targetname': 'foo.example.com.'}) got = value.template({'needle': 42}) self.assertIs(value, got) value = SvcbValue( {'svcpriority': 0, 'targetname': 'foo.{needle}.example.com.'} ) got = value.template({'needle': 42}) self.assertIsNot(value, got) self.assertEqual('foo.42.example.com.', got.targetname)