Files
ClusterCanvas/service/internal/settings/node_action_groups_test.go
T

203 lines
5.1 KiB
Go

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 TestLibraryActionInUse(t *testing.T) {
store := NodeActionGroupStore{
Groups: []NodeActionGroup{
{
ID: "g1",
NodeID: "n1",
Name: "Nightly",
Items: []NodeActionItem{
{
ID: "lib-item",
Source: ActionItemSourceLibrary,
LibraryActionID: "custom-1",
},
{
ID: "local-item",
Source: ActionItemSourceLocal,
Name: "Echo",
Kind: ActionKindShell,
Body: "echo hi",
},
},
},
},
}
if !LibraryActionInUse(store, "custom-1") {
t.Fatal("expected library action custom-1 to be in use")
}
if LibraryActionInUse(store, "other") {
t.Fatal("expected unrelated action not in use")
}
store.Groups[0].Items[0] = NodeActionItem{
ID: "cloned",
Source: ActionItemSourceLocal,
Name: "Echo host",
Kind: ActionKindShell,
Body: "echo",
}
if LibraryActionInUse(store, "custom-1") {
t.Fatal("expected cloned local item not to count as in use")
}
}
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)
}