/
githubmirror
/
grafana
Обзор
Документация
Войти
/
githubmirror
/
grafana
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
apps/provisioning/pkg/connection/github/connection_test.go
2 213 строк
76 KB
Alejandro
Provisioning: (3/8) Rework connection token validation (#129740)
31 июл 2026, 19:34
Не верифицирован
31 июл 2026, 19:34
3d59912
Код
Авторство
О чём код?
package github_test import ( "encoding/base64" "errors" "fmt" "net/http" "testing" "time" "github.com/golang-jwt/jwt/v4" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/connection" "github.com/grafana/grafana/apps/provisioning/pkg/connection/github" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) //nolint:gosec // Test RSA private key (generated for testing purposes only) const testPrivateKeyPEM = "-----BEGIN RSA PRIVATE KEY-----\n" + // trufflehog:ignore `MIIEowIBAAKCAQEAoInVbLY9io2Q/wHvUIXlEHg2Qyvd8eRzBAVEJ92DS6fx9H10 06V0VRm78S0MXyo6i+n8ZAbZ0/R+GWpP2Ephxm0Gs2zo+iO2mpB19xQFI4o6ZTOw b2WyjSaa2Vr4oyDkqti6AvfjW4VUAu932e08GkgwmmQSHXj7FX2CMWjgUwTTcuaX 65SHNKLNYLUP0HTumLzoZeqDTdoMMpKNdgH9Avr4/8vkVJ0mD6rqvxnw3JHsseNO WdQTxf2aApBNHIIKxWZ2i/ZmjLNey7kltgjEquGiBdJvip3fHhH5XHdkrXcjRtnw OJDnDmi5lQwv5yUBOSkbvbXRv/L/m0YLoD/fbwIDAQABAoIBAFfl//hM8/cnuesV +R1Con/ZAgTXQOdPqPXbmEyniVrkMqMmCdBUOBTcST4s5yg36+RtkeaGpb/ajyyF PAB2AYDucwvMpudGpJWOYTiOOp4R8hU1LvZfXVrRd1lo6NgQi4NLtNUpOtACeVQ+ H4Yv0YemXQ47mnuOoRNMK/u3q5NoIdSahWptXBgUno8KklNpUrH3IYWaUxfBzDN3 2xsVRTn2SfTSyoDmTDdTgptJONmoK1/sV7UsgWksdFc6XyYhsFAZgOGEJrBABRvF 546dyQ0cWxuPyVXpM7CN3tqC5ssvLjElg3LicK1V6gnjpdRnnvX88d1Eh3Uc/9IM OZInT2ECgYEA6W8sQXTWinyEwl8SDKKMbB2ApIghAcFgdRxprZE4WFxjsYNCNL70 dnSB7MRuzmxf5W77cV0N7JhH66N8HvY6Xq9olrpQ5dNttR4w8Pyv3wavDe8x7seL 5L2Xtbu7ihDr8Dk27MjiBSin3IxhBP5CJS910+pR6LrAWtEuU+FzFfECgYEAsA6y qxHhCMXlTnauXhsnmPd1g61q7chW8kLQFYtHMLlQlgjHTW7irDZ9cPbPYDNjwRLO 7KLorcpv2NKe7rqq2ZyCm6hf1b9WnlQjo3dLpNWMu6fhy/smK8MgbRqcWpX+oTKF 79mK6hbY7o6eBzsQHBl7Z+LBNuwYmp9qOodPa18CgYEArv6ipKdcNhFGzRfMRiCN OHederp6VACNuP2F05IsNUF9kxOdTEFirnKE++P+VU01TqA2azOhPp6iO+ohIGzi MR06QNSH1OL9OWvasK4dggpWrRGF00VQgDgJRTnpS4WH+lxJ6pRlrAxgWpv6F24s VAgSQr1Ejj2B+hMasdMvHWECgYBJ4uE4yhgXBnZlp4kmFV9Y4wF+cZkekaVrpn6N jBYkbKFVVfnOlWqru3KJpgsB5I9IyAvvY68iwIKQDFSG+/AXw4dMrC0MF3DSoZ0T TU2Br92QI7SvVod+djV1lGVp3ukt3XY4YqPZ+hywgUnw3uiz4j3YK2HLGup4ec6r IX5DIQKBgHRLzvT3zqtlR1Oh0vv098clLwt+pGzXOxzJpxioOa5UqK13xIpFXbcg iWUVh5YXCcuqaICUv4RLIEac5xQitk9Is/9IhP0NJ/81rHniosvdSpCeFXzxTImS B8Uc0WUgheB4+yVKGnYpYaSOgFFI5+1BYUva/wDHLy2pWHz39Usb -----END RSA PRIVATE KEY-----` func TestConnection_Mutate(t *testing.T) { privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testPrivateKeyPEM)) tests := []struct { name string connection *provisioning.Connection secrets github.ConnectionSecrets wantErr bool validateError func(t *testing.T, err error) validateResult func(t *testing.T, connection *provisioning.Connection) }{ { name: "should add URL to Github connection", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ PrivateKey: common.InlineSecureValue{ Create: common.NewSecretValue(privateKeyBase64), }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.NewSecretValue(privateKeyBase64), }, wantErr: false, validateResult: func(t *testing.T, connection *provisioning.Connection) { assert.Equal(t, "https://github.com/settings/installations/456", connection.Spec.URL) }, }, { name: "should not generate JWT token", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ PrivateKey: common.InlineSecureValue{ Create: common.NewSecretValue(privateKeyBase64), }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.NewSecretValue(privateKeyBase64), }, wantErr: false, validateResult: func(t *testing.T, connection *provisioning.Connection) { assert.Equal(t, "https://github.com/settings/installations/456", connection.Spec.URL) assert.True(t, connection.Secure.Token.Create.IsZero(), "Token should not be generated in mutator") }, }, { name: "should do nothing when GitHub config is nil", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GitlabConnectionType, OAuth: &provisioning.ConnectionOAuthConfig{ ClientID: "clientID", }, }, }, secrets: github.ConnectionSecrets{}, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := github.Mutate(t.Context(), tt.connection) if tt.wantErr { require.Error(t, err) if tt.validateError != nil { tt.validateError(t, err) } } else { require.NoError(t, err) if tt.validateResult != nil { tt.validateResult(t, tt.connection) } } }) } } func TestConnection_Test(t *testing.T) { const appID = "123" privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testPrivateKeyPEM)) token, err := github.GenerateJWTToken(appID, common.RawSecureValue(privateKeyBase64)) require.NoError(t, err) tests := []struct { name string connection *provisioning.Connection secrets github.ConnectionSecrets setupMock func(*github.MockGithubFactory, *github.MockClient) expectedCode int expectedErrors []provisioning.ErrorDetails expectSuccess bool }{ { name: "success - valid app and installation with all permissions", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{ ID: 456, Enabled: true, Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) }, expectedCode: http.StatusOK, expectSuccess: true, }, { // PrivateKey.Create is set → triggers the key-rotation branch (if-branch), // so GenerateJWTToken is called instead of parseJWTToken. // With an invalid secrets.PrivateKey the generation fails. name: "failure - privateKey is invalid when rotating key (PrivateKey.Create is set)", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ PrivateKey: common.InlineSecureValue{ Create: common.RawSecureValue(privateKeyBase64), }, Token: common.InlineSecureValue{ Create: token, }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: "invalidPrivateKey", Token: token, }, expectedCode: http.StatusUnauthorized, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeFieldValueInvalid, Field: "secure.privateKey", Detail: "invalid private key", }, }, }, { // No PrivateKey.Create → no key rotation → else branch parses the existing token. // With an invalid secrets.PrivateKey the parse fails. name: "failure - privateKey is invalid when validating existing token", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, // No Secure.PrivateKey.Create — no key rotation, uses else branch }, secrets: github.ConnectionSecrets{ PrivateKey: "invalidPrivateKey", Token: token, }, expectedCode: http.StatusUnauthorized, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeFieldValueInvalid, Field: "secure.privateKey", Detail: "invalid private key", }, }, }, { // Token is zero → if-branch → GenerateJWTToken is called. // With an invalid secrets.PrivateKey the generation fails. name: "failure - privateKey is invalid when generating token", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ PrivateKey: common.InlineSecureValue{ Create: common.RawSecureValue(privateKeyBase64), }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: "invalidPrivateKey", }, expectedCode: http.StatusUnauthorized, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeFieldValueInvalid, Field: "secure.privateKey", Detail: "invalid private key", }, }, }, { // No PrivateKey.Create → else branch → parseJWTToken validates the token issuer. // The token was issued for appID "123" but connection.AppID is "678" → issuer mismatch. name: "failure - token issuer is a different appID (else branch, no key rotation)", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "678", InstallationID: "456", }, }, // No Secure.PrivateKey.Create — no key rotation, uses else branch to check token issuer }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, // issued for appID "123", not "678" }, expectedCode: http.StatusUnauthorized, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeFieldValueInvalid, Field: "spec.github.appID", Detail: "invalid app ID", BadValue: "678", }, }, }, { // PrivateKey.Create is set + valid key + existing token → key rotation path: // regenerates token with new key and proceeds to GitHub API. name: "success - key rotation with valid new private key", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ PrivateKey: common.InlineSecureValue{ Create: common.RawSecureValue(privateKeyBase64), }, Token: common.InlineSecureValue{ Create: token, }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{ ID: 456, Enabled: true, Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) }, expectedCode: http.StatusOK, expectSuccess: true, }, { name: "failure - GetApp returns service unavailable", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{}, github.ErrServiceUnavailable) }, expectedCode: http.StatusServiceUnavailable, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeInternal, Detail: github.ErrServiceUnavailable.Error(), }, }, }, { name: "failure - GetApp returns other error (invalid token)", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{}, errors.New("unauthorized")) }, expectedCode: http.StatusUnprocessableEntity, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeFieldValueInvalid, Detail: "failed to GET app: unauthorized", }, }, }, { name: "failure - appID mismatch", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ID: 999, Slug: "wrong-app"}, nil) }, expectedCode: http.StatusBadRequest, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeFieldValueInvalid, Field: "spec.github.appID", Detail: "appID mismatch", BadValue: appID, }, }, }, { name: "failure - GetApp returns authentication error (401)", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{}, connection.ErrAuthentication) }, expectedCode: http.StatusUnauthorized, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeFieldValueInvalid, Field: "spec.github.appID", Detail: "authentication failed. The appID exists but could not be accessed with the privateKey. Verify appID is correct", BadValue: appID, }, { Type: metav1.CauseTypeFieldValueInvalid, Field: "secure.privateKey", Detail: "authentication failed. Verify privateKey is the generated private key for the appID", BadValue: "****", }, }, }, { name: "failure - GetApp returns not found (404)", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{}, github.ErrNotFound) }, expectedCode: http.StatusNotFound, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeFieldValueNotFound, Field: "spec.github.appID", Detail: "app not found", BadValue: appID, }, }, }, { name: "failure - missing contents permission", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionNone, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) }, expectedCode: http.StatusForbidden, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeForbidden, Field: "spec.github.appID", Detail: "GitHub App lacks required 'contents' permission: requires 'write', has ''", BadValue: appID, }, }, }, { name: "failure - insufficient contents permission (read instead of write)", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionRead, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) }, expectedCode: http.StatusForbidden, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeForbidden, Field: "spec.github.appID", Detail: "GitHub App lacks required 'contents' permission: requires 'write', has 'read'", BadValue: appID, }, }, }, { name: "failure - missing metadata permission", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionNone, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) }, expectedCode: http.StatusForbidden, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeForbidden, Field: "spec.github.appID", Detail: "GitHub App lacks required 'metadata' permission: requires 'read', has ''", BadValue: appID, }, }, }, { name: "failure - missing pull_requests permission", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionNone, Webhooks: github.PermissionWrite, }, }, nil) }, expectedCode: http.StatusForbidden, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeForbidden, Field: "spec.github.appID", Detail: "GitHub App lacks required 'pull_requests' permission: requires 'write', has ''", BadValue: appID, }, }, }, { name: "failure - missing webhooks permission", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionNone, }, }, nil) }, expectedCode: http.StatusForbidden, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeForbidden, Field: "spec.github.appID", Detail: "GitHub App lacks required 'webhooks' permission: requires 'write', has ''", BadValue: appID, }, }, }, { // A disabled webhook skips the webhooks check at both the app and installation level, // so a GitHub App without webhooks:write can still connect successfully. name: "success - webhook disabled, no webhooks permission on app or installation", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, Webhook: &provisioning.ConnectionWebhookConfig{Disabled: true}, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionNone, }, }, nil) mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{ ID: 456, Enabled: true, Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionNone, }, }, nil) }, expectedCode: http.StatusOK, expectSuccess: true, }, { // A disabled webhook only skips the webhooks check; other required permissions still apply. name: "failure - webhook disabled does not exempt missing contents permission", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, Webhook: &provisioning.ConnectionWebhookConfig{Disabled: true}, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionNone, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionNone, }, }, nil) }, expectedCode: http.StatusForbidden, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeForbidden, Field: "spec.github.appID", Detail: "GitHub App lacks required 'contents' permission: requires 'write', has ''", BadValue: appID, }, }, }, { name: "failure - multiple missing permissions", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionRead, Metadata: github.PermissionNone, PullRequests: github.PermissionNone, Webhooks: github.PermissionNone, }, }, nil) }, expectedCode: http.StatusForbidden, expectSuccess: false, }, { name: "failure - GetAppInstallation returns not found (404)", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{}, github.ErrNotFound) }, expectedCode: http.StatusNotFound, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeFieldValueInvalid, Field: "spec.github.installationID", Detail: "installation not found", BadValue: "456", }, }, }, { name: "failure - GetAppInstallation returns authentication error (401)", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{}, connection.ErrAuthentication) }, expectedCode: http.StatusUnauthorized, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeFieldValueInvalid, Field: "spec.github.installationID", Detail: connection.ErrAuthentication.Error(), BadValue: "456", }, }, }, { name: "failure - GetAppInstallation returns service unavailable (503)", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{}, github.ErrServiceUnavailable) }, expectedCode: http.StatusServiceUnavailable, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeFieldValueInvalid, Field: "spec.github.installationID", Detail: github.ErrServiceUnavailable.Error(), BadValue: "456", }, }, }, { name: "failure - GetAppInstallation returns generic error", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{}, errors.New("unexpected error")) }, expectedCode: http.StatusUnprocessableEntity, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeFieldValueInvalid, Detail: "failed to GET app installation: unexpected error", }, }, }, { name: "failure - installation missing contents permission", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{ ID: 456, Enabled: true, Permissions: github.Permissions{ Contents: github.PermissionNone, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) }, expectedCode: http.StatusForbidden, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeForbidden, Field: "spec.github.installationID", Detail: "GitHub App installation lacks required 'contents' permission: requires 'write', has ''. Accept the updated permissions at https://github.com/settings/installations/456", BadValue: "456", }, }, }, { name: "failure - installation insufficient contents permission (read instead of write)", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{ ID: 456, Enabled: true, Permissions: github.Permissions{ Contents: github.PermissionRead, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) }, expectedCode: http.StatusForbidden, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeForbidden, Field: "spec.github.installationID", Detail: "GitHub App installation lacks required 'contents' permission: requires 'write', has 'read'. Accept the updated permissions at https://github.com/settings/installations/456", BadValue: "456", }, }, }, { name: "failure - installation missing metadata permission", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{ ID: 456, Enabled: true, Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionNone, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) }, expectedCode: http.StatusForbidden, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeForbidden, Field: "spec.github.installationID", Detail: "GitHub App installation lacks required 'metadata' permission: requires 'read', has ''. Accept the updated permissions at https://github.com/settings/installations/456", BadValue: "456", }, }, }, { name: "failure - installation missing pull_requests permission", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{ ID: 456, Enabled: true, Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionNone, Webhooks: github.PermissionWrite, }, }, nil) }, expectedCode: http.StatusForbidden, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeForbidden, Field: "spec.github.installationID", Detail: "GitHub App installation lacks required 'pull_requests' permission: requires 'write', has ''. Accept the updated permissions at https://github.com/settings/installations/456", BadValue: "456", }, }, }, { name: "failure - installation missing webhooks permission", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{ ID: 456, Enabled: true, Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionNone, }, }, nil) }, expectedCode: http.StatusForbidden, expectSuccess: false, expectedErrors: []provisioning.ErrorDetails{ { Type: metav1.CauseTypeForbidden, Field: "spec.github.installationID", Detail: "GitHub App installation lacks required 'webhooks' permission: requires 'write', has ''. Accept the updated permissions at https://github.com/settings/installations/456", BadValue: "456", }, }, }, { name: "failure - installation multiple missing permissions", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionRead, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{ ID: 456, Enabled: true, Permissions: github.Permissions{ Contents: github.PermissionRead, Metadata: github.PermissionNone, PullRequests: github.PermissionNone, Webhooks: github.PermissionNone, }, }, nil) }, expectedCode: http.StatusForbidden, expectSuccess: false, }, { name: "success - write permission satisfies read requirement", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: appID, InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), Token: token, }, setupMock: func(mockFactory *github.MockGithubFactory, mockClient *github.MockClient) { mockFactory.EXPECT().New(mock.Anything, mock.Anything, mock.Anything).Return(mockClient, nil) mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ ID: 123, Slug: "test-app", Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionWrite, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{ ID: 456, Enabled: true, Permissions: github.Permissions{ Contents: github.PermissionWrite, Metadata: github.PermissionWrite, PullRequests: github.PermissionWrite, Webhooks: github.PermissionWrite, }, }, nil) }, expectedCode: http.StatusOK, expectSuccess: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockFactory := github.NewMockGithubFactory(t) mockClient := github.NewMockClient(t) if tt.setupMock != nil { tt.setupMock(mockFactory, mockClient) } // Mutate populates Spec.URL (the installation URL) as it would in production // admission, which Test reads for installation-permission error details. require.NoError(t, github.Mutate(t.Context(), tt.connection)) conn := github.NewConnection(tt.connection, mockFactory, tt.secrets) result, err := conn.Test(t.Context()) require.NoError(t, err) require.NotNil(t, result) assert.Equal(t, tt.expectedCode, result.Code) assert.Equal(t, tt.expectSuccess, result.Success) if tt.expectedErrors != nil { assert.Equal(t, tt.expectedErrors, result.Errors) } }) } } func TestConnection_ValidateToken(t *testing.T) { privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testPrivateKeyPEM)) // Generate a valid token using the existing function (expires in 10 minutes) validToken, err := github.GenerateJWTToken("123", common.RawSecureValue(privateKeyBase64)) require.NoError(t, err) _, exp, err := getIssuingAndExpirationTimeFromToken(validToken) require.NoError(t, err) require.False(t, exp.IsZero()) tests := []struct { name string appID string secrets github.ConnectionSecrets expectErr bool expectExpiresAt time.Time }{ { name: "valid token is usable with expiry", appID: "123", secrets: github.ConnectionSecrets{ Token: validToken, PrivateKey: common.RawSecureValue(privateKeyBase64), }, expectExpiresAt: exp, }, { name: "token issued for another appID is not usable", appID: "789", secrets: github.ConnectionSecrets{ Token: validToken, PrivateKey: common.RawSecureValue(privateKeyBase64), }, expectErr: true, }, { name: "invalid token format is not usable", appID: "123", secrets: github.ConnectionSecrets{ Token: common.RawSecureValue("not-a-valid-jwt-token"), PrivateKey: common.RawSecureValue(privateKeyBase64), }, expectErr: true, }, { name: "invalid private key is not usable", appID: "123", secrets: github.ConnectionSecrets{ Token: validToken, PrivateKey: common.RawSecureValue("not-base64"), }, expectErr: true, }, { name: "empty token is not usable", appID: "123", secrets: github.ConnectionSecrets{ Token: common.RawSecureValue(""), PrivateKey: common.RawSecureValue(privateKeyBase64), }, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockFactory := github.NewMockGithubFactory(t) obj := &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: tt.appID, InstallationID: "456", }, }, } conn := github.NewConnection(obj, mockFactory, tt.secrets) expiresAt, err := conn.ValidateToken() if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) assert.Equal(t, tt.expectExpiresAt, expiresAt) }) } } func TestConnection_GenerateConnectionToken(t *testing.T) { privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testPrivateKeyPEM)) tests := []struct { name string connection *provisioning.Connection secrets github.ConnectionSecrets expectedError string validateToken func(t *testing.T, token common.RawSecureValue) }{ { name: "success - generates valid JWT token", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), }, validateToken: func(t *testing.T, token common.RawSecureValue) { // Verify token is not empty assert.NotEmpty(t, token) // Verify token is a valid JWT by parsing it privateKeyPEM, err := base64.StdEncoding.DecodeString(privateKeyBase64) require.NoError(t, err) key, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyPEM) require.NoError(t, err) parsedToken, err := jwt.Parse(string(token), func(_ *jwt.Token) (any, error) { return &key.PublicKey, nil }, jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()})) require.NoError(t, err) assert.True(t, parsedToken.Valid) // Verify claims claims, ok := parsedToken.Claims.(jwt.MapClaims) require.True(t, ok) assert.Equal(t, "123", claims["iss"]) }, }, { name: "success - generates different token each time", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "789", InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), }, validateToken: func(t *testing.T, token common.RawSecureValue) { // Generate a second token and verify they're different (due to different timestamps) time.Sleep(1 * time.Second) // Ensure different iat claim token2, err := github.GenerateJWTToken("789", common.RawSecureValue(privateKeyBase64)) require.NoError(t, err) assert.NotEqual(t, token, token2, "tokens should differ due to timestamp") }, }, { name: "error - connection without GitHub config", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GitlabConnectionType, OAuth: &provisioning.ConnectionOAuthConfig{ ClientID: "clientID", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), }, expectedError: "connection is not a GitHub connection", }, { name: "error - nil GitHub config", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: nil, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), }, expectedError: "connection is not a GitHub connection", }, { name: "error - invalid private key (not base64)", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue("not-valid-base64!@#"), }, expectedError: "failed to decode base64 private key", }, { name: "error - invalid PEM format", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(base64.StdEncoding.EncodeToString([]byte("not-a-valid-pem-format"))), }, expectedError: "failed to parse private key", }, { name: "error - empty private key", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, }, secrets: github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(""), }, expectedError: "failed to parse private key", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockFactory := github.NewMockGithubFactory(t) conn := github.NewConnection(tt.connection, mockFactory, tt.secrets) token, err := conn.GenerateConnectionToken(t.Context()) if tt.expectedError != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.expectedError) } else { require.NoError(t, err) assert.NotEmpty(t, token) if tt.validateToken != nil { tt.validateToken(t, token) } } }) } } func TestConnection_GenerateRepositoryToken(t *testing.T) { tests := []struct { name string connection *provisioning.Connection repo *provisioning.Repository setupMock func(*github.MockGithubFactory) expectedToken *connection.ExpirableSecureValue expectedError string }{ { name: "success", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ Token: common.InlineSecureValue{ Create: common.RawSecureValue("jwt-token"), }, }, }, repo: &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{Name: "test-repo"}, Spec: provisioning.RepositorySpec{ Type: provisioning.GitHubRepositoryType, GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/test-owner/test-repo", }, }, }, setupMock: func(mockFactory *github.MockGithubFactory) { mockClient := github.NewMockClient(t) mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("jwt-token"), mock.Anything).Return(mockClient, nil) mockClient.EXPECT().CreateInstallationAccessToken(mock.Anything, "456", "test-repo"). Return(github.InstallationToken{ Token: "ghs_repository_token_123", // #nosec G101 -- test fixture, not a real credential ExpiresAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), }, nil) }, expectedToken: &connection.ExpirableSecureValue{ Token: common.RawSecureValue("ghs_repository_token_123"), ExpiresAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), }, }, { name: "nil repository returns error", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, }, repo: nil, expectedError: "a repository is required to generate a token", }, { name: "connection without GitHub config returns error", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GitlabConnectionType, OAuth: &provisioning.ConnectionOAuthConfig{ ClientID: "clientID", }, }, }, repo: &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{Name: "test-repo"}, Spec: provisioning.RepositorySpec{ Type: provisioning.GitHubRepositoryType, GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/test-owner/test-repo", }, }, }, expectedError: "connection is not a GitHub-based connection", }, { name: "repository without GitHub config returns error", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ Token: common.InlineSecureValue{ Create: common.RawSecureValue("jwt-token"), }, }, }, repo: &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{Name: "test-repo"}, Spec: provisioning.RepositorySpec{ Type: provisioning.GitLabRepositoryType, GitLab: &provisioning.GitLabRepositoryConfig{}, }, }, expectedError: "repository is not a GitHub-based repo", }, { name: "invalid repository URL returns error", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ Token: common.InlineSecureValue{ Create: common.RawSecureValue("jwt-token"), }, }, }, repo: &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{Name: "test-repo"}, Spec: provisioning.RepositorySpec{ Type: provisioning.GitHubRepositoryType, GitHub: &provisioning.GitHubRepositoryConfig{ URL: "invalid-url", }, }, }, expectedError: "failed to parse repo URL", }, { name: "GitHub API error", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ Token: common.InlineSecureValue{ Create: common.RawSecureValue("jwt-token"), }, }, }, repo: &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{Name: "test-repo"}, Spec: provisioning.RepositorySpec{ Type: provisioning.GitHubRepositoryType, GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/test-owner/test-repo", }, }, }, setupMock: func(mockFactory *github.MockGithubFactory) { mockClient := github.NewMockClient(t) mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("jwt-token"), mock.Anything).Return(mockClient, nil) mockClient.EXPECT().CreateInstallationAccessToken(mock.Anything, "456", "test-repo"). Return(github.InstallationToken{}, errors.New("API rate limit exceeded")) }, expectedError: "failed to create installation access token", }, { name: "GitHub error - unprocessable entity", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ Token: common.InlineSecureValue{ Create: common.RawSecureValue("jwt-token"), }, }, }, repo: &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{Name: "test-repo"}, Spec: provisioning.RepositorySpec{ Type: provisioning.GitHubRepositoryType, GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/test-owner/test-repo", }, }, }, setupMock: func(mockFactory *github.MockGithubFactory) { mockClient := github.NewMockClient(t) mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("jwt-token"), mock.Anything).Return(mockClient, nil) mockClient.EXPECT().CreateInstallationAccessToken(mock.Anything, "456", "test-repo"). Return(github.InstallationToken{}, github.ErrUnprocessableEntity) }, expectedError: connection.ErrRepositoryAccess.Error(), }, { name: "GitHub error - not found", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ Token: common.InlineSecureValue{ Create: common.RawSecureValue("jwt-token"), }, }, }, repo: &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{Name: "test-repo"}, Spec: provisioning.RepositorySpec{ Type: provisioning.GitHubRepositoryType, GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/test-owner/test-repo", }, }, }, setupMock: func(mockFactory *github.MockGithubFactory) { mockClient := github.NewMockClient(t) mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("jwt-token"), mock.Anything).Return(mockClient, nil) mockClient.EXPECT().CreateInstallationAccessToken(mock.Anything, "456", "test-repo"). Return(github.InstallationToken{}, github.ErrNotFound) }, expectedError: connection.ErrNotFound.Error(), }, { name: "GitHub authentication error", connection: &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ Token: common.InlineSecureValue{ Create: common.RawSecureValue("jwt-token"), }, }, }, repo: &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{Name: "test-repo"}, Spec: provisioning.RepositorySpec{ Type: provisioning.GitHubRepositoryType, GitHub: &provisioning.GitHubRepositoryConfig{ URL: "https://github.com/test-owner/test-repo", }, }, }, setupMock: func(mockFactory *github.MockGithubFactory) { mockClient := github.NewMockClient(t) mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("jwt-token"), mock.Anything).Return(mockClient, nil) mockClient.EXPECT().CreateInstallationAccessToken(mock.Anything, "456", "test-repo"). Return(github.InstallationToken{}, connection.ErrAuthentication) }, expectedError: connection.ErrAuthentication.Error(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockFactory := github.NewMockGithubFactory(t) if tt.setupMock != nil { tt.setupMock(mockFactory) } conn := github.NewConnection(tt.connection, mockFactory, github.ConnectionSecrets{ Token: tt.connection.Secure.Token.Create, PrivateKey: tt.connection.Secure.PrivateKey.Create, }) token, err := conn.GenerateRepositoryToken(t.Context(), tt.repo) if tt.expectedError != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.expectedError) } else { require.NoError(t, err) assert.Equal(t, tt.expectedToken, token) } }) } } func TestConnection_ListRepositories(t *testing.T) { t.Run("should list repositories successfully", func(t *testing.T) { c := &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ Token: common.InlineSecureValue{ Create: common.NewSecretValue("test-token"), }, }, } mockFactory := github.NewMockGithubFactory(t) mockClient := github.NewMockClient(t) mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("test-token"), mock.Anything).Return(mockClient, nil).Once() mockClient.EXPECT().CreateInstallationAccessToken(mock.Anything, "456", "").Return(github.InstallationToken{ Token: "ghs_installation_token", ExpiresAt: time.Now().Add(time.Hour), }, nil) mockInstallationClient := github.NewMockClient(t) mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("ghs_installation_token"), mock.Anything).Return(mockInstallationClient, nil) mockInstallationClient.EXPECT().ListInstallationRepositories(mock.Anything).Return([]github.Repository{ {Name: "repo1", Owner: "owner1", URL: "https://github.com/owner1/repo1"}, {Name: "repo2", Owner: "owner2", URL: "https://github.com/owner2/repo2"}, }, nil) conn := github.NewConnection(c, mockFactory, github.ConnectionSecrets{ Token: common.RawSecureValue("test-token"), }) repos, err := conn.ListRepositories(t.Context()) require.NoError(t, err) require.Len(t, repos, 2) assert.Equal(t, "repo1", repos[0].Name) assert.Equal(t, "owner1", repos[0].Owner) assert.Equal(t, "https://github.com/owner1/repo1", repos[0].URL) assert.Equal(t, "repo2", repos[1].Name) assert.Equal(t, "owner2", repos[1].Owner) assert.Equal(t, "https://github.com/owner2/repo2", repos[1].URL) }) t.Run("should return error when GitHub config is nil", func(t *testing.T) { c := &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GitlabConnectionType, }, } mockFactory := github.NewMockGithubFactory(t) conn := github.NewConnection(c, mockFactory, github.ConnectionSecrets{}) _, err := conn.ListRepositories(t.Context()) require.Error(t, err) assert.Contains(t, err.Error(), "github configuration is required") }) t.Run("should return error when listing repositories fails", func(t *testing.T) { c := &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ Token: common.InlineSecureValue{ Create: common.NewSecretValue("test-token"), }, }, } mockFactory := github.NewMockGithubFactory(t) mockClient := github.NewMockClient(t) mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("test-token"), mock.Anything).Return(mockClient, nil).Once() mockClient.EXPECT().CreateInstallationAccessToken(mock.Anything, "456", "").Return(github.InstallationToken{ Token: "ghs_installation_token", ExpiresAt: time.Now().Add(time.Hour), }, nil) mockInstallationClient := github.NewMockClient(t) mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("ghs_installation_token"), mock.Anything).Return(mockInstallationClient, nil) mockInstallationClient.EXPECT().ListInstallationRepositories(mock.Anything).Return(nil, assert.AnError) conn := github.NewConnection(c, mockFactory, github.ConnectionSecrets{ Token: common.RawSecureValue("test-token"), }) _, err := conn.ListRepositories(t.Context()) require.Error(t, err) assert.Contains(t, err.Error(), "list installation repositories") }) t.Run("should return error when creating installation token fails", func(t *testing.T) { c := &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ Token: common.InlineSecureValue{ Create: common.NewSecretValue("test-token"), }, }, } mockFactory := github.NewMockGithubFactory(t) mockClient := github.NewMockClient(t) mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("test-token"), mock.Anything).Return(mockClient, nil) mockClient.EXPECT().CreateInstallationAccessToken(mock.Anything, "456", "").Return(github.InstallationToken{}, assert.AnError) conn := github.NewConnection(c, mockFactory, github.ConnectionSecrets{ Token: common.RawSecureValue("test-token"), }) _, err := conn.ListRepositories(t.Context()) require.Error(t, err) assert.Contains(t, err.Error(), "failed to create installation access token") }) t.Run("should return error when service unavailable during token creation", func(t *testing.T) { c := &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ Token: common.InlineSecureValue{ Create: common.NewSecretValue("test-token"), }, }, } mockFactory := github.NewMockGithubFactory(t) mockClient := github.NewMockClient(t) mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("test-token"), mock.Anything).Return(mockClient, nil) mockClient.EXPECT().CreateInstallationAccessToken(mock.Anything, "456", "").Return(github.InstallationToken{}, github.ErrServiceUnavailable) conn := github.NewConnection(c, mockFactory, github.ConnectionSecrets{ Token: common.RawSecureValue("test-token"), }) _, err := conn.ListRepositories(t.Context()) require.Error(t, err) assert.Contains(t, err.Error(), "failed to create installation access token") }) t.Run("should return empty list when no repositories", func(t *testing.T) { c := &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{ AppID: "123", InstallationID: "456", }, }, Secure: provisioning.ConnectionSecure{ Token: common.InlineSecureValue{ Create: common.NewSecretValue("test-token"), }, }, } mockFactory := github.NewMockGithubFactory(t) mockClient := github.NewMockClient(t) mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("test-token"), mock.Anything).Return(mockClient, nil).Once() mockClient.EXPECT().CreateInstallationAccessToken(mock.Anything, "456", "").Return(github.InstallationToken{ Token: "ghs_installation_token", ExpiresAt: time.Now().Add(time.Hour), }, nil) mockInstallationClient := github.NewMockClient(t) mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("ghs_installation_token"), mock.Anything).Return(mockInstallationClient, nil) mockInstallationClient.EXPECT().ListInstallationRepositories(mock.Anything).Return([]github.Repository{}, nil) conn := github.NewConnection(c, mockFactory, github.ConnectionSecrets{ Token: common.RawSecureValue("test-token"), }) repos, err := conn.ListRepositories(t.Context()) require.NoError(t, err) require.Len(t, repos, 0) }) } // TestNewConnectionWithCustomConfig verifies that a connection built with an injected // ConnectionConfig resolves its GitHub App parameters from that config. spec.github carries // a different appID, so observing the injected value proves the custom config takes precedence. func TestNewConnectionWithCustomConfig(t *testing.T) { privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testPrivateKeyPEM)) cfg := github.NewMockConnectionConfig(t) cfg.EXPECT().AppID().Return("custom-app-id") obj := &provisioning.Connection{ ObjectMeta: metav1.ObjectMeta{Name: "test-connection"}, Spec: provisioning.ConnectionSpec{ Type: provisioning.GithubConnectionType, GitHub: &provisioning.GitHubConnectionConfig{AppID: "spec-app-id"}, }, } conn := github.NewConnectionWithCustomConfig(obj, github.NewMockGithubFactory(t), github.ConnectionSecrets{ PrivateKey: common.RawSecureValue(privateKeyBase64), }, cfg) token, err := conn.GenerateConnectionToken(t.Context()) require.NoError(t, err) // The JWT issuer is the appID; it must come from the injected config, not spec.github. privateKeyPEM, err := base64.StdEncoding.DecodeString(privateKeyBase64) require.NoError(t, err) key, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyPEM) require.NoError(t, err) parsedToken, err := jwt.Parse(string(token), func(_ *jwt.Token) (any, error) { return &key.PublicKey, nil }, jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()})) require.NoError(t, err) claims, ok := parsedToken.Claims.(jwt.MapClaims) require.True(t, ok) assert.Equal(t, "custom-app-id", claims["iss"], "issuer should come from the injected config, not spec.github") } func getIssuingAndExpirationTimeFromToken(token common.RawSecureValue) (time.Time, time.Time, error) { parser := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()})) parsedToken, _, err := parser.ParseUnverified(string(token), &jwt.RegisteredClaims{}) if err != nil { return time.Time{}, time.Time{}, fmt.Errorf("failed to parse token: %w", err) } claims, ok := parsedToken.Claims.(*jwt.RegisteredClaims) if !ok { return time.Time{}, time.Time{}, fmt.Errorf("unexpected token claims") } return claims.IssuedAt.Time, claims.ExpiresAt.Time, nil }