]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Test] composites: functional cases for Lua conditions
authorVsevolod Stakhov <vsevolod@rspamd.com>
Fri, 10 Jul 2026 07:32:18 +0000 (08:32 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Fri, 10 Jul 2026 07:32:18 +0000 (08:32 +0100)
Cover: option-inspecting condition, cross-symbol join on a shared
option value, numeric return as atom weight combined with an
expression limit, false condition, and depends_on deferring a
composite whose condition consults a postfilter symbol.

test/functional/cases/109_composites.robot
test/functional/lua/composites.lua

index 4d73cf4e8a9916c21721ece7bc2aa487648f2ece..97e360aa69c899c43f54f3c7fb620262f1a33411 100644 (file)
@@ -76,3 +76,19 @@ Composites - Opts RE Hit 3
   Expect Symbol With Score  SYMOPTS4  6.00
   Do Not Expect Symbol  SYMOPTS2
   Do Not Expect Symbol  SYMOPTS1
+
+Composites - Lua conditions
+  Scan File  ${MESSAGE}  opts=b-opt1
+  Expect Symbol With Score  COND_TRUE  5.00
+  Expect Symbol  COND_JOIN
+  Expect Symbol  COND_WEIGHT
+  Expect Symbol  COND_DEPENDS
+  Do Not Expect Symbol  COND_FALSE
+  Do Not Expect Symbol  COND_WEIGHT_MISS
+
+Composites - Lua condition join miss
+  Scan File  ${MESSAGE}  opts=zzz-no-match
+  Expect Symbol  COND_TRUE
+  Expect Symbol  COND_DEPENDS
+  Do Not Expect Symbol  COND_JOIN
+  Do Not Expect Symbol  COND_FALSE
index 648eda0ff290dcd8875e836d0849ad22bbe3ee2f..cdeeff76902c79d50665ff4aeaece190eaf7b4e9 100644 (file)
@@ -138,3 +138,132 @@ rspamd_config:register_symbol({
     end
   end
 })
+
+-- Composites with per-symbol Lua conditions; the auxiliary symbols fire only
+-- when the `opts` request header is present to keep the exact score
+-- assertions of the plain scan intact
+local function has_opts(task)
+  local h = task:get_request_header('opts')
+  return h and #tostring(h) > 0
+end
+
+rspamd_config:register_symbol({
+  name = 'COND_SYM_A',
+  score = 1.0,
+  callback = function(task)
+    if has_opts(task) then
+      return true, 1.0, 'a-opt1', 'a-opt2'
+    end
+  end
+})
+rspamd_config:register_symbol({
+  name = 'COND_SYM_B',
+  score = 1.0,
+  callback = function(task)
+    if has_opts(task) then
+      return true, 1.0, 'b-opt1'
+    end
+  end
+})
+rspamd_config:register_symbol({
+  name = 'COND_POSTFILTER_SYM',
+  type = 'postfilter',
+  score = 1.0,
+  callback = function(task)
+    if has_opts(task) then
+      task:insert_result('COND_POSTFILTER_SYM', 1.0, 'post-opt')
+    end
+  end
+})
+
+rspamd_config:add_composite('COND_TRUE', {
+  expression = 'COND_SYM_A & COND_SYM_B',
+  score = 5.0,
+  policy = 'leave',
+  conditions = {
+    COND_SYM_A = function(_, sym)
+      if not sym or sym.name ~= 'COND_SYM_A' then
+        return false
+      end
+      for _, o in ipairs(sym.options or {}) do
+        if o == 'a-opt2' then
+          return true
+        end
+      end
+      return false
+    end,
+  },
+})
+
+rspamd_config:add_composite('COND_FALSE', {
+  expression = 'COND_SYM_A & COND_SYM_B',
+  score = 5.0,
+  policy = 'leave',
+  conditions = {
+    COND_SYM_B = function()
+      return false
+    end,
+  },
+})
+
+-- Join on a shared option value between two independent symbols
+rspamd_config:add_composite('COND_JOIN', {
+  expression = 'OPTS & COND_SYM_B',
+  score = 5.0,
+  policy = 'leave',
+  conditions = {
+    OPTS = function(task, sym)
+      local other = task:get_symbol('COND_SYM_B')
+      if not other then
+        return false
+      end
+      local seen = {}
+      for _, o in ipairs(other[1].options or {}) do
+        seen[o] = true
+      end
+      for _, o in ipairs(sym.options or {}) do
+        if seen[o] then
+          return true
+        end
+      end
+      return false
+    end,
+  },
+})
+
+-- Numeric return is used as the atom weight in the expression
+rspamd_config:add_composite('COND_WEIGHT', {
+  expression = 'COND_SYM_A + COND_SYM_B > 5',
+  score = 5.0,
+  policy = 'leave',
+  conditions = {
+    COND_SYM_A = function()
+      return 10
+    end,
+  },
+})
+
+rspamd_config:add_composite('COND_WEIGHT_MISS', {
+  expression = 'COND_SYM_A + COND_SYM_B > 5',
+  score = 5.0,
+  policy = 'leave',
+  conditions = {
+    COND_SYM_A = function()
+      return 2
+    end,
+  },
+})
+
+-- The condition consults a postfilter symbol invisible to the expression;
+-- depends_on defers the composite to the second pass
+rspamd_config:add_composite('COND_DEPENDS', {
+  expression = 'COND_SYM_A',
+  score = 5.0,
+  policy = 'leave',
+  depends_on = { 'COND_POSTFILTER_SYM' },
+  conditions = {
+    COND_SYM_A = function(task)
+      return task:get_symbol('COND_POSTFILTER_SYM') ~= nil
+    end,
+  },
+})