package cli import ( "bytes" "os" "path/filepath" "strings" "testing" ) func TestHelp(t *testing.T) { var out, errb bytes.Buffer code := Run([]string{"help"}, nil, &out, &errb) if code != 0 || !strings.Contains(out.String(), "lectio") { t.Errorf("help code=%d out=%q", code, out.String()) } } func TestHelpFlag(t *testing.T) { var out, errb bytes.Buffer code := Run([]string{"-h"}, nil, &out, &errb) if code != 0 || !strings.Contains(out.String(), "lectio") { t.Errorf("-h code=%d out=%q", code, out.String()) } } func TestUnexpectedPositional(t *testing.T) { var out, errb bytes.Buffer if code := Run([]string{"bogus"}, nil, &out, &errb); code != 2 { t.Errorf("unexpected positional code=%d want 2 (stderr=%q)", code, errb.String()) } } func TestVersion(t *testing.T) { var out, errb bytes.Buffer if code := Run([]string{"--version"}, nil, &out, &errb); code != 0 { t.Errorf("version code=%d", code) } } // TestBadDate exercises the usage-error path for a malformed date: a token // that doesn't match dateRe's YYYY-MM-DD shape is left as an unrecognised // positional argument once flag parsing is done, which is a usage error. func TestBadDate(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) var out, errb bytes.Buffer if code := Run([]string{"13-13-13"}, nil, &out, &errb); code != 2 { t.Errorf("bad date code=%d want 2 (stderr=%q)", code, errb.String()) } } // TestTwoDatesIsAmbiguous exercises extractDate's ambiguity check: two // dateRe-shaped tokens can't both be the positional date. func TestTwoDatesIsAmbiguous(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) var out, errb bytes.Buffer if code := Run([]string{"2026-07-22", "2026-07-23"}, nil, &out, &errb); code != 2 { t.Errorf("two dates code=%d want 2 (stderr=%q)", code, errb.String()) } } // TestUnknownVersion exercises -b/--bible's version-validation usage-error // path without touching the network: the version code is checked against // the five known codes before any fetch is attempted. func TestUnknownVersion(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) var out, errb bytes.Buffer if code := Run([]string{"-b", "zzz"}, nil, &out, &errb); code != 2 { t.Errorf("unknown version code=%d want 2 (stderr=%q)", code, errb.String()) } } // TestLectionaryTradMapsToTraditional exercises -l/--lectionary's trad -> // traditional normalisation. It goes through extractDate/flag parsing and // validation only (no network): a bogus date lets it exit before any fetch // is attempted, but only after the -l value has already been validated and // mapped, which is what this test checks indirectly via the exit code (a // bad --lectionary would exit 2 for a different reason, so this alone // wouldn't distinguish the two -- see TestLectionaryBogus for that side). // Exercised directly against parseFlags below. func TestLectionaryTradMapsToTraditional(t *testing.T) { lectionary, err := normalizeLectionary("trad") if err != nil { t.Fatalf("normalizeLectionary(trad): %v", err) } if lectionary != "traditional" { t.Errorf("normalizeLectionary(trad) = %q, want traditional", lectionary) } } func TestLectionaryBogus(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) var out, errb bytes.Buffer if code := Run([]string{"-l", "bogus"}, nil, &out, &errb); code != 2 { t.Errorf("bogus lectionary code=%d want 2 (stderr=%q)", code, errb.String()) } if _, err := normalizeLectionary("bogus"); err == nil { t.Error("normalizeLectionary(bogus): want error, got nil") } } func TestLangBogus(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) var out, errb bytes.Buffer if code := Run([]string{"-g", "de"}, nil, &out, &errb); code != 2 { t.Errorf("bogus lang code=%d want 2 (stderr=%q)", code, errb.String()) } } // TestBannerForLang checks bannerFor's wording follows lang: pl reproduces // the pre-i18n Polish banner exactly ("Ewangelia na D" / "Czytania na D"), // en gives "Gospel for D" / "Readings for D". func TestBannerForLang(t *testing.T) { cases := []struct { lang string all bool want string }{ {"pl", false, "Ewangelia na 2026-07-22"}, {"pl", true, "Czytania na 2026-07-22"}, {"en", false, "Gospel for 2026-07-22"}, {"en", true, "Readings for 2026-07-22"}, } for _, c := range cases { if got := bannerFor(c.lang, c.all, "2026-07-22"); got != c.want { t.Errorf("bannerFor(%q, %v, ...) = %q, want %q", c.lang, c.all, got, c.want) } } } // TestCleanEmptyCache exercises -C/--clean's dispatch as a maintenance mode: // it must be handled before any render path (and before the network-hitting // -u/--update path) is reached. Pointing XDG_CACHE_HOME at an empty temp dir // keeps liturgy.CleanCache's directory-read hermetic (no network), and an // empty dir exercises the "nothing to remove" branch of the summary line. func TestCleanEmptyCache(t *testing.T) { t.Setenv("XDG_CACHE_HOME", t.TempDir()) var out, errb bytes.Buffer code := Run([]string{"-C"}, nil, &out, &errb) if code != 0 { t.Fatalf("--clean code=%d want 0 (stderr=%q)", code, errb.String()) } if !strings.Contains(out.String(), "clean") { t.Errorf("--clean stdout=%q, want a clean/nothing message", out.String()) } } // TestCleanRemovesOldEntries exercises the non-empty summary branch: a stale // cache pair sitting in XDG_CACHE_HOME must be reported as removed, entirely // via the filesystem (no network involved). func TestCleanRemovesOldEntries(t *testing.T) { dir := t.TempDir() t.Setenv("XDG_CACHE_HOME", dir) cacheDir := filepath.Join(dir, "lectio") if err := os.MkdirAll(cacheDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(cacheDir, "2020-01-01.html"), []byte("old"), 0o644); err != nil { t.Fatal(err) } var out, errb bytes.Buffer code := Run([]string{"--clean"}, nil, &out, &errb) if code != 0 { t.Fatalf("--clean code=%d want 0 (stderr=%q)", code, errb.String()) } if !strings.Contains(out.String(), "cleaned 1 cache entry") { t.Errorf("--clean stdout=%q, want a \"cleaned 1 cache entry\" message", out.String()) } if _, err := os.Stat(filepath.Join(cacheDir, "2020-01-01.html")); !os.IsNotExist(err) { t.Errorf("2020-01-01.html still exists after --clean") } } // TestCleanPreferredOverUpdate exercises Run's stated precedence: when both // --clean and -u/--update are given, --clean wins and -u's network-hitting // harvest path is never reached (proven here by the empty-cache dir plus a // zero exit code -- a network attempt against no test server would either // hang or return an error/non-zero code). func TestCleanPreferredOverUpdate(t *testing.T) { t.Setenv("XDG_CACHE_HOME", t.TempDir()) var out, errb bytes.Buffer code := Run([]string{"--clean", "-u"}, nil, &out, &errb) if code != 0 { t.Fatalf("--clean -u code=%d want 0 (stderr=%q)", code, errb.String()) } if !strings.Contains(out.String(), "clean") { t.Errorf("--clean -u stdout=%q, want a clean/nothing message", out.String()) } } // TestDateTokenAnyPosition exercises extractDate directly: the date token // is found regardless of where it appears among other flags. func TestDateTokenAnyPosition(t *testing.T) { cases := [][]string{ {"-a", "2026-07-22"}, {"2026-07-22", "-a"}, {"-a", "2026-07-22", "-r"}, } for _, args := range cases { date, rest, err := extractDate(args) if err != nil { t.Fatalf("extractDate(%v): %v", args, err) } if date != "2026-07-22" { t.Errorf("extractDate(%v) date = %q, want 2026-07-22", args, date) } for _, r := range rest { if r == "2026-07-22" { t.Errorf("extractDate(%v) rest = %v still contains the date", args, rest) } } } } // TestExtractDateDefaultsToday checks that omitting a date token leaves // today's date and passes every token through untouched. func TestExtractDateDefaultsToday(t *testing.T) { date, rest, err := extractDate([]string{"-a", "-r"}) if err != nil { t.Fatalf("extractDate: %v", err) } if date != today() { t.Errorf("extractDate date = %q, want today() = %q", date, today()) } if len(rest) != 2 || rest[0] != "-a" || rest[1] != "-r" { t.Errorf("extractDate rest = %v, want [-a -r]", rest) } }