ananke/testing/orchestrator/hooks_service_auth_matrix_test.go

788 lines
33 KiB
Go
Raw Permalink Normal View History

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(
`<html><body><form id="kc-form-login" action="/realms/master/login-actions/authenticate?session_code=test&execution=test&client_id=security-admin-console&tab_id=test" method="post"></form></body></html>`,
))
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("<html><body>console ok</body></html>"))
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(`<html><body><form id="kc-form-login" action="http://127.0.0.1:1/login"></form></body></html>`))
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(`<html><body><form id="kc-form-login" action="http://[::1"></form></body></html>`))
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(`<html><body><form id="kc-form-login" action="/realms/master/login-actions/authenticate"></form></body></html>`))
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(`<html><body><form id="kc-form-login" action="/realms/master/login-actions/authenticate"></form></body></html>`))
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("<html><body>console ok</body></html>"))
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(
`<html><form id="kc-form-login" action="https://example.test/login"></form></html>`,
"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(
`<html><form id="kc-form-login" action="javascript:void(0)"></form></html>`,
"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("<html><body>no form</body></html>"))
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(
`<html><body><form id="kc-form-login" action="/realms/master/login-actions/authenticate?session_code=test"></form></body></html>`,
))
case strings.Contains(r.URL.Path, "/realms/master/login-actions/authenticate"):
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`<html><body><form id="kc-form-login" action="/retry"></form></body></html>`))
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(`<html><body><form id="kc-form-login" action="/realms/atlas/login-actions/authenticate"></form></body></html>`))
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)
}
}