package vaultauth import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync/atomic" "testing" ) func writeSAToken(t *testing.T, content string) string { t.Helper() path := filepath.Join(t.TempDir(), "token") if err := os.WriteFile(path, []byte(content), 0o600); err != nil { t.Fatalf("write token: %v", err) } return path } func TestLogin(t *testing.T) { var gotPath string var gotBody map[string]string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path raw, _ := io.ReadAll(r.Body) _ = json.Unmarshal(raw, &gotBody) _, _ = w.Write([]byte(`{"auth":{"client_token":"s.vaulttoken"}}`)) })) defer srv.Close() c := New(srv.URL, "k8s/au/syd1", "repospawner", writeSAToken(t, " jwt-value\n")) tok, err := c.Login(context.Background()) if err != nil { t.Fatalf("Login: %v", err) } if tok != "s.vaulttoken" { t.Errorf("token = %q", tok) } if gotPath != "/v1/auth/k8s/au/syd1/login" { t.Errorf("path = %q", gotPath) } if gotBody["role"] != "repospawner" || gotBody["jwt"] != "jwt-value" { t.Errorf("body = %v (the projected token must be trimmed)", gotBody) } } func TestLoginRejectsEmptyToken(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`{"auth":{}}`)) })) defer srv.Close() c := New(srv.URL, "k8s/au/syd1", "repospawner", writeSAToken(t, "jwt")) if _, err := c.Login(context.Background()); err == nil { t.Fatal("expected an error when vault returns no client token") } } func TestLoginSurfacesStatus(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusForbidden) })) defer srv.Close() c := New(srv.URL, "k8s/au/syd1", "repospawner", writeSAToken(t, "jwt")) _, err := c.Login(context.Background()) if err == nil || !strings.Contains(err.Error(), "403") { t.Fatalf("error = %v, want one naming status 403", err) } } func TestCredsUsesVaultToken(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.HasSuffix(r.URL.Path, "/login"): _, _ = w.Write([]byte(`{"auth":{"client_token":"s.vaulttoken"}}`)) case r.URL.Path == "/v1/gitea/creds/repospawner": if got := r.Header.Get("X-Vault-Token"); got != "s.vaulttoken" { t.Errorf("X-Vault-Token = %q", got) } _, _ = w.Write([]byte(`{"data":{"username":"repospawner-abc","token":"gitea-token"}}`)) default: t.Errorf("unexpected path %s", r.URL.Path) } })) defer srv.Close() c := New(srv.URL, "k8s/au/syd1", "repospawner", writeSAToken(t, "jwt")) creds, err := c.Creds(context.Background(), "gitea/creds/repospawner") if err != nil { t.Fatalf("Creds: %v", err) } if creds.Token != "gitea-token" || creds.Username != "repospawner-abc" { t.Errorf("creds = %+v", creds) } if strings.Contains(creds.String(), "gitea-token") { t.Errorf("Creds.String() leaked the token: %s", creds.String()) } } func TestCredsRejectsEmptyToken(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasSuffix(r.URL.Path, "/login") { _, _ = w.Write([]byte(`{"auth":{"client_token":"s.t"}}`)) return } _, _ = w.Write([]byte(`{"data":{"username":"u"}}`)) })) defer srv.Close() c := New(srv.URL, "k8s/au/syd1", "repospawner", writeSAToken(t, "jwt")) if _, err := c.Creds(context.Background(), "gitea/creds/repospawner"); err == nil { t.Fatal("expected an error when the credential carries no token") } } func TestTokenSourceCachesUntilForced(t *testing.T) { var reads atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasSuffix(r.URL.Path, "/login") { _, _ = w.Write([]byte(`{"auth":{"client_token":"s.t"}}`)) return } n := reads.Add(1) _, _ = w.Write([]byte(`{"data":{"username":"u","token":"tok` + string(rune('0'+n)) + `"}}`)) })) defer srv.Close() src := NewTokenSource(New(srv.URL, "k8s/au/syd1", "repospawner", writeSAToken(t, "jwt")), "gitea/creds/repospawner") ctx := context.Background() first, err := src.Token(ctx, false) if err != nil { t.Fatalf("Token: %v", err) } cached, err := src.Token(ctx, false) if err != nil { t.Fatalf("Token: %v", err) } if cached != first { t.Errorf("cached token %q != first %q", cached, first) } forced, err := src.Token(ctx, true) if err != nil { t.Fatalf("Token: %v", err) } if forced == first { t.Errorf("forced mint returned the cached token %q", forced) } if reads.Load() != 2 { t.Errorf("credential reads = %d, want 2", reads.Load()) } } func TestLoginMissingTokenFile(t *testing.T) { c := New("https://vault.invalid", "k8s/au/syd1", "repospawner", filepath.Join(t.TempDir(), "absent")) if _, err := c.Login(context.Background()); err == nil { t.Fatal("expected an error when the projected token is missing") } }