Files
mesh-catalog/modules/systemd/cmd/systemd-tools/journal_test.go
T
jschoubben ebcc7761f1
mesh/check failed (2): unit: distribution/internal/holder.TestTheHolderOfTheTwoImageSeats/a_transfer_in_par…; and 1 more | ebcc7761f178 on d9f2b7bcfa07
mesh/delivery-group group fix/1975-kernel-log rejected: composed together: the checker's verdict at the members' heads (c265ee221ccf): unit: distribution/inter…
mesh/delivery closed: a newer head of the same pull request
journal refuses a boot argument it does not use (issue 1975, AC4); tests cite their acceptance labels
Builds: issue 1975 note 2: lead answer L4, acceptance 1975/AC1-AC4

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 02:24:44 +01:00

330 lines
12 KiB
Go

package main
// The journal verb's window, priority and match (the operator's direction 2026-10-07): every value one
// word of journalctl's argv, nothing read as an option, the cap kept, a secret never answered — and
// what has failed on the seat.
import (
"fmt"
"strings"
"testing"
)
// journalOf is a manager whose journalctl answers the given lines and whose unit has the given
// Environment=, keeping each call.
func journalOf(lines []string, env string, calls *[]call) *Manager {
return operator(fake(func(c call) Ran {
if contains(c.args, "journalctl") || c.cmd == "journalctl" {
return Ran{Stdout: strings.Join(lines, "\n") + "\n"}
}
if contains(c.args, "--property=Environment") {
return Ran{Stdout: env + "\n"}
}
return Ran{}
}, calls))
}
func journalArgs(t *testing.T, calls []call) []string {
t.Helper()
for _, c := range calls {
if c.cmd == "sudo" && len(c.args) > 1 && c.args[1] == "journalctl" {
return c.args[2:]
}
if c.cmd == "journalctl" {
return c.args
}
}
t.Fatalf("journalctl was not run: %+v", calls)
return nil
}
func TestAWindowAndAPriorityAreEachOneWordOfJournalctl(t *testing.T) {
var calls []call
m := journalOf([]string{"a"}, "", &calls)
_, err := m.Journal(System, "mail.service", JournalQuery{Lines: 50, Since: "-30min", Until: "2026-10-07T10:00:00.5Z", Priority: "warning"})
if err != nil {
t.Fatal(err)
}
got := strings.Join(journalArgs(t, calls), " ")
want := "--no-pager --output=short-iso --unit=mail.service --since=-30min --until=@1791367201 --priority=4 --lines=50"
if got != want {
t.Fatalf("journalctl was given\n %s\nnot\n %s", got, want)
}
if calls[0].cmd != "sudo" || calls[0].args[0] != "-n" {
t.Fatalf("the system journal was not read escalated: %+v", calls[0])
}
}
func TestTheFormsAWindowIsReadIn(t *testing.T) {
for _, ok := range []string{"-30min", "-2h", "+1h", "-1h30min", "2h ago", "30min ago", "yesterday", "now", "today",
"2026-10-07T09:30:00Z", "2026-10-07T09:30:00+02:00", "2026-10-07", "2026-10-07 09:30", "2026-10-07 09:30:15"} {
if _, err := (JournalQuery{Since: ok}).valid(); err != nil {
t.Errorf("%q refused: %v", ok, err)
}
}
for _, bad := range []string{"--user", "-u", "-D/etc", "--directory=/", "-30min --merge", "-30min;id", "$(id)",
"-x", "--", "1h; rm", "2026-10-07T09:30:00", "last week", "-30min\n--merge"} {
if _, err := (JournalQuery{Since: bad}).valid(); err == nil {
t.Errorf("since %q accepted", bad)
}
if _, err := (JournalQuery{Until: bad}).valid(); err == nil {
t.Errorf("until %q accepted", bad)
}
}
if _, err := (JournalQuery{Since: "2026-10-07T10:00:00Z", Until: "2026-10-07T09:00:00Z"}).valid(); err == nil {
t.Error("a window ending before it starts was accepted")
}
}
func TestPriorityIsANumberOrAName(t *testing.T) {
for in, want := range map[string]string{"0": "0", "7": "7", "err": "3", "ERROR": "3", "warning": "4", "debug": "7", "emerg": "0"} {
q, err := (JournalQuery{Priority: in}).valid()
if err != nil || q.Priority != want {
t.Errorf("%q: %q %v", in, q.Priority, err)
}
}
for _, bad := range []string{"8", "-1", "--user", "3..5", "loud", "33"} {
if _, err := (JournalQuery{Priority: bad}).valid(); err == nil {
t.Errorf("priority %q accepted", bad)
}
}
}
func TestNothingRunsWhenAValueIsRefused(t *testing.T) {
var calls []call
m := journalOf(nil, "", &calls)
for _, q := range []JournalQuery{{Since: "--merge"}, {Until: "-D/"}, {Priority: "--user"}, {Lines: -1},
{Match: "a\nb"}, {Match: strings.Repeat("x", LongestMatch+1)}} {
if _, err := m.Journal(System, "x.service", q); err == nil {
t.Errorf("%+v accepted", q)
}
}
if _, err := m.Journal(System, "--merge", JournalQuery{}); err == nil {
t.Error("an option was accepted as a unit")
}
if len(calls) != 0 {
t.Fatalf("something ran: %+v", calls)
}
}
func TestTheCapIsKeptAndTheDefaultIsAHundred(t *testing.T) {
for in, want := range map[int]int{0: DefaultLines, 1: 1, 2000: 2000, 2001: 2000, 1 << 30: 2000} {
q, err := (JournalQuery{Lines: in}).valid()
if err != nil || q.Lines != want {
t.Errorf("%d: %d %v", in, q.Lines, err)
}
}
// What the seat's schema carries is text, and an agent may send a number: both read the same.
for _, a := range []map[string]any{{"lines": "250"}, {"lines": float64(250)}} {
q, err := journalQuery(a)
if err != nil || q.Lines != 250 {
t.Errorf("%v: %+v %v", a, q, err)
}
}
for _, a := range []map[string]any{{"lines": "many"}, {"lines": 2.5}, {"since": float64(3)}, {"match": []any{"x"}}} {
if _, err := journalQuery(a); err == nil {
t.Errorf("%v accepted", a)
}
}
}
func TestAMatchIsAFixedStringOverTheWindowsLastLines(t *testing.T) {
var calls []call
lines := []string{"one (a.b)", "two a.b", "three axb", "four (a.b)"}
r, err := journalOf(lines, "", &calls).Journal(System, "x.service", JournalQuery{Lines: 1, Match: "(a.b)"})
if err != nil {
t.Fatal(err)
}
if got := r["lines"].([]string); len(got) != 1 || got[0] != "four (a.b)" {
t.Fatalf("%v", got)
}
if r["scanned"] != 4 || r["count"] != 1 || r["match"] != "(a.b)" {
t.Fatalf("%v", r)
}
if a := journalArgs(t, calls); a[len(a)-1] != fmt.Sprintf("--lines=%d", MatchScan) {
t.Fatalf("a match was not looked for over a scan: %v", a)
}
for _, a := range journalArgs(t, calls) {
if strings.Contains(a, "a.b") || strings.HasPrefix(a, "--grep") {
t.Fatalf("the match reached journalctl: %v", a)
}
}
}
func TestASecretTheUnitPrintedIsNeverAnsweredNorFoundByAMatch(t *testing.T) {
env := `HOME=/var/lib/x "DB_PASSWORD=hunter2hunter2" DATABASE_URL=postgres://app:s3cr3tpw@db/app PORT=5432`
lines := []string{
"starting with password hunter2hunter2",
"connecting to postgres://app:s3cr3tpw@db/app",
"proxy at https://u:otherpassword@example.test/",
"ran: tool --password=flagsecret1 --token tokensecret2",
"API_TOKEN=abcdefghij set",
"nothing secret here",
}
r, err := journalOf(lines, env, nil).Journal(System, "x.service", JournalQuery{})
if err != nil {
t.Fatal(err)
}
all := strings.Join(r["lines"].([]string), "\n")
for _, secret := range []string{"hunter2hunter2", "s3cr3tpw", "otherpassword", "flagsecret1", "tokensecret2", "abcdefghij"} {
if strings.Contains(all, secret) {
t.Errorf("%s was answered:\n%s", secret, all)
}
}
if !strings.Contains(all, "[redacted: DB_PASSWORD]") || !strings.Contains(all, "nothing secret here") {
t.Fatalf("%s", all)
}
if r["redacted"] == nil || r["leak"] == nil {
t.Fatalf("the redaction was not said: %v", r)
}
found, _ := journalOf(lines, env, nil).Journal(System, "x.service", JournalQuery{Match: "hunter2"})
if found["count"] != 0 {
t.Fatalf("a match found a secret: %v", found)
}
}
func TestAnUnreadableEnvironmentStillRedactsByShapeAndSaysSo(t *testing.T) {
m := operator(fake(func(c call) Ran {
if contains(c.args, "--property=Environment") {
return Ran{Status: 1, Stderr: "Failed to get properties: Access denied\n"}
}
return Ran{Stdout: "postgres://app:s3cr3tpw@db/app\n"}
}, nil))
r, err := m.Journal(System, "x.service", JournalQuery{})
if err != nil {
t.Fatal(err)
}
if strings.Contains(strings.Join(r["lines"].([]string), ""), "s3cr3tpw") || r["redaction"] == nil {
t.Fatalf("%v", r)
}
}
func TestALongLineIsCut(t *testing.T) {
r, _ := journalOf([]string{strings.Repeat("y", LongestLine+10)}, "", nil).Journal(System, "x.service", JournalQuery{})
if l := r["lines"].([]string)[0]; len(l) > LongestLine+len("…") {
t.Fatalf("a line of %d bytes", len(l))
}
}
func TestTheEnvironmentPropertyIsReadAsWords(t *testing.T) {
got := environment(`A=1 "B=two words" 'C=x\'y' D=`)
if strings.Join(got, "|") != "A=1|B=two words|C=x'y|D=" {
t.Fatalf("%q", got)
}
}
func TestFailedIsOnTheSeatAndNarrowsToAScope(t *testing.T) {
var calls []call
m := operator(fake(func(call) Ran { return Ran{Stdout: list} }, &calls))
var failed func(map[string]any) (any, error)
for _, tool := range tools(m) {
if tool.Name == seat+".failed" {
failed = tool.Run
}
if tool.Name == "systemd_failed" {
t.Fatal("the module still serves its own systemd_failed beside the seat's verb")
}
}
if failed == nil {
t.Fatal("the seat's failed is not served")
}
both, err := failed(map[string]any{})
if err != nil || len(both.(map[string]any)) != 2 {
t.Fatalf("%v %v", both, err)
}
calls = nil
one, err := failed(map[string]any{"scope": "system"})
if err != nil || len(one.(map[string]any)) != 1 || len(calls) != 1 || contains(calls[0].args, "--user") {
t.Fatalf("%v %v %+v", one, err, calls)
}
if _, err := failed(map[string]any{"scope": "everything"}); err == nil {
t.Fatal("a scope that is none was accepted")
}
}
// 1975/AC1: kernel-log reads journalctl --dmesg --boot=N under journal's escalation (issue 1975): the kernel's messages of one boot, in the journal verb's window, priority,
// match, cap and redaction, and a boot named 0, -1, and so on.
func TestTheKernelLogIsOneBootsMessagesInTheJournalsForms(t *testing.T) {
var calls []call
m := journalOf([]string{"2026-11-12T10:00:00+0100 host kernel: usb 1-1: new device"}, "", &calls)
got, err := m.KernelLog(JournalQuery{Lines: 50, Since: "-2h", Priority: "err", Boot: "-1"})
if err != nil {
t.Fatal(err)
}
want := "--no-pager --output=short-iso --dmesg --boot=-1 --since=-2h --priority=3 --lines=50"
if a := strings.Join(journalArgs(t, calls), " "); a != want {
t.Fatalf("journalctl was given\n %s\nnot\n %s", a, want)
}
if calls[0].cmd != "sudo" || got["boot"] != "-1" || got["count"] != 1 {
t.Fatalf("not escalated, or the answer is wrong: %+v %+v", calls[0], got)
}
calls = nil
if _, err := m.KernelLog(JournalQuery{}); err != nil {
t.Fatal(err)
}
if a := strings.Join(journalArgs(t, calls), " "); a != "--no-pager --output=short-iso --dmesg --boot=0 --lines=100" {
t.Fatalf("the default is this boot and a hundred lines: %s", a)
}
// Arguments arrive as the seat's strings or as numbers.
for _, a := range []map[string]any{{"boot": float64(-2)}, {"boot": "-2"}} {
q, err := journalQuery(a)
if err != nil || q.Boot != "-2" {
t.Errorf("%v: %+v %v", a, q, err)
}
}
}
// 1975/AC2: a boot that is not 0 to -9999 is refused and no command runs.
func TestAKernelLogBootIsRefusedBeforeAnythingRunsUnlessItIsZeroOrBefore(t *testing.T) {
for _, bad := range []string{"1", "abc", "--user", "-1 --merge", "-10000", "0x1", "-1;id"} {
var calls []call
m := journalOf(nil, "", &calls)
if _, err := m.KernelLog(JournalQuery{Boot: bad}); err == nil {
t.Errorf("boot %q accepted", bad)
}
if len(calls) != 0 {
t.Errorf("boot %q reached journalctl: %v", bad, calls)
}
}
}
// 1975/AC3: the window, priority, match, 2000-line cap and redaction are journal's.
func TestTheKernelLogKeepsTheCapTheMatchAndTheRedaction(t *testing.T) {
var calls []call
m := journalOf([]string{"k one", "k password=hunter2secretvalue here", "k two"}, "", &calls)
got, err := m.KernelLog(JournalQuery{Match: "k t", Lines: 5000})
if err != nil {
t.Fatal(err)
}
if a := strings.Join(journalArgs(t, calls), " "); !strings.HasSuffix(a, fmt.Sprintf("--lines=%d", MatchScan)) {
t.Errorf("a match scans the window's last %d lines: %s", MatchScan, a)
}
if lines := got["lines"].([]string); len(lines) != 1 || lines[0] != "k two" {
t.Errorf("the match kept %v", lines)
}
calls = nil
m = journalOf([]string{"k password=hunter2secretvalue here"}, "", &calls)
got, err = m.KernelLog(JournalQuery{Lines: 5000})
if err != nil {
t.Fatal(err)
}
if a := strings.Join(journalArgs(t, calls), " "); !strings.HasSuffix(a, "--lines=2000") {
t.Errorf("the cap is 2000: %s", a)
}
if strings.Contains(fmt.Sprint(got["lines"]), "hunter2") || got["redacted"] == nil {
t.Errorf("a secret in a kernel line was answered: %v", got)
}
}
// 1975/AC4: journal refuses a boot argument it does not use, never reading this boot quietly.
func TestJournalRefusesABootArgument(t *testing.T) {
var calls []call
m := journalOf([]string{"a"}, "", &calls)
if _, err := m.Journal(System, "mail.service", JournalQuery{Boot: "-1"}); err == nil {
t.Fatal("journal accepted a boot and read this boot")
}
if len(calls) != 0 {
t.Fatalf("a refused boot reached the manager: %v", calls)
}
}