--- /dev/null
+sql {
+ driver = "sqlite"
+ dialect = "sqlite"
+ sqlite {
+ filename = "$ENV{MODULE_TEST_DIR}/sql_sqlite/$ENV{TEST}/rlm_sql_sqlite.db"
+ 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
+
+ pool {
+ start = 1
+ min = 0
+ max = 1
+ spare = 1
+ lifetime = 1
+ retry_delay = 1
+ }
+
+ group_attribute = "SQL-Group"
+
+ $INCLUDE ${modconfdir}/${.:name}/main/${dialect}/queries.conf
+}
+
+sqlcounter dailycounter {
+ sql_module_instance = sql
+ dialect = ${modules.sql.dialect}
+ counter_name = &control.Daily-Session-Time
+ check_name = &control.Max-Daily-Session
+ reply_name = &reply.Session-Timeout
+ key = "%{&Stripped-User-Name || &User-Name}"
+ reply_message_name = &Reply-Message
+ reset = daily
+
+ $INCLUDE ${modconfdir}/sql/counter/${dialect}/${.:instance}.conf
+}
+
+sqlcounter dailycounter_extend {
+ sql_module_instance = sql
+ dialect = ${modules.sql.dialect}
+ counter_name = &control.Daily-Session-Time
+ check_name = &control.Max-Daily-Session
+ reply_name = &reply.Session-Timeout
+ auto_extend = yes
+ key = "%{&Stripped-User-Name || &User-Name}"
+ reply_message_name = &Reply-Message
+ reset = daily
+
+ $INCLUDE ${modconfdir}/sql/counter/${dialect}/dailycounter.conf
+}
+
+
+date {
+ format = "%Y-%m-%dT%H:%M:%SZ"
+ utc = yes
+}
--- /dev/null
+#
+# Test sqlcounter.
+#
+string date_str
+uint64 now
+uint64 start
+uint64 remaining
+
+#
+# Ensure no accounting records for the user
+#
+%sql("DELETE FROM radacct WHERE username = '%{User-Name}'")
+
+#
+# Calling the module before setting a limit should do nothing
+#
+dailycounter
+if (!noop) {
+ test_fail
+}
+
+#
+# Now set a limit and re-call the module
+#
+&control.Max-Daily-Session := 100
+
+dailycounter
+if (!updated) {
+ test_fail
+}
+
+#
+# Check attributes have been set
+#
+if !(&control.Daily-Session-Time == 0) {
+ test_fail
+}
+
+if !(&reply.Session-Timeout == 100) {
+ test_fail
+}
+
+#
+# Calculate the start date/time to compare with attribute set by the module
+#
+&date_str = %date('now')
+&now = %date(%{date_str})
+if (&date_str =~ /([0-9]{4}-[0-9]{2}-[0-9]{2}T)[0-9]{2}:[0-9]{2}:[0-9]{2}Z/) {
+ &date_str := "%{1}" + '00:00:00Z'
+}
+&start = %date(%{date_str})
+
+if !(&control.dailycounter-Reset-Start == &start) {
+ test_fail
+}
+
+#
+# Insert a fake accounting record
+#
+%sql("INSERT INTO radacct (acctsessionid, acctuniqueid, username, acctstarttime, acctsessiontime) values ('%{User-Name}', '%{User-Name}', '%{User-Name}', DATETIME('now'), 60)")
+
+#
+# Call the module again
+#
+dailycounter
+if (!updated) {
+ test_fail
+}
+
+#
+# Check the attributes have been updated
+#
+if !(&control.Daily-Session-Time == 60) {
+ test_fail
+}
+
+if !(&reply.Session-Timeout == 40) {
+ test_fail
+}
+
+#
+# Reduce the session duration in the fake accounting record
+#
+%sql("UPDATE radacct SET acctsessiontime = 10 WHERE acctuniqueid = '%{User-Name}'")
+
+#
+# Call the module again
+# If the reply attribute exists and is smaller than the new value, the old
+# value will be kept and the rcode will be `ok`
+#
+dailycounter
+if (!ok) {
+ test_fail
+}
+
+#
+# Check the attributes have been updated correctly.
+#
+if !(&control.Daily-Session-Time == 10) {
+ test_fail
+}
+
+if !(&reply.Session-Timeout == 40) {
+ test_fail
+}
+
+&reply := {}
+
+#
+# Insert a second fake accounting record, which when summed with the existing will exceed the limit.
+#
+%sql("INSERT INTO radacct (acctsessionid, acctuniqueid, username, acctstarttime, acctsessiontime) values ('%{User-Name}-2', '%{User-Name}-2', '%{User-Name}', DATETIME('now'), 99)")
+
+dailycounter {
+ reject = 1
+}
+if (!reject) {
+ test_fail
+}
+
+if !(&reply.Reply-Message == 'Your maximum daily usage has been reached') {
+ test_fail
+}
+if !(&control.Daily-Session-Time == 109) {
+ test_fail
+}
+
+#
+# Find how much time is left before the next reset and set the limit
+# so the user has enough remaining to get into the next period
+#
+&remaining = &control.dailycounter-Reset-End - &now
+&control.Max-Daily-Session := &remaining + 110
+
+&reply := {}
+
+dailycounter
+
+if !(&reply.Session-Timeout == (&control.Max-Daily-Session - 109)) {
+ test_fail
+}
+
+&reply := {}
+
+#
+# Now use module instance with auto_extend = yes
+# This sets the reply attribute to include the next period's allocation.
+# Allow some jitter in the reply to cope with crossing seconds boundaries during the test.
+#
+dailycounter_extend
+
+if !((&reply.Session-Timeout > (&remaining + &control.Max-Daily-Session - 2)) && (&reply.Session-Timeout <= (&remaining + &control.Max-Daily-Session))) {
+ test_fail
+}
+
+&reply := {}
+
+test_pass