Provisioning: count for Connection reference in Validation and Tester

This commit is contained in:
Daniele Ferru
2026-01-14 23:35:30 +01:00
parent 91ab753368
commit 39a5836ff0
23 changed files with 2423 additions and 522 deletions
@@ -1,11 +1,13 @@
package github
package github_test
import (
"context"
"encoding/base64"
"errors"
"testing"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
"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"
@@ -43,132 +45,386 @@ B8Uc0WUgheB4+yVKGnYpYaSOgFFI5+1BYUva/wDHLy2pWHz39Usb
-----END RSA PRIVATE KEY-----`
func TestConnection_Mutate(t *testing.T) {
t.Run("should add URL to Github connection", 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",
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),
},
},
},
Secure: provisioning.ConnectionSecure{
PrivateKey: common.InlineSecureValue{
Name: "test-private-key",
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 generate JWT token when private key is provided",
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),
},
},
},
}
mockFactory := NewMockGithubFactory(t)
conn := NewConnection(c, mockFactory)
require.NoError(t, conn.Mutate(context.Background()))
assert.Equal(t, "https://github.com/settings/installations/456", c.Spec.URL)
})
t.Run("should generate JWT token when private key is provided", func(t *testing.T) {
privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testPrivateKeyPEM))
c := &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.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.False(t, connection.Secure.Token.Create.IsZero(), "JWT token should be generated")
},
},
{
name: "should not generate JWT token when token is already there",
connection: &provisioning.Connection{
ObjectMeta: metav1.ObjectMeta{Name: "test-connection", Generation: 1},
Spec: provisioning.ConnectionSpec{
Type: provisioning.GithubConnectionType,
GitHub: &provisioning.GitHubConnectionConfig{
AppID: "123",
InstallationID: "456",
},
},
Secure: provisioning.ConnectionSecure{
PrivateKey: common.InlineSecureValue{
// The private key is already in the stoere
Name: "somePrivateKey",
},
Token: common.InlineSecureValue{
Create: common.NewSecretValue("someToken"),
},
},
},
Secure: provisioning.ConnectionSecure{
PrivateKey: common.InlineSecureValue{
Create: common.NewSecretValue(privateKeyBase64),
secrets: github.ConnectionSecrets{
PrivateKey: common.NewSecretValue(privateKeyBase64),
Token: common.NewSecretValue("someToken"),
},
wantErr: false,
validateResult: func(t *testing.T, connection *provisioning.Connection) {
assert.Equal(t, "https://github.com/settings/installations/456", connection.Spec.URL)
assert.False(t, connection.Secure.Token.Create.IsZero(), "JWT token should be generated")
assert.Equal(t, "someToken", connection.Secure.Token.Create.DangerouslyExposeAndConsumeValue())
},
},
{
name: "should do nothing when GitHub config is nil",
connection: &provisioning.Connection{
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
Spec: provisioning.ConnectionSpec{
Type: provisioning.GitlabConnectionType,
Gitlab: &provisioning.GitlabConnectionConfig{
ClientID: "clientID",
},
},
},
}
mockFactory := NewMockGithubFactory(t)
conn := NewConnection(c, mockFactory)
require.NoError(t, conn.Mutate(context.Background()))
assert.Equal(t, "https://github.com/settings/installations/456", c.Spec.URL)
assert.False(t, c.Secure.Token.Create.IsZero(), "JWT token should be generated")
})
t.Run("should do nothing when GitHub config is nil", func(t *testing.T) {
c := &provisioning.Connection{
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
Spec: provisioning.ConnectionSpec{
Type: provisioning.GitlabConnectionType,
Gitlab: &provisioning.GitlabConnectionConfig{
ClientID: "clientID",
secrets: github.ConnectionSecrets{},
wantErr: false,
},
{
name: "should fail when private key is not base64",
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("invalid-key"),
},
},
},
}
mockFactory := NewMockGithubFactory(t)
conn := NewConnection(c, mockFactory)
require.NoError(t, conn.Mutate(context.Background()))
})
t.Run("should fail when private key is not base64", 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",
secrets: github.ConnectionSecrets{
PrivateKey: "invalid-key",
},
wantErr: true,
validateError: func(t *testing.T, err error) {
assert.Contains(t, err.Error(), "failed to generate JWT token")
assert.Contains(t, err.Error(), "failed to decode base64 private key")
},
},
{
name: "should fail when private key is invalid",
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(base64.StdEncoding.EncodeToString([]byte("invalid-key"))),
},
},
},
Secure: provisioning.ConnectionSecure{
PrivateKey: common.InlineSecureValue{
Create: common.NewSecretValue("invalid-key"),
secrets: github.ConnectionSecrets{},
wantErr: true,
validateError: func(t *testing.T, err error) {
assert.Contains(t, err.Error(), "failed to generate JWT token")
assert.Contains(t, err.Error(), "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)
err := conn.Mutate(context.Background())
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_GenerateRepositoryToken(t *testing.T) {
tests := []struct {
name string
connection *provisioning.Connection
repo *provisioning.Repository
setupMock func(*github.MockGithubFactory)
expectedToken common.RawSecureValue
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"),
},
},
},
}
mockFactory := NewMockGithubFactory(t)
conn := NewConnection(c, mockFactory)
err := conn.Mutate(context.Background())
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to generate JWT token")
assert.Contains(t, err.Error(), "failed to decode base64 private key")
})
t.Run("should fail when private key is invalid", 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",
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",
},
},
},
Secure: provisioning.ConnectionSecure{
PrivateKey: common.InlineSecureValue{
Create: common.NewSecretValue(base64.StdEncoding.EncodeToString([]byte("invalid-key"))),
setupMock: func(mockFactory *github.MockGithubFactory) {
mockClient := github.NewMockClient(t)
mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("jwt-token")).Return(mockClient)
mockClient.EXPECT().CreateInstallationAccessToken(mock.Anything, "456", "test-repo").
Return(github.InstallationToken{Token: "ghs_repository_token_123", ExpiresAt: "2024-01-01T00:00:00Z"}, nil)
},
expectedToken: common.RawSecureValue("ghs_repository_token_123"),
},
{
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,
Gitlab: &provisioning.GitlabConnectionConfig{
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 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.GitHubRepositoryType,
GitHub: nil,
},
},
expectedError: "repository is not a GitHub 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")).Return(mockClient)
mockClient.EXPECT().CreateInstallationAccessToken(mock.Anything, "456", "test-repo").
Return(github.InstallationToken{}, errors.New("API rate limit exceeded"))
},
expectedError: "failed to create installation access token",
},
}
mockFactory := NewMockGithubFactory(t)
conn := NewConnection(c, mockFactory)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockFactory := github.NewMockGithubFactory(t)
if tt.setupMock != nil {
tt.setupMock(mockFactory)
}
err := conn.Mutate(context.Background())
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to generate JWT token")
assert.Contains(t, err.Error(), "failed to parse private key")
})
conn := github.NewConnection(tt.connection, mockFactory, github.ConnectionSecrets{
Token: tt.connection.Secure.Token.Create,
PrivateKey: tt.connection.Secure.PrivateKey.Create,
})
token, err := conn.GenerateRepositoryToken(context.Background(), 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_Validate(t *testing.T) {
tests := []struct {
name string
connection *provisioning.Connection
setupMock func(*MockGithubFactory)
setupMock func(*github.MockGithubFactory)
wantErr bool
errMsgContains []string
}{
@@ -314,12 +570,12 @@ func TestConnection_Validate(t *testing.T) {
},
},
wantErr: false,
setupMock: func(mockFactory *MockGithubFactory) {
mockClient := NewMockClient(t)
setupMock: func(mockFactory *github.MockGithubFactory) {
mockClient := github.NewMockClient(t)
mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("test-token")).Return(mockClient)
mockClient.EXPECT().GetApp(mock.Anything).Return(App{ID: 123, Slug: "test-app"}, nil)
mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(AppInstallation{ID: 456}, nil)
mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ID: 123, Slug: "test-app"}, nil)
mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{ID: 456}, nil)
},
},
{
@@ -344,11 +600,11 @@ func TestConnection_Validate(t *testing.T) {
},
wantErr: true,
errMsgContains: []string{"spec.token", "[REDACTED]"},
setupMock: func(mockFactory *MockGithubFactory) {
mockClient := NewMockClient(t)
setupMock: func(mockFactory *github.MockGithubFactory) {
mockClient := github.NewMockClient(t)
mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("test-token")).Return(mockClient)
mockClient.EXPECT().GetApp(mock.Anything).Return(App{}, assert.AnError)
mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{}, assert.AnError)
},
},
{
@@ -373,11 +629,11 @@ func TestConnection_Validate(t *testing.T) {
},
wantErr: true,
errMsgContains: []string{"spec.appID"},
setupMock: func(mockFactory *MockGithubFactory) {
mockClient := NewMockClient(t)
setupMock: func(mockFactory *github.MockGithubFactory) {
mockClient := github.NewMockClient(t)
mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("test-token")).Return(mockClient)
mockClient.EXPECT().GetApp(mock.Anything).Return(App{ID: 444, Slug: "test-app"}, nil)
mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ID: 444, Slug: "test-app"}, nil)
},
},
{
@@ -402,24 +658,27 @@ func TestConnection_Validate(t *testing.T) {
},
wantErr: true,
errMsgContains: []string{"spec.installationID", "456"},
setupMock: func(mockFactory *MockGithubFactory) {
mockClient := NewMockClient(t)
setupMock: func(mockFactory *github.MockGithubFactory) {
mockClient := github.NewMockClient(t)
mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("test-token")).Return(mockClient)
mockClient.EXPECT().GetApp(mock.Anything).Return(App{ID: 123, Slug: "test-app"}, nil)
mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(AppInstallation{}, assert.AnError)
mockClient.EXPECT().GetApp(mock.Anything).Return(github.App{ID: 123, Slug: "test-app"}, nil)
mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(github.AppInstallation{}, assert.AnError)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockFactory := NewMockGithubFactory(t)
mockFactory := github.NewMockGithubFactory(t)
if tt.setupMock != nil {
tt.setupMock(mockFactory)
}
conn := NewConnection(tt.connection, mockFactory)
conn := github.NewConnection(tt.connection, mockFactory, github.ConnectionSecrets{
PrivateKey: tt.connection.Secure.PrivateKey.Create,
Token: tt.connection.Secure.Token.Create,
})
err := conn.Validate(context.Background())
if tt.wantErr {
assert.Error(t, err)