package settings import ( "os" "path/filepath" "testing" "time" ) func TestDiskSafeFilename(t *testing.T) { cases := map[string]string{ "Daily Backup": "Daily_Backup", " hello!! ": "hello", "../etc/passwd": "etc_passwd", "": "unnamed", "a/b\\c:d*e?f": "a_b_c_d_e_f", } for input, want := range cases { got := DiskSafeFilename(input) if got != want { t.Fatalf("DiskSafeFilename(%q) = %q, want %q", input, got, want) } } } func TestNodeActionGroupsRoundTrip(t *testing.T) { dir := t.TempDir() now := time.Now().UTC() store := NodeActionGroupStore{ Groups: []NodeActionGroup{ { ID: "g1", NodeID: "n1", Name: "Nightly", Schedule: ActionGroupSchedule{ Enabled: true, Kind: ActionGroupScheduleInterval, EverySeconds: 60, Times: []ActionGroupTimeSpec{}, }, Items: []NodeActionItem{ { ID: "i1", Source: ActionItemSourceLibrary, LibraryActionID: BuiltinActionUptimeID, Env: []ActionEnvVar{}, }, }, CreatedAt: now, UpdatedAt: now, }, }, } if err := SaveNodeActionGroups(dir, store); err != nil { t.Fatalf("SaveNodeActionGroups: %v", err) } loaded, err := LoadNodeActionGroups(dir) if err != nil { t.Fatalf("LoadNodeActionGroups: %v", err) } if len(loaded.Groups) != 1 || loaded.Groups[0].Name != "Nightly" { t.Fatalf("loaded = %#v", loaded) } if len(loaded.Groups[0].Items) != 1 { t.Fatalf("items = %#v", loaded.Groups[0].Items) } } func TestIsScheduleDueInterval(t *testing.T) { now := time.Date(2026, 7, 19, 12, 0, 0, 0, time.Local) group := NodeActionGroup{ Schedule: ActionGroupSchedule{ Enabled: true, Kind: ActionGroupScheduleInterval, EverySeconds: 300, }, } if !IsScheduleDue(group, now) { t.Fatal("expected due when never run") } last := now.Add(-301 * time.Second) group.LastRunAt = &last if !IsScheduleDue(group, now) { t.Fatal("expected due after interval elapsed") } recent := now.Add(-10 * time.Second) group.LastRunAt = &recent if IsScheduleDue(group, now) { t.Fatal("expected not due within interval") } } func TestIsScheduleDueTimes(t *testing.T) { now := time.Date(2026, 7, 19, 14, 30, 10, 0, time.Local) // Sunday group := NodeActionGroup{ Schedule: ActionGroupSchedule{ Enabled: true, Kind: ActionGroupScheduleTimes, Times: []ActionGroupTimeSpec{ {Time: "14:30", DaysOfWeek: []int{0}}, }, }, } if !IsScheduleDue(group, now) { t.Fatal("expected due at matching time") } sameMinute := now group.LastRunAt = &sameMinute if IsScheduleDue(group, now) { t.Fatal("expected not due again in same minute") } group.LastRunAt = nil group.Schedule.Times[0].DaysOfWeek = []int{1} // Monday only if IsScheduleDue(group, now) { t.Fatal("expected not due on wrong weekday") } } func TestActionLogAppendAndList(t *testing.T) { dir := t.TempDir() record := ActionGroupRunRecord{ ID: "run-1", NodeID: "node-abc", GroupID: "group-1", GroupName: "Nightly Jobs", Trigger: ActionRunTriggerManual, StartedAt: time.Now().UTC(), FinishedAt: time.Now().UTC(), Actions: []ActionItemRunResult{}, } if err := AppendActionLogRun(dir, record); err != nil { t.Fatalf("AppendActionLogRun: %v", err) } path := ActionLogFilePath(dir, "node-abc", "Nightly Jobs") if _, err := os.Stat(path); err != nil { t.Fatalf("expected log file at %s: %v", path, err) } files, err := ListActionLogFiles(dir, "node-abc") if err != nil { t.Fatalf("ListActionLogFiles: %v", err) } if len(files) != 1 { t.Fatalf("files = %#v", files) } wantName := DiskSafeFilename("Nightly Jobs") + ".json" if files[0].Filename != wantName { t.Fatalf("filename = %q want %q", files[0].Filename, wantName) } loaded, err := ReadActionLogFileByName(dir, "node-abc", wantName) if err != nil { t.Fatalf("ReadActionLogFileByName: %v", err) } if len(loaded.Runs) != 1 || loaded.Runs[0].ID != "run-1" { t.Fatalf("loaded = %#v", loaded) } if _, err := ReadActionLogFileByName(dir, "node-abc", "../etc/passwd"); err == nil { t.Fatal("expected path traversal rejected") } _ = filepath.Base(wantName) }