mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
245 lines
7.5 KiB
TypeScript
245 lines
7.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
isUnit,
|
|
journalArgs,
|
|
parseBoots,
|
|
parseDetect,
|
|
parseEntry,
|
|
parseIsActive,
|
|
parseJournal,
|
|
parseJournalQuery,
|
|
parseOpenRcList,
|
|
parseSystemdTime,
|
|
parseTimers,
|
|
parseUnitList,
|
|
parseUsage,
|
|
shellQuote as q,
|
|
vacuumScript,
|
|
} from "@termix-ssh/plugin-sdk/host-scripts";
|
|
|
|
describe("names", () => {
|
|
it("quotes for bash", () => {
|
|
expect(q("a'b")).toBe(`'a'\\''b'`);
|
|
});
|
|
|
|
it("takes unit names and refuses anything else", () => {
|
|
expect(isUnit("nginx.service")).toBe(true);
|
|
expect(isUnit("getty@tty1.service")).toBe(true);
|
|
expect(isUnit("dev-disk-by\\x2duuid-1.device")).toBe(true);
|
|
expect(isUnit("-rf.service")).toBe(false);
|
|
expect(isUnit("a;b.service")).toBe(false);
|
|
expect(isUnit("a b.service")).toBe(false);
|
|
expect(isUnit("nginx")).toBe(false);
|
|
expect(isUnit("$(id).service")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("systemd parsing", () => {
|
|
const list = [
|
|
"cron.service loaded active running Regular background program",
|
|
"● bad.service loaded failed failed Broken thing",
|
|
"getty@tty1.service loaded active running Getty on tty1",
|
|
"@@TERMIX_FILES@@",
|
|
"cron.service enabled enabled",
|
|
"bad.service disabled enabled",
|
|
"getty@.service enabled enabled",
|
|
"old.service masked -",
|
|
"",
|
|
].join("\n");
|
|
|
|
it("merges loaded units with unit files and skips templates", () => {
|
|
const units = parseUnitList(list);
|
|
expect(units.map((u) => u.name)).toEqual([
|
|
"bad.service",
|
|
"cron.service",
|
|
"getty@tty1.service",
|
|
"old.service",
|
|
]);
|
|
const cron = units.find((u) => u.name === "cron.service")!;
|
|
expect(cron).toMatchObject({
|
|
active: "active",
|
|
sub: "running",
|
|
description: "Regular background program",
|
|
fileState: "enabled",
|
|
preset: "enabled",
|
|
});
|
|
expect(units.find((u) => u.name === "bad.service")!.active).toBe("failed");
|
|
expect(units.find((u) => u.name === "old.service")).toMatchObject({
|
|
load: "masked",
|
|
active: "inactive",
|
|
preset: "",
|
|
});
|
|
});
|
|
|
|
it("reads UTC times", () => {
|
|
expect(parseSystemdTime("Thu 2026-10-22 02:03:51 UTC")).toBe(
|
|
Date.UTC(2026, 9, 22, 2, 3, 51),
|
|
);
|
|
expect(parseSystemdTime("")).toBeNull();
|
|
expect(parseSystemdTime("n/a")).toBeNull();
|
|
});
|
|
|
|
it("reads timers from systemctl show blocks", () => {
|
|
const out = [
|
|
"TimersCalendar={ OnCalendar=*-*-* 06,18:00:00 ; next_elapse=@1792605600 }",
|
|
"NextElapseUSecRealtime=Thu 2026-10-22 02:03:51 UTC",
|
|
"LastTriggerUSec=Wed 2026-10-21 17:02:58 UTC",
|
|
"Id=apt-daily.timer",
|
|
"Triggers=apt-daily.service",
|
|
"ActiveState=active",
|
|
"",
|
|
"TimersMonotonic={ OnUnitActiveUSec=1d ; next_elapse=1d }",
|
|
"TimersMonotonic={ OnBootUSec=15min ; next_elapse=15min }",
|
|
"NextElapseUSecRealtime=",
|
|
"LastTriggerUSec=",
|
|
"Id=clean.timer",
|
|
"Triggers=clean.service",
|
|
].join("\n");
|
|
const timers = parseTimers(out);
|
|
expect(timers[0]).toMatchObject({
|
|
name: "apt-daily.timer",
|
|
activates: "apt-daily.service",
|
|
next: Date.UTC(2026, 9, 22, 2, 3, 51),
|
|
schedule: ["OnCalendar=*-*-* 06,18:00:00"],
|
|
});
|
|
expect(timers[1]).toMatchObject({
|
|
name: "clean.timer",
|
|
next: null,
|
|
last: null,
|
|
schedule: ["OnUnitActiveUSec=1d", "OnBootUSec=15min"],
|
|
});
|
|
});
|
|
|
|
it("maps is-active lines to units", () => {
|
|
const states = parseIsActive(
|
|
["a.service", "b.service"],
|
|
"active\nfailed\n",
|
|
);
|
|
expect(states.get("a.service")).toBe("active");
|
|
expect(states.get("b.service")).toBe("failed");
|
|
});
|
|
|
|
it("reads what detect prints", () => {
|
|
expect(
|
|
parseDetect(
|
|
"init=systemd\nversion=257\nos=Debian GNU/Linux 13 (trixie)\nuid=0\njournal=1\nfailed=2\n",
|
|
),
|
|
).toMatchObject({
|
|
init: "systemd",
|
|
version: "257",
|
|
root: true,
|
|
journal: true,
|
|
failed: 2,
|
|
});
|
|
expect(parseDetect("init=weird\nuid=1000\n")).toMatchObject({
|
|
init: "none",
|
|
root: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("journal", () => {
|
|
it("checks filters", () => {
|
|
const query = parseJournalQuery({
|
|
unit: ["nginx.service", "ssh.service"],
|
|
priority: "3",
|
|
since: "-1h",
|
|
boot: "-1",
|
|
grep: "fail'ed",
|
|
lines: "99999",
|
|
});
|
|
expect(query.lines).toBe(5000);
|
|
const args = journalArgs(query);
|
|
expect(args).toContain("-u 'nginx.service' -u 'ssh.service'");
|
|
expect(args).toContain("-p 3");
|
|
expect(args).toContain("--since '-1h'");
|
|
expect(args).toContain("-b '-1'");
|
|
expect(args).toContain(`-g 'fail'\\''ed'`);
|
|
expect(() => parseJournalQuery({ unit: "x;rm" })).toThrow();
|
|
expect(() => parseJournalQuery({ priority: "9" })).toThrow();
|
|
expect(() => parseJournalQuery({ since: "$(id)" })).toThrow();
|
|
expect(() => parseJournalQuery({ boot: "abc" })).toThrow();
|
|
});
|
|
|
|
it("parses entries, binary messages included", () => {
|
|
const entry = parseEntry(
|
|
JSON.stringify({
|
|
__REALTIME_TIMESTAMP: "1792612429684000",
|
|
PRIORITY: "3",
|
|
_SYSTEMD_UNIT: "x.service",
|
|
SYSLOG_IDENTIFIER: "x",
|
|
_PID: "12",
|
|
MESSAGE: [104, 105],
|
|
}),
|
|
);
|
|
expect(entry).toEqual({
|
|
time: 1792612429684,
|
|
priority: 3,
|
|
unit: "x.service",
|
|
ident: "x",
|
|
pid: "12",
|
|
message: "hi",
|
|
});
|
|
expect(parseEntry("not json")).toBeNull();
|
|
});
|
|
|
|
it("filters here when journalctl has no -g", () => {
|
|
const line = (message: string) =>
|
|
JSON.stringify({ __REALTIME_TIMESTAMP: "1000", MESSAGE: message });
|
|
const out = `@@TERMIX_NOGREP@@\n${line("Disk Error")}\n${line("ok")}\n`;
|
|
const query = parseJournalQuery({ grep: "error", lines: "10" });
|
|
expect(parseJournal(out, query).map((e) => e.message)).toEqual([
|
|
"Disk Error",
|
|
]);
|
|
});
|
|
|
|
it("reads boots and disk use", () => {
|
|
const boots = parseBoots(
|
|
[
|
|
"IDX BOOT ID FIRST ENTRY LAST ENTRY",
|
|
"-1 d2785a657f4c4561a1f436db27555b50 Tue 2026-10-20 13:56:45 UTC Wed 2026-10-21 10:37:00 UTC",
|
|
" 0 61b15588a75b4c0cb8dea321fcc438ab Wed 2026-10-21 10:38:10 UTC Wed 2026-10-21 20:00:00 UTC",
|
|
].join("\n"),
|
|
);
|
|
expect(boots.map((b) => b.offset)).toEqual([0, -1]);
|
|
expect(boots[0].first).toBe("Wed 2026-10-21 10:38:10 UTC");
|
|
expect(
|
|
parseUsage(
|
|
"Archived and active journals take up 378.6M in the file system.",
|
|
),
|
|
).toBe("378.6M");
|
|
});
|
|
|
|
it("only takes sane vacuum values", () => {
|
|
expect(vacuumScript({ size: "500M" })).toContain("--vacuum-size=500M");
|
|
expect(vacuumScript({ time: "4w" })).toContain("--vacuum-time=4w");
|
|
expect(() => vacuumScript({ size: "1; rm -rf /" })).toThrow();
|
|
expect(() => vacuumScript({})).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("openrc", () => {
|
|
it("maps services and runlevels", () => {
|
|
const units = parseOpenRcList(
|
|
[
|
|
" sshd [ started ]",
|
|
" crond [ stopped ]",
|
|
" nginx [ crashed ]",
|
|
"@@TERMIX_RUNLEVELS@@",
|
|
" sshd | default",
|
|
" nginx | ",
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
expect(units.find((u) => u.name === "sshd")).toMatchObject({
|
|
active: "active",
|
|
fileState: "enabled",
|
|
});
|
|
expect(units.find((u) => u.name === "nginx")).toMatchObject({
|
|
active: "failed",
|
|
fileState: "disabled",
|
|
});
|
|
expect(units.find((u) => u.name === "crond")!.active).toBe("inactive");
|
|
});
|
|
});
|