package main import ( "bytes" "context" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/kandev/kandev/internal/githubauth" ) const ( envGitHubCredentialBrokerURL = githubauth.CredentialBrokerURLEnv envGitHubCredentialLease = githubauth.CredentialLeaseEnv envGitHubCredentialTaskID = githubauth.CredentialTaskIDEnv envGitHubCredentialSessionID = githubauth.CredentialSessionIDEnv envGitHubCredentialRepository = githubauth.CredentialRepositoryEnv envGitHubCredentialOwner = githubauth.CredentialOwnerEnv envGitHubCredentialRepo = githubauth.CredentialRepoEnv envGitHubCredentialHost = githubauth.CredentialHostEnv envGitHubCredentialScopes = githubauth.CredentialScopesEnv ) func TestGitHubCredentialHelperGetsFreshCredential(t *testing.T) { var got githubBrokerResolveRequest server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("method = %s, want POST", r.Method) } if err := json.NewDecoder(r.Body).Decode(&got); err != nil { t.Errorf("decode request: %v", err) } w.Header().Set("Content-Type", "application/json") _, _ = io.WriteString(w, `{"username":"x-access-token","password":"fresh-token"}`) })) t.Cleanup(server.Close) env := githubCredentialTestEnv(server.URL) var stdout bytes.Buffer err := runGitHubCredentialHelper( context.Background(), []string{"get"}, strings.NewReader("protocol=https\nhost=github.com\npath=acme/widgets.git\n\n"), &stdout, lookupEnv(env), server.Client(), ) if err != nil { t.Fatalf("runGitHubCredentialHelper() error = %v", err) } if got.Lease != "opaque-lease" || got.TaskID != "task-1" || got.SessionID != "session-1" { t.Fatalf("broker scope = %+v", got) } if got.RepositoryID != "repo-1" || got.Owner != "acme" || got.Repo != "widgets" || got.Host != "github.com" { t.Fatalf("broker repository scope = %+v", got) } if want := "username=x-access-token\npassword=fresh-token\n\n"; stdout.String() != want { t.Fatalf("credential helper output = %q, want %q", stdout.String(), want) } } func TestGitHubCredentialHelperRejectsRepositoryScopeMismatch(t *testing.T) { requests := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { requests++ w.WriteHeader(http.StatusInternalServerError) })) t.Cleanup(server.Close) env := githubCredentialTestEnv(server.URL) err := runGitHubCredentialHelper( context.Background(), []string{"get"}, strings.NewReader("protocol=https\nhost=github.com\npath=other/repository.git\n\n"), io.Discard, lookupEnv(env), server.Client(), ) if err == nil || !strings.Contains(err.Error(), "does not match credential lease scope") { t.Fatalf("runGitHubCredentialHelper() error = %v, want scope mismatch", err) } if requests != 0 { t.Fatalf("broker requests = %d, want 0", requests) } } func TestGitHubCredentialHelperRejectsRepositoryPathCaseMismatch(t *testing.T) { env := githubCredentialTestEnv("https://broker.example/resolve") env[envGitHubCredentialOwner] = "Team" env[envGitHubCredentialRepo] = "Repo" err := runGitHubCredentialHelper( context.Background(), []string{"get"}, strings.NewReader("protocol=https\nhost=github.com\npath=team/repo.git\n\n"), io.Discard, lookupEnv(env), http.DefaultClient, ) if err == nil || !strings.Contains(err.Error(), "does not match credential lease scope") { t.Fatalf("runGitHubCredentialHelper() error = %v, want case-sensitive scope mismatch", err) } } func TestGitHubCredentialHelperRequiresCompleteScope(t *testing.T) { for name, input := range map[string]string{ "protocol": "host=github.com\npath=acme/widgets.git\n\n", "host": "protocol=https\npath=acme/widgets.git\n\n", "path": "protocol=https\nhost=github.com\n\n", } { t.Run(name, func(t *testing.T) { env := githubCredentialTestEnv("https://broker.example/resolve") err := runGitHubCredentialHelper( context.Background(), []string{"get"}, strings.NewReader(input), io.Discard, lookupEnv(env), http.DefaultClient, ) if err == nil { t.Fatal("runGitHubCredentialHelper() error = nil, want incomplete scope rejection") } }) } } func TestGitHubCredentialHelperSelectsRepositoryLease(t *testing.T) { var got githubBrokerResolveRequest server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := json.NewDecoder(r.Body).Decode(&got); err != nil { t.Errorf("decode request: %v", err) } _, _ = io.WriteString(w, `{"username":"x-access-token","password":"fresh-token"}`) })) t.Cleanup(server.Close) env := githubCredentialTestEnv(server.URL) env[envGitHubCredentialScopes] = `[ {"lease":"frontend-lease","task_id":"task-1","session_id":"session-1","repository_id":"repo-1","owner":"acme","repo":"frontend","host":"github.com"}, {"lease":"backend-lease","task_id":"task-1","session_id":"session-1","repository_id":"repo-2","owner":"acme","repo":"backend","host":"github.com"} ]` err := runGitHubCredentialHelper( context.Background(), []string{"get"}, strings.NewReader("protocol=https\nhost=github.com\npath=acme/backend.git\n\n"), io.Discard, lookupEnv(env), server.Client(), ) if err != nil { t.Fatalf("runGitHubCredentialHelper() error = %v", err) } if got.Lease != "backend-lease" || got.RepositoryID != "repo-2" { t.Fatalf("selected broker scope = %+v", got) } } func TestGitHubCredentialHelperPreservesExactProviderPath(t *testing.T) { var got githubBrokerResolveRequest server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := json.NewDecoder(r.Body).Decode(&got); err != nil { t.Errorf("decode request: %v", err) } _, _ = io.WriteString(w, `{"username":"x-token-auth","password":"fresh-token"}`) })) t.Cleanup(server.Close) env := githubCredentialTestEnv(server.URL) env[envGitHubCredentialScopes] = `[ {"lease":"opaque-lease","task_id":"task-1","session_id":"session-1","repository_id":"repo-1","owner":"context","repo":"scm/ENG/widgets","host":"bitbucket.example","path":"/context/scm/ENG/widgets"} ]` err := runGitHubCredentialHelper( context.Background(), []string{"get"}, strings.NewReader("protocol=https\nhost=bitbucket.example\npath=context/scm/ENG/widgets\n\n"), io.Discard, lookupEnv(env), server.Client(), ) if err != nil { t.Fatalf("runGitHubCredentialHelper() error = %v", err) } if got.Path != "/context/scm/ENG/widgets" { t.Fatalf("broker path = %q, want exact provider path", got.Path) } } func TestGitHubCredentialHelperIgnoresStoreAndErase(t *testing.T) { for _, operation := range []string{"store", "erase"} { t.Run(operation, func(t *testing.T) { if err := runGitHubCredentialHelper( context.Background(), []string{operation}, strings.NewReader("password=secret\n\n"), io.Discard, lookupEnv(nil), http.DefaultClient, ); err != nil { t.Fatalf("runGitHubCredentialHelper(%q) error = %v", operation, err) } }) } } func TestGitHubCredentialHelperSurfacesBrokerErrorBody(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnauthorized) _, _ = io.WriteString(w, `{"error":"authentication required"}`) })) t.Cleanup(server.Close) env := githubCredentialTestEnv(server.URL) err := runGitHubCredentialHelper( context.Background(), []string{"get"}, strings.NewReader("protocol=https\nhost=github.com\npath=acme/widgets.git\n\n"), io.Discard, lookupEnv(env), server.Client(), ) if err == nil { t.Fatal("runGitHubCredentialHelper() error = nil, want broker denial") } want := `resolve GitHub credential: broker returned HTTP 401: {"error":"authentication required"}` if err.Error() != want { t.Fatalf("error = %q, want %q", err.Error(), want) } } func TestGitHubCredentialHelperTruncatesAndSanitizesBrokerErrorBody(t *testing.T) { rawBody := "prefix\x00\x01\x1f control chars" + strings.Repeat("x", 600) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = io.WriteString(w, rawBody) })) t.Cleanup(server.Close) env := githubCredentialTestEnv(server.URL) err := runGitHubCredentialHelper( context.Background(), []string{"get"}, strings.NewReader("protocol=https\nhost=github.com\npath=acme/widgets.git\n\n"), io.Discard, lookupEnv(env), server.Client(), ) if err == nil { t.Fatal("runGitHubCredentialHelper() error = nil, want broker denial") } if !strings.HasPrefix(err.Error(), "resolve GitHub credential: broker returned HTTP 500: ") { t.Fatalf("error = %q, want HTTP 500 prefix", err.Error()) } appended := strings.TrimPrefix(err.Error(), "resolve GitHub credential: broker returned HTTP 500: ") if len(appended) > 512 { t.Fatalf("appended body length = %d, want <= 512", len(appended)) } for _, r := range appended { if r < 0x20 { t.Fatalf("appended body contains control char %q: %q", r, appended) } } } func TestGitHubCredentialHelperOmitsAppendedBodyWhenEmpty(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadGateway) })) t.Cleanup(server.Close) env := githubCredentialTestEnv(server.URL) err := runGitHubCredentialHelper( context.Background(), []string{"get"}, strings.NewReader("protocol=https\nhost=github.com\npath=acme/widgets.git\n\n"), io.Discard, lookupEnv(env), server.Client(), ) want := "resolve GitHub credential: broker returned HTTP 502" if err == nil || err.Error() != want { t.Fatalf("error = %v, want %q", err, want) } } func githubCredentialTestEnv(url string) map[string]string { return map[string]string{ envGitHubCredentialBrokerURL: url, envGitHubCredentialLease: "opaque-lease", envGitHubCredentialTaskID: "task-1", envGitHubCredentialSessionID: "session-1", envGitHubCredentialRepository: "repo-1", envGitHubCredentialOwner: "acme", envGitHubCredentialRepo: "widgets", envGitHubCredentialHost: "github.com", } } func lookupEnv(env map[string]string) func(string) string { return func(key string) string { return env[key] } }