package stacks import ( "context" "os" "path/filepath" "strings" "testing" ) func TestPathRejectsTraversal(t *testing.T) { s, err := New(t.TempDir()) if err != nil { t.Fatal(err) } for _, name := range []string{"../x", "/tmp/x", "a/b", ""} { if _, err := s.path(name); err == nil { t.Fatalf("expected %q to be rejected", name) } } if _, err := s.path("my-stack_1.2"); err != nil { t.Fatalf("valid name rejected: %v", err) } } func TestSaveStagesEnvAndSecretsBeforeComposeValidation(t *testing.T) { root := t.TempDir() bin := t.TempDir() docker := filepath.Join(bin, "docker") script := `#!/bin/sh set -eu [ -f .env ] [ -f secrets/api_key ] [ "$(cat secrets/api_key)" = "supersecret" ] exit 0 ` if err := os.WriteFile(docker, []byte(script), 0755); err != nil { t.Fatal(err) } t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) s, err := New(root) if err != nil { t.Fatal(err) } in := SaveInput{Compose: "services:\n app:\n image: nginx:alpine\n env_file: .env\nsecrets:\n api_key:\n file: ./secrets/api_key\n", Env: "A=B\n", Secrets: []SecretFile{{Name: "api_key", Content: "supersecret"}}} if err := s.Save(context.Background(), "demo", in); err != nil { t.Fatalf("save: %v", err) } if _, err := os.Stat(filepath.Join(root, "demo", "compose.yaml")); err != nil { t.Fatal(err) } if b, err := os.ReadFile(filepath.Join(root, "demo", "secrets", "api_key")); err != nil || string(b) != "supersecret" { t.Fatalf("secret not committed: %q %v", string(b), err) } } func TestSaveReportsDockerExecErrorWhenComposeIsSilent(t *testing.T) { root := t.TempDir() bin := t.TempDir() docker := filepath.Join(bin, "docker") if err := os.WriteFile(docker, []byte("#!/bin/sh\nexit 7\n"), 0755); err != nil { t.Fatal(err) } t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) s, _ := New(root) err := s.Save(context.Background(), "demo", SaveInput{Compose: "services:\n app:\n image: nginx\n"}) if err == nil || !strings.Contains(err.Error(), "exit status 7") { t.Fatalf("expected useful exit error, got %v", err) } } func TestDescriptorDigestPrefersDescriptor(t *testing.T) { v := map[string]any{"Descriptor": map[string]any{"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"} got := descriptorDigest(v) if got != "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" { t.Fatalf("got %q", got) } } func TestDeletePreservesUnmanagedStackDataByDefault(t *testing.T) { root := t.TempDir() s, err := New(root) if err != nil { t.Fatal(err) } dir := filepath.Join(root, "demo") if err := os.MkdirAll(filepath.Join(dir, "data"), 0750); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "compose.yaml"), []byte("services: {}\n"), 0640); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "data", "important.db"), []byte("keep"), 0640); err != nil { t.Fatal(err) } if err := s.Delete(context.Background(), "demo", false, false); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(dir, "compose.yaml")); !os.IsNotExist(err) { t.Fatalf("managed compose file should be removed, got %v", err) } if b, err := os.ReadFile(filepath.Join(dir, "data", "important.db")); err != nil || string(b) != "keep" { t.Fatalf("unmanaged bind-mount data was damaged: %q %v", b, err) } } func TestSafeDockerPositionalRejectsOptionLikeValues(t *testing.T) { for _, v := range []string{"--help", "-f", "bad\nvalue"} { if _, err := safeDockerPositional(v, "target"); err == nil { t.Fatalf("expected %q to be rejected", v) } } if got, err := safeDockerPositional("sha256:abc", "target"); err != nil || got != "sha256:abc" { t.Fatalf("valid target rejected: %q %v", got, err) } } func TestExecRejectsOptionLikeServiceName(t *testing.T) { s, err := New(t.TempDir()) if err != nil { t.Fatal(err) } if _, err := s.Exec(context.Background(), "demo", ExecInput{Service: "--index", Command: "id"}); err == nil { t.Fatal("expected invalid service name to be rejected before invoking Docker") } } func TestStackPathRejectsSymlinkDirectory(t *testing.T) { root := t.TempDir() outside := t.TempDir() if err := os.Symlink(outside, filepath.Join(root, "demo")); err != nil { t.Skipf("symlinks unavailable: %v", err) } s, err := New(root) if err != nil { t.Fatal(err) } if _, err := s.path("demo"); err == nil { t.Fatal("expected symlink stack directory to be rejected") } }