diff --git a/pkg/promlib/admission_handler_test.go b/pkg/promlib/admission_handler_test.go new file mode 100644 index 00000000000..4cc961a9256 --- /dev/null +++ b/pkg/promlib/admission_handler_test.go @@ -0,0 +1,247 @@ +package promlib + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/proto" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_ValidateAdmission(t *testing.T) { + t.Run("should return error when MutateAdmission fails and response is nil", func(t *testing.T) { + httpProvider := getMockProvider[*healthCheckSuccessRoundTripper]() + logger := backend.NewLoggerWith("logger", "test") + s := &Service{ + im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, logger, mockExtendClientOpts)), + logger: logger, + } + + req := &backend.AdmissionRequest{ + Kind: backend.GroupVersionKind{ + Group: "grafana-plugin-sdk-go", + Kind: "DataSourceInstanceSettings", + }, + ObjectBytes: []byte(`{"foo":"bar"}`), + } + + actual, err := s.ValidateAdmission(context.Background(), req) + assert.ErrorContains(t, err, "cannot parse invalid") + assert.Nil(t, actual) + }) + + t.Run("should return ValidationResponse when MutateAdmission succeeds", func(t *testing.T) { + httpProvider := getMockProvider[*healthCheckSuccessRoundTripper]() + logger := backend.NewLoggerWith("logger", "test") + s := &Service{ + im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, logger, mockExtendClientOpts)), + logger: logger, + } + + pb := &pluginv2.DataSourceInstanceSettings{ + ApiVersion: "v0alpha1", + Url: "https://grafana.example.com/", + } + data, err := proto.Marshal(pb) + assert.NoError(t, err) + + expected := &backend.ValidationResponse{ + Allowed: true, + } + + req := &backend.AdmissionRequest{ + Kind: backend.GroupVersionKind{ + Group: "grafana-plugin-sdk-go", + Kind: "DataSourceInstanceSettings", + }, + ObjectBytes: data, + } + + actual, err := s.ValidateAdmission(context.Background(), req) + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) +} + +func Test_MutateAdmission(t *testing.T) { + t.Run("should fail when Kind and Group do not match expected values", func(t *testing.T) { + httpProvider := getMockProvider[*healthCheckSuccessRoundTripper]() + logger := backend.NewLoggerWith("logger", "test") + s := &Service{ + im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, logger, mockExtendClientOpts)), + logger: logger, + } + + expected := getBadRequest("expected DataSourceInstanceSettings protobuf payload") + + req := &backend.AdmissionRequest{ + Kind: backend.GroupVersionKind{ + Group: "example.k8s.io", + Kind: "Pod", + }, + } + + actual, err := s.MutateAdmission(context.Background(), req) + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) + + t.Run("should return an error when object bytes is nil", func(t *testing.T) { + httpProvider := getMockProvider[*healthCheckSuccessRoundTripper]() + logger := backend.NewLoggerWith("logger", "test") + s := &Service{ + im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, logger, mockExtendClientOpts)), + logger: logger, + } + + expected := getBadRequest("missing datasource settings") + + req := &backend.AdmissionRequest{ + Kind: backend.GroupVersionKind{ + Group: "grafana-plugin-sdk-go", + Kind: "DataSourceInstanceSettings", + }, + } + + actual, err := s.MutateAdmission(context.Background(), req) + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) + + t.Run("should return an error when protobuf payload conversion fails", func(t *testing.T) { + httpProvider := getMockProvider[*healthCheckSuccessRoundTripper]() + logger := backend.NewLoggerWith("logger", "test") + s := &Service{ + im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, logger, mockExtendClientOpts)), + logger: logger, + } + + req := &backend.AdmissionRequest{ + Kind: backend.GroupVersionKind{ + Group: "grafana-plugin-sdk-go", + Kind: "DataSourceInstanceSettings", + }, + ObjectBytes: []byte(`{"foo":"bar"}`), + } + + actual, err := s.MutateAdmission(context.Background(), req) + assert.ErrorContains(t, err, "cannot parse invalid") + assert.Nil(t, actual) + }) + + t.Run("should return bad request error when settings APIVersion is invalid", func(t *testing.T) { + httpProvider := getMockProvider[*healthCheckSuccessRoundTripper]() + logger := backend.NewLoggerWith("logger", "test") + s := &Service{ + im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, logger, mockExtendClientOpts)), + logger: logger, + } + + pb := &pluginv2.DataSourceInstanceSettings{ + ApiVersion: "v0alpha2", + } + data, err := proto.Marshal(pb) + assert.NoError(t, err) + + expected := getBadRequest(fmt.Sprintf("expected apiVersion: v0alpha1, found: %s", pb.ApiVersion)) + + req := &backend.AdmissionRequest{ + Kind: backend.GroupVersionKind{ + Group: "grafana-plugin-sdk-go", + Kind: "DataSourceInstanceSettings", + }, + ObjectBytes: data, + } + + actual, err := s.MutateAdmission(context.Background(), req) + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) + + t.Run("should return bad request error when settings URL is invalid", func(t *testing.T) { + httpProvider := getMockProvider[*healthCheckSuccessRoundTripper]() + logger := backend.NewLoggerWith("logger", "test") + s := &Service{ + im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, logger, mockExtendClientOpts)), + logger: logger, + } + + pb := &pluginv2.DataSourceInstanceSettings{ + ApiVersion: "v0alpha1", + } + data, err := proto.Marshal(pb) + assert.NoError(t, err) + + expected := getBadRequest("missing URL value") + + req := &backend.AdmissionRequest{ + Kind: backend.GroupVersionKind{ + Group: "grafana-plugin-sdk-go", + Kind: "DataSourceInstanceSettings", + }, + ObjectBytes: data, + } + + actual, err := s.MutateAdmission(context.Background(), req) + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) + + t.Run("should return successfully mutation response", func(t *testing.T) { + httpProvider := getMockProvider[*healthCheckSuccessRoundTripper]() + logger := backend.NewLoggerWith("logger", "test") + s := &Service{ + im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, logger, mockExtendClientOpts)), + logger: logger, + } + + pb := &pluginv2.DataSourceInstanceSettings{ + ApiVersion: "v0alpha1", + Url: "https://grafana.example.com/", + } + data, err := proto.Marshal(pb) + assert.NoError(t, err) + + expected := &backend.MutationResponse{ + Allowed: true, + ObjectBytes: data, + } + + req := &backend.AdmissionRequest{ + Kind: backend.GroupVersionKind{ + Group: "grafana-plugin-sdk-go", + Kind: "DataSourceInstanceSettings", + }, + ObjectBytes: data, + } + + actual, err := s.MutateAdmission(context.Background(), req) + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) +} + +func Test_getBadRequest(t *testing.T) { + t.Run("should get a successful bad request", func(t *testing.T) { + msg := "missing URL value" + + expected := &backend.MutationResponse{ + Allowed: false, + Result: &backend.StatusResult{ + Status: "Failure", + Message: "missing URL value", + Reason: string(metav1.StatusReasonBadRequest), + Code: http.StatusBadRequest, + }, + } + + actual := getBadRequest(msg) + assert.Equal(t, expected, actual) + }) +}