Skip to content

Commit

Permalink
inject perses client and mock it for dashboard CRD tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jgbernalp committed Feb 19, 2024
1 parent c9fe4f6 commit 03f6103
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 67 deletions.
184 changes: 117 additions & 67 deletions controllers/dashboard_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ package controllers
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
persesv1alpha1 "github.com/perses/perses-operator/api/v1alpha1"
dashboardcontroller "github.com/perses/perses-operator/controllers/dashboards"
internal "github.com/perses/perses-operator/internal/perses"
common "github.com/perses/perses-operator/internal/perses/common"
"github.com/perses/perses/pkg/client/perseshttp"
persesv1 "github.com/perses/perses/pkg/model/api/v1"
persescommon "github.com/perses/perses/pkg/model/api/v1/common"
persesdashboard "github.com/perses/perses/pkg/model/api/v1/dashboard"
Expand All @@ -24,64 +25,101 @@ import (

var _ = Describe("Dashboard controller", func() {
Context("Dashboard controller test", func() {
const PersesName = "test-perses-dashboard"
const PersesName = "perses-for-dashboard"
const PersesNamespace = "perses-dashboard-test"
const DashboardName = "my-custom-dashboard"

ctx := context.Background()

namespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: PersesName,
Namespace: PersesName,
Name: PersesNamespace,
Namespace: PersesNamespace,
},
}

typeNamespaceName := types.NamespacedName{Name: PersesName, Namespace: PersesName}
persesNamespaceName := types.NamespacedName{Name: PersesName, Namespace: PersesNamespace}
dashboardNamespaceName := types.NamespacedName{Name: DashboardName, Namespace: PersesNamespace}

persesImage := "perses-dev.io/perses:test"

newDashboard := &persesv1.Dashboard{
Kind: "Dashboard",
Metadata: persesv1.ProjectMetadata{
Metadata: persesv1.Metadata{
Name: DashboardName,
},
},
Spec: persesv1.DashboardSpec{
Display: &persescommon.Display{
Name: DashboardName,
},
Layouts: []persesdashboard.Layout{},
Panels: map[string]*persesv1.Panel{
"panel1": {
Kind: "Panel",
Spec: persesv1.PanelSpec{
Display: persesv1.PanelDisplay{
Name: "test-panel",
},
Plugin: persescommon.Plugin{
Kind: "PrometheusPlugin",
Spec: map[string]interface{}{},
},
},
},
},
},
}

BeforeEach(func() {
By("Creating the Namespace to perform the tests")
err := k8sClient.Create(ctx, namespace)
Expect(err).To(Not(HaveOccurred()))

By("Setting the Image ENV VAR which stores the Operand image")
err = os.Setenv("PERSES_IMAGE", persesImage)
Expect(err).To(Not(HaveOccurred()))
})

AfterEach(func() {
By("Deleting the Namespace to perform the tests")
_ = k8sClient.Delete(ctx, namespace)

By("Removing the Image ENV VAR which stores the Operand image")
_ = os.Unsetenv("PERSES_IMAGE")
})

It("should successfully reconcile a custom resource dashboard for Perses", func() {
By("Creating the custom resource for the Kind Perses")
perses := &persesv1alpha1.Perses{}
err := k8sClient.Get(ctx, persesNamespaceName, perses)
if err != nil && errors.IsNotFound(err) {
perses := &persesv1alpha1.Perses{
ObjectMeta: metav1.ObjectMeta{
Name: PersesName,
Namespace: PersesNamespace,
},
Spec: persesv1alpha1.PersesSpec{
ContainerPort: 8080,
},
}

err = k8sClient.Create(ctx, perses)
Expect(err).To(Not(HaveOccurred()))
}

By("Creating the custom resource for the Kind PersesDashboard")
dashboard := &persesv1alpha1.PersesDashboard{}
err := k8sClient.Get(ctx, typeNamespaceName, dashboard)
err = k8sClient.Get(ctx, dashboardNamespaceName, dashboard)
if err != nil && errors.IsNotFound(err) {
perses := &persesv1alpha1.PersesDashboard{
ObjectMeta: metav1.ObjectMeta{
Name: PersesName,
Namespace: namespace.Name,
Name: DashboardName,
Namespace: PersesNamespace,
},
Spec: persesv1alpha1.Dashboard{
Dashboard: persesv1.Dashboard{
Kind: "Dashboard",
Spec: persesv1.DashboardSpec{
Display: &persescommon.Display{
Name: "test-dashboard",
},
Layouts: []persesdashboard.Layout{},
Panels: map[string]*persesv1.Panel{
"panel1": {
Kind: "Panel",
Spec: persesv1.PanelSpec{
Display: persesv1.PanelDisplay{
Name: "test-panel",
},
Plugin: persescommon.Plugin{
Kind: "PrometheusPlugin",
Spec: map[string]interface{}{},
},
},
},
},
},
},
Dashboard: *newDashboard,
},
}

Expand All @@ -92,70 +130,82 @@ var _ = Describe("Dashboard controller", func() {
By("Checking if the custom resource was successfully created")
Eventually(func() error {
found := &persesv1alpha1.PersesDashboard{}
return k8sClient.Get(ctx, typeNamespaceName, found)
return k8sClient.Get(ctx, dashboardNamespaceName, found)
}, time.Minute, time.Second).Should(Succeed())

svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "expected response")
}))
defer svr.Close()
// Mock the Perses API to assert that Is creating a new dashboard when reconciling
mockPersesClient := new(internal.MockClient)
mockDashboard := new(internal.MockDashboard)

mockPersesClient.On("Dashboard", PersesNamespace).Return(mockDashboard)
getDashboard := mockDashboard.On("Get", DashboardName).Return(&persesv1.Dashboard{}, perseshttp.RequestNotFoundError)
mockDashboard.On("Create", newDashboard).Return(&persesv1.Dashboard{}, nil)

By("Reconciling the custom resource created")
dashboardReconciler := &dashboardcontroller.PersesDashboardReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
ClientFactory: common.NewWithURL(svr.URL),
ClientFactory: common.NewWithClient(mockPersesClient),
}

// Errors might arise during reconciliation, but we are checking the final state of the resources
_, err = dashboardReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespaceName,
NamespacedName: dashboardNamespaceName,
})

Expect(err).To(Not(HaveOccurred()))

// By("Checking if the Perses API was called to create a dashboard")
// Eventually(func() error {
// return err
// }, time.Minute, time.Second).Should(Succeed())
// The dashboard was created in the Perses API
getDashboard.Unset()
mockDashboard.On("Get", DashboardName).Return(&persesv1.Dashboard{}, nil)

By("Checking if the Perses API was called to create a dashboard")
Eventually(func() error {
if !mockDashboard.AssertExpectations(GinkgoT()) {
return fmt.Errorf("The Perses API was not called to create a dashboard")
}
return nil
}, time.Minute, time.Second).Should(Succeed())

By("Checking the latest Status Condition added to the Perses instance")
By("Checking the latest Status Condition added to the Perses dashboard instance")
Eventually(func() error {
if dashboard.Status.Conditions != nil && len(dashboard.Status.Conditions) != 0 {
latestStatusCondition := dashboard.Status.Conditions[len(dashboard.Status.Conditions)-1]
dashboardWithStatus := &persesv1alpha1.PersesDashboard{}
err = k8sClient.Get(ctx, dashboardNamespaceName, dashboardWithStatus)

if dashboardWithStatus.Status.Conditions == nil || len(dashboardWithStatus.Status.Conditions) == 0 {
return fmt.Errorf("No status condition was added to the perses dashboard instance")
} else {
latestStatusCondition := dashboardWithStatus.Status.Conditions[len(dashboardWithStatus.Status.Conditions)-1]
expectedLatestStatusCondition := metav1.Condition{Type: common.TypeAvailablePerses,
Status: metav1.ConditionTrue, Reason: "Reconciling",
Message: fmt.Sprintf("Dashboard (%s) created successfully", dashboard.Name)}
if latestStatusCondition != expectedLatestStatusCondition {
return fmt.Errorf("The latest status condition added to the perses dashboard instance is not as expected")
Message: fmt.Sprintf("Dashboard (%s) created successfully", dashboardWithStatus.Name)}
if latestStatusCondition.Message != expectedLatestStatusCondition.Message && latestStatusCondition.Reason != expectedLatestStatusCondition.Reason && latestStatusCondition.Status != expectedLatestStatusCondition.Status && latestStatusCondition.Type != expectedLatestStatusCondition.Type {
return fmt.Errorf("The latest status condition added to the perses dashboard instance is not as expected, got: %v", expectedLatestStatusCondition)
}
}
return nil

return err
}, time.Minute, time.Second).Should(Succeed())

persesToDelete := &persesv1alpha1.PersesDashboard{}
err = k8sClient.Get(ctx, typeNamespaceName, persesToDelete)
mockDashboard.On("Delete", DashboardName).Return(nil)

dashboardToDelete := &persesv1alpha1.PersesDashboard{}
err = k8sClient.Get(ctx, dashboardNamespaceName, dashboardToDelete)
Expect(err).To(Not(HaveOccurred()))

By("Deleting the custom resource")
err = k8sClient.Delete(ctx, persesToDelete)
err = k8sClient.Delete(ctx, dashboardToDelete)
Expect(err).To(Not(HaveOccurred()))

// By("Checking if the Perses API was called to delete a dashboard")
// Eventually(func() error {
// return err
// }, time.Minute, time.Second).Should(Succeed())
_, err = dashboardReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: dashboardNamespaceName,
})

