package dockerctl import ( "context" "strings" "testing" ) type fakeRunner struct { calls [][]string output []byte err error } func (f *fakeRunner) Run(_ context.Context, name string, args ...string) ([]byte, error) { f.calls = append(f.calls, append([]string{name}, args...)) return f.output, f.err } func TestRecreateUsesAllowlistedComposeMetadata(t *testing.T) { f := &fakeRunner{output: []byte("ok")} ctl := Controller{Runner: f} target := Target{ContainerName: "agent", AllowedActions: []string{"recreate"}, ProjectDir: "/srv/stack", ComposeFiles: []string{"/srv/stack/compose.yml"}, ComposeService: "agent", ComposeProject: "stack", EnvFile: "/srv/stack/.env"} result := ctl.Execute(context.Background(), target, "recreate") if !result.Success { t.Fatalf("unexpected failure: %s", result.Error) } joined := strings.Join(f.calls[0], " ") for _, want := range []string{"docker compose", "--env-file /srv/stack/.env", "--force-recreate agent"} { if !strings.Contains(joined, want) { t.Fatalf("call %q missing %q", joined, want) } } } func TestRejectsNonAllowlistedAction(t *testing.T) { ctl := Controller{Runner: &fakeRunner{}} result := ctl.Execute(context.Background(), Target{ContainerName: "agent", AllowedActions: []string{"restart"}}, "recreate") if result.Success || result.Error == "" { t.Fatal("expected rejection") } } func TestValidateTargetsSetsDefaultAction(t *testing.T) { targets := []Target{{ContainerName: "agent", AllowedActions: []string{"recreate"}}} if err := ValidateTargets(targets); err != nil { t.Fatal(err) } if targets[0].DefaultAction != "recreate" { t.Fatalf("default action not set: %#v", targets[0]) } }