// 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 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 /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"); } // ---- the three shapes a line can take { Parsed p = P("# finger what\n" "right-ring-finger /usr/bin/logger -t fp ring\n" "left-little-finger no-unlock /etc/fingerprintd/panic.sh\n" "left-thumb no-unlock\n"); Check(p.Ok(), "a valid file parses"); Check(p.rules.size() == 3, "all three rules"); const Rule* ring = Find(p.rules, Finger::RightRing); Check(ring && ring->unlocks, "a command-only finger still unlocks"); Check(ring && ring->command == "/usr/bin/logger -t fp ring", "and the command arrives whole, spaces and all"); const Rule* duress = Find(p.rules, Finger::LeftLittle); Check(duress && !duress->unlocks, "no-unlock is recorded"); Check(duress && duress->command == "/etc/fingerprintd/panic.sh", "alongside its command -- the duress case needs both"); const Rule* thumb = Find(p.rules, Finger::LeftThumb); Check(thumb && !thumb->unlocks, "no-unlock alone is a complete rule"); Check(thumb && thumb->command.empty(), "with no command"); Check(Find(p.rules, Finger::RightIndex) == nullptr, "a finger with no rule has no rule"); } // ---- a command that merely STARTS like the keyword is a command { Parsed p = P("left-index-finger /usr/local/bin/no-unlock-helper\n"); Check(p.Ok(), "parses"); const Rule* r = Find(p.rules, Finger::LeftIndex); Check(r && r->unlocks, "the finger still unlocks"); Check(r && r->command == "/usr/local/bin/no-unlock-helper", "and the path was not mistaken for the keyword"); } // ---- every rejection rejects the whole file { struct Case { std::string_view text; Error want; std::string_view why; }; const Case cases[] = { { "not-a-finger /bin/true\n", Error::UnknownFinger, "an unknown finger name" }, { "right-index-finger\n", Error::NothingToDo, "a finger on its own, which the signal already covers" }, { "right-index-finger reboot\n", Error::RelativeCommand, "a command that is not an absolute path" }, { "right-index-finger no-unlock reboot\n", Error::RelativeCommand, "a relative command after the keyword" }, { "left-thumb no-unlock\nleft-thumb /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 no-unlock\n" "left-thumb 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::RelativeCommand, Error::NothingToDo, Error::DuplicateFinger }; 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; }