package cli import ( "bytes" "encoding/json" "os" "path/filepath" "strings" "testing" "git.cloudinit.dev/coreci/orca/internal/paths" ) // resetNSFlags zeroes the ns subcommand flag-bound vars so tests don't // leak state. func resetNSFlags() { nsCreateParent = "" nsCreateInheritsEnv = true nsCreateInheritsSecret = true nsInheritParent = "" } func writeDefaultsNS(t *testing.T, root string) { t.Helper() nsDir := filepath.Join(root, "_defaults") if err := os.MkdirAll(nsDir, 0o755); err != nil { t.Fatalf("mkdir _defaults: %v", err) } body := "---\nkind: Namespace\nname: _defaults\ninherits_env: true\ninherits_secrets: true\n---\n# defaults\n" if err := os.WriteFile(filepath.Join(nsDir, "ns.md"), []byte(body), 0o644); err != nil { t.Fatalf("write _defaults ns.md: %v", err) } } func writeCustomNS(t *testing.T, root, name, parentsList string) { t.Helper() nsDir := filepath.Join(root, name) if err := os.MkdirAll(nsDir, 0o755); err != nil { t.Fatalf("mkdir %s: %v", name, err) } body := "---\nkind: Namespace\nname: " + name + "\n" if parentsList != "" { body += "parents: " + parentsList + "\n" } body += "inherits_env: true\ninherits_secrets: true\n---\n# " + name + "\n" if err := os.WriteFile(filepath.Join(nsDir, "ns.md"), []byte(body), 0o644); err != nil { t.Fatalf("write %s ns.md: %v", name, err) } } func TestNSListEmpty(t *testing.T) { t.Setenv("ORCA_HOME", t.TempDir()) resetRootFlags(t) resetNSFlags() rootCmd.SetArgs([]string{"ns", "list"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns list: %v", err) } } func TestNSListWithNamespaces(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") rootCmd.SetArgs([]string{"ns", "list"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns list: %v", err) } } func TestNSCreateHappy(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() rootCmd.SetArgs([]string{"ns", "create", "prod"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns create: %v", err) } if _, err := os.Stat(filepath.Join(root, "prod", "ns.md")); err != nil { t.Fatalf("ns.md not created: %v", err) } for _, sub := range []string{"db", "jobs", "alloc"} { if _, err := os.Stat(filepath.Join(root, "prod", sub)); err != nil { t.Errorf("subdir %s not created: %v", sub, err) } } } func TestNSCreateWithParent(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "base", "") rootCmd.SetArgs([]string{"ns", "create", "child", "--parent", "base"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns create: %v", err) } data, err := os.ReadFile(filepath.Join(root, "child", "ns.md")) if err != nil { t.Fatalf("read ns.md: %v", err) } if !strings.Contains(string(data), "parents: [") || !strings.Contains(string(data), "\"base\"") { t.Errorf("ns.md missing parents: %s", string(data)) } } func TestNSCreateExisting(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") rootCmd.SetArgs([]string{"ns", "create", "prod"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected error creating existing namespace, got nil") } if !strings.Contains(err.Error(), "already exists") { t.Errorf("error = %q, want contains 'already exists'", err.Error()) } } func TestNSCreateDefaultsRefused(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() rootCmd.SetArgs([]string{"ns", "create", "_defaults"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected error creating _defaults, got nil") } } func TestNSCreateClusterRefused(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() rootCmd.SetArgs([]string{"ns", "create", "cluster"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected error creating cluster, got nil") } } func TestNSDeleteHappy(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") rootCmd.SetArgs([]string{"ns", "delete", "prod"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns delete: %v", err) } if _, err := os.Stat(filepath.Join(root, "prod")); !os.IsNotExist(err) { t.Errorf("prod dir still exists after delete") } } func TestNSDeleteDefaultsRefused(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) rootCmd.SetArgs([]string{"ns", "delete", "_defaults"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected error deleting _defaults, got nil") } } func TestNSDeleteNonEmpty(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") // put a job in jobs/ if err := os.MkdirAll(filepath.Join(root, "prod", "jobs"), 0o755); err != nil { t.Fatalf("mkdir jobs: %v", err) } if err := os.WriteFile(filepath.Join(root, "prod", "jobs", "j1.md"), []byte("x"), 0o644); err != nil { t.Fatalf("write job: %v", err) } rootCmd.SetArgs([]string{"ns", "delete", "prod"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected error deleting non-empty namespace, got nil") } if !strings.Contains(err.Error(), "refusing to delete") { t.Errorf("error = %q, want contains 'refusing to delete'", err.Error()) } } func TestNSDeleteMissing(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) rootCmd.SetArgs([]string{"ns", "delete", "ghost"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected error deleting missing namespace, got nil") } } func TestNSInspectHappy(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") rootCmd.SetArgs([]string{"ns", "inspect", "prod"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns inspect: %v", err) } } func TestNSInspectJSON(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetArgs([]string{"ns", "inspect", "prod", "--json"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns inspect --json: %v", err) } var result map[string]any if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil { t.Fatalf("unmarshal: %v\n%s", err, buf.String()) } if result["name"] != "prod" { t.Errorf("name = %v, want prod", result["name"]) } chain, _ := result["chain"].([]any) if len(chain) < 2 { t.Errorf("chain too short: %v", chain) } } func TestNSInspectMissingNamespace(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) rootCmd.SetArgs([]string{"ns", "inspect", "ghost"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected error for missing namespace, got nil") } } func TestNSValidateHappy(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") rootCmd.SetArgs([]string{"ns", "validate", "prod"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns validate: %v", err) } } func TestNSValidateCycle(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) // a -> b, b -> a (cycle) writeCustomNS(t, root, "a", "[\"b\"]") writeCustomNS(t, root, "b", "[\"a\"]") rootCmd.SetArgs([]string{"ns", "validate", "a"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected cycle error, got nil") } if !strings.Contains(err.Error(), "cycle") { t.Errorf("error = %q, want contains 'cycle'", err.Error()) } } func TestNSValidateMissingParent(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "x", "[\"ghost\"]") rootCmd.SetArgs([]string{"ns", "validate", "x"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected missing-parent error, got nil") } if !strings.Contains(err.Error(), "ghost") || !strings.Contains(err.Error(), "not found") { t.Errorf("error = %q, want contains ghost + not found", err.Error()) } } func TestNSValidateMissingNamespace(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) rootCmd.SetArgs([]string{"ns", "validate", "ghost"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected error for missing namespace, got nil") } } func TestRenderNSMd(t *testing.T) { body := renderNSMd("foo", []string{"_defaults"}, true, false) if !strings.Contains(body, "kind: Namespace") { t.Errorf("missing kind: %s", body) } if !strings.Contains(body, "name: foo") { t.Errorf("missing name: %s", body) } if !strings.Contains(body, "inherits_env: true") { t.Errorf("missing inherits_env true: %s", body) } if !strings.Contains(body, "inherits_secrets: false") { t.Errorf("missing inherits_secrets false: %s", body) } } func TestNSRootRegistered(t *testing.T) { found := false for _, c := range rootCmd.Commands() { if c.Use == "ns" { found = true break } } if !found { t.Errorf("ns command not registered on root") } // ensure subcommands present sub := map[string]bool{} for _, c := range rootCmd.Commands() { if c.Use == "ns" { for _, sc := range c.Commands() { sub[sc.Use] = true } } } for _, want := range []string{"list", "create ", "delete ", "inspect ", "validate ", "inherit ", "set-constraint ="} { if !sub[want] { t.Errorf("missing ns subcommand %q", want) } } } func TestNSListNoORCAHOME(t *testing.T) { // ORCA_HOME points at a nonexistent dir; list should error. t.Setenv("ORCA_HOME", filepath.Join(t.TempDir(), "nope")) resetRootFlags(t) resetNSFlags() rootCmd.SetArgs([]string{"ns", "list"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected error for missing ORCA_HOME, got nil") } } var _ = paths.DefaultNamespace // keep paths import alive func TestNSInheritSetsParent(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") rootCmd.SetArgs([]string{"ns", "inherit", "prod", "--parent", "_defaults"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns inherit: %v", err) } data, err := os.ReadFile(filepath.Join(root, "prod", "ns.md")) if err != nil { t.Fatalf("read ns.md: %v", err) } if !strings.Contains(string(data), "parents:") || !strings.Contains(string(data), "_defaults") { t.Errorf("ns.md missing parents: %s", string(data)) } } func TestNSInheritCycleRefused(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) // a -> b already; now set a's parent to b, then try b -> a. writeCustomNS(t, root, "a", `["b"]`) writeCustomNS(t, root, "b", "") resetNSFlags() rootCmd.SetArgs([]string{"ns", "inherit", "b", "--parent", "a"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected cycle error, got nil") } if !strings.Contains(err.Error(), "cycle") { t.Errorf("error = %q, want contains 'cycle'", err.Error()) } } func TestNSInheritSelfRefused(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") rootCmd.SetArgs([]string{"ns", "inherit", "prod", "--parent", "prod"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected self-inherit error, got nil") } } func TestNSInheritDefaultsRefused(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") rootCmd.SetArgs([]string{"ns", "inherit", "_defaults", "--parent", "prod"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected error setting parent on _defaults, got nil") } } func TestNSSetConstraint(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") rootCmd.SetArgs([]string{"ns", "set-constraint", "prod", "max-allocs=10"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns set-constraint: %v", err) } data, err := os.ReadFile(filepath.Join(root, "prod", "ns.md")) if err != nil { t.Fatalf("read ns.md: %v", err) } if !strings.Contains(string(data), "constraints:") || !strings.Contains(string(data), "max-allocs=10") { t.Errorf("ns.md missing constraints: %s", string(data)) } } func TestNSSetConstraintValidatePasses(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") rootCmd.SetArgs([]string{"ns", "set-constraint", "prod", "max-allocs=10"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns set-constraint: %v", err) } resetNSFlags() rootCmd.SetArgs([]string{"ns", "validate", "prod"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns validate after set-constraint: %v", err) } } func TestNSSetConstraintInspectShowsConstraint(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") rootCmd.SetArgs([]string{"ns", "set-constraint", "prod", "max-allocs=10"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns set-constraint: %v", err) } resetNSFlags() var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetArgs([]string{"ns", "inspect", "prod", "--json"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns inspect --json: %v", err) } var result map[string]any if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil { t.Fatalf("unmarshal: %v\n%s", err, buf.String()) } cons, _ := result["constraints"].([]any) found := false for _, c := range cons { if c == "max-allocs=10" { found = true } } if !found { t.Errorf("constraint max-allocs=10 not in inspect output: %v", cons) } } func TestNSSetConstraintInvalidFormat(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") rootCmd.SetArgs([]string{"ns", "set-constraint", "prod", "noequals"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected error for malformed constraint, got nil") } if !strings.Contains(err.Error(), "=") { t.Errorf("error = %q, want contains '='", err.Error()) } } func TestNSSetConstraintDuplicate(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") rootCmd.SetArgs([]string{"ns", "set-constraint", "prod", "max-allocs=10"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("ns set-constraint first: %v", err) } resetNSFlags() rootCmd.SetArgs([]string{"ns", "set-constraint", "prod", "max-allocs=10"}) err := rootCmd.Execute() if err == nil { t.Fatal("expected duplicate error, got nil") } if !strings.Contains(err.Error(), "already set") { t.Errorf("error = %q, want contains 'already set'", err.Error()) } } // --- REQ-120 / F4 path traversal regression tests --- // TestNSCreateTraversalRefused verifies that ns create rejects names // that would traverse outside ORCA_HOME via ".." or "/". func TestNSCreateTraversalRefused(t *testing.T) { bad := []string{ "..", "../etc", "foo/../bar", "/etc", "etc/", "foo/bar", "-x", "--flag", "with space", "tab\there", "newline\nname", } for _, name := range bad { t.Run(name, func(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() rootCmd.SetArgs([]string{"ns", "create", name}) err := rootCmd.Execute() if err == nil { t.Errorf("ns create %q should fail, got nil", name) } // Verify no directory was created outside ORCA_HOME. // For ".." and "../etc", the danger is a dir was created // outside root. Check root's parent has no new orca dirs. parent := filepath.Dir(root) entries, _ := os.ReadDir(parent) for _, e := range entries { // The temp dir itself is fine; anything else that looks // like an orca namespace (has ns.md) outside root is a // leak. if e.Name() == filepath.Base(root) { continue } if _, err := os.Stat(filepath.Join(parent, e.Name(), "ns.md")); err == nil { t.Errorf("namespace dir leaked outside ORCA_HOME: %s", filepath.Join(parent, e.Name())) } } }) } } // TestNSInheritTraversalRefused verifies --parent rejects traversal. func TestNSInheritTraversalRefused(t *testing.T) { root := t.TempDir() t.Setenv("ORCA_HOME", root) resetRootFlags(t) resetNSFlags() writeDefaultsNS(t, root) writeCustomNS(t, root, "prod", "") rootCmd.SetArgs([]string{"ns", "inherit", "prod", "--parent", "../../etc"}) err := rootCmd.Execute() if err == nil { t.Fatal("ns inherit with traversal --parent should fail") } }