Files
kandev/apps/backend/cmd/agentctl/github_cli_shim_test.go
T
2026-07-27 18:10:12 +01:00

210 lines
7.2 KiB
Go

package main
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func TestInstallGitHubCLIShimCreatesManagedTools(t *testing.T) {
root := t.TempDir()
binary := filepath.Join(root, "agentctl")
if err := os.WriteFile(binary, []byte("agentctl"), 0o700); err != nil {
t.Fatal(err)
}
shimDir, cleanup, err := installGitHubCLIShim(binary, root)
if err != nil {
t.Fatalf("installGitHubCLIShim() error = %v", err)
}
t.Cleanup(cleanup)
for _, name := range []string{githubCLIShimName(), "agentctl"} {
entrypoint := filepath.Join(shimDir, name)
if _, err := os.Stat(entrypoint); err != nil {
t.Fatalf("stat %s managed tool: %v", name, err)
}
if filepath.Dir(entrypoint) != shimDir {
t.Fatalf("entrypoint = %q, want inside %q", entrypoint, shimDir)
}
}
}
func TestPathWithoutDirectoryPreservesPathList(t *testing.T) {
path := strings.Join([]string{"/bin", "/shim", "/usr/bin"}, string(os.PathListSeparator))
want := strings.Join([]string{"/bin", "/usr/bin"}, string(os.PathListSeparator))
if got := pathWithoutDirectory(path, "/shim"); got != want {
t.Fatalf("pathWithoutDirectory() = %q, want %q", got, want)
}
}
func TestIsGitHubCLIShimInvocation(t *testing.T) {
for _, name := range []string{"/tmp/shims/gh", `C:\\shims\\gh.exe`} {
if !isGitHubCLIShimInvocation(name) {
t.Fatalf("isGitHubCLIShimInvocation(%q) = false", name)
}
}
if isGitHubCLIShimInvocation("/usr/local/bin/agentctl") {
t.Fatal("agentctl executable was mistaken for gh shim")
}
}
func TestGitHubCLIShimRefreshesAndIsolatesEachInvocation(t *testing.T) {
issued := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
issued++
_ = json.NewEncoder(w).Encode(map[string]string{
"username": "x-access-token",
"password": "fresh-token-" + string(rune('0'+issued)),
})
}))
t.Cleanup(server.Close)
env := githubCredentialTestEnv(server.URL)
env["PATH"] = "/shim:/usr/bin"
env["GITHUB_TOKEN"] = "parent-token-must-not-win"
var childTokens []string
var configDirs []string
runner := func(_ context.Context, executable string, args []string, childEnv []string, _ io.Reader, _, _ io.Writer) error {
if executable != "/usr/bin/gh" {
t.Errorf("executable = %q, want /usr/bin/gh", executable)
}
if len(args) != 2 || args[0] != "pr" || args[1] != "list" {
t.Errorf("args = %v", args)
}
childTokens = append(childTokens, envValue(childEnv, "GH_TOKEN"))
configDir := envValue(childEnv, "GH_CONFIG_DIR")
configDirs = append(configDirs, configDir)
if _, err := os.Stat(configDir); err != nil {
t.Errorf("GH_CONFIG_DIR was not available to gh: %v", err)
}
if got := envValue(childEnv, "GITHUB_TOKEN"); got != "" {
t.Errorf("child GITHUB_TOKEN = %q, want removed", got)
}
return nil
}
lookPath := func(file, path string) (string, error) {
if file != "gh" || path != "/usr/bin" {
t.Fatalf("lookPath(%q, %q)", file, path)
}
return "/usr/bin/gh", nil
}
for range 2 {
if err := runGitHubCLIShim(
context.Background(), []string{"pr", "list"}, strings.NewReader(""), io.Discard, io.Discard,
lookupEnv(env), func() []string { return envMap(env) }, server.Client(), "/shim", lookPath, runner,
); err != nil {
t.Fatalf("runGitHubCLIShim() error = %v", err)
}
}
if got, want := strings.Join(childTokens, ","), "fresh-token-1,fresh-token-2"; got != want {
t.Fatalf("child GH_TOKEN values = %q, want %q", got, want)
}
if configDirs[0] == "" || configDirs[0] == configDirs[1] {
t.Fatalf("GH_CONFIG_DIR values = %v, want unique isolated directories", configDirs)
}
for _, configDir := range configDirs {
if _, err := os.Stat(configDir); !os.IsNotExist(err) {
t.Fatalf("GH_CONFIG_DIR %q remains after invocation", configDir)
}
}
}
func TestGitHubCLIShimSelectsRepositoryLease(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":"backend-token"}`)
}))
t.Cleanup(server.Close)
env := githubCredentialTestEnv(server.URL)
env["PATH"] = "/shim:/usr/bin"
env["GH_REPO"] = "acme/backend"
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"}
]`
var childToken string
err := runGitHubCLIShim(
context.Background(), []string{"pr", "list"}, strings.NewReader(""), io.Discard, io.Discard,
lookupEnv(env), func() []string { return envMap(env) }, server.Client(), "/shim",
func(string, string) (string, error) { return "/usr/bin/gh", nil },
func(_ context.Context, _ string, _ []string, childEnv []string, _ io.Reader, _, _ io.Writer) error {
childToken = envValue(childEnv, "GH_TOKEN")
return nil
},
)
if err != nil {
t.Fatalf("runGitHubCLIShim() error = %v", err)
}
if got.Lease != "backend-lease" || got.RepositoryID != "repo-2" {
t.Fatalf("selected broker scope = %+v", got)
}
if childToken != "backend-token" {
t.Fatalf("child GH_TOKEN = %q, want backend-token", childToken)
}
}
func TestParseGitHubCLIRepository(t *testing.T) {
tests := []struct {
name string
raw string
defaultHost string
want githubCLIRepository
}{
{name: "owner and repo", raw: "acme/widgets", defaultHost: "github.com", want: githubCLIRepository{host: "github.com", owner: "acme", repo: "widgets"}},
{name: "enterprise", raw: "github.example.com/acme/widgets", want: githubCLIRepository{host: "github.example.com", owner: "acme", repo: "widgets"}},
{name: "HTTPS remote", raw: "https://github.com/acme/widgets.git", want: githubCLIRepository{host: "github.com", owner: "acme", repo: "widgets"}},
{name: "SSH remote", raw: "git@github.com:acme/widgets.git", want: githubCLIRepository{host: "github.com", owner: "acme", repo: "widgets"}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := parseGitHubCLIRepository(test.raw, test.defaultHost)
if err != nil {
t.Fatalf("parseGitHubCLIRepository() error = %v", err)
}
if *got != test.want {
t.Fatalf("parseGitHubCLIRepository() = %+v, want %+v", *got, test.want)
}
})
}
}
func TestGitHubCLIRepositoryArgument(t *testing.T) {
for _, args := range [][]string{
{"pr", "list", "-R", "acme/widgets"},
{"pr", "list", "--repo=acme/widgets"},
{"pr", "list", "-Racme/widgets"},
} {
got, found, err := githubCLIRepositoryArgument(args)
if err != nil || !found || got != "acme/widgets" {
t.Fatalf("githubCLIRepositoryArgument(%v) = %q, %v, %v", args, got, found, err)
}
}
}
func envMap(env map[string]string) []string {
result := make([]string, 0, len(env))
for key, value := range env {
result = append(result, key+"="+value)
}
return result
}
func envValue(env []string, key string) string {
prefix := key + "="
for _, entry := range env {
if strings.HasPrefix(entry, prefix) {
return strings.TrimPrefix(entry, prefix)
}
}
return ""
}