diff --git a/internal/config/config.go b/internal/config/config.go index bae2e44..d54dc9d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,11 +26,12 @@ type AuthorizationConfig struct { } type BuildkiteConfig struct { - Token string `env:"BUILDKITE_API_TOKEN, required"` ApiURL string // internal only + Token string `env:"BUILDKITE_API_TOKEN, required"` } type GithubConfig struct { + ApiURL string // internal only PrivateKey string `env:"GITHUB_APP_PRIVATE_KEY, required"` ApplicationID int64 `env:"GITHUB_APP_ID, required"` InstallationID int64 `env:"GITHUB_APP_INSTALLATION_ID, required"` diff --git a/internal/github/token.go b/internal/github/token.go index b759f02..beec20d 100644 --- a/internal/github/token.go +++ b/internal/github/token.go @@ -40,6 +40,18 @@ func New(cfg config.GithubConfig) (Client, error) { }, ) + // for testing use + if cfg.ApiURL != "" { + apiURL := cfg.ApiURL + if !strings.HasSuffix(apiURL, "/") { + apiURL += "/" + } + + appInstallationTransport.BaseURL = cfg.ApiURL + u, _ := url.Parse(apiURL) + client.BaseURL = u + } + return Client{ client, cfg.InstallationID, @@ -52,8 +64,10 @@ func (c Client) CreateAccessToken(ctx context.Context, repositoryURL string) (st return "", time.Time{}, err } - qualifiedIdentifier, _ := strings.CutSuffix(u.Path, ".git") - _, repoName, _ := strings.Cut(qualifiedIdentifier[1:], "/") + // qualifiedIdentifier, _ := strings.CutSuffix(u.Path, ".git") + // _, repoName, _ := strings.Cut(qualifiedIdentifier[1:], "/") + + _, repoName := RepoForURL(*u) tok, r, err := c.client.Apps.CreateInstallationToken(ctx, c.installationID, &github.InstallationTokenOptions{ diff --git a/internal/github/token_test.go b/internal/github/token_test.go new file mode 100644 index 0000000..d4087ee --- /dev/null +++ b/internal/github/token_test.go @@ -0,0 +1,134 @@ +package github_test + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "net/http" + "net/http/httptest" + "testing" + "time" + + api "github.com/google/go-github/v61/github" + "github.com/jamestelfer/ghauth/internal/config" + "github.com/jamestelfer/ghauth/internal/github" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateAccessToken_Succeeds(t *testing.T) { + router := http.NewServeMux() + + expectedExpiry := time.Date(1980, 01, 01, 0, 0, 0, 0, time.UTC) + actualInstallation := "unknown" + + router.HandleFunc("/app/installations/{installationID}/access_tokens", func(w http.ResponseWriter, r *http.Request) { + actualInstallation = r.PathValue("installationID") + + JSON(w, &api.InstallationToken{ + Token: api.String("expected-token"), + ExpiresAt: &api.Timestamp{Time: expectedExpiry}, + }) + }) + + svr := httptest.NewServer(router) + defer svr.Close() + + // generate valid key for testing + key := generateKey(t) + + gh, err := github.New(config.GithubConfig{ + ApiURL: svr.URL, + PrivateKey: key, + ApplicationID: 10, + InstallationID: 20, + }) + require.NoError(t, err) + + token, expiry, err := gh.CreateAccessToken(context.Background(), "https://github.com/organization/repository") + + require.NoError(t, err) + assert.Equal(t, "expected-token", token) + assert.Equal(t, expectedExpiry, expiry) + assert.Equal(t, "20", actualInstallation) +} + +func TestCreateAccessToken_Fails_On_Invalid_URL(t *testing.T) { + router := http.NewServeMux() + + router.HandleFunc("/app/installations/{installationID}/access_tokens", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTeapot) + }) + + svr := httptest.NewServer(router) + defer svr.Close() + + // generate valid key for testing + key := generateKey(t) + + gh, err := github.New(config.GithubConfig{ + ApiURL: svr.URL, + PrivateKey: key, + ApplicationID: 10, + InstallationID: 20, + }) + require.NoError(t, err) + + _, _, err = gh.CreateAccessToken(context.Background(), "sch_eme://invalid_url/") + + require.Error(t, err) + assert.ErrorContains(t, err, "first path segment in URL") +} + +func TestCreateAccessToken_Fails_On_Failed_Request(t *testing.T) { + router := http.NewServeMux() + + router.HandleFunc("/app/installations/{installationID}/access_tokens", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTeapot) + }) + + svr := httptest.NewServer(router) + defer svr.Close() + + // generate valid key for testing + key := generateKey(t) + + gh, err := github.New(config.GithubConfig{ + ApiURL: svr.URL, + PrivateKey: key, + ApplicationID: 10, + InstallationID: 20, + }) + require.NoError(t, err) + + _, _, err = gh.CreateAccessToken(context.Background(), "https://dodgey") + + require.Error(t, err) + assert.ErrorContains(t, err, ": 418") +} + +func JSON(w http.ResponseWriter, payload any) { + w.Header().Set("Content-Type", "application/json") + res, _ := json.Marshal(payload) + _, _ = w.Write(res) +} + +// generateKey creates and PEM encodes a valid RSA private key for testing. +func generateKey(t *testing.T) string { + t.Helper() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + privateKeyPEM := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + } + + key := pem.EncodeToMemory(privateKeyPEM) + + return string(key) +}