summaryrefslogtreecommitdiffstats
path: root/src/tests/modules
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/modules
parentInitial commit. (diff)
downloadfreeradius-50b37d4a27d3295a29afca2286f1a5a086142cec.tar.xz
freeradius-50b37d4a27d3295a29afca2286f1a5a086142cec.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/modules/README.rst18
-rw-r--r--src/tests/modules/all.mk40
-rw-r--r--src/tests/modules/always/all.mk3
-rw-r--r--src/tests/modules/always/module.conf7
-rw-r--r--src/tests/modules/always/replace.unlang11
-rw-r--r--src/tests/modules/always/set_rcode.unlang44
-rw-r--r--src/tests/modules/always/set_status_dead.unlang18
-rw-r--r--src/tests/modules/always/set_status_revive.unlang28
-rw-r--r--src/tests/modules/cache/rbtree/all.mk2
-rw-r--r--src/tests/modules/default-input.attrs11
-rw-r--r--src/tests/modules/files/addcontrol.attrs13
-rw-r--r--src/tests/modules/files/addcontrol.unlang8
-rw-r--r--src/tests/modules/files/addreply.attrs12
-rw-r--r--src/tests/modules/files/addreply.unlang4
-rw-r--r--src/tests/modules/files/all.mk3
-rw-r--r--src/tests/modules/files/authorize92
-rw-r--r--src/tests/modules/files/bob.attrs11
-rw-r--r--src/tests/modules/files/bob.unlang4
-rw-r--r--src/tests/modules/files/doug.attrs11
-rw-r--r--src/tests/modules/files/doug.unlang4
-rw-r--r--src/tests/modules/files/fall-through.attrs11
-rw-r--r--src/tests/modules/files/fall-through.unlang4
-rw-r--r--src/tests/modules/files/filterreply.attrs10
-rw-r--r--src/tests/modules/files/filterreply.unlang4
-rw-r--r--src/tests/modules/files/module.conf9
-rw-r--r--src/tests/modules/files/subreply.attrs12
-rw-r--r--src/tests/modules/files/subreply.unlang4
-rw-r--r--src/tests/modules/json/all.mk3
-rw-r--r--src/tests/modules/json/encode.attrs13
-rw-r--r--src/tests/modules/json/encode.unlang233
-rw-r--r--src/tests/modules/json/module.conf150
-rw-r--r--src/tests/modules/ldap/acct.attrs35
-rw-r--r--src/tests/modules/ldap/acct.unlang23
-rw-r--r--src/tests/modules/ldap/all.mk8
-rw-r--r--src/tests/modules/ldap/auth.attrs15
-rw-r--r--src/tests/modules/ldap/auth.unlang72
l---------src/tests/modules/ldap/example.com.ldif1
-rw-r--r--src/tests/modules/ldap/groups_rfc2307bis.attrs15
-rw-r--r--src/tests/modules/ldap/groups_rfc2307bis.unlang41
-rw-r--r--src/tests/modules/ldap/module.conf537
-rw-r--r--src/tests/modules/pap/all.mk3
-rw-r--r--src/tests/modules/pap/module.conf1
-rw-r--r--src/tests/modules/pap/pbkfd2_dig_big.attrs11
-rw-r--r--src/tests/modules/pap/pbkfd2_dig_big.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_dig_small.attrs11
-rw-r--r--src/tests/modules/pap/pbkfd2_dig_small.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_iter0.attrs11
-rw-r--r--src/tests/modules/pap/pbkfd2_iter0.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_iter1.attrs11
-rw-r--r--src/tests/modules/pap/pbkfd2_iter1.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_iter1000.attrs11
-rw-r--r--src/tests/modules/pap/pbkfd2_iter1000.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_iter100000.attrs10
-rw-r--r--src/tests/modules/pap/pbkfd2_iter100000.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_iter_big.attrs11
-rw-r--r--src/tests/modules/pap/pbkfd2_iter_big.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_iter_miss.attrs11
-rw-r--r--src/tests/modules/pap/pbkfd2_iter_miss.unlang19
-rw-r--r--src/tests/modules/pap/pbkfd2_iter_small.attrs11
-rw-r--r--src/tests/modules/pap/pbkfd2_iter_small.unlang19
-rw-r--r--src/tests/modules/pap/pbkfd2_passlib.attrs10
-rw-r--r--src/tests/modules/pap/pbkfd2_passlib.unlang20
-rw-r--r--src/tests/modules/pap/pbkfd2_salt0.attrs10
-rw-r--r--src/tests/modules/pap/pbkfd2_salt0.unlang19
-rw-r--r--src/tests/modules/pap/pbkfd2_salt1.attrs10
-rw-r--r--src/tests/modules/pap/pbkfd2_salt1.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_salt1024.attrs10
-rw-r--r--src/tests/modules/pap/pbkfd2_salt1024.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_salt64.attrs10
-rw-r--r--src/tests/modules/pap/pbkfd2_salt64.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_salt_big.attrs10
-rw-r--r--src/tests/modules/pap/pbkfd2_salt_big.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_salt_small.attrs10
-rw-r--r--src/tests/modules/pap/pbkfd2_salt_small.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_sha1.attrs10
-rw-r--r--src/tests/modules/pap/pbkfd2_sha1.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_sha2_224.attrs10
-rw-r--r--src/tests/modules/pap/pbkfd2_sha2_224.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_sha2_256.attrs10
-rw-r--r--src/tests/modules/pap/pbkfd2_sha2_256.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_sha2_384.attrs10
-rw-r--r--src/tests/modules/pap/pbkfd2_sha2_384.unlang17
-rw-r--r--src/tests/modules/pap/pbkfd2_sha2_512.attrs10
-rw-r--r--src/tests/modules/pap/pbkfd2_sha2_512.unlang17
-rw-r--r--src/tests/modules/preprocess/all.mk3
-rw-r--r--src/tests/modules/preprocess/hints2
-rw-r--r--src/tests/modules/preprocess/huntgroups0
-rw-r--r--src/tests/modules/preprocess/module.conf4
-rw-r--r--src/tests/modules/preprocess/xlat.attrs12
-rw-r--r--src/tests/modules/preprocess/xlat.unlang14
-rw-r--r--src/tests/modules/radiusd.conf103
-rw-r--r--src/tests/modules/sql/.gitignore1
-rw-r--r--src/tests/modules/sql/acct_0_start.attrs37
-rw-r--r--src/tests/modules/sql/acct_0_start.unlang40
-rw-r--r--src/tests/modules/sql/acct_1_update.attrs37
-rw-r--r--src/tests/modules/sql/acct_1_update.unlang30
-rw-r--r--src/tests/modules/sql/acct_2_stop.attrs38
-rw-r--r--src/tests/modules/sql/acct_2_stop.unlang40
-rw-r--r--src/tests/modules/sql/acct_start_conflict.attrs37
-rw-r--r--src/tests/modules/sql/acct_start_conflict.unlang76
-rw-r--r--src/tests/modules/sql/acct_update_no_start.attrs37
-rw-r--r--src/tests/modules/sql/acct_update_no_start.unlang40
-rw-r--r--src/tests/modules/sql/auth.attrs12
-rw-r--r--src/tests/modules/sql/auth.unlang39
-rw-r--r--src/tests/modules/sql/reject.attrs12
-rw-r--r--src/tests/modules/sql/reject.unlang39
-rw-r--r--src/tests/modules/sql_mysql/.gitignore1
l---------src/tests/modules/sql_mysql/acct_0_start.attrs1
l---------src/tests/modules/sql_mysql/acct_0_start.unlang1
l---------src/tests/modules/sql_mysql/acct_1_update.attrs1
l---------src/tests/modules/sql_mysql/acct_1_update.unlang1
l---------src/tests/modules/sql_mysql/acct_2_stop.attrs1
l---------src/tests/modules/sql_mysql/acct_2_stop.unlang1
l---------src/tests/modules/sql_mysql/acct_start_conflict.attrs1
l---------src/tests/modules/sql_mysql/acct_start_conflict.unlang1
l---------src/tests/modules/sql_mysql/acct_update_no_start.attrs1
l---------src/tests/modules/sql_mysql/acct_update_no_start.unlang1
-rw-r--r--src/tests/modules/sql_mysql/all.mk6
l---------src/tests/modules/sql_mysql/auth.attrs1
l---------src/tests/modules/sql_mysql/auth.unlang1
-rw-r--r--src/tests/modules/sql_mysql/module.conf53
l---------src/tests/modules/sql_mysql/reject.attrs1
l---------src/tests/modules/sql_mysql/reject.unlang1
-rw-r--r--src/tests/modules/sql_postgresql/.gitignore1
l---------src/tests/modules/sql_postgresql/acct_0_start.attrs1
l---------src/tests/modules/sql_postgresql/acct_0_start.unlang1
l---------src/tests/modules/sql_postgresql/acct_1_update.attrs1
l---------src/tests/modules/sql_postgresql/acct_1_update.unlang1
l---------src/tests/modules/sql_postgresql/acct_2_stop.attrs1
l---------src/tests/modules/sql_postgresql/acct_2_stop.unlang1
l---------src/tests/modules/sql_postgresql/acct_start_conflict.attrs1
l---------src/tests/modules/sql_postgresql/acct_start_conflict.unlang1
l---------src/tests/modules/sql_postgresql/acct_update_no_start.attrs1
l---------src/tests/modules/sql_postgresql/acct_update_no_start.unlang1
-rw-r--r--src/tests/modules/sql_postgresql/all.mk6
l---------src/tests/modules/sql_postgresql/auth.attrs1
l---------src/tests/modules/sql_postgresql/auth.unlang1
-rw-r--r--src/tests/modules/sql_postgresql/module.conf52
l---------src/tests/modules/sql_postgresql/reject.attrs1
l---------src/tests/modules/sql_postgresql/reject.unlang1
-rw-r--r--src/tests/modules/sql_sqlite/.gitignore1
l---------src/tests/modules/sql_sqlite/acct_0_start.attrs1
l---------src/tests/modules/sql_sqlite/acct_0_start.unlang1
l---------src/tests/modules/sql_sqlite/acct_1_update.attrs1
l---------src/tests/modules/sql_sqlite/acct_1_update.unlang1
l---------src/tests/modules/sql_sqlite/acct_2_stop.attrs1
l---------src/tests/modules/sql_sqlite/acct_2_stop.unlang1
l---------src/tests/modules/sql_sqlite/acct_start_conflict.attrs1
l---------src/tests/modules/sql_sqlite/acct_start_conflict.unlang1
l---------src/tests/modules/sql_sqlite/acct_update_no_start.attrs1
l---------src/tests/modules/sql_sqlite/acct_update_no_start.unlang1
-rw-r--r--src/tests/modules/sql_sqlite/all.mk3
l---------src/tests/modules/sql_sqlite/auth.attrs1
l---------src/tests/modules/sql_sqlite/auth.unlang1
-rw-r--r--src/tests/modules/sql_sqlite/module.conf52
l---------src/tests/modules/sql_sqlite/reject.attrs1
l---------src/tests/modules/sql_sqlite/reject.unlang1
-rw-r--r--src/tests/modules/test.mk165
-rw-r--r--src/tests/modules/unbound/all.mk3
-rw-r--r--src/tests/modules/unbound/dns.attrs11
-rw-r--r--src/tests/modules/unbound/dns.unlang53
-rw-r--r--src/tests/modules/unbound/module.conf4
-rw-r--r--src/tests/modules/unbound/unbound.conf6
163 files changed, 3244 insertions, 0 deletions
diff --git a/src/tests/modules/README.rst b/src/tests/modules/README.rst
new file mode 100644
index 0000000..164509d
--- /dev/null
+++ b/src/tests/modules/README.rst
@@ -0,0 +1,18 @@
+Module Tests
+------------
+
+To test module `foo`, create a directory `foo`, and put a file `all.mk` into it, e.g.
+
+ foo/all.mk
+
+All of the tests for the module should go here. The tests will be run
+*only* if the module is available, and has been built correctly on the system.
+
+The file should contain a target "MODULE.test". This is the main
+target used to test the module. The framework automatically makes the
+tests depend on the module (i.e. library). So if the module source
+changes, you can just do `make MODULE.test`. The module will be
+re-built, and the tests will be run.
+
+Note: all SQL tests share the same tests definitions (see sql directory).
+The modules themselves simply link to the actual tests files.
diff --git a/src/tests/modules/all.mk b/src/tests/modules/all.mk
new file mode 100644
index 0000000..9960df7
--- /dev/null
+++ b/src/tests/modules/all.mk
@@ -0,0 +1,40 @@
+#
+# Find the subdirs which have "all.mk"
+#
+TEST_SUBDIRS := $(patsubst src/tests/modules/%/all.mk,%,$(wildcard src/tests/modules/*/all.mk))
+
+#
+# Find out which of those have a similar target. i.e. modules/foo -> rlm_foo.la
+#
+TEST_TARGETS := $(foreach x,$(TEST_SUBDIRS),$(findstring rlm_$x.la,$(ALL_TGTS)))
+
+TEST_BUILT := $(patsubst rlm_%.la,%,$(TEST_TARGETS))
+
+#
+# Ensure that the tests depend on the module, so that changes to the
+# module will re-run the test
+#
+$(foreach x,$(TEST_BUILT),$(eval $x.test: rlm_$x.la))
+
+######################################################################
+
+#
+# And do the same thing for sub-directories
+#
+TEST_SUBSUBDIRS := $(patsubst src/tests/modules/%/all.mk,%,$(wildcard src/tests/modules/*/*/all.mk))
+
+TEST_SUBTARGETS := $(foreach x,$(TEST_SUBSUBDIRS),$(findstring rlm_$(subst /,_,$x).la,$(ALL_TGTS)))
+
+TEST_SUBBUILT := $(patsubst rlm_%.la,%,$(TEST_SUBTARGETS))
+
+$(foreach x,$(TEST_SUBBUILT),$(eval $x.test: rlm_$(subst /,_,$x).la))
+
+######################################################################
+#
+# For the remaining subdirs, add on the directory to include.
+# test.mk will run the tests for all modules
+# It is included last so that the module specific makefiles can be processed first
+# (modules that require a test server can set the corresponding require_test_server variable)
+#
+SUBMAKEFILES := $(addsuffix /all.mk,$(TEST_BUILT) $(subst _,/,$(TEST_SUBBUILT))) test.mk
+
diff --git a/src/tests/modules/always/all.mk b/src/tests/modules/always/all.mk
new file mode 100644
index 0000000..8f1127f
--- /dev/null
+++ b/src/tests/modules/always/all.mk
@@ -0,0 +1,3 @@
+#
+# Test the "always" module
+#
diff --git a/src/tests/modules/always/module.conf b/src/tests/modules/always/module.conf
new file mode 100644
index 0000000..39995e5
--- /dev/null
+++ b/src/tests/modules/always/module.conf
@@ -0,0 +1,7 @@
+always my_reject {
+ rcode = reject
+}
+
+always db_status {
+ rcode = ok
+}
diff --git a/src/tests/modules/always/replace.unlang b/src/tests/modules/always/replace.unlang
new file mode 100644
index 0000000..1d502f7
--- /dev/null
+++ b/src/tests/modules/always/replace.unlang
@@ -0,0 +1,11 @@
+%{poke:my_reject.rcode=ok}
+
+my_reject # should be "ok"
+
+update control {
+ Cleartext-Password := "hello"
+}
+
+update reply {
+ Filter-Id := "success"
+}
diff --git a/src/tests/modules/always/set_rcode.unlang b/src/tests/modules/always/set_rcode.unlang
new file mode 100644
index 0000000..faaed28
--- /dev/null
+++ b/src/tests/modules/always/set_rcode.unlang
@@ -0,0 +1,44 @@
+#
+# Set status to "notfound". xlat should expand to previous status, "alive"
+#
+if ("%{db_status:notfound}" != "alive") {
+ update reply {
+ Filter-Id += "failed"
+ }
+}
+
+
+#
+# Verify that the status was changed
+#
+db_status
+if (!notfound) {
+ update reply {
+ Filter-Id += "failed"
+ }
+}
+
+
+#
+# Fetch status using xlat without setting the status
+#
+if ("%{db_status:}" != "notfound") {
+ update reply {
+ Filter-Id += "failed"
+ }
+}
+
+
+#
+# Verify that the status did not change
+#
+db_status
+if (notfound) {
+ update reply {
+ Filter-Id += "success"
+ }
+}
+
+update control {
+ Cleartext-Password := "hello"
+}
diff --git a/src/tests/modules/always/set_status_dead.unlang b/src/tests/modules/always/set_status_dead.unlang
new file mode 100644
index 0000000..6b29ede
--- /dev/null
+++ b/src/tests/modules/always/set_status_dead.unlang
@@ -0,0 +1,18 @@
+#
+# Set the module status to dead, call it and check that it fails
+#
+%{db_status:dead}
+
+db_status {
+ fail = 1
+}
+
+if (fail) {
+ update reply {
+ Filter-Id := "success"
+ }
+}
+
+update control {
+ Cleartext-Password := "hello"
+}
diff --git a/src/tests/modules/always/set_status_revive.unlang b/src/tests/modules/always/set_status_revive.unlang
new file mode 100644
index 0000000..3e71d39
--- /dev/null
+++ b/src/tests/modules/always/set_status_revive.unlang
@@ -0,0 +1,28 @@
+#
+# Fail a module...
+#
+%{db_status:dead}
+db_status {
+ fail = 1
+}
+if (!fail) {
+ update reply {
+ Filter-Id += "failed"
+ }
+}
+
+
+#
+# ... Now revive it
+#
+%{db_status:alive}
+db_status
+if (ok) {
+ update reply {
+ Filter-Id += "success"
+ }
+}
+
+update control {
+ Cleartext-Password := "hello"
+}
diff --git a/src/tests/modules/cache/rbtree/all.mk b/src/tests/modules/cache/rbtree/all.mk
new file mode 100644
index 0000000..8f89aa6
--- /dev/null
+++ b/src/tests/modules/cache/rbtree/all.mk
@@ -0,0 +1,2 @@
+cache_rbtree.test:
+
diff --git a/src/tests/modules/default-input.attrs b/src/tests/modules/default-input.attrs
new file mode 100644
index 0000000..d24ac4b
--- /dev/null
+++ b/src/tests/modules/default-input.attrs
@@ -0,0 +1,11 @@
+#
+# Input packet
+#
+User-Name = "bob"
+User-Password = "hello"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Filter-Id == 'success'
diff --git a/src/tests/modules/files/addcontrol.attrs b/src/tests/modules/files/addcontrol.attrs
new file mode 100644
index 0000000..7588b9c
--- /dev/null
+++ b/src/tests/modules/files/addcontrol.attrs
@@ -0,0 +1,13 @@
+#
+# Input packet
+#
+User-Name = "addcontrol"
+User-Password = "testing123"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Cleartext-Password == 'testing123'
+Reply-Message == "success1"
+Reply-Message == "success2"
diff --git a/src/tests/modules/files/addcontrol.unlang b/src/tests/modules/files/addcontrol.unlang
new file mode 100644
index 0000000..5b431f1
--- /dev/null
+++ b/src/tests/modules/files/addcontrol.unlang
@@ -0,0 +1,8 @@
+#
+# Run the "files" module
+#
+files
+
+update {
+ &reply: += &control:[*]
+}
diff --git a/src/tests/modules/files/addreply.attrs b/src/tests/modules/files/addreply.attrs
new file mode 100644
index 0000000..69e1a19
--- /dev/null
+++ b/src/tests/modules/files/addreply.attrs
@@ -0,0 +1,12 @@
+#
+# Input packet
+#
+User-Name = "addreply"
+User-Password = "testing123"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Reply-Message == 'success1'
+Reply-Message == 'success2'
diff --git a/src/tests/modules/files/addreply.unlang b/src/tests/modules/files/addreply.unlang
new file mode 100644
index 0000000..456c666
--- /dev/null
+++ b/src/tests/modules/files/addreply.unlang
@@ -0,0 +1,4 @@
+#
+# Run the "files" module
+#
+files
diff --git a/src/tests/modules/files/all.mk b/src/tests/modules/files/all.mk
new file mode 100644
index 0000000..07449db
--- /dev/null
+++ b/src/tests/modules/files/all.mk
@@ -0,0 +1,3 @@
+#
+# Test the "files" module
+#
diff --git a/src/tests/modules/files/authorize b/src/tests/modules/files/authorize
new file mode 100644
index 0000000..b85f6a2
--- /dev/null
+++ b/src/tests/modules/files/authorize
@@ -0,0 +1,92 @@
+#
+# Test if the "users" file works
+#
+
+
+#
+# Basic syntax tests with comments. Parsing only.
+#
+
+user Cleartext-Password := "hello" # comment!
+
+
+user2 # comment!
+ Reply-Message := "24"
+
+
+#
+# Setting ":=" of reply and control items
+#
+
+bob Cleartext-Password := "hello"
+ Reply-Message := "success"
+
+
+#
+# Detect erroneous Fall-Through
+#
+
+doug Cleartext-Password := "goodbye"
+ Reply-Message := "success"
+
+doug
+ Reply-Message := "unreachable"
+
+
+#
+# Fall-Through across a non-matching entry
+#
+
+famous Cleartext-Password := "bradpitt"
+ Fall-Through = yes
+
+unused Cleartext-Password := "jabberwocky"
+ Reply-Message := "fail"
+
+famous
+ Reply-Message := "success"
+
+
+#
+# Modification of the reply list
+#
+
+addreply Cleartext-Password := "testing123"
+ Reply-Message := "success1",
+ Fall-Through = yes
+
+addreply
+ Reply-Message += "success2"
+
+
+subreply Cleartext-Password := "testing123"
+ Reply-Message := "success1",
+ Reply-Message += "success2",
+ Reply-Message += "success3",
+ Fall-Through = yes
+
+subreply Cleartext-Password := "testing123"
+ Reply-Message -= "success2"
+
+
+filterreply Cleartext-Password := "testing123"
+ Reply-Message := "success1",
+ Reply-Message += "success2",
+ Fall-Through = yes
+
+filterreply Cleartext-Password := "testing123"
+ Reply-Message !* ANY
+
+
+#
+# Addition "+=" to the control list
+#
+# Note: Set ":=" of control items is already tested with Cleartext-Password
+# Note: Filtering "!*" does not apply to control items as this would overload
+# the operator syntax since "!*" checks that no such attribute in the
+# request.
+
+addcontrol Cleartext-Password := "testing123", Reply-Message := "success1"
+ Fall-Through = yes
+
+addcontrol Reply-Message += "success2"
diff --git a/src/tests/modules/files/bob.attrs b/src/tests/modules/files/bob.attrs
new file mode 100644
index 0000000..a4acfab
--- /dev/null
+++ b/src/tests/modules/files/bob.attrs
@@ -0,0 +1,11 @@
+#
+# Input packet
+#
+User-Name = "doug"
+User-Password = "goodbye"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Reply-Message == 'success'
diff --git a/src/tests/modules/files/bob.unlang b/src/tests/modules/files/bob.unlang
new file mode 100644
index 0000000..456c666
--- /dev/null
+++ b/src/tests/modules/files/bob.unlang
@@ -0,0 +1,4 @@
+#
+# Run the "files" module
+#
+files
diff --git a/src/tests/modules/files/doug.attrs b/src/tests/modules/files/doug.attrs
new file mode 100644
index 0000000..a4acfab
--- /dev/null
+++ b/src/tests/modules/files/doug.attrs
@@ -0,0 +1,11 @@
+#
+# Input packet
+#
+User-Name = "doug"
+User-Password = "goodbye"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Reply-Message == 'success'
diff --git a/src/tests/modules/files/doug.unlang b/src/tests/modules/files/doug.unlang
new file mode 100644
index 0000000..456c666
--- /dev/null
+++ b/src/tests/modules/files/doug.unlang
@@ -0,0 +1,4 @@
+#
+# Run the "files" module
+#
+files
diff --git a/src/tests/modules/files/fall-through.attrs b/src/tests/modules/files/fall-through.attrs
new file mode 100644
index 0000000..899d3d9
--- /dev/null
+++ b/src/tests/modules/files/fall-through.attrs
@@ -0,0 +1,11 @@
+#
+# Input packet
+#
+User-Name = "famous"
+User-Password = "bradpitt"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Reply-Message == 'success'
diff --git a/src/tests/modules/files/fall-through.unlang b/src/tests/modules/files/fall-through.unlang
new file mode 100644
index 0000000..456c666
--- /dev/null
+++ b/src/tests/modules/files/fall-through.unlang
@@ -0,0 +1,4 @@
+#
+# Run the "files" module
+#
+files
diff --git a/src/tests/modules/files/filterreply.attrs b/src/tests/modules/files/filterreply.attrs
new file mode 100644
index 0000000..c1add29
--- /dev/null
+++ b/src/tests/modules/files/filterreply.attrs
@@ -0,0 +1,10 @@
+#
+# Input packet
+#
+User-Name = "filterreply"
+User-Password = "testing123"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/files/filterreply.unlang b/src/tests/modules/files/filterreply.unlang
new file mode 100644
index 0000000..456c666
--- /dev/null
+++ b/src/tests/modules/files/filterreply.unlang
@@ -0,0 +1,4 @@
+#
+# Run the "files" module
+#
+files
diff --git a/src/tests/modules/files/module.conf b/src/tests/modules/files/module.conf
new file mode 100644
index 0000000..12b46ac
--- /dev/null
+++ b/src/tests/modules/files/module.conf
@@ -0,0 +1,9 @@
+files {
+ # The default key attribute to use for matches. The content
+ # of this attribute is used to match the "name" of the
+ # entry.
+ #key = "%{%{Stripped-User-Name}:-%{User-Name}}"
+
+ # The old "users" style file is now located here.
+ filename = $ENV{MODULE_TEST_DIR}/authorize
+}
diff --git a/src/tests/modules/files/subreply.attrs b/src/tests/modules/files/subreply.attrs
new file mode 100644
index 0000000..6fe6237
--- /dev/null
+++ b/src/tests/modules/files/subreply.attrs
@@ -0,0 +1,12 @@
+#
+# Input packet
+#
+User-Name = "subreply"
+User-Password = "testing123"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Reply-Message == 'success1'
+Reply-Message == 'success3'
diff --git a/src/tests/modules/files/subreply.unlang b/src/tests/modules/files/subreply.unlang
new file mode 100644
index 0000000..456c666
--- /dev/null
+++ b/src/tests/modules/files/subreply.unlang
@@ -0,0 +1,4 @@
+#
+# Run the "files" module
+#
+files
diff --git a/src/tests/modules/json/all.mk b/src/tests/modules/json/all.mk
new file mode 100644
index 0000000..4d3197d
--- /dev/null
+++ b/src/tests/modules/json/all.mk
@@ -0,0 +1,3 @@
+#
+# Test the "json" module
+#
diff --git a/src/tests/modules/json/encode.attrs b/src/tests/modules/json/encode.attrs
new file mode 100644
index 0000000..ea8d653
--- /dev/null
+++ b/src/tests/modules/json/encode.attrs
@@ -0,0 +1,13 @@
+#
+# Input packet
+#
+User-Name = 'john'
+Filter-Id = "f1"
+Filter-Id += "f2"
+NAS-Port = 999
+Service-Type = Login-User
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/json/encode.unlang b/src/tests/modules/json/encode.unlang
new file mode 100644
index 0000000..6c7a1fe
--- /dev/null
+++ b/src/tests/modules/json/encode.unlang
@@ -0,0 +1,233 @@
+#
+# json_encode tests
+#
+
+
+# 0. Check basic xlat parsing
+
+update control {
+ &Tmp-String-1 := "%{json_encode:&request:[*]}"
+ &Tmp-String-2 := "%{json_encode:&request:[*] }"
+ &Tmp-String-3 := "%{json_encode: &request:[*]}"
+ &Tmp-String-4 := "%{json_encode: &request:[*] }"
+ &Tmp-String-5 := "%{json_encode: &request:[*] !&Filter-Id }"
+ &Tmp-String-6 := "%{json_encode:&request:[*] ! }"
+## Check defaults are the same as output_mode "object":
+ &Tmp-String-7 := "%{json_object_encode:&request:[*]}"
+ &Tmp-String-8 := "%{json_object_no_encode:&request:[*]}"
+}
+
+
+if (&control:Tmp-String-1 != '{"User-Name":{"type":"string","value":"john"},"Filter-Id":{"type":"string","value":["f1","f2"]},"NAS-Port":{"type":"integer","value":999},"Service-Type":{"type":"integer","value":"Login-User"}}') {
+ test_fail
+}
+
+# Check xlat input formats
+if (&control:Tmp-String-1 != &control:Tmp-String-2 || \
+ &control:Tmp-String-1 != &control:Tmp-String-3 || \
+ &control:Tmp-String-1 != &control:Tmp-String-4) {
+ test_fail
+}
+
+# Check defaults
+if (&control:Tmp-String-1 != &control:Tmp-String-7 || \
+ &control:Tmp-String-1 != &control:Tmp-String-8) {
+ test_fail
+}
+
+if (&control:Tmp-String-5 != '{"User-Name":{"type":"string","value":"john"},"NAS-Port":{"type":"integer","value":999},"Service-Type":{"type":"integer","value":"Login-User"}}') {
+ test_fail
+}
+
+if (&control:Tmp-String-6 != '') {
+ test_fail
+}
+
+update control {
+ &Tmp-String-1 !* ANY
+ &Tmp-String-2 !* ANY
+ &Tmp-String-3 !* ANY
+ &Tmp-String-4 !* ANY
+ &Tmp-String-5 !* ANY
+ &Tmp-String-6 !* ANY
+ &Tmp-String-7 !* ANY
+ &Tmp-String-8 !* ANY
+}
+
+
+# 1a. Output mode "object" tests
+
+# These are unsorted dictionaries. Hopefully json-c doesn't suddenly
+# decide that it's going to use a different ordering of the keys...
+
+update control {
+ &Tmp-String-1 := "%{json_object_encode:&request:[*]}"
+ &Tmp-String-2 := "%{json_object_ex_encode:&request:[*]}"
+}
+
+if (&control:Tmp-String-1 != '{"User-Name":{"type":"string","value":"john"},"Filter-Id":{"type":"string","value":["f1","f2"]},"NAS-Port":{"type":"integer","value":999},"Service-Type":{"type":"integer","value":"Login-User"}}') {
+ test_fail
+}
+
+if (&control:Tmp-String-2 != '{"pf:User-Name":{"type":"string","value":["john"]},"pf:Filter-Id":{"type":"string","value":["f1","f2"]},"pf:NAS-Port":{"type":"integer","value":["999"]},"pf:Service-Type":{"type":"integer","value":["1"]}}') {
+ test_fail
+}
+
+# 1b. "object" empty inputs
+
+update control {
+ &Tmp-String-1 := "%{json_object_encode:!&request:[*]}"
+}
+
+if (&control:Tmp-String-1 != '{}') {
+ test_fail
+}
+
+update control {
+ &Tmp-String-1 !* ANY
+ &Tmp-String-2 !* ANY
+ &Module-Failure-Message !* ANY
+}
+
+
+# 2a. Output mode "object_simple" tests
+
+update control {
+ &Tmp-String-1 := "%{json_object_simple_encode:&request:[*]}"
+ &Tmp-String-2 := "%{json_object_simple_ex_encode:&request:[*]}"
+}
+
+if (&control:Tmp-String-1 != '{"User-Name":"john","Filter-Id":["f1","f2"],"NAS-Port":999,"Service-Type":"Login-User"}') {
+ test_fail
+}
+
+if (&control:Tmp-String-2 != '{"pf:User-Name":["john"],"pf:Filter-Id":["f1","f2"],"pf:NAS-Port":["999"],"pf:Service-Type":["1"]}') {
+ test_fail
+}
+
+# 2b. "object_simple" empty inputs
+
+update control {
+ &Tmp-String-1 := "%{json_object_simple_encode:!&request:[*]}"
+}
+
+if (&control:Tmp-String-1 != '{}') {
+ test_fail
+}
+
+update control {
+ &Tmp-String-1 !* ANY
+ &Tmp-String-2 !* ANY
+ &Module-Failure-Message !* ANY
+}
+
+
+# 3a. Output mode "array" tests
+
+update control {
+ &Tmp-String-1 := "%{json_array_encode:&request:[*]}"
+ &Tmp-String-2 := "%{json_array_ex_encode:&request:[*]}"
+}
+
+if (&control:Tmp-String-1 != '[{"name":"User-Name","type":"string","value":"john"},{"name":"Filter-Id","type":"string","value":"f1"},{"name":"Filter-Id","type":"string","value":"f2"},{"name":"NAS-Port","type":"integer","value":999},{"name":"Service-Type","type":"integer","value":"Login-User"}]') {
+ test_fail
+}
+
+if (&control:Tmp-String-2 != '[{"name":"pf:User-Name","type":"string","value":["john"]},{"name":"pf:Filter-Id","type":"string","value":["f1","f2"]},{"name":"pf:NAS-Port","type":"integer","value":["999"]},{"name":"pf:Service-Type","type":"integer","value":["1"]}]') {
+ test_fail
+}
+
+# 3b. "array" empty inputs
+
+update control {
+ &Tmp-String-1 := "%{json_array_encode:!&request:[*]}"
+}
+
+if (&control:Tmp-String-1 != '[]') {
+ test_fail
+}
+
+update control {
+ &Tmp-String-1 !* ANY
+ &Tmp-String-2 !* ANY
+ &Module-Failure-Message !* ANY
+}
+
+
+# 4a. Output mode "array_of_names" tests
+
+update control {
+ &Tmp-String-1 := "%{json_array_names_encode:&request:[*]}"
+ &Tmp-String-2 := "%{json_array_names_ex_encode:&request:[*]}"
+}
+
+if (&control:Tmp-String-1 != '["User-Name","Filter-Id","Filter-Id","NAS-Port","Service-Type"]') {
+ test_fail
+}
+
+if (&control:Tmp-String-2 != '["pf:User-Name","pf:Filter-Id","pf:Filter-Id","pf:NAS-Port","pf:Service-Type"]') {
+ test_fail
+}
+
+# 4b. "array_of_names" empty inputs
+
+update control {
+ &Tmp-String-1 := "%{json_array_names_encode:!&request:[*]}"
+}
+
+if (&control:Tmp-String-1 != '[]') {
+ test_fail
+}
+
+update control {
+ &Tmp-String-1 !* ANY
+ &Tmp-String-2 !* ANY
+ &Module-Failure-Message !* ANY
+}
+
+
+# 5a. Output mode "array_of_values" tests
+
+update control {
+ &Tmp-String-1 := "%{json_array_values_encode:&request:[*]}"
+ &Tmp-String-2 := "%{json_array_values_ex_encode:&request:[*]}"
+}
+
+if (&control:Tmp-String-1 != '["john","f1","f2",999,"Login-User"]') {
+ test_fail
+}
+
+if (&control:Tmp-String-2 != '["john","f1","f2","999","1"]') {
+ test_fail
+}
+
+# 5b. "array_of_values" empty inputs
+
+update control {
+ &Tmp-String-1 := "%{json_array_values_encode:!&request:[*]}"
+}
+
+if (&control:Tmp-String-1 != '[]') {
+ test_fail
+}
+
+update control {
+ &Tmp-String-1 !* ANY
+ &Tmp-String-2 !* ANY
+ &Module-Failure-Message !* ANY
+}
+
+
+# Convert `make json.test` unlang update output to tests, for when
+# things need updating.
+#
+# cat \
+# | cut -c44- \
+# | sed -e 's/\\"/"/g' \
+# -e 's/\s*$//' \
+# -e "s/:= \"/== '/" \
+# -e 's/^/if (/' \
+# -e "s/\"$/') {/" \
+# -e "s/$/\n test_pass\n} else {\n test_fail\n}\n/"
+
+test_pass
diff --git a/src/tests/modules/json/module.conf b/src/tests/modules/json/module.conf
new file mode 100644
index 0000000..04d1b1d
--- /dev/null
+++ b/src/tests/modules/json/module.conf
@@ -0,0 +1,150 @@
+json {
+}
+
+
+#
+# Output mode "object"
+#
+
+json json_object {
+ encode {
+ output_mode = object
+ }
+}
+
+json json_object_no {
+ encode {
+ output_mode = object
+
+ value {
+ single_value_as_array = no
+ enum_as_integer = no
+ always_string = no
+ }
+ }
+}
+
+
+json json_object_ex {
+ encode {
+ output_mode = object
+
+ attribute {
+ prefix = "pf"
+ }
+
+ value {
+ single_value_as_array = yes
+ enum_as_integer = yes
+ always_string = yes
+ }
+ }
+}
+
+
+#
+# Output mode "object_simple"
+#
+
+json json_object_simple {
+ encode {
+ output_mode = object_simple
+ }
+}
+
+json json_object_simple_ex {
+ encode {
+ output_mode = object_simple
+
+ attribute {
+ prefix = "pf"
+ }
+
+ value {
+ single_value_as_array = yes
+ enum_as_integer = yes
+ always_string = yes
+ }
+ }
+}
+
+
+#
+# Output mode "array"
+#
+
+json json_array {
+ encode {
+ output_mode = array
+ }
+}
+
+json json_array_ex {
+ encode {
+ output_mode = array
+
+ attribute {
+ prefix = "pf"
+ }
+
+ value {
+ single_value_as_array = yes
+ enum_as_integer = yes
+ always_string = yes
+ }
+ }
+}
+
+
+#
+# Output mode "array_of_names"
+#
+
+json json_array_names {
+ encode {
+ output_mode = array_of_names
+ }
+}
+
+json json_array_names_ex {
+ encode {
+ output_mode = array_of_names
+
+ attribute {
+ prefix = "pf"
+ }
+
+ value {
+ single_value_as_array = yes # not valid
+ enum_as_integer = yes # not valid
+ always_string = yes # not valid
+ }
+ }
+}
+
+
+#
+# Output mode "array_of_values"
+#
+
+json json_array_values {
+ encode {
+ output_mode = array_of_values
+ }
+}
+
+json json_array_values_ex {
+ encode {
+ output_mode = array_of_values
+
+ attribute {
+ prefix = "pf" # not valid
+ }
+
+ value {
+ single_value_as_array = yes # not valid
+ enum_as_integer = yes
+ always_string = yes
+ }
+ }
+}
diff --git a/src/tests/modules/ldap/acct.attrs b/src/tests/modules/ldap/acct.attrs
new file mode 100644
index 0000000..1d57034
--- /dev/null
+++ b/src/tests/modules/ldap/acct.attrs
@@ -0,0 +1,35 @@
+#
+# Input packet
+#
+User-Name = 'john'
+NAS-Port = 17826193
+NAS-IP-Address = 192.0.2.10
+Framed-IP-Address = 198.51.100.59
+NAS-Identifier = 'nas.example.org'
+Acct-Status-Type = Start
+Acct-Delay-Time = 1
+Acct-Input-Octets = 0
+Acct-Output-Octets = 0
+Acct-Session-Id = '00000000'
+Acct-Unique-Session-Id = '00000000'
+Acct-Authentic = RADIUS
+Acct-Session-Time = 0
+Acct-Input-Packets = 0
+Acct-Output-Packets = 0
+Acct-Input-Gigawords = 0
+Acct-Output-Gigawords = 0
+Event-Timestamp = 'Feb 1 2015 08:28:58 WIB'
+NAS-Port-Type = Ethernet
+NAS-Port-Id = 'port 001'
+Service-Type = Framed-User
+Framed-Protocol = PPP
+Acct-Link-Count = 0
+Idle-Timeout = 0
+Session-Timeout = 604800
+Access-Loop-Encapsulation = 0x000000
+Proxy-State = 0x323531
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/ldap/acct.unlang b/src/tests/modules/ldap/acct.unlang
new file mode 100644
index 0000000..2297ea7
--- /dev/null
+++ b/src/tests/modules/ldap/acct.unlang
@@ -0,0 +1,23 @@
+#
+# Run the "ldap" module
+# PRE: auth
+#
+ldap.accounting {
+}
+if (ok) {
+ test_pass
+}
+else {
+ test_fail
+}
+
+update {
+ Tmp-String-0 := "%{ldap:ldap://$ENV{TEST_SERVER}/uid=john,ou=people,dc=example,dc=com?description}"
+}
+
+if (&Tmp-String-0 != "User john is online") {
+ test_fail
+}
+else {
+ test_pass
+}
diff --git a/src/tests/modules/ldap/all.mk b/src/tests/modules/ldap/all.mk
new file mode 100644
index 0000000..1fc53d1
--- /dev/null
+++ b/src/tests/modules/ldap/all.mk
@@ -0,0 +1,8 @@
+#
+# Test the "ldap" module
+#
+
+# MODULE.test is the main target for this module.
+
+# Don't test ldap if TEST_SERVER ENV is not set
+ldap_require_test_server := 1
diff --git a/src/tests/modules/ldap/auth.attrs b/src/tests/modules/ldap/auth.attrs
new file mode 100644
index 0000000..be988ee
--- /dev/null
+++ b/src/tests/modules/ldap/auth.attrs
@@ -0,0 +1,15 @@
+#
+# Input packet
+#
+User-Name = "john"
+User-Password = "password"
+NAS-IP-Address = 1.2.3.5
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Idle-Timeout == 3600
+Session-Timeout == 7200
+Acct-Interim-Interval == 1800
+Framed-IP-Netmask == "255.255.0.0"
diff --git a/src/tests/modules/ldap/auth.unlang b/src/tests/modules/ldap/auth.unlang
new file mode 100644
index 0000000..edf14bf
--- /dev/null
+++ b/src/tests/modules/ldap/auth.unlang
@@ -0,0 +1,72 @@
+#
+# Run the "ldap" module
+#
+ldap
+
+if (&control:NAS-IP-Address != 1.2.3.4) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+if (&control:Reply-Message != "Hello world") {
+ test_fail
+}
+else {
+ test_pass
+}
+
+# Cmp operator means Framed-IP-Address is ignored
+if (&control:Framed-IP-Address) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+# IP netmask defined in profile1 should overwrite radprofile value.
+if (&reply:Framed-IP-Netmask != 255.255.0.0) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+if (&reply:Acct-Interim-Interval != 1800) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+if (&reply:Idle-Timeout != 3600) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+if (&reply:Session-Timeout != 7200) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+if ("%{pairs:reply:}" == "") {
+ test_fail
+}
+
+ldap.post-auth
+
+update {
+ Tmp-String-0 := "%{ldap:ldap://$ENV{TEST_SERVER}/uid=john,ou=people,dc=example,dc=com?description}"
+}
+
+if (&Tmp-String-0 != "User %{User-Name} authenticated") {
+ test_fail
+}
+else {
+ test_pass
+}
diff --git a/src/tests/modules/ldap/example.com.ldif b/src/tests/modules/ldap/example.com.ldif
new file mode 120000
index 0000000..055d379
--- /dev/null
+++ b/src/tests/modules/ldap/example.com.ldif
@@ -0,0 +1 @@
+../../salt-test-server/salt/ldap/base.ldif \ No newline at end of file
diff --git a/src/tests/modules/ldap/groups_rfc2307bis.attrs b/src/tests/modules/ldap/groups_rfc2307bis.attrs
new file mode 100644
index 0000000..be988ee
--- /dev/null
+++ b/src/tests/modules/ldap/groups_rfc2307bis.attrs
@@ -0,0 +1,15 @@
+#
+# Input packet
+#
+User-Name = "john"
+User-Password = "password"
+NAS-IP-Address = 1.2.3.5
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Idle-Timeout == 3600
+Session-Timeout == 7200
+Acct-Interim-Interval == 1800
+Framed-IP-Netmask == "255.255.0.0"
diff --git a/src/tests/modules/ldap/groups_rfc2307bis.unlang b/src/tests/modules/ldap/groups_rfc2307bis.unlang
new file mode 100644
index 0000000..b8f48b5
--- /dev/null
+++ b/src/tests/modules/ldap/groups_rfc2307bis.unlang
@@ -0,0 +1,41 @@
+#
+# Run the "ldap" module
+#
+ldap
+
+#
+# Resolve using group name attribute
+#
+if (LDAP-Group == 'foo') {
+ test_pass
+}
+else {
+ test_fail
+}
+
+#
+# Resolve using group DN
+#
+if (LDAP-Group == 'cn=foo,ou=groups,dc=example,dc=com') {
+ test_pass
+}
+else {
+ test_fail
+}
+
+#
+# Check we have these values cached
+#
+if (&control:LDAP-Cached-Membership[*] == 'foo') {
+ test_pass
+}
+else {
+ test_fail
+}
+
+if (&control:LDAP-Cached-Membership[*] == 'cn=foo,ou=groups,dc=example,dc=com') {
+ test_pass
+}
+else {
+ test_fail
+}
diff --git a/src/tests/modules/ldap/module.conf b/src/tests/modules/ldap/module.conf
new file mode 100644
index 0000000..dcf3126
--- /dev/null
+++ b/src/tests/modules/ldap/module.conf
@@ -0,0 +1,537 @@
+# -*- text -*-
+#
+# $Id$
+
+#
+# Lightweight Directory Access Protocol (LDAP)
+#
+ldap {
+ # Note that this needs to match the name(s) in the LDAP server
+ # certificate, if you're using ldaps. See OpenLDAP documentation
+ # for the behavioral semantics of specifying more than one host.
+ #
+ # Depending on the libldap in use, server may be an LDAP URI.
+ # In the case of OpenLDAP this allows additional the following
+ # additional schemes:
+ # - ldaps:// (LDAP over SSL)
+ # - ldapi:// (LDAP over Unix socket)
+ # - ldapc:// (Connectionless LDAP)
+ server = $ENV{LDAP_TEST_SERVER}
+# server = 'ldap.rrdns.example.org'
+
+ # Port to connect on, defaults to 389, will be ignored for LDAP URIs.
+ port = $ENV{LDAP_TEST_SERVER_PORT}
+
+ # Administrator account for searching and possibly modifying.
+ identity = 'cn=admin,dc=example,dc=com'
+ password = secret
+
+ # Unless overridden in another section, the dn from which all
+ # searches will start from.
+ base_dn = 'dc=example,dc=com'
+
+ # SASL parameters to use for admin binds
+ #
+ # When we're prompted by the SASL library, these control
+ # the responses given.
+ #
+ sasl {
+ # SASL mechanism
+# mech = 'PLAIN'
+
+ # SASL authorisation identity to proxy.
+# proxy = 'autz_id'
+
+ # SASL realm. Used for kerberos.
+# realm = 'example.org'
+ }
+
+ #
+ # Generic valuepair attribute
+ #
+
+ # If set, this will attribute will be retrieved in addition to any
+ # mapped attributes.
+ #
+ # Values should be in the format:
+ # <radius attr> <op> <value>
+ #
+ # Where:
+ # <radius attr>: Is the attribute you wish to create
+ # with any valid list and request qualifiers.
+ # <op>: Is any assignment operator (=, :=, +=, -=).
+ # <value>: Is the value to parse into the new valuepair.
+ # If the value is wrapped in double quotes it
+ # will be xlat expanded.
+ valuepair_attribute = 'radiusAttribute'
+
+ #
+ # Mapping of LDAP directory attributes to RADIUS dictionary attributes.
+ #
+
+ # WARNING: Although this format is almost identical to the unlang
+ # update section format, it does *NOT* mean that you can use other
+ # unlang constructs in module configuration files.
+ #
+ # Configuration items are in the format:
+ # <radius attr> <op> <ldap attr>
+ #
+ # Where:
+ # <radius attr>: Is the destination RADIUS attribute
+ # with any valid list and request qualifiers.
+ # <op>: Is any assignment attribute (=, :=, +=, -=).
+ # <ldap attr>: Is the attribute associated with user or
+ # profile objects in the LDAP directory.
+ # If the attribute name is wrapped in double
+ # quotes it will be xlat expanded.
+ #
+ # Request and list qualifiers may also be placed after the 'update'
+ # section name to set defaults destination requests/lists
+ # for unqualified RADIUS attributes.
+ #
+ # Note: LDAP attribute names should be single quoted unless you want
+ # the name value to be derived from an xlat expansion, or an
+ # attribute ref.
+ update {
+ control:Password-With-Header += 'userPassword'
+ reply:Idle-Timeout := 'radiusIdleTimeout'
+ reply:Framed-IP-Netmask := 'radiusFramedIPNetmask'
+# control:NT-Password := 'ntPassword'
+# reply:Reply-Message := 'radiusReplyMessage'
+# reply:Tunnel-Type := 'radiusTunnelType'
+# reply:Tunnel-Medium-Type := 'radiusTunnelMediumType'
+# reply:Tunnel-Private-Group-ID := 'radiusTunnelPrivategroupId'
+
+ # Where only a list is specified as the RADIUS attribute,
+ # the value of the LDAP attribute is parsed as a valuepair
+ # in the same format as the 'valuepair_attribute' (above).
+ control: += 'radiusControlAttribute'
+ request: += 'radiusRequestAttribute'
+ reply: += 'radiusReplyAttribute'
+ }
+
+ # Set to yes if you have eDirectory and want to use the universal
+ # password mechanism.
+# edir = no
+
+ # Set to yes if you want to bind as the user after retrieving the
+ # Cleartext-Password. This will consume the login grace, and
+ # verify user authorization.
+# edir_autz = no
+
+ # Note: set_auth_type was removed in v3.x.x
+ # Equivalent functionality can be achieved by adding the following
+ # stanza to the authorize {} section of your virtual server.
+ #
+ # ldap
+ # if ((ok || updated) && User-Password) {
+ # update {
+ # control:Auth-Type := ldap
+ # }
+ # }
+
+ #
+ # User object identification.
+ #
+ user {
+ # Where to start searching in the tree for users
+ base_dn = "ou=people,${..base_dn}"
+
+ # Filter for user objects, should be specific enough
+ # to identify a single user object.
+ filter = "(uid=%{%{Stripped-User-Name}:-%{User-Name}})"
+
+ # SASL parameters to use for user binds
+ #
+ # When we're prompted by the SASL library, these control
+ # the responses given.
+ #
+ # Any of the config items below may be an attribute ref
+ # or and expansion, so different SASL mechs, proxy IDs
+ # and realms may be used for different users.
+ sasl {
+ # SASL mechanism
+# mech = 'PLAIN'
+
+ # SASL authorisation identity to proxy.
+# proxy = &User-Name
+
+ # SASL realm. Used for kerberos.
+# realm = 'example.org'
+ }
+
+ # Search scope, may be 'base', 'one', sub' or 'children'
+# scope = 'sub'
+
+ # If this is undefined, anyone is authorised.
+ # If it is defined, the contents of this attribute
+ # determine whether or not the user is authorised
+# access_attribute = 'dialupAccess'
+
+ # Control whether the presence of 'access_attribute'
+ # allows access, or denys access.
+ #
+ # If 'yes', and the access_attribute is present, or
+ # 'no' and the access_attribute is absent then access
+ # will be allowed.
+ #
+ # If 'yes', and the access_attribute is absent, or
+ # 'no' and the access_attribute is present, then
+ # access will not be allowed.
+ #
+ # If the value of the access_attribute is 'false', it
+ # will negate the result.
+ #
+ # e.g.
+ # access_positive = yes
+ # access_attribute = userAccessAllowed
+ #
+ # With an LDAP object containing:
+ # userAccessAllowed: false
+ #
+ # Will result in the user being locked out.
+# access_positive = yes
+ }
+
+ #
+ # User membership checking.
+ #
+ group {
+ # Where to start searching in the tree for groups
+ base_dn = "ou=groups,${..base_dn}"
+
+ # Filter for group objects, should match all available
+ # group objects a user might be a member of.
+ filter = '(objectClass=groupOfNames)'
+
+ # Search scope, may be 'base', 'one', sub' or 'children'
+ scope = 'sub'
+
+ # Attribute that uniquely identifies a group.
+ # Is used when converting group DNs to group
+ # names.
+ name_attribute = cn
+
+ # Filter to find group objects a user is a member of.
+ # That is, group objects with attributes that
+ # identify members (the inverse of membership_attribute).
+ membership_filter = "(|(member=%{control:Ldap-UserDn})(memberUid=%{%{Stripped-User-Name}:-%{User-Name}}))"
+
+ # The attribute in user objects which contain the names
+ # or DNs of groups a user is a member of.
+ #
+ # Unless a conversion between group name and group DN is
+ # needed, there's no requirement for the group objects
+ # referenced to actually exist.
+ membership_attribute = 'memberOf'
+
+ # If cacheable_name or cacheable_dn are enabled,
+ # all group information for the user will be
+ # retrieved from the directory and written to LDAP-Group
+ # attributes appropriate for the instance of rlm_ldap.
+ #
+ # For group comparisons these attributes will be checked
+ # instead of querying the LDAP directory directly.
+ #
+ # This feature is intended to be used with rlm_cache.
+ #
+ # If you wish to use this feature, you should enable
+ # the type that matches the format of your check items
+ # i.e. if your groups are specified as DNs then enable
+ # cacheable_dn else enable cacheable_name.
+ cacheable_name = yes
+ cacheable_dn = yes
+
+ # Override the normal cache attribute (<inst>-LDAP-Group)
+ # and create a custom attribute. This can help if multiple
+ # module instances are used in fail-over.
+ cache_attribute = 'LDAP-Cached-Membership'
+ }
+
+ #
+ # User profiles. RADIUS profile objects contain sets of attributes
+ # to insert into the request. These attributes are mapped using
+ # the same mapping scheme applied to user objects.
+ #
+ profile {
+ # Filter for RADIUS profile objects
+ filter = '(objectclass=radiusprofile)'
+
+ # The default profile applied to all users.
+ default = 'cn=radprofile,ou=profiles,dc=example,dc=com'
+
+ # The list of profiles which are applied (after the default)
+ # to all users.
+ # The 'User-Profile' attribute in the control list
+ # will override this setting at run-time.
+ attribute = 'radiusProfileDn'
+ }
+
+ #
+ # Bulk load clients from the directory
+ #
+ client {
+ # Where to start searching in the tree for clients
+ base_dn = "ou=clients,${..base_dn}"
+
+ #
+ # Filter to match client objects
+ #
+ filter = '(objectClass=radiusClient)'
+
+ # Search scope, may be 'base', 'one', 'sub' or 'children'
+# scope = 'sub'
+
+ #
+ # Sets default values (not obtained from LDAP) for new client entries
+ #
+ template {
+# login = 'test'
+# password = 'test'
+# proto = tcp
+# require_message_authenticator = yes
+
+ # Uncomment to add a home_server with the same
+ # attributes as the client.
+# coa_server {
+# response_window = 2.0
+# }
+ }
+
+ #
+ # Client attribute mappings are in the format:
+ # <client attribute> = <ldap attribute>
+ #
+ # The following attributes are required:
+ # * ipaddr | ipv4addr | ipv6addr - Client IP Address.
+ # * secret - RADIUS shared secret.
+ #
+ # All other attributes usually supported in a client
+ # definition are also supported here.
+ #
+ # Schemas are available in doc/schemas/ldap for openldap and eDirectory
+ #
+ attribute {
+ ipaddr = 'radiusClientIdentifier'
+ secret = 'radiusClientSecret'
+# shortname = 'radiusClientShortname'
+# nas_type = 'radiusClientType'
+# virtual_server = 'radiusClientVirtualServer'
+# require_message_authenticator = 'radiusClientRequireMa'
+ }
+ }
+
+ # Load clients on startup
+# read_clients = no
+
+ #
+ # Modify user object on receiving Accounting-Request
+ #
+
+ # Useful for recording things like the last time the user logged
+ # in, or the Acct-Session-ID for CoA/DM.
+ #
+ # LDAP modification items are in the format:
+ # <ldap attr> <op> <value>
+ #
+ # Where:
+ # <ldap attr>: The LDAP attribute to add modify or delete.
+ # <op>: One of the assignment operators:
+ # (:=, +=, -=, ++).
+ # Note: '=' is *not* supported.
+ # <value>: The value to add modify or delete.
+ #
+ # WARNING: If using the ':=' operator with a multi-valued LDAP
+ # attribute, all instances of the attribute will be removed and
+ # replaced with a single attribute.
+ accounting {
+ reference = "%{tolower:type.%{Acct-Status-Type}}"
+
+ type {
+ start {
+ update {
+ description := "User %{User-Name} is online"
+ }
+ }
+
+ interim-update {
+ update {
+ description := "Last seen at %S"
+ }
+ }
+
+ stop {
+ update {
+ description := "Offline at %S"
+ }
+ }
+ }
+ }
+
+ #
+ # Post-Auth can modify LDAP objects too
+ #
+ post-auth {
+ update {
+ description := "User %{User-Name} authenticated"
+ }
+ }
+
+ #
+ # LDAP connection-specific options.
+ #
+ # These options set timeouts, keep-alives, etc. for the connections.
+ #
+ options {
+ # Control under which situations aliases are followed.
+ # May be one of 'never', 'searching', 'finding' or 'always'
+ # default: libldap's default which is usually 'never'.
+ #
+ # LDAP_OPT_DEREF is set to this value.
+# dereference = 'always'
+
+ #
+ # The following two configuration items control whether the
+ # server follows references returned by LDAP directory.
+ # They are mostly for Active Directory compatibility.
+ # If you set these to 'no', then searches will likely return
+ # 'operations error', instead of a useful result.
+ #
+ chase_referrals = yes
+ rebind = yes
+
+ # Seconds to wait for LDAP query to finish. default: 20
+ timeout = 10
+
+ # Seconds LDAP server has to process the query (server-side
+ # time limit). default: 20
+ #
+ # LDAP_OPT_TIMELIMIT is set to this value.
+ timelimit = 3
+
+ # Seconds to wait for response of the server. (network
+ # failures) default: 10
+ #
+ # LDAP_OPT_NETWORK_TIMEOUT is set to this value.
+ net_timeout = 1
+
+ # LDAP_OPT_X_KEEPALIVE_IDLE
+ idle = 60
+
+ # LDAP_OPT_X_KEEPALIVE_PROBES
+ probes = 3
+
+ # LDAP_OPT_X_KEEPALIVE_INTERVAL
+ interval = 3
+
+ # ldap_debug: debug flag for LDAP SDK
+ # (see OpenLDAP documentation). Set this to enable
+ # huge amounts of LDAP debugging on the screen.
+ # You should only use this if you are an LDAP expert.
+ #
+ # default: 0x0000 (no debugging messages)
+ # Example:(LDAP_DEBUG_FILTER+LDAP_DEBUG_CONNS)
+ ldap_debug = 0x0801
+ }
+
+ #
+ # This subsection configures the tls related items
+ # that control how FreeRADIUS connects to an LDAP
+ # server. It contains all of the 'tls_*' configuration
+ # entries used in older versions of FreeRADIUS. Those
+ # configuration entries can still be used, but we recommend
+ # using these.
+ #
+ tls {
+ # Set this to 'yes' to use TLS encrypted connections
+ # to the LDAP database by using the StartTLS extended
+ # operation.
+ #
+ # The StartTLS operation is supposed to be
+ # used with normal ldap connections instead of
+ # using ldaps (port 636) connections
+# start_tls = yes
+
+# ca_file = ${certdir}/cacert.pem
+
+# ca_path = ${certdir}
+# certificate_file = /path/to/radius.crt
+# private_key_file = /path/to/radius.key
+# random_file = ${certdir}/random
+
+ # Certificate Verification requirements. Can be:
+ # 'never' (don't even bother trying)
+ # 'allow' (try, but don't fail if the certificate
+ # can't be verified)
+ # 'demand' (fail if the certificate doesn't verify.)
+ #
+ # The default is 'allow'
+# require_cert = 'demand'
+ }
+
+
+ # As of version 3.0, the 'pool' section has replaced the
+ # following configuration items:
+ #
+ # ldap_connections_number
+
+ # The connection pool is new for 3.0, and will be used in many
+ # modules, for all kinds of connection-related activity.
+ #
+ # When the server is not threaded, the connection pool
+ # limits are ignored, and only one connection is used.
+ pool {
+ # Number of connections to start
+ start = 5
+
+ # Minimum number of connections to keep open
+ min = 4
+
+ # Maximum number of connections
+ #
+ # If these connections are all in use and a new one
+ # is requested, the request will NOT get a connection.
+ #
+ # Setting 'max' to LESS than the number of threads means
+ # that some threads may starve, and you will see errors
+ # like 'No connections available and at max connection limit'
+ #
+ # Setting 'max' to MORE than the number of threads means
+ # that there are more connections than necessary.
+ max = 4
+
+ # Spare connections to be left idle
+ #
+ # NOTE: Idle connections WILL be closed if 'idle_timeout'
+ # is set.
+ spare = 3
+
+ # Number of uses before the connection is closed
+ #
+ # 0 means 'infinite'
+ uses = 0
+
+ # The lifetime (in seconds) of the connection
+ lifetime = 0
+
+ # Idle timeout (in seconds). A connection which is
+ # unused for this length of time will be closed.
+ idle_timeout = 60
+
+ # The number of seconds to wait after the server tries
+ # to open a connection, and fails. During this time,
+ # no new connections will be opened.
+ #
+ retry_delay = 1
+
+ # NOTE: All configuration settings are enforced. If a
+ # connection is closed because of 'idle_timeout',
+ # 'uses', or 'lifetime', then the total number of
+ # connections MAY fall below 'min'. When that
+ # happens, it will open a new connection. It will
+ # also log a WARNING message.
+ #
+ # The solution is to either lower the 'min' connections,
+ # or increase lifetime/idle_timeout.
+ }
+}
diff --git a/src/tests/modules/pap/all.mk b/src/tests/modules/pap/all.mk
new file mode 100644
index 0000000..5c1de6f
--- /dev/null
+++ b/src/tests/modules/pap/all.mk
@@ -0,0 +1,3 @@
+#
+# Test the "pap" module
+#
diff --git a/src/tests/modules/pap/module.conf b/src/tests/modules/pap/module.conf
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/tests/modules/pap/module.conf
@@ -0,0 +1 @@
+
diff --git a/src/tests/modules/pap/pbkfd2_dig_big.attrs b/src/tests/modules/pap/pbkfd2_dig_big.attrs
new file mode 100644
index 0000000..90fc451
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_dig_big.attrs
@@ -0,0 +1,11 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_dig_big'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+
diff --git a/src/tests/modules/pap/pbkfd2_dig_big.unlang b/src/tests/modules/pap/pbkfd2_dig_big.unlang
new file mode 100644
index 0000000..449967f
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_dig_big.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_dig_big') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AAAAAQ:E+VXOSsE8RwyYGdygQoW9Q==:UivlvrwHML4VtZHMJLiT/xlH7oyoyvbXQceivptq9TI='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_dig_small.attrs b/src/tests/modules/pap/pbkfd2_dig_small.attrs
new file mode 100644
index 0000000..dbc5bdd
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_dig_small.attrs
@@ -0,0 +1,11 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_dig_small'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+
diff --git a/src/tests/modules/pap/pbkfd2_dig_small.unlang b/src/tests/modules/pap/pbkfd2_dig_small.unlang
new file mode 100644
index 0000000..37f08ee
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_dig_small.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_dig_small') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AAAAAQ:E+VXOSsE8RwyYGdygQoW9Q==:UivlvrwHML4VtZHMJLiT/xlH7oyoyvbXQceivptq9TI'
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_iter0.attrs b/src/tests/modules/pap/pbkfd2_iter0.attrs
new file mode 100644
index 0000000..871017e
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_iter0.attrs
@@ -0,0 +1,11 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_iter0'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+
diff --git a/src/tests/modules/pap/pbkfd2_iter0.unlang b/src/tests/modules/pap/pbkfd2_iter0.unlang
new file mode 100644
index 0000000..ca362c9
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_iter0.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_iter0') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AAAAAP:CuNDJ9NimZoP5ljnPNCBUA==:f09zV7dReGg5SIv/EXY9tCL4XQRr5guhL0Q6UXSKI3c='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_iter1.attrs b/src/tests/modules/pap/pbkfd2_iter1.attrs
new file mode 100644
index 0000000..e3d62cb
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_iter1.attrs
@@ -0,0 +1,11 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_iter1'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+
diff --git a/src/tests/modules/pap/pbkfd2_iter1.unlang b/src/tests/modules/pap/pbkfd2_iter1.unlang
new file mode 100644
index 0000000..6758c9b
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_iter1.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_iter1') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AAAAAQ:OErtptMl2hOxhQqvNw7sNw==:4KkrgL+3Q9j8KlHPivtApBKRZAjyWjtDWmZEz2UjNko='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_iter1000.attrs b/src/tests/modules/pap/pbkfd2_iter1000.attrs
new file mode 100644
index 0000000..10a19c3
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_iter1000.attrs
@@ -0,0 +1,11 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_iter1000'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+
diff --git a/src/tests/modules/pap/pbkfd2_iter1000.unlang b/src/tests/modules/pap/pbkfd2_iter1000.unlang
new file mode 100644
index 0000000..18fe680
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_iter1000.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_iter1000') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AAAD6A:yhmqoKrtPLY2KYK6cNjnfw==:Y6gkSZEo4TRtlsryHqnGYZhoe2qn5tJ4IUyyVHb/3WU='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_iter100000.attrs b/src/tests/modules/pap/pbkfd2_iter100000.attrs
new file mode 100644
index 0000000..8da916c
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_iter100000.attrs
@@ -0,0 +1,10 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_iter100000'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkfd2_iter100000.unlang b/src/tests/modules/pap/pbkfd2_iter100000.unlang
new file mode 100644
index 0000000..a1253e6
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_iter100000.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_iter100000') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AA9CQA:fCfnJGMVC1QLtTOPiaSICA==:KCmjMpQ+lokMvyFTl4f4pPJNc0xJq4iHZPdtHa0OEXM='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_iter_big.attrs b/src/tests/modules/pap/pbkfd2_iter_big.attrs
new file mode 100644
index 0000000..9f8dddb
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_iter_big.attrs
@@ -0,0 +1,11 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_iter_big'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+
diff --git a/src/tests/modules/pap/pbkfd2_iter_big.unlang b/src/tests/modules/pap/pbkfd2_iter_big.unlang
new file mode 100644
index 0000000..464d944
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_iter_big.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_iter_big') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AAAAAQ==:E+VXOSsE8RwyYGdygQoW9Q==:UivlvrwHML4VtZHMJLiT/xlH7oyoyvbXQceivptq9TI='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_iter_miss.attrs b/src/tests/modules/pap/pbkfd2_iter_miss.attrs
new file mode 100644
index 0000000..983db26
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_iter_miss.attrs
@@ -0,0 +1,11 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_iter_miss'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+
diff --git a/src/tests/modules/pap/pbkfd2_iter_miss.unlang b/src/tests/modules/pap/pbkfd2_iter_miss.unlang
new file mode 100644
index 0000000..44d961a
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_iter_miss.unlang
@@ -0,0 +1,19 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_iter_miss') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256::E+VXOSsE8RwyYGdygQoW9Q==:UivlvrwHML4VtZHMJLiT/xlH7oyoyvbXQceivptq9TI='
+ }
+ pap.authorize
+ pap.authenticate {
+ invalid = 1
+ }
+ if (invalid) {
+ test_pass
+ } else {
+ test_fail
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_iter_small.attrs b/src/tests/modules/pap/pbkfd2_iter_small.attrs
new file mode 100644
index 0000000..af8351b
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_iter_small.attrs
@@ -0,0 +1,11 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_iter_small'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+
diff --git a/src/tests/modules/pap/pbkfd2_iter_small.unlang b/src/tests/modules/pap/pbkfd2_iter_small.unlang
new file mode 100644
index 0000000..7edee80
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_iter_small.unlang
@@ -0,0 +1,19 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_iter_small') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AAAAA:E+VXOSsE8RwyYGdygQoW9Q==:UivlvrwHML4VtZHMJLiT/xlH7oyoyvbXQceivptq9TI='
+ }
+ pap.authorize
+ pap.authenticate {
+ invalid = 1
+ }
+ if (invalid) {
+ test_pass
+ } else {
+ test_fail
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_passlib.attrs b/src/tests/modules/pap/pbkfd2_passlib.attrs
new file mode 100644
index 0000000..29738bb
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_passlib.attrs
@@ -0,0 +1,10 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_passlib'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkfd2_passlib.unlang b/src/tests/modules/pap/pbkfd2_passlib.unlang
new file mode 100644
index 0000000..6d0f27d
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_passlib.unlang
@@ -0,0 +1,20 @@
+# Fixme - Base64 decode seems off for alt base64
+test_pass
+
+#if ("${feature.tls}" == no) {
+# test_pass
+# return
+#}
+
+#if (&User-Name == 'pbkdf2_passlib') {
+# update control {
+# &PBKDF2-Password := '$pbkdf2-sha256$29000$9t7be09prfXee2/NOUeotQ$Y.RDnnq8vsezSZSKy1QNy6xhKPdoBIwc.0XDdRm9sJ8'
+# }
+# pap.authorize
+# pap.authenticate
+# if (!ok) {
+# test_fail
+# } else {
+# test_pass
+# }
+#}
diff --git a/src/tests/modules/pap/pbkfd2_salt0.attrs b/src/tests/modules/pap/pbkfd2_salt0.attrs
new file mode 100644
index 0000000..7e6d209
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_salt0.attrs
@@ -0,0 +1,10 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_salt0'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkfd2_salt0.unlang b/src/tests/modules/pap/pbkfd2_salt0.unlang
new file mode 100644
index 0000000..173f768
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_salt0.unlang
@@ -0,0 +1,19 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_salt0') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AAAnEA::4RJEKVFQ5nE8126aURI0cJO9tqy/DIAhq64piBEwshA='
+ }
+ pap.authorize
+ pap.authenticate {
+ invalid = 1
+ }
+ if (invalid) {
+ test_pass
+ } else {
+ test_fail
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_salt1.attrs b/src/tests/modules/pap/pbkfd2_salt1.attrs
new file mode 100644
index 0000000..20ff1fe
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_salt1.attrs
@@ -0,0 +1,10 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_salt1'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkfd2_salt1.unlang b/src/tests/modules/pap/pbkfd2_salt1.unlang
new file mode 100644
index 0000000..4aa0fce
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_salt1.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_salt1') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AAAnEA:qg==:KQzCdedgOZYFwx+mQp1TKA8VM4fwf02pqSdJEh2ekwM='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_salt1024.attrs b/src/tests/modules/pap/pbkfd2_salt1024.attrs
new file mode 100644
index 0000000..30f2706
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_salt1024.attrs
@@ -0,0 +1,10 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_salt1024'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkfd2_salt1024.unlang b/src/tests/modules/pap/pbkfd2_salt1024.unlang
new file mode 100644
index 0000000..a4aab5e
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_salt1024.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_salt1024') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AAAnEA:/IUrkJKe+1kzJNBw7aAMbnQuSFZpjbCqPeKso3cbuSUzWinxngxjK8yyZLiWwF+WE/0Gplfx25zZEQNTdRTvjZZNefoxQBR8Hht0FpdU9YiEBaeErwVo63EDEu83+ycvB18uH0IXpJKGSSkIPRfGpHT3BkwJDGo5SqjRJadDsyQzkc/WJCMrrfJ0igaWMxb5eR5J8qfXIjBFepRrOOU6acZGtANW8qvDYLJwN+TMd9Jb1wDDY14eoAlKglTF21S3kewNMkDDyeP+oDYv29t1S/soFUnnB+Pb5IdR6pDy2VDGx4jFZMQGshSHWTYQFqgulavS/tGEF8TvzcorrJZKuksAjKdTSmfZ6j4aBY3U+oMSQ+2lO131pkNfNQuMsDfr72r9wUA2xRgUiL/J7CgKn7mamL2OCaksl0Rw2PGqqIaHvAYS6Q1EoIzsmLNrWBYYqTRLyCGZw6+hUOahYRon2lglGmnuWHPfowU+LgcaR5gF1QjvTXhXQ8I39mB3ePgdi+7TUn644Z1FB+JTqGJbue92x4V40Zyyy+Qdt52QsR49iYokbKAwQRiqfVJ7J8NzCY/kIQnqT9RE0NCxZoMBRzboZxVPchxdpmWGQ9dXP06PqIuDCFFiJlVQUfyPMgOAxIlVJ/9NAmj5MWFdWMrmlBNDx9ihEV1FdTv23iFZH5Ejg+x4D3qN5oOyCDL2i9lobzFXh5z4EDpbbogQaFkUzqKEaxRGPBrfYVOi6XXYujVUnxHJaRxbs2UqjpJNsXMg8f7P78aRvOKCIbW70CHWlt7nF0pA5+kFUQRLXKuq7bW+ivoXKeDW5o4FVP3+Pcr67+DOsUXuehALLj9Mu2ICWlMIV/AWcM2szaqk1bwSo7bAeG4RtDKmNjGA7gpnT+w2x+/qS1eWbc832Sumqc1IA8aY6HNVDPsJZf99To4BR+N0rCoQQ/KIZybI31mQagR3+FR9yNzqWzKIl+qf69RTc1CbUCkKVF8pxWZ0ocP+CAdoKadgpdF8evQIiGcUD73HiJ0RsDWo21y0tN0P5jfzWo3WMhCk9e2wl6o1JAfKw54uHzWJnNlGLBK1LXF+R2m+WvNGBgvUhh4PtYV9gPSudumFdk614oak/Aqcn6xi+YZqOMPkW4WYaiczhHyS7qAyefqKaQkRVYS0Af+79CSjlxZJq57HrD7/1E+d/i0gKmSAbPe80uGHs2a13V3VxztFMBi4xD7zj9Mq7+0goVPD4MNXcR651MZ7vxDRGbvPPmclddZe/nkTEn1YB/909b9mC5P/XzximZYW8gEhBReZouukADRTAjuH8zgSIv6/uyTURnmSVoOumVLBpL7veJIzDm4dZ38BWiasiBnzgMuG9A==:RUoCF5O11OgwLFMTqnKY/yRJy6DYh+yNq4xHZC7COGM='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_salt64.attrs b/src/tests/modules/pap/pbkfd2_salt64.attrs
new file mode 100644
index 0000000..da036f3
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_salt64.attrs
@@ -0,0 +1,10 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_salt64'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept \ No newline at end of file
diff --git a/src/tests/modules/pap/pbkfd2_salt64.unlang b/src/tests/modules/pap/pbkfd2_salt64.unlang
new file mode 100644
index 0000000..754cdba
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_salt64.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_salt64') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AAAnEA:msGxE1XuC+wlgRr+H4+ioyxZuiN3KYLUSky2FINDTq7KJylKt4XnqloV+FuHGXUbOu1EWcsFp51u2z8wdXVnQQ==:rAV9BeEJH5kt9uZ6pJt0o5pYpN5LQRe4MAYyk2jvjpU='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_salt_big.attrs b/src/tests/modules/pap/pbkfd2_salt_big.attrs
new file mode 100644
index 0000000..ccb593e
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_salt_big.attrs
@@ -0,0 +1,10 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_salt_big'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkfd2_salt_big.unlang b/src/tests/modules/pap/pbkfd2_salt_big.unlang
new file mode 100644
index 0000000..cfc96c5
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_salt_big.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_salt_big') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AAAAAQ:E+VXOSsE8RwyYGdygQoW9QA==:pF23EcxNBhJLQ+9JRtd9wQ1Gz+k4i6YjeNZq+7DRBX8='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_salt_small.attrs b/src/tests/modules/pap/pbkfd2_salt_small.attrs
new file mode 100644
index 0000000..1c2fa20
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_salt_small.attrs
@@ -0,0 +1,10 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_salt_small'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkfd2_salt_small.unlang b/src/tests/modules/pap/pbkfd2_salt_small.unlang
new file mode 100644
index 0000000..e46982f
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_salt_small.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_salt_small') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AAAAAQ:E+VXOSsE8RwyYGdygQoW9Q=:UivlvrwHML4VtZHMJLiT/xlH7oyoyvbXQceivptq9TI='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_sha1.attrs b/src/tests/modules/pap/pbkfd2_sha1.attrs
new file mode 100644
index 0000000..ef7538f
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_sha1.attrs
@@ -0,0 +1,10 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_sha1'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkfd2_sha1.unlang b/src/tests/modules/pap/pbkfd2_sha1.unlang
new file mode 100644
index 0000000..5b79691
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_sha1.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_sha1') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA1:AAAD6A:Xw1P133xrwk=:dtQBXQRiR/No5A8Ip3JFGF/qUC0='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_sha2_224.attrs b/src/tests/modules/pap/pbkfd2_sha2_224.attrs
new file mode 100644
index 0000000..413f893
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_sha2_224.attrs
@@ -0,0 +1,10 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_sha2_224'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkfd2_sha2_224.unlang b/src/tests/modules/pap/pbkfd2_sha2_224.unlang
new file mode 100644
index 0000000..00fa626
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_sha2_224.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_sha2_224') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+224:AAAnEA:UHScBrg/ZWOyBKqQdAh7bw==:tcFp6CDrkIYdhwa60g24U4ko+mBxzAiFxlpPnA=='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_sha2_256.attrs b/src/tests/modules/pap/pbkfd2_sha2_256.attrs
new file mode 100644
index 0000000..3066682
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_sha2_256.attrs
@@ -0,0 +1,10 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_sha2_256'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkfd2_sha2_256.unlang b/src/tests/modules/pap/pbkfd2_sha2_256.unlang
new file mode 100644
index 0000000..5c4efce
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_sha2_256.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_sha2_256') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+256:AAAnEA:a/8HbYW2HWsMthN27JI+Ew==:3nPlXYOlOuDCFOfethUomHxTXkG9JCivOdvh6FDNdGw='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_sha2_384.attrs b/src/tests/modules/pap/pbkfd2_sha2_384.attrs
new file mode 100644
index 0000000..9e43450
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_sha2_384.attrs
@@ -0,0 +1,10 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_sha2_384'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkfd2_sha2_384.unlang b/src/tests/modules/pap/pbkfd2_sha2_384.unlang
new file mode 100644
index 0000000..034bb83
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_sha2_384.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_sha2_384') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+384:AAAnEA:pyHRsYLfNZdjszRcu6eHrA==:ktGfNmZ6PyD8FNEgPzFK1fypKERZ13pgvFl+PQdyKouaMXsXIiWPuTMXHqDUCWsx'
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/pap/pbkfd2_sha2_512.attrs b/src/tests/modules/pap/pbkfd2_sha2_512.attrs
new file mode 100644
index 0000000..b908615
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_sha2_512.attrs
@@ -0,0 +1,10 @@
+#
+# Input packet
+#
+User-Name = 'pbkdf2_sha2_512'
+User-Password = 'password'
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/pap/pbkfd2_sha2_512.unlang b/src/tests/modules/pap/pbkfd2_sha2_512.unlang
new file mode 100644
index 0000000..95c1f3e
--- /dev/null
+++ b/src/tests/modules/pap/pbkfd2_sha2_512.unlang
@@ -0,0 +1,17 @@
+if ("${feature.tls}" == no) {
+ test_pass
+ return
+}
+
+if (&User-Name == 'pbkdf2_sha2_512') {
+ update control {
+ &PBKDF2-Password := 'HMACSHA2+512:AAAnEA:TG8Mb94NEmfPLaePwi5CFA==:SYSFeRf9jr4Uo5DB4NvNUEuc1gmEiLjTac5J4WgyKa7mO58KHKWop9xWmcFeuLtUN/iexLTNSgcubOugAyZcog=='
+ }
+ pap.authorize
+ pap.authenticate
+ if (!ok) {
+ test_fail
+ } else {
+ test_pass
+ }
+}
diff --git a/src/tests/modules/preprocess/all.mk b/src/tests/modules/preprocess/all.mk
new file mode 100644
index 0000000..5cfad60
--- /dev/null
+++ b/src/tests/modules/preprocess/all.mk
@@ -0,0 +1,3 @@
+#
+# Test the "preprocess" module
+#
diff --git a/src/tests/modules/preprocess/hints b/src/tests/modules/preprocess/hints
new file mode 100644
index 0000000..14ceafc
--- /dev/null
+++ b/src/tests/modules/preprocess/hints
@@ -0,0 +1,2 @@
+DEFAULT
+ Calling-Station-Id := "%{User-Name}@%{NAS-IP-Address}"
diff --git a/src/tests/modules/preprocess/huntgroups b/src/tests/modules/preprocess/huntgroups
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/tests/modules/preprocess/huntgroups
diff --git a/src/tests/modules/preprocess/module.conf b/src/tests/modules/preprocess/module.conf
new file mode 100644
index 0000000..7c51fa6
--- /dev/null
+++ b/src/tests/modules/preprocess/module.conf
@@ -0,0 +1,4 @@
+preprocess {
+ hints = $ENV{MODULE_TEST_DIR}/hints
+ huntgroups = $ENV{MODULE_TEST_DIR}/huntgroups
+}
diff --git a/src/tests/modules/preprocess/xlat.attrs b/src/tests/modules/preprocess/xlat.attrs
new file mode 100644
index 0000000..e7170d1
--- /dev/null
+++ b/src/tests/modules/preprocess/xlat.attrs
@@ -0,0 +1,12 @@
+#
+# Input packet
+#
+User-Name = "bob"
+User-Password = "bob"
+NAS-IP-Address = 127.0.0.1
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Filter-Id == 'success'
diff --git a/src/tests/modules/preprocess/xlat.unlang b/src/tests/modules/preprocess/xlat.unlang
new file mode 100644
index 0000000..da53982
--- /dev/null
+++ b/src/tests/modules/preprocess/xlat.unlang
@@ -0,0 +1,14 @@
+#
+# Run the preprocess module
+#
+preprocess
+
+if (Calling-Station-Id == "bob@127.0.0.1") {
+ update reply {
+ Filter-Id := "success"
+ }
+}
+
+update control {
+ Cleartext-Password := "%{User-Name}"
+}
diff --git a/src/tests/modules/radiusd.conf b/src/tests/modules/radiusd.conf
new file mode 100644
index 0000000..220f341
--- /dev/null
+++ b/src/tests/modules/radiusd.conf
@@ -0,0 +1,103 @@
+#
+# Minimal radiusd.conf for testing modules
+#
+
+raddb = raddb
+
+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 $ENV{MODULE_TEST_DIR}/module.conf
+}
+
+server default {
+ authorize {
+ #
+ # Include the test file specified by the
+ # KEYWORD environment variable.
+ #
+ $INCLUDE $ENV{MODULE_TEST_UNLANG}
+
+ pap
+ }
+
+ authenticate {
+ pap
+ }
+}
+
+policy {
+ test_pass {
+ update control {
+ Tmp-String-8 := "%{expr:%{%{control:Tmp-String-8}:-0} + 1}"
+ Auth-Type := Accept
+ }
+ }
+
+ test_fail {
+ update reply {
+ Reply-Message := "fail %{%{control:Tmp-String-8}:-0}"
+ }
+ reject
+ }
+
+ #
+ # 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 session state list in debugging (-X) mode
+ #
+ debug_session_state {
+ if("%{debug_attr:session-state:}" == '') {
+ noop
+ }
+ }
+
+ #
+ # Outputs the contents of the main lists in debugging (-X) mode
+ #
+ debug_all {
+ debug_control
+ debug_request
+ debug_reply
+ debug_session_state
+ }
+}
diff --git a/src/tests/modules/sql/.gitignore b/src/tests/modules/sql/.gitignore
new file mode 100644
index 0000000..405551a
--- /dev/null
+++ b/src/tests/modules/sql/.gitignore
@@ -0,0 +1 @@
+rlm_sql_sqlite.db
diff --git a/src/tests/modules/sql/acct_0_start.attrs b/src/tests/modules/sql/acct_0_start.attrs
new file mode 100644
index 0000000..01257ce
--- /dev/null
+++ b/src/tests/modules/sql/acct_0_start.attrs
@@ -0,0 +1,37 @@
+#
+# Input packet
+#
+User-Name = 'user0@example.org'
+NAS-Port = 17826193
+NAS-IP-Address = 192.0.2.10
+Framed-IP-Address = 198.51.100.59
+NAS-Identifier = 'nas.example.org'
+Acct-Status-Type = Start
+Acct-Delay-Time = 1
+Acct-Input-Octets = 0
+Acct-Output-Octets = 0
+Acct-Session-Id = '00000000'
+Acct-Unique-Session-Id = '00000000'
+Acct-Authentic = RADIUS
+Acct-Session-Time = 0
+Acct-Input-Packets = 0
+Acct-Output-Packets = 0
+Acct-Input-Gigawords = 0
+Acct-Output-Gigawords = 0
+Event-Timestamp = 'Feb 1 2015 08:28:58 WIB'
+NAS-Port-Type = Ethernet
+NAS-Port-Id = 'port 001'
+Service-Type = Framed-User
+Framed-Protocol = PPP
+Acct-Link-Count = 0
+Idle-Timeout = 0
+Session-Timeout = 604800
+Access-Loop-Encapsulation = 0x000000
+Proxy-State = 0x323531
+
+#
+# Expected answer
+#
+# There's not an Accounting-Failed packet type in RADIUS...
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/sql/acct_0_start.unlang b/src/tests/modules/sql/acct_0_start.unlang
new file mode 100644
index 0000000..64921b1
--- /dev/null
+++ b/src/tests/modules/sql/acct_0_start.unlang
@@ -0,0 +1,40 @@
+#
+# Clear out old data
+#
+update {
+ Tmp-String-0 := "%{sql:DELETE FROM radacct WHERE AcctSessionId = '00000000'}"
+}
+if (!&Tmp-String-0) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+sql.accounting
+if (ok) {
+ test_pass
+}
+else {
+ test_fail
+}
+
+update {
+ Tmp-Integer-0 := "%{sql:SELECT count(*) FROM radacct WHERE AcctSessionId = '00000000'}"
+}
+if (!&Tmp-Integer-0 || (&Tmp-Integer-0 != 1)) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+update {
+ Tmp-Integer-0 := "%{sql:SELECT acctsessiontime FROM radacct WHERE AcctSessionId = '00000000'}"
+}
+if (!&Tmp-Integer-0 || (&Tmp-Integer-0 != 0)) {
+ test_fail
+}
+else {
+ test_pass
+}
diff --git a/src/tests/modules/sql/acct_1_update.attrs b/src/tests/modules/sql/acct_1_update.attrs
new file mode 100644
index 0000000..28db958
--- /dev/null
+++ b/src/tests/modules/sql/acct_1_update.attrs
@@ -0,0 +1,37 @@
+#
+# Input packet
+#
+User-Name = 'user1@example.org'
+NAS-Port = 17826193
+NAS-IP-Address = 192.0.2.10
+Framed-IP-Address = 198.51.100.59
+NAS-Identifier = 'nas.example.org'
+Acct-Status-Type = Interim-Update
+Acct-Delay-Time = 1
+Acct-Input-Octets = 10
+Acct-Output-Octets = 10
+Acct-Session-Id = '00000001'
+Acct-Unique-Session-Id = '00000001'
+Acct-Authentic = RADIUS
+Acct-Session-Time = 30
+Acct-Input-Packets = 10
+Acct-Output-Packets = 10
+Acct-Input-Gigawords = 1
+Acct-Output-Gigawords = 1
+Event-Timestamp = 'Feb 1 2015 08:28:28 WIB'
+NAS-Port-Type = Ethernet
+NAS-Port-Id = 'port 001'
+Service-Type = Framed-User
+Framed-Protocol = PPP
+Acct-Link-Count = 0
+Idle-Timeout = 0
+Session-Timeout = 604800
+Access-Loop-Encapsulation = 0x000000
+Proxy-State = 0x323531
+
+#
+# Expected answer
+#
+# There's not an Accounting-Failed packet type in RADIUS...
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/sql/acct_1_update.unlang b/src/tests/modules/sql/acct_1_update.unlang
new file mode 100644
index 0000000..e566a4a
--- /dev/null
+++ b/src/tests/modules/sql/acct_1_update.unlang
@@ -0,0 +1,30 @@
+#
+# PRE: acct_0_start
+#
+sql.accounting
+if (ok) {
+ test_pass
+}
+else {
+ test_fail
+}
+
+update {
+ Tmp-Integer-0 := "%{sql:SELECT count(*) FROM radacct WHERE AcctSessionId = '00000001'}"
+}
+if (!&Tmp-Integer-0 || (&Tmp-Integer-0 != 1)) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+update {
+ Tmp-Integer-0 := "%{sql:SELECT acctsessiontime FROM radacct WHERE AcctSessionId = '00000001'}"
+}
+if (!&Tmp-Integer-0 || (&Tmp-Integer-0 != 30)) {
+ test_fail
+}
+else {
+ test_pass
+}
diff --git a/src/tests/modules/sql/acct_2_stop.attrs b/src/tests/modules/sql/acct_2_stop.attrs
new file mode 100644
index 0000000..e932f84
--- /dev/null
+++ b/src/tests/modules/sql/acct_2_stop.attrs
@@ -0,0 +1,38 @@
+#
+# Input packet
+#
+User-Name = 'user2@example.org'
+NAS-Port = 17826193
+NAS-IP-Address = 192.0.2.10
+Framed-IP-Address = 198.51.100.59
+NAS-Identifier = 'nas.example.org'
+Acct-Status-Type = Stop
+Acct-Terminate-Cause = User-Request
+Acct-Delay-Time = 1
+Acct-Input-Octets = 15
+Acct-Output-Octets = 15
+Acct-Session-Id = '00000002'
+Acct-Unique-Session-Id = '00000002'
+Acct-Authentic = RADIUS
+Acct-Session-Time = 120
+Acct-Input-Packets = 15
+Acct-Output-Packets = 15
+Acct-Input-Gigawords = 1
+Acct-Output-Gigawords = 1
+Event-Timestamp = 'Feb 1 2015 08:28:58 WIB'
+NAS-Port-Type = Ethernet
+NAS-Port-Id = 'port 001'
+Service-Type = Framed-User
+Framed-Protocol = PPP
+Acct-Link-Count = 0
+Idle-Timeout = 0
+Session-Timeout = 604800
+Access-Loop-Encapsulation = 0x000000
+Proxy-State = 0x323531
+
+#
+# Expected answer
+#
+# There's not an Accounting-Failed packet type in RADIUS...
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/sql/acct_2_stop.unlang b/src/tests/modules/sql/acct_2_stop.unlang
new file mode 100644
index 0000000..3386c71
--- /dev/null
+++ b/src/tests/modules/sql/acct_2_stop.unlang
@@ -0,0 +1,40 @@
+#
+# PRE: acct_1_update
+#
+sql.accounting
+if (ok) {
+ test_pass
+}
+else {
+ test_fail
+}
+
+update {
+ Tmp-Integer-0 := "%{sql:SELECT count(*) FROM radacct WHERE AcctSessionId = '00000002'}"
+}
+if (!&Tmp-Integer-0 || (&Tmp-Integer-0 != 1)) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+update {
+ Tmp-Integer-0 := "%{sql:SELECT acctsessiontime FROM radacct WHERE AcctSessionId = '00000002'}"
+}
+if (!&Tmp-Integer-0 || (&Tmp-Integer-0 != 120)) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+update {
+ Tmp-String-0 := "%{sql:SELECT AcctTerminateCause FROM radacct WHERE AcctSessionId = '00000002'}"
+}
+if (!&Tmp-String-0 || (&Tmp-String-0 != 'User-Request')) {
+ test_fail
+}
+else {
+ test_pass
+}
diff --git a/src/tests/modules/sql/acct_start_conflict.attrs b/src/tests/modules/sql/acct_start_conflict.attrs
new file mode 100644
index 0000000..2bcade3
--- /dev/null
+++ b/src/tests/modules/sql/acct_start_conflict.attrs
@@ -0,0 +1,37 @@
+#
+# Input packet
+#
+User-Name = 'user3@example.org'
+NAS-Port = 17826193
+NAS-IP-Address = 192.0.2.10
+Framed-IP-Address = 198.51.100.59
+NAS-Identifier = 'nas.example.org'
+Acct-Status-Type = Start
+Acct-Delay-Time = 1
+Acct-Input-Octets = 0
+Acct-Output-Octets = 0
+Acct-Session-Id = '00000003'
+Acct-Unique-Session-Id = '00000003'
+Acct-Authentic = RADIUS
+Acct-Session-Time = 0
+Acct-Input-Packets = 0
+Acct-Output-Packets = 0
+Acct-Input-Gigawords = 0
+Acct-Output-Gigawords = 0
+Event-Timestamp = 'Feb 1 2015 08:28:58 WIB'
+NAS-Port-Type = Ethernet
+NAS-Port-Id = 'port 001'
+Service-Type = Framed-User
+Framed-Protocol = PPP
+Acct-Link-Count = 0
+Idle-Timeout = 0
+Session-Timeout = 604800
+Access-Loop-Encapsulation = 0x000000
+Proxy-State = 0x323531
+
+#
+# Expected answer
+#
+# There's not an Accounting-Failed packet type in RADIUS...
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/sql/acct_start_conflict.unlang b/src/tests/modules/sql/acct_start_conflict.unlang
new file mode 100644
index 0000000..65e69e0
--- /dev/null
+++ b/src/tests/modules/sql/acct_start_conflict.unlang
@@ -0,0 +1,76 @@
+#
+# Check that conflicting unique IDs triggers failover to alternative query
+#
+
+#
+# Clear out old data
+#
+update {
+ Tmp-String-0 := "%{sql:DELETE FROM radacct WHERE AcctSessionId = '00000003'}"
+}
+if (!&Tmp-String-0) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+#
+# Insert the Accounting-Request start
+#
+sql.accounting
+if (ok) {
+ test_pass
+}
+else {
+ test_fail
+}
+
+#
+# Check the database has at least one row
+#
+update {
+ Tmp-Integer-0 := "%{sql:SELECT count(*) FROM radacct WHERE AcctSessionId = '00000003'}"
+}
+if (!&Tmp-Integer-0 || (&Tmp-Integer-0 != 1)) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+#
+# Check acctsessiontime matches the value in the request
+#
+update {
+ Tmp-Integer-0 := "%{sql:SELECT acctsessiontime FROM radacct WHERE AcctSessionId = '00000003'}"
+}
+if (!&Tmp-Integer-0 || (&Tmp-Integer-0 != 0)) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+#
+# Change acctsessiontime and verify it's updated
+#
+update request {
+ Connect-Info = 'updated'
+}
+sql.accounting
+if (ok) {
+ test_pass
+}
+else {
+ test_fail
+}
+update {
+ Tmp-String-0 := "%{sql:SELECT connectinfo_start FROM radacct WHERE AcctSessionId = '00000003'}"
+}
+if (!&Tmp-Integer-0 || (&Tmp-String-0 != 'updated')) {
+ test_fail
+}
+else {
+ test_pass
+}
diff --git a/src/tests/modules/sql/acct_update_no_start.attrs b/src/tests/modules/sql/acct_update_no_start.attrs
new file mode 100644
index 0000000..6f3049e
--- /dev/null
+++ b/src/tests/modules/sql/acct_update_no_start.attrs
@@ -0,0 +1,37 @@
+#
+# Input packet
+#
+User-Name = 'user4@example.org'
+NAS-Port = 17826193
+NAS-IP-Address = 192.0.2.10
+Framed-IP-Address = 198.51.100.59
+NAS-Identifier = 'nas.example.org'
+Acct-Status-Type = Interim-Update
+Acct-Delay-Time = 1
+Acct-Input-Octets = 10
+Acct-Output-Octets = 10
+Acct-Session-Id = '00000004'
+Acct-Unique-Session-Id = '00000004'
+Acct-Authentic = RADIUS
+Acct-Session-Time = 30
+Acct-Input-Packets = 10
+Acct-Output-Packets = 10
+Acct-Input-Gigawords = 1
+Acct-Output-Gigawords = 1
+Event-Timestamp = 'Feb 1 2015 08:28:28 WIB'
+NAS-Port-Type = Ethernet
+NAS-Port-Id = 'port 001'
+Service-Type = Framed-User
+Framed-Protocol = PPP
+Acct-Link-Count = 0
+Idle-Timeout = 0
+Session-Timeout = 604800
+Access-Loop-Encapsulation = 0x000000
+Proxy-State = 0x323531
+
+#
+# Expected answer
+#
+# There's not an Accounting-Failed packet type in RADIUS...
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/sql/acct_update_no_start.unlang b/src/tests/modules/sql/acct_update_no_start.unlang
new file mode 100644
index 0000000..3875b2d
--- /dev/null
+++ b/src/tests/modules/sql/acct_update_no_start.unlang
@@ -0,0 +1,40 @@
+#
+# Clear out old data
+#
+update {
+ Tmp-String-0 := "%{sql:DELETE FROM radacct WHERE AcctSessionId = '00000004'}"
+}
+if (!&Tmp-String-0) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+sql.accounting
+if (ok) {
+ test_pass
+}
+else {
+ test_fail
+}
+
+update {
+ Tmp-Integer-0 := "%{sql:SELECT count(*) FROM radacct WHERE AcctSessionId = '00000004'}"
+}
+if (!&Tmp-Integer-0 || (&Tmp-Integer-0 != 1)) {
+ test_fail
+}
+else {
+ test_pass
+}
+
+update {
+ Tmp-Integer-0 := "%{sql:SELECT acctsessiontime FROM radacct WHERE AcctSessionId = '00000004'}"
+}
+if (!&Tmp-Integer-0 || (&Tmp-Integer-0 != 30)) {
+ test_fail
+}
+else {
+ test_pass
+}
diff --git a/src/tests/modules/sql/auth.attrs b/src/tests/modules/sql/auth.attrs
new file mode 100644
index 0000000..e7d1498
--- /dev/null
+++ b/src/tests/modules/sql/auth.attrs
@@ -0,0 +1,12 @@
+#
+# Input packet
+#
+User-Name = "user_auth"
+User-Password = "password"
+NAS-IP-Address = "1.2.3.4"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
+Idle-Timeout == 3600
diff --git a/src/tests/modules/sql/auth.unlang b/src/tests/modules/sql/auth.unlang
new file mode 100644
index 0000000..0d76538
--- /dev/null
+++ b/src/tests/modules/sql/auth.unlang
@@ -0,0 +1,39 @@
+#
+# Clear out old data
+#
+update {
+ Tmp-String-0 := "%{sql:DELETE FROM radcheck WHERE username = 'user_auth'}"
+}
+if (!&Tmp-String-0) {
+ test_fail
+}
+
+update {
+ Tmp-String-0 := "%{sql:INSERT INTO radcheck (username, attribute, op, value) VALUES ('user_auth', 'NAS-IP-Address', '==', '1.2.3.4')}"
+}
+if (!&Tmp-String-0) {
+ test_fail
+}
+
+update {
+ Tmp-String-0 := "%{sql:INSERT INTO radcheck (username, attribute, op, value) VALUES ('user_auth', 'Cleartext-Password', ':=', 'password')}"
+}
+if (!&Tmp-String-0) {
+ test_fail
+}
+
+update {
+ Tmp-String-0 := "%{sql:DELETE FROM radreply WHERE username = 'user_auth'}"
+}
+if (!&Tmp-String-0) {
+ test_fail
+}
+
+update {
+ Tmp-String-0 := "%{sql:INSERT INTO radreply (username, attribute, op, value) VALUES ('user_auth', 'Idle-Timeout', ':=', '3600')}"
+}
+if (!&Tmp-String-0) {
+ test_fail
+}
+
+sql
diff --git a/src/tests/modules/sql/reject.attrs b/src/tests/modules/sql/reject.attrs
new file mode 100644
index 0000000..cb0b9a2
--- /dev/null
+++ b/src/tests/modules/sql/reject.attrs
@@ -0,0 +1,12 @@
+#
+# Input packet
+#
+User-Name = "user_reject"
+User-Password = "password"
+NAS-IP-Address = "1.2.3.4"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Reject
+Reply-Message == "Authentication failed"
diff --git a/src/tests/modules/sql/reject.unlang b/src/tests/modules/sql/reject.unlang
new file mode 100644
index 0000000..b4afb09
--- /dev/null
+++ b/src/tests/modules/sql/reject.unlang
@@ -0,0 +1,39 @@
+#
+# Clear out old data
+#
+update {
+ Tmp-String-0 := "%{sql:DELETE FROM radcheck WHERE username = 'user_reject'}"
+}
+if (!&Tmp-String-0) {
+ test_fail
+}
+
+update {
+ Tmp-String-0 := "%{sql:INSERT INTO radcheck (username, attribute, op, value) VALUES ('user_reject', 'NAS-IP-Address', '==', '1.2.3.4')}"
+}
+if (!&Tmp-String-0) {
+ test_fail
+}
+
+update {
+ Tmp-String-0 := "%{sql:INSERT INTO radcheck (username, attribute, op, value) VALUES ('user_reject', 'Cleartext-Password', ':=', 'wrong-password')}"
+}
+if (!&Tmp-String-0) {
+ test_fail
+}
+
+update {
+ Tmp-String-0 := "%{sql:DELETE FROM radreply WHERE username = 'user_reject'}"
+}
+if (!&Tmp-String-0) {
+ test_fail
+}
+
+update {
+ Tmp-String-0 := "%{sql:INSERT INTO radreply (username, attribute, op, value) VALUES ('user_reject', 'Reply-Message', ':=', 'Authentication failed')}"
+}
+if (!&Tmp-String-0) {
+ test_fail
+}
+
+sql
diff --git a/src/tests/modules/sql_mysql/.gitignore b/src/tests/modules/sql_mysql/.gitignore
new file mode 100644
index 0000000..405551a
--- /dev/null
+++ b/src/tests/modules/sql_mysql/.gitignore
@@ -0,0 +1 @@
+rlm_sql_sqlite.db
diff --git a/src/tests/modules/sql_mysql/acct_0_start.attrs b/src/tests/modules/sql_mysql/acct_0_start.attrs
new file mode 120000
index 0000000..24e17ae
--- /dev/null
+++ b/src/tests/modules/sql_mysql/acct_0_start.attrs
@@ -0,0 +1 @@
+../sql/acct_0_start.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_mysql/acct_0_start.unlang b/src/tests/modules/sql_mysql/acct_0_start.unlang
new file mode 120000
index 0000000..3fe3e99
--- /dev/null
+++ b/src/tests/modules/sql_mysql/acct_0_start.unlang
@@ -0,0 +1 @@
+../sql/acct_0_start.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_mysql/acct_1_update.attrs b/src/tests/modules/sql_mysql/acct_1_update.attrs
new file mode 120000
index 0000000..1ab772d
--- /dev/null
+++ b/src/tests/modules/sql_mysql/acct_1_update.attrs
@@ -0,0 +1 @@
+../sql/acct_1_update.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_mysql/acct_1_update.unlang b/src/tests/modules/sql_mysql/acct_1_update.unlang
new file mode 120000
index 0000000..b69ff9b
--- /dev/null
+++ b/src/tests/modules/sql_mysql/acct_1_update.unlang
@@ -0,0 +1 @@
+../sql/acct_1_update.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_mysql/acct_2_stop.attrs b/src/tests/modules/sql_mysql/acct_2_stop.attrs
new file mode 120000
index 0000000..ea73931
--- /dev/null
+++ b/src/tests/modules/sql_mysql/acct_2_stop.attrs
@@ -0,0 +1 @@
+../sql/acct_2_stop.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_mysql/acct_2_stop.unlang b/src/tests/modules/sql_mysql/acct_2_stop.unlang
new file mode 120000
index 0000000..ea0be56
--- /dev/null
+++ b/src/tests/modules/sql_mysql/acct_2_stop.unlang
@@ -0,0 +1 @@
+../sql/acct_2_stop.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_mysql/acct_start_conflict.attrs b/src/tests/modules/sql_mysql/acct_start_conflict.attrs
new file mode 120000
index 0000000..117a505
--- /dev/null
+++ b/src/tests/modules/sql_mysql/acct_start_conflict.attrs
@@ -0,0 +1 @@
+../sql/acct_start_conflict.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_mysql/acct_start_conflict.unlang b/src/tests/modules/sql_mysql/acct_start_conflict.unlang
new file mode 120000
index 0000000..da35798
--- /dev/null
+++ b/src/tests/modules/sql_mysql/acct_start_conflict.unlang
@@ -0,0 +1 @@
+../sql/acct_start_conflict.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_mysql/acct_update_no_start.attrs b/src/tests/modules/sql_mysql/acct_update_no_start.attrs
new file mode 120000
index 0000000..328867f
--- /dev/null
+++ b/src/tests/modules/sql_mysql/acct_update_no_start.attrs
@@ -0,0 +1 @@
+../sql/acct_update_no_start.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_mysql/acct_update_no_start.unlang b/src/tests/modules/sql_mysql/acct_update_no_start.unlang
new file mode 120000
index 0000000..6837977
--- /dev/null
+++ b/src/tests/modules/sql_mysql/acct_update_no_start.unlang
@@ -0,0 +1 @@
+../sql/acct_update_no_start.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_mysql/all.mk b/src/tests/modules/sql_mysql/all.mk
new file mode 100644
index 0000000..337528b
--- /dev/null
+++ b/src/tests/modules/sql_mysql/all.mk
@@ -0,0 +1,6 @@
+#
+# Test the mysql module
+#
+
+# Don't test sql_mysql if TEST_SERVER ENV is not set
+sql_mysql_require_test_server := 1
diff --git a/src/tests/modules/sql_mysql/auth.attrs b/src/tests/modules/sql_mysql/auth.attrs
new file mode 120000
index 0000000..6b30b6b
--- /dev/null
+++ b/src/tests/modules/sql_mysql/auth.attrs
@@ -0,0 +1 @@
+../sql/auth.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_mysql/auth.unlang b/src/tests/modules/sql_mysql/auth.unlang
new file mode 120000
index 0000000..3ccd80e
--- /dev/null
+++ b/src/tests/modules/sql_mysql/auth.unlang
@@ -0,0 +1 @@
+../sql/auth.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_mysql/module.conf b/src/tests/modules/sql_mysql/module.conf
new file mode 100644
index 0000000..e3aa02d
--- /dev/null
+++ b/src/tests/modules/sql_mysql/module.conf
@@ -0,0 +1,53 @@
+sql {
+ driver = "rlm_sql_mysql"
+ dialect = "mysql"
+
+ # Connection info:
+ #
+ server = $ENV{SQL_MYSQL_TEST_SERVER}
+ port = 3306
+ login = "radius"
+ password = "radpass"
+
+ # Database table configuration for everything except Oracle
+ radius_db = "radius"
+ radius_db = "radius"
+
+ acct_table1 = "radacct"
+ acct_table2 = "radacct"
+ postauth_table = "radpostauth"
+ authcheck_table = "radcheck"
+ groupcheck_table = "radgroupcheck"
+ authreply_table = "radreply"
+ groupreply_table = "radgroupreply"
+ usergroup_table = "radusergroup"
+ read_groups = yes
+ read_profiles = yes
+
+ # Remove stale session if checkrad does not see a double login
+ delete_stale_sessions = yes
+
+ pool {
+ start = 1
+ min = 0
+ max = 1
+ spare = 3
+ uses = 2
+ lifetime = 1
+ idle_timeout = 60
+ retry_delay = 1
+ }
+
+ # Set to 'yes' to read radius clients from the database ('nas' table)
+ # Clients will ONLY be read on server startup.
+# read_clients = yes
+
+ # Table to keep radius client info
+ client_table = "nas"
+
+ # The group attribute specific to this instance of rlm_sql
+ group_attribute = "SQL-Group"
+
+ # Read database-specific queries
+ $INCLUDE ${modconfdir}/${.:name}/main/${dialect}/queries.conf
+}
diff --git a/src/tests/modules/sql_mysql/reject.attrs b/src/tests/modules/sql_mysql/reject.attrs
new file mode 120000
index 0000000..71a187f
--- /dev/null
+++ b/src/tests/modules/sql_mysql/reject.attrs
@@ -0,0 +1 @@
+../sql/reject.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_mysql/reject.unlang b/src/tests/modules/sql_mysql/reject.unlang
new file mode 120000
index 0000000..379839f
--- /dev/null
+++ b/src/tests/modules/sql_mysql/reject.unlang
@@ -0,0 +1 @@
+../sql/reject.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_postgresql/.gitignore b/src/tests/modules/sql_postgresql/.gitignore
new file mode 100644
index 0000000..405551a
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/.gitignore
@@ -0,0 +1 @@
+rlm_sql_sqlite.db
diff --git a/src/tests/modules/sql_postgresql/acct_0_start.attrs b/src/tests/modules/sql_postgresql/acct_0_start.attrs
new file mode 120000
index 0000000..24e17ae
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/acct_0_start.attrs
@@ -0,0 +1 @@
+../sql/acct_0_start.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_postgresql/acct_0_start.unlang b/src/tests/modules/sql_postgresql/acct_0_start.unlang
new file mode 120000
index 0000000..3fe3e99
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/acct_0_start.unlang
@@ -0,0 +1 @@
+../sql/acct_0_start.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_postgresql/acct_1_update.attrs b/src/tests/modules/sql_postgresql/acct_1_update.attrs
new file mode 120000
index 0000000..1ab772d
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/acct_1_update.attrs
@@ -0,0 +1 @@
+../sql/acct_1_update.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_postgresql/acct_1_update.unlang b/src/tests/modules/sql_postgresql/acct_1_update.unlang
new file mode 120000
index 0000000..b69ff9b
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/acct_1_update.unlang
@@ -0,0 +1 @@
+../sql/acct_1_update.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_postgresql/acct_2_stop.attrs b/src/tests/modules/sql_postgresql/acct_2_stop.attrs
new file mode 120000
index 0000000..ea73931
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/acct_2_stop.attrs
@@ -0,0 +1 @@
+../sql/acct_2_stop.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_postgresql/acct_2_stop.unlang b/src/tests/modules/sql_postgresql/acct_2_stop.unlang
new file mode 120000
index 0000000..ea0be56
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/acct_2_stop.unlang
@@ -0,0 +1 @@
+../sql/acct_2_stop.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_postgresql/acct_start_conflict.attrs b/src/tests/modules/sql_postgresql/acct_start_conflict.attrs
new file mode 120000
index 0000000..117a505
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/acct_start_conflict.attrs
@@ -0,0 +1 @@
+../sql/acct_start_conflict.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_postgresql/acct_start_conflict.unlang b/src/tests/modules/sql_postgresql/acct_start_conflict.unlang
new file mode 120000
index 0000000..da35798
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/acct_start_conflict.unlang
@@ -0,0 +1 @@
+../sql/acct_start_conflict.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_postgresql/acct_update_no_start.attrs b/src/tests/modules/sql_postgresql/acct_update_no_start.attrs
new file mode 120000
index 0000000..328867f
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/acct_update_no_start.attrs
@@ -0,0 +1 @@
+../sql/acct_update_no_start.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_postgresql/acct_update_no_start.unlang b/src/tests/modules/sql_postgresql/acct_update_no_start.unlang
new file mode 120000
index 0000000..6837977
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/acct_update_no_start.unlang
@@ -0,0 +1 @@
+../sql/acct_update_no_start.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_postgresql/all.mk b/src/tests/modules/sql_postgresql/all.mk
new file mode 100644
index 0000000..efd20d9
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/all.mk
@@ -0,0 +1,6 @@
+#
+# Test the postgresql module
+#
+
+# Don't test sql_postgresql if TEST_SERVER ENV is not set
+sql_postgresql_require_test_server := 1
diff --git a/src/tests/modules/sql_postgresql/auth.attrs b/src/tests/modules/sql_postgresql/auth.attrs
new file mode 120000
index 0000000..6b30b6b
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/auth.attrs
@@ -0,0 +1 @@
+../sql/auth.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_postgresql/auth.unlang b/src/tests/modules/sql_postgresql/auth.unlang
new file mode 120000
index 0000000..3ccd80e
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/auth.unlang
@@ -0,0 +1 @@
+../sql/auth.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_postgresql/module.conf b/src/tests/modules/sql_postgresql/module.conf
new file mode 100644
index 0000000..ee9a8a9
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/module.conf
@@ -0,0 +1,52 @@
+sql {
+ driver = "rlm_sql_postgresql"
+ dialect = "postgresql"
+
+ # Connection info:
+ #
+ server = $ENV{SQL_POSTGRESQL_TEST_SERVER}
+ port = 5432
+ login = "radius"
+ password = "radpass"
+
+ # Database table configuration for everything except Oracle
+ radius_db = "radius"
+
+ acct_table1 = "radacct"
+ acct_table2 = "radacct"
+ postauth_table = "radpostauth"
+ authcheck_table = "radcheck"
+ groupcheck_table = "radgroupcheck"
+ authreply_table = "radreply"
+ groupreply_table = "radgroupreply"
+ usergroup_table = "radusergroup"
+ read_groups = yes
+ read_profiles = yes
+
+ # Remove stale session if checkrad does not see a double login
+ delete_stale_sessions = yes
+
+ pool {
+ start = 1
+ min = 0
+ max = 1
+ spare = 3
+ uses = 2
+ lifetime = 1
+ idle_timeout = 60
+ retry_delay = 1
+ }
+
+ # Set to 'yes' to read radius clients from the database ('nas' table)
+ # Clients will ONLY be read on server startup.
+# read_clients = yes
+
+ # Table to keep radius client info
+ client_table = "nas"
+
+ # The group attribute specific to this instance of rlm_sql
+ group_attribute = "SQL-Group"
+
+ # Read database-specific queries
+ $INCLUDE ${modconfdir}/${.:name}/main/${dialect}/queries.conf
+}
diff --git a/src/tests/modules/sql_postgresql/reject.attrs b/src/tests/modules/sql_postgresql/reject.attrs
new file mode 120000
index 0000000..71a187f
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/reject.attrs
@@ -0,0 +1 @@
+../sql/reject.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_postgresql/reject.unlang b/src/tests/modules/sql_postgresql/reject.unlang
new file mode 120000
index 0000000..379839f
--- /dev/null
+++ b/src/tests/modules/sql_postgresql/reject.unlang
@@ -0,0 +1 @@
+../sql/reject.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_sqlite/.gitignore b/src/tests/modules/sql_sqlite/.gitignore
new file mode 100644
index 0000000..405551a
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/.gitignore
@@ -0,0 +1 @@
+rlm_sql_sqlite.db
diff --git a/src/tests/modules/sql_sqlite/acct_0_start.attrs b/src/tests/modules/sql_sqlite/acct_0_start.attrs
new file mode 120000
index 0000000..24e17ae
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/acct_0_start.attrs
@@ -0,0 +1 @@
+../sql/acct_0_start.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_sqlite/acct_0_start.unlang b/src/tests/modules/sql_sqlite/acct_0_start.unlang
new file mode 120000
index 0000000..3fe3e99
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/acct_0_start.unlang
@@ -0,0 +1 @@
+../sql/acct_0_start.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_sqlite/acct_1_update.attrs b/src/tests/modules/sql_sqlite/acct_1_update.attrs
new file mode 120000
index 0000000..1ab772d
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/acct_1_update.attrs
@@ -0,0 +1 @@
+../sql/acct_1_update.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_sqlite/acct_1_update.unlang b/src/tests/modules/sql_sqlite/acct_1_update.unlang
new file mode 120000
index 0000000..b69ff9b
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/acct_1_update.unlang
@@ -0,0 +1 @@
+../sql/acct_1_update.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_sqlite/acct_2_stop.attrs b/src/tests/modules/sql_sqlite/acct_2_stop.attrs
new file mode 120000
index 0000000..ea73931
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/acct_2_stop.attrs
@@ -0,0 +1 @@
+../sql/acct_2_stop.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_sqlite/acct_2_stop.unlang b/src/tests/modules/sql_sqlite/acct_2_stop.unlang
new file mode 120000
index 0000000..ea0be56
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/acct_2_stop.unlang
@@ -0,0 +1 @@
+../sql/acct_2_stop.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_sqlite/acct_start_conflict.attrs b/src/tests/modules/sql_sqlite/acct_start_conflict.attrs
new file mode 120000
index 0000000..117a505
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/acct_start_conflict.attrs
@@ -0,0 +1 @@
+../sql/acct_start_conflict.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_sqlite/acct_start_conflict.unlang b/src/tests/modules/sql_sqlite/acct_start_conflict.unlang
new file mode 120000
index 0000000..da35798
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/acct_start_conflict.unlang
@@ -0,0 +1 @@
+../sql/acct_start_conflict.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_sqlite/acct_update_no_start.attrs b/src/tests/modules/sql_sqlite/acct_update_no_start.attrs
new file mode 120000
index 0000000..328867f
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/acct_update_no_start.attrs
@@ -0,0 +1 @@
+../sql/acct_update_no_start.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_sqlite/acct_update_no_start.unlang b/src/tests/modules/sql_sqlite/acct_update_no_start.unlang
new file mode 120000
index 0000000..6837977
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/acct_update_no_start.unlang
@@ -0,0 +1 @@
+../sql/acct_update_no_start.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_sqlite/all.mk b/src/tests/modules/sql_sqlite/all.mk
new file mode 100644
index 0000000..a7907f1
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/all.mk
@@ -0,0 +1,3 @@
+#
+# Test the sqlite module
+#
diff --git a/src/tests/modules/sql_sqlite/auth.attrs b/src/tests/modules/sql_sqlite/auth.attrs
new file mode 120000
index 0000000..6b30b6b
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/auth.attrs
@@ -0,0 +1 @@
+../sql/auth.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_sqlite/auth.unlang b/src/tests/modules/sql_sqlite/auth.unlang
new file mode 120000
index 0000000..3ccd80e
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/auth.unlang
@@ -0,0 +1 @@
+../sql/auth.unlang \ No newline at end of file
diff --git a/src/tests/modules/sql_sqlite/module.conf b/src/tests/modules/sql_sqlite/module.conf
new file mode 100644
index 0000000..1d8ac74
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/module.conf
@@ -0,0 +1,52 @@
+sql {
+ driver = "rlm_sql_sqlite"
+ dialect = "sqlite"
+ sqlite {
+ # Path to the sqlite database
+ filename = "$ENV{MODULE_TEST_DIR}/sql_sqlite/rlm_sql_sqlite.db"
+
+ # If the file above does not exist and bootstrap is set
+ # a new database file will be created, and the SQL statements
+ # contained within the file will be executed.
+ bootstrap = "${modconfdir}/${..:name}/main/${..dialect}/schema.sql"
+ }
+ radius_db = "radius"
+
+ acct_table1 = "radacct"
+ acct_table2 = "radacct"
+ postauth_table = "radpostauth"
+ authcheck_table = "radcheck"
+ groupcheck_table = "radgroupcheck"
+ authreply_table = "radreply"
+ groupreply_table = "radgroupreply"
+ usergroup_table = "radusergroup"
+ read_groups = yes
+ read_profiles = yes
+
+ # Remove stale session if checkrad does not see a double login
+ delete_stale_sessions = yes
+
+ pool {
+ start = 1
+ min = 0
+ max = 1
+ spare = 3
+ uses = 2
+ lifetime = 1
+ idle_timeout = 60
+ retry_delay = 1
+ }
+
+ # Set to 'yes' to read radius clients from the database ('nas' table)
+ # Clients will ONLY be read on server startup.
+# read_clients = yes
+
+ # Table to keep radius client info
+ client_table = "nas"
+
+ # The group attribute specific to this instance of rlm_sql
+ group_attribute = "SQL-Group"
+
+ # Read database-specific queries
+ $INCLUDE ${modconfdir}/${.:name}/main/${dialect}/queries.conf
+}
diff --git a/src/tests/modules/sql_sqlite/reject.attrs b/src/tests/modules/sql_sqlite/reject.attrs
new file mode 120000
index 0000000..71a187f
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/reject.attrs
@@ -0,0 +1 @@
+../sql/reject.attrs \ No newline at end of file
diff --git a/src/tests/modules/sql_sqlite/reject.unlang b/src/tests/modules/sql_sqlite/reject.unlang
new file mode 120000
index 0000000..379839f
--- /dev/null
+++ b/src/tests/modules/sql_sqlite/reject.unlang
@@ -0,0 +1 @@
+../sql/reject.unlang \ No newline at end of file
diff --git a/src/tests/modules/test.mk b/src/tests/modules/test.mk
new file mode 100644
index 0000000..b217ff7
--- /dev/null
+++ b/src/tests/modules/test.mk
@@ -0,0 +1,165 @@
+#
+# Add the module tests to the overall dependencies
+#
+
+TESTS.MODULES_FILES :=
+
+# If module requires test server, make sure TEST_SERVER of <MODULE>_TEST_SERVER variables are defined
+# If TEST_SERVER is defined, define <MODULE>_TEST_SERVER for all modules that have CHECK_MODULE_TEST_CAN_BE_RUN
+define CHECK_MODULE_TEST_CAN_BE_RUN
+ ifndef ${1}_require_test_server
+ tests.modules: ${1}.test
+ else
+ ifdef TEST_SERVER
+ tests.modules: ${1}.test
+ export $(shell echo ${1} | tr a-z A-Z)_TEST_SERVER := $(TEST_SERVER)
+ endif
+ ifdef $(shell echo ${1} | tr a-z A-Z)_TEST_SERVER
+ tests.modules: ${1}.test
+ endif
+ endif
+endef
+$(foreach x,$(TEST_BUILT) $(TEST_SUBBUILT),$(eval $(call CHECK_MODULE_TEST_CAN_BE_RUN,$x)))
+
+######################################################################
+#
+# And now more makefile magic to automatically run the tests
+# for each module.
+#
+
+define DEFAULT_ATTRS
+ifeq "$(wildcard ${1}.attrs)"
+${1}.attrs
+else
+src/tests/modules/default-input.attrs
+endif
+endef
+
+#
+# Files in the output dir depend on the unit tests
+#
+# src/tests/$(MODULE_DIR)/FOO.unlang unlang for the test
+# src/tests/$(MODULE_DIR)/FOO.attrs input RADIUS and output filter
+# build/tests/$(MODULE_DIR)/FOO.out updated if the test succeeds
+# build/tests/$(MODULE_DIR)/FOO.log debug output for the test
+#
+# 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/modules/%: src/tests/modules/%.unlang $(BUILD_DIR)/tests/modules/%.attrs $(TESTBINDIR)/unittest | build.raddb
+ @mkdir -p $(dir $@)
+ @echo MODULE-TEST $(lastword $(subst /, ,$(dir $@))) $(basename $(notdir $@))
+ @if ! MODULE_TEST_DIR=$(dir $<) MODULE_TEST_UNLANG=$< $(TESTBIN)/unittest -D share -d src/tests/modules/ -i $@.attrs -f $@.attrs -xxx > $@.log 2>&1; then \
+ if ! grep ERROR $< 2>&1 > /dev/null; then \
+ cat $@.log; \
+ echo "# $@.log"; \
+ echo MODULE_TEST_DIR=$(dir $<) MODULE_TEST_UNLANG=$< $(TESTBIN)/unittest -D share -d src/tests/modules/ -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 MODULE_TEST_DIR=$(dir $<) MODULE_TEST_UNLANG=$< $(TESTBIN)/unittest -D share -d src/tests/modules/ -i $@.attrs -f $@.attrs -xx; \
+ exit 1; \
+ fi \
+ fi
+ @touch $@
+
+#
+# Sometimes we have a default input. So use that. Otherwise, use
+# the input specific to the test.
+#
+MODULE_UNLANG := $(wildcard src/tests/modules/*/*.unlang src/tests/modules/*/*/*.unlang)
+MODULE_ATTRS_REQUIRES := $(patsubst %.unlang,%.attrs,$(MODULE_UNLANG))
+MODULE_ATTRS_EXISTS := $(wildcard src/tests/modules/*/*.attrs src/tests/modules/*/*/*.attrs)
+MODULE_ATTRS_NEEDS := $(filter-out $(MODULE_ATTRS_EXISTS),$(MODULE_ATTRS_REQUIRES))
+
+MODULE_CONF_REQUIRES := $(patsubst %.unlang,%.conf,$(MODULE_UNLANG))
+MODULE_CONF_EXISTS := $(wildcard src/tests/modules/*/*.conf src/tests/modules/*/*/*.attrs)
+MODULE_CONF_NEEDS := $(filter-out $(MODULE_CONF_EXISTS),$(MODULE_CONF_REQUIRES))
+
+#
+# The complete list of tests which are to be run
+#
+MODULE_TESTS := $(patsubst src/tests/modules/%/all.mk,%,$(wildcard src/tests/modules/*/all.mk))
+
+
+#
+# Target-specific rules
+#
+define MODULE_COPY_FILE
+$(BUILD_DIR)/${1}: src/${1}
+ @mkdir -p $$(@D)
+ @cp $$< $$@
+
+endef
+
+#
+# Default rules
+#
+define MODULE_COPY_ATTR
+$(BUILD_DIR)/${1}: src/tests/modules/default-input.attrs
+ @mkdir -p $$(@D)
+ @cp $$< $$@
+endef
+
+#
+# FIXME: get this working
+#
+define MODULE_COPY_CONF
+$(BUILD_DIR)/${1}: src/tests/modules/${2}/module.conf
+ @mkdir -p $$(@D)
+ @cp $$< $$@
+endef
+
+define MODULE_FILE_TARGET
+$(BUILD_DIR)/${1}: src/${1}.unlang $(BUILD_DIR)/${1}.attrs
+
+endef
+
+define MODULE_TEST_TARGET
+${1}.test: $(patsubst %.unlang,%,$(subst src,$(BUILD_DIR),$(filter src/tests/modules/${1}/%,$(MODULE_UNLANG))))
+
+TESTS.MODULES_FILES += $(patsubst %.unlang,%,$(subst src,$(BUILD_DIR),$(filter src/tests/modules/${1}/%,$(MODULE_UNLANG))))
+endef
+
+#
+# Create the rules from the list of input files
+#
+$(foreach x,$(MODULE_ATTRS_EXISTS),$(eval $(call MODULE_COPY_FILE,$(subst src/,,$x))))
+$(foreach x,$(MODULE_CONF_EXISTS),$(eval $(call MODULE_COPY_FILE,$(subst src/,,$x))))
+
+$(foreach x,$(MODULE_ATTRS_NEEDS),$(eval $(call MODULE_COPY_ATTR,$(subst src/,,$x))))
+# FIXME: copy src/tests/modules/*/module.conf to the right place, too
+
+$(foreach x,$(MODULE_UNLANG),$(eval $(call MODULE_FILE_TARGET,$(patsubst %.unlang,%,$(subst src/,,$x)))))
+$(foreach x,$(MODULE_TESTS),$(eval $(call MODULE_TEST_TARGET,$x)))
+
+$(TESTS.MODULES_FILES): $(TESTS.AUTH_FILES)
+
+.PHONY: clean.modules.test
+clean.modules.test:
+ @rm -rf $(BUILD_DIR)/tests/modules/
+
+#
+# For each file, look for precursor test.
+# Ensure that each test depends on its precursors.
+#
+-include $(BUILD_DIR)/tests/modules/depends.mk
+
+$(BUILD_DIR)/tests/modules/depends.mk: $(MODULE_UNLANG) | $(BUILD_DIR)/tests/modules
+ @rm -f $@
+ @for x in $^; do \
+ y=`grep PRE $$x | awk '{ print $$3 }'`; \
+ if [ "$$y" != "" ]; then \
+ z=`echo $$x | sed 's,src/,$(BUILD_DIR)/', | sed 's/.unlang//'`; \
+ d=$$(basename $$(dirname $$x)); \
+ echo "$$z: $(BUILD_DIR)/tests/modules/$$d/$$y" >> $@; \
+ echo "" >> $@; \
+ fi \
+ done
diff --git a/src/tests/modules/unbound/all.mk b/src/tests/modules/unbound/all.mk
new file mode 100644
index 0000000..d64039f
--- /dev/null
+++ b/src/tests/modules/unbound/all.mk
@@ -0,0 +1,3 @@
+#
+# Test the "unbound" module
+#
diff --git a/src/tests/modules/unbound/dns.attrs b/src/tests/modules/unbound/dns.attrs
new file mode 100644
index 0000000..1cce1c5
--- /dev/null
+++ b/src/tests/modules/unbound/dns.attrs
@@ -0,0 +1,11 @@
+#
+# Input packet
+#
+Packet-Type = Access-Request
+User-Name = "bob"
+User-Password = "hello"
+
+#
+# Expected answer
+#
+Response-Packet-Type == Access-Accept
diff --git a/src/tests/modules/unbound/dns.unlang b/src/tests/modules/unbound/dns.unlang
new file mode 100644
index 0000000..d53e433
--- /dev/null
+++ b/src/tests/modules/unbound/dns.unlang
@@ -0,0 +1,53 @@
+# Use builtin "local" zone
+update request {
+ &Tmp-IP-Address-0 := "%{dns-a:localhost}"
+}
+
+if (&Tmp-IP-Address-0 != 127.0.0.1) {
+ test_fail
+}
+
+update request {
+ &Tmp-String-0 := "%{dns-aaaa:localhost}"
+}
+
+if (&Tmp-String-0 != "::1") {
+ test_fail
+}
+
+update request {
+ &Tmp-String-1 := "%{dns-ptr:1.0.0.127.in-addr.arpa}"
+}
+
+if (&Tmp-String-1 != "localhost") {
+ test_fail
+}
+
+# Use local data in module config to allow for dotted names
+update request {
+ &Tmp-IP-Address-0 := "%{dns-a:www.example.com}"
+}
+
+if (&Tmp-IP-Address-0 != 192.168.1.1) {
+ test_fail
+}
+
+update request {
+ &Tmp-String-0 := "%{dns-ptr:1.1.168.192.in-addr.arpa}"
+}
+
+if (&Tmp-String-0 != "www.example.com") {
+ test_fail
+}
+
+# Try a real, known, network response
+# Temporarily disabled while there is a bug in unbound
+#update request {
+# &Tmp-String-0 := "%{dns-ptr:8.8.8.8.in-addr.arpa}"
+#}
+
+#if (&Tmp-String-0 != "dns.google") {
+# test_fail
+#}
+
+test_pass \ No newline at end of file
diff --git a/src/tests/modules/unbound/module.conf b/src/tests/modules/unbound/module.conf
new file mode 100644
index 0000000..c0430d2
--- /dev/null
+++ b/src/tests/modules/unbound/module.conf
@@ -0,0 +1,4 @@
+unbound dns {
+ filename = "$ENV{MODULE_TEST_DIR}/unbound.conf"
+ timeout = 3000
+}
diff --git a/src/tests/modules/unbound/unbound.conf b/src/tests/modules/unbound/unbound.conf
new file mode 100644
index 0000000..33fc461
--- /dev/null
+++ b/src/tests/modules/unbound/unbound.conf
@@ -0,0 +1,6 @@
+server:
+ num-threads: 2
+ local-data: 'www.example.com. A 192.168.1.1'
+ local-data: 'example.com. MX 10 mail.example.com'
+ local-data: 'example.com. MX 20 mail2.example.com'
+ local-data-ptr: '192.168.1.1 www.example.com'