Expect(err).To(Not(HaveOccurred()))

By("Checking the latest Status Condition added to the Perses instance")
By("Checking if the Perses API was called to delete a dashboard")
Eventually(func() error {
if dashboard.Status.Conditions != nil && len(dashboard.Status.Conditions) != 0 {
latestStatusCondition := dashboard.Status.Conditions[len(dashboard.Status.Conditions)-1]
expectedLatestStatusCondition := metav1.Condition{Type: common.TypeAvailablePerses,
Status: metav1.ConditionTrue, Reason: "Finalizing",
Message: fmt.Sprintf("Finalizer operations for custom resource %s name were successfully accomplished", dashboard.Name)}
if latestStatusCondition != expectedLatestStatusCondition {
return fmt.Errorf("The latest status condition added to the perses instance is not as expected")
}
if !mockDashboard.AssertExpectations(GinkgoT()) {
return fmt.Errorf("The Perses API was not called to create a dashboard")
}
return nil
}, time.Minute, time.Second).Should(Succeed())
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/onsi/gomega v1.27.10
github.com/perses/perses v0.43.1-0.20240212140825-8efdfab0f93d
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.28.3
k8s.io/apimachinery v0.28.3
Expand Down Expand Up @@ -48,11 +49,13 @@ require (
github.com/nexucis/lamenv v0.5.2 // indirect
github.com/perses/common v0.22.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.18.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.46.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.25.0 // indirect
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand Down
12 changes: 12 additions & 0 deletions internal/perses/common/perses_client_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,15 @@ func (f *PersesClientFactoryWithURL) CreateClient(config persesv1alpha1.Perses)

return persesClient, nil
}

type PersesClientFactoryWithClient struct {
client v1.ClientInterface
}

func NewWithClient(client v1.ClientInterface) PersesClientFactory {
return &PersesClientFactoryWithClient{client: client}
}

func (f *PersesClientFactoryWithClient) CreateClient(config persesv1alpha1.Perses) (v1.ClientInterface, error) {
return f.client, nil
}
54 changes: 54 additions & 0 deletions internal/perses/perses_mock_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package perses

import (
v1 "github.com/perses/perses/pkg/client/api/v1"
modelv1 "github.com/perses/perses/pkg/model/api/v1"
"github.com/stretchr/testify/mock"
)

type MockClient struct {
v1.ClientInterface
mock.Mock
}

type project struct {
v1.ProjectInterface
}

func (p *project) Get(name string) (*modelv1.Project, error) {
return nil, nil
}

func (c *MockClient) Project() v1.ProjectInterface {
return &project{}
}

type MockDashboard struct {
v1.DashboardInterface
mock.Mock
}

func (c *MockClient) Dashboard(project string) v1.DashboardInterface {
args := c.Called(project)
return args.Get(0).(v1.DashboardInterface)
}

func (d *MockDashboard) Get(name string) (*modelv1.Dashboard, error) {
args := d.Called(name)
return args.Get(0).(*modelv1.Dashboard), args.Error(1)
}

func (d *MockDashboard) Update(dashboard *modelv1.Dashboard) (*modelv1.Dashboard, error) {
args := d.Called(dashboard)
return args.Get(0).(*modelv1.Dashboard), args.Error(1)
}

func (d *MockDashboard) Delete(name string) error {
args := d.Called(name)
return args.Error(0)
}

func (d *MockDashboard) Create(dashboard *modelv1.Dashboard) (*modelv1.Dashboard, error) {
args := d.Called(dashboard)
return args.Get(0).(*modelv1.Dashboard), args.Error(1)
}

0 comments on commit 03f6103

Please sign in to comment.