package orchestrator import ( "context" "encoding/base64" "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "scm.bstein.dev/bstein/ananke/internal/cluster" "scm.bstein.dev/bstein/ananke/internal/config" ) func testSecretJSON(username, password string) string { return fmt.Sprintf( `{"data":{"username":"%s","password":"%s"}}`, base64.StdEncoding.EncodeToString([]byte(username)), base64.StdEncoding.EncodeToString([]byte(password)), ) } func authSettings(baseURL string) config.ServiceChecklistAuthSettings { return config.ServiceChecklistAuthSettings{ Mode: "keycloak_robotuser", KeycloakBaseURL: baseURL, Realm: "atlas", RobotUsername: "robotuser", AdminSecretNamespace: "sso", AdminSecretName: "keycloak-admin", AdminSecretUsernameKey: "username", AdminSecretPasswordKey: "password", } } func serveMockKeycloakAdminBrowserFlow(w http.ResponseWriter, r *http.Request) bool { switch { case strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte( `
`, )) return true case strings.Contains(r.URL.Path, "/realms/master/login-actions/authenticate"): http.Redirect(w, r, "/admin/master/console/", http.StatusFound) return true case strings.Contains(r.URL.Path, "/admin/master/console/"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("console ok")) return true default: return false } } // TestHookServiceAuthChecklistSuccess runs one orchestration or CLI step. // Signature: TestHookServiceAuthChecklistSuccess(t *testing.T). // Why: validates full robotuser-authenticated checklist flow with final URL and // body markers so startup gates reflect real post-login user behavior. func TestHookServiceAuthChecklistSuccess(t *testing.T) { var appServer *httptest.Server appMux := http.NewServeMux() appMux.HandleFunc("/session/bootstrap", func(w http.ResponseWriter, _ *http.Request) { http.SetCookie(w, &http.Cookie{Name: "robot_session", Value: "ok", Path: "/"}) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("bootstrap ok")) }) appMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { http.Redirect(w, r, "/app/home", http.StatusFound) return } cookie, err := r.Cookie("robot_session") if err != nil || strings.TrimSpace(cookie.Value) == "" { http.Redirect(w, r, "/oauth2/sign_in", http.StatusFound) return } if r.URL.Path == "/app/home" { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("OpenSearch Dashboards")) return } if r.URL.Path == "/oauth2/sign_in" { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("sign in")) return } w.WriteHeader(http.StatusNotFound) }) appServer = httptest.NewTLSServer(appMux) defer appServer.Close() kcMux := http.NewServeMux() kcMux.HandleFunc("/realms/master/protocol/openid-connect/token", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"access_token":"admin-token"}`)) }) kcMux.HandleFunc("/admin/realms/atlas/users", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`[{"id":"robot-id"}]`)) }) kcMux.HandleFunc("/admin/realms/atlas/users/robot-id/impersonation", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(fmt.Sprintf(`{"redirect":"%s/session/bootstrap"}`, appServer.URL))) }) kcMux.HandleFunc("/realms/master/protocol/openid-connect/auth", func(w http.ResponseWriter, r *http.Request) { if serveMockKeycloakAdminBrowserFlow(w, r) { return } }) kcMux.HandleFunc("/realms/master/login-actions/authenticate", func(w http.ResponseWriter, r *http.Request) { if serveMockKeycloakAdminBrowserFlow(w, r) { return } }) kcMux.HandleFunc("/admin/master/console/", func(w http.ResponseWriter, r *http.Request) { if serveMockKeycloakAdminBrowserFlow(w, r) { return } }) kcServer := httptest.NewTLSServer(kcMux) defer kcServer.Close() cfg := lifecycleConfig(t) cfg.Startup.ServiceChecklistAuth = authSettings(kcServer.URL) recorder := &commandRecorder{} base := lifecycleDispatcher(recorder) run := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) { command := name + " " + strings.Join(args, " ") if name == "kubectl" && strings.Contains(command, "-n sso get secret keycloak-admin -o json") { recorder.record(name, args) return testSecretJSON("admin", "password"), nil } return base(ctx, timeout, name, args...) } orch, _ := newHookOrchestrator(t, cfg, run, run) check := config.ServiceChecklistCheck{ Name: "logs-ui-user-session", URL: appServer.URL + "/", AcceptedStatuses: []int{200}, RequireRobotAuth: true, FollowRedirects: true, InsecureSkipTLS: true, FinalURLContains: "/app/home", FinalURLNotContains: "/oauth2/sign_in", BodyContains: "OpenSearch Dashboards", TimeoutSeconds: 5, } ok, detail := orch.TestHookServiceCheckReady(context.Background(), check) if !ok { t.Fatalf("expected authenticated checklist success, detail=%q", detail) } } // TestHookServiceAuthModeAndSecretErrors runs one orchestration or CLI step. // Signature: TestHookServiceAuthModeAndSecretErrors(t *testing.T). // Why: covers auth mode guards and secret decode error branches to keep startup // failures explicit when robot-auth prerequisites are missing. func TestHookServiceAuthModeAndSecretErrors(t *testing.T) { cfg := lifecycleConfig(t) client := &http.Client{Timeout: time.Second} cfgNone := lifecycleConfig(t) cfgNone.Startup.ServiceChecklistAuth.Mode = "none" orchNone, _ := newHookOrchestrator(t, cfgNone, nil, nil) if err := orchNone.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil { t.Fatalf("expected auth mode none to fail") } if _, err := orchNone.TestHookChecklistAuthHTTPClient(context.Background(), time.Second, false); err == nil { t.Fatalf("expected checklist auth client init to fail when mode=none") } cfgBad := lifecycleConfig(t) cfgBad.Startup.ServiceChecklistAuth.Mode = "bad-mode" orchBad, _ := newHookOrchestrator(t, cfgBad, nil, nil) if err := orchBad.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil { t.Fatalf("expected unsupported auth mode to fail") } base := lifecycleDispatcher(&commandRecorder{}) runKubectlErr := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) { if name == "kubectl" { return "", errors.New("kubectl denied") } return base(ctx, timeout, name, args...) } orchKubectlErr, _ := newHookOrchestrator(t, cfg, runKubectlErr, runKubectlErr) if _, err := orchKubectlErr.TestHookKubernetesSecretValue(context.Background(), "sso", "keycloak-admin", "username"); err == nil { t.Fatalf("expected kubectl error branch") } if _, _, err := orchKubectlErr.TestHookKeycloakAdminCredentials(context.Background(), cfg.Startup.ServiceChecklistAuth); err == nil { t.Fatalf("expected keycloakAdminCredentials to fail on username secret lookup") } if err := orchKubectlErr.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil { t.Fatalf("expected auth session failure when secret lookup fails") } runBadJSON := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) { if name == "kubectl" { return "{bad", nil } return base(ctx, timeout, name, args...) } orchBadJSON, _ := newHookOrchestrator(t, cfg, runBadJSON, runBadJSON) if _, err := orchBadJSON.TestHookKubernetesSecretValue(context.Background(), "sso", "keycloak-admin", "username"); err == nil { t.Fatalf("expected secret decode error branch") } runMissingKey := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) { if name == "kubectl" { return `{"data":{"password":"cGFzcw=="}}`, nil } return base(ctx, timeout, name, args...) } orchMissingKey, _ := newHookOrchestrator(t, cfg, runMissingKey, runMissingKey) if _, err := orchMissingKey.TestHookKubernetesSecretValue(context.Background(), "sso", "keycloak-admin", "username"); err == nil { t.Fatalf("expected missing key branch") } if err := orchMissingKey.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil { t.Fatalf("expected auth session failure when username key is missing") } runMissingPassword := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) { if name == "kubectl" { return `{"data":{"username":"YWRtaW4="}}`, nil } return base(ctx, timeout, name, args...) } orchMissingPassword, _ := newHookOrchestrator(t, cfg, runMissingPassword, runMissingPassword) if _, _, err := orchMissingPassword.TestHookKeycloakAdminCredentials(context.Background(), cfg.Startup.ServiceChecklistAuth); err == nil { t.Fatalf("expected keycloakAdminCredentials to fail on password secret lookup") } if err := orchMissingPassword.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil { t.Fatalf("expected auth session failure when password key is missing") } runBadB64 := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) { if name == "kubectl" { return `{"data":{"username":"###"}}`, nil } return base(ctx, timeout, name, args...) } orchBadB64, _ := newHookOrchestrator(t, cfg, runBadB64, runBadB64) if _, err := orchBadB64.TestHookKubernetesSecretValue(context.Background(), "sso", "keycloak-admin", "username"); err == nil { t.Fatalf("expected base64 decode branch") } runEmptyValue := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) { if name == "kubectl" { return `{"data":{"username":"IA=="}}`, nil } return base(ctx, timeout, name, args...) } orchEmptyValue, _ := newHookOrchestrator(t, cfg, runEmptyValue, runEmptyValue) if _, err := orchEmptyValue.TestHookKubernetesSecretValue(context.Background(), "sso", "keycloak-admin", "username"); err == nil { t.Fatalf("expected empty decoded value branch") } if got := cluster.TestHookCompactHTTPBody([]byte(" hello world \n test ")); got != "hello world test" { t.Fatalf("unexpected compact body %q", got) } if got := cluster.TestHookCompactHTTPBody([]byte(" \n\t ")); got != "" { t.Fatalf("expected compact empty body, got %q", got) } if got := cluster.TestHookKeycloakBaseURL(config.ServiceChecklistAuthSettings{KeycloakBaseURL: "https://sso.bstein.dev/"}); got != "https://sso.bstein.dev" { t.Fatalf("unexpected normalized base URL %q", got) } } // TestHookServiceAuthHTTPErrorBranches runs one orchestration or CLI step. // Signature: TestHookServiceAuthHTTPErrorBranches(t *testing.T). // Why: covers token/user/impersonation parser and status branches so startup // diagnostics remain actionable during auth failures. func TestHookServiceAuthHTTPErrorBranches(t *testing.T) { cfg := lifecycleConfig(t) orch, _ := newHookOrchestrator(t, cfg, nil, nil) client := &http.Client{Timeout: 2 * time.Second} authBadURL := authSettings("://bad-url") if _, err := orch.TestHookKeycloakAdminToken(context.Background(), client, authBadURL, "admin", "pw"); err == nil { t.Fatalf("expected request-build failure for bad base URL") } if _, err := orch.TestHookKeycloakRobotUserID(context.Background(), client, authBadURL, "token"); err == nil { t.Fatalf("expected robot-user request-build failure for bad base URL") } if _, err := orch.TestHookKeycloakImpersonationRedirect(context.Background(), client, authBadURL, "token", "robot"); err == nil { t.Fatalf("expected impersonation request-build failure for bad base URL") } authRequestErr := authSettings("http://127.0.0.1:1") if _, err := orch.TestHookKeycloakAdminToken(context.Background(), client, authRequestErr, "admin", "pw"); err == nil { t.Fatalf("expected admin token request error branch") } if _, err := orch.TestHookKeycloakRobotUserID(context.Background(), client, authRequestErr, "token"); err == nil { t.Fatalf("expected robot user request error branch") } if _, err := orch.TestHookKeycloakImpersonationRedirect(context.Background(), client, authRequestErr, "token", "robot"); err == nil { t.Fatalf("expected impersonation request error branch") } kcError := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/token"): w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) case strings.Contains(r.URL.Path, "/users") && strings.Contains(r.URL.RawQuery, "username=robotuser"): w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"error":"boom"}`)) default: w.WriteHeader(http.StatusBadGateway) } })) defer kcError.Close() authError := authSettings(kcError.URL) if _, err := orch.TestHookKeycloakAdminToken(context.Background(), client, authError, "admin", "pw"); err == nil { t.Fatalf("expected non-2xx token branch") } if _, err := orch.TestHookKeycloakRobotUserID(context.Background(), client, authError, "token"); err == nil { t.Fatalf("expected non-2xx robot user branch") } kcDecode := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/token"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("not-json")) case strings.Contains(r.URL.Path, "/users") && strings.Contains(r.URL.RawQuery, "username=robotuser"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("not-json")) case strings.Contains(r.URL.Path, "/impersonation"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("not-json")) default: w.WriteHeader(http.StatusNotFound) } })) defer kcDecode.Close() authDecode := authSettings(kcDecode.URL) if _, err := orch.TestHookKeycloakAdminToken(context.Background(), client, authDecode, "admin", "pw"); err == nil { t.Fatalf("expected token decode error branch") } if _, err := orch.TestHookKeycloakRobotUserID(context.Background(), client, authDecode, "token"); err == nil { t.Fatalf("expected robot user decode error branch") } if _, err := orch.TestHookKeycloakImpersonationRedirect(context.Background(), client, authDecode, "token", "robot"); err == nil { t.Fatalf("expected impersonation decode error branch") } kcMissing := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/token"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"access_token":""}`)) case strings.Contains(r.URL.Path, "/users") && strings.Contains(r.URL.RawQuery, "username=robotuser"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`[]`)) case strings.Contains(r.URL.Path, "/impersonation"): w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"error":"bad request"}`)) default: w.WriteHeader(http.StatusNotFound) } })) defer kcMissing.Close() authMissing := authSettings(kcMissing.URL) if _, err := orch.TestHookKeycloakAdminToken(context.Background(), client, authMissing, "admin", "pw"); err == nil { t.Fatalf("expected missing access_token branch") } if _, err := orch.TestHookKeycloakRobotUserID(context.Background(), client, authMissing, "token"); err == nil { t.Fatalf("expected missing robot user branch") } if _, err := orch.TestHookKeycloakImpersonationRedirect(context.Background(), client, authMissing, "token", "robot"); err == nil { t.Fatalf("expected impersonation non-2xx branch") } } // TestHookKeycloakAdminBrowserLoginAndParserBranches runs one orchestration or CLI step. // Signature: TestHookKeycloakAdminBrowserLoginAndParserBranches(t *testing.T). // Why: covers admin-browser bootstrap and form-action parser branches that // drive robotuser-authenticated startup checks. func TestHookKeycloakAdminBrowserLoginAndParserBranches(t *testing.T) { cfg := lifecycleConfig(t) orch, _ := newHookOrchestrator(t, cfg, nil, nil) client := &http.Client{Timeout: 2 * time.Second} t.Run("invalid base url build failure", func(t *testing.T) { auth := authSettings("://bad-url") if err := orch.TestHookKeycloakAdminBrowserLogin(context.Background(), client, auth, "admin", "pw"); err == nil { t.Fatalf("expected browser login request-build failure") } }) t.Run("auth page non-2xx", func(t *testing.T) { kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth") { w.WriteHeader(http.StatusBadGateway) _, _ = w.Write([]byte("bad gateway")) return } w.WriteHeader(http.StatusNotFound) })) defer kc.Close() if err := orch.TestHookKeycloakAdminBrowserLogin(context.Background(), client, authSettings(kc.URL), "admin", "pw"); err == nil { t.Fatalf("expected non-2xx auth page failure") } }) t.Run("login submit request failure", func(t *testing.T) { kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth") { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(``)) return } w.WriteHeader(http.StatusNotFound) })) defer kc.Close() if err := orch.TestHookKeycloakAdminBrowserLogin(context.Background(), client, authSettings(kc.URL), "admin", "pw"); err == nil { t.Fatalf("expected login submit request failure") } }) t.Run("build login request failure", func(t *testing.T) { kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth") { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(``)) return } w.WriteHeader(http.StatusNotFound) })) defer kc.Close() if err := orch.TestHookKeycloakAdminBrowserLogin(context.Background(), client, authSettings(kc.URL), "admin", "pw"); err == nil { t.Fatalf("expected login request-build failure") } }) t.Run("login submit returns 5xx", func(t *testing.T) { kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(``)) case strings.Contains(r.URL.Path, "/realms/master/login-actions/authenticate"): w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("boom")) default: w.WriteHeader(http.StatusNotFound) } })) defer kc.Close() if err := orch.TestHookKeycloakAdminBrowserLogin(context.Background(), client, authSettings(kc.URL), "admin", "pw"); err == nil { t.Fatalf("expected login 5xx branch") } }) t.Run("login success path", func(t *testing.T) { kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth"): _, _ = w.Write([]byte(``)) case strings.Contains(r.URL.Path, "/realms/master/login-actions/authenticate"): http.Redirect(w, r, "/admin/master/console/", http.StatusFound) case strings.Contains(r.URL.Path, "/admin/master/console/"): _, _ = w.Write([]byte("console ok")) default: w.WriteHeader(http.StatusNotFound) } })) defer kc.Close() if err := orch.TestHookKeycloakAdminBrowserLogin(context.Background(), client, authSettings(kc.URL), "admin", "pw"); err != nil { t.Fatalf("expected browser login success, got %v", err) } }) t.Run("form action parser absolute and invalid", func(t *testing.T) { action, err := cluster.TestHookKeycloakLoginFormAction( ``, "https://sso.bstein.dev", ) if err != nil || action != "https://example.test/login" { t.Fatalf("expected absolute action parse success, action=%q err=%v", action, err) } if _, err := cluster.TestHookKeycloakLoginFormAction( ``, "https://sso.bstein.dev", ); err == nil { t.Fatalf("expected unsupported action format failure") } }) } // TestHookServiceChecklistProbeBranches runs one orchestration or CLI step. // Signature: TestHookServiceChecklistProbeBranches(t *testing.T). // Why: exercises redirect + final-url probe branches, including robot-auth // initialization failures and redirect suppression behavior. func TestHookServiceChecklistProbeBranches(t *testing.T) { cfg := lifecycleConfig(t) cfg.Startup.ServiceChecklistAuth.Mode = "none" orch, _ := newHookOrchestrator(t, cfg, nil, nil) if _, _, _, _, err := orch.TestHookHTTPChecklistProbeWithLocation(context.Background(), config.ServiceChecklistCheck{ URL: "https://example.invalid/", RequireRobotAuth: true, TimeoutSeconds: 1, }); err == nil { t.Fatalf("expected robot auth initialization failure when mode=none") } redirectServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/next", http.StatusFound) })) defer redirectServer.Close() orchNoAuth, _ := newHookOrchestrator(t, lifecycleConfig(t), nil, nil) status, _, location, finalURL, err := orchNoAuth.TestHookHTTPChecklistProbeWithLocation(context.Background(), config.ServiceChecklistCheck{ URL: redirectServer.URL, FollowRedirects: false, TimeoutSeconds: 2, }) if err != nil { t.Fatalf("unexpected redirect probe error: %v", err) } if status != http.StatusFound { t.Fatalf("expected 302 status when redirects disabled, got %d", status) } if !strings.Contains(location, "/next") { t.Fatalf("expected location header for redirect response, got %q", location) } if !strings.Contains(finalURL, redirectServer.URL) { t.Fatalf("expected final URL to remain original request URL, got %q", finalURL) } } // TestHookAuthenticateRobotChecklistSessionFailureStages runs one orchestration or CLI step. // Signature: TestHookAuthenticateRobotChecklistSessionFailureStages(t *testing.T). // Why: drives authenticateRobotChecklistSession through downstream error stages // (robot lookup, impersonation, redirect-build, redirect-request) to maintain // resilient startup diagnostics. func TestHookAuthenticateRobotChecklistSessionFailureStages(t *testing.T) { client := &http.Client{Timeout: 3 * time.Second} recorder := &commandRecorder{} base := lifecycleDispatcher(recorder) secretRun := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) { command := name + " " + strings.Join(args, " ") if name == "kubectl" && strings.Contains(command, "-n sso get secret keycloak-admin -o json") { return testSecretJSON("admin", "password"), nil } return base(ctx, timeout, name, args...) } t.Run("robot-user lookup failure", func(t *testing.T) { kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if serveMockKeycloakAdminBrowserFlow(w, r) { return } switch { case strings.Contains(r.URL.Path, "/token"): _, _ = w.Write([]byte(`{"access_token":"admin-token"}`)) case strings.Contains(r.URL.Path, "/users"): w.WriteHeader(http.StatusBadGateway) _, _ = w.Write([]byte(`{"error":"lookup failed"}`)) default: w.WriteHeader(http.StatusOK) } })) defer kc.Close() cfg := lifecycleConfig(t) cfg.Startup.ServiceChecklistAuth = authSettings(kc.URL) orch, _ := newHookOrchestrator(t, cfg, secretRun, secretRun) if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil { t.Fatalf("expected robot-user lookup failure branch") } }) t.Run("impersonation failure", func(t *testing.T) { kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if serveMockKeycloakAdminBrowserFlow(w, r) { return } switch { case strings.Contains(r.URL.Path, "/token"): _, _ = w.Write([]byte(`{"access_token":"admin-token"}`)) case strings.Contains(r.URL.Path, "/users"): _, _ = w.Write([]byte(`[{"id":"robot-id"}]`)) case strings.Contains(r.URL.Path, "/impersonation"): w.WriteHeader(http.StatusBadGateway) _, _ = w.Write([]byte(`{"error":"impersonation failed"}`)) default: w.WriteHeader(http.StatusOK) } })) defer kc.Close() cfg := lifecycleConfig(t) cfg.Startup.ServiceChecklistAuth = authSettings(kc.URL) orch, _ := newHookOrchestrator(t, cfg, secretRun, secretRun) if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil { t.Fatalf("expected impersonation failure branch") } }) t.Run("redirect url build failure", func(t *testing.T) { kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if serveMockKeycloakAdminBrowserFlow(w, r) { return } switch { case strings.Contains(r.URL.Path, "/token"): _, _ = w.Write([]byte(`{"access_token":"admin-token"}`)) case strings.Contains(r.URL.Path, "/users"): _, _ = w.Write([]byte(`[{"id":"robot-id"}]`)) case strings.Contains(r.URL.Path, "/impersonation"): _, _ = w.Write([]byte(`{"redirect":"://bad"}`)) default: w.WriteHeader(http.StatusOK) } })) defer kc.Close() cfg := lifecycleConfig(t) cfg.Startup.ServiceChecklistAuth = authSettings(kc.URL) orch, _ := newHookOrchestrator(t, cfg, secretRun, secretRun) if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil { t.Fatalf("expected redirect request-build failure branch") } }) t.Run("redirect request failure", func(t *testing.T) { kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if serveMockKeycloakAdminBrowserFlow(w, r) { return } switch { case strings.Contains(r.URL.Path, "/token"): _, _ = w.Write([]byte(`{"access_token":"admin-token"}`)) case strings.Contains(r.URL.Path, "/users"): _, _ = w.Write([]byte(`[{"id":"robot-id"}]`)) case strings.Contains(r.URL.Path, "/impersonation"): _, _ = w.Write([]byte(`{"redirect":"http://127.0.0.1:1/nowhere"}`)) default: w.WriteHeader(http.StatusOK) } })) defer kc.Close() cfg := lifecycleConfig(t) cfg.Startup.ServiceChecklistAuth = authSettings(kc.URL) orch, _ := newHookOrchestrator(t, cfg, secretRun, secretRun) if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil { t.Fatalf("expected redirect request failure branch") } }) t.Run("admin login page missing form action", func(t *testing.T) { kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("no form")) default: w.WriteHeader(http.StatusNotFound) } })) defer kc.Close() cfg := lifecycleConfig(t) cfg.Startup.ServiceChecklistAuth = authSettings(kc.URL) orch, _ := newHookOrchestrator(t, cfg, secretRun, secretRun) if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil { t.Fatalf("expected admin login form parse failure branch") } }) t.Run("admin login remains on login form", func(t *testing.T) { kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte( ``, )) case strings.Contains(r.URL.Path, "/realms/master/login-actions/authenticate"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(``)) default: w.WriteHeader(http.StatusNotFound) } })) defer kc.Close() cfg := lifecycleConfig(t) cfg.Startup.ServiceChecklistAuth = authSettings(kc.URL) orch, _ := newHookOrchestrator(t, cfg, secretRun, secretRun) if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil { t.Fatalf("expected admin login failure branch when login form persists") } }) t.Run("robot bootstrap ends on keycloak login flow", func(t *testing.T) { baseURL := "" kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if serveMockKeycloakAdminBrowserFlow(w, r) { return } switch { case strings.Contains(r.URL.Path, "/token"): _, _ = w.Write([]byte(`{"access_token":"admin-token"}`)) case strings.Contains(r.URL.Path, "/users"): _, _ = w.Write([]byte(`[{"id":"robot-id"}]`)) case strings.Contains(r.URL.Path, "/impersonation"): _, _ = w.Write([]byte(fmt.Sprintf(`{"redirect":"%s/realms/atlas/protocol/openid-connect/auth?client_id=logs"}`, baseURL))) case strings.Contains(r.URL.Path, "/realms/atlas/protocol/openid-connect/auth"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(``)) default: w.WriteHeader(http.StatusNotFound) } })) defer kc.Close() baseURL = kc.URL cfg := lifecycleConfig(t) cfg.Startup.ServiceChecklistAuth = authSettings(kc.URL) orch, _ := newHookOrchestrator(t, cfg, secretRun, secretRun) if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil { t.Fatalf("expected robot bootstrap final-url auth-flow failure") } }) } // TestHookServiceAuthFallbackRedirect runs one orchestration or CLI step. // Signature: TestHookServiceAuthFallbackRedirect(t *testing.T). // Why: covers empty impersonation redirect fallback to realm account URL so // session bootstrap is resilient to Keycloak response shape differences. func TestHookServiceAuthFallbackRedirect(t *testing.T) { kcMux := http.NewServeMux() kcMux.HandleFunc("/realms/master/protocol/openid-connect/token", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"access_token":"admin-token"}`)) }) kcMux.HandleFunc("/admin/realms/atlas/users", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`[{"id":"robot-id"}]`)) }) kcMux.HandleFunc("/admin/realms/atlas/users/robot-id/impersonation", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"redirect":""}`)) }) kcMux.HandleFunc("/realms/atlas/account/", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("account ok")) }) kcMux.HandleFunc("/realms/master/protocol/openid-connect/auth", func(w http.ResponseWriter, r *http.Request) { if serveMockKeycloakAdminBrowserFlow(w, r) { return } }) kcMux.HandleFunc("/realms/master/login-actions/authenticate", func(w http.ResponseWriter, r *http.Request) { if serveMockKeycloakAdminBrowserFlow(w, r) { return } }) kcMux.HandleFunc("/admin/master/console/", func(w http.ResponseWriter, r *http.Request) { if serveMockKeycloakAdminBrowserFlow(w, r) { return } }) kcServer := httptest.NewTLSServer(kcMux) defer kcServer.Close() cfg := lifecycleConfig(t) cfg.Startup.ServiceChecklistAuth = authSettings(kcServer.URL) recorder := &commandRecorder{} base := lifecycleDispatcher(recorder) run := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) { command := name + " " + strings.Join(args, " ") if name == "kubectl" && strings.Contains(command, "-n sso get secret keycloak-admin -o json") { return testSecretJSON("admin", "password"), nil } return base(ctx, timeout, name, args...) } orch, _ := newHookOrchestrator(t, cfg, run, run) if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), &http.Client{Timeout: 4 * time.Second, Transport: &http.Transport{}}); err == nil { t.Fatalf("expected auth bootstrap without TLS skip to fail against TLS test server") } if _, err := orch.TestHookChecklistAuthHTTPClient(context.Background(), 4*time.Second, true); err != nil { t.Fatalf("expected checklist auth client fallback redirect path success, got %v", err) } }