summaryrefslogtreecommitdiffstats
path: root/src/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to 'src/tests/unit')
-rw-r--r--src/tests/unit/all.mk53
-rw-r--r--src/tests/unit/ascend.txt5
-rw-r--r--src/tests/unit/condition.txt679
-rw-r--r--src/tests/unit/dhcp.txt44
-rw-r--r--src/tests/unit/eapol_key_msg.txt14
-rw-r--r--src/tests/unit/errors.txt17
-rw-r--r--src/tests/unit/escape.txt74
-rw-r--r--src/tests/unit/extended.txt103
-rw-r--r--src/tests/unit/lucent.txt11
-rw-r--r--src/tests/unit/rfc.txt204
-rw-r--r--src/tests/unit/rfc4849.txt49
-rw-r--r--src/tests/unit/tunnel.txt87
-rw-r--r--src/tests/unit/vendor.txt48
-rw-r--r--src/tests/unit/wimax.txt171
-rw-r--r--src/tests/unit/xlat.txt142
15 files changed, 1701 insertions, 0 deletions
diff --git a/src/tests/unit/all.mk b/src/tests/unit/all.mk
new file mode 100644
index 0000000..f8cef43
--- /dev/null
+++ b/src/tests/unit/all.mk
@@ -0,0 +1,53 @@
+#
+# Unit tests for individual pieces of functionality.
+#
+
+#
+# The files are put here in order. Later tests need
+# functionality from earlier tests.
+#
+FILES := rfc.txt errors.txt extended.txt lucent.txt wimax.txt \
+ escape.txt condition.txt xlat.txt vendor.txt dhcp.txt ascend.txt \
+ rfc4849.txt eapol_key_msg.txt
+
+#
+# Create the output directory
+#
+.PHONY: $(BUILD_DIR)/tests/unit
+$(BUILD_DIR)/tests/unit:
+ @mkdir -p $@
+
+.PHONY: $(BUILD_DIR)/share
+$(BUILD_DIR)/share:
+ @mkdir -p $@
+
+#
+# We need $INCLUDE in the output file, so we pass 2 parameters to 'echo'
+# No idea how portable that is...
+#
+$(BUILD_DIR)/share/dictionary: $(top_srcdir)/share/dictionary $(top_srcdir)/share/dictionary.dhcp | $(BUILD_DIR)/share
+ @rm -f $@
+ @for x in $^; do \
+ echo '$$INCLUDE ' "$$x" >> $@; \
+ done
+
+#
+# Files in the output dir depend on the unit tests
+#
+$(BUILD_DIR)/tests/unit/%: $(DIR)/% $(BUILD_DIR)/bin/radattr $(TESTBINDIR)/radattr $(BUILD_DIR)/share/dictionary | $(BUILD_DIR)/tests/unit
+ @echo UNIT-TEST $(notdir $@)
+ @if ! $(TESTBIN)/radattr -D $(BUILD_DIR)/share $<; then \
+ echo "$(TESTBIN)/radattr -D $(BUILD_DIR)/share $<"; \
+ exit 1; \
+ fi
+ @touch $@
+
+#
+# Get all of the unit test output files
+#
+TESTS.UNIT_FILES := $(addprefix $(BUILD_DIR)/tests/unit/,$(FILES))
+
+#
+# Depend on the output files, and create the directory first.
+#
+tests.unit: $(TESTS.UNIT_FILES)
diff --git a/src/tests/unit/ascend.txt b/src/tests/unit/ascend.txt
new file mode 100644
index 0000000..4acb223
--- /dev/null
+++ b/src/tests/unit/ascend.txt
@@ -0,0 +1,5 @@
+#
+# Ascend data filters
+#
+encode Ascend-Data-Filter = "ip in drop tcp dstport > 1023"
+data 1a 28 00 00 02 11 f2 22 01 00 01 00 00 00 00 00 00 00 00 00 00 00 06 00 00 00 03 ff 00 03 00 00 00 00 00 00 00 00 00 00
diff --git a/src/tests/unit/condition.txt b/src/tests/unit/condition.txt
new file mode 100644
index 0000000..bbe24d2
--- /dev/null
+++ b/src/tests/unit/condition.txt
@@ -0,0 +1,679 @@
+#
+# Tests for parsing conditional expressions.
+#
+# $Id$
+#
+
+#
+# A bunch of errors, in the order that the error strings
+# appear in parser.c
+#
+
+
+# All IP address literals should be parsed as prefixes
+condition ("foo\
+data ERROR offset 6 End of string after escape
+
+condition ("foo
+data ERROR offset 2 Unterminated string
+
+condition ()
+data ERROR offset 1 Empty string is invalid
+
+condition (!)
+data ERROR offset 2 Empty string is invalid
+
+condition (foo == bar
+data ERROR offset 11 No closing brace at end of string
+
+condition (|| b)
+data ERROR offset 1 Empty string is invalid
+
+condition ((ok || handled) foo)
+data ERROR offset 17 Unexpected text after condition
+
+# escapes in names are illegal
+condition (ok\ foo || handled)
+data ERROR offset 3 Unexpected escape
+
+condition (ok FOO handled)
+data ERROR offset 4 Invalid text. Expected comparison operator
+
+condition (ok !x handled)
+data ERROR offset 4 Invalid operator
+
+condition (ok =x handled)
+data ERROR offset 4 Invalid operator
+
+condition (ok =~ handled)
+data ERROR offset 7 Expected regular expression
+
+condition (ok == /foo/)
+data ERROR offset 7 Unexpected regular expression
+
+condition (ok == handled"foo")
+data ERROR offset 14 Unexpected start of string
+
+# And now we have a bunch of VALID conditions we want to parse.
+
+# sillyness is OK
+condition ((((((ok))))))
+data ok
+
+#
+# Extra braces get squashed
+#
+condition (&User-Name == &User-Password)
+data &User-Name == &User-Password
+
+condition (!ok)
+data !ok
+
+condition !(ok)
+data !ok
+
+condition !!ok
+data ERROR offset 1 Double negation is invalid
+
+condition !(!ok)
+data ok
+
+#
+# These next two are identical after normalization
+#
+condition (&User-Name == &User-Password || &Filter-Id == &Reply-Message)
+data &User-Name == &User-Password || &Filter-Id == &Reply-Message
+
+condition ((&User-Name == &User-Password) || (&Filter-Id == &Reply-Message))
+data &User-Name == &User-Password || &Filter-Id == &Reply-Message
+
+condition (!(&User-Name == &User-Password) || (&Filter-Id == &Reply-Message))
+data !&User-Name == &User-Password || &Filter-Id == &Reply-Message
+
+# different from the previous ones.
+condition (!((&User-Name == &User-Password) || (&Filter-Id == &Reply-Message)))
+data !(&User-Name == &User-Password || &Filter-Id == &Reply-Message)
+
+condition (!(&User-Name == &User-Password) || (&Filter-Id == &Reply-Message))
+data !&User-Name == &User-Password || &Filter-Id == &Reply-Message
+
+condition ((a == b) || (c == d)))
+data ERROR offset 22 Unexpected closing brace
+
+condition (handled && (Response-Packet-Type == Access-Challenge))
+data handled && &Response-Packet-Type == Access-Challenge
+
+# This is OK, without the braces
+condition handled && &Response-Packet-Type == Access-Challenge
+data handled && &Response-Packet-Type == Access-Challenge
+
+# and this, though it's not a good idea.
+condition handled &&&Response-Packet-Type == Access-Challenge
+data handled && &Response-Packet-Type == Access-Challenge
+
+condition /foo/ =~ bar
+data ERROR offset 0 Conditional check cannot begin with a regular expression
+
+condition reply == request
+data ERROR offset 0 Cannot use list references in condition
+
+condition reply == "hello"
+data ERROR offset 0 Cannot use list references in condition
+
+condition "hello" == reply
+data ERROR offset 0 Cannot use list references in condition
+
+condition request:User-Name == reply:User-Name
+data &User-Name == &reply:User-Name
+
+#
+# Convert !~ to !(COND) for regex
+#
+condition foo =~ /bar/
+data foo =~ /bar/
+
+condition foo !~ /bar/
+data !foo =~ /bar/
+
+condition !foo !~ /bar/
+data foo =~ /bar/
+
+#
+# Convert != to !(COND) for normal checks
+#
+condition &User-Name == &User-Password
+data &User-Name == &User-Password
+
+condition &User-Name != &User-Password
+data !&User-Name == &User-Password
+
+condition !&User-Name != &User-Password
+data &User-Name == &User-Password
+
+condition <ipv6addr>foo
+data ERROR offset 0 Cannot do cast for existence check
+
+condition <ipaddr>Filter-Id == &Framed-IP-Address
+data <ipaddr>&Filter-Id == &Framed-IP-Address
+
+condition <ipaddr>Filter-Id == <ipaddr>&Framed-IP-Address
+data ERROR offset 21 Unnecessary cast
+
+condition <ipaddr>Filter-Id == <integer>&Framed-IP-Address
+data ERROR offset 21 Cannot cast to a different data type
+
+condition <ipaddr>Filter-Id == <blerg>&Framed-IP-Address
+data ERROR offset 22 Invalid data type in cast
+
+#
+# Normalize things
+#
+condition <ipaddr>Filter-Id == "127.0.0.1"
+data <ipaddr>&Filter-Id == '127.0.0.1'
+
+condition <ipaddr>127.0.0.1 < &Framed-IP-Address
+data &Framed-IP-Address > 127.0.0.1
+
+# =* and !* are only for attrs / lists
+condition "foo" !* bar
+data ERROR offset 6 Cannot use !* on a string
+
+condition "foo" =* bar
+data ERROR offset 6 Cannot use =* on a string
+
+# existence checks don't need the RHS
+condition User-Name =* bar
+data &User-Name
+
+condition User-Name !* bar
+data !&User-Name
+
+condition !User-Name =* bar
+data !&User-Name
+
+condition !User-Name !* bar
+data &User-Name
+
+# redundant casts get squashed
+condition <ipaddr>Framed-IP-Address == 127.0.0.1
+data &Framed-IP-Address == 127.0.0.1
+
+condition <cidr>Framed-IP-Address <= 192.168.0.0/16
+data <ipv4prefix>&Framed-IP-Address <= 192.168.0.0/16
+
+# All IP address literals should be parsed as prefixes
+condition Framed-IP-Address <= 192.168.0.0/16
+data <ipv4prefix>&Framed-IP-Address <= 192.168.0.0/16
+
+# string attributes must be string
+condition User-Name == "bob"
+data &User-Name == "bob"
+
+condition User-Name == `bob`
+data &User-Name == `bob`
+
+condition User-Name == 'bob'
+data &User-Name == 'bob'
+
+condition User-Name == bob
+data ERROR offset 13 Must have string as value for attribute
+
+# Integer (etc.) types must be "bare"
+condition Session-Timeout == 10
+data &Session-Timeout == 10
+
+condition Session-Timeout == '10'
+data ERROR offset 19 Value must be an unquoted string
+
+# Except for dates, which can be humanly readable!
+# This one is be an expansion, so it's left as-is.
+condition Event-Timestamp == "January 1, 2012 %{blah}"
+data &Event-Timestamp == "January 1, 2012 %{blah}"
+
+# This one is NOT an expansion, so it's parsed into normal form
+condition Event-Timestamp == 'January 1, 2012'
+#data &Event-Timestamp == 'Jan 1 2012 00:00:00 EST'
+
+# literals are parsed when the conditions are parsed
+condition <integer>X == 1
+data ERROR offset 9 Failed to parse field
+
+condition NAS-Port == X
+data ERROR offset 12 Failed to parse value for attribute
+
+#
+# The RHS is a static string, so this gets mashed to a literal,
+# and then statically evaluated.
+#
+condition <ipaddr>127.0.0.1 == "127.0.0.1"
+data true
+
+condition <ipaddr>127.0.0.1 == "%{sql: 127.0.0.1}"
+data <ipaddr>127.0.0.1 == "%{sql: 127.0.0.1}"
+
+condition <ether> 00:11:22:33:44:55 == "00:11:22:33:44:55"
+data true
+
+condition <ether> 00:11:22:33:44:55 == "ff:11:22:33:44:55"
+data false
+
+condition <ether> 00:11:22:33:44:55 == "%{sql:00:11:22:33:44:55}"
+data <ether>00:11:22:33:44:55 == "%{sql:00:11:22:33:44:55}"
+
+condition <ether> 00:XX:22:33:44:55 == 00:11:22:33:44:55
+data ERROR offset 8 Failed to parse field
+
+#
+# Tests for boolean data types.
+#
+condition true
+data true
+
+condition 1
+data true
+
+condition false
+data false
+
+condition 0
+data false
+
+condition true && (User-Name == "bob")
+data &User-Name == "bob"
+
+condition false && (User-Name == "bob")
+data false
+
+condition false || (User-Name == "bob")
+data &User-Name == "bob"
+
+condition true || (User-Name == "bob")
+data true
+
+#
+# Both sides static data with a cast: evaluate at parse time.
+#
+condition <integer>20 < 100
+data true
+
+#
+# Both sides literal: evaluate at parse time
+#
+condition ('foo' == 'bar')
+data false
+
+condition ('foo' < 'bar')
+data false
+
+condition ('foo' > 'bar')
+data true
+
+condition ('foo' == 'foo')
+data true
+
+#
+# Double-quotes strings without expansions are literals
+#
+condition ("foo" == "%{sql: foo}")
+data foo == "%{sql: foo}"
+
+condition ("foo bar" == "%{sql: foo}")
+data "foo bar" == "%{sql: foo}"
+
+condition ("foo" == "bar")
+data false
+
+condition ("foo" == 'bar')
+data false
+
+#
+# The RHS gets parsed as a VPT_TYPE_DATA, which is
+# a double-quoted string. Except that there's no '%'
+# in it, so it reverts back to a literal.
+#
+condition (&User-Name == "bob")
+data &User-Name == "bob"
+
+condition (&User-Name == "%{sql: blah}")
+data &User-Name == "%{sql: blah}"
+
+condition <ipaddr>127.0.0.1 == 2130706433
+data true
+
+# /32 suffix should be trimmed for this type
+condition <ipaddr>127.0.0.1/32 == 127.0.0.1
+data true
+
+condition <ipaddr>127.0.0.1/327 == 127.0.0.1
+data ERROR offset 8 Failed to parse field
+
+condition <ipaddr>127.0.0.1/32 == 127.0.0.1
+data true
+
+condition (/foo/)
+data ERROR offset 1 Conditional check cannot begin with a regular expression
+
+#
+# Tests for (FOO).
+#
+condition (1)
+data true
+
+condition (0)
+data false
+
+condition (true)
+data true
+
+condition (false)
+data false
+
+condition ('')
+data false
+
+condition ("")
+data false
+
+#
+# Integers are true, as are non-zero strings
+#
+condition (4)
+data true
+
+condition ('a')
+data true
+
+condition (a)
+data ERROR offset 1 Expected a module return code
+
+#
+# Module return codes are OK
+#
+condition (ok)
+data ok
+
+condition (handled)
+data handled
+
+condition (fail)
+data fail
+
+condition ("a")
+data true
+
+condition (`a`)
+data `a`
+
+condition (User-name)
+data &User-Name
+
+#
+# Forbidden data types in cast
+#
+condition (<vsa>"foo" == &User-Name)
+data ERROR offset 2 Forbidden data type in cast
+
+#
+# Must have attribute references on the LHS of a condition.
+#
+condition ("foo" == &User-Name)
+data ERROR offset 1 Cannot use attribute reference on right side of condition
+
+#
+# If the LHS is a cast to a type, and the RHS is an attribute
+# of the same type, then re-write it so that the attribute
+# is on the LHS of the condition.
+#
+condition <string>"foo" == &User-Name
+data &User-Name == "foo"
+
+condition <integer>"%{expr: 1 + 1}" < &NAS-Port
+data &NAS-Port > "%{expr: 1 + 1}"
+
+condition &Filter-Id == &Framed-IP-Address
+data ERROR offset 0 Attribute comparisons must be of the same data type
+
+condition <ipaddr>127.0.0.1 == &Filter-Id
+data ERROR offset 0 Attribute comparisons must be of the same data type
+
+condition &Tmp-Integer64-0 == &request:Foo-Stuff-Bar
+data &Tmp-Integer64-0 == &Foo-Stuff-Bar
+
+condition &Tmp-Integer64-0 == &reply:Foo-Stuff-Bar
+data &Tmp-Integer64-0 == &reply:Foo-Stuff-Bar
+
+#
+# Casting attributes of different size
+#
+condition <ipaddr>&Tmp-Integer64-0 == &Framed-IP-Address
+data ERROR offset 0 Cannot cast to attribute of incompatible size
+
+condition <ipaddr>&PMIP6-Home-IPv4-HoA == &Framed-IP-Address
+data ERROR offset 0 Cannot cast to attribute of incompatible size
+
+# but these are allowed
+condition <ether>&Tmp-Integer64-0 == "%{module: foo}"
+data <ether>&Tmp-Integer64-0 == "%{module: foo}"
+
+condition <ipaddr>&Filter-Id == &Framed-IP-Address
+data <ipaddr>&Filter-Id == &Framed-IP-Address
+
+condition <ipaddr>&Class == &Framed-IP-Address
+data <ipaddr>&Class == &Framed-IP-Address
+
+#
+# Tags of zero mean restrict to attributes with no tag
+#
+condition &Tunnel-Password:0 == "Hello"
+data &Tunnel-Password:0 == "Hello"
+
+condition &Tunnel-Password:1 == "Hello"
+data &Tunnel-Password:1 == "Hello"
+
+#
+# Single quoted strings are left as-is
+#
+condition &Tunnel-Password:1 == 'Hello'
+data &Tunnel-Password:1 == 'Hello'
+
+#
+# zero offset into arrays get parsed and ignored
+#
+condition &User-Name[0] == "bob"
+data &User-Name[0] == "bob"
+
+condition &User-Name[1] == "bob"
+data &User-Name[1] == "bob"
+
+condition &User-Name[n] == "bob"
+data &User-Name[n] == "bob"
+
+condition &Tunnel-Password:1[0] == "Hello"
+data &Tunnel-Password:1[0] == "Hello"
+
+condition &Tunnel-Password:1[3] == "Hello"
+data &Tunnel-Password:1[3] == "Hello"
+
+#
+# This is allowed for pass2-fixups. Foo-Bar MAY be an attribute.
+# If so allow it so that pass2 can fix it up. Until then,
+# it's an unknown attribute
+#
+condition &Foo-Bar
+data &Foo-Bar
+
+# Same types are optimized
+#
+# FIXME: the tests don't currently run the "pass2" checks.
+# This test should really be:
+#
+# data &Acct-Input-Octets > &Session-Timeout
+#
+condition &Acct-Input-Octets > "%{Session-Timeout}"
+data &Acct-Input-Octets > "%{Session-Timeout}"
+
+# Separate types aren't optimized
+condition &Acct-Input-Octets-64 > "%{Session-Timeout}"
+data &Acct-Input-Octets-64 > "%{Session-Timeout}"
+
+#
+# Parse OIDs into known attributes, where possible.
+#
+condition &Attr-26.24757.84.9.5.4 == 0x1a99
+data &WiMAX-PFDv2-Src-Port == 6809
+
+#
+# This OID is known, but the data is malformed.
+# This is disallowed. Fix the configuration so that
+# it works.
+#
+condition &Attr-26.24757.84.9.5.7 == 0x1a99
+data ERROR offset 27 Failed to parse value for attribute
+
+# This one is really unknown
+condition &Attr-26.24757.84.9.5.15 == 0x1a99
+data &Attr-26.24757.84.9.5.15 == 0x1a99
+
+#
+# Invalid array references.
+#
+condition &User-Name[a] == 'bob'
+data ERROR offset 11 Array index is not an integer
+
+condition &User-Name == &Filter-Id[a]
+data ERROR offset 25 Array index is not an integer
+
+#
+# This one is still wrong.
+#
+condition User-Name[a] == 'bob'
+data false
+
+#
+# Bounds checks...
+#
+condition &User-Name[1001] == 'bob'
+data ERROR offset 11 Invalid array reference '1001' (should be between 0-1000)
+
+condition &User-Name[-1] == 'bob'
+data ERROR offset 11 Invalid array reference '-1' (should be between 0-1000)
+
+#
+# Tags
+#
+condition &Tunnel-Private-Group-Id:10 == 'test'
+data &Tunnel-Private-Group-Id:10 == 'test'
+
+condition &User-Name:10 == 'test'
+data ERROR offset 10 Attribute 'User-Name' cannot have a tag
+
+#
+# Tags are always wrong for attributes which aren't tagged.
+#
+condition &User-Name:0 == 'test'
+data ERROR offset 10 Attribute 'User-Name' cannot have a tag
+
+#
+# Bounds checks...
+#
+condition &Tunnel-Private-Group-Id:32 == 'test'
+data ERROR offset 25 Invalid tag value '32' (should be between 0-31)
+
+condition &request:Tunnel-Private-Group-Id:-1 == 'test'
+data ERROR offset 33 Invalid tag value '-1' (should be between 0-31)
+
+#
+# Sometimes the attribute/condition parser needs to fallback to bare words
+#
+condition request:Foo == 'request:Foo'
+data true
+
+condition request:Foo+Bar == request:Foo+Bar
+data true
+
+condition &request:Foo+Bar == 'request:Foo+Bar'
+data ERROR offset 12 Unexpected text after unknown attr
+
+condition 'request:Foo+d' == &request:Foo+Bar
+data ERROR offset 31 Unexpected text after unknown attr
+
+# Attribute tags are not allowed for unknown attributes
+condition &request:FooBar:0 == &request:FooBar
+data ERROR offset 15 Unexpected text after unknown attr
+
+condition &not-a-list:User-Name == &not-a-list:User-Name
+data ERROR offset 1 Invalid list qualifier
+
+# . is a valid dictionary name attribute, so we can't error out in pass1
+condition &not-a-packet.User-Name == &not-a-packet.User-Name
+data &not-a-packet.User-Name == &not-a-packet.User-Name
+
+#
+# The LHS is a string with ASCII 5C 30 30 30 inside of it.
+#
+condition ('i have scary embedded things\000 inside me' == "i have scary embedded things\000 inside me")
+data false
+
+#
+# 'Unknown' attributes which are defined in the main dictionary
+# should be resolved to their real names.
+condition &Attr-1 == 'bar'
+data &User-Name == 'bar'
+
+condition &Vendor-11344-Attr-1 == 127.0.0.1
+data &FreeRADIUS-Proxied-To == 127.0.0.1
+
+condition &FreeRADIUS-Attr-1 == 127.0.0.1
+data &FreeRADIUS-Proxied-To == 127.0.0.1
+
+#
+# Escape the backslashes correctly
+# And print them correctly
+#
+condition &User-Name =~ /@|\\/
+data &User-Name =~ /@|\\/
+
+condition &User-Name == '\\'
+data &User-Name == '\\'
+
+condition &User-Name !~ /^foo\nbar$/
+data !&User-Name =~ /^foo\nbar$/
+
+condition &User-Name == "@|\\"
+data &User-Name == "@|\\"
+
+condition &User-Name != "foo\nbar"
+data !&User-Name == "foo\nbar"
+
+condition User-Name =~ /^([^\\]*)\\(.*)$/
+data &User-Name =~ /^([^\\]*)\\(.*)$/
+
+#
+# We require explicit casts
+#
+condition 192.168.0.0/16 > 192.168.1.2
+data false
+
+condition <ipv4prefix>192.168.0.0/16 > 192.168.1.2
+data true
+
+condition <ipv4prefix>&NAS-IP-Address == 192.168.0.0/24
+data <ipv4prefix>&NAS-IP-Address == 192.168.0.0/24
+
+condition <ipv4prefix>192.168.0.0/24 > &NAS-IP-Address
+data <ipv4prefix>192.168.0.0/24 > &NAS-IP-Address
+
+#
+# We add casts to the LHS if necessary
+#
+condition &NAS-IP-Address < &PMIP6-Home-IPv4-HoA
+data <ipv4prefix>&NAS-IP-Address < &PMIP6-Home-IPv4-HoA
+
+condition &NAS-IP-Address < 192.168/16
+data <ipv4prefix>&NAS-IP-Address < 192.168.0.0/16
+
+condition &NAS-IP-Address < "%{echo: 192.168/16}"
+data <ipv4prefix>&NAS-IP-Address < "%{echo: 192.168/16}"
+
+condition &NAS-IP-Address < `/bin/echo 192.168/16`
+data <ipv4prefix>&NAS-IP-Address < `/bin/echo 192.168/16`
diff --git a/src/tests/unit/dhcp.txt b/src/tests/unit/dhcp.txt
new file mode 100644
index 0000000..db7eae2
--- /dev/null
+++ b/src/tests/unit/dhcp.txt
@@ -0,0 +1,44 @@
+#
+# Test vectors for DHCP attributes
+#
+
+encode-dhcp DHCP-Subnet-Mask = 255.255.0.0
+data 01 04 ff ff 00 00
+
+decode-dhcp -
+data DHCP-Subnet-Mask = 255.255.0.0
+
+#
+# A long one... with a weird DHCP-specific vendor ID.
+#
+decode-dhcp 3501013d0701001ceaadac1e37070103060f2c2e2f3c094d5346545f495054565232011c4c41424f4c54322065746820312f312f30312f30312f31302f312f3209120000197f0d050b4c4142373336304f4c5432
+data DHCP-Message-Type = DHCP-Discover, DHCP-Client-Identifier = 0x01001ceaadac1e, DHCP-Parameter-Request-List = DHCP-Subnet-Mask, DHCP-Parameter-Request-List = DHCP-Router-Address, DHCP-Parameter-Request-List = DHCP-Domain-Name-Server, DHCP-Parameter-Request-List = DHCP-Domain-Name, DHCP-Parameter-Request-List = DHCP-NETBIOS-Name-Servers, DHCP-Parameter-Request-List = DHCP-NETBIOS-Node-Type, DHCP-Parameter-Request-List = DHCP-NETBIOS, DHCP-Vendor-Class-Identifier = 0x4d5346545f49505456, DHCP-Relay-Circuit-Id = 0x4c41424f4c54322065746820312f312f30312f30312f31302f312f32, DHCP-Vendor-Specific-Information = 0x0000197f0d050b4c4142373336304f4c5432
+
+
+encode-dhcp DHCP-Agent-Circuit-Id = 0xabcdef, DHCP-Relay-Remote-Id = 0x010203040506
+data 52 0d 01 03 ab cd ef 02 06 01 02 03 04 05 06
+
+decode-dhcp -
+data DHCP-Relay-Circuit-Id = 0xabcdef, DHCP-Relay-Remote-Id = 0x010203040506
+
+# 35 01 01
+# 3d 06 00 a0 bd 11 22 33
+# 37 05 2b 29 01 03 06
+# 3c 09 76 69 61 73 61 74 31 2e 30
+# 52 31
+# 06 1f 30 30 41 30 42 44 31 31 32 32 33 33 40 73 62 32 2e 72 65 73 2e 76 69 61 73 61 74 2e 63 6f 6d
+# 02 06 00 a0 bc 6c 7d 3a
+# 01 06 00 a0 bc 33 22 11
+# 39 02 05 dc
+# 7d 1e
+# 00 00 0d e9 19
+# 01 06 30 30 30 31 39 46
+# 02 0a 52 4e 56 35 30 34 35 39 34 34
+# 03 03 41 54 41
+# ff 00
+
+decode-dhcp 7d 1e 00 00 0d e9 19 01 06 30 30 30 31 39 46 02 0a 52 4e 56 35 30 34 35 39 34 34 03 03 41 54 41
+data ADSL-Forum-Device-Manufacturer-OUI = 0x303030313946, ADSL-Forum-Device-Serial-Number = "RNV5045944", ADSL-Forum-Device-Product-Class = "ATA"
+
+encode-dhcp -
+data 7d 1e 00 00 0d e9 19 01 06 30 30 30 31 39 46 02 0a 52 4e 56 35 30 34 35 39 34 34 03 03 41 54 41
diff --git a/src/tests/unit/eapol_key_msg.txt b/src/tests/unit/eapol_key_msg.txt
new file mode 100644
index 0000000..c6f83ab
--- /dev/null
+++ b/src/tests/unit/eapol_key_msg.txt
@@ -0,0 +1,14 @@
+#
+# For sending EAPoL key messages in RADIUS.
+#
+encode FreeRADIUS-802.1X-Anonce = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+data f5 29 1a 00 00 00 2c 50 01 aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa
+
+decode -
+data FreeRADIUS-802.1X-Anonce = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+
+encode FreeRADIUS-802.1X-EAPoL-Key-Msg = 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+data f5 ff 1a 80 00 00 2c 50 02 bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa f5 08 1a 00 aa aa aa aa
+
+decode -
+data FreeRADIUS-802.1X-EAPoL-Key-Msg = 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
diff --git a/src/tests/unit/errors.txt b/src/tests/unit/errors.txt
new file mode 100644
index 0000000..801b501
--- /dev/null
+++ b/src/tests/unit/errors.txt
@@ -0,0 +1,17 @@
+#
+# Bad attributes
+#
+decode 01 04 00
+data rad_attr2vp: Insufficient data
+
+decode 01 01 00
+data rad_attr2vp: Insufficient data
+
+encode Attr-26.1.256 = 0x00000001
+data Number '256' out of allowed range in attribute identifier
+
+encode Attr-240.1 = 0x01
+data Standard attributes cannot use OIDs
+
+encode Attr-26.1.1 = 0x01
+data 1a 09 00 00 00 01 01 03 01
diff --git a/src/tests/unit/escape.txt b/src/tests/unit/escape.txt
new file mode 100644
index 0000000..b65e1b7
--- /dev/null
+++ b/src/tests/unit/escape.txt
@@ -0,0 +1,74 @@
+#
+# Like the conditional tests, but tests for escape sequences
+#
+condition "bob" == 0x626f62
+data true
+
+condition 0x == '0x'
+data ERROR offset 2 Empty octet string is invalid
+
+condition 'foo' == 0x
+data ERROR offset 9 Empty octet string is invalid
+
+# \n gets escaped in double quoted strings
+condition "\n" == 0x0a
+data true
+
+# but not in single quoted strings
+condition '\n' == 0x5c6e
+data true
+
+condition '\'' == 0x27
+data true
+
+condition "'" == 0x27
+data true
+
+condition "\"" == 0x22
+data true
+
+condition 0x22 == '"'
+data true
+
+condition '\'' == "'"
+data true
+
+condition '\\' == "\\"
+data true
+
+#
+# The first string is \ + x
+#
+condition '\x' == "x"
+data false
+
+# embedded zeros are OK
+condition "a\000a" == 0x610061
+data true
+
+condition "aa\000" == 0x616100
+data true
+
+condition 'aa\000' == 0x61615c303030
+data true
+
+condition 'aa\000' == "aa\000"
+data false
+
+condition 'a\n' == "a\n"
+data false
+
+condition 0x626f62 == 'bob'
+data true
+
+condition 0x626f62 == "bob"
+data true
+
+condition 0x626f62 == bob
+data true
+
+condition \n == 0x0a
+data ERROR offset 1 Unexpected escape
+
+condition a\n == 0x610a
+data ERROR offset 2 Unexpected escape
diff --git a/src/tests/unit/extended.txt b/src/tests/unit/extended.txt
new file mode 100644
index 0000000..9810b19
--- /dev/null
+++ b/src/tests/unit/extended.txt
@@ -0,0 +1,103 @@
+# Example attributes as used in the "extended attributes" draft.
+raw 241.1 "bob"
+data f1 06 01 62 6f 62
+
+raw 241.2 {1 23 45 }
+data f1 07 02 01 04 23 45
+
+raw 241.2 {1 23 45 } { 2 67 89 }
+data f1 0b 02 01 04 23 45 02 04 67 89
+
+raw 241.2 {1 23 45 } { 3 { 1 ab cd } }
+data f1 0d 02 01 04 23 45 03 06 01 04 ab cd
+
+raw 241.2 {1 23 45 } { 3 { 1 ab cd } {2 "foo" } }
+data f1 12 02 01 04 23 45 03 0b 01 04 ab cd 02 05 66 6f 6f
+
+raw 241.1 {1 { 2 { 3 { 4 { 5 cd ef } } } } }
+data f1 0f 01 01 0c 02 0a 03 08 04 06 05 04 cd ef
+
+raw 241.26.1.4 "test"
+data f1 0c 1a 00 00 00 01 04 74 65 73 74
+
+raw 241.26.1.5 { 3 "test" }
+data f1 0e 1a 00 00 00 01 05 03 06 74 65 73 74
+
+# More examples.
+raw 245.1 "bob"
+data f5 07 01 00 62 6f 62
+
+raw 245.2 {1 23 45 }
+data f5 08 02 00 01 04 23 45
+
+raw 245.2 {1 23 45 } { 2 67 89 }
+data f5 0c 02 00 01 04 23 45 02 04 67 89
+
+raw 245.2 {1 23 45 } { 3 { 1 ab cd } }
+data f5 0e 02 00 01 04 23 45 03 06 01 04 ab cd
+
+raw 245.2 {1 23 45 } { 3 { 1 ab cd } {2 "foo" } }
+data f5 13 02 00 01 04 23 45 03 0b 01 04 ab cd 02 05 66 6f 6f
+
+raw 245.1 {1 { 2 { 3 { 4 { 5 cd ef } } } } }
+data f5 10 01 00 01 0c 02 0a 03 08 04 06 05 04 cd ef
+
+raw 245.26.1.4 "test"
+data f5 0d 1a 00 00 00 00 01 04 74 65 73 74
+
+raw 245.26.1.5 { 3 "test" }
+data f5 0f 1a 00 00 00 00 01 05 03 06 74 65 73 74
+
+raw 245.4 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccc
+data f5 ff 04 80 aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa ab bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb f5 13 04 00 cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc
+
+#
+# 256 copies of 'x'
+#
+raw 245.1 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+data f5 ff 01 80 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 f5 09 01 00 78 78 78 78 78
+
+#
+# And it decodes to an attribute with 256 x's
+#
+decode f5 ff 01 80 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 f5 09 01 00 78 78 78 78 79
+data Attr-245.1 = 0x78787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787879
+
+#
+# A VSA which has lots of data
+#
+raw 245.26.1.6 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccc13456789
+data f5 ff 1a 80 00 00 00 01 06 aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa ab bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb f5 17 1a 00 bb bb bb bb bb cc cc cc cc cc cc cc cc cc cc 13 45 67 89
+
+decode f5 ff 1a 80 00 00 00 01 06 aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa ab bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb f5 17 1a 00 bb bb bb bb bb cc cc cc cc cc cc cc cc cc cc 13 45 67 89
+data Attr-245.26.1.6 = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccc13456789
+
+# Same as above, but the first attribute doesn't have
+# the "continuation" bit set.
+decode f5 ff 1a 00 00 00 00 01 06 aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa ab bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb f5 17 1a 00 bb bb bb bb bb cc cc cc cc cc cc cc cc cc cc 13 45 67 89
+data Attr-245.26.1.6 = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, Attr-245.26 = 0xbbbbbbbbbbcccccccccccccccccccc13456789
+
+
+# again, but the second one attr is not an extended attr
+decode f5 ff 1a 80 00 00 00 01 06 aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa ab bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb 01 05 62 6f 62
+data Attr-245 = 0x1a800000000106aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, User-Name = "bob"
+
+# No data means that the attribute is an "invalid attribute"
+decode f5 04 01 00
+data Attr-245 = 0x0100
+
+# No "flags" field means it's an invalid attribute.
+decode f5 03 01
+data Attr-245 = 0x01
+
+decode f5 09 1a 00 00 00 00 01 06
+data Attr-245.26 = 0x0000000106
+
+decode f5 0a 1a 00 00 00 00 01 06 01
+data Attr-245.26.1.6 = 0x01
+
+decode f5 09 1a 80 00 00 00 01 06 f5 05 1a 80 01
+data Attr-245.26.1.6 = 0x01
+
+decode f5 0a 1a 80 00 00 00 01 06 01 f5 05 1a 80 01
+data Attr-245.26.1.6 = 0x0101
diff --git a/src/tests/unit/lucent.txt b/src/tests/unit/lucent.txt
new file mode 100644
index 0000000..e21d03c
--- /dev/null
+++ b/src/tests/unit/lucent.txt
@@ -0,0 +1,11 @@
+encode Lucent-Max-Shared-Users = 1
+data 1a 0d 00 00 12 ee 00 02 07 00 00 00 01
+
+decode -
+data Lucent-Max-Shared-Users = 1
+
+decode 1a 0d 00 00 12 ee ff 02 07 00 00 00 01
+data Attr-26.4846.65282 = 0x00000001
+
+encode -
+data 1a 0d 00 00 12 ee ff 02 07 00 00 00 01
diff --git a/src/tests/unit/rfc.txt b/src/tests/unit/rfc.txt
new file mode 100644
index 0000000..8e95526
--- /dev/null
+++ b/src/tests/unit/rfc.txt
@@ -0,0 +1,204 @@
+# All attribute lengths are implicit, and are calculated automatically
+#
+# Input is of the form:
+#
+# WORD ...
+#
+# The WORD is a keyword which indicates the format of the following text.
+# WORD is one of:
+#
+# raw - read the grammar defined below, and encode an attribute.
+# The grammar supports a trivial way of describing RADIUS
+# attributes, without reference to dictionaries or fancy
+# parsers
+#
+# encode - reads "Attribute-Name = value", encodes it, and prints
+# the result as text.
+# use "-" to encode the output of the last command
+#
+# decode - reads hex, and decodes it "Attribute-Name = value"
+# use "-" to decode the output of the last command
+#
+# data - the expected output of the previous command, in ASCII form.
+# if the actual command output is different, an error message
+# is produced, and the program terminates.
+#
+#
+# The "raw" input satisfies the following grammar:
+#
+# Identifier = 1*DIGIT *( "." 1*DIGIT )
+#
+# HEXCHAR = HEXDIG HEXDIG
+#
+# STRING = DQUOTE *CHAR DQUOTE
+#
+# TLV = "{" 1*DIGIT DATA "}"
+#
+# DATA = 1*HEXCHAR / 1*TLV / STRING
+#
+# LINE = Identifier DATA
+#
+# The "Identifier" is a RADIUS attribute identifier, as given in the draft.
+#
+# e.g. 1 for User-Name
+# 26.9.1 Vendor-Specific, Cisco, Cisco-AVPAir
+# 241.1 Extended Attribute, number 1
+# 241.2.3 Extended Attribute 2, data type TLV, TLV type 3
+# etc.
+#
+# The "DATA" portion is the contents of the RADIUS Attribute.
+#
+# 123456789abcdef hex string
+# 12 34 56 ab with spaces for clarity
+# "hello" Text string
+# { 1 abcdef } TLV, TLV-Type 1, data "abcdef"
+#
+# TLVs can be nested:
+#
+# { tlv-type { tlv-type data } } { 3 { 4 01020304 } }
+#
+# TLVs can be concatencated
+#
+# {tlv-type data } { tlv-type data} { 3 040506 } { 8 aabbcc }
+#
+# The "raw" data is encoded without reference to dictionaries. Any
+# valid string is parsed to a RADIUS attribute. The resulting RADIUS
+# attribute *may not* be correctly formatted to the relevant RADIUS
+# specifications. i.e. you can use this tool to create attribute 1
+# (User-Name), which is encoded as a series of TLVs. That's up to you.
+#
+# The purpose of the "raw" command is to have a simple way of encoding
+# attributes which is independent of any dictionaries or packet processing
+# routines.
+#
+# The output data is the hex version of the encoded attribute.
+#
+
+encode User-Name = "bob"
+data 01 05 62 6f 62
+
+decode -
+data User-Name = "bob"
+
+decode 01 05 62 6f 62
+data User-Name = "bob"
+
+#
+# The Type/Length is OK, but the attribute data is of the wrong size.
+#
+decode 04 04 ab cd
+data Attr-4 = 0xabcd
+
+# Zero-length attributes
+decode 01 02
+data
+
+# don't encode zero-length attributes
+encode User-Name = ""
+data
+
+# except for CUI. Thank you, WiMAX!
+decode 59 02
+data Chargeable-User-Identity = 0x
+
+# Hah! Thought you had it figured out, didn't you?
+encode -
+data 59 02
+
+attribute Framed-IP-Address = 127.0.0.1/32
+data Framed-IP-Address = 127.0.0.1
+
+attribute Framed-IP-Address = 127.0.0.1/323
+data Invalid IPv4 mask length "/323". Should be between 0-32
+
+attribute Framed-IP-Address = 127.0.0.1/30
+data Invalid IPv4 mask length "/30". Only "/32" permitted for non-prefix types
+
+attribute Framed-IP-Address = *
+data Framed-IP-Address = 0.0.0.0
+
+attribute Framed-IP-Address = 127
+data Framed-IP-Address = 0.0.0.127
+
+attribute Framed-IP-Address = 127.0
+data Framed-IP-Address = 127.0.0.0
+
+attribute Framed-IPv6-Prefix = ::1
+data Framed-IPv6-Prefix = ::1/128
+
+attribute Framed-IPv6-Prefix = ::1/200
+data Invalid IPv6 mask length "/200". Should be between 0-128
+
+attribute Framed-IPv6-Prefix = ::1/200
+data Invalid IPv6 mask length "/200". Should be between 0-128
+
+attribute Framed-IPv6-Prefix = 11:22:33:44:55:66:77:88/128
+data Framed-IPv6-Prefix = 11:22:33:44:55:66:77:88/128
+
+attribute Framed-IPv6-Prefix = *
+data Framed-IPv6-Prefix = ::/128
+
+attribute PMIP6-Home-IPv4-HoA = 127/8
+data PMIP6-Home-IPv4-HoA = 127.0.0.0/8
+
+attribute PMIP6-Home-IPv4-HoA = 127/8
+data PMIP6-Home-IPv4-HoA = 127.0.0.0/8
+
+#
+# Octets outside of the mask are OK, but
+# are mashed to zero.
+#
+attribute PMIP6-Home-IPv4-HoA = 127.63/8
+data PMIP6-Home-IPv4-HoA = 127.0.0.0/8
+
+#
+# Unless you give a good mask.
+#
+attribute PMIP6-Home-IPv4-HoA = 127.63/16
+data PMIP6-Home-IPv4-HoA = 127.63.0.0/16
+
+attribute PMIP6-Home-IPv4-HoA = 127.999/16
+data Failed to parse IPv4 address string "127.999/16"
+
+attribute PMIP6-Home-IPv4-HoA = 127.bob/16
+data Failed to parse IPv4 address string "127.bob/16"
+
+attribute PMIP6-Home-IPv4-HoA = 127.63/15
+data PMIP6-Home-IPv4-HoA = 127.62.0.0/15
+
+attribute PMIP6-Home-IPv4-HoA = 127.63.1/24
+data PMIP6-Home-IPv4-HoA = 127.63.1.0/24
+
+attribute PMIP6-Home-IPv4-HoA = 127.63.1.6
+data PMIP6-Home-IPv4-HoA = 127.63.1.6/32
+
+attribute PMIP6-Home-IPv4-HoA = 256/8
+data Failed to parse IPv4 address string "256/8"
+
+attribute PMIP6-Home-IPv4-HoA = bob/8
+data Failed to parse IPv4 address string "bob/8"
+
+#
+# A "concat" attribute, with no data
+#
+decode 89 02
+data PKM-SS-Cert = 0x
+
+#
+# The configuration can use the old names, but they
+# get automatically converted to the new names.
+#
+attribute User-Service-Type = 1
+data Service-Type = Login-User
+
+#
+# Or with weirdly formatted data
+#
+decode 89 03 ff 89 02 89 03 fe
+data PKM-SS-Cert = 0xfffe
+
+$INCLUDE tunnel.txt
+$INCLUDE errors.txt
+$INCLUDE extended.txt
+$INCLUDE lucent.txt
+$INCLUDE wimax.txt
diff --git a/src/tests/unit/rfc4849.txt b/src/tests/unit/rfc4849.txt
new file mode 100644
index 0000000..957a9e9
--- /dev/null
+++ b/src/tests/unit/rfc4849.txt
@@ -0,0 +1,49 @@
+#
+# RFC 4849 NAS-Filter-Rule
+#
+# Individual rules are packed together as with EAP-Message,
+# but the rules are separated by a 0x00 byte.
+#
+encode NAS-Filter-Rule = "hello"
+data 5c 07 68 65 6c 6c 6f
+
+decode -
+data NAS-Filter-Rule = "hello"
+
+
+encode NAS-Filter-Rule = "hello", NAS-Filter-Rule += "bob"
+data 5c 0b 68 65 6c 6c 6f 00 62 6f 62
+
+decode -
+data NAS-Filter-Rule = "hello", NAS-Filter-Rule = "bob"
+
+encode NAS-Filter-Rule = "hello", NAS-Filter-Rule += "bob", NAS-Filter-Rule += "stuff"
+data 5c 11 68 65 6c 6c 6f 00 62 6f 62 00 73 74 75 66 66
+
+decode -
+data NAS-Filter-Rule = "hello", NAS-Filter-Rule = "bob", NAS-Filter-Rule = "stuff"
+
+#
+# And large amounts of data
+#
+
+# 250x and then space (to tell where the 'x' ends
+encode NAS-Filter-Rule = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ", NAS-Filter-Rule += "bob"
+data 5c ff 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 20 00 62 5c 04 6f 62
+
+decode -
+data NAS-Filter-Rule = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ", NAS-Filter-Rule = "bob"
+
+# 251x + ' '
+encode NAS-Filter-Rule = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ", NAS-Filter-Rule += "bob"
+data 5c ff 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 20 00 5c 05 62 6f 62
+
+decode -
+data NAS-Filter-Rule = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ", NAS-Filter-Rule = "bob"
+
+# 252x + ' '
+encode NAS-Filter-Rule = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ", NAS-Filter-Rule += "bob"
+data 5c ff 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 20 5c 06 00 62 6f 62
+
+decode -
+data NAS-Filter-Rule = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ", NAS-Filter-Rule = "bob"
diff --git a/src/tests/unit/tunnel.txt b/src/tests/unit/tunnel.txt
new file mode 100644
index 0000000..da0cb31
--- /dev/null
+++ b/src/tests/unit/tunnel.txt
@@ -0,0 +1,87 @@
+#
+# We can't look at the data here, because the encoded Tunnel-Password has a 2 byte
+# random salt
+#
+encode Tunnel-Password:0 = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxabc"
+decode -
+data Tunnel-Password:0 = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxabc"
+
+encode Tunnel-Password:0 = "0"
+decode -
+data Tunnel-Password:0 = "0"
+
+encode Tunnel-Password:0 = "01"
+decode -
+data Tunnel-Password:0 = "01"
+
+encode Tunnel-Password:0 = "012"
+decode -
+data Tunnel-Password:0 = "012"
+
+encode Tunnel-Password:0 = "0123"
+decode -
+data Tunnel-Password:0 = "0123"
+
+encode Tunnel-Password:0 = "01234"
+decode -
+data Tunnel-Password:0 = "01234"
+
+encode Tunnel-Password:0 = "012345"
+decode -
+data Tunnel-Password:0 = "012345"
+
+encode Tunnel-Password:0 = "0123456"
+decode -
+data Tunnel-Password:0 = "0123456"
+
+encode Tunnel-Password:0 = "01234567"
+decode -
+data Tunnel-Password:0 = "01234567"
+
+encode Tunnel-Password:0 = "012345678"
+decode -
+data Tunnel-Password:0 = "012345678"
+
+encode Tunnel-Password:0 = "0123456789"
+decode -
+data Tunnel-Password:0 = "0123456789"
+
+encode Tunnel-Password:0 = "0123456789a"
+decode -
+data Tunnel-Password:0 = "0123456789a"
+
+encode Tunnel-Password:0 = "0123456789ab"
+decode -
+data Tunnel-Password:0 = "0123456789ab"
+
+encode Tunnel-Password:0 = "0123456789abc"
+decode -
+data Tunnel-Password:0 = "0123456789abc"
+
+encode Tunnel-Password:0 = "0123456789abcd"
+decode -
+data Tunnel-Password:0 = "0123456789abcd"
+
+encode Tunnel-Password:0 = "0123456789abcde"
+decode -
+data Tunnel-Password:0 = "0123456789abcde"
+
+encode Tunnel-Password:0 = "0123456789abcdef"
+decode -
+data Tunnel-Password:0 = "0123456789abcdef"
+
+#
+# We can't look at the data here, because the encoded Tunnel-Password has a 2 byte
+# random salt
+#
+encode Tunnel-Password:0 := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+decode -
+data Tunnel-Password:0 = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+
+#
+# 1 octet for the tag. 2 octets for salt. One octet for encrypted length.
+# 249 octets left for real data.
+#
+encode Tunnel-Password:0 = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx123456789ab"
+decode -
+data Tunnel-Password:0 = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx123456789"
diff --git a/src/tests/unit/vendor.txt b/src/tests/unit/vendor.txt
new file mode 100644
index 0000000..1325f49
--- /dev/null
+++ b/src/tests/unit/vendor.txt
@@ -0,0 +1,48 @@
+encode SN-VPN-Name = "foo"
+data 1a 0d 00 00 1f e4 00 02 00 07 66 6f 6f
+
+decode 1a 0d 00 00 1f e4 00 02 00 07 66 6f 6f
+data SN-VPN-Name = "foo"
+
+encode USR-Event-Id = 1234
+data 1a 0e 00 00 01 ad 00 00 bf be 00 00 04 d2
+
+decode 1a 0e 00 00 01 ad 00 00 bf be 00 00 04 d2
+data USR-Event-Id = 1234
+
+decode 1a 15 00 00 4e 20 01 0f 6c 69 74 68 69 61 73 70 72 69 6e 67 73
+data Attr-26.20000.1 = 0x6c6974686961737072696e6773
+
+decode 1a 2e 00 00 00 2b 1c 02 01 06 00 00 00 00 3c 20 31 35 35 2e 34 2e 31 32 2e 31 30 30 20 30 30 3a 30 30 3a 30 30 3a 30 30 3a 30 30 3a 30 30
+data 3Com-User-Access-Level = 3Com-Visitor, 3Com-Ip-Host-Addr = "155.4.12.100 00:00:00:00:00:00"
+
+decode 1a 2c 00 00 00 2b 01 06 00 00 00 00 3c 20 31 35 35 2e 34 2e 31 32 2e 31 30 30 20 30 30 3a 30 30 3a 30 30 3a 30 30 3a 30 30 3a 30 30
+data 3Com-User-Access-Level = 3Com-Visitor, 3Com-Ip-Host-Addr = "155.4.12.100 00:00:00:00:00:00"
+
+encode Vendor-Specific = 0xabcdef
+data Must use 'Attr-26 = ...' instead of 'Vendor-Specific = ...'
+
+encode Attr-26 = 0x00000009abcdef
+data 1a 09 00 00 00 09 ab cd ef
+
+attribute Attr-26 = 0x00000009abcdef
+data Attr-26 = 0x00000009abcdef
+attribute Ascend-Data-Filter = 0x01010100010203040a0b0c0d05200600000504d2020200000000000000000000
+data Ascend-Data-Filter = "ip in forward srcip 1.2.3.4/5 dstip 10.11.12.13/32 tcp srcport = 5 dstport = 1234"
+
+encode -
+data 1a 28 00 00 02 11 f2 22 01 01 01 00 01 02 03 04 0a 0b 0c 0d 05 20 06 00 00 05 04 d2 02 02 00 00 00 00 00 00 00 00 00 00
+
+decode 1a2800000211f22201010100010203040a0b0c0d05200600000504d2020200000000000000000000
+data Ascend-Data-Filter = "ip in forward srcip 1.2.3.4/5 dstip 10.11.12.13/32 tcp srcport = 5 dstport = 1234"
+
+# this untagged tunnel encrypted VSA is valid in both access accepts and CoA requests
+encode ERX-LI-Action = off
+decode -
+data ERX-LI-Action = off
+
+packet coa_request
+original null
+encode ERX-LI-Action = off
+decode -
+data ERX-LI-Action = off
diff --git a/src/tests/unit/wimax.txt b/src/tests/unit/wimax.txt
new file mode 100644
index 0000000..0917097
--- /dev/null
+++ b/src/tests/unit/wimax.txt
@@ -0,0 +1,171 @@
+#
+# Test vectors for WiMAX attributes.
+#
+encode WiMAX-Release = "1.0"
+data 1a 0e 00 00 60 b5 01 08 00 01 05 31 2e 30
+
+decode -
+data WiMAX-Release = "1.0"
+
+decode 1a 08 00 00 60 b5 01 02
+data Attr-26 = 0x000060b50102
+
+decode 1a 0a 00 00 60 b5 01 04 00 01
+data Attr-26.24757.1 = 0x01
+
+encode WiMAX-Accounting-Capabilities = 1
+data 1a 0c 00 00 60 b5 01 06 00 02 03 01
+
+decode -
+data WiMAX-Accounting-Capabilities = IP-Session-Based
+
+encode WiMAX-Release = "1.0", WiMAX-Accounting-Capabilities = 1
+data 1a 11 00 00 60 b5 01 0b 00 01 05 31 2e 30 02 03 01
+
+decode -
+data WiMAX-Release = "1.0", WiMAX-Accounting-Capabilities = IP-Session-Based
+
+encode -
+data 1a 11 00 00 60 b5 01 0b 00 01 05 31 2e 30 02 03 01
+
+encode WiMAX-PFDv2-Classifier-Direction = 1
+data 1a 0e 00 00 60 b5 54 08 00 09 05 04 03 01
+
+encode WiMAX-PFDv2-Classifier-Direction = 1, WiMAX-PFDv2-Src-Port = 6809
+data 1a 14 00 00 60 b5 54 0e 00 09 0b 04 03 01 05 06 04 04 1a 99
+
+decode -
+data WiMAX-PFDv2-Classifier-Direction = 1, WiMAX-PFDv2-Src-Port = 6809
+
+decode 1a 11 00 00 60 b5 54 0b 00 09 08 05 06 04 04 1a 99
+data WiMAX-PFDv2-Src-Port = 6809
+
+# 26.24757.89.9.4 has the correct length.
+# 26.24757.89.9.5 has the correct length.
+# 26.24757.89.9.5.4 has the wrong length.
+decode 1a 14 00 00 60 b5 54 0e 00 09 0b 04 03 01 05 06 04 05 1a 99
+data WiMAX-PFDv2-Classifier-Direction = 1, Attr-26.24757.84.9.5 = 0x04051a99
+
+# The 26.24757.1 has the wrong length
+decode 1a 11 00 00 60 b5 01 0a 00 01 05 31 2e 30 02 03 01
+data Attr-26 = 0x000060b5010a000105312e30020301
+
+encode -
+data 1a 11 00 00 60 b5 01 0a 00 01 05 31 2e 30 02 03 01
+
+decode 1a 11 00 00 60 b5 01 0c 00 01 05 31 2e 30 02 03 01
+data Attr-26 = 0x000060b5010c000105312e30020301
+
+encode -
+data 1a 11 00 00 60 b5 01 0c 00 01 05 31 2e 30 02 03 01
+
+# 26.24757.1.1 has the wrong length
+decode 1a 11 00 00 60 b5 01 0b 00 01 04 31 2e 30 02 03 01
+data Attr-26.24757.1 = 0x0104312e30020301
+
+decode 1a 11 00 00 60 b5 01 0b 00 01 06 31 2e 30 02 03 01
+data Attr-26.24757.1 = 0x0106312e30020301
+
+encode -
+data 1a 11 00 00 60 b5 01 0b 00 01 06 31 2e 30 02 03 01
+
+
+# 26.24757.1.2 has the wrong length
+decode 1a 11 00 00 60 b5 01 0b 00 01 05 31 2e 30 02 02 01
+data Attr-26.24757.1 = 0x0105312e30020201
+
+encode -
+data 1a 11 00 00 60 b5 01 0b 00 01 05 31 2e 30 02 02 01
+
+# 26.24757.1.1 has the correct length
+# 26.24757.1.2 has the wrong length
+# This means that 26.24757.1 is invalid, and we create a raw attribute.
+decode 1a 11 00 00 60 b5 01 0b 00 01 05 31 2e 30 02 04 01
+data Attr-26.24757.1 = 0x0105312e30020401
+
+encode -
+data 1a 11 00 00 60 b5 01 0b 00 01 05 31 2e 30 02 04 01
+
+encode WiMAX-PFDv2-Eth-Priority-Range-Low = 55
+data 1a 12 00 00 60 b5 54 0c 00 09 09 09 07 03 05 01 03 37
+
+encode WiMAX-PFDv2-Eth-Priority-Range-Low = 55, WiMAX-PFDv2-Eth-Priority-Range-High = 84
+data 1a 15 00 00 60 b5 54 0f 00 09 0c 09 0a 03 08 01 03 37 02 03 54
+
+decode -
+data WiMAX-PFDv2-Eth-Priority-Range-Low = 55, WiMAX-PFDv2-Eth-Priority-Range-High = 84
+
+# A less efficient encoding of the above data
+decode 1a 17 00 00 60 b5 54 11 00 09 0e 09 0c 03 05 01 03 37 03 05 02 03 54
+data WiMAX-PFDv2-Eth-Priority-Range-Low = 55, WiMAX-PFDv2-Eth-Priority-Range-High = 84
+
+# 26.24757.84.9.9.3.1 has the wrong length
+decode 1a 15 00 00 60 b5 54 0f 00 09 0c 09 0a 03 08 01 04 37 02 03 54
+data Attr-26.24757.84.9.9.3 = 0x010437020354
+
+# 26.24757.84.9.9.3.2 has the wrong length
+decode 1a 15 00 00 60 b5 54 0f 00 09 0c 09 0a 03 08 01 03 37 02 04 54
+data Attr-26.24757.84.9.9.3 = 0x010337020454
+
+# 26.24757.84.9.9.3.2 has the wrong length
+# This means that the SECOND 26.24757.84.9.9.3 is invalid.
+decode 1a 17 00 00 60 b5 54 11 00 09 0e 09 0c 03 05 01 03 37 03 05 02 04 54
+data WiMAX-PFDv2-Eth-Priority-Range-Low = 55, Attr-26.24757.84.9.9.3 = 0x020454
+
+# 26.24757.84.9.9.3.1 has the wrong length
+# This means that 26.24757.84.9.9.3 is invalid.
+decode 1a 17 00 00 60 b5 54 11 00 09 0e 09 0c 03 05 01 02 37 03 05 02 03 54
+data Attr-26.24757.84.9.9.3 = 0x010237, WiMAX-PFDv2-Eth-Priority-Range-High = 84
+
+#
+# Simple test for continued attributes
+#
+decode 1a 0e 00 00 60 b5 01 08 80 01 05 31 2e 30 1a 0c 00 00 60 b5 01 06 00 02 03 00
+data WiMAX-Release = "1.0", WiMAX-Accounting-Capabilities = No-Accounting
+
+#
+# See if encoding multiple attributes works
+#
+encode WiMAX-Packet-Data-Flow-Id := 32, WiMAX-Service-Data-Flow-ID := 32, WiMAX-Service-Profile-ID := 32
+data 1a 17 00 00 60 b5 1c 11 00 01 04 00 20 02 04 00 20 03 06 00 00 00 20
+
+encode WiMAX-Packet-Data-Flow-Id := 33, WiMAX-Service-Data-Flow-ID := 33, WiMAX-Service-Profile-ID := 33
+data 1a 17 00 00 60 b5 1c 11 00 01 04 00 21 02 04 00 21 03 06 00 00 00 21
+
+encode WiMAX-Packet-Data-Flow-Id := 32, WiMAX-Service-Data-Flow-ID := 32, WiMAX-Service-Profile-ID := 32, WiMAX-Packet-Data-Flow-Id := 33, WiMAX-Service-Data-Flow-ID := 33, WiMAX-Service-Profile-ID := 33
+data 1a 25 00 00 60 b5 1c 1f 00 01 04 00 20 02 04 00 20 03 06 00 00 00 20 01 04 00 21 02 04 00 21 03 06 00 00 00 21
+
+encode WiMAX-Packet-Data-Flow-Id := 32, WiMAX-Service-Data-Flow-ID := 32, WiMAX-Service-Profile-ID := 32, WiMAX-Packet-Data-Flow-Id := 33, WiMAX-Service-Data-Flow-ID := 33, WiMAX-Service-Profile-ID := 33, Session-Timeout := 7200
+data 1a 25 00 00 60 b5 1c 1f 00 01 04 00 20 02 04 00 20 03 06 00 00 00 20 01 04 00 21 02 04 00 21 03 06 00 00 00 21 1b 06 00 00 1c 20
+
+encode Acct-Interim-Interval := 3600, WiMAX-Packet-Data-Flow-Id := 32, WiMAX-Service-Data-Flow-ID := 32, WiMAX-Service-Profile-ID := 32, WiMAX-Packet-Data-Flow-Id := 33, WiMAX-Service-Data-Flow-ID := 33, WiMAX-Service-Profile-ID := 33, Session-Timeout := 7200
+data 55 06 00 00 0e 10 1a 25 00 00 60 b5 1c 1f 00 01 04 00 20 02 04 00 20 03 06 00 00 00 20 01 04 00 21 02 04 00 21 03 06 00 00 00 21 1b 06 00 00 1c 20
+
+encode WiMAX-Packet-Data-Flow-Id := 32, WiMAX-Service-Data-Flow-ID := 32, WiMAX-Service-Profile-ID := 32, Session-Timeout := 7200, WiMAX-Packet-Data-Flow-Id := 33, WiMAX-Service-Data-Flow-ID := 33, WiMAX-Service-Profile-ID := 33
+data 1a 17 00 00 60 b5 1c 11 00 01 04 00 20 02 04 00 20 03 06 00 00 00 20 1b 06 00 00 1c 20 1a 17 00 00 60 b5 1c 11 00 01 04 00 21 02 04 00 21 03 06 00 00 00 21
+
+encode WiMAX-Capability = 0x01ff45454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545040301
+data 1a ff 00 00 60 b5 01 f9 80 01 ff 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 45 1a 15 00 00 60 b5 01 0f 00 45 45 45 45 45 45 45 45 45 04 03 01
+
+decode -
+data WiMAX-Release = "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE", WiMAX-Idle-Mode-Notification-Cap = Supported
+
+#
+# Continuation is set, but there's no continued data.
+decode 1a 0b 00 00 60 b5 31 05 80 00 00
+data Attr-26 = 0x000060b53105800000
+
+encode WiMAX-GMT-Timezone-offset = -1
+data 1a 0d 00 00 60 b5 03 07 00 ff ff ff ff
+
+decode -
+data WiMAX-GMT-Timezone-offset = -1
+
+#
+# It's like a disease which keeps spreading.
+#
+encode Telrad-Reference-QOS-Profile-Name = "garbage"
+data 1a 14 00 00 14 cb 01 0e 00 03 0b 04 09 67 61 72 62 61 67 65
+
+decode -
+data Telrad-Reference-QOS-Profile-Name = "garbage"
diff --git a/src/tests/unit/xlat.txt b/src/tests/unit/xlat.txt
new file mode 100644
index 0000000..5dc4893
--- /dev/null
+++ b/src/tests/unit/xlat.txt
@@ -0,0 +1,142 @@
+#
+# Tests for xlat expansion
+#
+
+xlat %{foo: bar}
+data ERROR offset 2 'Unknown module'
+
+xlat %{test:bar}
+data %{test:bar}
+
+xlat %{1}
+data %{1}
+
+xlat %{33}
+data ERROR offset 2 'Invalid regex reference. Must be in range 0-32'
+
+xlat %{%{foo}:-%{bar}}
+data ERROR offset 4 'Unknown attribute'
+
+xlat %{%{User-Name}:-%{bar}}
+data ERROR offset 18 'Unknown attribute'
+
+xlat %{%{User-Name}:-bar}
+data %{%{User-Name}:-bar}
+
+xlat %{%{test:bar}:-%{User-Name}}
+data %{%{test:bar}:-%{User-Name}}
+
+xlat %{%{test:bar}:-%{%{User-Name}:-bar}}
+data %{%{test:bar}:-%{%{User-Name}:-bar}}
+
+xlat %{Tunnel-Password}
+data %{Tunnel-Password}
+
+xlat %{Tunnel-Password:1}
+data %{Tunnel-Password:1}
+
+xlat %{Tunnel-Password:1[3]}
+data %{Tunnel-Password:1[3]}
+
+xlat %{Tunnel-Password:1[*]}
+data %{Tunnel-Password:1[*]}
+
+xlat %{Tunnel-Password:1[#]}
+data %{Tunnel-Password:1[#]}
+
+xlat %{reply:Tunnel-Password}
+data %{reply:Tunnel-Password}
+
+xlat %{reply:Tunnel-Password:1}
+data %{reply:Tunnel-Password:1}
+
+xlat %{reply:Tunnel-Password:1[3]}
+data %{reply:Tunnel-Password:1[3]}
+
+xlat %{reply:Tunnel-Password:1[*]}
+data %{reply:Tunnel-Password:1[*]}
+
+xlat %{reply:Tunnel-Password:1[#]}
+data %{reply:Tunnel-Password:1[#]}
+
+xlat %{User-Name[3]}
+data %{User-Name[3]}
+
+xlat %{User-Name[*]}
+data %{User-Name[*]}
+
+xlat %{User-Name[#]}
+data %{User-Name[#]}
+
+xlat %{request:User-Name[3]}
+data %{User-Name[3]}
+
+xlat %{request:User-Name[*]}
+data %{User-Name[*]}
+
+xlat %{request:User-Name[#]}
+data %{User-Name[#]}
+
+xlat %{coa:User-Name[#]}
+data %{coa:User-Name[#]}
+
+xlat %{coaX:User-Name[#]}
+data ERROR offset 2 'Unknown module'
+
+xlat %{3GPP-SGSN-Address}
+data %{3GPP-SGSN-Address}
+
+xlat %{%{Operator-Name}:-}
+data %{%{Operator-Name}:-}
+
+xlat %{%{}:-}
+data ERROR offset 4 'Empty expression is invalid'
+
+xlat %{%{}:-foo}
+data ERROR offset 4 'Empty expression is invalid'
+
+xlat %{}
+data ERROR offset 2 'Empty expression is invalid'
+
+xlat %{ }
+data ERROR offset 2 'Invalid attribute name'
+
+xlat %{%{User-Name}:-}
+data %{%{User-Name}:-}
+
+xlat "Hello %S goo"
+data "Hello %S goo"
+
+xlat "%{Foreach-Variable-0}"
+data "%{Foreach-Variable-0}"
+
+#
+# 3GPP stuff, to distinguish "list:3GPP" from
+# "attribute:tag"
+#
+xlat "%{request:3GPP-IMSI}"
+data "%{3GPP-IMSI}"
+
+xlat "%{reply:3GPP-IMSI}"
+data "%{reply:3GPP-IMSI}"
+
+xlat "%{reply:3GPP-IMSI[2]}"
+data "%{reply:3GPP-IMSI[2]}"
+
+xlat /([A-Z0-9\-]*)_%{Calling-Station-Id}/
+data /([A-Z0-9\-]*)_%{Calling-Station-Id}/
+
+xlat %{length:1 + 2
+data ERROR offset 14 'Missing closing brace at end of string'
+
+xlat "%t\tfoo"
+data "%t\tfoo"
+
+xlat "%t\t%{Client-IP-Address}"
+data "%t\t%{Client-IP-Address}"
+
+xlat "foo %{test}"
+data ERROR offset 11 'Missing content in expansion'
+
+xlat "foo %{test:foo}"
+data "foo %{test:foo}"