// SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // lint-disable-file fixed-width-types /* Fingerprintd:Actions unit tests. This table decides what root executes when a finger touches the sensor, so the tests here are mostly about REFUSAL. The load-bearing properties: * a file anyone but root can write is rejected before a single rule is read, * a malformed rule rejects the WHOLE file rather than being skipped -- a half-applied policy is the dangerous outcome, not the safe one, * a system command must be an absolute path, because resolving a bare name through PATH would make what root runs depend on an environment this daemon does not control, * and no rule at all means no behaviour change, which is what keeps the feature absent until someone configures it. */ import std; import Fingerprintd; using namespace fingerprintd::actions; using fingerprintd::store::Finger; namespace { int Failures = 0; void Check(bool cond, std::string_view msg) { if (!cond) { std::println(std::cerr, "FAIL: {}", msg); ++Failures; } } Parsed P(std::string_view t, bool rootOnly = true) { return Parse(t, rootOnly); } } int main() { // ---- the file's own permissions are checked before its contents { Parsed p = P("right-index-finger system match /bin/true", /*rootOnly*/ false); Check(!p.Ok(), "a file others can write is refused"); Check(p.error == Error::NotWritableOnlyByRoot, "and refused for that reason"); Check(p.line == 0, "the file is the fault, not a line"); Check(p.rules.empty(), "nothing is parsed out of it"); } // ---- no config is not an error; it is the feature being off { Parsed p = P(""); Check(p.Ok() && p.rules.empty(), "an empty file yields no rules"); Parsed c = P("# nothing but a comment\n\n # indented\n"); Check(c.Ok() && c.rules.empty(), "comments and blank lines are ignored"); } // ---- a well-formed table { Parsed p = P("# finger where verdict command\n" "right-index-finger session match\n" "left-little-finger system no-match /etc/fingerprintd/panic.sh\n"); Check(p.Ok(), "a valid file parses"); Check(p.rules.size() == 2, "both rules"); const Rule* idx = Find(p.rules, Finger::RightIndex); Check(idx != nullptr, "the index finger has a rule"); Check(idx && idx->where == Where::Session, "session"); Check(idx && idx->verdict == Verdict::Match, "and it still unlocks"); Check(idx && idx->command.empty(), "a session rule carries no command"); const Rule* pin = Find(p.rules, Finger::LeftLittle); Check(pin != nullptr, "the duress finger has a rule"); Check(pin && pin->where == Where::System, "root runs it"); Check(pin && pin->verdict == Verdict::NoMatch, "and the client is told it did NOT match -- the whole point of duress"); Check(pin && pin->command == "/etc/fingerprintd/panic.sh", "the command"); Check(Find(p.rules, Finger::RightThumb) == nullptr, "a finger with no rule has no rule"); } // ---- a command may contain spaces; the tail is not re-split { Parsed p = P("right-ring-finger system match /usr/bin/env FOO=1 /usr/local/bin/x -v\n"); Check(p.Ok(), "a command with arguments parses"); const Rule* r = Find(p.rules, Finger::RightRing); Check(r && r->command == "/usr/bin/env FOO=1 /usr/local/bin/x -v", "and arrives whole"); } // ---- every rejection rejects the whole file { struct Case { std::string_view text; Error want; std::string_view why; }; const Case cases[] = { { "not-a-finger system match /bin/true\n", Error::UnknownFinger, "an unknown finger name" }, { "right-index-finger elsewhere match /bin/true\n", Error::UnknownWhere, "an unknown 'where'" }, { "right-index-finger system maybe /bin/true\n", Error::UnknownVerdict, "an unknown verdict" }, { "right-index-finger system match\n", Error::MissingCommand, "a system rule with no command" }, { "right-index-finger session match /bin/true\n", Error::UnexpectedCommand, "a session rule with a command" }, { "right-index-finger system match reboot\n", Error::RelativeCommand, "a command that is not an absolute path" }, { "right-index-finger system match\n", Error::MissingCommand, "a truncated line" }, { "right-index-finger session match\nright-index-finger system match /bin/true\n", Error::DuplicateFinger, "two rules for one finger" }, }; for (const Case& c : cases) { Parsed p = P(c.text); Check(!p.Ok(), std::format("rejected: {}", c.why)); Check(p.error == c.want, std::format("for the right reason: {}", c.why)); Check(p.rules.empty(), std::format("and yields NO rules at all: {}", c.why)); } } // ---- a valid rule before a bad one is discarded with it { Parsed p = P("right-index-finger session match\n" "left-thumb system match reboot\n"); Check(!p.Ok(), "the file fails"); Check(p.line == 2, "on the offending line"); Check(p.rules.empty(), "and the GOOD rule above it is discarded too -- a half-applied " "policy is the dangerous outcome"); } // ---- every Error has a description; a switch that forgets one shows up here { const Error all[] = { Error::None, Error::NotWritableOnlyByRoot, Error::UnknownFinger, Error::UnknownWhere, Error::UnknownVerdict, Error::MissingCommand, Error::UnexpectedCommand, Error::DuplicateFinger, Error::RelativeCommand }; for (Error e : all) Check(Describe(e) != "unknown", "every error describes itself"); } if (Failures == 0) std::println("Actions: all checks passed"); return Failures == 0 ? 0 : 1; }