package sessions import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" ) // ── helpers ─────────────────────────────────────────────────────────────────── // runningReadyPod returns minimal k8s pod JSON: phase Running + Ready condition. func runningReadyPod(name string) map[string]any { return map[string]any{ "metadata": map[string]any{"name": name}, "status": map[string]any{ "phase": "Running", "conditions": []any{ map[string]any{"type": "Ready", "status": "True"}, }, }, } } // respondK8sJSON writes a JSON body with the given status code. func respondK8sJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(body) //nolint:errcheck } // newTestSM builds a SandboxManager pointed at the given test server URL. func newTestSM(url string) *SandboxManager { return &SandboxManager{ k8sHost: url, k8sClient: &http.Client{Timeout: 5 * time.Second}, pvcSize: "1Gi", readyTimeout: 3 * time.Second, } } // ── TestSandboxCreateOrResume_CreateNew ─────────────────────────────────────── // No existing resources → creates PVC, NetworkPolicy, Pod, Service; Resumed=false. func TestSandboxCreateOrResume_CreateNew(t *testing.T) { id := "newid" var podGetCount int32 mux := http.NewServeMux() // PVC: GET → 404, POST → 201 mux.HandleFunc("GET /api/v1/namespaces/poimen/persistentvolumeclaims/{name}", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) }) mux.HandleFunc("POST /api/v1/namespaces/poimen/persistentvolumeclaims", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusCreated) }) // NetworkPolicy (fixed name "pi-session-sandbox"): GET → 404, POST → 201 mux.HandleFunc("GET /apis/networking.k8s.io/v1/namespaces/poimen/networkpolicies/{name}", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) }) mux.HandleFunc("POST /apis/networking.k8s.io/v1/namespaces/poimen/networkpolicies", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusCreated) }) // Pod: first GET (getPodPhase) → 404; second GET (waitPodReady) → Running+Ready; POST → 201 mux.HandleFunc("GET /api/v1/namespaces/poimen/pods/{name}", func(w http.ResponseWriter, _ *http.Request) { if atomic.AddInt32(&podGetCount, 1) == 1 { w.WriteHeader(http.StatusNotFound) return } respondK8sJSON(w, http.StatusOK, runningReadyPod(podName(id))) }) mux.HandleFunc("POST /api/v1/namespaces/poimen/pods", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusCreated) }) // Service: GET → 404, POST → 201 mux.HandleFunc("GET /api/v1/namespaces/poimen/services/{name}", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) }) mux.HandleFunc("POST /api/v1/namespaces/poimen/services", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusCreated) }) srv := httptest.NewServer(mux) defer srv.Close() result, err := newTestSM(srv.URL).CreateOrResume(context.Background(), id) if err != nil { t.Fatalf("CreateOrResume: %v", err) } if want := svcDNS(id); result.SvcDNS != want { t.Errorf("SvcDNS: want %q, got %q", want, result.SvcDNS) } if result.Resumed { t.Error("want Resumed=false, got true") } } // ── TestSandboxCreateOrResume_Resume ───────────────────────────────────────── // PVC exists + pod Running → Resumed=true, no POST calls. func TestSandboxCreateOrResume_Resume(t *testing.T) { id := "resumeid" mux := http.NewServeMux() // PVC exists mux.HandleFunc("GET /api/v1/namespaces/poimen/persistentvolumeclaims/{name}", func(w http.ResponseWriter, _ *http.Request) { respondK8sJSON(w, http.StatusOK, map[string]any{"metadata": map[string]any{"name": pvcName(id)}}) }) // Pod exists and Running (getPodPhase uses GET /{name}) mux.HandleFunc("GET /api/v1/namespaces/poimen/pods/{name}", func(w http.ResponseWriter, _ *http.Request) { respondK8sJSON(w, http.StatusOK, map[string]any{ "metadata": map[string]any{"name": podName(id)}, "status": map[string]any{"phase": "Running"}, }) }) // Any POST → 500 to assert no creates are issued mux.HandleFunc("POST /", func(w http.ResponseWriter, r *http.Request) { t.Errorf("unexpected POST to %s", r.URL.Path) w.WriteHeader(http.StatusInternalServerError) }) srv := httptest.NewServer(mux) defer srv.Close() result, err := newTestSM(srv.URL).CreateOrResume(context.Background(), id) if err != nil { t.Fatalf("CreateOrResume: %v", err) } if !result.Resumed { t.Error("want Resumed=true, got false") } if want := svcDNS(id); result.SvcDNS != want { t.Errorf("SvcDNS: want %q, got %q", want, result.SvcDNS) } } // ── TestSandboxDelete ───────────────────────────────────────────────────────── // All DELETEs succeed → no error returned. func TestSandboxDelete(t *testing.T) { id := "deleteid" mux := http.NewServeMux() mux.HandleFunc("DELETE /api/v1/namespaces/poimen/pods/{name}", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) mux.HandleFunc("DELETE /api/v1/namespaces/poimen/services/{name}", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) mux.HandleFunc("DELETE /api/v1/namespaces/poimen/persistentvolumeclaims/{name}", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) srv := httptest.NewServer(mux) defer srv.Close() if err := newTestSM(srv.URL).Delete(context.Background(), id); err != nil { t.Errorf("Delete: %v", err) } } // ── TestSandboxDelete_NotFound ──────────────────────────────────────────────── // All DELETEs return 404 → isNotFound errors swallowed → no error returned. func TestSandboxDelete_NotFound(t *testing.T) { id := "ghostid" mux := http.NewServeMux() mux.HandleFunc("DELETE /api/v1/namespaces/poimen/pods/{name}", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) }) mux.HandleFunc("DELETE /api/v1/namespaces/poimen/services/{name}", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) }) mux.HandleFunc("DELETE /api/v1/namespaces/poimen/persistentvolumeclaims/{name}", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) }) srv := httptest.NewServer(mux) defer srv.Close() if err := newTestSM(srv.URL).Delete(context.Background(), id); err != nil { t.Errorf("Delete_NotFound: want nil, got %v", err) } } // ── TestSandboxReapIdlePods ─────────────────────────────────────────────────── // pod-a=Succeeded → deleted; pod-b=Running → skipped. func TestSandboxReapIdlePods(t *testing.T) { var deletedA int32 mux := http.NewServeMux() // List all pi-session pods (query string not part of mux path matching) mux.HandleFunc("GET /api/v1/namespaces/poimen/pods", func(w http.ResponseWriter, _ *http.Request) { respondK8sJSON(w, http.StatusOK, map[string]any{ "items": []any{ map[string]any{ "metadata": map[string]any{"name": "pod-a"}, "status": map[string]any{"phase": "Succeeded"}, }, map[string]any{ "metadata": map[string]any{"name": "pod-b"}, "status": map[string]any{"phase": "Running"}, }, }, }) }) mux.HandleFunc("DELETE /api/v1/namespaces/poimen/pods/{name}", func(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") if name == "pod-b" { t.Errorf("pod-b (Running) must NOT be deleted") } if name == "pod-a" { atomic.AddInt32(&deletedA, 1) } w.WriteHeader(http.StatusOK) }) srv := httptest.NewServer(mux) defer srv.Close() newTestSM(srv.URL).ReapIdlePods(context.Background()) if got := atomic.LoadInt32(&deletedA); got != 1 { t.Errorf("pod-a delete count: want 1, got %d", got) } } // ── TestSandboxNameHelpers ──────────────────────────────────────────────────── func TestSandboxNameHelpers(t *testing.T) { cases := []struct{ fn, want, got string }{ {"pvcName", "workspace-session-abc", pvcName("abc")}, {"podName", "pi-session-abc", podName("abc")}, {"svcName", "pi-session-abc", svcName("abc")}, {"svcDNS", "pi-session-abc.poimen.svc.cluster.local:8787", svcDNS("abc")}, } for _, c := range cases { if c.got != c.want { t.Errorf("%s: want %q, got %q", c.fn, c.want, c.got) } } } // ── TestSandboxIsNotFound ───────────────────────────────────────────────────── func TestSandboxIsNotFound(t *testing.T) { if !isNotFound(&k8sNotFound{}) { t.Error("isNotFound(&k8sNotFound{}) must be true") } if isNotFound(fmt.Errorf("other")) { t.Error("isNotFound(fmt.Errorf) must be false") } if isNotFound(nil) { t.Error("isNotFound(nil) must be false") } }