iocaine 3.0.0

The deadliest poison known to AI
Documentation
-- SPDX-FileCopyrightText: 2025 Gergely Nagy
-- SPDX-FileContributor: Gergely Nagy
--
-- SPDX-License-Identifier: MIT

local decide = require("decide")
local output = require("output")

function test_decide_ai_robots_txt()
   local request = make_request()
   request:set_header("user-agent", "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.2; +https://openai.com/gptbot)")

   return decide(request:share()) == "garbage"
end

function test_decide_major_browsers_ok()
   local request = make_request()
   request:set_header("user-agent", "Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0")
   request:set_header("sec-fetch-mode", "document")

   return decide(request:share()) == "default"
end

function test_decide_major_browsers_expected_fail()
   local request = make_request()
   request:set_header("user-agent", "Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0")

   return decide(request:share()) == "garbage"
end

function test_decide_unwanted_visitor()
   local request = make_request()
   request:set_header("user-agent", "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot)")

   return decide(request:share()) == "garbage"
end

function test_decide_curl()
   local request = make_request()
   request:set_header("user-agent", "curl/8.14.1")

   return decide(request:share()) == "default"
end

function test_output_421()
   local request = make_request()
   request:set_header("user-agent", "curl/8.14.1")
   request = request:share()

   local response = output(request, decide(request))
   return response.status == 421
end

function test_output_garbage()
   local request = make_request()
   request:set_header("user-agent", "PerplexityBot")
   request = request:share()

   local response = output(request, decide(request))
   return response.status == 200 and response:header("content-type") == "text/html"
end

function test_output_wrong_decision()
   local request = make_request()
   request:set_header("user-agent", "PerplexityBot")
   request = request:share()

   local response = output(request, "wrong-decision")
   return response.status == 200 and response:header("content-type") == "text/html"
end

function test_output_with_trusted_header()
   if iocaine.config["trusted-decision-header"] == nil then
      return true
   end

   local request = make_request()
   request:set_header("user-agent", "PerplexityBot")
   request:set_header(iocaine.config["trusted-decision-header"], "default")
   request = request:share()

   local response = output(request, decide(request))
   return response.status == 421
end

local tests = {
   ["decide_ai_robots_txt"] = test_decide_ai_robots_txt,
   ["decide_major_browsers_ok"] = test_decide_major_browsers_ok,
   ["decide_major_browsers_expected_fail"] = test_decide_major_browsers_expected_fail,
   ["decide_unwanted_visitor"] = test_decide_unwanted_visitor,
   ["decide_curl"] = test_decide_curl,
   ["output_421"] = test_output_421,
   ["output_garbage"] = test_output_garbage,
   ["output_wrong_decision"] = test_output_wrong_decision,
   ["output_with_trusted_header"] = test_output_with_trusted_header,
}

function run_tests()
   local succeeded = 0
   local failed = 0
   local count = 0
   local total = length(tests)

   for name, f in pairs(tests) do
      count = count + 1
      io.write("Test " .. count .. " / " .. total .. ": " .. name .. "...")
      if f() then
         succeeded = succeeded + 1
         ansi_colored_result(92, "ok")
      else
         failed = failed + 1
         ansi_colored_result(91, "fail")
      end
   end

   print("Ran " .. count .. " tests, " .. succeeded .. " succeeded, " .. failed .. " failed.")

   return failed == 0
end

function ansi_colored_result(color, message)
   print(" " .. string.char(27) .. '[' .. tostring(color) .. 'm' .. message .. string.char(27) .. "[0m")
end

function length(t)
   local count = 0
   for _ in pairs(t) do
      count = count + 1
   end
   return count
end

function make_request()
   local request = iocaine.Request("GET", "/")
   request:set_header("host", "tests.example.com")

   return request
end

return run_tests