diff --git a/src/pkg/config/checkout_root_test.go b/src/pkg/config/checkout_root_test.go new file mode 100644 index 000000000..846abbc5b --- /dev/null +++ b/src/pkg/config/checkout_root_test.go @@ -0,0 +1,52 @@ +package config + +import ( + "path/filepath" + "testing" +) + +// TestCheckoutRootFor pins ProjectConfig.CheckoutRootFor (config.go), which +// maps a monitored repo to its host-local checkout root (kubestellar/hive#5227) +// and was previously untested at 0% coverage. The traversal guard matters: +// the result is a filesystem path built from config strings, and a name of +// ".." or one carrying a separator must never escape CheckoutsDir. +func TestCheckoutRootFor(t *testing.T) { + cases := []struct { + name string + dir string + repo string + want string + }{ + {"bare repo name", "/data/checkouts", "hive", filepath.Join("/data/checkouts", "hive")}, + {"org-qualified slug uses name only", "/data/checkouts", "kubestellar/hive", filepath.Join("/data/checkouts", "hive")}, + {"deep slug uses last segment", "/data/checkouts", "gitlab.com/group/sub/repo", filepath.Join("/data/checkouts", "repo")}, + {"empty checkouts dir is a no-op", "", "hive", ""}, + {"whitespace-only checkouts dir is a no-op", " ", "hive", ""}, + {"empty repo is a no-op", "/data/checkouts", "", ""}, + {"slug with trailing slash has no name", "/data/checkouts", "kubestellar/", ""}, + {"dot name refused", "/data/checkouts", ".", ""}, + {"dotdot name refused", "/data/checkouts", "..", ""}, + {"org-qualified dotdot refused", "/data/checkouts", "kubestellar/..", ""}, + {"backslash in name refused", "/data/checkouts", `evil\name`, ""}, + {"org-qualified backslash refused", "/data/checkouts", `org/..\evil`, ""}, + {"surrounding whitespace trimmed", " /data/checkouts ", " hive ", filepath.Join("/data/checkouts", "hive")}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + p := &ProjectConfig{CheckoutsDir: tc.dir} + if got := p.CheckoutRootFor(tc.repo); got != tc.want { + t.Fatalf("CheckoutRootFor(%q) with CheckoutsDir=%q = %q, want %q", tc.repo, tc.dir, got, tc.want) + } + }) + } +} + +// A refused or unconfigured lookup must return exactly "" — the documented +// no-op sentinel — never a partial path the caller might join or stat. +func TestCheckoutRootFor_NoOpIsEmptyString(t *testing.T) { + p := &ProjectConfig{} + if got := p.CheckoutRootFor("kubestellar/hive"); got != "" { + t.Fatalf("unconfigured CheckoutRootFor = %q, want empty string", got) + } +} diff --git a/src/pkg/dashboard/terminal_urls_handler_test.go b/src/pkg/dashboard/terminal_urls_handler_test.go new file mode 100644 index 000000000..b47cc36a7 --- /dev/null +++ b/src/pkg/dashboard/terminal_urls_handler_test.go @@ -0,0 +1,69 @@ +package dashboard + +import ( + "encoding/json" + "net/http" + "testing" +) + +// These tests pin the HTTP contract of handleAgentTerminalURLs (#5188), +// previously at 0% coverage: the pipeline behind it (prepareTerminalURLs, +// filterAuthURLs) is tested in terminal_urls_test.go, but the handler's own +// branches — nil deps, the "no pane is not an error" contract, and the +// no-store header — were exercised nowhere. + +func decodeTerminalURLs(t *testing.T, body []byte) (urls, authURLs []string) { + t.Helper() + var resp struct { + URLs []string `json:"urls"` + AuthURLs []string `json:"authUrls"` + } + if err := json.Unmarshal(body, &resp); err != nil { + t.Fatalf("response is not the documented JSON shape: %v (body %q)", err, body) + } + if resp.URLs == nil || resp.AuthURLs == nil { + t.Fatalf("urls/authUrls must be empty lists, never null: body %q", body) + } + return resp.URLs, resp.AuthURLs +} + +// With no agent manager wired, the endpoint reports 503 rather than panicking +// on a nil dereference — same contract as handleAgentFullLog. +func TestHandleAgentTerminalURLs_NoManager(t *testing.T) { + s, _ := apiServer(t) + s.deps.AgentMgr = nil + rec := doGet(s, "/api/agents/scanner/terminal-urls") + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("status = %d, want 503 when AgentMgr is nil", rec.Code) + } +} + +// A known-but-not-running agent has no tmux pane to read. Unlike the full-log +// endpoint (which 404s), this endpoint documents "nothing to copy right now" +// as a normal state: 200 with empty lists, so the dashboard hides the control +// instead of surfacing an error. +func TestHandleAgentTerminalURLs_NoActiveSessionIsEmptyNotError(t *testing.T) { + s, _ := apiServer(t) + rec := doGet(s, "/api/agents/scanner/terminal-urls") + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200 for an agent with no active session", rec.Code) + } + urls, authURLs := decodeTerminalURLs(t, rec.Body.Bytes()) + if len(urls) != 0 || len(authURLs) != 0 { + t.Fatalf("want empty lists for an agent with no pane, got urls=%v authUrls=%v", urls, authURLs) + } +} + +// A nonexistent agent likewise yields empty lists, never an error — the +// handler treats every CaptureFullLog failure as "no pane". +func TestHandleAgentTerminalURLs_UnknownAgentIsEmptyNotError(t *testing.T) { + s, _ := apiServer(t) + rec := doGet(s, "/api/agents/nonexistent/terminal-urls") + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200 for an unknown agent", rec.Code) + } + urls, authURLs := decodeTerminalURLs(t, rec.Body.Bytes()) + if len(urls) != 0 || len(authURLs) != 0 { + t.Fatalf("want empty lists for an unknown agent, got urls=%v authUrls=%v", urls, authURLs) + } +}