summaryrefslogtreecommitdiffstats
path: root/src/tests/keywords
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:49:46 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:49:46 +0000
commit50b37d4a27d3295a29afca2286f1a5a086142cec (patch)
tree9212f763934ee090ef72d823f559f52ce387f268 /src/tests/keywords
parentInitial commit. (diff)
downloadfreeradius-upstream.tar.xz
freeradius-upstream.zip
Adding upstream version 3.2.1+dfsg.upstream/3.2.1+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--src/tests/keywords/3gpp19
-rw-r--r--src/tests/keywords/README.md43
-rw-r--r--src/tests/keywords/all.mk123
-rw-r--r--src/tests/keywords/array53
-rw-r--r--src/tests/keywords/base64141
-rw-r--r--src/tests/keywords/break-error11
-rw-r--r--src/tests/keywords/cache229
-rw-r--r--src/tests/keywords/case-attr-error20
-rw-r--r--src/tests/keywords/case-empty23
-rw-r--r--src/tests/keywords/case-empty-string25
-rw-r--r--src/tests/keywords/case-list19
-rw-r--r--src/tests/keywords/cast-byte25
-rw-r--r--src/tests/keywords/cast-integer25
-rw-r--r--src/tests/keywords/cast-ipaddr442
-rw-r--r--src/tests/keywords/cast-short25
-rw-r--r--src/tests/keywords/cmp20
-rw-r--r--src/tests/keywords/cmp-ipaddr20
-rw-r--r--src/tests/keywords/comments48
-rw-r--r--src/tests/keywords/count-error11
-rw-r--r--src/tests/keywords/crypt151
-rw-r--r--src/tests/keywords/default-input.attrs11
-rw-r--r--src/tests/keywords/else-error14
-rw-r--r--src/tests/keywords/escape67
-rw-r--r--src/tests/keywords/escape-sequences95
-rw-r--r--src/tests/keywords/expand39
-rw-r--r--src/tests/keywords/expr108
-rw-r--r--src/tests/keywords/foreach5
-rw-r--r--src/tests/keywords/foreach-break73
-rw-r--r--src/tests/keywords/foreach-break-246
-rw-r--r--src/tests/keywords/foreach-break-344
-rw-r--r--src/tests/keywords/foreach-break-444
-rw-r--r--src/tests/keywords/foreach-break.attrs18
-rw-r--r--src/tests/keywords/foreach-error5
-rw-r--r--src/tests/keywords/foreach-isolation38
-rw-r--r--src/tests/keywords/foreach-list5
-rw-r--r--src/tests/keywords/foreach-list.attrs21
-rw-r--r--src/tests/keywords/foreach-nested9
-rw-r--r--src/tests/keywords/foreach-nested.attrs25
-rw-r--r--src/tests/keywords/foreach-regex26
-rw-r--r--src/tests/keywords/foreach-regex.attrs16
-rw-r--r--src/tests/keywords/foreach-return52
-rw-r--r--src/tests/keywords/foreach-varied-depth43
-rw-r--r--src/tests/keywords/foreach.attrs18
-rw-r--r--src/tests/keywords/hex141
-rw-r--r--src/tests/keywords/if10
-rw-r--r--src/tests/keywords/if-bob15
-rw-r--r--src/tests/keywords/if-else15
-rw-r--r--src/tests/keywords/if-elsif19
-rw-r--r--src/tests/keywords/if-multivalue173
-rw-r--r--src/tests/keywords/if-paircmp27
-rw-r--r--src/tests/keywords/if-rcode-error11
-rw-r--r--src/tests/keywords/if-regex-bad-attribute21
-rw-r--r--src/tests/keywords/if-regex-error12
-rw-r--r--src/tests/keywords/if-regex-match183
-rw-r--r--src/tests/keywords/if-regex-match-comp149
-rw-r--r--src/tests/keywords/if-regex-match-comp.attrs7
-rw-r--r--src/tests/keywords/if-regex-match-named117
-rw-r--r--src/tests/keywords/if-regex-match-named.attrs6
-rw-r--r--src/tests/keywords/if-regex-match.attrs7
-rw-r--r--src/tests/keywords/if-regex-multivalue26
-rw-r--r--src/tests/keywords/if-skip42
-rw-r--r--src/tests/keywords/integer209
-rw-r--r--src/tests/keywords/ipaddr51
-rw-r--r--src/tests/keywords/ipaddr-error10
-rw-r--r--src/tests/keywords/ipaddr.attrs12
-rw-r--r--src/tests/keywords/ipprefix52
-rw-r--r--src/tests/keywords/length155
-rw-r--r--src/tests/keywords/load-balance97
-rw-r--r--src/tests/keywords/log7
-rw-r--r--src/tests/keywords/map-xlat25
-rw-r--r--src/tests/keywords/md458
-rw-r--r--src/tests/keywords/md560
-rw-r--r--src/tests/keywords/module-failure-message40
-rw-r--r--src/tests/keywords/ok-return13
-rw-r--r--src/tests/keywords/ok-return.attrs4
-rw-r--r--src/tests/keywords/pad62
-rw-r--r--src/tests/keywords/pairs42
-rw-r--r--src/tests/keywords/pap146
-rw-r--r--src/tests/keywords/pap-ssha2114
-rw-r--r--src/tests/keywords/radiusd.conf127
-rw-r--r--src/tests/keywords/redundant17
-rw-r--r--src/tests/keywords/redundant-error6
-rw-r--r--src/tests/keywords/redundant-load-balance65
-rw-r--r--src/tests/keywords/redundant-redundant73
-rw-r--r--src/tests/keywords/regex-escape29
-rw-r--r--src/tests/keywords/regex-lhs27
-rw-r--r--src/tests/keywords/return33
-rw-r--r--src/tests/keywords/return-group22
-rw-r--r--src/tests/keywords/return-group.attrs4
-rw-r--r--src/tests/keywords/return-section35
-rw-r--r--src/tests/keywords/sha160
-rw-r--r--src/tests/keywords/sha281
-rw-r--r--src/tests/keywords/smash6
-rw-r--r--src/tests/keywords/string19
-rw-r--r--src/tests/keywords/substring418
-rw-r--r--src/tests/keywords/switch19
-rw-r--r--src/tests/keywords/switch-attr-cast34
-rw-r--r--src/tests/keywords/switch-attr-cmp36
-rw-r--r--src/tests/keywords/switch-default22
-rw-r--r--src/tests/keywords/switch-escape43
-rw-r--r--src/tests/keywords/switch-nodefault22
-rw-r--r--src/tests/keywords/switch-value-error29
-rw-r--r--src/tests/keywords/switch-value-error227
-rw-r--r--src/tests/keywords/switch-virtual23
-rw-r--r--src/tests/keywords/switch-xlat-error17
-rw-r--r--src/tests/keywords/truncation109
-rw-r--r--src/tests/keywords/unknown84
-rw-r--r--src/tests/keywords/unknown-if8
-rw-r--r--src/tests/keywords/unknown-name15
-rw-r--r--src/tests/keywords/unknown-update6
-rw-r--r--src/tests/keywords/update7
-rw-r--r--src/tests/keywords/update-add-ref-index118
-rw-r--r--src/tests/keywords/update-add-ref-tag118
-rw-r--r--src/tests/keywords/update-all9
-rw-r--r--src/tests/keywords/update-array63
-rw-r--r--src/tests/keywords/update-delete40
-rw-r--r--src/tests/keywords/update-error9
-rw-r--r--src/tests/keywords/update-error-29
-rw-r--r--src/tests/keywords/update-error-310
-rw-r--r--src/tests/keywords/update-exec94
-rw-r--r--src/tests/keywords/update-filter75
-rw-r--r--src/tests/keywords/update-index52
-rw-r--r--src/tests/keywords/update-list-error19
-rw-r--r--src/tests/keywords/update-operator85
-rw-r--r--src/tests/keywords/update-prepend65
-rw-r--r--src/tests/keywords/update-remove-any50
-rw-r--r--src/tests/keywords/update-remove-index100
-rw-r--r--src/tests/keywords/update-remove-list40
-rw-r--r--src/tests/keywords/update-remove-tag275
-rw-r--r--src/tests/keywords/update-remove-value116
-rw-r--r--src/tests/keywords/update-tag176
-rw-r--r--src/tests/keywords/update-xlat61
-rw-r--r--src/tests/keywords/urlquote50
-rw-r--r--src/tests/keywords/virtual12
-rw-r--r--src/tests/keywords/virtual-exists12
-rw-r--r--src/tests/keywords/virtual-load-balance14
-rw-r--r--src/tests/keywords/virtual-rhs16
-rw-r--r--src/tests/keywords/virtual_policy15
-rw-r--r--src/tests/keywords/wimax31
-rw-r--r--src/tests/keywords/wimax-comboip19
-rw-r--r--src/tests/keywords/with_dots19
-rw-r--r--src/tests/keywords/xlat-attr62
-rw-r--r--src/tests/keywords/xlat-attr-index53
-rw-r--r--src/tests/keywords/xlat-attr-tag225
-rw-r--r--src/tests/keywords/xlat-concat40
-rw-r--r--src/tests/keywords/xlat-error12
-rw-r--r--src/tests/keywords/xlat-explode91
-rw-r--r--src/tests/keywords/xlat-list64
-rw-r--r--src/tests/keywords/xlat-octets36
-rw-r--r--src/tests/keywords/xlat-virtual-attr131
150 files changed, 8442 insertions, 0 deletions
diff --git a/src/tests/keywords/3gpp b/src/tests/keywords/3gpp
new file mode 100644
index 0000000..05e3fb2
--- /dev/null
+++ b/src/tests/keywords/3gpp
@@ -0,0 +1,19 @@
+#
+# PRE: update
+#
+update request {
+ 3GPP-IMSI := "hello"
+}
+
+#
+# "request:[0-9]" should be parsed as a list followed
+# by an attribute.
+#
+update control {
+ Tmp-String-0 := "%{3GPP-IMSI}"
+ Tmp-String-1 := "%{request:3GPP-IMSI}"
+}
+
+update reply {
+ Filter-Id := "filter"
+} \ No newline at end of file
diff --git a/src/tests/keywords/README.md b/src/tests/keywords/README.md
new file mode 100644
index 0000000..68ce136
--- /dev/null
+++ b/src/tests/keywords/README.md
@@ -0,0 +1,43 @@
+# The Keyword test Framework
+
+See `update` and `default-input.attrs` for examples.
+
+In short, the test framework assumes Access-Request with PAP
+authentication. The password is hard-coded into the configuration,
+and can't be changed.
+
+The entire test suite consists of two files:
+
+* foo
+
+ Contains a short piece of "unlang". The shorter the better. The
+ goal is to do something useful in unlang, and modify the input
+ packet and/or the reply.
+
+ If the test depends on another one, it should name the other test
+ at the top of the file. For example, the `if-else` test depends
+ on the `if` test. This dependency is given by the following lines
+ at the top of the `if-else` file:
+
+ `# PRE: if`
+
+* foo.attrs
+
+ Contains the input packet and the filter for the reply. There
+ always has to be attributes in the input, and filter attributes in the
+ reply.
+
+ If `foo` doesn't exist, then the `default-input.attrs` file is used.
+ This allows many tests to be simplified, as all they need is a
+ little bit of "unlang".
+
+
+## How it works
+
+The input packet is passed into the unit test framework, through the
+unlang snippet in `foo`, and filtered through the reply filter in
+`foo.attrs`. If everything matches, then the test case passes.
+
+To add a test, just put `foo` and (optionally) `foo.attrs` into this
+directory. The build framework will pick them up and automatically
+run them.
diff --git a/src/tests/keywords/all.mk b/src/tests/keywords/all.mk
new file mode 100644
index 0000000..739b738
--- /dev/null
+++ b/src/tests/keywords/all.mk
@@ -0,0 +1,123 @@
+#
+# Unit tests for unlang keywords
+#
+
+#
+# The test files are files without extensions.
+# The list is unordered. The order is added in the next step by looking
+# at precursors.
+#
+KEYWORD_FILES := $(filter-out %.conf %.md %.attrs %.mk %~ %.rej,$(subst $(DIR)/,,$(wildcard $(DIR)/*)))
+
+ifeq "$(OPENSSL_LIBS)" ""
+KEYWORD_FILES := $(filter-out pap-ssha2,$(KEYWORD_FILES))
+endif
+
+#
+# Create the output directory
+#
+.PHONY: $(BUILD_DIR)/tests/keywords
+$(BUILD_DIR)/tests/keywords:
+ @mkdir -p $@
+
+#
+# Find which input files are needed by the tests
+# strip out the ones which exist
+# move the filenames to the build directory.
+#
+BOOTSTRAP_EXISTS := $(addprefix $(DIR)/,$(addsuffix .attrs,$(KEYWORD_FILES)))
+BOOTSTRAP_NEEDS := $(filter-out $(wildcard $(BOOTSTRAP_EXISTS)),$(BOOTSTRAP_EXISTS))
+BOOTSTRAP := $(subst $(DIR),$(BUILD_DIR)/tests/keywords,$(BOOTSTRAP_NEEDS))
+
+#
+# For each file, look for precursor test.
+# Ensure that each test depends on its precursors.
+#
+-include $(BUILD_DIR)/tests/keywords/depends.mk
+
+export OPENSSL_LIBS
+
+$(BUILD_DIR)/tests/keywords/depends.mk: $(addprefix $(DIR)/,$(KEYWORD_FILES)) | $(BUILD_DIR)/tests/keywords
+ @rm -f $@
+ @for x in $^; do \
+ y=`grep 'PRE: ' $$x | sed 's/.*://;s/ / /g;s, , $(BUILD_DIR)/tests/keywords/,g'`; \
+ if [ "$$y" != "" ]; then \
+ z=`echo $$x | sed 's,src/,$(BUILD_DIR)/',`; \
+ echo "$$z: $$y" >> $@; \
+ echo "" >> $@; \
+ fi \
+ done
+
+#
+# These ones get copied over from the default input
+#
+$(BOOTSTRAP): $(DIR)/default-input.attrs | $(BUILD_DIR)/tests/keywords
+ @cp $< $@
+
+#
+# These ones get copied over from their original files
+#
+$(BUILD_DIR)/tests/keywords/%.attrs: $(DIR)/%.attrs | $(BUILD_DIR)/tests/keywords
+ @cp $< $@
+
+#
+# Don't auto-remove the files copied by the rule just above.
+# It's unnecessary, and it clutters the output with crap.
+#
+.PRECIOUS: $(BUILD_DIR)/tests/keywords/%.attrs
+
+KEYWORD_MODULES := $(shell grep -- mods-enabled src/tests/keywords/radiusd.conf | sed 's,.*/,,')
+KEYWORD_RADDB := $(addprefix raddb/mods-enabled/,$(KEYWORD_MODULES))
+KEYWORD_LIBS := $(addsuffix .la,$(addprefix rlm_,$(KEYWORD_MODULES))) rlm_example.la rlm_cache.la
+
+#
+# Files in the output dir depend on the unit tests
+#
+# src/tests/keywords/FOO unlang for the test
+# src/tests/keywords/FOO.attrs input RADIUS and output filter
+# build/tests/keywords/FOO updated if the test succeeds
+# build/tests/keywords/FOO.log debug output for the test
+#
+# Auto-depend on modules via $(shell grep INCLUDE $(DIR)/radiusd.conf | grep mods-enabled | sed 's/.*}/raddb/'))
+#
+# If the test fails, then look for ERROR in the input. No error
+# means it's unexpected, so we die.
+#
+# Otherwise, check the log file for a parse error which matches the
+# ERROR line in the input.
+#
+$(BUILD_DIR)/tests/keywords/%: ${DIR}/% $(BUILD_DIR)/tests/keywords/%.attrs $(TESTBINDIR)/unittest | $(BUILD_DIR)/tests/keywords $(KEYWORD_RADDB) $(KEYWORD_LIBS) build.raddb rlm_cache_rbtree.la rlm_test.la rlm_unix.la
+ @echo UNIT-TEST $(notdir $@)
+ @if ! KEYWORD=$(notdir $@) $(TESTBIN)/unittest -D share -d src/tests/keywords/ -i $@.attrs -f $@.attrs -xx > $@.log 2>&1; then \
+ if ! grep ERROR $< 2>&1 > /dev/null; then \
+ cat $@.log; \
+ echo "# $@.log"; \
+ echo KEYWORD=$(notdir $@) $(TESTBIN)/unittest -D share -d src/tests/keywords/ -i $@.attrs -f $@.attrs -xx; \
+ exit 1; \
+ fi; \
+ FOUND=$$(grep ^$< $@.log | head -1 | sed 's/:.*//;s/.*\[//;s/\].*//'); \
+ EXPECTED=$$(grep -n ERROR $< | sed 's/:.*//'); \
+ if [ "$$EXPECTED" != "$$FOUND" ]; then \
+ cat $@.log; \
+ echo "# $@.log"; \
+ echo KEYWORD=$(notdir $@) $(TESTBIN)/unittest -D share -d src/tests/keywords/ -i $@.attrs -f $@.attrs -xx; \
+ exit 1; \
+ fi \
+ fi
+ @touch $@
+
+#
+# Get all of the unit test output files
+#
+TESTS.KEYWORDS_FILES := $(addprefix $(BUILD_DIR)/tests/keywords/,$(KEYWORD_FILES))
+
+#
+# Depend on the output files, and create the directory first.
+#
+tests.keywords: $(TESTS.KEYWORDS_FILES)
+
+$(TESTS.KEYWORDS_FILES): $(TESTS.XLAT_FILES) $(TESTS.MAP_FILES)
+
+.PHONY: clean.tests.keywords
+clean.tests.keywords:
+ @rm -rf $(BUILD_DIR)/tests/keywords/
diff --git a/src/tests/keywords/array b/src/tests/keywords/array
new file mode 100644
index 0000000..a901a2b
--- /dev/null
+++ b/src/tests/keywords/array
@@ -0,0 +1,53 @@
+#
+# PRE: if
+#
+# Tests for dereferencing the Nth attribute
+#
+update reply {
+ Filter-Id := "filter"
+}
+
+update request {
+ Class := 0x01020304
+ Class += 0x05060708
+ Class += 0x090a0b0c
+}
+
+if (&Class != 0x01020304) {
+ update reply {
+ Filter-Id := "fail 0"
+ }
+}
+
+# Must be the same as above
+if (&Class[0] != 0x01020304) {
+ update reply {
+ Filter-Id += "fail 0a"
+ }
+}
+
+if (&Class[1] != 0x05060708) {
+ update reply {
+ Filter-Id += "fail 1"
+ }
+}
+
+if (&Class[2] != 0x090a0b0c) {
+ update reply {
+ Filter-Id += "fail 2"
+ }
+}
+
+# must not exist
+if (&Class[3]) {
+ update reply {
+ Filter-Id += "fail 3"
+ }
+}
+
+# Last element of the array
+if (&Class[n] != 0x090a0b0c) {
+ update reply {
+ Filter-Id += "fail 4"
+ }
+}
diff --git a/src/tests/keywords/base64 b/src/tests/keywords/base64
new file mode 100644
index 0000000..9661252
--- /dev/null
+++ b/src/tests/keywords/base64
@@ -0,0 +1,141 @@
+#
+# PRE: hex
+#
+update reply {
+ Filter-Id := "filter"
+}
+
+update request {
+ Tmp-String-0 := '9870'
+ Tmp-Octets-0 := 0x39383731
+ Tmp-IP-Address-0 := 57.56.55.50
+ Tmp-Date-0 := 959985459
+ Tmp-Integer-0 := 959985460
+ Tmp-Cast-Abinary := 'ip out forward srcip 57.56.55.53/32 udp dstport = 1812'
+ Tmp-Cast-IfId := '0000:0000:3938:3737'
+ Tmp-Cast-IPv6Addr := '::3938:3738'
+ Tmp-Cast-IPv6Prefix := '::3938:3739/128'
+ Tmp-Cast-Byte := 58
+ Tmp-Cast-Short := 14139
+ Tmp-Cast-Ethernet := 00:00:39:38:37:3c
+ Tmp-Cast-Integer64 := 1152921505566832445
+ Tmp-Cast-IPv4Prefix := 57.56.55.62/32
+}
+
+update request {
+ Tmp-String-0 := "%{base64:&Tmp-String-0}"
+ Tmp-String-1 := "%{base64:&Tmp-Octets-0}"
+ Tmp-String-2 := "%{base64:&Tmp-IP-Address-0}"
+ Tmp-String-3 := "%{base64:&Tmp-Date-0}"
+ Tmp-String-4 := "%{base64:&Tmp-Integer-0}"
+ Tmp-String-5 := "%{base64:&Tmp-Cast-Abinary}"
+ Tmp-String-6 := "%{base64:&Tmp-Cast-Ifid}"
+ Tmp-String-7 := "%{base64:&Tmp-Cast-IPv6Addr}"
+ Tmp-String-8 := "%{base64:&Tmp-Cast-IPv6Prefix}"
+ Tmp-String-9 := "%{base64:&Tmp-Cast-Byte}"
+}
+
+# String - bin 0x39383730
+if (Tmp-String-0 != 'OTg3MA==') {
+ update reply {
+ Filter-Id += 'fail 0'
+ }
+}
+
+# Octets - bin 0x39383731
+if (Tmp-String-1 != 'OTg3MQ==') {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+# IP Address - bin 0x39383732
+if (Tmp-String-2 != 'OTg3Mg==') {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+# Date - bin 0x39383733
+if (Tmp-String-3 != 'OTg3Mw==') {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+# Integer - bin 0x39383734
+if (Tmp-String-4 != 'OTg3NA==') {
+ update reply {
+ Filter-Id += 'fail 4'
+ }
+}
+
+# Abinary - bin 0x0101000039383735000000002000110000000714000200000000000000000000
+if (Tmp-String-5 != 'AQEAADk4NzUAAAAAIAARAAAABxQAAgAAAAAAAAAAAAA=') {
+ update reply {
+ Filter-Id += 'fail 5'
+ }
+}
+
+# ifid - bin 0x0000000039383737
+if (Tmp-String-6 != 'AAAAADk4Nzc=') {
+ update reply {
+ Filter-Id += 'fail 6'
+ }
+}
+
+# ipv6addr - bin 0x00000000000000000000000039383738
+if (Tmp-String-7 != 'AAAAAAAAAAAAAAAAOTg3OA==') {
+ update reply {
+ Filter-ID += 'fail 7'
+ }
+}
+
+# ipv6addrprefix - bin 0x008000000000000000000000000039383739
+if (Tmp-String-8 != 'AIAAAAAAAAAAAAAAAAA5ODc5') {
+ update reply {
+ Filter-ID += 'fail 8'
+ }
+}
+
+# byte - bin 0x3a
+if (Tmp-String-9 != 'Og==') {
+ update reply {
+ Filter-ID += 'fail 9'
+ }
+}
+
+update request {
+ Tmp-String-0 := "%{base64:&Tmp-Cast-Short}"
+ Tmp-String-1 := "%{base64:&Tmp-Cast-Ethernet}"
+ Tmp-String-2 := "%{base64:&Tmp-Cast-Integer64}"
+ Tmp-String-3 := "%{base64:&Tmp-Cast-IPv4Prefix}"
+}
+
+# short - bin 0x373b
+if (Tmp-String-0 != 'Nzs=') {
+ update reply {
+ Filter-ID += 'fail 9'
+ }
+}
+
+# ethernet - bin 0x00003938373c
+if (Tmp-String-1 != 'AAA5ODc8') {
+ update reply {
+ Filter-Id += 'fail 10'
+ }
+}
+
+# integer64 - bin 0x100000003938373d
+if (Tmp-String-2 != 'EAAAADk4Nz0=') {
+ update reply {
+ Filter-Id += 'fail 11'
+ }
+}
+
+# ipv4prefix - bin 0x00203938373e
+if (Tmp-String-3 != 'ACA5ODc+') {
+ update reply {
+ Filter-Id += 'fail 12'
+ }
+}
diff --git a/src/tests/keywords/break-error b/src/tests/keywords/break-error
new file mode 100644
index 0000000..fff20f0
--- /dev/null
+++ b/src/tests/keywords/break-error
@@ -0,0 +1,11 @@
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update reply {
+ Filter-Id := "filter"
+}
+
+if (User-Name == "bob") {
+ break # ERROR
+}
diff --git a/src/tests/keywords/cache b/src/tests/keywords/cache
new file mode 100644
index 0000000..58ce207
--- /dev/null
+++ b/src/tests/keywords/cache
@@ -0,0 +1,229 @@
+#
+# PRE: update if
+#
+update {
+ &control:Cleartext-Password := 'hello'
+ &request:Tmp-String-0 := 'testkey'
+ &reply:Filter-Id := 'filter'
+}
+
+
+#
+# Basic store and retrieve
+#
+update control {
+ &control:Tmp-String-1 := 'cache me'
+}
+
+cache
+if (!updated) {
+ update reply {
+ Filter-Id := 'fail 0'
+ }
+ reject
+}
+
+# Check status-only works correctly (should return ok and consume attribute)
+update control {
+ Cache-Status-Only := 'yes'
+}
+cache
+if (!ok) {
+ update reply {
+ Filter-Id := 'fail 2a'
+ }
+ reject
+}
+
+if (&control:Cache-Status-Only) {
+ update reply {
+ Filter-Id := 'fail 2b'
+ }
+ reject
+}
+
+# Retrieve the entry (should be copied to request list)
+cache
+if (!ok) {
+ update reply {
+ Filter-Id := 'fail 3a'
+ }
+ reject
+}
+
+if (&request:Tmp-String-1 != &control:Tmp-String-1) {
+ update reply {
+ Filter-Id := 'fail 3b'
+ }
+}
+
+# Retrieving the entry should not expire it
+update request {
+ Tmp-String-1 !* ANY
+}
+
+cache
+if (!ok) {
+ update reply {
+ Filter-Id := 'fail 4a'
+ }
+ reject
+}
+
+if (&request:Tmp-String-1 != &control:Tmp-String-1) {
+ update reply {
+ Filter-Id := 'fail 4b'
+ }
+ reject
+}
+
+# Force expiry of the entry
+update control {
+ Cache-TTL := 0
+}
+cache
+if (!ok) {
+ update reply {
+ Filter-Id := 'fail 5'
+ }
+ reject
+}
+
+# Check status-only works correctly (should return notfound and consume attribute)
+update control {
+ Cache-Status-Only := 'yes'
+}
+cache
+if (!notfound) {
+ update reply {
+ Filter-Id := 'fail 6a'
+ }
+ reject
+}
+
+if (&control:Cache-Status-Only) {
+ update reply {
+ Filter-Id := 'fail 6b'
+ }
+ reject
+}
+
+# Check read-only works correctly (should return notfound and consume attribute)
+update control {
+ Cache-Read-Only := 'yes'
+}
+cache
+if (!notfound) {
+ update reply {
+ Filter-Id := 'fail 7a'
+ }
+ reject
+}
+
+if (&control:Cache-Read-Only) {
+ update reply {
+ Filter-Id := 'fail 7b'
+ }
+ reject
+}
+
+# ...and check the entry wasn't recreated
+update control {
+ Cache-Status-Only := 'yes'
+}
+cache
+if (!notfound) {
+ update reply {
+ Filter-Id := 'fail 7c'
+ }
+ reject
+}
+
+# This should still allow the creation of a new entry
+update control {
+ Cache-TTL := -1
+}
+cache
+if (!updated) {
+ update reply {
+ Filter-Id := 'fail 8a'
+ }
+ reject
+}
+
+cache
+if (!ok) {
+ update reply {
+ Filter-Id := 'fail 8b'
+ }
+ reject
+}
+
+if (&Cache-TTL) {
+ update reply {
+ Filter-Id := 'fail 8c'
+ }
+ reject
+}
+
+if (&request:Tmp-String-1 != &control:Tmp-String-1) {
+ update reply {
+ Filter-Id := 'fail 8d'
+ }
+ reject
+}
+
+update control {
+ Tmp-String-1 := 'cache me2'
+}
+
+# Updating the Cache-TTL shouldn't make things go boom (we can't really check if it works)
+update control {
+ Cache-TTL := 30
+}
+cache
+if (!ok) {
+ update reply {
+ Filter-Id := 'fail 9a'
+ }
+ reject
+}
+
+# Request Tmp-String-1 shouldn't have been updated yet
+if (&request:Tmp-String-1 == &control:Tmp-String-1) {
+ update reply {
+ Filter-Id := 'fail 9b'
+ }
+ reject
+}
+
+# Check that a new entry is created
+update control {
+ Cache-TTL := -1
+}
+cache
+if (!updated) {
+ update reply {
+ Filter-Id := 'fail 10a'
+ }
+ reject
+}
+
+# Check Cache-Entry-Hits is updated as we expect
+if (&request:Cache-Entry-Hits != 0) {
+ update reply {
+ Filter-Id := 'fail 12a'
+ }
+ reject
+}
+
+cache
+
+if (&request:Cache-Entry-Hits != 1) {
+ update reply {
+ Filter-Id := 'fail 12b'
+ }
+ reject
+}
+
+
diff --git a/src/tests/keywords/case-attr-error b/src/tests/keywords/case-attr-error
new file mode 100644
index 0000000..e9516cc
--- /dev/null
+++ b/src/tests/keywords/case-attr-error
@@ -0,0 +1,20 @@
+# PRE: case-empty
+#
+update reply {
+ Filter-Id := "fail"
+}
+
+switch &reply:Filter-Id {
+ # deliberately empty
+ case "filter" {
+ }
+
+ case &Not-Dynamically-Allocated { # ERROR
+ update reply {
+ Filter-Id := "fail"
+ }
+ }
+
+ case {
+ }
+}
diff --git a/src/tests/keywords/case-empty b/src/tests/keywords/case-empty
new file mode 100644
index 0000000..46ea054
--- /dev/null
+++ b/src/tests/keywords/case-empty
@@ -0,0 +1,23 @@
+# PRE: switch
+#
+update reply {
+ Filter-Id := "filter"
+}
+
+switch &reply:Filter-Id {
+ # deliberately empty
+ case "filter" {
+ }
+
+ case "fail" {
+ update reply {
+ Filter-Id := "fail"
+ }
+ }
+
+ case {
+ update reply {
+ Filter-Id := "default"
+ }
+ }
+}
diff --git a/src/tests/keywords/case-empty-string b/src/tests/keywords/case-empty-string
new file mode 100644
index 0000000..4b8e7cd
--- /dev/null
+++ b/src/tests/keywords/case-empty-string
@@ -0,0 +1,25 @@
+# PRE: switch
+#
+update request {
+ Filter-Id := ""
+}
+
+switch &Filter-Id {
+ case "" {
+ update reply {
+ Filter-Id := "filter"
+ }
+ }
+
+ case "doug" {
+ update reply {
+ Filter-Id := "doug"
+ }
+ }
+
+ case {
+ update reply {
+ Filter-Id := "default"
+ }
+ }
+}
diff --git a/src/tests/keywords/case-list b/src/tests/keywords/case-list
new file mode 100644
index 0000000..e4a0290
--- /dev/null
+++ b/src/tests/keywords/case-list
@@ -0,0 +1,19 @@
+switch &User-Name {
+ case "bob" {
+ update reply {
+ Filter-Id := "filter"
+ }
+ }
+
+ case &reply: { # ERROR
+ update reply {
+ Filter-Id := "doug"
+ }
+ }
+
+ case {
+ update reply {
+ Filter-Id := "default"
+ }
+ }
+}
diff --git a/src/tests/keywords/cast-byte b/src/tests/keywords/cast-byte
new file mode 100644
index 0000000..4663d95
--- /dev/null
+++ b/src/tests/keywords/cast-byte
@@ -0,0 +1,25 @@
+#
+# PRE: update if
+#
+update {
+ control:Cleartext-Password := 'hello'
+ request:Class := 0xad
+}
+
+if (<byte>Class == 173) {
+ update reply {
+ Filter-Id := "filter"
+ }
+}
+
+if (<byte>Class < 173) {
+ update reply {
+ Filter-Id += "wrong"
+ }
+}
+
+if (<byte>Class > 173) {
+ update reply {
+ Filter-Id += "wrong"
+ }
+}
diff --git a/src/tests/keywords/cast-integer b/src/tests/keywords/cast-integer
new file mode 100644
index 0000000..4972ee7
--- /dev/null
+++ b/src/tests/keywords/cast-integer
@@ -0,0 +1,25 @@
+#
+# PRE: update if
+#
+update {
+ control:Cleartext-Password := 'hello'
+ request:Class := 0x00000101
+}
+
+if (<integer>Class == 257) {
+ update reply {
+ Filter-Id := "filter"
+ }
+}
+
+if (<integer>Class < 256) {
+ update reply {
+ Filter-Id += "wrong"
+ }
+}
+
+if (<integer>Class > 257) {
+ update reply {
+ Filter-Id += "wrong"
+ }
+}
diff --git a/src/tests/keywords/cast-ipaddr b/src/tests/keywords/cast-ipaddr
new file mode 100644
index 0000000..f0dd356
--- /dev/null
+++ b/src/tests/keywords/cast-ipaddr
@@ -0,0 +1,442 @@
+#
+# PRE: update if
+#
+update {
+ control:Cleartext-Password := 'hello'
+ request:NAS-IP-Address := 127.0.0.1
+ request:Tmp-Integer-0 := 2130706433
+ reply:Filter-Id := "filter"
+}
+
+update request {
+ Tmp-String-0 := "%{request:NAS-IP-Address}"
+}
+
+if (<ipaddr>Tmp-Integer-0 != NAS-IP-Address) {
+ update reply {
+ Filter-Id += 'Fail 0'
+ }
+}
+
+#
+# Update statements do implicit casts, so we can check
+# cast results are correct, by using the update to perform
+# the cast, and looking at the results.
+#
+update request {
+ Tmp-Cast-Ipaddr := 203.0.113.1
+ Tmp-Cast-IPv4Prefix := 203.0.113.0/24
+ Tmp-Cast-IPv4Prefix += 203.0.113.1/32
+ Tmp-Cast-IPv6Addr := 2001:DB8::1
+ Tmp-Cast-IPv6Addr += ::ffff:203.0.113.1
+ Tmp-Cast-IPv6Prefix := 2001:DB8::/32
+ Tmp-Cast-IPv6Prefix += ::ffff:203.0.113.1/128
+ Tmp-Cast-IPv6Prefix += ::ffff:203.0.113.1/64
+}
+
+#
+# IPv4 address to IPv6 address
+#
+update control {
+ Tmp-Cast-IPv6addr := &Tmp-Cast-IPaddr
+}
+
+if (&control:Tmp-Cast-IPv6addr != ::ffff:203.0.113.1) {
+ update reply {
+ Filter-Id += 'Fail 1'
+ }
+}
+
+#
+# IPv6 address to IPv4 address
+#
+update control {
+ Tmp-Cast-IPaddr := &control:Tmp-Cast-IPv6addr
+}
+
+if (&control:Tmp-Cast-IPaddr != 203.0.113.1) {
+ update reply {
+ Filter-Id += 'Fail 2'
+ }
+}
+
+#
+# IPv4 prefix to IPv6 prefix
+#
+update control {
+ Tmp-Cast-IPv6Prefix := &Tmp-Cast-IPv4Prefix
+}
+
+if (&control:Tmp-Cast-IPv6Prefix != ::ffff:203.0.113.0/120) {
+ update reply {
+ Filter-Id += 'Fail 31'
+ }
+}
+
+#
+# IPv6 prefix to IPv4 prefix
+#
+update control {
+ Tmp-Cast-IPv4Prefix := &control:Tmp-Cast-IPv6Prefix
+}
+
+if (&control:Tmp-Cast-IPv4Prefix != 203.0.113.1/24) {
+ update reply {
+ Filter-Id += 'Fail 4'
+ }
+}
+
+#
+# IPv4 prefix (32) to IPv6 address
+#
+update control {
+ Tmp-Cast-IPv6Addr := &Tmp-Cast-IPv4Prefix[1]
+}
+
+if (&control:Tmp-Cast-IPv6Addr != ::ffff:203.0.113.1) {
+ update reply {
+ Filter-Id += 'Fail 5'
+ }
+}
+
+#
+# IPv6 prefix (128) to IPv4 address
+#
+update control {
+ Tmp-Cast-Ipaddr := &Tmp-Cast-IPv6Prefix[1]
+}
+
+if (&control:Tmp-Cast-Ipaddr != 203.0.113.1/32) {
+ update reply {
+ Filter-Id += 'Fail 6'
+ }
+}
+
+#
+# IPv4 address to IPv6 prefix (128)
+#
+update control {
+ Tmp-Cast-IPv6Prefix := &Tmp-Cast-Ipaddr
+}
+
+if (&control:Tmp-Cast-IPv6Prefix != ::ffff:203.0.113.1/128) {
+ update reply {
+ Filter-Id += 'Fail 7'
+ }
+}
+
+#
+# IPv6 address to IPv4 prefix (32)
+#
+update control {
+ Tmp-Cast-IPv4Prefix := &Tmp-Cast-IPv6Addr[1]
+}
+
+if (&control:Tmp-Cast-IPv4Prefix != 203.0.113.1/32) {
+ update reply {
+ Filter-Id += 'Fail 8'
+ }
+}
+
+#
+# IPv4 address to IPv4 prefix (32)
+#
+update control {
+ Tmp-Cast-IPv4Prefix := &Tmp-Cast-Ipaddr
+}
+
+if (&control:Tmp-Cast-IPv4Prefix != 203.0.113.1/32) {
+ update reply {
+ Filter-Id += 'Fail 9'
+ }
+}
+
+#
+# IPv6 address to IPv6 prefix (128)
+#
+update control {
+ Tmp-Cast-IPv6Prefix := Tmp-Cast-Ipv6addr
+}
+
+if (&control:Tmp-Cast-IPv6Prefix != 2001:DB8::1/128) {
+ update reply {
+ Filter-Id += 'Fail 11'
+ }
+}
+
+#
+# IPv4 prefix (32) to IPv4 address
+#
+update control {
+ Tmp-Cast-Ipaddr := &Tmp-Cast-IPv4Prefix[1]
+}
+
+if (&control:Tmp-Cast-Ipaddr != 203.0.113.1) {
+ update reply {
+ Filter-Id += 'Fail 12'
+ }
+}
+
+#
+# IPv6 prefix (128) to IPv6 address
+#
+update control {
+ Tmp-Cast-IPv6Addr := &Tmp-Cast-IPv6Prefix[1]
+}
+
+if (&control:Tmp-Cast-IPv6Addr != ::ffff:203.0.113.1) {
+ update reply {
+ Filter-Id += 'Fail 13'
+ }
+}
+
+#
+# And the invalid cases...
+#
+
+#
+# IPv6 Prefix < 128 to IPv6 address
+#
+redundant {
+ group {
+ update control {
+ Tmp-Cast-IPv6Addr := Tmp-Cast-IPv6Prefix
+ }
+ update reply {
+ Filter-Id += 'Fail 14'
+ }
+ }
+ group {
+ if ("%{Module-Failure-Message}" != 'Attribute conversion failed: Invalid cast from ipv6prefix to ipv6addr. Only /128 prefixes may be cast to IP address types') {
+ update reply {
+ Filter-Id += 'Fail 14.5'
+ }
+ }
+ update request {
+ Module-Failure-Message !* ANY
+ }
+ ok
+ }
+}
+
+#
+# IPv6 Prefix < 128 to IPv4 address
+#
+redundant {
+ group {
+ update control {
+ Tmp-Cast-Ipaddr := &Tmp-Cast-IPv6Prefix[2]
+ }
+ update reply {
+ Filter-Id += 'Fail 15'
+ }
+ }
+ group {
+ if ("%{Module-Failure-Message}" != 'Attribute conversion failed: Invalid cast from ipv6prefix to ipaddr. Only /128 prefixes may be cast to IP address types') {
+ update reply {
+ Filter-Id += 'Fail 15.5'
+ }
+ }
+ update request {
+ Module-Failure-Message !* ANY
+ }
+ ok
+ }
+}
+
+#
+# IPv6 Prefix < 96 to IPv4 prefix (causes part of the IPv4/v6 mapping prefix to be masked off)
+#
+redundant {
+ group {
+ update control {
+ Tmp-Cast-Ipv4Prefix := &Tmp-Cast-IPv6Prefix[2]
+ }
+ update reply {
+ Filter-Id += 'Fail 16'
+ }
+ }
+ group {
+ if ("%{Module-Failure-Message}" != 'Attribute conversion failed: Invalid cast from ipv6prefix to ipv4prefix. No IPv4-IPv6 mapping prefix') {
+ update reply {
+ Filter-Id += 'Fail 16.5'
+ }
+ }
+ update request {
+ Module-Failure-Message !* ANY
+ }
+ ok
+ }
+}
+
+#
+# IPv4 Prefix < 32 to IPv6 address
+#
+redundant {
+ group {
+ update control {
+ Tmp-Cast-IPv6Addr := &Tmp-Cast-IPv4Prefix
+ }
+ update reply {
+ Filter-Id += 'Fail 17'
+ }
+ }
+ group {
+ if ("%{Module-Failure-Message}" != 'Attribute conversion failed: Invalid cast from ipv4prefix to ipv6addr. Only /32 prefixes may be cast to IP address types') {
+ update reply {
+ Filter-Id += 'Fail 17.5'
+ }
+ }
+ update request {
+ Module-Failure-Message !* ANY
+ }
+ ok
+ }
+}
+
+#
+# IPv4 Prefix < 32 to IPv4 address
+#
+redundant {
+ group {
+ update control {
+ Tmp-Cast-Ipaddr := &Tmp-Cast-IPv4Prefix
+ }
+ update reply {
+ Filter-Id += 'Fail 17'
+ }
+ }
+ group {
+ if ("%{Module-Failure-Message}" != 'Attribute conversion failed: Invalid cast from ipv4prefix to ipaddr. Only /32 prefixes may be cast to IP address types') {
+ update reply {
+ Filter-Id += 'Fail 17.5'
+ }
+ }
+ update request {
+ Module-Failure-Message !* ANY
+ }
+ ok
+ }
+}
+
+#
+# IPv6 Prefix outside mapping range to IPv4 address
+#
+redundant {
+ group {
+ update control {
+ Tmp-Cast-Ipaddr := &Tmp-Cast-IPv6Prefix
+ }
+ update reply {
+ Filter-Id += 'Fail 18'
+ }
+ }
+ group {
+ if ("%{Module-Failure-Message}" != 'Attribute conversion failed: Invalid cast from ipv6prefix to ipaddr. Only /128 prefixes may be cast to IP address types') {
+ update reply {
+ Filter-Id += 'Fail 18.5'
+ }
+ }
+ update request {
+ Module-Failure-Message !* ANY
+ }
+ ok
+ }
+}
+
+#
+# IPv6 Prefix outside mapping range to IPv4 prefix
+#
+redundant {
+ group {
+ update control {
+ Tmp-Cast-IPv4Prefix := &Tmp-Cast-IPv6Prefix
+ }
+ update reply {
+ Filter-Id += 'Fail 19'
+ }
+ }
+ group {
+ if ("%{Module-Failure-Message}" != 'Attribute conversion failed: Invalid cast from ipv6prefix to ipv4prefix. No IPv4-IPv6 mapping prefix') {
+ update reply {
+ Filter-Id += 'Fail 19.5'
+ }
+ }
+ update request {
+ Module-Failure-Message !* ANY
+ }
+ ok
+ }
+}
+
+#
+# IPv6 Address outside mapping range to IPv4 address
+#
+redundant {
+ group {
+ update control {
+ Tmp-Cast-Ipaddr := &Tmp-Cast-IPv6Addr
+ }
+ update reply {
+ Filter-Id += 'Fail 20'
+ }
+ }
+ group {
+ if ("%{Module-Failure-Message}" != 'Attribute conversion failed: Invalid cast from ipv6addr to ipaddr. No IPv4-IPv6 mapping prefix') {
+ update reply {
+ Filter-Id += 'Fail 20'
+ }
+ }
+ update request {
+ Module-Failure-Message !* ANY
+ }
+ ok
+ }
+}
+
+#
+# IPv6 Address outside mapping range to IPv4 prefix
+#
+redundant {
+ group {
+ update control {
+ Tmp-Cast-IPv4Prefix := &Tmp-Cast-IPv6Addr
+ }
+ update reply {
+ Filter-Id += 'Fail 21'
+ }
+ }
+ group {
+ if ("%{Module-Failure-Message}" != 'Attribute conversion failed: Invalid cast from ipv6addr to ipv4prefix. No IPv4-IPv6 mapping prefix') {
+ update reply {
+ Filter-Id += 'Fail 21.5'
+ }
+ }
+ update request {
+ Module-Failure-Message !* ANY
+ }
+ ok
+ }
+}
+
+#
+# IPv4 address to integer
+#
+update request {
+ Tmp-Integer-0 := &NAS-IP-Address
+}
+
+if (Tmp-Integer-0 != 0x7f000001) {
+ update reply {
+ Filter-Id += 'Fail 22'
+ }
+}
+
+#
+# Check the decimal value just for the heck of it
+#
+if (Tmp-Integer-0 != 2130706433) {
+ update reply {
+ Filter-Id += 'Fail 23'
+ }
+}
diff --git a/src/tests/keywords/cast-short b/src/tests/keywords/cast-short
new file mode 100644
index 0000000..a17b379
--- /dev/null
+++ b/src/tests/keywords/cast-short
@@ -0,0 +1,25 @@
+#
+# PRE: update if
+#
+update {
+ control:Cleartext-Password := 'hello'
+ request:Class := 0x0101
+}
+
+if (<short>Class == 257) {
+ update reply {
+ Filter-Id := "filter"
+ }
+}
+
+if (<short>Class < 256) {
+ update reply {
+ Filter-Id += "wrong"
+ }
+}
+
+if (<short>Class > 257) {
+ update reply {
+ Filter-Id += "wrong"
+ }
+}
diff --git a/src/tests/keywords/cmp b/src/tests/keywords/cmp
new file mode 100644
index 0000000..4cd59b8
--- /dev/null
+++ b/src/tests/keywords/cmp
@@ -0,0 +1,20 @@
+#
+# PRE: update
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update request {
+ Called-Station-Id := "This is a test"
+ Calling-Station-Id := "This is a test"
+}
+
+#
+# Check attribute references
+#
+if (Called-Station-Id == &Calling-Station-Id) {
+ update reply {
+ Filter-Id := "filter"
+ }
+}
diff --git a/src/tests/keywords/cmp-ipaddr b/src/tests/keywords/cmp-ipaddr
new file mode 100644
index 0000000..bf197af
--- /dev/null
+++ b/src/tests/keywords/cmp-ipaddr
@@ -0,0 +1,20 @@
+#
+# PRE: update
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update request {
+ NAS-IP-Address := 127.0.0.1
+ Framed-IP-Address := 127.0.0.1
+}
+
+#
+# Check attribute references
+#
+if (NAS-IP-Address == &Framed-IP-Address) {
+ update reply {
+ Filter-Id := "filter"
+ }
+}
diff --git a/src/tests/keywords/comments b/src/tests/keywords/comments
new file mode 100644
index 0000000..483bd05
--- /dev/null
+++ b/src/tests/keywords/comments
@@ -0,0 +1,48 @@
+#
+# PRE: update if
+#
+
+# One comment
+#{ Two comment
+#} Three comment
+#'Four'
+#"Five comment"
+##Six Comment#
+ #Seven comment (yes i'm meant to be tabbed in)
+ #Eight comment (yes i'm meant to have spaces before me)
+ #Nine comment (tabs and spaces, are you crazy?!)
+
+update { #}'{ Opening block with extra special chars {} '"
+ control:Cleartext-Password := 'hello' # This should update the password so the test passes
+ reply:Filter-Id := 'filter'# Eek! Too close
+} #{'} Closing block with extra special chars {} '"
+
+update { request:Tmp-String-0 := 'candy' } # Comment after unicorn block
+
+update request {
+ request:Reply-Message += 'I am #literally a comment #'
+ request:Reply-Message += "I am #literally a comment #"
+}
+
+if (&request:Tmp-String-0 != 'candy') {
+ update reply {
+ reply:Filter-Id += 'fail 0'
+ }
+}
+
+if (&request:Reply-Message[0] != 'I am #literally a comment #') {
+ update reply {
+ reply:Filter-Id += 'fail 1'
+ }
+}
+
+if (&request:Reply-Message[1] != "I am #literally a comment #") {
+ update reply {
+ reply:Filter-Id += 'fail 2'
+ }
+}
+
+ok # I'm a comment after a module call
+ok # I'm a comment # after a module {} call
+
+ok, ok, ok, ok, ok
diff --git a/src/tests/keywords/count-error b/src/tests/keywords/count-error
new file mode 100644
index 0000000..f0723cb
--- /dev/null
+++ b/src/tests/keywords/count-error
@@ -0,0 +1,11 @@
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update reply {
+ Filter-Id := "filter"
+}
+
+update request {
+ Tmp-String-0 := &reply:Filter-Id[#] # ERROR
+}
diff --git a/src/tests/keywords/crypt b/src/tests/keywords/crypt
new file mode 100644
index 0000000..e6d63aa
--- /dev/null
+++ b/src/tests/keywords/crypt
@@ -0,0 +1,151 @@
+#
+# PRE: update if
+#
+
+# Skip all these tests if crypt_r was not available
+#
+if ("%{crypt:&User-Password}") {
+ noop
+}
+if ("%{request:Module-Failure-Message[0]}" !~ /^Crypt not available at compile time/) {
+
+
+# Set required attributes
+#
+update reply {
+ &Filter-Id := "filter"
+}
+
+update request {
+ &Tmp-String-0 := 'foo'
+ &Tmp-String-1 := 'foo:bar'
+ &Tmp-String-2 := 'f:'
+ &Tmp-String-3 := &User-Password
+ &Tmp-String-4 := &control:Cleartext-Password
+ &Tmp-String-5 := 'fwtLWDtMiSbH8lmXCMIVfrSMJjF'
+ &Tmp-String-8 := 'aa'
+ &Tmp-String-9 := '$1$abcdefgh'
+}
+
+
+# Check for error on no salt
+#
+if ("%{crypt:&User-Password}") {
+ update reply {
+ &Filter-Id += 'fail 1a'
+ }
+}
+
+if ("%{request:Module-Failure-Message[0]}" != 'No salt specified in crypt xlat') {
+ update reply {
+ &Filter-Id += 'fail 1b'
+ }
+}
+
+
+# Check DES - all crypt_r() implementations should do this.
+#
+if ("%{crypt:aa:foo}" != "aaKNIEDOaueR6") {
+ update reply {
+ &Filter-Id += 'fail 2a'
+ }
+}
+
+if ("%{crypt:&Tmp-String-8:foo}" != "aaKNIEDOaueR6") {
+ update reply {
+ &Filter-Id += 'fail 2b'
+ }
+}
+
+if ("%{crypt:aa:&User-Password}" != "aaPwJ9XL9Y99E") {
+ update reply {
+ &Filter-Id += 'fail 2c'
+ }
+}
+
+
+# Test we can encrypt and then authenticate
+#
+update {
+ &request:User-Password := &request:Tmp-String-5
+ &control:Crypt-Password := "%{crypt:AZ:&Tmp-String-5}"
+ &control:Cleartext-Password !* ""
+}
+
+group {
+ pap.authenticate {
+ fail = 1
+ reject = 1
+ }
+
+ if (!ok) {
+ update reply {
+ &Filter-Id += 'fail 3'
+ }
+ }
+}
+
+update {
+ &request:User-Password := &Tmp-String-3
+ &control:Cleartext-Password := &Tmp-String-4
+}
+
+
+# Clear Module-Failure-Message so below tests work no matter what
+# happened above
+#
+update request {
+ &Module-Failure-Message !* ""
+}
+
+
+# Check colons in password
+#
+if ("%{crypt:aa:foo:bar}" != "aadzEnaZwH90k") {
+ update reply {
+ &Filter-Id += 'fail 4a'
+ }
+}
+
+if ("%{crypt:aa:&Tmp-String-1}" != "aadzEnaZwH90k") {
+ update reply {
+ &Filter-Id += 'fail 4b'
+ }
+}
+
+
+# Check invalid chars in salt
+#
+# In this case, depending on the library implementation, crypt
+# seems to either return an empty string (null) and set an error,
+# or it will return an invalid hash beginning with '*'.
+#
+update request {
+ &Tmp-String-7 := "%{crypt:&Tmp-String-2:foo}"
+}
+
+if (&Tmp-String-7 !~ /^\*/ && \
+ "%{request:Module-Failure-Message[0]}" !~ /Crypt salt has the wrong format/) {
+ update reply {
+ &Filter-Id += 'fail 5a'
+ }
+}
+
+
+# Convert the Cleartext-Password to Password-With-Header and auth with that
+#
+update control {
+ &Password-With-Header := "{crypt}%{crypt:$1$abcdefgh:&Tmp-String-4}"
+ &Crypt-Password !* ""
+ &Cleartext-Password !* ""
+}
+
+
+# Crypt not available at compile time? Force the test to pass.
+#
+}
+else {
+ update reply {
+ &Filter-Id := "filter"
+ }
+}
diff --git a/src/tests/keywords/default-input.attrs b/src/tests/keywords/default-input.attrs
new file mode 100644
index 0000000..93566a6
--- /dev/null
+++ b/src/tests/keywords/default-input.attrs
@@ -0,0 +1,11 @@
+#
+# Input packet
+#
+User-Name = "bob"
+User-Password = "hello"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Filter-Id == 'filter'
diff --git a/src/tests/keywords/else-error b/src/tests/keywords/else-error
new file mode 100644
index 0000000..3816270
--- /dev/null
+++ b/src/tests/keywords/else-error
@@ -0,0 +1,14 @@
+#
+# PRE: update if
+#
+# "else" has to be preceded by an "if" or "elsif"
+#
+if (1) {
+ update reply {
+ Filter-Id := "filter"
+ }
+
+ else { # ERROR
+ ok
+ }
+}
diff --git a/src/tests/keywords/escape b/src/tests/keywords/escape
new file mode 100644
index 0000000..5d0b3bc
--- /dev/null
+++ b/src/tests/keywords/escape
@@ -0,0 +1,67 @@
+#
+# PRE: update if xlat-attr-index
+#
+update request {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := "filter"
+
+ Tmp-String-0 := '@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /'
+ Tmp-String-1 := '±§#$%^&+={[}];<,>?`|"'
+ Tmp-String-2 := '™¥¤'
+ Tmp-String-3 := '=C2=B1=C2=A7=23=24=25=5E=26=2B=3D=7B=5B=7D=5D=3B=3C=2C=3E=3F=60=7C=22'
+ Tmp-String-4 := '=E2=84=A2=C2=A5=C2=A4'
+ Tmp-String-5 := '=40=61=62=63=64=65=66=67'
+
+ # Mixture of safe and unsafe chars
+ Tmp-String-6 := 'ŒČÿ'
+ Tmp-String-7 := 'Œ=C4=8Cÿ'
+}
+
+if (<string>"%{escape:%{request:Tmp-String-0}}" != &Tmp-String-0) {
+ update reply {
+ Filter-Id += 'Fail 1'
+ }
+}
+
+if (<string>"%{escape:%{request:Tmp-String-1}}" != &Tmp-String-3) {
+ update reply {
+ Filter-Id += 'Fail 2'
+ }
+}
+
+if (<string>"%{escape:%{request:Tmp-String-2}}" != &Tmp-String-4) {
+ update reply {
+ Filter-Id += 'Fail 3'
+ }
+}
+
+if (<string>"%{unescape:%{request:Tmp-String-0}}" != &Tmp-String-0) {
+ update reply {
+ Filter-Id += 'Fail 4'
+ }
+}
+
+if (<string>"%{unescape:%{request:Tmp-String-3}}" != "%{Tmp-String-1}") {
+ update reply {
+ Filter-Id += 'Fail 5'
+ }
+}
+
+if (<string>"%{unescape:%{request:Tmp-String-4}}" != &Tmp-String-2) {
+ update reply {
+ Filter-Id += 'Fail 6'
+ }
+}
+
+if (<string>"%{escape:%{request:Tmp-String-6}}" != &Tmp-String-7) {
+ update reply {
+ Filter-Id += 'Fail 7'
+ }
+}
+
+if (<string>"%{unescape:%{request:Tmp-String-7}}" != &Tmp-String-6) {
+ update reply {
+ Filter-Id += 'Fail 8'
+ }
+}
+
diff --git a/src/tests/keywords/escape-sequences b/src/tests/keywords/escape-sequences
new file mode 100644
index 0000000..967656d
--- /dev/null
+++ b/src/tests/keywords/escape-sequences
@@ -0,0 +1,95 @@
+#
+# PRE: update if xlat-attr-index
+#
+update request {
+ control:Cleartext-Password := 'hello'
+ Tmp-Octets-0 := 0x69206861766520736361727920656d626564646564207468696e67730020696e73696465206d65
+ Tmp-Octets-1 := 0x30783031013078303707307830410A307830440D222230786230b0C2b0
+ Tmp-String-0 := "i have scary embedded things\000 inside me"
+ Tmp-String-0 += "0x01\0010x07\0070x0A\n0x0D\r\"\"0xb0\260°"
+
+ # and again with single quoted strings.
+ # unlike other languages, \r, \t, and \n have meaning inside of 'string'
+ Tmp-String-1 := 'i have scary embedded things\000 inside me'
+ Tmp-String-1 += '0x01\0010x07\0070x0A\n0x0D\r""0xb0\260°'
+
+ Tmp-String-2 := 'i have scary embedded things\000 inside me'
+ Tmp-String-2 += "0x01\0010x07\0070x0A\n0x0D\r''0xb0\260°"
+
+ reply:Filter-Id := "filter"
+}
+
+
+if ("%{length:&Tmp-String-0}" != 39) {
+ update reply {
+ Filter-Id += 'fail l-0'
+ }
+}
+
+if ("%{length:&Tmp-String-1}" != 42) {
+ update reply {
+ Filter-Id += 'fail l-1'
+ }
+}
+
+if ("%{string:Tmp-Octets-0}" != "i have scary embedded things\000 inside me") {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+if (&Tmp-String-0 != "i have scary embedded things\000 inside me") {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+if ("%{string:Tmp-Octets-1}" != "0x01\0010x07\0070x0A\n0x0D\r\"\"0xb0\260°") {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+if ("%{Tmp-String-0[0]}" != "i have scary embedded things\000 inside me") {
+ update reply {
+ Filter-Id += 'fail 4'
+ }
+}
+
+if ("%{Tmp-String-0[1]}" != "0x01\0010x07\0070x0A\n0x0D\r\"\"0xb0\260°") {
+ update reply {
+ Filter-Id += 'fail 5'
+ }
+}
+
+# And another slightly different codepath...
+if ("%{Tmp-String-0[*]}" != "i have scary embedded things\000 inside me,0x01\0010x07\0070x0A\n0x0D\r\"\"0xb0\260°") {
+ update reply {
+ Filter-Id += 'fail 6'
+ }
+}
+
+if (&Tmp-String-0[0] != &Tmp-String-0[0]) {
+ update reply {
+ Filter-Id += 'fail 7'
+ }
+}
+
+#
+# This seems weird... double escapes for most things, but single escapes
+# for the quotation marks.
+#
+if ("%{Tmp-String-2[1]}" != "0x01\0010x07\0070x0A\n0x0D\r''0xb0\260°") {
+ update reply {
+ Filter-Id += 'fail 8'
+ }
+}
+
+#
+# And again as an attribute reference
+#
+if (&Tmp-String-2[1] != "0x01\0010x07\0070x0A\n0x0D\r''0xb0\260°") {
+ update reply {
+ Filter-Id += 'fail 9'
+ }
+}
diff --git a/src/tests/keywords/expand b/src/tests/keywords/expand
new file mode 100644
index 0000000..ada0ee9
--- /dev/null
+++ b/src/tests/keywords/expand
@@ -0,0 +1,39 @@
+#
+# PRE: update switch
+#
+
+#
+# This is a virtual attribute.
+# It is NOT optimized to
+#
+# switch &Request-Processing-Stage
+#
+# because it doesn't really exist.
+# The xlat expansion code will take care of
+# returning the string value of the "attribute"
+#
+switch "%{Request-Processing-Stage}" {
+ case authorize {
+ update reply {
+ Filter-Id := "filter"
+ }
+ }
+
+ case authenticate {
+ update reply {
+ Filter-Id := "authenticate"
+ }
+ }
+
+ case bob {
+ update reply {
+ Filter-Id := "bob"
+ }
+ }
+
+ case {
+ update reply {
+ Filter-Id := "default"
+ }
+ }
+}
diff --git a/src/tests/keywords/expr b/src/tests/keywords/expr
new file mode 100644
index 0000000..7645931
--- /dev/null
+++ b/src/tests/keywords/expr
@@ -0,0 +1,108 @@
+#
+# PRE: update if
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update reply {
+ Filter-Id := "filter"
+}
+
+#
+# Simple
+#
+if ("%{expr: 1 + 2 + 3 + 4}" != 10) {
+ update reply {
+ Filter-Id := "fail-1"
+ }
+}
+
+#
+# Precedence
+#
+if ("%{expr: 1 + 2 * 3 + 4}" != 11) {
+ update reply {
+ Filter-Id := "fail-2"
+ }
+}
+
+#
+# attribute references
+#
+update request {
+ Tmp-Integer-0 = 1
+ Tmp-Integer-1 = 3
+ Tmp-Integer-2 = 4
+ Tmp-Date-0 = "%l"
+}
+
+if ("%{expr: 1 + 2 * &Tmp-Integer-1 + 4}" != 11) {
+ update reply {
+ Filter-Id := "fail-3"
+ }
+}
+
+if ("%{expr: 1 + 2 * (&Tmp-Integer-1 + 4)}" != 15) {
+ update reply {
+ Filter-Id := "fail-4"
+ }
+}
+
+if ("%{expr: 1 + 2 * (&Tmp-Integer-1 + &Tmp-Integer-2)}" != 15) {
+ update reply {
+ Filter-Id := "fail-5"
+ }
+}
+
+if ("%{expr: 1 & ~1}" != 0) {
+ update reply {
+ Filter-Id := "fail-6"
+ }
+}
+
+if ("%{expr: 1 & ~2}" != 1) {
+ update reply {
+ Filter-Id := "fail-7"
+ }
+}
+
+if ("%{expr: -1 * 2}" != -2) {
+ update reply {
+ Filter-Id := "fail-8"
+ }
+}
+
+if ("%{expr: 2 - -1}" != 3) {
+ update reply {
+ Filter-Id := "fail-9"
+ }
+}
+
+if ("%{expr: 1 << 2 | 1}" != 5) {
+ update reply {
+ Filter-Id := "fail-10"
+ }
+}
+
+if ("%{expr: &Tmp-Date-0}" <= 0) {
+ update reply {
+ Filter-Id := "fail-11"
+ }
+}
+
+#
+# Unary negation
+#
+if ("%{expr: 6 + -(1 + 3)}" != 2) {
+ update reply {
+ Filter-Id := "fail-12"
+ }
+}
+
+if ("%{expr: 6 * -&Tmp-Integer-2}" != -24) {
+ update reply {
+ Filter-Id := "fail-13"
+ }
+}
+
diff --git a/src/tests/keywords/foreach b/src/tests/keywords/foreach
new file mode 100644
index 0000000..9a4c266
--- /dev/null
+++ b/src/tests/keywords/foreach
@@ -0,0 +1,5 @@
+foreach Filter-Id {
+ update reply {
+ Called-Station-Id += "%{Foreach-Variable-0}"
+ }
+}
diff --git a/src/tests/keywords/foreach-break b/src/tests/keywords/foreach-break
new file mode 100644
index 0000000..67812fe
--- /dev/null
+++ b/src/tests/keywords/foreach-break
@@ -0,0 +1,73 @@
+# PRE: foreach
+#
+
+#
+# We DON'T want to see this one.
+#
+update request {
+ Filter-Id += "broken"
+}
+
+foreach Filter-Id {
+ #
+ # If we see this one, "break" out of the
+ # foreach loop.
+ #
+ if ("%{Foreach-Variable-0}" == "broken") {
+ break
+ }
+
+ update reply {
+ Called-Station-Id += "%{Foreach-Variable-0}"
+ }
+}
+
+
+#
+# Adding attribute during request and immediately breaking
+#
+update {
+ request:Filter-Id += "1"
+ request:Filter-Id += "2"
+}
+
+foreach &request:Reply-Message {
+ if("%{Foreach-Variable-0}" == "1") {
+ update {
+ request:Filter-Id += "3"
+ }
+ break
+
+ update reply {
+ Filter-Id := "fail-break-1"
+ }
+ }
+}
+
+update {
+ request:Filter-Id !* ANY
+}
+
+#
+# Adding attribute during request and continuing
+#
+update {
+ request:Filter-Id += "1"
+ request:Filter-Id += "2"
+}
+
+foreach &request:Reply-Message {
+ if("%{Foreach-Variable-0}" == "1") {
+ update {
+ request:Filter-Id += "3"
+ }
+ }
+
+ if ("%{Foreach-Variable-0}" == "3") {
+ break
+
+ update reply {
+ Filter-Id := "fail-break-2"
+ }
+ }
+}
diff --git a/src/tests/keywords/foreach-break-2 b/src/tests/keywords/foreach-break-2
new file mode 100644
index 0000000..b1f6040
--- /dev/null
+++ b/src/tests/keywords/foreach-break-2
@@ -0,0 +1,46 @@
+#
+# PRE: foreach foreach-break
+#
+
+update request {
+ Calling-Station-Id := "ABCDEF_8"
+}
+
+update control {
+ &Tmp-String-0 := "0"
+ &Tmp-String-0 += "1"
+ &Tmp-String-0 += "2"
+ &Tmp-String-0 += "3"
+ &Tmp-String-0 += "4"
+ &Tmp-String-0 += "5"
+ &Tmp-String-0 += "6"
+ &Tmp-String-0 += "7"
+ &Tmp-String-0 += "8"
+ &Tmp-String-0 += "9"
+ &Tmp-String-0 += "a"
+ &Tmp-String-0 += "b"
+ &Tmp-String-0 += "c"
+ &Tmp-String-0 += "d"
+ &Tmp-String-0 += "e"
+ &Tmp-String-0 += "f"
+ &Tmp-String-0 += "g"
+}
+
+foreach control:Tmp-String-0 {
+ if ("%{Calling-Station-Id[*]}" =~ /([A-Z0-9\-]*)_%{Foreach-Variable-0}/) {
+ update request {
+ Called-Station-Id := "%{1}"
+ }
+ update reply {
+ Filter-Id := "filter"
+ }
+
+ break
+ }
+ elsif ("%{Foreach-Variable-0}" == '9') {
+ update reply {
+ Filter-Id := "fail-9"
+ }
+ reject
+ }
+}
diff --git a/src/tests/keywords/foreach-break-3 b/src/tests/keywords/foreach-break-3
new file mode 100644
index 0000000..af03da6
--- /dev/null
+++ b/src/tests/keywords/foreach-break-3
@@ -0,0 +1,44 @@
+#
+# PRE: foreach foreach-break
+#
+
+update request {
+ Calling-Station-Id := "8"
+}
+
+update control {
+ &Calling-Station-Id := "0"
+ &Calling-Station-Id += "1"
+ &Calling-Station-Id += "2"
+ &Calling-Station-Id += "3"
+ &Calling-Station-Id += "4"
+ &Calling-Station-Id += "5"
+ &Calling-Station-Id += "6"
+ &Calling-Station-Id += "7"
+ &Calling-Station-Id += "8"
+ &Calling-Station-Id += "9"
+ &Calling-Station-Id += "a"
+ &Calling-Station-Id += "b"
+ &Calling-Station-Id += "c"
+ &Calling-Station-Id += "d"
+ &Calling-Station-Id += "e"
+ &Calling-Station-Id += "f"
+ &Calling-Station-Id += "g"
+}
+
+foreach &control:Calling-Station-Id {
+ if (&request:Calling-Station-Id == "%{Foreach-Variable-0}") {
+ update reply {
+ Filter-Id := "filter"
+ }
+
+ break
+ }
+ elsif ("%{Foreach-Variable-0}" == '9') {
+ update reply {
+ Filter-Id := "fail-9"
+ }
+
+ reject
+ }
+}
diff --git a/src/tests/keywords/foreach-break-4 b/src/tests/keywords/foreach-break-4
new file mode 100644
index 0000000..037af8e
--- /dev/null
+++ b/src/tests/keywords/foreach-break-4
@@ -0,0 +1,44 @@
+#
+# PRE: foreach foreach-break-3
+#
+
+update request {
+ Calling-Station-Id := "8"
+}
+
+update control {
+ &Calling-Station-Id := "0"
+ &Calling-Station-Id += "1"
+ &Calling-Station-Id += "2"
+ &Calling-Station-Id += "3"
+ &Calling-Station-Id += "4"
+ &Calling-Station-Id += "5"
+ &Calling-Station-Id += "6"
+ &Calling-Station-Id += "7"
+ &Calling-Station-Id += "8"
+ &Calling-Station-Id += "9"
+ &Calling-Station-Id += "a"
+ &Calling-Station-Id += "b"
+ &Calling-Station-Id += "c"
+ &Calling-Station-Id += "d"
+ &Calling-Station-Id += "e"
+ &Calling-Station-Id += "f"
+ &Calling-Station-Id += "g"
+}
+
+foreach &control:Calling-Station-Id {
+ if (&request:Calling-Station-Id == "%{Foreach-Variable-0}") {
+ update reply {
+ Filter-Id := "filter"
+ }
+
+ break
+ }
+ elsif ("%{Foreach-Variable-0}" == '9') {
+ update reply {
+ Filter-Id := "fail-9"
+ }
+
+ reject
+ }
+}
diff --git a/src/tests/keywords/foreach-break.attrs b/src/tests/keywords/foreach-break.attrs
new file mode 100644
index 0000000..26c2876
--- /dev/null
+++ b/src/tests/keywords/foreach-break.attrs
@@ -0,0 +1,18 @@
+#
+# Input packet
+#
+User-Name = "bob"
+User-Password = "hello"
+Filter-Id = "1"
+Filter-Id += "2"
+Filter-Id += "3"
+Filter-Id += "4"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Called-Station-Id == "1"
+Called-Station-Id == "2"
+Called-Station-Id == "3"
+Called-Station-Id == "4"
diff --git a/src/tests/keywords/foreach-error b/src/tests/keywords/foreach-error
new file mode 100644
index 0000000..fb4a236
--- /dev/null
+++ b/src/tests/keywords/foreach-error
@@ -0,0 +1,5 @@
+foreach "%{expr:1 + 2}" { # ERROR
+ update reply {
+ Called-Station-Id += "%{Foreach-Variable-0}"
+ }
+}
diff --git a/src/tests/keywords/foreach-isolation b/src/tests/keywords/foreach-isolation
new file mode 100644
index 0000000..b77806d
--- /dev/null
+++ b/src/tests/keywords/foreach-isolation
@@ -0,0 +1,38 @@
+#
+# PRE: foreach if-multivalue
+#
+
+update {
+ &reply:Filter-Id := 'filter'
+ &control:Tmp-String-0 := '0'
+ &control:Tmp-String-0 += '1'
+ &control:Tmp-String-0 += '2'
+ &control:Tmp-String-0 += '3'
+}
+
+foreach control:Tmp-String-0 {
+ update control {
+ Tmp-String-0 -= "%{expr:%{Foreach-Variable-0} + 1}"
+ }
+ update request {
+ Tmp-String-0 += "%{Foreach-Variable-0}"
+ }
+}
+
+if (!&Tmp-String-0[0] || !&Tmp-String-0[1] || !&Tmp-String-0[2] || !&Tmp-String-0[3]) {
+ update reply {
+ Filter-Id += 'Fail 0'
+ }
+}
+
+if ((&Tmp-String-0[0] != '0') || (&Tmp-String-0[1] != '1') || (&Tmp-String-0[2] != '2') || (&Tmp-String-0[3] != '3')) {
+ update reply {
+ Filter-Id += 'Fail 1'
+ }
+}
+
+if (!&control:Tmp-String-0[0] || &control:Tmp-String-0[1] || &control:Tmp-String-0[2] || &control:Tmp-String-0[3]) {
+ update reply {
+ Filter-Id += 'Fail 2'
+ }
+}
diff --git a/src/tests/keywords/foreach-list b/src/tests/keywords/foreach-list
new file mode 100644
index 0000000..4780e4f
--- /dev/null
+++ b/src/tests/keywords/foreach-list
@@ -0,0 +1,5 @@
+foreach &request: {
+ update reply {
+ Called-Station-Id += "%{Foreach-Variable-0}"
+ }
+}
diff --git a/src/tests/keywords/foreach-list.attrs b/src/tests/keywords/foreach-list.attrs
new file mode 100644
index 0000000..aedd599
--- /dev/null
+++ b/src/tests/keywords/foreach-list.attrs
@@ -0,0 +1,21 @@
+#
+# Input packet
+#
+User-Name = "bob"
+User-Password = "hello"
+Filter-Id = "1"
+Filter-Id += "2"
+Filter-Id += "3"
+Filter-Id += "4"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Called-Station-Id == 'bob'
+Called-Station-Id == 'hello'
+Called-Station-Id == '1'
+Called-Station-Id == '2'
+Called-Station-Id == '3'
+Called-Station-Id == '4'
+
diff --git a/src/tests/keywords/foreach-nested b/src/tests/keywords/foreach-nested
new file mode 100644
index 0000000..b6109a3
--- /dev/null
+++ b/src/tests/keywords/foreach-nested
@@ -0,0 +1,9 @@
+# PRE: foreach
+#
+foreach Filter-Id {
+ foreach Calling-Station-Id {
+ update reply {
+ Called-Station-Id += "%{Foreach-Variable-0} %{Foreach-Variable-1}"
+ }
+ }
+}
diff --git a/src/tests/keywords/foreach-nested.attrs b/src/tests/keywords/foreach-nested.attrs
new file mode 100644
index 0000000..52d1f81
--- /dev/null
+++ b/src/tests/keywords/foreach-nested.attrs
@@ -0,0 +1,25 @@
+#
+# Input packet
+#
+User-Name = "bob"
+User-Password = "hello"
+Filter-Id = "1"
+Filter-Id += "2"
+Filter-Id += "3"
+Filter-Id += "4"
+Calling-Station-Id = "foo\n"
+Calling-Station-Id += "bar"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Called-Station-Id == '1 foo\n'
+Called-Station-Id == '1 bar'
+Called-Station-Id == '2 foo\n'
+Called-Station-Id == '2 bar'
+Called-Station-Id == '3 foo\n'
+Called-Station-Id == '3 bar'
+Called-Station-Id == '4 foo\n'
+Called-Station-Id == '4 bar'
+
diff --git a/src/tests/keywords/foreach-regex b/src/tests/keywords/foreach-regex
new file mode 100644
index 0000000..dab57a3
--- /dev/null
+++ b/src/tests/keywords/foreach-regex
@@ -0,0 +1,26 @@
+# PRE: foreach if-regex-match
+
+# This is what most people end up using foreach for,
+# so we should probably test it works.
+update request {
+ Tmp-String-0 := "cisco"
+}
+
+# Expanded regex
+foreach Cisco-AVPair {
+ if ("%{Foreach-Variable-0}" =~ /^%{Tmp-String-0}=(.*)$/i) {
+ update reply {
+ Called-Station-Id += "%{1}"
+ }
+ }
+}
+
+# Compiled regex
+foreach Cisco-AVPair {
+ if ("%{Foreach-Variable-0}" =~ /^stupid=(.*)$/i) {
+ update reply {
+ Called-Station-Id += "%{1}"
+ }
+ }
+}
+
diff --git a/src/tests/keywords/foreach-regex.attrs b/src/tests/keywords/foreach-regex.attrs
new file mode 100644
index 0000000..79996c7
--- /dev/null
+++ b/src/tests/keywords/foreach-regex.attrs
@@ -0,0 +1,16 @@
+#
+# Input packet
+#
+User-Name = "bob"
+User-Password = "hello"
+Cisco-AVPair = "stupid=1"
+Cisco-AVPair += "retarded=2"
+Cisco-AVPair += "cisco=3"
+Cisco-AVPair += "shit=4"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Called-Station-Id == "3"
+Called-Station-Id == "1"
diff --git a/src/tests/keywords/foreach-return b/src/tests/keywords/foreach-return
new file mode 100644
index 0000000..05409c9
--- /dev/null
+++ b/src/tests/keywords/foreach-return
@@ -0,0 +1,52 @@
+# PRE: foreach foreach-break
+#
+
+update control {
+ Cleartext-Password := 'hello'
+}
+
+#
+# Adding attribute during request and immediately returning should still work
+#
+update request {
+ Filter-Id := "1"
+ Filter-Id += "2"
+ Filter-Id += "3"
+ Filter-Id += "4"
+ Filter-Id += "5"
+}
+
+foreach &Filter-Id {
+ if ("%{Foreach-Variable-0}" == "3") {
+ update reply {
+ Filter-Id := "filter"
+ }
+
+ #
+ # We need this because the "return" below
+ # will prevent the "pap" module from being run
+ # in the "authorize" section.
+ #
+ update control {
+ Auth-Type := PAP
+ }
+
+ #
+ # Stop processing "authorize", and go to the next section.
+ #
+ return
+
+ #
+ # Shouldn't reach this
+ #
+ update reply {
+ Filter-Id := "fail"
+ }
+ }
+
+ if ("%{Foreach-Variable-0}" == "4") {
+ update reply {
+ Filter-Id := "fail-4"
+ }
+ }
+}
diff --git a/src/tests/keywords/foreach-varied-depth b/src/tests/keywords/foreach-varied-depth
new file mode 100644
index 0000000..3c3918d
--- /dev/null
+++ b/src/tests/keywords/foreach-varied-depth
@@ -0,0 +1,43 @@
+update {
+ control:Tmp-String-0 := "ssid=ABCDEF"
+ control:Tmp-String-0 += "ssid=GHIJKL"
+ reply:Filter-Id := "filter"
+}
+
+if (User-Name) {
+ foreach &control:Tmp-String-0 {
+ if ("%{Foreach-Variable-0}" =~ /(.*)/) {
+ update control {
+ Tmp-String-1 := "%{1}"
+ }
+ }
+ }
+}
+
+if (&control:Tmp-String-1 != 'ssid=GHIJKL') {
+ update reply {
+ Filter-Id += 'fail 0'
+ }
+}
+
+update control {
+ Tmp-String-1 !* ANY
+}
+
+foreach &control:Tmp-String-0 {
+ if ("%{Foreach-Variable-0}" =~ /(.*)/) {
+ update control {
+ Tmp-String-1 := "%{1}"
+ }
+ }
+}
+
+if (&control:Tmp-String-1 != 'ssid=GHIJKL') {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+update control {
+ Tmp-String-1 !* ANY
+}
diff --git a/src/tests/keywords/foreach.attrs b/src/tests/keywords/foreach.attrs
new file mode 100644
index 0000000..26c2876
--- /dev/null
+++ b/src/tests/keywords/foreach.attrs
@@ -0,0 +1,18 @@
+#
+# Input packet
+#
+User-Name = "bob"
+User-Password = "hello"
+Filter-Id = "1"
+Filter-Id += "2"
+Filter-Id += "3"
+Filter-Id += "4"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Called-Station-Id == "1"
+Called-Station-Id == "2"
+Called-Station-Id == "3"
+Called-Station-Id == "4"
diff --git a/src/tests/keywords/hex b/src/tests/keywords/hex
new file mode 100644
index 0000000..b55bab2
--- /dev/null
+++ b/src/tests/keywords/hex
@@ -0,0 +1,141 @@
+#
+# PRE: update
+#
+update reply {
+ Filter-Id := "filter"
+}
+
+update request {
+ Tmp-String-0 := '9870'
+ Tmp-Octets-0 := 0x39383731
+ Tmp-IP-Address-0 := 57.56.55.50
+ Tmp-Date-0 := 959985459
+ Tmp-Integer-0 := 959985460
+ Tmp-Cast-Abinary := 'ip out forward srcip 57.56.55.53/32 udp dstport = 1812'
+ Tmp-Cast-IfId := '0000:0000:3938:3737'
+ Tmp-Cast-IPv6Addr := '::3938:3738'
+ Tmp-Cast-IPv6Prefix := '::3938:3739/128'
+ Tmp-Cast-Byte := 58
+ Tmp-Cast-Short := 14139
+ Tmp-Cast-Ethernet := 00:00:39:38:37:3c
+ Tmp-Cast-Integer64 := 1152921505566832445
+ Tmp-Cast-IPv4Prefix := 57.56.55.62/32
+}
+
+update request {
+ Tmp-String-0 := "%{hex:Tmp-String-0}"
+ Tmp-String-1 := "%{hex:Tmp-Octets-0}"
+ Tmp-String-2 := "%{hex:Tmp-IP-Address-0}"
+ Tmp-String-3 := "%{hex:Tmp-Date-0}"
+ Tmp-String-4 := "%{hex:Tmp-Integer-0}"
+ Tmp-String-5 := "%{hex:Tmp-Cast-Abinary}"
+ Tmp-String-6 := "%{hex:Tmp-Cast-Ifid}"
+ Tmp-String-7 := "%{hex:Tmp-Cast-IPv6Addr}"
+ Tmp-String-8 := "%{hex:Tmp-Cast-IPv6Prefix}"
+ Tmp-String-9 := "%{hex:Tmp-Cast-Byte}"
+}
+
+# String
+if (Tmp-String-0 != '39383730') {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+# Octets
+if (Tmp-String-1 != '39383731') {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+# IP Address
+if (Tmp-String-2 != '39383732') {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+# Date
+if (Tmp-String-3 != '39383733') {
+ update reply {
+ Filter-Id += 'fail 4'
+ }
+}
+
+# Integer
+if (Tmp-String-4 != '39383734') {
+ update reply {
+ Filter-Id += 'fail 5'
+ }
+}
+
+# Abinary
+if (Tmp-String-5 != '0101000039383735000000002000110000000714000200000000000000000000') {
+ update reply {
+ Filter-Id += 'fail 6'
+ }
+}
+
+# ifid
+if (Tmp-String-6 != '0000000039383737') {
+ update reply {
+ Filter-Id += 'fail 7'
+ }
+}
+
+# ipv6addr
+if (Tmp-String-7 != '00000000000000000000000039383738') {
+ update reply {
+ Filter-ID += 'fail 8'
+ }
+}
+
+# ipv6addrprefix
+if (Tmp-String-8 != '008000000000000000000000000039383739') {
+ update reply {
+ Filter-ID += 'fail 9'
+ }
+}
+
+# byte
+if (Tmp-String-9 != '3a') {
+ update reply {
+ Filter-ID += "fail 10 - expected 3a got %{Tmp-String-9}"
+ }
+}
+
+update request {
+ Tmp-String-0 := "%{hex:Tmp-Cast-Short}"
+ Tmp-String-1 := "%{hex:Tmp-Cast-Ethernet}"
+ Tmp-String-2 := "%{hex:Tmp-Cast-Integer64}"
+ Tmp-String-3 := "%{hex:Tmp-Cast-IPv4Prefix}"
+}
+
+# short
+if (Tmp-String-0 != '373b') {
+ update reply {
+ Filter-ID += 'fail 11'
+ }
+}
+
+# ethernet
+if (Tmp-String-1 != '00003938373c') {
+ update reply {
+ Filter-Id += 'fail 12'
+ }
+}
+
+# integer64
+if (Tmp-String-2 != '100000003938373d') {
+ update reply {
+ Filter-Id += 'fail 13'
+ }
+}
+
+# ipv4prefix
+if (Tmp-String-3 != '00203938373e') {
+ update reply {
+ Filter-Id += 'fail 14 expected 00203938373e got %{Tmp-String-3}'
+ }
+}
diff --git a/src/tests/keywords/if b/src/tests/keywords/if
new file mode 100644
index 0000000..a146029
--- /dev/null
+++ b/src/tests/keywords/if
@@ -0,0 +1,10 @@
+#
+# PRE: update
+#
+# Static if condition
+#
+if (1) {
+ update reply {
+ Filter-Id := "filter"
+ }
+}
diff --git a/src/tests/keywords/if-bob b/src/tests/keywords/if-bob
new file mode 100644
index 0000000..4e8ae3c
--- /dev/null
+++ b/src/tests/keywords/if-bob
@@ -0,0 +1,15 @@
+# PRE: if
+#
+# Matching "if" conditions
+#
+if (User-Name == "bob") {
+ update reply {
+ Filter-Id := "filter"
+ }
+}
+
+if (User-Name != "bob") {
+ update reply {
+ Filter-Id := "not bob"
+ }
+} \ No newline at end of file
diff --git a/src/tests/keywords/if-else b/src/tests/keywords/if-else
new file mode 100644
index 0000000..788d606
--- /dev/null
+++ b/src/tests/keywords/if-else
@@ -0,0 +1,15 @@
+#
+# PRE: if
+#
+# Matching "if" conditions
+#
+if (User-Name != "bob") {
+ update reply {
+ Filter-Id := "not bob"
+ }
+}
+else {
+ update reply {
+ Filter-Id := "filter"
+ }
+}
diff --git a/src/tests/keywords/if-elsif b/src/tests/keywords/if-elsif
new file mode 100644
index 0000000..c0a41ed
--- /dev/null
+++ b/src/tests/keywords/if-elsif
@@ -0,0 +1,19 @@
+# PRE: if if-else
+#
+# Matching "if" conditions
+#
+if (User-Name != "bob") {
+ update reply {
+ Filter-Id := "not bob"
+ }
+}
+elsif (User-Name == "bob") {
+ update reply {
+ Filter-Id := "filter"
+ }
+}
+else {
+ update reply {
+ Filter-Id := "last else"
+ }
+}
diff --git a/src/tests/keywords/if-multivalue b/src/tests/keywords/if-multivalue
new file mode 100644
index 0000000..f12d6fe
--- /dev/null
+++ b/src/tests/keywords/if-multivalue
@@ -0,0 +1,173 @@
+#
+# PRE: update if
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+update request {
+ Tmp-String-0 := 'foo'
+ Tmp-String-0 += 'bar'
+ Tmp-String-0 += 'baz'
+
+ Tmp-String-1 := 'GROUP ADMINISTRATORS'
+ Tmp-String-1 += 'GROUP STUDENTS'
+ Tmp-String-1 += 'GROUP PEONS'
+
+ Tmp-String-2 := 'PEONS'
+ Tmp-String-2 += 'STUDENTS'
+ Tmp-String-2 += 'ADMINISTRATORS'
+
+ Tmp-String-3 := 'no'
+ Tmp-String-3 += 'no'
+ Tmp-String-3 += 'yes'
+
+ Tmp-Integer-0 := 1
+ Tmp-Integer-0 += 2
+ Tmp-Integer-0 += 5
+}
+
+update control {
+ Tmp-String-0 := 'foo'
+ Tmp-String-0 += 'bar'
+ Tmp-String-0 += 'baz'
+
+ Tmp-String-1 := 'boink'
+ Tmp-String-1 += 'tard'
+ Tmp-String-1 += 'dink'
+ Tmp-String-1 += 'slink'
+
+ Tmp-Integer-0 := 01
+ Tmp-Integer-0 += 02
+ Tmp-Integer-0 += 05
+ Tmp-Integer-0 += 04
+
+ Tmp-Integer-1 := 10
+ Tmp-Integer-1 += 20
+ Tmp-Integer-1 += 30
+}
+
+#
+# Mmmm O(N^2)
+#
+if (&request:Tmp-String-0[*] != &control:Tmp-String-0[*]) {
+ update reply {
+ Filter-Id += 'fail 0'
+ }
+}
+
+if (&request:Tmp-String-0[*] == &control:Tmp-String-1[*]) {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+if (&request:Tmp-String-1[*] == &control:Tmp-String-0[*]) {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+#
+# Integer comparison and normalisation
+#
+if (&request:Tmp-Integer-0 != &control:Tmp-Integer-0) {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+#
+# if any value of request:Tmp-Integer-0 > any value of
+# request:Tmp-Integer-1 then evaluate to true
+#
+if (&request:Tmp-Integer-0[*] > &control:Tmp-Integer-1[*]) {
+ update reply {
+ Filter-Id += 'fail 4'
+ }
+}
+
+#
+# Compiled regex comparisons
+#
+if (&request:Tmp-String-1[*] !~ /PEONS$/) {
+ update reply {
+ Filter-Id += 'fail 5'
+ }
+}
+
+if (&control:Tmp-String-1 =~ /PEONS$/) {
+ update reply {
+ Filter-Id += 'fail 6'
+ }
+}
+
+if (&control:Tmp-String-1 =~ /DINKS$/) {
+ update reply {
+ Filter-Id += 'fail 7'
+ }
+}
+
+#
+# Dynamic regex comparisons
+#
+if (&request:Tmp-String-1[*] !~ /%{Tmp-String-2[0]}$/) {
+ update reply {
+ Filter-Id += 'fail 8'
+ }
+}
+
+if (&request:Tmp-String-1 =~ /%{Tmp-String-2[1]}$/) {
+ update reply {
+ Filter-Id += 'fail 9'
+ }
+}
+
+if (&request:Tmp-String-1 !~ /%{Tmp-String-2[2]}$/) {
+ update reply {
+ Filter-Id += 'fail 10'
+ }
+}
+
+if (&request:Tmp-String-1 =~ /%{Tmp-String-2[#]}$/) {
+ update reply {
+ Filter-Id += 'fail 11'
+ }
+}
+
+#
+# XLAT virtual comparisons
+#
+if (&control:Tmp-Integer-0[*] != "%{control:Tmp-Integer-0[#]}") {
+ update reply {
+ Filter-Id += 'fail 12'
+ }
+}
+
+#
+# Literal comparisons
+#
+if (&control:Tmp-String-1[*] != 'boink') {
+ update reply {
+ Filter-Id += 'fail 13'
+ }
+}
+
+if (&control:Tmp-String-1[*] == 'foo') {
+ update reply {
+ Filter-Id += 'fail 14'
+ }
+}
+
+if (&request:Tmp-Integer-0[*] > 10) {
+ update reply {
+ Filter-Id += 'fail 15'
+ }
+}
+
+if (!(&request:Tmp-Integer-0[*] < 10)) {
+ update reply {
+ Filter-Id += 'fail 16'
+ }
+}
diff --git a/src/tests/keywords/if-paircmp b/src/tests/keywords/if-paircmp
new file mode 100644
index 0000000..6ed06e3
--- /dev/null
+++ b/src/tests/keywords/if-paircmp
@@ -0,0 +1,27 @@
+#
+# PRE: update if
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+#
+# Paircmp
+#
+
+#
+# Passing 'yes' causes the test paircmp to return match
+# Passing 'no' causes the test paircmp to return a non-match
+#
+if (&Test-Paircmp != 'yes') {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+if (&Test-Paircmp == 'no') {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
diff --git a/src/tests/keywords/if-rcode-error b/src/tests/keywords/if-rcode-error
new file mode 100644
index 0000000..fed8a49
--- /dev/null
+++ b/src/tests/keywords/if-rcode-error
@@ -0,0 +1,11 @@
+# PRE: if
+#
+# return code in an "if" section.
+#
+if (User-Name == "bob") {
+ update reply {
+ Filter-Id := "filter"
+ }
+
+ ok = reject # ERROR
+}
diff --git a/src/tests/keywords/if-regex-bad-attribute b/src/tests/keywords/if-regex-bad-attribute
new file mode 100644
index 0000000..f330fde
--- /dev/null
+++ b/src/tests/keywords/if-regex-bad-attribute
@@ -0,0 +1,21 @@
+#
+# PRE: if-regex-match if-regex-error
+#
+
+#
+# This should parse
+#
+if (&User-Name =~ /%{User-Name}/) {
+ update reply {
+ Filter-Id := "filter"
+ }
+}
+
+#
+# Check regexes which refer to unknown attributes
+#
+if (&User-Name =~ /%{What-The-Heck-Is-This-Thing}/) { # ERROR
+ update reply {
+ Filter-Id := "filter"
+ }
+}
diff --git a/src/tests/keywords/if-regex-error b/src/tests/keywords/if-regex-error
new file mode 100644
index 0000000..f618e82
--- /dev/null
+++ b/src/tests/keywords/if-regex-error
@@ -0,0 +1,12 @@
+#
+# PRE: if-regex-match
+#
+
+#
+# Check that bad regular expressions will fail
+#
+if (&User-Name =~ /[a-3]/) { # ERROR
+ update reply {
+ Filter-Id := "filter"
+ }
+}
diff --git a/src/tests/keywords/if-regex-match b/src/tests/keywords/if-regex-match
new file mode 100644
index 0000000..458e455
--- /dev/null
+++ b/src/tests/keywords/if-regex-match
@@ -0,0 +1,183 @@
+# PRE: if
+#
+update request {
+ Tmp-Integer-0 := '123456789'
+}
+
+# Non matching on attribute ref
+if (User-Name !~ /^([0-9])_([0-9])?_([0-9]*)_([0-9]+)_([^_])_(6)_([7-8])%{Tmp-String-0}/) {
+ update reply {
+ Filter-Id += 'Fail 0'
+ }
+}
+
+# Matching on xlat expanded value
+if ("%{User-Name}" !~ /^([0-9])_([0-9])?_([0-9]*)_([0-9]+)_([^_])_(6)_([7-8])%{Tmp-String-0}/) {
+ update reply {
+ Filter-Id += 'Fail 1'
+ }
+}
+
+# Matching on attribute ref with capture groups
+if (User-Name =~ /^([0-9])_([0-9])?_([0-9]*)_([0-9]+)_([^_])_(6)_([7-8])%{Tmp-String-0}/) {
+ # Test all the capture groups
+ update {
+ reply:User-Name := "%{7}_%{6}_%{5}_%{4}_%{3}_%{2}_%{1}_%{0}"
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 2'
+ }
+}
+
+# Checking capture groups are cleared out correctly
+if (User-Name =~ /^([0-9])_%{Tmp-String-0}/) {
+ if ("%{0}%{1}%{2}%{3}%{4}%{5}%{6}%{7}" != '1_1') {
+ update reply {
+ Filter-Id += 'Fail 3'
+ }
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 3.5'
+ }
+}
+
+# Checking capture groups are cleared out correctly when there are no matches
+if (User-Name =~ /^.%{Tmp-String-0}/) {
+ if ("%{0}%{1}%{2}%{3}%{4}%{5}%{6}%{7}" != '1') {
+ update reply {
+ Filter-Id += 'Fail 4'
+ }
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 4.5'
+ }
+}
+
+# Checking full capture group range
+if ('a_b_c_d_e_f_g_h_i_j_k_l_m_n_o_p_q_r_s_t_u_v_w_x_y_z_A_B_C_D_E_F' =~ /^(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)$/) {
+ if ("%{0}%{1}%{2}%{3}%{4}%{5}%{6}%{7}%{8}%{9}%{10}%{11}%{12}%{13}%{14}%{15}%{16}%{17}%{18}%{19}%{20}%{21}%{22}%{23}%{24}%{25}%{26}%{27}%{28}%{29}%{30}%{31}%{32}" != 'a_b_c_d_e_f_g_h_i_j_k_l_m_n_o_p_q_r_s_t_u_v_w_x_y_z_A_B_C_D_E_FabcdefghijklmnopqrstuvwxyzABCDEF') {
+ update reply {
+ Filter-Id += 'Fail 6'
+ }
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 6.5'
+ }
+}
+
+# Checking full capture group overun
+if ('a_b_c_d_e_f_g_h_i_j_k_l_m_n_o_p_q_r_s_t_u_v_w_x_y_z_A_B_C_D_E_F_G' =~ /^(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)_(.)$/) {
+ if ("%{0}%{1}%{2}%{3}%{4}%{5}%{6}%{7}%{8}%{9}%{10}%{11}%{12}%{13}%{14}%{15}%{16}%{17}%{18}%{19}%{20}%{21}%{22}%{23}%{24}%{25}%{26}%{27}%{28}%{29}%{30}%{31}%{32}" != 'a_b_c_d_e_f_g_h_i_j_k_l_m_n_o_p_q_r_s_t_u_v_w_x_y_z_A_B_C_D_E_F_GabcdefghijklmnopqrstuvwxyzABCDEF') {
+ update reply {
+ Filter-Id += 'Fail 7'
+ }
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 7.5'
+ }
+}
+
+# uncompiled - ref - insensitive
+if (Calling-Station-Id !~ /:roamyroam%{Tmp-String-0}$/i) {
+ update reply {
+ Filter-Id += 'Fail 8'
+ }
+}
+
+# uncompiled - expansion - insensitive
+if ("%{Calling-Station-Id}" !~ /:roamyroam%{Tmp-String-0}$/i) {
+ update reply {
+ Filter-Id += 'Fail 9'
+ }
+}
+
+# uncompiled - enum - ref - insensitive
+if (Service-Type !~ /^framed-user%{Tmp-String-0}$/i) {
+ update reply {
+ Filter-Id += 'Fail 10'
+ }
+}
+
+# uncompiled - enum - expansion - insensitive
+if ("%{Service-Type}" !~ /^framed-user%{Tmp-String-0}$/i) {
+ update reply {
+ Filter-Id += 'Fail 11'
+ }
+}
+
+# uncompiled - enum - ref
+if (Service-Type =~ /^framed-user%{Tmp-String-0}$/) {
+ update reply {
+ Filter-Id += 'Fail 12'
+ }
+}
+
+# uncompiled - integer - ref
+if (Tmp-Integer-0 !~ /%{Tmp-Integer-0}/) {
+ update reply {
+ Filter-Id += 'Fail 13'
+ }
+}
+
+update request {
+ Tmp-String-0 := "foo\nbar"
+}
+
+# uncompiled - ref - multiline
+if (&Tmp-String-0 !~ /^foo$%{Tmp-String-8}/m) {
+ update reply {
+ Filter-Id += 'Fail 14'
+ }
+}
+
+# uncompiled - ref - non-multiline
+if (&Tmp-String-0 =~ /^foo$%{Tmp-String-8}/) {
+ update reply {
+ Filter-Id += 'Fail 15'
+ }
+}
+
+# uncompiled - ref - non-multiline
+if (&Tmp-String-0 !~ /^foo\nbar%{Tmp-String-8}$/) {
+ update reply {
+ Filter-Id += 'Fail 16'
+ }
+}
+
+# uncompiled - ref - multiline
+if (&Tmp-String-0 !~ /^bar%{Tmp-String-8}$/m) {
+ update reply {
+ Filter-Id += 'Fail 17'
+ }
+}
+
+# uncompiled - ref - multiline - sensitive
+if (&Tmp-String-0 =~ /^BAR%{Tmp-String-8}$/m) {
+ update reply {
+ Filter-Id += 'Fail 18'
+ }
+}
+
+# uncompiled - ref - multiline - insensitive
+if (&Tmp-String-0 !~ /^BAR%{Tmp-String-8}$/mi) {
+ update reply {
+ Filter-Id += 'Fail 19'
+ }
+}
+
+# uncompiled - ref - multiline - insensitive (flag order reversed)
+if (&Tmp-String-0 !~ /^BAR%{Tmp-String-8}$/im) {
+ update reply {
+ Filter-Id += 'Fail 20'
+ }
+}
diff --git a/src/tests/keywords/if-regex-match-comp b/src/tests/keywords/if-regex-match-comp
new file mode 100644
index 0000000..c9c2d15
--- /dev/null
+++ b/src/tests/keywords/if-regex-match-comp
@@ -0,0 +1,149 @@
+# PRE: if
+#
+
+# Non matching on attribute ref
+if (User-Name !~ /^([0-9])_([0-9])?_([0-9]*)_([0-9]+)_([^_])_(6)_([7-8])/) {
+ update reply {
+ Filter-Id += 'Fail 0'
+ }
+}
+
+# Matching on xlat expanded value
+if ("%{User-Name}" !~ /^([0-9])_([0-9])?_([0-9]*)_([0-9]+)_([^_])_(6)_([7-8])/) {
+ update reply {
+ Filter-Id += 'Fail 1'
+ }
+}
+
+# Matching on attribute ref with capture groups
+if (User-Name =~ /^([0-9])_([0-9])?_([0-9]*)_([0-9]+)_([^_])_(6)_([7-8])/) {
+ # Test all the capture groups
+ update {
+ reply:User-Name := "%{7}_%{6}_%{5}_%{4}_%{3}_%{2}_%{1}_%{0}"
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 2'
+ }
+}
+
+# Checking capture groups are cleared out correctly
+if (User-Name =~ /^([0-9])_/) {
+ if ("%{0}%{1}%{2}%{3}%{4}%{5}%{6}%{7}" != '1_1') {
+ update reply {
+ Filter-Id += 'Fail 3'
+ }
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 3.5'
+ }
+}
+
+# Checking capture groups are cleared out correctly when there are no matches
+if (User-Name =~ /^./) {
+ if ("%{0}%{1}%{2}%{3}%{4}%{5}%{6}%{7}" != '1') {
+ update reply {
+ Filter-Id += 'Fail 4'
+ }
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 4.5'
+ }
+}
+
+# compiled - ref - insensitive
+if (Calling-Station-Id !~ /:roamyroam$/i) {
+ update reply {
+ Filter-Id += 'Fail 5'
+ }
+}
+
+# compiled - expansion - insensitive
+if ("%{Calling-Station-Id}" !~ /:roamyroam$/i) {
+ update reply {
+ Filter-Id += 'Fail 6'
+ }
+}
+
+# compiled - enum - ref - insensitive
+if (Service-Type !~ /^framed-user$/i) {
+ update reply {
+ Filter-Id += 'Fail 7'
+ }
+}
+
+# compiled - enum - expansion - insensitive
+if ("%{Service-Type}" !~ /^framed-user$/i) {
+ update reply {
+ Filter-Id += 'Fail 8'
+ }
+}
+
+# compiled - enum - ref
+if (Service-Type =~ /^framed-user$/) {
+ update reply {
+ Filter-Id += 'Fail 9'
+ }
+}
+
+update request {
+ Tmp-String-0 := "foo\nbar"
+}
+
+# compiled - ref - multiline
+if (&Tmp-String-0 !~ /^foo$/m) {
+ update reply {
+ Filter-Id += 'Fail 14'
+ }
+}
+
+# compiled - ref - non-multiline
+if (&Tmp-String-0 =~ /^foo$/) {
+ update reply {
+ Filter-Id += 'Fail 15'
+ }
+}
+
+# compiled - ref - non-multiline
+
+# Not all POSIX implementations support the \n character classes
+# so only run this test if the server was built with libpcre.
+if (("${feature.regex-pcre}" == 'yes') && (&Tmp-String-0 !~ /^foo\nbar$/)) {
+ update reply {
+ Filter-Id += 'Fail 16'
+ }
+}
+
+# compiled - ref - multiline
+if (&Tmp-String-0 !~ /^bar$/m) {
+ update reply {
+ Filter-Id += 'Fail 17'
+ }
+}
+
+# compiled - ref - multiline - sensitive
+if (&Tmp-String-0 =~ /^BAR$/m) {
+ update reply {
+ Filter-Id += 'Fail 17'
+ }
+}
+
+# compiled - ref - multiline - insensitive
+if (&Tmp-String-0 !~ /^BAR$/mi) {
+ update reply {
+ Filter-Id += 'Fail 17'
+ }
+}
+
+# compiled - ref - multiline - insensitive (flag order reversed)
+if (&Tmp-String-0 !~ /^BAR$/im) {
+ update reply {
+ Filter-Id += 'Fail 18'
+ }
+}
+
diff --git a/src/tests/keywords/if-regex-match-comp.attrs b/src/tests/keywords/if-regex-match-comp.attrs
new file mode 100644
index 0000000..ba7188d
--- /dev/null
+++ b/src/tests/keywords/if-regex-match-comp.attrs
@@ -0,0 +1,7 @@
+User-Name = '1_2_3_4_5_6_7'
+User-Password = 'hello'
+Service-Type := 'Framed-User'
+Calling-Station-ID := '00:11:22:33:44:55:66:ROAMYROAM'
+
+Response-Packet-Type == Access-Accept
+User-Name == '7_6_5_4_3_2_1_1_2_3_4_5_6_7'
diff --git a/src/tests/keywords/if-regex-match-named b/src/tests/keywords/if-regex-match-named
new file mode 100644
index 0000000..2aa665f
--- /dev/null
+++ b/src/tests/keywords/if-regex-match-named
@@ -0,0 +1,117 @@
+# PRE: if
+#
+if ('${feature.regex-pcre}' == 'yes') {
+update request {
+ Tmp-Integer-0 := '123456789'
+ Tmp-Integer-1 := 1
+}
+
+# Check failures when no previous capture
+if ("%{regex:}" != "") {
+ update reply {
+ Filter-Id += 'Fail 0.1'
+ }
+}
+
+if ("%{regex:foo}" != "") {
+ update reply {
+ Filter-Id += 'Fail 0.2'
+ }
+}
+
+if ("%{regex:%{Tmp-Integer-1}}" != "") {
+ update reply {
+ Filter-Id += 'Fail 0.3'
+ }
+}
+
+if ("%{regex:1}" != "") {
+ update reply {
+ Filter-Id += 'Fail 0.4'
+ }
+}
+
+# uncompiled - ref - named capture groups
+if (User-Name =~ /^(?<one>[0-9])_(?<two>[0-9])?_(?<three>[0-9]*)_(?<four>[0-9]+)_(?<five>[^_])_(?<six>6)_(?<seven>[7-8])%{Tmp-String-0}/) {
+ if ("%{regex:seven}_%{regex:six}_%{regex:five}_%{regex:four}_%{regex:three}_%{regex:two}_%{regex:one}_%{0}" != '7_6_5_4_3_2_1_1_2_3_4_5_6_7') {
+ update reply {
+ Filter-Id += 'Fail 1'
+ }
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 1.5'
+ }
+}
+
+# Checking capture groups are cleared out correctly
+if (User-Name =~ /^(?<one>[0-9])_%{Tmp-String-0}/) {
+ if ("%{0}%{regex:one}%{regex:two}%{regex:three}%{regex:four}%{regex:five}%{regex:six}%{regex:seven}" != '1_1') {
+ update reply {
+ Filter-Id += 'Fail 2'
+ }
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 2.5'
+ }
+}
+
+# Checking capture groups are cleared out correctly when there are no matches
+if (User-Name =~ /^.%{Tmp-String-0}/) {
+ if ("%{0}%{regex:one}%{regex:two}%{regex:three}%{regex:four}%{regex:five}%{regex:six}%{regex:seven}" != '1') {
+ update reply {
+ Filter-Id += 'Fail 3'
+ }
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 3.5'
+ }
+}
+
+# compiled - ref - named capture groups
+if (User-Name =~ /^(?<one>[0-9])_(?<two>[0-9])?_(?<three>[0-9]*)_(?<four>[0-9]+)_(?<five>[^_])_(?<six>6)_(?<seven>[7-8])/) {
+ if ("%{regex:seven}_%{regex:six}_%{regex:five}_%{regex:four}_%{regex:three}_%{regex:two}_%{regex:one}_%{0}" != '7_6_5_4_3_2_1_1_2_3_4_5_6_7') {
+ update reply {
+ Filter-Id += 'Fail 4'
+ }
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 4.5'
+ }
+}
+
+# compiled - xlat - named capture groups
+if ('1_2_3_4_5_6_7' =~ /^(?<one>[0-9])_(?<two>[0-9])?_(?<three>[0-9]*)_(?<four>[0-9]+)_(?<five>[^_])_(?<six>6)_(?<seven>[7-8])/) {
+ if ("%{regex:seven}_%{regex:six}_%{regex:five}_%{regex:four}_%{regex:three}_%{regex:two}_%{regex:one}_%{0}" != '7_6_5_4_3_2_1_1_2_3_4_5_6_7') {
+ update reply {
+ Filter-Id += 'Fail 5'
+ }
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 5.5'
+ }
+}
+
+# compiled - ref - named capture groups (numeric indexes)
+if (User-Name =~ /^(?<one>[0-9])_(?<two>[0-9])?_(?<three>[0-9]*)_(?<four>[0-9]+)_(?<five>[^_])_(?<six>6)_(?<seven>[7-8])/) {
+ if ("%{7}_%{6}_%{5}_%{4}_%{3}_%{2}_%{1}_%{0}" != '7_6_5_4_3_2_1_1_2_3_4_5_6_7') {
+ update reply {
+ Filter-Id += 'Fail 4'
+ }
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 4.5'
+ }
+}
+}
diff --git a/src/tests/keywords/if-regex-match-named.attrs b/src/tests/keywords/if-regex-match-named.attrs
new file mode 100644
index 0000000..867ed23
--- /dev/null
+++ b/src/tests/keywords/if-regex-match-named.attrs
@@ -0,0 +1,6 @@
+User-Name = '1_2_3_4_5_6_7'
+User-Password = 'hello'
+Service-Type := 'Framed-User'
+Calling-Station-ID := '00:11:22:33:44:55:66:ROAMYROAM'
+
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/keywords/if-regex-match.attrs b/src/tests/keywords/if-regex-match.attrs
new file mode 100644
index 0000000..ba7188d
--- /dev/null
+++ b/src/tests/keywords/if-regex-match.attrs
@@ -0,0 +1,7 @@
+User-Name = '1_2_3_4_5_6_7'
+User-Password = 'hello'
+Service-Type := 'Framed-User'
+Calling-Station-ID := '00:11:22:33:44:55:66:ROAMYROAM'
+
+Response-Packet-Type == Access-Accept
+User-Name == '7_6_5_4_3_2_1_1_2_3_4_5_6_7'
diff --git a/src/tests/keywords/if-regex-multivalue b/src/tests/keywords/if-regex-multivalue
new file mode 100644
index 0000000..7358c93
--- /dev/null
+++ b/src/tests/keywords/if-regex-multivalue
@@ -0,0 +1,26 @@
+#
+# PRE: update if
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+update request {
+ Cisco-AVPair := 'foo=bar'
+ Cisco-AVPair += 'bar=baz'
+ Cisco-AVPair += 'baz=foo'
+}
+
+if (&Cisco-AVPair[*] =~ /bar=(.*)/) {
+ if ("%{1}" != 'baz') {
+ update reply {
+ Filter-Id += 'Fail 1'
+ }
+ }
+}
+else {
+ update reply {
+ Filter-Id += 'Fail 2'
+ }
+}
diff --git a/src/tests/keywords/if-skip b/src/tests/keywords/if-skip
new file mode 100644
index 0000000..0e74f22
--- /dev/null
+++ b/src/tests/keywords/if-skip
@@ -0,0 +1,42 @@
+# PRE: if
+#
+# Conditions which statically evaluate to "false"
+# have their entire contents skipped on load.
+#
+# Conditions which statically evaluate to "true"
+# have the following "else" sections skipped, too.
+#
+# i.e. we can reference things which don't exist,
+# and they'll get ignored.
+#
+if (0) {
+ no-such-module
+}
+
+if (0) {
+ no-such-module
+}
+else {
+ ok
+}
+
+if (1) {
+ ok
+}
+else {
+ no-such-module
+}
+
+if (1) {
+ ok
+}
+elsif ("%{foo:bar}") { # no pass2
+ no-such-module
+}
+else {
+ no-such-module
+}
+
+update reply {
+ Filter-Id := "filter"
+}
diff --git a/src/tests/keywords/integer b/src/tests/keywords/integer
new file mode 100644
index 0000000..7e43270
--- /dev/null
+++ b/src/tests/keywords/integer
@@ -0,0 +1,209 @@
+#
+# PRE: update
+#
+update reply {
+ Filter-Id := "filter"
+}
+
+update request {
+ Tmp-String-0 := '9870'
+ Tmp-String-1 := '98709870'
+ Tmp-String-2 := '987098709870'
+ Tmp-Octets-0 := 0x39383731
+ Tmp-Octets-1 := 0x3938373139383731
+ Tmp-Octets-2 := 0x393837313938373139383731
+ Tmp-IP-Address-0 := 57.56.55.50
+ Tmp-Date-0 := 959985459
+ Tmp-Integer-0 := 959985460
+ Tmp-Cast-Abinary := 'ip out forward srcip 57.56.55.53/32 udp dstport = 1812'
+ Tmp-Cast-IfId := '0000:0000:3938:3737'
+ Tmp-Cast-IPv6Addr := '::3938:3738'
+ Tmp-Cast-IPv6Prefix := '::3938:3739/128'
+ Tmp-Cast-Byte := 58
+ Tmp-Cast-Short := 14139
+ Tmp-Cast-Ethernet := 00:00:39:38:37:3c
+ Tmp-Cast-Integer64 := 1152921505566832445
+ Tmp-Cast-IPv4Prefix := 57.56.55.62/32
+}
+
+update request {
+ Tmp-String-2 := "%{integer:Tmp-IP-Address-0}"
+ Tmp-String-3 := "%{integer:Tmp-Date-0}"
+ Tmp-String-4 := "%{integer:Tmp-Integer-0}"
+ Tmp-String-5 := "%{integer:Tmp-Cast-Abinary}"
+ Tmp-String-6 := "%{integer:Tmp-Cast-Ifid}"
+ Tmp-String-7 := "%{integer:Tmp-Cast-IPv6Addr}"
+ Tmp-String-8 := "%{integer:Tmp-Cast-IPv6Prefix}"
+}
+
+# String - network order representation of a 4 char string
+update request {
+ Tmp-Integer-0 := "%{integer:Tmp-String-0}"
+}
+if ((Tmp-String-0 != "%{string:Tmp-Integer-0}") || (Tmp-Integer-0 != 959985456)) {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+# String - network order representation of a 8 char string
+update request {
+ Tmp-Integer64-0 := "%{integer:Tmp-String-1}"
+}
+if ((Tmp-String-1 != "%{string:Tmp-Integer64-0}") || (Tmp-Integer64-0 != 4123106139115632432)) {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+# String - Can't convert 12 byte string to integer (our biggest native size is a 64bit unsigned int)
+if ("%{integer:Tmp-String-2}" != '') {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+# Octets - network order representation of a 4 byte octet string
+update request {
+ Tmp-Integer-0 := "%{integer:Tmp-Octets-0}"
+}
+if (Tmp-Octets-0 != "0x%{hex:Tmp-Integer-0}") {
+ update reply {
+ Filter-Id += 'fail 4a'
+ }
+}
+
+if (Tmp-Integer-0 != 959985457) {
+ update reply {
+ Filter-Id += 'fail 4b'
+ }
+}
+
+# Octets - network order representation of a 8 byte octet string
+update request {
+ Tmp-Integer64-0 := "%{integer:Tmp-Octets-1}"
+}
+if (Tmp-Octets-1 != "0x%{hex:Tmp-Integer64-0}") {
+ update reply {
+ Filter-Id += 'fail 5a'
+ }
+}
+
+if (Tmp-Integer64-0 != 4123106143410599729) {
+ update reply {
+ Filter-Id += 'fail 5b'
+ }
+}
+
+# String - Can't convert 12 byte octet string to integer (our biggest native size is a 64bit unsigned int)
+if ("%{integer:Tmp-Octets-2}" != '') {
+ update reply {
+ Filter-Id += 'fail 6'
+ }
+}
+
+# IP Address
+if (Tmp-String-2 != '959985458') {
+ update reply {
+ Filter-Id += 'fail 7'
+ }
+}
+
+if (<ipaddr>Tmp-String-2 != &Tmp-IP-Address-0) {
+ update reply {
+ Filter-Id += 'fail 8'
+ }
+}
+
+# Date
+if (Tmp-String-3 != '959985459') {
+ update reply {
+ Filter-Id += 'fail 9'
+ }
+}
+
+# Integer
+if (Tmp-String-4 != '959985460') {
+ update reply {
+ Filter-Id += 'fail 10'
+ }
+}
+
+# Abinary - Can't convert ascend binary to an integer
+if (Tmp-String-5 != '') {
+ update reply {
+ Filter-Id += 'fail 11'
+ }
+}
+
+# ifid - Can't convert interface ID to an integer
+if (Tmp-String-6 != '') {
+ update reply {
+ Filter-Id += 'fail 12'
+ }
+}
+
+# ipv6addr - Can't convert IPv6 to integer
+if (Tmp-String-7 != '959985464') {
+ update reply {
+ Filter-ID += 'fail 13'
+ }
+}
+
+# ipv6addrprefix
+if (Tmp-String-8 != '959985465') {
+ update reply {
+ Filter-ID += 'fail 14'
+ }
+}
+update request {
+ Tmp-String-0 := "%{integer:Tmp-Cast-Byte}"
+ Tmp-String-1 := "%{integer:Tmp-Cast-Short}"
+ Tmp-String-2 := "%{integer:Tmp-Cast-Ethernet}"
+ Tmp-String-3 := "%{integer:Tmp-Cast-Integer64}"
+ Tmp-String-4 := "%{integer:Tmp-Cast-IPv4Prefix}"
+}
+
+# byte
+if (Tmp-String-0 != '58') {
+ update reply {
+ Filter-ID += 'fail 15'
+ }
+}
+
+# short
+if (Tmp-String-1 != '14139') {
+ update reply {
+ Filter-ID += 'fail 16'
+ }
+}
+
+# ethernet
+if (Tmp-String-2 != '62913607630848') {
+ update reply {
+ Filter-Id += 'fail 17'
+ }
+}
+if (<ether>Tmp-String-2 != &Tmp-Cast-Ethernet) {
+ update reply {
+ Filter-Id += 'fail 18'
+ }
+}
+
+# integer64
+if (Tmp-String-3 != '1152921505566832445') {
+ update reply {
+ Filter-Id += 'fail 19'
+ }
+}
+
+# ipv4prefix
+if (Tmp-String-4 != '959985470') {
+ update reply {
+ Filter-Id += 'fail 20'
+ }
+}
+
+
+
+
diff --git a/src/tests/keywords/ipaddr b/src/tests/keywords/ipaddr
new file mode 100644
index 0000000..3010a23
--- /dev/null
+++ b/src/tests/keywords/ipaddr
@@ -0,0 +1,51 @@
+#
+# PRE: update if
+#
+update control {
+ Cleartext-Password := 'hello'
+ reply:Filter-Id := "filter"
+}
+
+update request {
+ # Dotted Quad
+ Tmp-IP-Address-0 := 127.0.0.1
+
+ # Dotted Quad with prefix
+ Tmp-IP-Address-1 := 127.0.0.2/32
+
+ # Hex (0x)
+ Tmp-IP-Address-2 := 0x7f000003
+
+ # Decimal
+ Tmp-IP-Address-3 := 2130706436
+}
+
+if (NAS-IP-Address != 127.0.0.1) {
+ update reply {
+ Filter-Id += "fail 1"
+ }
+}
+
+if (Tmp-IP-Address-0 != 127.0.0.1) {
+ update reply {
+ Filter-Id += "fail 2"
+ }
+}
+
+if (Tmp-IP-Address-1 != 127.0.0.2) {
+ update reply {
+ Filter-Id += "fail 3"
+ }
+}
+
+if (Tmp-IP-Address-2 != 127.0.0.3) {
+ update reply {
+ Filter-Id += "fail 4"
+ }
+}
+
+if (Tmp-IP-Address-3 != 127.0.0.4) {
+ update reply {
+ Filter-Id += "fail 5"
+ }
+}
diff --git a/src/tests/keywords/ipaddr-error b/src/tests/keywords/ipaddr-error
new file mode 100644
index 0000000..5483a4f
--- /dev/null
+++ b/src/tests/keywords/ipaddr-error
@@ -0,0 +1,10 @@
+#
+# PRE: update ipaddr
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update reply {
+ Framed-IP-Address := 256.1 # ERROR
+}
diff --git a/src/tests/keywords/ipaddr.attrs b/src/tests/keywords/ipaddr.attrs
new file mode 100644
index 0000000..ab9c27e
--- /dev/null
+++ b/src/tests/keywords/ipaddr.attrs
@@ -0,0 +1,12 @@
+#
+# Input packet
+#
+User-Name = "bob"
+User-Password = "hello"
+NAS-IP-Address = 127.0.0.1
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Filter-Id == 'filter'
diff --git a/src/tests/keywords/ipprefix b/src/tests/keywords/ipprefix
new file mode 100644
index 0000000..0ab8dce
--- /dev/null
+++ b/src/tests/keywords/ipprefix
@@ -0,0 +1,52 @@
+#
+# PRE: update if
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update reply {
+ Filter-Id := "filter"
+}
+
+update control {
+ Tmp-Cast-IPv4Prefix := 198.51.100.255/16
+ Tmp-Cast-IPv6Prefix := ::198.51.100.255/112
+ Framed-IP-Address := 198.51.0.1
+}
+
+if ("%{control:Tmp-Cast-IPv6Prefix}" != '::198.51.0.0/112') {
+ update reply {
+ Filter-Id += "Fail 0"
+ }
+}
+
+if ("%{control:Tmp-Cast-IPv4Prefix}" != '198.51.0.0/16') {
+ update reply {
+ Filter-Id += "Fail 1"
+ }
+}
+
+if (control:Tmp-Cast-IPv6Prefix != ::198.51.0.0/112) {
+ update reply {
+ Filter-Id += "Fail 2"
+ }
+}
+
+if (control:Tmp-Cast-IPv4Prefix != 198.51.0.0/16) {
+ update reply {
+ Filter-Id += "Fail 3"
+ }
+}
+
+if (!(&control:Tmp-Cast-IPv4Prefix < 198.0.0.0/8)) {
+ update reply {
+ Filter-Id += "Fail 4"
+ }
+}
+
+if (!(&control:Framed-IP-Address < 198.51.0.0/16)) {
+ update reply {
+ Filter-Id += "Fail 5"
+ }
+}
diff --git a/src/tests/keywords/length b/src/tests/keywords/length
new file mode 100644
index 0000000..ad37fc8
--- /dev/null
+++ b/src/tests/keywords/length
@@ -0,0 +1,155 @@
+#
+# PRE: hex
+#
+update reply {
+ Filter-Id := "filter"
+}
+
+update request {
+ Tmp-String-0 := '\
+abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\
+abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\
+abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'
+ Tmp-String-2 := '9870'
+ Tmp-Octets-0 := 0x39383731
+ Tmp-IP-Address-0 := 57.56.55.50
+ Tmp-Date-0 := 959985459
+ Tmp-Integer-0 := 959985460
+ Tmp-Cast-Abinary := 'ip out forward srcip 57.56.55.53/32 udp dstport = 1812'
+ Tmp-Cast-IfId := '0000:0000:3938:3737'
+ Tmp-Cast-IPv6Addr := '::3938:3738'
+ Tmp-Cast-IPv6Prefix := '::3938:3739/128'
+ Tmp-Cast-Byte := 58
+ Tmp-Cast-Short := 14139
+ Tmp-Cast-Ethernet := 00:00:39:38:37:3c
+ Tmp-Cast-Integer64 := 1152921505566832445
+ Tmp-Cast-IPv4Prefix := 57.56.55.62/32
+}
+
+update request {
+ Tmp-Integer-0 := "%{length:Tmp-String-0}"
+}
+
+if (Tmp-Integer-0 != 260) {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+update request {
+ Tmp-Integer-0 := "%{length:Tmp-String-2}"
+ Tmp-Integer-1 := "%{length:Tmp-Octets-0}"
+ Tmp-Integer-2 := "%{length:Tmp-IP-Address-0}"
+ Tmp-Integer-3 := "%{length:Tmp-Date-0}"
+ Tmp-Integer-4 := "%{length:Tmp-Integer-0}"
+ Tmp-Integer-5 := "%{length:Tmp-Cast-Abinary}"
+ Tmp-Integer-6 := "%{length:Tmp-Cast-Ifid}"
+ Tmp-Integer-7 := "%{length:Tmp-Cast-IPv6Addr}"
+ Tmp-Integer-8 := "%{length:Tmp-Cast-IPv6Prefix}"
+ Tmp-Integer-9 := "%{length:Tmp-Cast-Byte}"
+}
+
+# String - bin 0x39383730
+if (Tmp-Integer-0 != 4) {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+# Octets - bin 0x39383731
+if (Tmp-Integer-1 != 4) {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+# IP Address - bin 0x39383732
+if (Tmp-Integer-2 != 4) {
+ update reply {
+ Filter-Id += 'fail 4'
+ }
+}
+
+# Date - bin 0x39383733
+if (Tmp-Integer-3 != 4) {
+ update reply {
+ Filter-Id += 'fail 5'
+ }
+}
+
+# Integer - bin 0x39383734
+if (Tmp-Integer-4 != 4) {
+ update reply {
+ Filter-Id += 'fail 6'
+ }
+}
+
+# Abinary - bin 0x0101000039383735000000002000110000000714000200000000000000000000
+if (Tmp-Integer-5 != 32) {
+ update reply {
+ Filter-Id += 'fail 7'
+ }
+}
+
+# ifid - bin 0x0000000039383737
+if (Tmp-Integer-6 != 8) {
+ update reply {
+ Filter-Id += 'fail 8'
+ }
+}
+
+# ipv6addr - bin 0x00000000000000000000000039383738
+if (Tmp-Integer-7 != 16) {
+ update reply {
+ Filter-ID += 'fail 9'
+ }
+}
+
+# ipv6addrprefix - bin 0x008000000000000000000000000039383739
+if (Tmp-Integer-8 != 18) {
+ update reply {
+ Filter-ID += 'fail 10'
+ }
+}
+
+# byte - bin 0x3a
+if (Tmp-Integer-9 != 1) {
+ update reply {
+ Filter-ID += 'fail 11'
+ }
+}
+
+update request {
+ Tmp-Integer-0 := "%{length:Tmp-Cast-Short}"
+ Tmp-Integer-1 := "%{length:Tmp-Cast-Ethernet}"
+ Tmp-Integer-2 := "%{length:Tmp-Cast-Integer64}"
+ Tmp-Integer-3 := "%{length:Tmp-Cast-IPv4Prefix}"
+}
+
+# short - bin 0x373b
+if (Tmp-Integer-0 != 2) {
+ update reply {
+ Filter-ID += 'fail 12'
+ }
+}
+
+# ethernet - bin 0x00003938373c
+if (Tmp-Integer-1 != 6) {
+ update reply {
+ Filter-Id += 'fail 13'
+ }
+}
+
+# integer64 - bin 0x100000003938373d
+if (Tmp-Integer-2 != 8) {
+ update reply {
+ Filter-Id += 'fail 14'
+ }
+}
+
+# ipv4prefix - bin 0x00203938373e
+if (Tmp-Integer-3 != 6) {
+ update reply {
+ Filter-Id += 'fail 15'
+ }
+}
diff --git a/src/tests/keywords/load-balance b/src/tests/keywords/load-balance
new file mode 100644
index 0000000..d07939a
--- /dev/null
+++ b/src/tests/keywords/load-balance
@@ -0,0 +1,97 @@
+# PRE: update if foreach
+#
+# Load-Balance blocks.
+#
+# Should distribute load between the modules.
+#
+update request {
+ Tmp-Integer-0 := 0
+ Tmp-Integer-1 := 0
+
+ Tmp-Integer-2 += 0 # 0
+ Tmp-Integer-2 += 1
+ Tmp-Integer-2 += 2
+ Tmp-Integer-2 += 3
+ Tmp-Integer-2 += 4
+ Tmp-Integer-2 += 5
+ Tmp-Integer-2 += 6
+ Tmp-Integer-2 += 7
+ Tmp-Integer-2 += 8
+ Tmp-Integer-2 += 9 # 10
+ Tmp-Integer-2 += 0
+ Tmp-Integer-2 += 1
+ Tmp-Integer-2 += 2
+ Tmp-Integer-2 += 3
+ Tmp-Integer-2 += 4
+ Tmp-Integer-2 += 5
+ Tmp-Integer-2 += 6
+ Tmp-Integer-2 += 7
+ Tmp-Integer-2 += 8
+ Tmp-Integer-2 += 9 # 20
+ Tmp-Integer-2 += 0
+ Tmp-Integer-2 += 1
+ Tmp-Integer-2 += 2
+ Tmp-Integer-2 += 3
+ Tmp-Integer-2 += 4
+ Tmp-Integer-2 += 5
+ Tmp-Integer-2 += 6
+ Tmp-Integer-2 += 7
+ Tmp-Integer-2 += 8
+ Tmp-Integer-2 += 9 # 30
+ Tmp-Integer-2 += 0
+ Tmp-Integer-2 += 1
+ Tmp-Integer-2 += 2
+ Tmp-Integer-2 += 3
+ Tmp-Integer-2 += 4
+ Tmp-Integer-2 += 5
+ Tmp-Integer-2 += 6
+ Tmp-Integer-2 += 7
+ Tmp-Integer-2 += 8
+ Tmp-Integer-2 += 9 # 40
+ Tmp-Integer-2 += 0
+ Tmp-Integer-2 += 1
+ Tmp-Integer-2 += 2
+ Tmp-Integer-2 += 3
+ Tmp-Integer-2 += 4
+ Tmp-Integer-2 += 5
+ Tmp-Integer-2 += 6
+ Tmp-Integer-2 += 7
+ Tmp-Integer-2 += 8
+ Tmp-Integer-2 += 9 # 49
+}
+
+#
+# Loop 0..9
+#
+foreach &Tmp-Integer-2 {
+ load-balance {
+ group {
+ update request {
+ Tmp-Integer-0 := "%{expr:%{Tmp-Integer-0} + 1}"
+ Filter-Id += "PICKED GROUP 1 %{Tmp-Integer-0} TIME(S)"
+ }
+ ok
+ }
+ group {
+ update request {
+ Tmp-Integer-1 := "%{expr:%{Tmp-Integer-1} + 1}"
+ Filter-Id += "PICKED GROUP 2 %{Tmp-Integer-1} TIME(S)"
+ }
+ ok
+ }
+ }
+}
+
+# The chances of one group being used over another 50 times by random occurrence
+# is quite small, and if this happens repeatedly, it's likely there's a bug in
+# the load-balance code or random number generator.
+if ((&Tmp-Integer-0 == 0) || (&Tmp-Integer-1 == 0)) {
+ update reply {
+ Filter-Id += "fail 1 %{Tmp-Integer-0}/%{Tmp-Integer-1})"
+ }
+}
+else {
+ update reply {
+ Filter-Id := 'filter'
+ }
+}
diff --git a/src/tests/keywords/log b/src/tests/keywords/log
new file mode 100644
index 0000000..97a2557
--- /dev/null
+++ b/src/tests/keywords/log
@@ -0,0 +1,7 @@
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update reply {
+ Filter-Id := "filter"
+}
diff --git a/src/tests/keywords/map-xlat b/src/tests/keywords/map-xlat
new file mode 100644
index 0000000..24446a5
--- /dev/null
+++ b/src/tests/keywords/map-xlat
@@ -0,0 +1,25 @@
+#
+# PRE: update
+#
+# Test the map xlat
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := "filter"
+}
+
+update {
+ Tmp-String-0 := '&control:Tmp-String-0 := \'testing123\''
+}
+
+if ("%{map:%{Tmp-String-0}}" != 1) {
+ update reply {
+ Filter-Id += 'Fail 0'
+ }
+}
+
+if (&control:Tmp-String-0 != 'testing123') {
+ update reply {
+ Filter-Id += 'Fail 1'
+ }
+}
diff --git a/src/tests/keywords/md4 b/src/tests/keywords/md4
new file mode 100644
index 0000000..7e9b1ff
--- /dev/null
+++ b/src/tests/keywords/md4
@@ -0,0 +1,58 @@
+#
+# PRE: update if
+#
+update reply {
+ Filter-Id := "filter"
+}
+
+update {
+ control:Cleartext-Password := 'hello'
+ request:Tmp-String-0 := "This is a string\n"
+ request:Tmp-Octets-0 := 0x000504030201
+ request:Tmp-String-1 := "what do ya want for nothing?"
+ request:Tmp-String-2 := "Jefe"
+}
+
+#
+# Put "This is a string" into a file and call "md5sum" on it.
+# You should get this string.
+#
+if ("%{md4:This is a string\n}" != '1f60d5cd85e17bfbdda7c923822f060c') {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+if ("%{md4:&Tmp-String-0}" != '1f60d5cd85e17bfbdda7c923822f060c') {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+if ("%{md4:&request:Tmp-String-0}" != '1f60d5cd85e17bfbdda7c923822f060c') {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+if ("%{md4:%{request:Tmp-String-0}}" != '1f60d5cd85e17bfbdda7c923822f060c') {
+ update reply {
+ Filter-Id += 'fail 4'
+ }
+}
+
+#
+# MD4 should also be able to cope with references to octet attributes
+#
+if ("%{md4:&request:Tmp-Octets-0}" != 'ac3ed17b3cf19ec38352ec534a932fc6') {
+ update reply {
+ Filter-Id += 'fail 5'
+ }
+}
+
+if ("%{md4:&Tmp-String-1}" != 'f7b44afb9cfdc877aa99d44654fe808b') {
+ update reply {
+ Filter-Id += 'fail 6'
+ }
+}
+
diff --git a/src/tests/keywords/md5 b/src/tests/keywords/md5
new file mode 100644
index 0000000..a973660
--- /dev/null
+++ b/src/tests/keywords/md5
@@ -0,0 +1,60 @@
+#
+# PRE: update if
+#
+update reply {
+ Filter-Id := "filter"
+}
+
+update {
+ control:Cleartext-Password := 'hello'
+ request:Tmp-String-0 := "This is a string\n"
+ request:Tmp-Octets-0 := 0x000504030201
+ request:Tmp-String-1 := "what do ya want for nothing?"
+ request:Tmp-String-2 := "Jefe"
+}
+
+#
+# Put "This is a string" into a file and call "md5sum" on it.
+# You should get this string.
+#
+if ("%{md5:This is a string\n}" != '9ac4dbbc3c0ad2429e61d0df5dc28add') {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+if ("%{md5:&Tmp-String-0}" != '9ac4dbbc3c0ad2429e61d0df5dc28add') {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+if ("%{md5:&request:Tmp-String-0}" != '9ac4dbbc3c0ad2429e61d0df5dc28add') {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+if ("%{md5:%{request:Tmp-String-0}}" != '9ac4dbbc3c0ad2429e61d0df5dc28add') {
+ update reply {
+ Filter-Id += 'fail 4'
+ }
+}
+
+#
+# MD5 should also be able to cope with references to octet attributes
+#
+if ("%{md5:&request:Tmp-Octets-0}" != 'c1e7fa505b2fc1fd0da6cac3db6f6f44') {
+ update reply {
+ Filter-Id += 'fail 5'
+ }
+}
+
+#
+# MD5 HMAC with attribute references
+#
+if ("%{hmacmd5:&Tmp-String-1 &Tmp-String-2}" != '750c783e6ab0b503eaa86e310a5db738') {
+ update reply {
+ Filter-Id += 'fail 6'
+ }
+}
diff --git a/src/tests/keywords/module-failure-message b/src/tests/keywords/module-failure-message
new file mode 100644
index 0000000..51b1ef4
--- /dev/null
+++ b/src/tests/keywords/module-failure-message
@@ -0,0 +1,40 @@
+#
+# PRE: update
+#
+# Remove all attributes in a list
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+test
+if ("%{request:Module-Failure-Message[0]}" != 'test: RERROR error message') {
+ update reply {
+ Filter-Id += 'Fail 0'
+ }
+}
+
+if ("%{request:Module-Failure-Message[1]}" != 'test: RDEBUG error message') {
+ update reply {
+ Filter-Id += 'Fail 1'
+ }
+}
+
+if ("%{request:Module-Failure-Message[2]}" != 'test: RDEBUG2 error message') {
+ update reply {
+ Filter-Id += 'Fail 2'
+ }
+}
+
+if ("%{request:Module-Failure-Message[3]}" != 'test: RDEBUG3 error message') {
+ update reply {
+ Filter-Id += 'Fail 3'
+ }
+}
+
+if ("%{request:Module-Failure-Message[4]}" != 'test: RDEBUG4 error message') {
+ update reply {
+ Filter-Id += 'Fail 4'
+ }
+}
diff --git a/src/tests/keywords/ok-return b/src/tests/keywords/ok-return
new file mode 100644
index 0000000..f79ef02
--- /dev/null
+++ b/src/tests/keywords/ok-return
@@ -0,0 +1,13 @@
+update {
+ control:Auth-Type = 'Accept'
+ reply:Reply-Message = 'pass'
+}
+
+# Section should exit after this statement
+ok {
+ ok = return
+}
+
+update {
+ reply:Reply-Message := 'fail'
+}
diff --git a/src/tests/keywords/ok-return.attrs b/src/tests/keywords/ok-return.attrs
new file mode 100644
index 0000000..ad30f4d
--- /dev/null
+++ b/src/tests/keywords/ok-return.attrs
@@ -0,0 +1,4 @@
+User-Name = 'test'
+
+Response-Packet-Type == Access-Accept
+Reply-Message == 'pass'
diff --git a/src/tests/keywords/pad b/src/tests/keywords/pad
new file mode 100644
index 0000000..0a1f9ce
--- /dev/null
+++ b/src/tests/keywords/pad
@@ -0,0 +1,62 @@
+#
+# PRE: if update return
+#
+
+update request {
+ Tmp-String-0 = "test"
+}
+
+#
+# rpad tests
+#
+
+if ("%{rpad:&Tmp-String-0 7}" != "test ") {
+ update reply {
+ Filter-Id := "fail 1"
+ }
+ return
+}
+
+if ("%{rpad:&Tmp-String-0 2}" != "te") {
+ update reply {
+ Filter-Id := "fail 2"
+ }
+ return
+}
+
+if ("%{rpad:&Tmp-String-0 7 x}" != "testxxx") {
+ update reply {
+ Filter-Id := "fail 1"
+ }
+ return
+}
+
+#
+# lpad tests
+#
+
+if ("%{lpad:&Tmp-String-0 7}" != " test") {
+ update reply {
+ Filter-Id := "fail 1"
+ }
+ return
+}
+
+if ("%{lpad:&Tmp-String-0 2}" != "te") {
+ update reply {
+ Filter-Id := "fail 2"
+ }
+ return
+}
+
+if ("%{lpad:&Tmp-String-0 7 x}" != "xxxtest") {
+ update reply {
+ Filter-Id := "fail 1"
+ }
+ return
+}
+
+update reply {
+ Filter-Id := "filter"
+}
+
diff --git a/src/tests/keywords/pairs b/src/tests/keywords/pairs
new file mode 100644
index 0000000..4230c57
--- /dev/null
+++ b/src/tests/keywords/pairs
@@ -0,0 +1,42 @@
+#
+# PRE: update if
+#
+update {
+ control:Cleartext-Password := 'hello'
+ request:Tmp-String-0 := "This is a string"
+ request:Tmp-String-0 += "This is another one"
+ request:Tmp-Octets-0 := 0x000504030201
+ request:Tmp-Integer-0 := 7331
+ request:Tunnel-Private-Group-Id:5 = 127.0.0.1
+ reply:Filter-Id = 'filter'
+}
+
+if ("%{pairs:request:}" != "User-Name = \"bob\", User-Password = \"hello\", Tmp-String-0 = \"This is a string\", Tmp-String-0 = \"This is another one\", Tmp-Octets-0 = 0x000504030201, Tmp-Integer-0 = 7331, Tunnel-Private-Group-Id:5 = \"127.0.0.1\"") {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+if ("%{pairs:Tmp-String-0}" != "Tmp-String-0 = \"This is a string\"") {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+if ("%{pairs:Tmp-String-0[*]}" != "Tmp-String-0 = \"This is a string\", Tmp-String-0 = \"This is another one\"") {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+if ("%{pairs:control:}" != "Cleartext-Password = \"hello\"") {
+ update reply {
+ Filter-Id += 'fail 4'
+ }
+}
+
+if ("%{pairs:control:User-Name}" != '') {
+ update reply {
+ Filter-Id += 'fail 5'
+ }
+}
diff --git a/src/tests/keywords/pap b/src/tests/keywords/pap
new file mode 100644
index 0000000..a347b7c
--- /dev/null
+++ b/src/tests/keywords/pap
@@ -0,0 +1,146 @@
+#
+# PRE: update if
+#
+update {
+ reply:Filter-Id := 'filter'
+ control: !* ANY
+ request:Tmp-String-0 := "5RNqNl8iYLbkCc7JhR8as4TtDDCX6otuuWtcja8rITUyx9zrnHSe9tTHGmKK" # 60 byte salt
+}
+
+#
+# Unencoded Cleartext-Password in password with header
+#
+update {
+ control:Password-With-Header := "%{request:User-Password}"
+}
+pap.authorize
+pap.authenticate {
+ reject = 1
+}
+if (reject) {
+ update reply {
+ Filter-Id += 'fail 0'
+ }
+}
+
+update {
+ control: !* ANY
+}
+
+#
+# Base64 encoded Cleartext-Password in password with header
+#
+update {
+ Tmp-String-1 := "{clear}%{User-Password}"
+}
+update {
+ control:Password-With-Header := "%{base64:&request:Tmp-String-1}"
+}
+pap.authorize
+pap.authenticate {
+ reject = 1
+}
+if (reject) {
+ update reply {
+ Filter-Id += 'fail 0'
+ }
+}
+
+update {
+ control: !* ANY
+}
+
+#
+# Hex encoded SSHA password
+#
+update {
+ control:Password-With-Header += "{ssha}%{sha1:%{request:User-Password}%{&request:Tmp-String-0}}%{hex:&request:Tmp-String-0}"
+}
+
+pap.authorize
+pap.authenticate {
+ reject = 1
+}
+if (reject) {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+update {
+ control: !* ANY
+}
+
+#
+# Base64 encoded SSHA password
+#
+update {
+ control:Tmp-String-1 := "%{sha1:%{request:User-Password}%{&request:Tmp-String-0}}%{hex:&request:Tmp-String-0}"
+}
+
+# To Binary
+update {
+ control:Tmp-Octets-0 := "0x%{control:Tmp-String-1}"
+}
+
+# To Base64
+update {
+ control:Tmp-String-1 := "%{base64:&control:Tmp-Octets-0}"
+}
+
+update {
+ control:Password-With-Header += "{ssha}%{control:Tmp-String-1}"
+}
+
+pap.authorize
+pap.authenticate {
+ reject = 1
+}
+if (reject) {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+update {
+ control: !* ANY
+}
+
+#
+# Base64 of Base64 encoded SSHA password
+#
+update {
+ control:Tmp-String-1 := "%{sha1:%{request:User-Password}%{&request:Tmp-String-0}}%{hex:&request:Tmp-String-0}"
+}
+
+# To Binary
+update {
+ control:Tmp-Octets-0 := "0x%{control:Tmp-String-1}"
+}
+
+# To Base64
+update {
+ control:Tmp-String-1 := "{ssha}%{base64:&control:Tmp-Octets-0}"
+}
+
+update {
+ control:Password-With-Header += "%{base64:&control:Tmp-String-1}"
+}
+
+pap.authorize
+pap.authenticate {
+ reject = 1
+}
+if (reject) {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+update {
+ control: !* ANY
+}
+
+update control {
+ Auth-Type := Accept
+}
diff --git a/src/tests/keywords/pap-ssha2 b/src/tests/keywords/pap-ssha2
new file mode 100644
index 0000000..a8c9c9b
--- /dev/null
+++ b/src/tests/keywords/pap-ssha2
@@ -0,0 +1,114 @@
+#
+# PRE: update if pap
+#
+
+#
+# Skip if the server wasn't built with openssl
+#
+if ('${feature.tls}' != 'yes') {
+ update control {
+ Response-Packet-Type := Access-Accept
+ }
+ handled
+}
+
+update {
+ reply:Filter-Id := 'filter'
+ control: !* ANY
+ request:Tmp-String-0 := "5RNqNl8iYLbkCc7JhR8as4TtDDCX6otuuWtcja8rITUyx9zrnHSe9tTHGmKK" # 60 byte salt
+}
+
+#
+# Hex encoded SSHA2-512 password
+#
+update {
+ control:Password-With-Header += "{ssha512}%{sha512:%{request:User-Password}%{&request:Tmp-String-0}}%{hex:&request:Tmp-String-0}"
+}
+
+pap.authorize
+pap.authenticate {
+ reject = 1
+}
+if (reject) {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+update {
+ control: !* ANY
+}
+
+#
+# Base64 encoded SSHA2-512 password
+#
+update {
+ control:Tmp-String-1 := "%{sha512:%{request:User-Password}%{&request:Tmp-String-0}}%{hex:&request:Tmp-String-0}"
+}
+
+# To Binary
+update {
+ control:Tmp-Octets-0 := "0x%{control:Tmp-String-1}"
+}
+
+# To Base64
+update {
+ control:Tmp-String-1 := "%{base64:&control:Tmp-Octets-0}"
+}
+
+update {
+ control:Password-With-Header += "{ssha512}%{control:Tmp-String-1}"
+}
+
+pap.authorize
+pap.authenticate {
+ reject = 1
+}
+if (reject) {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+update {
+ control: !* ANY
+}
+
+#
+# Base64 of Base64 encoded SSHA2-512 password
+#
+update {
+ control:Tmp-String-1 := "%{sha512:%{request:User-Password}%{&request:Tmp-String-0}}%{hex:&request:Tmp-String-0}"
+}
+
+# To Binary
+update {
+ control:Tmp-Octets-0 := "0x%{control:Tmp-String-1}"
+}
+
+# To Base64
+update {
+ control:Tmp-String-1 := "{ssha512}%{base64:&control:Tmp-Octets-0}"
+}
+
+update {
+ control:Password-With-Header += "%{base64:&control:Tmp-String-1}"
+}
+
+pap.authorize
+pap.authenticate {
+ reject = 1
+}
+if (reject) {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+update {
+ control: !* ANY
+}
+
+update control {
+ Auth-Type := Accept
+}
diff --git a/src/tests/keywords/radiusd.conf b/src/tests/keywords/radiusd.conf
new file mode 100644
index 0000000..58cb356
--- /dev/null
+++ b/src/tests/keywords/radiusd.conf
@@ -0,0 +1,127 @@
+#
+# Minimal radiusd.conf for testing keywords
+#
+
+raddb = raddb
+keyword = src/tests/keywords
+
+modconfdir = ${raddb}/mods-config
+
+correct_escapes = true
+
+# Only for testing!
+# Setting this on a production system is a BAD IDEA.
+security {
+ allow_vulnerable_openssl = yes
+}
+
+modules {
+ $INCLUDE ${raddb}/mods-enabled/always
+
+ $INCLUDE ${raddb}/mods-enabled/pap
+
+ $INCLUDE ${raddb}/mods-enabled/expr
+
+ $INCLUDE ${raddb}/mods-enabled/unpack
+
+ test {
+
+ }
+
+ unix {
+ }
+
+ cache {
+ driver = "rlm_cache_rbtree"
+
+ key = "%{Tmp-String-0}"
+ ttl = 2
+
+ update {
+ &request:Tmp-String-1 := &control:Tmp-String-1
+ &request:Tmp-Integer-0 := &control:Tmp-Integer-0
+ &control: += &reply:
+ }
+
+ add_stats = yes
+ }
+}
+
+policy {
+ #
+ # Outputs the contents of the control list in debugging (-X) mode
+ #
+ debug_control {
+ if("%{debug_attr:control:}" == '') {
+ noop
+ }
+ }
+
+ #
+ # Outputs the contents of the request list in debugging (-X) mode
+ #
+ debug_request {
+ if("%{debug_attr:request:}" == '') {
+ noop
+ }
+ }
+
+ #
+ # Outputs the contents of the reply list in debugging (-X) mode
+ #
+ debug_reply {
+ if("%{debug_attr:reply:}" == '') {
+ noop
+ }
+ }
+
+ #
+ # Outputs the contents of the main lists in debugging (-X) mode
+ #
+ debug_all {
+ debug_control
+ debug_request
+ debug_reply
+ }
+
+ #
+ # Just check that this can be referred to as "virtual_policy.post-auth"
+ #
+ virtual_policy {
+ ok
+ }
+
+ with.dots {
+ ok
+ }
+}
+
+instantiate {
+ #
+ # Just check that this can be referred to as "virtual_instantiate.post-auth"
+ #
+ load-balance virtual_instantiate {
+ ok
+ ok
+ }
+}
+
+server default {
+ authorize {
+ update control {
+ Cleartext-Password := 'hello'
+ }
+
+ #
+ # Include the test file specified by the
+ # KEYWORD environment variable.
+ #
+ $INCLUDE ${keyword}/$ENV{KEYWORD}
+
+ pap
+ }
+
+ authenticate {
+ pap
+ }
+}
diff --git a/src/tests/keywords/redundant b/src/tests/keywords/redundant
new file mode 100644
index 0000000..cebe7be
--- /dev/null
+++ b/src/tests/keywords/redundant
@@ -0,0 +1,17 @@
+# PRE: update if foreach
+#
+# Redundant blocks.
+#
+# The first one fails, so the second one is used
+#
+redundant {
+ group {
+ fail
+ }
+
+ group {
+ update reply {
+ Filter-Id := "filter"
+ }
+ }
+}
diff --git a/src/tests/keywords/redundant-error b/src/tests/keywords/redundant-error
new file mode 100644
index 0000000..bec4335
--- /dev/null
+++ b/src/tests/keywords/redundant-error
@@ -0,0 +1,6 @@
+#
+# Check that redundant blocks can't be empty
+#
+redundant { # ERROR
+
+}
diff --git a/src/tests/keywords/redundant-load-balance b/src/tests/keywords/redundant-load-balance
new file mode 100644
index 0000000..27721f1
--- /dev/null
+++ b/src/tests/keywords/redundant-load-balance
@@ -0,0 +1,65 @@
+# PRE: update if foreach
+#
+# Redundant blocks.
+#
+# The first one fails, so the second one is used
+#
+update request {
+ Tmp-Integer-0 := 0
+ Tmp-Integer-1 += 0
+ Tmp-Integer-1 += 1
+ Tmp-Integer-1 += 2
+ Tmp-Integer-1 += 3
+ Tmp-Integer-1 += 4
+ Tmp-Integer-1 += 5
+ Tmp-Integer-1 += 6
+ Tmp-Integer-1 += 7
+ Tmp-Integer-1 += 8
+ Tmp-Integer-1 += 9
+
+}
+
+#
+# Loop 0..9
+#
+foreach &Tmp-Integer-1 {
+ redundant-load-balance {
+ group {
+ # fail on even numbered values, succeed on odd numbered ones
+ if ("%{expr:%{Foreach-Variable-0} %% 2}" == 0) {
+ fail
+ }
+ else {
+ update request {
+ Tmp-Integer-0 := "%{expr:%{Tmp-Integer-0} + 1}"
+ Filter-Id += "SUCCEED ODD %{Foreach-Variable-0} %{Tmp-Integer-0}"
+ }
+ ok
+ }
+ }
+ group {
+ # succeed on even-numbered values, fail on off-numbered ones.
+ if ("%{expr:%{Foreach-Variable-0} %% 2}" == 1) {
+ fail
+ }
+ else {
+ update request {
+ Tmp-Integer-0 := "%{expr:%{Tmp-Integer-0} + 1}"
+ Filter-Id += "SUCCEED EVEN %{Foreach-Variable-0} %{Tmp-Integer-0}"
+ }
+ ok
+ }
+ }
+ }
+}
+
+if (&Tmp-Integer-0 != "%{Tmp-Integer-1[#]}") {
+ update reply {
+ Filter-Id += "fail 2 (passed %{Tmp-Integer-0}/%{Tmp-Integer-1[#]})"
+ }
+}
+else {
+ update reply {
+ Filter-Id := 'filter'
+ }
+}
diff --git a/src/tests/keywords/redundant-redundant b/src/tests/keywords/redundant-redundant
new file mode 100644
index 0000000..294f53e
--- /dev/null
+++ b/src/tests/keywords/redundant-redundant
@@ -0,0 +1,73 @@
+# PRE: update if foreach redundant redundant-load-balance
+#
+# Nested redundant blocks.
+#
+#
+update request {
+ Tmp-Integer-2 := 0
+ Tmp-Integer-3 := 0
+ Tmp-Integer-4 := 0
+ Tmp-Integer-5 := 0
+}
+
+redundant {
+ redundant-load-balance {
+ group {
+ update request {
+ Tmp-Integer-2 := "%{expr:&Tmp-Integer-2 + 1}"
+ }
+ fail
+ }
+ group {
+ update request {
+ Tmp-Integer-3 := "%{expr:&Tmp-Integer-3 + 1}"
+ }
+ fail
+ }
+ group {
+ update request {
+ Tmp-Integer-4 := "%{expr:&Tmp-Integer-4 + 1}"
+ }
+ fail
+ }
+ group {
+ update request {
+ Tmp-Integer-5 := "%{expr:&Tmp-Integer-5 + 1}"
+ }
+ fail
+ }
+ }
+ ok
+}
+
+if (&Tmp-Integer-2 != 1) {
+ update reply {
+ Filter-Id += 'Fail 3a'
+ }
+ return
+}
+
+if (&Tmp-Integer-3 != 1) {
+ update reply {
+ Filter-Id += 'Fail 3b'
+ }
+ return
+}
+
+if (&Tmp-Integer-4 != 1) {
+ update reply {
+ Filter-Id += 'Fail 3c'
+ }
+ return
+}
+
+if (&Tmp-Integer-5 != 1) {
+ update reply {
+ Filter-Id += 'Fail 3d'
+ }
+ return
+}
+
+update reply {
+ Filter-Id := "filter"
+} \ No newline at end of file
diff --git a/src/tests/keywords/regex-escape b/src/tests/keywords/regex-escape
new file mode 100644
index 0000000..4ab1e5b
--- /dev/null
+++ b/src/tests/keywords/regex-escape
@@ -0,0 +1,29 @@
+#
+# PRE: update if
+#
+
+#
+# Strings which are expanded in a regex have regex special
+# characters escaped. Because the input strings are unsafe.
+#
+update request {
+ Tmp-String-0 := "example.com"
+ Tmp-String-1 := "exampleXcom"
+}
+
+if ("exampleXcom" =~ /%{Tmp-String-0}/) {
+ update reply {
+ Filter-Id := "fail 1"
+ }
+}
+
+elsif (&Tmp-String-1 =~ /%{Tmp-String-0}/) {
+ update reply {
+ Filter-Id := "fail 2"
+ }
+}
+else {
+ update reply {
+ Filter-Id := "filter"
+ }
+} \ No newline at end of file
diff --git a/src/tests/keywords/regex-lhs b/src/tests/keywords/regex-lhs
new file mode 100644
index 0000000..91b0b20
--- /dev/null
+++ b/src/tests/keywords/regex-lhs
@@ -0,0 +1,27 @@
+#
+# PRE: update if regex-escape
+#
+
+#
+# Strings which are expanded in a regex have regex special
+# characters escaped. Because the input strings are unsafe.
+#
+update request {
+ Tmp-String-0 := "example.com"
+ Tmp-String-1 := "^foo$bar"
+}
+
+if (&Tmp-String-0 !~ /example\.com$/) {
+ update reply {
+ Filter-Id := "fail 1"
+ }
+}
+elsif (&Tmp-String-1 !~ /\^foo\$bar/) {
+ update reply {
+ Filter-Id := "fail 1"
+ }
+} else {
+ update reply {
+ Filter-Id := "filter"
+ }
+} \ No newline at end of file
diff --git a/src/tests/keywords/return b/src/tests/keywords/return
new file mode 100644
index 0000000..49779a5
--- /dev/null
+++ b/src/tests/keywords/return
@@ -0,0 +1,33 @@
+#
+# PRE: update if
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+if (User-Name == "bob") {
+ update reply {
+ Filter-Id := "filter"
+ }
+
+ #
+ # We need this because the "return" below
+ # will prevent the "pap" module from being run
+ # in the "authorize" section.
+ #
+ update control {
+ Auth-Type := PAP
+ }
+
+ #
+ # Stop processing "authorize", and go to the next section.
+ #
+ return
+
+ #
+ # Shouldn't reach this
+ #
+ update reply {
+ Filter-Id := "fail"
+ }
+}
diff --git a/src/tests/keywords/return-group b/src/tests/keywords/return-group
new file mode 100644
index 0000000..92978e4
--- /dev/null
+++ b/src/tests/keywords/return-group
@@ -0,0 +1,22 @@
+# PRE: return
+#
+update {
+ control:Auth-Type = 'Accept'
+}
+
+group {
+ # Section should exit after this statement
+ ok {
+ ok = return
+ }
+
+ # This entry should never be reached
+ update {
+ reply:Reply-Message := 'fail'
+ }
+}
+
+# We should continue processing after the previous group.
+update {
+ reply:Reply-Message += 'pass'
+}
diff --git a/src/tests/keywords/return-group.attrs b/src/tests/keywords/return-group.attrs
new file mode 100644
index 0000000..ad30f4d
--- /dev/null
+++ b/src/tests/keywords/return-group.attrs
@@ -0,0 +1,4 @@
+User-Name = 'test'
+
+Response-Packet-Type == Access-Accept
+Reply-Message == 'pass'
diff --git a/src/tests/keywords/return-section b/src/tests/keywords/return-section
new file mode 100644
index 0000000..21ecea1
--- /dev/null
+++ b/src/tests/keywords/return-section
@@ -0,0 +1,35 @@
+#
+# PRE: update if
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+if (User-Name == "bob") {
+ update reply {
+ Filter-Id := "filter"
+ }
+
+ #
+ # We need this because the "return" below
+ # will prevent the "pap" module from being run
+ # in the "authorize" section.
+ #
+ update control {
+ Auth-Type := PAP
+ }
+
+ #
+ # Stop processing "authorize", and go to the next section.
+ #
+ return { # ERROR
+ ok = 1
+ }
+
+ #
+ # Shouldn't reach this
+ #
+ update reply {
+ Filter-Id := "fail"
+ }
+}
diff --git a/src/tests/keywords/sha1 b/src/tests/keywords/sha1
new file mode 100644
index 0000000..0d577a9
--- /dev/null
+++ b/src/tests/keywords/sha1
@@ -0,0 +1,60 @@
+#
+# PRE: update if
+#
+update {
+ control:Cleartext-Password := 'hello'
+ request:Tmp-String-0 := "This is a string\n"
+ request:Tmp-Octets-0 := 0x000504030201
+ request:Tmp-String-1 := "what do ya want for nothing?"
+ request:Tmp-String-2 := "Jefe"
+}
+
+update reply {
+ Filter-Id := 'filter'
+}
+
+#
+# Put "This is a string" into a file and call "sha1sum" on it.
+# You should get this string.
+#
+if ("%{sha1:This is a string\n}" != 'cc7edf1ccc4bdf1e0ec8f72b95388b65218ecf0c') {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+if ("%{sha1:&Tmp-String-0}" != 'cc7edf1ccc4bdf1e0ec8f72b95388b65218ecf0c') {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+if ("%{sha1:&request:Tmp-String-0}" != 'cc7edf1ccc4bdf1e0ec8f72b95388b65218ecf0c') {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+if ("%{sha1:%{Tmp-String-0}}" != 'cc7edf1ccc4bdf1e0ec8f72b95388b65218ecf0c') {
+ update reply {
+ Filter-Id += 'fail 4'
+ }
+}
+
+#
+# SHA1 should also be able to cope with references to octet attributes
+#
+if ("%{sha1:&request:Tmp-Octets-0}" != '365b244645fe7294dff062174996572319d5a82c') {
+ update reply {
+ Filter-Id += 'fail 5'
+ }
+}
+
+#
+# SHA1 HMAC with attribute references
+#
+if ("%{hmacsha1:&Tmp-String-1 &Tmp-String-2}" != 'effcdf6ae5eb2fa2d27416d5f184df9c259a7c79') {
+ update reply {
+ Filter-Id += 'fail 6'
+ }
+}
diff --git a/src/tests/keywords/sha2 b/src/tests/keywords/sha2
new file mode 100644
index 0000000..89c54f4
--- /dev/null
+++ b/src/tests/keywords/sha2
@@ -0,0 +1,81 @@
+#
+# PRE: update if
+#
+if ("$ENV{OPENSSL_LIBS}" != "") {
+
+update {
+ control:Cleartext-Password := 'hello'
+ request:Tmp-String-0 := "This is a string\n"
+ request:Tmp-Octets-0 := 0x000504030201
+}
+
+update reply {
+ Filter-Id := 'filter'
+}
+
+#
+# Put "This is a string" into a file and call "sha256sum" on it.
+# You should get this string.
+#
+if ("%{sha256:This is a string\n}" != 'b3716a1ab53042bb392034f29071e13b0c38aa19b4edd75d9a76022f91189124') {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+if ("%{sha256:&Tmp-String-0}" != 'b3716a1ab53042bb392034f29071e13b0c38aa19b4edd75d9a76022f91189124') {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+if ("%{sha256:&request:Tmp-String-0}" != 'b3716a1ab53042bb392034f29071e13b0c38aa19b4edd75d9a76022f91189124') {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+if ("%{sha256:%{Tmp-String-0}}" != 'b3716a1ab53042bb392034f29071e13b0c38aa19b4edd75d9a76022f91189124') {
+ update reply {
+ Filter-Id += 'fail 4'
+ }
+}
+
+#
+# SHA256 should also be able to cope with references to octet attributes
+#
+if ("%{sha256:&request:Tmp-Octets-0}" != 'f307e202b881fded70e58017aa0c4d7b29c76ab25d02bf078301a5f6635187eb') {
+ update reply {
+ Filter-Id += 'fail 5'
+ }
+}
+
+#
+# SHA512 and SHA256 share common code paths, so the tests don't need to be
+# as exhaustive.
+#
+if ("%{sha512:This is a string\n}" != '56b57df5cce42d4e35c644649798ea23ec16f4f4626e78faf4d2d8f430ea349bcc28cd5532457c82f0aa66bf68988346039fe75b900a92ff94fd53993d45990f') {
+ update reply {
+ Filter-Id += 'fail 6'
+ }
+}
+
+if ("%{sha512:&Tmp-String-0}" != '56b57df5cce42d4e35c644649798ea23ec16f4f4626e78faf4d2d8f430ea349bcc28cd5532457c82f0aa66bf68988346039fe75b900a92ff94fd53993d45990f') {
+ update reply {
+ Filter-Id += 'fail 7'
+ }
+}
+
+if ("%{sha512:&request:Tmp-Octets-0}" != 'de80271eb5e03a1c24dd0cd823a22305a743ee3a54f1de5bf97adbf56984561154bfb6928b1da4ccc3f5dde9f4032ad461937b60b9ace4ad3898cf45c90596d7') {
+ update reply {
+ Filter-Id += 'fail 8'
+ }
+}
+
+}
+
+else { # no OPENSSL. Force the test to pass
+ update reply {
+ Filter-Id := 'filter'
+ }
+}
diff --git a/src/tests/keywords/smash b/src/tests/keywords/smash
new file mode 100644
index 0000000..fd19d3f
--- /dev/null
+++ b/src/tests/keywords/smash
@@ -0,0 +1,6 @@
+#
+# PRE: update
+#
+# This gives the game away.
+#
+update { control:Cleartext-Password := 'hello', reply:Filter-Id := "filter" }
diff --git a/src/tests/keywords/string b/src/tests/keywords/string
new file mode 100644
index 0000000..bcf0fcf
--- /dev/null
+++ b/src/tests/keywords/string
@@ -0,0 +1,19 @@
+#
+# PRE: cmp
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update request {
+ Tmp-String-0 := "this\000is\000a\000string"
+}
+
+#
+# %{string:...} is explicitly not binary safe
+#
+if ("%{string:Tmp-String-0}" == "this") {
+ update reply {
+ Filter-Id := "filter"
+ }
+}
diff --git a/src/tests/keywords/substring b/src/tests/keywords/substring
new file mode 100644
index 0000000..0ce4e9d
--- /dev/null
+++ b/src/tests/keywords/substring
@@ -0,0 +1,418 @@
+#
+# PRE: update
+#
+# Check substring xlat works correctly
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+update request {
+ Tmp-String-0 := 'foo bar'
+ Tmp-Integer-0 := 54786512
+ Tmp-IP-Address-0 := 192.168.56.34
+ Tmp-Cast-Ethernet := 01:23:45:67:89:ab
+}
+
+update request {
+ Tmp-String-1 := "%{substring:&Tmp-String-0 2 3}"
+ Tmp-String-2 := "%{substring:&Tmp-String-0 -3 2}"
+ Tmp-String-3 := "%{substring:&Tmp-String-0 1 -3}"
+ Tmp-String-4 := "%{substring:&Tmp-String-0 -4 -1}"
+ Tmp-String-5 := "%{substring:&Tmp-String-0 8 5}"
+ Tmp-String-6 := "%{substring:&Tmp-String-0 4 -10}"
+ Tmp-String-7 := "%{substring:&Tmp-String-0 0 7}"
+ Tmp-String-8 := "%{substring:&Tmp-String-0 3 0}"
+ Tmp-String-9 := "%{substring:&Tmp-String-0 4 8}"
+}
+
+if (Tmp-String-1 != 'o b') {
+ update reply {
+ Filter-Id += 'fail 0.1'
+ }
+}
+
+if (Tmp-String-2 != 'ba') {
+ update reply {
+ Filter-Id += 'fail 0.2'
+ }
+}
+
+if (Tmp-String-3 != 'oo ') {
+ update reply {
+ Filter-Id += 'fail 0.3'
+ }
+}
+
+if (Tmp-String-4 != ' ba') {
+ update reply {
+ Filter-Id += 'fail 0.4'
+ }
+}
+
+if (Tmp-String-5 != '') {
+ update reply {
+ Filter-Id += 'fail 0.5'
+ }
+}
+
+if (Tmp-String-6 != '') {
+ update reply {
+ Filter-Id += 'fail 0.6'
+ }
+}
+
+if (Tmp-String-7 != 'foo bar') {
+ update reply {
+ Filter-Id += 'fail 0.7'
+ }
+}
+
+if (Tmp-String-8 != '') {
+ update reply {
+ Filter-Id += 'fail 0.8'
+ }
+}
+
+if (Tmp-String-9 != 'bar') {
+ update reply {
+ Filter-Id += 'fail 0.9'
+ }
+}
+
+update request {
+ Tmp-String-0 := ' foo bar '
+}
+
+update request {
+ Tmp-String-1 := "%{substring: &Tmp-String-0 2 3}"
+ Tmp-String-2 := "%{substring:&Tmp-String-0 -3 2}"
+ Tmp-String-3 := "%{substring:&Tmp-String-0 1 -3}"
+ Tmp-String-4 := "%{substring:&Tmp-String-0 -4 -1}"
+ Tmp-String-5 := "%{substring:&Tmp-String-0 10 5}"
+ Tmp-String-6 := "%{substring:&Tmp-String-0 4 -10}"
+ Tmp-String-7 := "%{substring:&Tmp-String-0 0 9}"
+ Tmp-String-8 := "%{substring:&Tmp-String-0 3 0}"
+ Tmp-String-9 := "%{substring:&Tmp-String-0 4 10}"
+}
+
+if (Tmp-String-1 != 'oo ') {
+ update reply {
+ Filter-Id += 'fail 1.1'
+ }
+}
+
+if (Tmp-String-2 != 'ar') {
+ update reply {
+ Filter-Id += 'fail 1.2'
+ }
+}
+
+if (Tmp-String-3 != 'foo b') {
+ update reply {
+ Filter-Id += 'fail 1.3'
+ }
+}
+
+if (Tmp-String-4 != 'bar') {
+ update reply {
+ Filter-Id += 'fail 1.4'
+ }
+}
+
+if (Tmp-String-5 != '') {
+ update reply {
+ Filter-Id += 'fail 1.5'
+ }
+}
+
+if (Tmp-String-6 != '') {
+ update reply {
+ Filter-Id += 'fail 1.6'
+ }
+}
+
+if (Tmp-String-7 != ' foo bar ') {
+ update reply {
+ Filter-Id += 'fail 1.7'
+ }
+}
+
+if (Tmp-String-8 != '') {
+ update reply {
+ Filter-Id += 'fail 1.8'
+ }
+}
+
+if (Tmp-String-9 != ' bar ') {
+ update reply {
+ Filter-Id += 'fail 1.9'
+ }
+}
+
+update request {
+ Tmp-String-1 := "%{substring:&Tmp-Integer-0 2 3}"
+ Tmp-String-2 := "%{substring:&Tmp-Integer-0 -3 2}"
+ Tmp-String-3 := "%{substring:&Tmp-Integer-0 1 -3}"
+ Tmp-String-4 := "%{substring:&Tmp-Integer-0 -4 -1}"
+ Tmp-String-5 := "%{substring:&Tmp-Integer-0 8 5}"
+ Tmp-String-6 := "%{substring:&Tmp-Integer-0 4 -10}"
+ Tmp-String-7 := "%{substring:&Tmp-Integer-0 0 8}"
+ Tmp-String-8 := "%{substring:&Tmp-Integer-0 5 0}"
+ Tmp-String-9 := "%{substring:&Tmp-Integer-0 4 10}"
+}
+
+if (Tmp-String-1 != '786') {
+ update reply {
+ Filter-Id += 'fail 2.1'
+ }
+}
+
+if (Tmp-String-2 != '51') {
+ update reply {
+ Filter-Id += 'fail 2.2'
+ }
+}
+
+if (Tmp-String-3 != '4786') {
+ update reply {
+ Filter-Id += 'fail 2.3'
+ }
+}
+
+if (Tmp-String-4 != '651') {
+ update reply {
+ Filter-Id += 'fail 2.4'
+ }
+}
+
+if (Tmp-String-5 != '') {
+ update reply {
+ Filter-Id += 'fail 2.5'
+ }
+}
+
+if (Tmp-String-6 != '') {
+ update reply {
+ Filter-Id += 'fail 2.6'
+ }
+}
+
+if (Tmp-String-7 != '54786512') {
+ update reply {
+ Filter-Id += 'fail 2.7'
+ }
+}
+
+if (Tmp-String-8 != '') {
+ update reply {
+ Filter-Id += 'fail 2.8'
+ }
+}
+
+if (Tmp-String-9 != '6512') {
+ update reply {
+ Filter-Id += 'fail 2.9'
+ }
+}
+
+update request {
+ Tmp-String-1 := "%{substring:&Tmp-IP-Address-0 2 3}"
+ Tmp-String-2 := "%{substring:&Tmp-IP-Address-0 -3 2}"
+ Tmp-String-3 := "%{substring:&Tmp-IP-Address-0 1 -3}"
+ Tmp-String-4 := "%{substring:&Tmp-IP-Address-0 -4 -1}"
+ Tmp-String-5 := "%{substring:&Tmp-IP-Address-0 15 5}"
+ Tmp-String-6 := "%{substring:&Tmp-IP-Address-0 4 -20}"
+ Tmp-String-7 := "%{substring:&Tmp-IP-Address-0 0 13}"
+ Tmp-String-8 := "%{substring:&Tmp-IP-Address-0 6 0}"
+ Tmp-String-9 := "%{substring:&Tmp-IP-Address-0 8 12}"
+}
+
+if (Tmp-String-1 != '2.1') {
+ update reply {
+ Filter-Id += 'fail 3.1'
+ }
+}
+
+if (Tmp-String-2 != '.3') {
+ update reply {
+ Filter-Id += 'fail 3.2'
+ }
+}
+
+if (Tmp-String-3 != '92.168.56') {
+ update reply {
+ Filter-Id += 'fail 3.3'
+ }
+}
+
+if (Tmp-String-4 != '6.3') {
+ update reply {
+ Filter-Id += 'fail 3.4'
+ }
+}
+
+if (Tmp-String-5 != '') {
+ update reply {
+ Filter-Id += 'fail 3.5'
+ }
+}
+
+if (Tmp-String-6 != '') {
+ update reply {
+ Filter-Id += 'fail 3.6'
+ }
+}
+
+if (Tmp-String-7 != '192.168.56.34') {
+ update reply {
+ Filter-Id += 'fail 3.7'
+ }
+}
+
+if (Tmp-String-8 != '') {
+ update reply {
+ Filter-Id += 'fail 3.8'
+ }
+}
+
+if (Tmp-String-9 != '56.34') {
+ update reply {
+ Filter-Id += 'fail 3.9'
+ }
+}
+
+update request {
+ Tmp-String-1 := "%{substring:&Tmp-Cast-Ethernet 2 3}"
+ Tmp-String-2 := "%{substring:&Tmp-Cast-Ethernet -3 2}"
+ Tmp-String-3 := "%{substring:&Tmp-Cast-Ethernet 1 -3}"
+ Tmp-String-4 := "%{substring:&Tmp-Cast-Ethernet -4 -1}"
+ Tmp-String-5 := "%{substring:&Tmp-Cast-Ethernet 20 5}"
+ Tmp-String-6 := "%{substring:&Tmp-Cast-Ethernet 4 -20}"
+ Tmp-String-7 := "%{substring:&Tmp-Cast-Ethernet 0 17}"
+ Tmp-String-8 := "%{substring:&Tmp-Cast-Ethernet 8 0}"
+ Tmp-String-9 := "%{substring:&Tmp-Cast-Ethernet 9 12}"
+}
+
+if (Tmp-String-1 != ':23') {
+ update reply {
+ Filter-Id += 'fail 4.1'
+ }
+}
+
+if (Tmp-String-2 != ':a') {
+ update reply {
+ Filter-Id += 'fail 4.2'
+ }
+}
+
+if (Tmp-String-3 != '1:23:45:67:89') {
+ update reply {
+ Filter-Id += 'fail 4.3'
+ }
+}
+
+if (Tmp-String-4 != '9:a') {
+ update reply {
+ Filter-Id += 'fail 4.4'
+ }
+}
+
+if (Tmp-String-5 != '') {
+ update reply {
+ Filter-Id += 'fail 4.5'
+ }
+}
+
+if (Tmp-String-6 != '') {
+ update reply {
+ Filter-Id += 'fail 4.6'
+ }
+}
+
+if (Tmp-String-7 != '01:23:45:67:89:ab') {
+ update reply {
+ Filter-Id += 'fail 4.7'
+ }
+}
+
+if (Tmp-String-8 != '') {
+ update reply {
+ Filter-Id += 'fail 4.8'
+ }
+}
+
+if (Tmp-String-9 != '67:89:ab') {
+ update reply {
+ Filter-Id += 'fail 4.9'
+ }
+}
+
+update request {
+ Tmp-String-1 := "%{substring:foo bar 2 3}"
+ Tmp-String-2 := "%{substring:foo bar -3 2}"
+ Tmp-String-3 := "%{substring:foo bar 1 -3}"
+ Tmp-String-4 := "%{substring:foo bar -4 -1}"
+ Tmp-String-5 := "%{substring:foo bar 8 5}"
+ Tmp-String-6 := "%{substring:foo bar 4 -10}"
+ Tmp-String-7 := "%{substring: foo bar 0 9}"
+ Tmp-String-8 := "%{substring: foo bar 5 0}"
+ Tmp-String-9 := "%{substring: foo bar 4 10}"
+}
+
+debug_request
+
+if (Tmp-String-1 != 'o b') {
+ update reply {
+ Filter-Id += 'fail 5.1'
+ }
+}
+
+if (Tmp-String-2 != 'ba') {
+ update reply {
+ Filter-Id += 'fail 5.2'
+ }
+}
+
+if (Tmp-String-3 != 'oo ') {
+ update reply {
+ Filter-Id += 'fail 5.3'
+ }
+}
+
+if (Tmp-String-4 != ' ba') {
+ update reply {
+ Filter-Id += 'fail 5.4'
+ }
+}
+
+if (Tmp-String-5 != '') {
+ update reply {
+ Filter-Id += 'fail 5.5'
+ }
+}
+
+if (Tmp-String-6 != '') {
+ update reply {
+ Filter-Id += 'fail 5.6'
+ }
+}
+
+if (Tmp-String-7 != ' foo bar ') {
+ update reply {
+ Filter-Id += 'fail 5.7'
+ }
+}
+
+if (Tmp-String-8 != '') {
+ update reply {
+ Filter-Id += 'fail 5.8'
+ }
+}
+
+if (Tmp-String-9 != ' bar ') {
+ update reply {
+ Filter-Id += 'fail 5.9'
+ }
+}
diff --git a/src/tests/keywords/switch b/src/tests/keywords/switch
new file mode 100644
index 0000000..f64aeaf
--- /dev/null
+++ b/src/tests/keywords/switch
@@ -0,0 +1,19 @@
+switch &User-Name {
+ case "bob" {
+ update reply {
+ Filter-Id := "filter"
+ }
+ }
+
+ case "doug" {
+ update reply {
+ Filter-Id := "doug"
+ }
+ }
+
+ case {
+ update reply {
+ Filter-Id := "default"
+ }
+ }
+}
diff --git a/src/tests/keywords/switch-attr-cast b/src/tests/keywords/switch-attr-cast
new file mode 100644
index 0000000..e271a18
--- /dev/null
+++ b/src/tests/keywords/switch-attr-cast
@@ -0,0 +1,34 @@
+#
+# PRE: switch switch-attr-cmp
+#
+
+update request {
+ Service-Type := Login-User
+ Filter-Id := "Login-User"
+}
+
+switch &Service-Type {
+ case "%{expr: 1 + 2}" {
+ update reply {
+ Filter-Id := "3"
+ }
+ }
+
+ #
+ # The Filter-Id will get printed to a string,
+ # have the string parsed as a Service-Type attr,
+ # and then that compared to the input Service-Type
+ #
+ case &Filter-Id {
+ update reply {
+ Filter-Id := "filter"
+ }
+ }
+
+ case {
+ update reply {
+ Filter-Id := "default"
+ }
+ }
+
+}
diff --git a/src/tests/keywords/switch-attr-cmp b/src/tests/keywords/switch-attr-cmp
new file mode 100644
index 0000000..e28ded8
--- /dev/null
+++ b/src/tests/keywords/switch-attr-cmp
@@ -0,0 +1,36 @@
+#
+# PRE: switch
+#
+update request {
+ Tmp-String-0 := &User-Name
+}
+
+#
+# A switch statement where we compare two attributes
+#
+switch &User-Name {
+ case &Tmp-String-0 {
+ update reply {
+ Filter-Id := "filter"
+ }
+ }
+
+ case "bob" {
+ update reply {
+ Filter-Id := "failed 0"
+ }
+ }
+
+ case "doug" {
+ update reply {
+ Filter-Id := "failed 1"
+ }
+ }
+
+ case {
+ update reply {
+ Filter-Id := "failed 2"
+ }
+ }
+
+}
diff --git a/src/tests/keywords/switch-default b/src/tests/keywords/switch-default
new file mode 100644
index 0000000..6115ed9
--- /dev/null
+++ b/src/tests/keywords/switch-default
@@ -0,0 +1,22 @@
+# PRE: switch
+#
+switch User-Name {
+ case "harry" {
+ update reply {
+ Filter-Id := "harry"
+ }
+ }
+
+ case "doug" {
+ update reply {
+ Filter-Id := "doug"
+ }
+ }
+
+ case {
+ update reply {
+ Filter-Id := "filter"
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/tests/keywords/switch-escape b/src/tests/keywords/switch-escape
new file mode 100644
index 0000000..50d9fdf
--- /dev/null
+++ b/src/tests/keywords/switch-escape
@@ -0,0 +1,43 @@
+update request {
+ &Tmp-String-0 := 'foo'
+}
+
+switch "%{tolower:%{request:Tmp-String-0}}" {
+ case 'foo' {
+ update reply {
+ Filter-Id := "filter"
+ }
+ }
+
+ case '' {
+ update reply {
+ Filter-Id += "fail-empty-1"
+ }
+ }
+
+ case {
+ update reply {
+ Filter-Id += "fail-default-1"
+ }
+ }
+}
+
+switch "%{request:Tmp-String-0}" {
+ case 'foo' {
+ update reply {
+ Filter-Id := "filter"
+ }
+ }
+
+ case '' {
+ update reply {
+ Filter-Id += "fail-empty-2"
+ }
+ }
+
+ case {
+ update reply {
+ Filter-Id += "fail-default-2"
+ }
+ }
+}
diff --git a/src/tests/keywords/switch-nodefault b/src/tests/keywords/switch-nodefault
new file mode 100644
index 0000000..5fb9469
--- /dev/null
+++ b/src/tests/keywords/switch-nodefault
@@ -0,0 +1,22 @@
+#
+# User-Name is "bob", and a switch statement
+# with no "default" should not crash the server.
+#
+switch &User-Name {
+ case "doug" {
+ update reply {
+ Filter-Id := "doug"
+ }
+ }
+}
+
+if (&reply:Filter-Id) {
+ update reply {
+ Filter-Id := "fail 1"
+ }
+}
+else {
+ update reply {
+ Filter-Id := "filter"
+ }
+} \ No newline at end of file
diff --git a/src/tests/keywords/switch-value-error b/src/tests/keywords/switch-value-error
new file mode 100644
index 0000000..18db9e1
--- /dev/null
+++ b/src/tests/keywords/switch-value-error
@@ -0,0 +1,29 @@
+#
+# PRE: switch
+#
+switch &Service-Type {
+ case "%{expr: 1 + 2}" {
+ update reply {
+ Filter-Id := "3"
+ }
+ }
+
+ case Login-User {
+ update reply {
+ Filter-Id := "Login-User"
+ }
+ }
+
+ case No-Such-Value { # ERROR
+ update reply {
+ Filter-Id := "FAILED"
+ }
+ }
+
+ case {
+ update reply {
+ Filter-Id := "default"
+ }
+ }
+
+}
diff --git a/src/tests/keywords/switch-value-error2 b/src/tests/keywords/switch-value-error2
new file mode 100644
index 0000000..a291e7b
--- /dev/null
+++ b/src/tests/keywords/switch-value-error2
@@ -0,0 +1,27 @@
+#
+# PRE: switch-value-error
+#
+# The same as "switch-value-error", but the attribute
+# is hidden inside of an xlat expansion. We now turn
+# simple attribute xlats into templates.
+#
+switch "%{Service-Type}" { # ERROR
+ case "%{expr: 1 + 2}" {
+ update reply {
+ Filter-Id := "3"
+ }
+ }
+
+ case Login-User {
+ update reply {
+ Filter-Id := "Login-User"
+ }
+ }
+
+ case {
+ update reply {
+ Filter-Id := "default"
+ }
+ }
+
+}
diff --git a/src/tests/keywords/switch-virtual b/src/tests/keywords/switch-virtual
new file mode 100644
index 0000000..659604d
--- /dev/null
+++ b/src/tests/keywords/switch-virtual
@@ -0,0 +1,23 @@
+#
+# PRE: update switch
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+#
+# Virtual attribute references get mashed to xlats
+#
+switch &Packet-Type {
+ case Access-Request {
+ update reply {
+ Filter-Id := "filter"
+ }
+ }
+
+ case {
+ update reply {
+ Filter-Id := "fail"
+ }
+ }
+} \ No newline at end of file
diff --git a/src/tests/keywords/switch-xlat-error b/src/tests/keywords/switch-xlat-error
new file mode 100644
index 0000000..d03f6d3
--- /dev/null
+++ b/src/tests/keywords/switch-xlat-error
@@ -0,0 +1,17 @@
+#
+# PRE: switch
+#
+switch &User-Name {
+ case "%{no-such-module:bob}" { # ERROR
+ update reply {
+ Filter-Id := "fail"
+ }
+ }
+
+ case {
+ update reply {
+ Filter-Id := "default"
+ }
+ }
+
+}
diff --git a/src/tests/keywords/truncation b/src/tests/keywords/truncation
new file mode 100644
index 0000000..8217e36
--- /dev/null
+++ b/src/tests/keywords/truncation
@@ -0,0 +1,109 @@
+# cat /dev/urandom | env LC_CTYPE=C tr -cd 'a-f0-9' | head -c <n>
+
+update reply {
+ Filter-Id := "filter"
+}
+
+# 8192 - 0x (2) - '' (2) there are unlikely to be any static buffers this big outside of the conffile parser
+update request {
+ Tmp-Octets-0 := '0x\
+d8abccb7834711af1de1812be2579febe946f5d7beef6fa5c7074c0cb917e9b91e23e14b016f27610097c16c0e0fad88176e077b24198c770746159\
+05b8810d1c8b774d98889fa5c6027cde5e9c56dd4f7c48298c7713aeca5ba5dcfd506032ad05b1396f50e825b633d5a6af0dce6181b640287e03a65\
+8734df46a86341556f28455b3f377313a5a2ac8c8267b8a5de559b95f9b493a68b9e0278485f9e3914d702b2b7b90ee85ff393461f197386d09b836\
+5a8ae85ea025aea095f5d834c2ddf21e9a16b945da03c97f8a52687f19c605af9a245878b4bc5ed15b7937371ad629081159cf7de613d02c43f5cab\
+3529abbe61a6da1e55c685c2310c8eccb452f9758bf63fddfa58cdffb5cbf912f90e628310978dd1b3b7c2d3a08dd7ca6ca51081a114b013cc9d4c1\
+9f5ce0b1af81166c9402c105019a0fa9d15996c4f3d5fa35226bf88166598ff4f619322866df276d8b92fad06d120470c29217d29abb96ff861751e\
+acebbe839e28287c8cd485769d9a09e5bab524bde5776e15037d19503a2d032658e21aec7090032707ba0d662fcfa99e5e5ec183f8c63c6022bf281\
+22cf090fb80263f70c523aa0e00dd9f610c5b669754092253edd6f83b57bdd7a251514cdd99ab86cfa98b11469d1d1118aba0a149db6f0ccceb2f17\
+06bf33f657c7b8547dc284dfc10fdc1678c184901aa17f756d18df43409e1c4ead239cb78c46a9c412ccb097bba2244bb680bb249c15004d796731f\
+3890c1f94d8e0eda81487da9a65f817b5dbac4e639fce063f61a7db87d8b9d4b855120f56c3a0d57cb61ced57092506da881337280a18b2729717f5\
+133978362ca7941f9dbe9462ae5a46886c4d3c51b633278d6ad58a331c95e481cbbae866f046cb1a59ae2545fa32b12d4772244fd24bb91f7611d0c\
+beffbc5dee309c671fb5a171eacf45b20641e1cb4db306965eb43e98cb900ee805065c7adf4db3cdb97bbf8116dbb29f5ac07c7041a5f02d1a4fd1f\
+a7be6da388146d6b80ae8e152ac873cbf1f88c98cdd2e1de02df76461a1c50139f558f43ac1a91824fc48ea35976fab29d412f1ad990e528893ad4c\
+b6045b63d2a7af0657f1c348302e674ef0b39bd689b3b33dd3664c3e2386d0ea3241e70d2a2990b6243638d68033cc6040758e11f82ef783db2ae94\
+d2a8dbccd25fe2dce554837a2ca424199e4bd5442215623a77bffd603b575c847f88763fff1d6d0ff314851b1af06f99fb44d0a3115ad4389462452\
+c2331be24d486f6b5f2224cdfe44909e0a3bec47cdf6fe49fa60518e37950f986677e7f45617adcb20bc02990d3fe60de54ad2d1950bdd423e752c4\
+60f4eeaf2eeece07f3dea26db63cc0bc222a6f3228131cdb3360b9ec7e8f861c3e0bc822f9da00fef17f44ab4de7c166e5cb07d5c671f0364d4bccb\
+dc7f24868e066998bc8b3ab9052171b967162d536082b0a11c24f263c0e5f656b8349f970124af712cf098e8b08910aefcf7d42dd8a74e84ae6f9c4\
+006aa09483715b514833291c9d2e2bcdda567a16a493ac4fa17d5dff7a04b9f7f967febe7a8bdf911b1b668716be73fbf6f2416609224494dca1a18\
+1c1c7bb298671d39bdc1f664c833c9d2d4c08a65866db885929611fcd0c33139bc2959a865cbb686694896b539daa1db265ba78298cb3a09a331ef0\
+1423a7fd04b38ccdfbf1182a493c7a53c720a2dcf486249ff2b674341e77c2c0d5eb78efc07f295d562a77667457df58ea419c488680b9ce51aac01\
+4cd78984f70f29aec2a9b77fd45d38e0b07257b71f2146ccb11cd07681cbfff787b39b7ce9c42ce60513eccec2b490c68378e86bd441735b30410be\
+8710e71b2a326bd8b929abde80f25a0adb312f8ed5bb748866ddd8fb5fa81855d194afbdb1d511be453e21ab3f6481e47fa86b0697342706f85b8e8\
+84c6bf427390ef4c62532797de25004bb9d293e196ed29950e14682c68a8b6ab7c03256bc493b61eb3e44dc16726fcedb6c6776e2e9e0b3450fe6c6\
+24eef42e4806ef7c8abc367b7f1c83b7955ff6579d03362b428ca7711d228cf88402b60fd162ef4819216b9db66b4a1b0a5e0651a7901290eafa3d5\
+c698e40f73b3d66345d9e328d50ef2c767d370879890980d0d989de122ab22d7ef9979832b022e676eba4630d49434ec70025b44cb7146f9688d102\
+ceb55e6ca8ee9b9a66327da80f46367f452de7e2a84bf702b155539c7fa488d86c83faf3f150fda6d75eb476e2c31d73cea88148d9f02d178b0c098\
+5b4061830231ca405fbf41547ed9c036f53fc706b0fd1bb084e84a9e6dec81b4b4eea46c5ea26e99993ee930ccfeff4a185ffbce8a937243fa4734b\
+c24abbf465b0cd1f87686d44762140b5dfe57ae904cd8ed121f055d8a1bbf9c8a977133215f135e563b935451a7f797e72ef8f96390bc2477ad7aac\
+d2679557bac3b9a030926ff0801cc692dd641cf4f8a7bd9bdee21cdab3f7fbcf7d1d0de51101c5d816b19db49109958a8e75662ac700653112cd86c\
+10cbf45d3ea16f861de2d46714dfba8a34c1ad7ed4d5484802270187c3c7f70f7a51858479d8504e3b6267194404011b416329401ec55940f890f33\
+97d4bb3eab46990a4a03cdae9951ccd0bd42584ec88262f7d80193f09843c1fca7627b9cf708c646e84883b19fe3a8987912d04f1c0d1c779b740a7\
+fea0270374416a3987dd91d706e22e8c9698ef870d7f80d1c3042914a34736c185e542976d0145531c8eb20c3a6c5a5f747e97228ff55425fd93222\
+2d802ca36ac61555f69be4d645532579ddadb24a821f0bab1fb1f3b724ed4759b5303fe0015626ad00879e1a363d5e8b3aa0b7f3e572df3dc5f7352\
+7eb30b78e70dbe6ca81052315925d3892485168dd6f900289d1a0f9285e52988480bfda6a2c8b28cbd175b4dec735240c8f79afd5c93ed058da5551\
+8f20fb09edf4b4112c352604876f4083b68e534874d0b9ce2f977be2bcea0f9c123e119a70d269bc26ece608c39c98dbb069118148b01e403c16076\
+330b5d7929a7a7d724a7030d514ff0e38bee78e0044ffa508fc9793d7fc59075fd825e68d34e58568bffd8b20791862a8d0686763e49f1e3ed8a728\
+e33bb32a57c6374d153f93ba3cd283ce0af9a0a3b0ced4f83e3d604ff66f6584a35b51938ff4a0282c51f6f8561bd77b842ed0fd39fc43be825652f\
+f2c021f2b4d2b8c8ed2cf8eeb16f86863e59b374eda5a17e1bfc0b5a54169dd3e62b84a81bc1afd5cc3a1b193484428bcc3afb5344da990697f6787\
+5f72bc1ab04f52f75bc671b8d1239fc811d44032822abc95f8ea3465cdabdce5f83d5895d50fcbd97ad88e6b193172a4cf5f51814d9348498b1736e\
+de25f8b5afc8556d3b9573cb2ae8f51e8f0bca2f048d883592c9ab317e87864d48ca901e2aa2774360c77ba3f0a76c21f10155e32e91a2239d00e6b\
+0279bbc8977fadbea04b7b67575437fd5f6611a8889ecbde5fe45fcab8db9b62e831aadcd0ade54a94d30b6963d727979c116ef9f4a9a16a0e7a76e\
+d4f1177ad0ac7022beb12581629a7fd36c8ca5eeaa40d1a0d88cb0f701b288baae6c00a74c0b0dc3ffa6e9710bf3264684c1b9ff220710b2348cc93\
+d658d9eddae36232a0d0e06439d5dafdc7271eafbb3d16a17e36de7470c7a5ed034d735e2c216073ba6b4be7b319718d5afbd634684f5468d26afd0\
+63edfbc7fe6c8e98914eff397fcfdf44a10b84ba90b30d1a5da6823aab1792d666b32bdd0516ce3590c5dc2b49a6eabdb28cdc72dbc24e60ba0883f\
+953f0a99d29c1089b06063588b8bd965ba42e27f618d2b046fb3db93161981bbcd67fc965b0084ac25993445732b52d18cd2868e6b545214a2d501a\
+0002d2043d9a9790905dde296f16517e295dd295a97991c205d50dba331e86f06b0195e52fdd21f8e65e6b00fe7165e1a951805e5947aaee125f180\
+a2127375d8898aba1af3a3db34470e78bed99917a6bb87725788c1e2c8f35d6022d51bafdc3e69673cbdd80dc9cfde7dc1bd06e3a6437c720cb7b6c\
+2b3bc808111593d5f2a88c6e5b499c51155f4ff0267606abcbfa03dc5e4c108e7f3d33989952df2f4f1476df8fe53eaa39d2338a87b6ccc91587a67\
+bc0da2f5667248ce969b91f1521c5bfda97c8f062b62d768a9d302f4a4bd0e67646c6666a9fea93749a65d06fdcd0168fe81edac542fc287503a592\
+61591a686b418ac2602e2464f6679ef7ecf5387af45e5fdd72224695e454d6676687b03a5858afe71dda7180a59ef2e1d8c81c4a9c8375e62c43711\
+cbd0e7d437a5913bc4d7e8fbb71f68e4805ad41bd624c06e84761dbda3014c1576a866c073ca32b37007884730ac9f831b95db8ce10f0db45e009d0\
+6e1ad8c96c340c98debee0dd0ca9e6694fe7a69306c15e6a7054cfd612e070f538c6ff0c1a34b257df10087b650da852a5b663a5795a335193dfe64\
+1950ea614ae05b0c202cedabd1ebe6da561e167fe81dd9fc190740a20db38f5356e61f8bed7b3db783a6b53e48f408a8b08f9f79738438a3ed0c4bc\
+f444d5922c76ff03c943a3c72c513047aa75caf51381b5c306fdbaedcd4001a2df456c3afef7559241f553dffac2dfd9ac94570e50c571329203bf0\
+38cbdac9bc76f896635356c402f2f5c105eb4e334f7792716e8e8883352870e949623f29bb8fac428bc858ec3297166afe358e1947b0c2337309315\
+062a040129f978b036657e9323550765cc4c14aaa4c24572eb17a0aa940ba336fd8fd385fb46b5b0c067c59ee42922d2702feb7b43d2ffef11cbdf0\
+1f11c92ac18197a387f0633fd19509b19c0b8a2f63983f6c0a6286593c84d5866ae8d91139b141e8bdf3b7ae7df7d92754186e545f4b600fbe69494\
+3ae4bcef324b9a3f47ba5c835b54a010ca42f3b9cc5368278c148c9b02ea8c4c9f244fd49fa69c9a5feac5376e07f52e64be9c873afca3ebf776fe9\
+fc813bd2e58c6d0dfd951570dbff4b5e73ce547cd100ab320b6944e887d611b3425bfcdf3bfa852ff5804119a1f69e33d624eda6f9e5d1fb2811f9f\
+9f5b3224d009e09621a8fcb524f89c6fcf38f7c933aa027de2b48aa9ddbfa55f56b383f06b8feef09e4709bc4fdcbab659bab4a277a2fbebb98970e\
+44bd68a61b264c695d8ae27a676e123b5fb60c7c4cbc88f24b6554dcfebfde607500f50ec85f589eaa4213ccaa237598660a66009bc56d55455487a\
+28fa24d62e50df57feab0d1c8ec77b1085002d922c52d4a3092f8693c3b9ba9725a4d225637350812b534e93209b414b4516642eca67490199a9217\
+614322972fa2fcc5c7fc2695e9b5762d442e2c7dd8403d14228aa90df58e2ffe1a3c9922f6c5d62649664b63c017fbf3859723e7e97ea6710097683\
+6408a97de3ad7d902c7be0296fb3d476de2460602f65eb7edacc2e4f49b1652fe8948f7afb1cc9f83f3283e6013304160cd2ac7c311c492133252a6\
+5ccf45c5af9c05a47905ac8bfe55f9b912c3fca856abe7863f87392b6f6b0069e3d8412a056b1034aaac506bf2ca51760aa180c5f43a751beb06e88\
+fc6afab4c5dd4aae4a2e6f0293c15278d557c2925acba90c73eeea09ebb95f0d469aa77ae983a0e69dacd55bd8a7e78a41df5227de35af05127fa3b\
+a02f4a3ab98d75992d68a15d393387fe9ef01041569570ad6fe884764e55567311bcacfcffae76554dcfebfde607500f50ec85f589eaade607500f5\
+3ae4bcef324b9a3f47ba5c835b54a010ca42f3b9cc5368278c148c9b02ea8c4c9f244fd49fab'
+}
+
+# Actual length of octet string is 4084 bytes
+update request {
+ Tmp-Integer-0 := "%{length:Tmp-Octets-0}"
+}
+
+if (Tmp-Integer-0 != 4084) {
+ update reply {
+ Filter-Id += 'Fail 0'
+ }
+}
+
+# Octets are expanded to 8168 hexits
+if ("%{Tmp-Octets-0}" !~ /^0x([0-9a-f]+)$/) {
+ update reply {
+ Filter-Id += 'Fail 1'
+ }
+}
+
+update request {
+ Tmp-String-0 := "%{1}"
+}
+
+if ("%{length:Tmp-String-0}" != 8168) {
+ update reply {
+ Filter-Id += 'Fail 2'
+ }
+}
+
+# We can't do any more until all the xlat code uses dynamically allocated buffers
diff --git a/src/tests/keywords/unknown b/src/tests/keywords/unknown
new file mode 100644
index 0000000..eb96591
--- /dev/null
+++ b/src/tests/keywords/unknown
@@ -0,0 +1,84 @@
+#
+# PRE: wimax
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update reply {
+ Filter-Id := "filter"
+}
+
+update request {
+ FreeRADIUS-Proxied-To := 127.0.0.2
+}
+
+#
+# Check that a known but malformed attr is malformed
+#
+update request {
+ Attr-26.24757.84.9.5.7 = 0xab
+}
+
+if (&Attr-26.24757.84.9.5.7 != 0xab) {
+ update reply {
+ Filter-Id += 'Fail 1'
+ }
+}
+
+#
+# Check that an unknown attr is OK
+#
+update request {
+ Attr-26.24757.84.9.5.15 = 0xabcdef
+}
+
+if (&Attr-26.24757.84.9.5.15 != 0xabcdef) {
+ update reply {
+ Filter-Id += 'Fail 2'
+ }
+}
+
+#
+# Check that unknown attributes which are defined
+# get automatically resolved to the real attribute.
+#
+if (&Vendor-11344-Attr-1 == 127.0.0.1) {
+ update reply {
+ Filter-Id += 'Fail 3'
+ }
+}
+
+if (&Vendor-11344-Attr-1 != 127.0.0.2) {
+ update reply {
+ Filter-Id += 'Fail 4'
+ }
+}
+
+update request {
+ &Vendor-11344-Attr-1 := 127.0.0.1
+}
+
+if (&FreeRADIUS-Proxied-To == 127.0.0.2) {
+ update reply {
+ Filter-Id += 'Fail 5'
+ }
+}
+
+if (&FreeRADIUS-Proxied-To != 127.0.0.1) {
+ update reply {
+ Filter-Id += 'Fail 6'
+ }
+}
+
+if (&Vendor-11344-Attr-1 == 127.0.0.2) {
+ update reply {
+ Filter-Id += 'Fail 7'
+ }
+}
+
+if (&Vendor-11344-Attr-1 != 127.0.0.1) {
+ update reply {
+ Filter-Id += 'Fail 8'
+ }
+}
diff --git a/src/tests/keywords/unknown-if b/src/tests/keywords/unknown-if
new file mode 100644
index 0000000..3dccbff
--- /dev/null
+++ b/src/tests/keywords/unknown-if
@@ -0,0 +1,8 @@
+#
+# PRE: unknown-update
+#
+if (&This-Does-Not-Exist == 1) { # ERROR
+ update reply {
+ Filter-Id := "fail"
+ }
+}
diff --git a/src/tests/keywords/unknown-name b/src/tests/keywords/unknown-name
new file mode 100644
index 0000000..5a99ac9
--- /dev/null
+++ b/src/tests/keywords/unknown-name
@@ -0,0 +1,15 @@
+#
+# PRE: update unknown
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+#
+# This delayed binding should result in a parse error
+#
+if (&User-Name == &Foo-Bar-Baz) { # ERROR
+ update reply {
+ Filter-Id += "fail-2"
+ }
+}
diff --git a/src/tests/keywords/unknown-update b/src/tests/keywords/unknown-update
new file mode 100644
index 0000000..1403879
--- /dev/null
+++ b/src/tests/keywords/unknown-update
@@ -0,0 +1,6 @@
+#
+# PRE: update unknown
+#
+update control {
+ This-Does-Not-Exist = 1 # ERROR
+}
diff --git a/src/tests/keywords/update b/src/tests/keywords/update
new file mode 100644
index 0000000..97a2557
--- /dev/null
+++ b/src/tests/keywords/update
@@ -0,0 +1,7 @@
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update reply {
+ Filter-Id := "filter"
+}
diff --git a/src/tests/keywords/update-add-ref-index b/src/tests/keywords/update-add-ref-index
new file mode 100644
index 0000000..7f5e74a
--- /dev/null
+++ b/src/tests/keywords/update-add-ref-index
@@ -0,0 +1,118 @@
+#
+# PRE: update array
+#
+
+update request {
+ reply:Filter-Id := "filter"
+ Class := 0x01020304
+ Class += 0x05060708
+ Class += 0x090a0b0c
+}
+
+#
+# Copy all the class attributes to Proxy-State
+#
+update request {
+ Proxy-State += &Class[*]
+}
+
+if (&Proxy-State[0] != 0x01020304) {
+ update reply {
+ Filter-Id := "fail 0a"
+ }
+}
+
+# Must be the same as above
+if (&Proxy-State[1] != 0x05060708) {
+ update reply {
+ Filter-Id += "fail 0b"
+ }
+}
+
+if (&Proxy-State[2] != 0x090a0b0c) {
+ update reply {
+ Filter-Id += "fail 0c"
+ }
+}
+
+# must not exist
+if (&Proxy-State[3]) {
+ update reply {
+ Filter-Id += "fail 1"
+ }
+}
+
+# Remove all the Proxy-State attributes
+update request {
+ Proxy-State !* ANY
+}
+
+#
+# Copy the first instance (implicitly) of class
+#
+update request {
+ Proxy-State += &Class
+}
+
+if (&Proxy-State[0] != 0x01020304) {
+ update reply {
+ Filter-Id := "fail 2a"
+ }
+}
+
+# Must be the same as above
+if (&Proxy-State[1]) {
+ update reply {
+ Filter-Id += "fail 2b"
+ }
+}
+
+# Remove all the Proxy-State attributes
+update request {
+ Proxy-State !* ANY
+}
+
+#
+# Copy the first instance (explicitly) of class
+#
+update request {
+ Proxy-State += &Class[0]
+}
+
+if (&Proxy-State[0] != 0x01020304) {
+ update reply {
+ Filter-Id := "fail 3a"
+ }
+}
+
+# Must be the same as above
+if (&Proxy-State[1]) {
+ update reply {
+ Filter-Id += "fail 3b"
+ }
+}
+
+# Remove all the Proxy-State attributes
+update request {
+ Proxy-State !* ANY
+}
+
+#
+# Copy the second instance of class
+#
+update request {
+ Proxy-State += &Class[1]
+}
+
+if (&Proxy-State[0] != 0x05060708) {
+ update reply {
+ Filter-Id := "fail 4a"
+ }
+}
+
+# Must be the same as above
+if (&Proxy-State[1]) {
+ update reply {
+ Filter-Id += "fail 4b"
+ }
+}
diff --git a/src/tests/keywords/update-add-ref-tag b/src/tests/keywords/update-add-ref-tag
new file mode 100644
index 0000000..39800b8
--- /dev/null
+++ b/src/tests/keywords/update-add-ref-tag
@@ -0,0 +1,118 @@
+#
+# PRE: update array
+#
+
+update request {
+ reply:Filter-Id := "filter"
+ request:Tunnel-Server-Endpoint += '192.0.1.1'
+ request:Tunnel-Server-Endpoint += '192.0.1.2'
+ request:Tunnel-Server-Endpoint:1 += '192.0.1.1'
+ request:Tunnel-Server-Endpoint:2 += '192.0.2.1'
+ request:Tunnel-Server-Endpoint:2 += '192.0.2.2'
+ request:Tunnel-Server-Endpoint:3 += '192.0.3.1'
+}
+
+#
+# Copy all Tunnel-Server-Endpoint attributes
+#
+update request {
+ control:Tunnel-Server-Endpoint += &Tunnel-Server-Endpoint[*]
+}
+
+if ((&control:Tunnel-Server-Endpoint[0] != '192.0.1.1') || \
+ (&control:Tunnel-Server-Endpoint[1] != '192.0.1.2') || \
+ (&control:Tunnel-Server-Endpoint[2] != '192.0.1.1') || \
+ (&control:Tunnel-Server-Endpoint[3] != '192.0.2.1') || \
+ (&control:Tunnel-Server-Endpoint[4] != '192.0.2.2') || \
+ (&control:Tunnel-Server-Endpoint[5] != '192.0.3.1') || \
+ &control:Tunnel-Server-Endpoint[6]) {
+ update reply {
+ Filter-Id := "fail 0"
+ }
+}
+
+#
+# Clear out control attributes...
+#
+update control {
+ Tunnel-Server-Endpoint !* ANY
+}
+
+#
+# Copy all Tunnel-Server-Endpoint attributes with tag 2
+#
+update request {
+ control:Tunnel-Server-Endpoint += &Tunnel-Server-Endpoint:2[*]
+}
+
+if ((&control:Tunnel-Server-Endpoint[0] != '192.0.2.1') || \
+ (&control:Tunnel-Server-Endpoint[1] != '192.0.2.2') || \
+ &control:Tunnel-Server-Endpoint[2]) {
+ update reply {
+ Filter-Id := "fail 1"
+ }
+}
+
+#
+# Clear out control attributes...
+#
+update control {
+ Tunnel-Server-Endpoint !* ANY
+}
+
+#
+# Copy all Tunnel-Server-Endpoint attributes with no tag
+#
+update request {
+ control:Tunnel-Server-Endpoint += &Tunnel-Server-Endpoint:0[*]
+}
+
+if ((&control:Tunnel-Server-Endpoint[0] != '192.0.1.1') || \
+ (&control:Tunnel-Server-Endpoint[1] != '192.0.1.2') || \
+ &control:Tunnel-Server-Endpoint[2]) {
+ update reply {
+ Filter-Id := "fail 2"
+ }
+}
+
+#
+# Clear out control attributes...
+#
+update control {
+ Tunnel-Server-Endpoint !* ANY
+}
+
+#
+# Copy the first attribute with tag 2 (implicit)
+#
+update request {
+ control:Tunnel-Server-Endpoint += &Tunnel-Server-Endpoint:2
+}
+
+if ((&control:Tunnel-Server-Endpoint[0] != '192.0.2.1') || \
+ &control:Tunnel-Server-Endpoint[1]) {
+ update reply {
+ Filter-Id := "fail 3"
+ }
+}
+
+#
+# Clear out control attributes...
+#
+update control {
+ Tunnel-Server-Endpoint !* ANY
+}
+
+#
+# Copy the first attribute with tag 2 (explicit)
+#
+update request {
+ control:Tunnel-Server-Endpoint += &Tunnel-Server-Endpoint:2[0]
+}
+
+if ((&control:Tunnel-Server-Endpoint[0] != '192.0.2.1') || \
+ &control:Tunnel-Server-Endpoint[1]) {
+ update reply {
+ Filter-Id := "fail 4"
+ }
+}
diff --git a/src/tests/keywords/update-all b/src/tests/keywords/update-all
new file mode 100644
index 0000000..549a122
--- /dev/null
+++ b/src/tests/keywords/update-all
@@ -0,0 +1,9 @@
+#
+# PRE: update
+#
+# A more generic "update" mechanism
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := "filter"
+}
diff --git a/src/tests/keywords/update-array b/src/tests/keywords/update-array
new file mode 100644
index 0000000..c872f4e
--- /dev/null
+++ b/src/tests/keywords/update-array
@@ -0,0 +1,63 @@
+#
+# PRE: update array
+#
+
+update request {
+ Class := 0x01020304
+ Class += 0x05060708
+ Class += 0x090a0b0c
+}
+
+
+#
+# Use array references in the RHS
+# of the update section
+#
+
+update request {
+ Proxy-State += &Class[0]
+ Proxy-State += &Class[1]
+ Proxy-State += &Class[2]
+}
+
+if (&Proxy-State != 0x01020304) {
+ update reply {
+ Filter-Id := "fail 0"
+ }
+}
+
+# Must be the same as above
+if (&Proxy-State[0] != 0x01020304) {
+ update reply {
+ Filter-Id += "fail 0a"
+ }
+}
+
+if (&Proxy-State[1] != 0x05060708) {
+ update reply {
+ Filter-Id += "fail 1"
+ }
+}
+
+if (&Proxy-State[2] != 0x090a0b0c) {
+ update reply {
+ Filter-Id += "fail 2"
+ }
+}
+
+# must not exist
+if (&Proxy-State[3]) {
+ update reply {
+ Filter-Id += "fail 3"
+ }
+}
+
+#
+# The test passes only if no test above
+# added a Filter-Id
+#
+if (!reply:Filter-Id) {
+ update reply {
+ Filter-Id := "filter"
+ }
+} \ No newline at end of file
diff --git a/src/tests/keywords/update-delete b/src/tests/keywords/update-delete
new file mode 100644
index 0000000..a5c2d5a
--- /dev/null
+++ b/src/tests/keywords/update-delete
@@ -0,0 +1,40 @@
+#
+# PRE: update
+#
+# Remove all attributes in a list
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+update request {
+ Tmp-String-0 := 'foobarbaz'
+ Tmp-Integer-0 := 123456789
+ Tmp-IP-Address-0 := 192.0.2.1
+}
+
+if ((Tmp-String-0 != 'foobarbaz') || (Tmp-Integer-0 != 123456789) || (Tmp-IP-Address-0 != 192.0.2.1)) {
+ update {
+ reply:Filter-Id := 'fail'
+ }
+}
+
+# Remove all attributes in the control list
+update {
+ request: !* ANY
+}
+
+# All attributes should now of been removed
+if ((Tmp-String-0 && (Tmp-String-0 == 'foobarbaz')) || \
+ (Tmp-Integer-0 && (Tmp-Integer-0 == 123456789)) || \
+ (Tmp-IP-Address-0 && (Tmp-IP-Address-0 == 192.0.2.1))) {
+ update {
+ reply:Filter-Id := 'fail'
+ }
+}
+
+# This will of been removed too
+update request {
+ User-Password := 'hello'
+}
diff --git a/src/tests/keywords/update-error b/src/tests/keywords/update-error
new file mode 100644
index 0000000..92e0ed2
--- /dev/null
+++ b/src/tests/keywords/update-error
@@ -0,0 +1,9 @@
+#
+# PRE: update
+#
+# It's an error to update lists that don't exist.
+#
+update no-such-list { # ERROR
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := "filter"
+}
diff --git a/src/tests/keywords/update-error-2 b/src/tests/keywords/update-error-2
new file mode 100644
index 0000000..1fe98f0
--- /dev/null
+++ b/src/tests/keywords/update-error-2
@@ -0,0 +1,9 @@
+#
+# PRE: update-error
+#
+# It's an error to update lists that don't exist.
+#
+update {
+ no-such-list:Cleartext-Password := 'hello' # ERROR
+ reply:Filter-Id := "filter"
+}
diff --git a/src/tests/keywords/update-error-3 b/src/tests/keywords/update-error-3
new file mode 100644
index 0000000..ffab73a
--- /dev/null
+++ b/src/tests/keywords/update-error-3
@@ -0,0 +1,10 @@
+#
+# PRE: update-error
+#
+# It's an error to assign literal values which are not
+# part of the set of enumerated values for an attribute
+#
+update {
+ Service-Type := 'hello' # ERROR
+ reply:Filter-Id := "filter"
+}
diff --git a/src/tests/keywords/update-exec b/src/tests/keywords/update-exec
new file mode 100644
index 0000000..b9a0f73
--- /dev/null
+++ b/src/tests/keywords/update-exec
@@ -0,0 +1,94 @@
+#
+# PRE: update if redundant
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update reply {
+ Filter-Id := "filter"
+}
+
+#
+# Exec with script output to attribute
+#
+update request {
+ Tmp-String-0 = `/bin/sh -c "echo 'foo bar baz'"`
+}
+
+if (Tmp-String-0 != "foo bar baz") {
+ update reply {
+ Filter-Id += "fail 1"
+ }
+}
+
+#
+# Exec with output to list (single attribute)
+#
+update {
+ request: = `/bin/sh -c "echo Tmp-String-0 := foo"`
+}
+
+if (Tmp-String-0 != 'foo') {
+ update reply {
+ Filter-Id += "fail 2"
+ }
+}
+
+#
+# Exec with output to list (multiple attributes)
+#
+update {
+ request: = `/bin/sh -c 'echo Tmp-String-0 := foo, Tmp-String-1 := bar'`
+}
+
+if ((Tmp-String-0 != 'foo') || (Tmp-String-1 != 'bar')) {
+ update reply {
+ Filter-Id += "fail 3"
+ }
+}
+
+#
+# Failed exec (malformed attributes) - check no attributes are added
+#
+update request {
+ Tmp-String-0 !* ANY
+ Tmp-String-1 !* ANY
+}
+
+redundant {
+ group {
+ update {
+ request: = `/bin/sh -c 'echo Tmp-String-0 := foo, Tmp-String-1 ?= bar'`
+ }
+ }
+ ok
+}
+if (Tmp-String-0 || Tmp-String-1) {
+ update reply {
+ Filter-Id += "fail 4"
+ }
+}
+
+#
+# Exec with output to list - error code
+#
+update request {
+ Tmp-String-0 !* ANY
+ Tmp-String-1 !* ANY
+}
+
+redundant {
+ group {
+ update {
+ request: = `/bin/sh -c 'echo Tmp-String-0 := foo; exit 64'`
+ }
+ }
+ ok
+}
+if (Tmp-String-0) {
+ update reply {
+ Filter-Id += "fail 5"
+ }
+}
+
diff --git a/src/tests/keywords/update-filter b/src/tests/keywords/update-filter
new file mode 100644
index 0000000..30f96be
--- /dev/null
+++ b/src/tests/keywords/update-filter
@@ -0,0 +1,75 @@
+#
+# PRE: update
+#
+update control {
+ Tmp-Integer-0 := 5
+ Tmp-Integer-0 += 10
+ Tmp-Integer-0 += 15
+ Tmp-Integer-0 += 20
+ Tmp-String-0 := 'foo'
+ Tmp-String-0 += 'baz'
+ Tmp-String-0 += 'boink'
+}
+
+#
+# Reset the request list
+#
+update {
+ &request: !* ANY
+ &request: += &control:[*]
+}
+
+debug_request
+
+#
+# Only matching attributes of the specified type should remain
+#
+update request {
+ &Tmp-Integer-0 == 10
+}
+
+if (&Tmp-Integer-0[0] != 10) {
+ update reply {
+ Filter-Id += "fail 1"
+ }
+}
+
+if ("%{Tmp-Integer-0[#]}" != 1) {
+ update reply {
+ Filter-Id += "fail 2"
+ }
+}
+
+if ("%{Tmp-String-0[#]}" != 3) {
+ update reply {
+ Filter-Id += "fail 3"
+ }
+}
+
+debug_request
+
+#
+# Only matching attributes of the specified type should remain
+#
+update request {
+ &Tmp-String-0 == 'baz'
+}
+
+if (&Tmp-String-0[0] != 'baz') {
+ update reply {
+ Filter-Id += "fail 4"
+ }
+}
+
+if ("%{Tmp-String-0[#]}" != 1) {
+ update reply {
+ Filter-Id += "fail 5"
+ }
+}
+
+update {
+ control:Auth-Type := Accept
+ reply:Filter-Id := "filter"
+}
+
+debug_request
diff --git a/src/tests/keywords/update-index b/src/tests/keywords/update-index
new file mode 100644
index 0000000..390aca7
--- /dev/null
+++ b/src/tests/keywords/update-index
@@ -0,0 +1,52 @@
+#
+# PRE: update update-remove-index
+#
+# A more generic "update" mechanism
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := "filter"
+}
+
+update {
+ control:Reply-Message += 'a'
+ control:Reply-Message += 'b'
+ control:Reply-Message += 'c'
+}
+
+if ((&control:Reply-Message[0] != 'a') || (&control:Reply-Message[1] != 'b') || (&control:Reply-Message[2] != 'c')) {
+ update {
+ reply:Filter-Id := 'Fail 0'
+ }
+}
+
+# Overwrite a specific index, and check the value here is replaced
+update {
+ &control:Reply-Message[1] := 'd'
+}
+
+if ((&control:Reply-Message[0] != 'a') || (&control:Reply-Message[1] != 'd') || (&control:Reply-Message[2] != 'c')) {
+ update {
+ reply:Filter-Id := 'Fail 1'
+ }
+}
+
+# Check isolation...
+update {
+ &control:Reply-Message[0] := &control:Reply-Message[0]
+}
+
+if ((&control:Reply-Message[0] != 'a') || (&control:Reply-Message[1] != 'd') || (&control:Reply-Message[2] != 'c')) {
+ update {
+ reply:Filter-Id := 'Fail 2'
+ }
+}
+
+# Verify we haven't acquired any extra..
+
+if ("%{control:Reply-Message[#]}" != 3) {
+ update {
+ reply:Filter-Id := 'Fail 3'
+ }
+}
+
diff --git a/src/tests/keywords/update-list-error b/src/tests/keywords/update-list-error
new file mode 100644
index 0000000..2613055
--- /dev/null
+++ b/src/tests/keywords/update-list-error
@@ -0,0 +1,19 @@
+#
+# PRE: update
+#
+# It's an error to update lists that don't exist.
+#
+update {
+ request := reply
+ config += request
+ reply !* ANY
+}
+
+update {
+ reply += `/path/to/foo bar baz`
+}
+
+
+update {
+ request := nope # ERROR
+}
diff --git a/src/tests/keywords/update-operator b/src/tests/keywords/update-operator
new file mode 100644
index 0000000..ccbcb16
--- /dev/null
+++ b/src/tests/keywords/update-operator
@@ -0,0 +1,85 @@
+#
+# PRE: update
+#
+
+#
+# Set it.
+#
+update request {
+ NAS-Port := 1000
+}
+
+#
+# Enforce it.
+#
+update request {
+ NAS-Port == 1000
+}
+
+if (NAS-Port != 1000) {
+ update reply {
+ Filter-Id += "fail 1"
+ }
+}
+
+#
+# Enforce to new lower value.
+#
+update request {
+ NAS-Port <= 500
+}
+
+if (NAS-Port != 500) {
+ update reply {
+ Filter-Id += "fail 2 - expected 500, got %{NAS-Port}"
+ }
+}
+
+#
+# Enforce to new higher value
+#
+update request {
+ NAS-Port >= 2000
+}
+
+if (NAS-Port != 2000) {
+ update reply {
+ Filter-Id += "fail 3 - expected 2000, got %{NAS-Port}"
+ }
+}
+
+#
+# Enforce value which previously didn't exist.
+#
+update request {
+ Idle-Timeout >= 14400
+}
+
+if (&request:Idle-Timeout != 14400) {
+ update reply {
+ Filter-Id += "fail Idle-Timeout >= 14400"
+ }
+}
+
+# non-existent attribute
+update request {
+ Class -= 0xabcdef
+}
+
+update request {
+ Class -= &Class
+}
+
+update request {
+ NAS-Port -= &NAS-Port
+}
+
+if (!reply:Filter-Id) {
+ update control {
+ Cleartext-Password := 'hello'
+ }
+
+ update reply {
+ Filter-Id := "filter"
+ }
+}
diff --git a/src/tests/keywords/update-prepend b/src/tests/keywords/update-prepend
new file mode 100644
index 0000000..4ba9335
--- /dev/null
+++ b/src/tests/keywords/update-prepend
@@ -0,0 +1,65 @@
+#
+# PRE: update
+#
+update control {
+ &Tmp-String-0 := 'foo'
+ &Tmp-String-0 += 'baz'
+}
+
+# Reset the request list
+update {
+ &request !* ANY
+ &request += &control
+}
+
+debug_request
+
+# Prepend a single value
+update request {
+ &Tmp-String-0 ^= 'boink'
+}
+
+# The prepended value should be first followd by the other two
+if (("%{Tmp-String-0[0]}" != 'boink') || ("%{Tmp-String-0[1]}" != 'foo') || ("%{Tmp-String-0[2]}" != 'baz')) {
+ update reply {
+ Filter-Id += "fail 1"
+ }
+}
+
+if ("%{Tmp-String-0[#]}" != 3) {
+ update reply {
+ Filter-Id += "fail 1a"
+ }
+}
+
+# Add an extra element to the start of control
+update control {
+ &Tmp-String-0 ^= 'wibble'
+}
+
+# Prepend control to request
+update {
+ &request ^= &control
+}
+
+debug_request
+
+# The attributes should now be "wibble", "foo", "baz", "boink", "foo", "baz"
+if (("%{Tmp-String-0[0]}" != 'wibble') || ("%{Tmp-String-0[1]}" != 'foo') || ("%{Tmp-String-0[2]}" != 'baz') || ("%{Tmp-String-0[3]}" != 'boink') || ("%{Tmp-String-0[4]}" != 'foo') || ("%{Tmp-String-0[5]}" != 'baz')) {
+ update reply {
+ Filter-Id += "fail 2"
+ }
+}
+
+if ("%{Tmp-String-0[#]}" != 6) {
+ update reply {
+ Filter-Id += "fail 2a"
+ }
+}
+
+if (!reply:Filter-Id) {
+ update {
+ &request:User-Password := 'hello'
+ &reply:Filter-Id := 'filter'
+ }
+}
diff --git a/src/tests/keywords/update-remove-any b/src/tests/keywords/update-remove-any
new file mode 100644
index 0000000..e0ef600
--- /dev/null
+++ b/src/tests/keywords/update-remove-any
@@ -0,0 +1,50 @@
+#
+# PRE: update
+#
+# Remove all attributes in a list
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+update {
+ request:Tmp-String-0 := 'foobarbaz'
+ request:Tmp-Integer-0 := 123456789
+ request:Tmp-IP-Address-0 := 192.0.2.1
+ request:Tmp-IP-Address-0 += 192.0.2.2
+ control:Tmp-IP-Address-0 := 192.0.2.1
+ control:Tmp-IP-Address-0 += 192.0.2.3
+}
+
+if (("%{Tmp-IP-Address-0[0]}" != 192.0.2.1) || ("%{Tmp-IP-Address-0[1]}" != 192.0.2.2)) {
+ update {
+ reply:Filter-Id := 'fail 1'
+ }
+}
+
+# Remove all attributes in the control list
+update {
+ request:Tmp-IP-Address-0 !* ANY
+}
+
+# Non Tmp-IP-Address-0 address attributes should still be in the request list
+if ((Tmp-String-0 != 'foobarbaz') || (Tmp-Integer-0 != 123456789)) {
+ update reply {
+ reply:Filter-Id += 'fail 2'
+ }
+}
+
+# There should be no Tmp-IP-Address attributes in the request list
+if (Tmp-IP-Address-0 || ("%{Tmp-IP-Address-0[1]}" != '')) {
+ update {
+ reply:Filter-Id += 'fail 3'
+ }
+}
+
+# But there should still be some in the control list
+if ((control:Tmp-IP-Address-0 != 192.0.2.1) || ("%{control:Tmp-IP-Address-0[1]}" != 192.0.2.3)) {
+ update {
+ reply:Filter-Id += 'fail 4'
+ }
+}
diff --git a/src/tests/keywords/update-remove-index b/src/tests/keywords/update-remove-index
new file mode 100644
index 0000000..58df9a5
--- /dev/null
+++ b/src/tests/keywords/update-remove-index
@@ -0,0 +1,100 @@
+#
+# PRE: update update-remove-value
+#
+# Remove all attributes in a list
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+update {
+ request:Tmp-String-0 := 'foobarbaz'
+ request:Tmp-Integer-0 := 123456789
+ request:Tmp-IP-Address-0 := 192.0.2.1
+ request:Tmp-IP-Address-0 += 192.0.2.2
+ request:Tmp-IP-Address-0 += 192.0.2.3
+ request:Tmp-IP-Address-0 += 192.0.2.2
+ request:Tmp-IP-Address-0 += 192.0.2.4
+}
+
+
+update request {
+ Tmp-IP-Address-0[3] -= 192.0.2.2
+}
+
+# Only the 1st, 2nd, 3rd and 5th Tmp-IP-Address attributes should still be in the list
+if (("%{Tmp-IP-Address-0[0]}" != '192.0.2.1') || \
+ ("%{Tmp-IP-Address-0[1]}" != '192.0.2.2') || \
+ ("%{Tmp-IP-Address-0[2]}" != '192.0.2.3') || \
+ ("%{Tmp-IP-Address-0[3]}" != '192.0.2.4') || \
+ ("%{Tmp-IP-Address-0[4]}" != '')) {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+# There's still a 192.0.2.2 but it's not at index 3
+update request {
+ Tmp-IP-Address-0[3] -= 192.0.2.2
+}
+
+# Should be the same as the previous result
+if (("%{Tmp-IP-Address-0[0]}" != '192.0.2.1') || \
+ ("%{Tmp-IP-Address-0[1]}" != '192.0.2.2') || \
+ ("%{Tmp-IP-Address-0[2]}" != '192.0.2.3') || \
+ ("%{Tmp-IP-Address-0[3]}" != '192.0.2.4') || \
+ ("%{Tmp-IP-Address-0[4]}" != '')) {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+# Remove whatever's at index 0
+update request {
+ Tmp-IP-Address-0[0] !* ANY
+}
+
+# IP address at index 0 should be removed
+if (("%{Tmp-IP-Address-0[0]}" != '192.0.2.2') || \
+ ("%{Tmp-IP-Address-0[1]}" != '192.0.2.3') || \
+ ("%{Tmp-IP-Address-0[2]}" != '192.0.2.4') || \
+ ("%{Tmp-IP-Address-0[3]}" != '')) {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+# Remove whatever's at index 3 (should be nothing)
+update request {
+ Tmp-IP-Address-0[3] !* ANY
+}
+
+# Should be the same as the previous result
+if (("%{Tmp-IP-Address-0[0]}" != '192.0.2.2') || \
+ ("%{Tmp-IP-Address-0[1]}" != '192.0.2.3') || \
+ ("%{Tmp-IP-Address-0[2]}" != '192.0.2.4') || \
+ ("%{Tmp-IP-Address-0[3]}" != '')) {
+ update reply {
+ Filter-Id += 'fail 4'
+ }
+}
+
+# Remove all instances of Tmp-IP-Address
+update request {
+ Tmp-IP-Address-0 !* ANY
+}
+
+# No more IP address attributes!
+if ("%{Tmp-IP-Address-0[0]}" != '') {
+ update reply {
+ Filter-Id += 'fail 5'
+ }
+}
+
+# Non Tmp-IP-Address-0 address attributes should still be in the request list
+if ((Tmp-String-0 != 'foobarbaz') || (Tmp-Integer-0 != 123456789)) {
+ update reply {
+ Filter-Id += 'fail 6'
+ }
+}
diff --git a/src/tests/keywords/update-remove-list b/src/tests/keywords/update-remove-list
new file mode 100644
index 0000000..22ae577
--- /dev/null
+++ b/src/tests/keywords/update-remove-list
@@ -0,0 +1,40 @@
+#
+# PRE: update
+#
+# Remove all attributes in a list
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+update request {
+ Tmp-String-0 := 'foobarbaz'
+ Tmp-Integer-0 := 123456789
+ Tmp-IP-Address-0 := 192.0.2.1
+}
+
+if ((Tmp-String-0 != 'foobarbaz') || (Tmp-Integer-0 != 123456789) || (Tmp-IP-Address-0 != 192.0.2.1)) {
+ update reply {
+ Filter-Id += 'fail 0'
+ }
+}
+
+# Remove all attributes in the control list
+update {
+ request: !* ANY
+}
+
+# All attributes should now of been removed
+if ((Tmp-String-0 && (Tmp-String-0 == 'foobarbaz')) || \
+ (Tmp-Integer-0 && (Tmp-Integer-0 == 123456789)) || \
+ (Tmp-IP-Address-0 && (Tmp-IP-Address-0 == 192.0.2.1))) {
+ update reply {
+ Filter-Id := 'fail 1'
+ }
+}
+
+# This will of been removed too
+update request {
+ User-Password := 'hello'
+}
diff --git a/src/tests/keywords/update-remove-tag b/src/tests/keywords/update-remove-tag
new file mode 100644
index 0000000..3328789
--- /dev/null
+++ b/src/tests/keywords/update-remove-tag
@@ -0,0 +1,275 @@
+#
+# PRE: update update-remove-value update-remove-index update-tag
+#
+# Remove all attributes in a list
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+update {
+ request:Tunnel-Server-Endpoint += '192.0.1.1'
+ request:Tunnel-Server-Endpoint += '192.0.1.2'
+ request:Tunnel-Server-Endpoint:1 += '192.0.1.1'
+ request:Tunnel-Server-Endpoint:2 += '192.0.2.1'
+ request:Tunnel-Server-Endpoint:2 += '192.0.2.2'
+ request:Tunnel-Server-Endpoint:3 += '192.0.3.1'
+ request:Tunnel-Server-Endpoint:3 += '192.0.3.2'
+ request:Tunnel-Server-Endpoint:3 += '192.0.3.3'
+ control: += request:
+}
+
+# Check [#] is working correctly (should probably be another set of tests)
+if (("%{request:Tunnel-Server-Endpoint[#]}" != 8) || \
+ ("%{request:Tunnel-Server-Endpoint:0[#]}" != 2) || \
+ ("%{request:Tunnel-Server-Endpoint:1[#]}" != 1) || \
+ ("%{request:Tunnel-Server-Endpoint:2[#]}" != 2) || \
+ ("%{request:Tunnel-Server-Endpoint:3[#]}" != 3)) {
+ update reply {
+ Filter-Id += 'fail 0'
+ }
+}
+
+update {
+ Tunnel-Server-Endpoint !* ANY
+}
+
+# List should now be empty
+if ("%{request:Tunnel-Server-Endpoint[#]}" != 0) {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+# Reset the list
+update {
+ request: += control:
+}
+
+# Now remove all Tunnel-Server-Endpoint attributes with :2
+update {
+ Tunnel-Server-Endpoint:2 !* ANY
+}
+
+if (("%{request:Tunnel-Server-Endpoint[#]}" != 6) || \
+ ("%{request:Tunnel-Server-Endpoint:0[#]}" != 2) || \
+ ("%{request:Tunnel-Server-Endpoint:1[#]}" != 1) || \
+ ("%{request:Tunnel-Server-Endpoint:2[#]}" != 0) || \
+ ("%{request:Tunnel-Server-Endpoint:3[#]}" != 3)) {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+# Now remove all Tunnel-Server-Endpoint attributes with :0 (no tags)
+update {
+ Tunnel-Server-Endpoint:0 !* ANY
+}
+
+if (("%{request:Tunnel-Server-Endpoint[#]}" != 4) || \
+ ("%{request:Tunnel-Server-Endpoint:0[#]}" != 0) || \
+ ("%{request:Tunnel-Server-Endpoint:1[#]}" != 1) || \
+ ("%{request:Tunnel-Server-Endpoint:2[#]}" != 0) || \
+ ("%{request:Tunnel-Server-Endpoint:3[#]}" != 3)) {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+# Now remove all Tunnel-Server-Endpoint attributes with :3
+update {
+ Tunnel-Server-Endpoint:3 !* ANY
+}
+
+if (("%{request:Tunnel-Server-Endpoint[#]}" != 1) || \
+ ("%{request:Tunnel-Server-Endpoint:0[#]}" != 0) || \
+ ("%{request:Tunnel-Server-Endpoint:1[#]}" != 1) || \
+ ("%{request:Tunnel-Server-Endpoint:2[#]}" != 0) || \
+ ("%{request:Tunnel-Server-Endpoint:3[#]}" != 0)) {
+ update reply {
+ Filter-Id += 'fail 4'
+ }
+}
+
+# Now remove all Tunnel-Server-Endpoint attributes with :1
+update {
+ Tunnel-Server-Endpoint:1 !* ANY
+}
+
+if (("%{request:Tunnel-Server-Endpoint[#]}" != 0) || \
+ ("%{request:Tunnel-Server-Endpoint:0[#]}" != 0) || \
+ ("%{request:Tunnel-Server-Endpoint:1[#]}" != 0) || \
+ ("%{request:Tunnel-Server-Endpoint:2[#]}" != 0) || \
+ ("%{request:Tunnel-Server-Endpoint:3[#]}" != 0)) {
+ update reply {
+ Filter-Id += 'fail 5'
+ }
+}
+
+# Reset the list
+update {
+ request: += control:
+}
+
+# Remove all Tunnel-Server-Endpoint attributes at :3[0] (none)
+update {
+ Tunnel-Server-Endpoint:1[3] !* ANY
+}
+
+if (Tunnel-Server-Endpoint:3[0] != '192.0.3.1') {
+ update reply {
+ Filter-Id += 'fail 6'
+ }
+}
+
+if (Tunnel-Server-Endpoint:3[1] != '192.0.3.2') {
+ update reply {
+ Filter-Id += 'fail 7'
+ }
+}
+
+if (Tunnel-Server-Endpoint:3[2] != '192.0.3.3') {
+ update reply {
+ Filter-Id += 'fail 8'
+ }
+}
+
+# Remove all Tunnel-Server-Endpoint attributes at :3[1]
+update {
+ Tunnel-Server-Endpoint:3[1] !* ANY
+}
+
+if (Tunnel-Server-Endpoint:3[0] != '192.0.3.1') {
+ update reply {
+ Filter-Id += 'fail 9'
+ }
+}
+
+if (Tunnel-Server-Endpoint:3[1] != '192.0.3.3') {
+ update reply {
+ Filter-Id += 'fail 10'
+ }
+}
+
+# Remove any Tunnel-Server-Endpoint with a value of '192.0.1.1' (should remove both tagged and untagged versions)
+update {
+ Tunnel-Server-Endpoint -= '192.0.1.1'
+}
+
+# Also checks whether presence checks for tagged attributes work correctly
+if (request:Tunnel-Server-Endpoint:1) {
+ update reply {
+ Filter-Id += 'fail 11'
+ }
+}
+
+if (request:Tunnel-Server-Endpoint:0[0] != '192.0.1.2') {
+ update reply {
+ Filter-Id += 'fail 12'
+ }
+}
+
+# Remove any Tunnel-Server-Endpoint with a value of '192.0.3.1'
+update {
+ Tunnel-Server-Endpoint:3 -= '192.0.3.2'
+}
+
+if (request:Tunnel-Server-Endpoint:3[0] != '192.0.3.1') {
+ update reply {
+ Filter-Id += 'fail 13'
+ }
+}
+
+if (request:Tunnel-Server-Endpoint:3[1] != '192.0.3.3') {
+ update reply {
+ Filter-Id += 'fail 14'
+ }
+}
+
+# Reset the list
+update {
+ request: !* ANY
+}
+update {
+ request: += control:
+}
+
+# Remove only the tagged version of '192.0.1.1'
+update {
+ request:Tunnel-Server-Endpoint:1 -= '192.0.1.1'
+}
+
+if (request:Tunnel-Server-Endpoint:0[0] != '192.0.1.1') {
+ update reply {
+ Filter-Id += 'fail 15'
+ }
+}
+
+# Reset the list
+update {
+ request: !* ANY
+}
+update {
+ request: += control:
+}
+
+# Remove only the untagged version of '192.0.1.1'
+update {
+ request:Tunnel-Server-Endpoint:0 -= '192.0.1.1'
+}
+
+if (request:Tunnel-Server-Endpoint:1[0] != '192.0.1.1') {
+ update reply {
+ Filter-Id += 'fail 16'
+ }
+}
+
+# Remove the value of Tunnel-Server-Endpoint:3 at index 1 only if it matches '192.0.3.3' (which it does)
+update {
+ Tunnel-Server-Endpoint:3[1] -= '192.0.3.2'
+}
+
+if (Tunnel-Server-Endpoint:3[0] != '192.0.3.1') {
+ update reply {
+ Filter-Id += 'fail 17'
+ }
+}
+
+if (Tunnel-Server-Endpoint:3[1] != '192.0.3.3') {
+ update reply {
+ Filter-Id += 'fail 18'
+ }
+}
+
+# Reset the list
+update {
+ request: !* ANY
+}
+update {
+ request: += control:
+}
+
+# Remove the value of Tunnel-Server-Endpoint:3 at index 1 only if it matches '192.0.3.4' (which it doesn't)
+update {
+ Tunnel-Server-Endpoint:3[1] -= '192.0.3.4'
+}
+
+if (Tunnel-Server-Endpoint:3[0] != '192.0.3.1') {
+ update reply {
+ Filter-Id += 'fail 19'
+ }
+}
+
+if (Tunnel-Server-Endpoint:3[1] != '192.0.3.2') {
+ update reply {
+ Filter-Id += 'fail 20'
+ }
+}
+
+if (Tunnel-Server-Endpoint:3[2] != '192.0.3.3') {
+ update reply {
+ Filter-Id += 'fail 21'
+ }
+}
+
diff --git a/src/tests/keywords/update-remove-value b/src/tests/keywords/update-remove-value
new file mode 100644
index 0000000..3fd1f94
--- /dev/null
+++ b/src/tests/keywords/update-remove-value
@@ -0,0 +1,116 @@
+#
+# PRE: update
+#
+# Remove all attributes in a list
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+update {
+ request:Tmp-String-0 := 'foobarbaz'
+ request:Tmp-Integer-0 := 123456789
+ request:Tmp-IP-Address-0 := 192.0.2.1
+ request:Tmp-IP-Address-0 += 192.0.2.2
+ request:Tmp-IP-Address-0 += 192.0.2.3
+ request:Tmp-IP-Address-0 += 192.0.2.4
+ control:Tmp-IP-Address-0 := 192.0.2.1
+ control:Tmp-IP-Address-0 += 192.0.2.3
+}
+
+if (("%{Tmp-IP-Address-0[0]}" != 192.0.2.1) || \
+ ("%{Tmp-IP-Address-0[1]}" != 192.0.2.2) || \
+ ("%{Tmp-IP-Address-0[2]}" != 192.0.2.3) || \
+ ("%{Tmp-IP-Address-0[3]}" != 192.0.2.4)) {
+ update reply {
+ Filter-Id += 'fail 0'
+ }
+}
+
+# Remove Tmp-IP-Address-0 with a specific value
+update {
+ request:Tmp-IP-Address-0 -= 192.0.2.1
+}
+
+# Only the 2nd, 3rd and 4th Tmp-IP-Address attributes should still be in the list
+if (("%{Tmp-IP-Address-0[0]}" != '192.0.2.2') || \
+ ("%{Tmp-IP-Address-0[1]}" != '192.0.2.3') || \
+ ("%{Tmp-IP-Address-0[2]}" != '192.0.2.4') || \
+ ("%{Tmp-IP-Address-0[3]}" != '')) {
+ update reply {
+ Filter-Id += 'fail 1'
+ }
+}
+
+# Remove Tmp-IP-Address-0 with a specific value (somewhere in the middle)
+update {
+ request:Tmp-IP-Address-0 -= 192.0.2.3
+}
+
+# Only the 1st, and 3rd Tmp-IP-Address attributes should still be in the list
+if (("%{Tmp-IP-Address-0[0]}" != '192.0.2.2') || \
+ ("%{Tmp-IP-Address-0[1]}" != '192.0.2.4') || \
+ ("%{Tmp-IP-Address-0[2]}" != '')) {
+ update reply {
+ Filter-Id += 'fail 2'
+ }
+}
+
+# Remove Tmp-IP-Address-0 with a specific value (which doesn't exist)
+update {
+ request:Tmp-IP-Address-0 -= 192.0.2.3
+}
+
+# Only the 1st, and 3rd Tmp-IP-Address attributes should still be in the list
+if (("%{Tmp-IP-Address-0[0]}" != '192.0.2.2') || \
+ ("%{Tmp-IP-Address-0[1]}" != '192.0.2.4') || \
+ ("%{Tmp-IP-Address-0[2]}" != '')) {
+ update reply {
+ Filter-Id += 'fail 3'
+ }
+}
+
+# Remove Tmp-IP-Address-4 (which doesn't exist - more to check for SEGV/assert)
+update {
+ request:Tmp-IP-Address-4 -= 192.0.2.3
+}
+
+# Remove Tmp-IP-Address-0 with a specific value
+update {
+ request:Tmp-IP-Address-0 -= 192.0.2.4
+}
+
+# Only the 1st, and 3rd Tmp-IP-Address attributes should still be in the list
+if (("%{Tmp-IP-Address-0[0]}" != '192.0.2.2') || \
+ ("%{Tmp-IP-Address-0[1]}" != '')) {
+ update reply {
+ Filter-Id += 'fail 4'
+ }
+}
+
+# Remove Tmp-IP-Address-0 with a specific value
+update {
+ request:Tmp-IP-Address-0 -= 192.0.2.2
+}
+
+# Only the 1st, and 3rd Tmp-IP-Address attributes should still be in the list
+if ("%{Tmp-IP-Address-0[0]}" != '') {
+ update reply {
+ Filter-Id += 'fail 5'
+ }
+}
+
+# Non Tmp-IP-Address-0 address attributes should still be in the request list
+if ((Tmp-String-0 != 'foobarbaz') || (Tmp-Integer-0 != 123456789)) {
+ update reply {
+ Filter-Id += 'fail 6'
+ }
+}
+
+# But there should still be some in the control list
+if (("%{control:Tmp-IP-Address-0[0]}" != 192.0.2.1) || ("%{control:Tmp-IP-Address-0[1]}" != 192.0.2.3)) {
+ update {
+ Filter-Id += 'fail 7'
+ }
+}
diff --git a/src/tests/keywords/update-tag b/src/tests/keywords/update-tag
new file mode 100644
index 0000000..15afd59
--- /dev/null
+++ b/src/tests/keywords/update-tag
@@ -0,0 +1,176 @@
+#
+# PRE: update
+#
+# Remove all attributes in a list
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+update request {
+ Tunnel-Server-Endpoint:0 := '192.0.1.1' # Should not be tagged
+ Tunnel-Server-Endpoint:0 += '192.0.1.2' # Should not be tagged
+ Tunnel-Server-Endpoint:1 := '192.0.2.1'
+ Tunnel-Server-Endpoint:1 += '192.0.2.2'
+ Tunnel-Server-Endpoint:2 := '192.0.3.1'
+ Tunnel-Server-Endpoint:2 += '192.0.3.2'
+}
+
+update request {
+ Tmp-Integer-0 := "%{debug_attr:request:}"
+}
+
+
+#
+# Selecting on attributes which have no tag (0)
+#
+if (Tunnel-Server-Endpoint:0[0] != '192.0.1.1') {
+ update {
+ reply:Filter-Id += 'fail 1'
+ }
+}
+
+if (Tunnel-Server-Endpoint:0[1] != '192.0.1.2') {
+ update {
+ reply:Filter-Id += 'fail 2'
+ }
+}
+
+#
+# Selecting on attributes with no tag specified (should match all of that type)
+#
+if (Tunnel-Server-Endpoint[0] != '192.0.1.1') {
+ update {
+ reply:Filter-Id += 'fail 3'
+ }
+}
+
+if (Tunnel-Server-Endpoint[1] != '192.0.1.2') {
+ update {
+ reply:Filter-Id += 'fail 4'
+ }
+}
+
+if (Tunnel-Server-Endpoint[2] != '192.0.2.1') {
+ update {
+ reply:Filter-Id += 'fail 5'
+ }
+}
+
+#
+# Now the none xlat version
+#
+# Check that access attributes by tag works first
+if (Tunnel-Server-Endpoint:2 != '192.0.3.1') {
+ update {
+ reply:Filter-Id += 'fail 6'
+ }
+}
+
+if (Tunnel-Server-Endpoint:2 == '192.0.3.2') {
+ update {
+ reply:Filter-Id += 'fail 7'
+ }
+}
+
+if (Tunnel-Server-Endpoint:1 != '192.0.2.1') {
+ update {
+ reply:Filter-Id += 'fail 8'
+ }
+}
+
+# Get the first instance of Tunnel-Server-Endpoint:2
+if (Tunnel-Server-Endpoint:2[0] != '192.0.3.1') {
+ update {
+ reply:Filter-Id += 'fail 9'
+ }
+}
+
+# Get the first instance of Tunnel-Server-Endpoint:2
+if (Tunnel-Server-Endpoint:2[1] != '192.0.3.2') {
+ update {
+ reply:Filter-Id += 'fail 10'
+ }
+}
+
+#
+# Assignment (bare)
+#
+update request {
+ Tmp-String-1 += &Tunnel-Server-Endpoint:2 # 0
+ Tmp-String-1 += &Tunnel-Server-Endpoint:2 # 1
+ Tmp-String-1 += &Tunnel-Server-Endpoint:1 # 2
+ Tmp-String-1 += &Tunnel-Server-Endpoint:2[0] # 3
+ Tmp-String-1 += &Tunnel-Server-Endpoint:2[1] # 4
+ Tmp-String-1 += &Tunnel-Server-Endpoint:0[0] # 5
+ Tmp-String-1 += &Tunnel-Server-Endpoint:0[1] # 6
+ Tmp-String-1 += &Tunnel-Server-Endpoint:0[2] # 7 (No attribute should be added here)
+ Tmp-String-1 += &Tunnel-Server-Endpoint[0] # 8
+ Tmp-String-1 += &Tunnel-Server-Endpoint[1] # 9
+ Tmp-String-1 += &Tunnel-Server-Endpoint[2] # 10
+}
+
+# Check that access attributes by tag works first
+if (Tmp-String-1[0] != '192.0.3.1') {
+ update {
+ reply:Filter-Id += 'fail 11'
+ }
+}
+
+if (Tmp-String-1[1] == '192.0.3.2') {
+ update {
+ reply:Filter-Id += 'fail 12'
+ }
+}
+
+if (Tmp-String-1[2] != '192.0.2.1') {
+ update {
+ reply:Filter-Id += 'fail 13'
+ }
+}
+
+# Get the first instance of Tunnel-Server-Endpoint:2
+if (Tmp-String-1[3] != '192.0.3.1') {
+ update {
+ reply:Filter-Id += 'fail 14'
+ }
+}
+
+# Get the first instance of Tunnel-Server-Endpoint:2
+if (Tmp-String-1[4] != '192.0.3.2') {
+ update {
+ reply:Filter-Id += 'fail 15'
+ }
+}
+
+# Now check the assignment
+if (Tmp-String-1[5] != '192.0.1.1') {
+ update {
+ reply:Filter-Id += 'fail 16'
+ }
+}
+
+if (Tmp-String-1[6] != '192.0.1.2') {
+ update {
+ reply:Filter-Id += 'fail 17'
+ }
+}
+
+if (Tmp-String-1[7] != '192.0.1.1') {
+ update {
+ reply:Filter-Id += 'fail 19'
+ }
+}
+
+if (Tmp-String-1[8] != '192.0.1.2') {
+ update {
+ reply:Filter-Id += 'fail 20'
+ }
+}
+
+if (Tmp-String-1[9] != '192.0.2.1') {
+ update {
+ reply:Filter-Id += 'fail 21'
+ }
+}
diff --git a/src/tests/keywords/update-xlat b/src/tests/keywords/update-xlat
new file mode 100644
index 0000000..59230dc
--- /dev/null
+++ b/src/tests/keywords/update-xlat
@@ -0,0 +1,61 @@
+#
+# PRE: update
+#
+# Form attribute references with xlats
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+
+update request {
+ Tmp-String-0 := 'Tmp-String-1'
+}
+
+#
+# Shouldn't update Tmp-String-0, should instead update Tmp-String-1
+# ... maybe this is what Alan meant when he was talking about people
+# doing stupid things with this feature.
+#
+update request {
+ "%{Tmp-String-0}" := 'hello'
+}
+
+if (&Tmp-String-1 != 'hello') {
+ update reply {
+ Filter-Id += 'Fail 0'
+ }
+}
+
+if (&Tmp-String-0 == 'hello') {
+ update reply {
+ Filter-Id += 'Fail 1'
+ }
+}
+
+#
+# Try updating an attribute specified by an OID string
+#
+update {
+ Tmp-Integer-0 := 11344
+}
+update {
+ "Vendor-%{Tmp-Integer-0}-Attr-1" := 127.0.0.1
+}
+
+if (&FreeRADIUS-Proxied-To != 127.0.0.1) {
+ update reply {
+ Filter-Id += 'Fail 2'
+ }
+}
+
+update {
+ "Attr-%{attr_num:Tmp-String-1}" := 'hello2'
+}
+
+if (&Tmp-String-1 != 'hello2') {
+ update reply {
+ Filter-Id += 'Fail 3'
+ }
+}
diff --git a/src/tests/keywords/urlquote b/src/tests/keywords/urlquote
new file mode 100644
index 0000000..35057d8
--- /dev/null
+++ b/src/tests/keywords/urlquote
@@ -0,0 +1,50 @@
+#
+# PRE: update if
+#
+update {
+ # Some encoders replace ~ with %7E RFC3986 Section 2.4 says this should not be done.
+ request:Tmp-String-0 := '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_.~'
+ request:Tmp-String-1 := '±§!@#$%^&*()+={[}]:;"\'|\<,>?/`'
+ request:Tmp-String-2 := '™œ¥¤'
+ request:Tmp-String-3 := '%C2%B1%C2%A7%21%40%23%24%25%5E%26%2A%28%29%2B%3D%7B%5B%7D%5D%3A%3B%22%27%7C%5C%3C%2C%3E%3F%2F%60'
+
+ request:Tmp-String-4 := '%E2%84%A2%C5%93%C2%A5%C2%A4'
+ reply:Filter-ID := 'filter'
+}
+
+
+if (<string>"%{urlquote:%{request:Tmp-String-0}}" != &Tmp-String-0) {
+ update reply {
+ Filter-Id += 'Fail 1'
+ }
+}
+
+if (<string>"%{urlquote:%{request:Tmp-String-1}}" != &Tmp-String-3) {
+ update reply {
+ Filter-Id += 'Fail 2'
+ }
+}
+
+if (<string>"%{urlquote:%{request:Tmp-String-2}}" != &Tmp-String-4) {
+ update reply {
+ Filter-Id += 'Fail 3'
+ }
+}
+
+if (<string>"%{urlunquote:%{request:Tmp-String-0}}" != &Tmp-String-0) {
+ update reply {
+ Filter-Id += 'Fail 4'
+ }
+}
+
+if (<string>"%{urlunquote:%{request:Tmp-String-3}}" != &Tmp-String-1) {
+ update reply {
+ Filter-Id += 'Fail 5'
+ }
+}
+
+if (<string>"%{urlunquote:%{request:Tmp-String-4}}" != &Tmp-String-2) {
+ update reply {
+ Filter-Id += 'Fail 6'
+ }
+}
diff --git a/src/tests/keywords/virtual b/src/tests/keywords/virtual
new file mode 100644
index 0000000..d6dbe32
--- /dev/null
+++ b/src/tests/keywords/virtual
@@ -0,0 +1,12 @@
+#
+# PRE: update if
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+if (request:Packet-Type == Access-Request) {
+ update reply {
+ Filter-Id := "filter"
+ }
+}
diff --git a/src/tests/keywords/virtual-exists b/src/tests/keywords/virtual-exists
new file mode 100644
index 0000000..7a8e8f3
--- /dev/null
+++ b/src/tests/keywords/virtual-exists
@@ -0,0 +1,12 @@
+#
+# PRE: update if
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+if (&Client-Shortname) {
+ update reply {
+ Filter-Id := "filter"
+ }
+}
diff --git a/src/tests/keywords/virtual-load-balance b/src/tests/keywords/virtual-load-balance
new file mode 100644
index 0000000..256c2ff
--- /dev/null
+++ b/src/tests/keywords/virtual-load-balance
@@ -0,0 +1,14 @@
+# PRE: update if foreach
+#
+# Virtual Load-Balance blocks.
+#
+
+#
+# Both of these should parse.
+#
+virtual_instantiate
+virtual_instantiate.post-auth
+
+update reply {
+ Filter-Id := 'filter'
+}
diff --git a/src/tests/keywords/virtual-rhs b/src/tests/keywords/virtual-rhs
new file mode 100644
index 0000000..0d21e7f
--- /dev/null
+++ b/src/tests/keywords/virtual-rhs
@@ -0,0 +1,16 @@
+#
+# PRE: update if
+#
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update request {
+ Tmp-String-0 := "<UNKNOWN-CLIENT>"
+}
+
+if (&Tmp-String-0 == &Client-Shortname) {
+ update reply {
+ Filter-Id := "filter"
+ }
+}
diff --git a/src/tests/keywords/virtual_policy b/src/tests/keywords/virtual_policy
new file mode 100644
index 0000000..4ab00e2
--- /dev/null
+++ b/src/tests/keywords/virtual_policy
@@ -0,0 +1,15 @@
+# PRE: update if foreach
+#
+# Virtual policies
+#
+
+
+#
+# Both of these should parse.
+#
+virtual_policy
+virtual_policy.post-auth
+
+update reply {
+ Filter-Id := 'filter'
+}
diff --git a/src/tests/keywords/wimax b/src/tests/keywords/wimax
new file mode 100644
index 0000000..f149da4
--- /dev/null
+++ b/src/tests/keywords/wimax
@@ -0,0 +1,31 @@
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update request {
+ WiMAX-PFDv2-Src-Port = 6809
+}
+
+if (WiMAX-PFDv2-Src-Port != 6809) {
+ update reply {
+ Filter-Id := "fail-1"
+ }
+}
+
+#
+# This is known, and should be renamed
+update request {
+ Attr-26.24757.84.9.5.7 = 0x01
+}
+
+if (WiMAX-PFDv2-Src-Assigned != 1) {
+ update reply {
+ Filter-Id := "fail-2"
+ }
+}
+
+if (!reply:Filter-Id) {
+ update reply {
+ Filter-Id := "filter"
+ }
+} \ No newline at end of file
diff --git a/src/tests/keywords/wimax-comboip b/src/tests/keywords/wimax-comboip
new file mode 100644
index 0000000..c4c8a3f
--- /dev/null
+++ b/src/tests/keywords/wimax-comboip
@@ -0,0 +1,19 @@
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update request {
+ WiMAX-DHCPv4-Server := 192.0.2.23
+}
+
+if (WiMAX-DHCPv4-Server != 192.0.2.23) {
+ update reply {
+ Filter-Id := "fail-1"
+ }
+}
+
+if (!reply:Filter-Id) {
+ update reply {
+ Filter-Id := "filter"
+ }
+} \ No newline at end of file
diff --git a/src/tests/keywords/with_dots b/src/tests/keywords/with_dots
new file mode 100644
index 0000000..4fc6b06
--- /dev/null
+++ b/src/tests/keywords/with_dots
@@ -0,0 +1,19 @@
+#
+# PRE: update
+#
+
+#
+# Ensure that policies can have dots.
+#
+# The main problem is that conf section references
+# also have dots in them...
+#
+with.dots
+
+update control {
+ Cleartext-Password := 'hello'
+}
+
+update reply {
+ Filter-Id := "filter"
+}
diff --git a/src/tests/keywords/xlat-attr b/src/tests/keywords/xlat-attr
new file mode 100644
index 0000000..d19495a
--- /dev/null
+++ b/src/tests/keywords/xlat-attr
@@ -0,0 +1,62 @@
+#
+# PRE: update
+#
+# Check attribute info xlats work correctly
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+update request {
+ Reply-Message := 'foo'
+ FreeRADIUS-Proxied-To := 127.0.0.1
+}
+
+if ("%{attr:&FreeRADIUS-Proxied-To}" != 'FreeRADIUS-Proxied-To') {
+ update reply {
+ Filter-Id += 'Fail 0'
+ }
+}
+
+if ("%{attr_num:&FreeRADIUS-Proxied-To}" != 1) {
+ update reply {
+ Filter-Id += 'Fail 2'
+ }
+}
+
+if ("%{vendor:&FreeRADIUS-Proxied-To}" != 'FreeRADIUS') {
+ update reply {
+ Filter-Id += 'Fail 3'
+ }
+}
+
+if ("%{vendor_num:&FreeRADIUS-Proxied-To}" != 11344) {
+ update reply {
+ Filter-Id += 'Fail 4'
+ }
+}
+
+if ("%{attr:&Reply-Message}" != 'Reply-Message') {
+ update reply {
+ Filter-Id += 'Fail 5'
+ }
+}
+
+if ("%{attr_num:&Reply-Message}" != 18) {
+ update reply {
+ Filter-Id += 'Fail 6'
+ }
+}
+
+if ("%{vendor:&Reply-Message}" != '') {
+ update reply {
+ Filter-Id += 'Fail 7'
+ }
+}
+
+if ("%{vendor_num:&Reply-Message}" != 0) {
+ update reply {
+ Filter-Id += 'Fail 8'
+ }
+}
diff --git a/src/tests/keywords/xlat-attr-index b/src/tests/keywords/xlat-attr-index
new file mode 100644
index 0000000..c967dd6
--- /dev/null
+++ b/src/tests/keywords/xlat-attr-index
@@ -0,0 +1,53 @@
+#
+# PRE: update
+#
+# Remove all attributes in a list
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+update request {
+ Tmp-IP-Address-0 := 192.0.2.1
+ Tmp-IP-Address-0 += 192.0.2.2
+}
+
+if ("%{Tmp-IP-Address-0[#]}" != 2) {
+ update {
+ reply:Filter-Id += 'fail 0'
+ }
+}
+
+if (("%{Tmp-IP-Address-0[0]}" != 192.0.2.1) || ("%{Tmp-IP-Address-0[1]}" != 192.0.2.2)) {
+ update {
+ reply:Filter-Id += 'fail 1'
+ }
+}
+
+if ("%{Tmp-IP-Address-0[*]}" != '192.0.2.1,192.0.2.2') {
+ update {
+ reply:Filter-Id += 'fail 2'
+ }
+}
+
+# Try calling these xlats in mapping too, they may get optimised to VPTs which is a
+# different code path.
+update request {
+ Tmp-IP-Address-1 += "%{Tmp-IP-Address-0[1]}"
+ Tmp-IP-Address-1 += "%{Tmp-IP-Address-0[0]}"
+ Tmp-String-0 = "%{Tmp-IP-Address-0[*]}"
+ Tmp-Integer-0 = "%{Tmp-IP-Address-0[#]}"
+}
+
+if (Tmp-String-0 != '192.0.2.1,192.0.2.2') {
+ update {
+ reply:Filter-Id += 'fail 3'
+ }
+}
+
+if (Tmp-Integer-0 != 2) {
+ update {
+ reply:Filter-Id += 'fail 4'
+ }
+}
diff --git a/src/tests/keywords/xlat-attr-tag b/src/tests/keywords/xlat-attr-tag
new file mode 100644
index 0000000..c0bd8b6
--- /dev/null
+++ b/src/tests/keywords/xlat-attr-tag
@@ -0,0 +1,225 @@
+#
+# PRE: update
+#
+# Remove all attributes in a list
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+update request {
+ Tunnel-Server-Endpoint := '192.0.1.1' # Should not be tagged
+ Tunnel-Server-Endpoint:0 += '192.0.1.2' # Should not be tagged
+ Tunnel-Server-Endpoint:1 := '192.0.2.1'
+ Tunnel-Server-Endpoint:1 += '192.0.2.2'
+ Tunnel-Server-Endpoint:2 := '192.0.3.1'
+ Tunnel-Server-Endpoint:2 += '192.0.3.2'
+}
+
+update request {
+ Tmp-Integer-0 := "%{debug_attr:request:}"
+}
+
+# Check the tag printing xlat works correctly
+if ("%{tag:Tunnel-Server-Endpoint[0]}" != '') {
+ update {
+ reply:Filter-Id += 'fail 0a'
+ }
+}
+
+if ("%{tag:Tunnel-Server-Endpoint[1]}" != '') {
+ update {
+ reply:Filter-Id += 'fail 0b'
+ }
+}
+
+
+if ("%{tag:Tunnel-Server-Endpoint[2]}" != '1') {
+ update {
+ reply:Filter-Id += 'fail 0c'
+ }
+}
+
+if ("%{tag:Tunnel-Server-Endpoint[5]}" != '2') {
+ update {
+ reply:Filter-Id += 'fail 0d'
+ }
+}
+
+if ("%{tag:Tunnel-Server-Endpoint[6]}" != '') {
+ update {
+ reply:Filter-Id += 'fail 0e'
+ }
+}
+
+if ("%{tag:control:Cleartext-Password}" != '') {
+ update {
+ reply:Filter-Id += 'fail 0f'
+ }
+}
+
+# Check that access attributes by tag works first
+if ("%{Tunnel-Server-Endpoint:2}" != '192.0.3.1') {
+ update {
+ reply:Filter-Id += 'fail 1'
+ }
+}
+
+if ("%{Tunnel-Server-Endpoint:2}" == '192.0.3.2') {
+ update {
+ reply:Filter-Id += 'fail 2'
+ }
+}
+
+if ("%{Tunnel-Server-Endpoint:1}" != '192.0.2.1') {
+ update {
+ reply:Filter-Id += 'fail 3'
+ }
+}
+
+# Get the first instance of Tunnel-Server-Endpoint:2
+if ("%{Tunnel-Server-Endpoint:2[0]}" != '192.0.3.1') {
+ update {
+ reply:Filter-Id += 'fail 4'
+ }
+}
+
+# Get the first instance of Tunnel-Server-Endpoint:2
+if ("%{Tunnel-Server-Endpoint:2[1]}" != '192.0.3.2') {
+ update {
+ reply:Filter-Id += 'fail 5'
+ }
+}
+
+if ("%{Tunnel-Server-Endpoint:0[2]}" != '') {
+ update {
+ reply:Filter-Id += 'fail 6'
+ }
+}
+
+if ("%{Tunnel-Server-Endpoint:0[0]}" != '192.0.1.1') {
+ update {
+ reply:Filter-Id += 'fail 7'
+ }
+}
+
+if ("%{Tunnel-Server-Endpoint:0[1]}" != '192.0.1.2') {
+ update {
+ reply:Filter-Id += 'fail 8'
+ }
+}
+
+if ("%{Tunnel-Server-Endpoint:0[2]}" != '') {
+ update {
+ reply:Filter-Id += 'fail 9'
+ }
+}
+
+#
+# Selecting on attributes with no tag specified (should match all of that type)
+#
+if ("%{Tunnel-Server-Endpoint[0]}" != '192.0.1.1') {
+ update {
+ reply:Filter-Id += 'fail 10'
+ }
+}
+
+if ("%{Tunnel-Server-Endpoint[1]}" != '192.0.1.2') {
+ update {
+ reply:Filter-Id += 'fail 11'
+ }
+}
+
+if ("%{Tunnel-Server-Endpoint[2]}" != '192.0.2.1') {
+ update {
+ reply:Filter-Id += 'fail 12'
+ }
+}
+
+#
+# Assignment (xlat)
+#
+update request {
+ Tmp-String-0 += "%{Tunnel-Server-Endpoint:2}" #0
+ Tmp-String-0 += "%{Tunnel-Server-Endpoint:2}" #1
+ Tmp-String-0 += "%{Tunnel-Server-Endpoint:1}" #2
+ Tmp-String-0 += "%{Tunnel-Server-Endpoint:2[0]}" #3
+ Tmp-String-0 += "%{Tunnel-Server-Endpoint:2[1]}" #4
+ Tmp-String-0 += "%{Tunnel-Server-Endpoint:0[0]}" #5
+ Tmp-String-0 += "%{Tunnel-Server-Endpoint:0[1]}" #6
+ Tmp-String-0 += "%{Tunnel-Server-Endpoint:0[2]}" #7
+ Tmp-String-0 += "%{Tunnel-Server-Endpoint[0]}" #8
+ Tmp-String-0 += "%{Tunnel-Server-Endpoint[1]}" #9
+ Tmp-String-0 += "%{Tunnel-Server-Endpoint[2]}" #10
+}
+
+# Check that access attributes by tag works first
+if (Tmp-String-0[0] != '192.0.3.1') {
+ update {
+ reply:Filter-Id += 'fail 13'
+ }
+}
+
+if (Tmp-String-0[1] == '192.0.3.2') {
+ update {
+ reply:Filter-Id += 'fail 14'
+ }
+}
+
+if (Tmp-String-0[2] != '192.0.2.1') {
+ update {
+ reply:Filter-Id += 'fail 15'
+ }
+}
+
+# Get the first instance of Tunnel-Server-Endpoint:2
+if (Tmp-String-0[3] != '192.0.3.1') {
+ update {
+ reply:Filter-Id += 'fail 16'
+ }
+}
+
+# Get the first instance of Tunnel-Server-Endpoint:2
+if (Tmp-String-0[4] != '192.0.3.2') {
+ update {
+ reply:Filter-Id += 'fail 17'
+ }
+}
+
+# Now check the assignment
+if (Tmp-String-0[5] != '192.0.1.1') {
+ update {
+ reply:Filter-Id += 'fail 18'
+ }
+}
+
+if (Tmp-String-0[6] != '192.0.1.2') {
+ update {
+ reply:Filter-Id += 'fail 19'
+ }
+}
+
+if (Tmp-String-0[7] != '') {
+ update {
+ reply:Filter-Id += 'fail 20'
+ }
+}
+
+if (Tmp-String-0[8] != '192.0.1.1') {
+ update {
+ reply:Filter-Id += 'fail 21'
+ }
+}
+
+if (Tmp-String-0[9] != '192.0.1.2') {
+ update {
+ reply:Filter-Id += 'fail 22'
+ }
+}
+
+if (Tmp-String-0[10] != '192.0.2.1') {
+ update {
+ reply:Filter-Id += 'fail 23'
+ }
+}
diff --git a/src/tests/keywords/xlat-concat b/src/tests/keywords/xlat-concat
new file mode 100644
index 0000000..e0c55a9
--- /dev/null
+++ b/src/tests/keywords/xlat-concat
@@ -0,0 +1,40 @@
+#
+# PRE: xlat-list
+#
+# concat xlat
+#
+
+update control {
+ control !* ANY
+}
+
+update control {
+ Tmp-IP-Address-0 := 192.0.2.1
+ Tmp-IP-Address-0 += 192.0.2.2
+}
+
+update request {
+ Tmp-String-0 := "%{concat:control:[*] ;}"
+}
+
+if (Tmp-String-0 != '192.0.2.1;192.0.2.2') {
+ update {
+ reply:Filter-Id += 'fail 1'
+ }
+}
+
+update request {
+ Tmp-String-0 := "%{concat:control:[*] X}"
+}
+
+if (Tmp-String-0 != '192.0.2.1X192.0.2.2') {
+ update {
+ reply:Filter-Id += 'fail 2'
+ }
+}
+
+# Boilerplate junk
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
diff --git a/src/tests/keywords/xlat-error b/src/tests/keywords/xlat-error
new file mode 100644
index 0000000..b6a2587
--- /dev/null
+++ b/src/tests/keywords/xlat-error
@@ -0,0 +1,12 @@
+#
+# PRE: if xlat-attr-tag
+#
+
+#
+# missing a closing } for the expansion in the string.
+#
+if ("%{expr:1 + 2" == 3) { # ERROR
+ update reply {
+ Filter-Id := "fail"
+ }
+}
diff --git a/src/tests/keywords/xlat-explode b/src/tests/keywords/xlat-explode
new file mode 100644
index 0000000..ea727d9
--- /dev/null
+++ b/src/tests/keywords/xlat-explode
@@ -0,0 +1,91 @@
+#
+# PRE: update
+#
+# Check explode works correctly
+#
+update {
+ request:Class := '1=1|my_attr=2|my_attr=hello|'
+ request:Calling-Station-ID += '|'
+ control:User-Name += '|hello|goodbye'
+ control:User-Name += '|morning|night|1|'
+ control:Reply-Message := 'Can\'t touch this'
+ reply:Filter-Id = 'filter'
+}
+
+if ("%{explode:&Class |}" != 3) {
+ update reply {
+ Filter-Id += 'Fail 0'
+ }
+}
+
+if ("%{Class[#]}" != 3) {
+ update reply {
+ Filter-Id += 'Fail 1'
+ }
+}
+
+if ((&Class[0] != '1=1') || (&Class[1] != 'my_attr=2') || (&Class[2] != 'my_attr=hello')) {
+ update reply {
+ Filter-Id += 'Fail 2'
+ }
+}
+
+if (&Class[3]) {
+ update reply {
+ Filter-Id += 'Fail 3'
+ }
+}
+
+if ("%{explode:&control:Calling-Station-Id |}" != 0) {
+ update reply {
+ filter-Id += 'Fail 4'
+ }
+}
+
+if ("%{explode:&control:User-Name[*] |}" != 5) {
+ update reply {
+ Filter-Id += 'Fail 5'
+ }
+}
+
+if ("%{control:User-Name[#]}" != 5) {
+ update reply {
+ Filter-Id += 'Fail 6'
+ }
+}
+
+if ((&control:User-Name[0] != 'hello') || \
+ (&control:User-Name[1] != 'goodbye') || \
+ (&control:User-Name[2] != 'morning') || \
+ (&control:User-Name[3] != 'night') || \
+ (&control:User-Name[4] != '1')) {
+ update reply {
+ Filter-Id += 'Fail 7'
+ }
+}
+
+if (&control:User-Name[5]) {
+ update reply {
+ Filter-Id += 'Fail 8'
+ }
+}
+
+if ("%{explode:&control:Reply-Message |}" != 0) {
+ update reply {
+ Filter-Id += 'Fail 9'
+ }
+}
+
+if ("%{control:Reply-Message[#]}" != 1) {
+ update reply {
+ Filter-Id += 'Fail 10'
+ }
+}
+
+if (&control:Reply-Message != 'Can\'t touch this') {
+ update reply {
+ Filter-Id += 'Fail 11'
+ }
+}
+
+debug_all
diff --git a/src/tests/keywords/xlat-list b/src/tests/keywords/xlat-list
new file mode 100644
index 0000000..fcd9e84
--- /dev/null
+++ b/src/tests/keywords/xlat-list
@@ -0,0 +1,64 @@
+#
+# PRE: update
+#
+update control {
+ control !* ANY
+}
+
+update control {
+ Tmp-IP-Address-0 := 192.0.2.1
+ Tmp-IP-Address-0 += 192.0.2.2
+}
+
+if ("%{control:[#]}" != 2) {
+ update {
+ reply:Filter-Id += 'fail 0'
+ }
+}
+
+debug_control
+
+if (("%{control:[0]}" != 192.0.2.1) || ("%{control:[1]}" != 192.0.2.2)) {
+ update {
+ reply:Filter-Id += 'fail 1'
+ }
+}
+
+if (("%{control:[n]}" != 192.0.2.2)) {
+ update {
+ reply:Filter-Id += 'fail 1a'
+ }
+}
+
+if ("%{control:[*]}" != '192.0.2.1,192.0.2.2') {
+ update {
+ reply:Filter-Id += 'fail 2'
+ }
+}
+
+# Try calling these xlats in mapping too, they may get optimised to VPTs which is a
+# different code path.
+update request {
+ Tmp-IP-Address-1 += "%{control:[1]}"
+ Tmp-IP-Address-1 += "%{control:[0]}"
+ Tmp-String-0 = "%{control:[*]}"
+ Tmp-Integer-0 = "%{control:[#]}"
+}
+
+if (Tmp-String-0 != '192.0.2.1,192.0.2.2') {
+ update {
+ reply:Filter-Id += 'fail 3'
+ }
+}
+
+if (Tmp-Integer-0 != 2) {
+ update {
+ reply:Filter-Id += 'fail 4'
+ }
+}
+
+# Boilerplate junk
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
diff --git a/src/tests/keywords/xlat-octets b/src/tests/keywords/xlat-octets
new file mode 100644
index 0000000..ea9543c
--- /dev/null
+++ b/src/tests/keywords/xlat-octets
@@ -0,0 +1,36 @@
+#
+# PRE: update if
+#
+# Remove all attributes in a list
+#
+update {
+ control:Cleartext-Password := 'hello'
+ reply:Filter-Id := 'filter'
+}
+
+#
+# Regression test for 0x prefix. xlat expanded
+# octet strings must NOT have a 0x prefix added
+#
+update request {
+ Tmp-Octets-0 := 0x0001020304050607
+ Tmp-Octets-0 += 0x0706050403020100
+}
+
+if ("%{Tmp-Octets-0}" != '0x0001020304050607') {
+ update {
+ reply:Filter-Id := 'fail 1'
+ }
+}
+
+if ("%{Tmp-Octets-0[0]}" != '0x0001020304050607') {
+ update {
+ reply:Filter-Id += 'fail 2'
+ }
+}
+
+if ("%{Tmp-Octets-0[*]}" != '0x0001020304050607,0x0706050403020100') {
+ update {
+ reply:Filter-Id += 'fail 3'
+ }
+}
diff --git a/src/tests/keywords/xlat-virtual-attr b/src/tests/keywords/xlat-virtual-attr
new file mode 100644
index 0000000..e476993
--- /dev/null
+++ b/src/tests/keywords/xlat-virtual-attr
@@ -0,0 +1,131 @@
+#
+# PRE: if
+#
+
+update reply {
+ Filter-Id := "filter"
+}
+
+if ("%{Client-Shortname}" != '<UNKNOWN-CLIENT>') {
+ update reply {
+ Filter-Id += "fail 0"
+ }
+}
+
+if ("%{Request-Processing-Stage}" != 'authorize') {
+ update reply {
+ Filter-Id += "fail 1"
+ }
+}
+
+if ("%{Virtual-Server}" != 'default') {
+ update reply {
+ Filter-Id += "fail 2"
+ }
+}
+
+if ("%{Module-Return-Code}" != '') {
+ update reply {
+ Filter-Id += "fail 3a"
+ }
+}
+
+ok
+if ("%{Module-Return-Code}" != 'ok') {
+ update reply {
+ Filter-Id += "fail 3b"
+ }
+}
+
+if ("%{Packet-Type}" != 'Access-Request') {
+ update reply {
+ Filter-Id += "fail 4"
+ }
+}
+
+# Response hasn't been set yet
+if ("%{Response-Packet-Type}" != '') {
+ update reply {
+ Filter-Id += "fail 5"
+ }
+}
+
+if ("%{Packet-Authentication-Vector}" != '0x00000000000000000000000000000000') {
+ update reply {
+ Filter-Id += "fail 6"
+ }
+}
+
+if ("%{Client-IP-Address}" != 127.0.0.1) {
+ update reply {
+ Filter-Id += "fail 7a"
+ }
+}
+
+if ("%{Packet-Src-IP-Address}" != 127.0.0.1) {
+ update reply {
+ Filter-Id += "fail 7b"
+ }
+}
+
+if ("%{Packet-Dst-IP-Address}" != 127.0.0.1) {
+ update reply {
+ Filter-Id += "fail 8"
+ }
+}
+
+# Can't have both...
+if ("%{Packet-Src-IPv6-Address}" != '') {
+ update reply {
+ Filter-Id += "fail 9"
+ }
+}
+
+if ("%{Packet-Dst-IPv6-Address}" != '') {
+ update reply {
+ Filter-Id += "fail 10"
+ }
+}
+
+if ("%{Packet-Src-Port}" != '18120') {
+ update reply {
+ Filter-Id += "fail 11"
+ }
+}
+
+if ("%{Packet-Dst-Port}" != '1812') {
+ update reply {
+ Filter-Id += "fail 12"
+ }
+}
+
+
+# We should allow the user to overload virtual attributes
+update request {
+ Client-Shortname := 'my_test_client'
+}
+
+if ("%{Client-Shortname}" != 'my_test_client') {
+ update reply {
+ Filter-Id += "fail 13"
+ }
+}
+
+# Operations on virtual attributes should be the same as on real ones
+if ("%{Virtual-Server[0]}" != 'default') {
+ update reply {
+ Filter-Id += "fail 14"
+ }
+}
+
+if ("%{Virtual-Server[*]}" != 'default') {
+ update reply {
+ Filter-Id += "fail 15"
+ }
+}
+
+if ("%{Virtual-Server[#]}" != 1) {
+ update reply {
+ Filter-Id += "fail 16"
+ }
+}