package main import ( "bytes" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" ) const ( apiToken = "ak-api-token-secret" outpostKey = "outpost-key-secret" destPath = "kubernetes/namespace/authentik/default/outpost-token" ) // fakeEstate serves the Vault (approle + KV-v2 read/write) and Authentik // (outpost search + view_key) endpoints the seed flow needs. func fakeEstate(t *testing.T) (vaultURL, authentikURL string) { t.Helper() vmux := http.NewServeMux() vmux.HandleFunc("/v1/auth/approle/login", func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, `{"auth":{"client_token":"s.vaulttoken"}}`) }) vmux.HandleFunc("/v1/kv/data/service/authentik/agent-api-token", func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, `{"data":{"data":{"token":"`+apiToken+`"}}}`) }) vmux.HandleFunc("/v1/kv/data/"+destPath, func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, `{"data":{"version":7}}`) }) vs := httptest.NewServer(vmux) t.Cleanup(vs.Close) amux := http.NewServeMux() amux.HandleFunc("/api/v3/outposts/instances/", func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, `{"results":[{"pk":"1","name":"k8s-outpost","token_identifier":"ak-outpost-k8s"}]}`) }) amux.HandleFunc("/api/v3/core/tokens/ak-outpost-k8s/view_key/", func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, `{"key":"`+outpostKey+`"}`) }) as := httptest.NewServer(amux) t.Cleanup(as.Close) return vs.URL, as.URL } // The command prints identifiers and the KV version only — never a secret. func TestSeedOutpostOutputHasNoSecrets(t *testing.T) { vaultURL, authentikURL := fakeEstate(t) t.Setenv("VAULT_ADDR", vaultURL) t.Setenv("AGENT_APPROLE_ROLE_ID", "role-xyz") var out bytes.Buffer cmd := newRootCmd() cmd.SetOut(&out) cmd.SetErr(&out) cmd.SetArgs([]string{ "seed-outpost", "--outpost", "k8s-outpost", "--dest-path", destPath, "--authentik-url", authentikURL, }) if err := cmd.Execute(); err != nil { t.Fatalf("Execute: %v", err) } got := out.String() for _, want := range []string{"k8s-outpost", "ak-outpost-k8s", "kv/" + destPath, "version: 7"} { if !strings.Contains(got, want) { t.Errorf("output missing %q:\n%s", want, got) } } for _, secret := range []string{apiToken, outpostKey} { if strings.Contains(got, secret) { t.Fatalf("output leaks a secret:\n%s", got) } } } const oauthPath = "kubernetes/namespace/repospawner/default/oauth-credentials" // fakeOAuthVault serves approle login plus a KV-v2 path that already holds a // client_secret, and records what gets written back. func fakeOAuthVault(t *testing.T, existing map[string]string) (vaultURL string, written *map[string]string) { t.Helper() writes := map[string]string{} mux := http.NewServeMux() mux.HandleFunc("/v1/auth/approle/login", func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, `{"auth":{"client_token":"s.vaulttoken"}}`) }) mux.HandleFunc("/v1/kv/data/"+oauthPath, func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { var body struct { Data map[string]string `json:"data"` } _ = json.NewDecoder(r.Body).Decode(&body) for k, v := range body.Data { writes[k] = v } _, _ = io.WriteString(w, `{"data":{"version":4}}`) return } payload, _ := json.Marshal(map[string]any{"data": map[string]any{"data": existing}}) _, _ = w.Write(payload) }) srv := httptest.NewServer(mux) t.Cleanup(srv.Close) return srv.URL, &writes } // The command prints key names and the KV version only — never a value. func TestSeedOAuthOutputHasNoSecrets(t *testing.T) { const existingSecret = "existing-client-secret-value" vaultURL, written := fakeOAuthVault(t, map[string]string{"client_secret": existingSecret}) t.Setenv("VAULT_ADDR", vaultURL) t.Setenv("AGENT_APPROLE_ROLE_ID", "role-xyz") var out bytes.Buffer cmd := newRootCmd() cmd.SetOut(&out) cmd.SetErr(&out) cmd.SetArgs([]string{"seed-oauth", "--path", oauthPath, "--client-id", "mediamark-client-id"}) if err := cmd.Execute(); err != nil { t.Fatalf("Execute: %v", err) } got := out.String() for _, want := range []string{"kv/" + oauthPath, "client_id, client_secret, cookie_secret", "client_secret: kept", "cookie_secret: created", "version: 4"} { if !strings.Contains(got, want) { t.Errorf("output missing %q:\n%s", want, got) } } for key, value := range *written { if key == "client_id" { continue } if strings.Contains(got, value) { t.Fatalf("output leaks the %s value:\n%s", key, got) } } if strings.Contains(got, existingSecret) { t.Fatalf("output leaks the existing client_secret:\n%s", got) } } func TestSeedOAuthRequiresFlags(t *testing.T) { for name, args := range map[string][]string{ "no path": {"seed-oauth", "--client-id", "mediamark-client-id"}, "no client-id": {"seed-oauth", "--path", oauthPath}, } { t.Run(name, func(t *testing.T) { cmd := newRootCmd() cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(args) if err := cmd.Execute(); err == nil { t.Fatal("Execute() = nil, want a missing-required-flag error") } }) } } func TestSeedOutpostRequiresFlags(t *testing.T) { for name, args := range map[string][]string{ "no outpost": {"seed-outpost", "--dest-path", destPath}, "no dest-path": {"seed-outpost", "--outpost", "k8s-outpost"}, } { t.Run(name, func(t *testing.T) { cmd := newRootCmd() cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(args) if err := cmd.Execute(); err == nil { t.Fatal("Execute() = nil, want a missing-required-flag error") } }) } }