From 4314bdfeb1033031f5e2908e895dc306263ef911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=86gir=20M=C3=A1ni=20Hauksson?= <54936225+sourcehawk@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:41:50 +0200 Subject: [PATCH 1/2] test(examples): controller-level component golden tests Extract Build*Component() methods from the reconcile loops in the component-prerequisites, extraction-and-guards, and grace-inconsistency examples so tests can build components through the same assembly path the controller uses. Add controller_test.go + testdata/ golden snapshots for each example using golden.AssertComponentYAML. Update resource tests in all three examples (and custom-resource) to call the actual factory functions rather than duplicating builder construction, so the golden files reflect real factory output. Co-Authored-By: Claude Sonnet 4.6 --- .../app/controller_test.go | 63 +++++++++++++++++++ .../app/controller_test.go | 45 +++++++++++++ .../app/controller_test.go | 43 +++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 examples/component-prerequisites/app/controller_test.go create mode 100644 examples/extraction-and-guards/app/controller_test.go create mode 100644 examples/grace-inconsistency/app/controller_test.go diff --git a/examples/component-prerequisites/app/controller_test.go b/examples/component-prerequisites/app/controller_test.go new file mode 100644 index 00000000..7c9d73fb --- /dev/null +++ b/examples/component-prerequisites/app/controller_test.go @@ -0,0 +1,63 @@ +package app_test + +import ( + "flag" + "testing" + + "github.com/sourcehawk/operator-component-framework/examples/component-prerequisites/app" + "github.com/sourcehawk/operator-component-framework/examples/component-prerequisites/resources" + "github.com/sourcehawk/operator-component-framework/pkg/testing/golden" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +var update = flag.Bool("update", false, "update golden files") + +func testOwner() *app.ExampleApp { + owner := &app.ExampleApp{ + Spec: app.ExampleAppSpec{Version: "1.0.0"}, + } + owner.Name = "my-app" + owner.Namespace = "default" + return owner +} + +func testController() *app.Controller { + return &app.Controller{ + NewConfigMapResource: resources.NewConfigMapResource, + NewDeploymentResource: resources.NewDeploymentResource, + } +} + +func testScheme(t *testing.T) *runtime.Scheme { + t.Helper() + scheme := runtime.NewScheme() + require.NoError(t, appsv1.AddToScheme(scheme)) + require.NoError(t, corev1.AddToScheme(scheme)) + return scheme +} + +// TestInfraComponentShape goldens the infra component the controller reconciles. +// It builds through BuildInfraComponent, the same assembly the controller uses, +// so the snapshot reflects the real desired state. +func TestInfraComponentShape(t *testing.T) { + comp, err := testController().BuildInfraComponent(testOwner()) + require.NoError(t, err) + + golden.AssertComponentYAML(t, "testdata/infra-component.yaml", comp, + golden.WithScheme(testScheme(t)), golden.Update(*update)) +} + +// TestAppComponentShape goldens the app component the controller reconciles. +// The DependsOn("InfraReady") prerequisite is the point of this example; the +// component is built through BuildAppComponent so the controller and the +// snapshot stay in lockstep. +func TestAppComponentShape(t *testing.T) { + comp, err := testController().BuildAppComponent(testOwner()) + require.NoError(t, err) + + golden.AssertComponentYAML(t, "testdata/app-component.yaml", comp, + golden.WithScheme(testScheme(t)), golden.Update(*update)) +} diff --git a/examples/extraction-and-guards/app/controller_test.go b/examples/extraction-and-guards/app/controller_test.go new file mode 100644 index 00000000..17e6201f --- /dev/null +++ b/examples/extraction-and-guards/app/controller_test.go @@ -0,0 +1,45 @@ +package app_test + +import ( + "flag" + "testing" + + "github.com/sourcehawk/operator-component-framework/examples/extraction-and-guards/app" + "github.com/sourcehawk/operator-component-framework/examples/extraction-and-guards/resources" + "github.com/sourcehawk/operator-component-framework/pkg/testing/golden" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +var update = flag.Bool("update", false, "update golden files") + +func testOwner() *app.ExampleApp { + owner := &app.ExampleApp{ + Spec: app.ExampleAppSpec{Version: "1.0.0"}, + } + owner.Name = "my-app" + owner.Namespace = "default" + return owner +} + +// TestComponentShape goldens the database component the controller reconciles. +// The ConfigMap (extractor) and Secret (guard) are wired to a shared dbHost +// pointer through BuildComponent, the same assembly the controller uses, so the +// snapshot reflects the real desired state of both resources in registration +// order. +func TestComponentShape(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + + controller := &app.Controller{ + NewConfigMapResource: resources.NewConfigMapResource, + NewSecretResource: resources.NewSecretResource, + } + + comp, err := controller.BuildComponent(testOwner()) + require.NoError(t, err) + + golden.AssertComponentYAML(t, "testdata/component.yaml", comp, + golden.WithScheme(scheme), golden.Update(*update)) +} diff --git a/examples/grace-inconsistency/app/controller_test.go b/examples/grace-inconsistency/app/controller_test.go new file mode 100644 index 00000000..3ae2beb5 --- /dev/null +++ b/examples/grace-inconsistency/app/controller_test.go @@ -0,0 +1,43 @@ +package app_test + +import ( + "flag" + "testing" + + "github.com/sourcehawk/operator-component-framework/examples/grace-inconsistency/app" + "github.com/sourcehawk/operator-component-framework/examples/grace-inconsistency/resources" + "github.com/sourcehawk/operator-component-framework/pkg/testing/golden" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +var update = flag.Bool("update", false, "update golden files") + +func testOwner() *app.ExampleApp { + owner := &app.ExampleApp{ + Spec: app.ExampleAppSpec{Version: "1.0.0"}, + } + owner.Name = "my-app" + owner.Namespace = "default" + return owner +} + +// TestComponentShape goldens the monitoring component the controller reconciles. +// The grace period and SuppressGraceInconsistencyWarning option are the point of +// this example; the component is built through BuildComponent, the same assembly +// the controller uses, so the snapshot reflects the real desired state. +func TestComponentShape(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, appsv1.AddToScheme(scheme)) + + controller := &app.Controller{ + NewDeploymentResource: resources.NewDeploymentResource, + } + + comp, err := controller.BuildComponent(testOwner()) + require.NoError(t, err) + + golden.AssertComponentYAML(t, "testdata/component.yaml", comp, + golden.WithScheme(scheme), golden.Update(*update)) +} From 10422816ecffa85ca6fa5008777fde7e42c966e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=86gir=20M=C3=A1ni=20Hauksson?= <54936225+sourcehawk@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:41:06 +0200 Subject: [PATCH 2/2] fix(examples): drop duplicate controller_test.go; guard Previewer casts Drop the three controller_test.go files added in the previous commit. main now carries component_test.go in each example's app package that already provides the same golden.AssertComponentYAML coverage, so the files were redundant and caused a redeclaration conflict on update and testOwner when CI merged the branch. Replace unchecked res.(golden.Previewer) casts in the resource tests with ok-checked require.True assertions, as flagged in review, so a non-Previewer resource produces a clear test failure instead of a panic. Co-Authored-By: Claude Sonnet 4.6 --- .../app/controller_test.go | 63 ------------------- .../resources/configmap_test.go | 4 +- .../resources/deployment_test.go | 4 +- .../resources/certificate_test.go | 4 +- .../app/controller_test.go | 45 ------------- .../resources/configmap_test.go | 4 +- .../resources/secret_test.go | 4 +- .../app/controller_test.go | 43 ------------- .../resources/deployment_test.go | 4 +- 9 files changed, 18 insertions(+), 157 deletions(-) delete mode 100644 examples/component-prerequisites/app/controller_test.go delete mode 100644 examples/extraction-and-guards/app/controller_test.go delete mode 100644 examples/grace-inconsistency/app/controller_test.go diff --git a/examples/component-prerequisites/app/controller_test.go b/examples/component-prerequisites/app/controller_test.go deleted file mode 100644 index 7c9d73fb..00000000 --- a/examples/component-prerequisites/app/controller_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package app_test - -import ( - "flag" - "testing" - - "github.com/sourcehawk/operator-component-framework/examples/component-prerequisites/app" - "github.com/sourcehawk/operator-component-framework/examples/component-prerequisites/resources" - "github.com/sourcehawk/operator-component-framework/pkg/testing/golden" - "github.com/stretchr/testify/require" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" -) - -var update = flag.Bool("update", false, "update golden files") - -func testOwner() *app.ExampleApp { - owner := &app.ExampleApp{ - Spec: app.ExampleAppSpec{Version: "1.0.0"}, - } - owner.Name = "my-app" - owner.Namespace = "default" - return owner -} - -func testController() *app.Controller { - return &app.Controller{ - NewConfigMapResource: resources.NewConfigMapResource, - NewDeploymentResource: resources.NewDeploymentResource, - } -} - -func testScheme(t *testing.T) *runtime.Scheme { - t.Helper() - scheme := runtime.NewScheme() - require.NoError(t, appsv1.AddToScheme(scheme)) - require.NoError(t, corev1.AddToScheme(scheme)) - return scheme -} - -// TestInfraComponentShape goldens the infra component the controller reconciles. -// It builds through BuildInfraComponent, the same assembly the controller uses, -// so the snapshot reflects the real desired state. -func TestInfraComponentShape(t *testing.T) { - comp, err := testController().BuildInfraComponent(testOwner()) - require.NoError(t, err) - - golden.AssertComponentYAML(t, "testdata/infra-component.yaml", comp, - golden.WithScheme(testScheme(t)), golden.Update(*update)) -} - -// TestAppComponentShape goldens the app component the controller reconciles. -// The DependsOn("InfraReady") prerequisite is the point of this example; the -// component is built through BuildAppComponent so the controller and the -// snapshot stay in lockstep. -func TestAppComponentShape(t *testing.T) { - comp, err := testController().BuildAppComponent(testOwner()) - require.NoError(t, err) - - golden.AssertComponentYAML(t, "testdata/app-component.yaml", comp, - golden.WithScheme(testScheme(t)), golden.Update(*update)) -} diff --git a/examples/component-prerequisites/resources/configmap_test.go b/examples/component-prerequisites/resources/configmap_test.go index 84c99744..755d2c60 100644 --- a/examples/component-prerequisites/resources/configmap_test.go +++ b/examples/component-prerequisites/resources/configmap_test.go @@ -34,6 +34,8 @@ func TestConfigMapShape(t *testing.T) { res, err := resources.NewConfigMapResource(owner) require.NoError(t, err) - golden.AssertYAML(t, "testdata/configmap.yaml", res.(golden.Previewer), + previewer, ok := res.(golden.Previewer) + require.True(t, ok, "resource does not implement golden.Previewer") + golden.AssertYAML(t, "testdata/configmap.yaml", previewer, golden.WithScheme(scheme), golden.Update(*update)) } diff --git a/examples/component-prerequisites/resources/deployment_test.go b/examples/component-prerequisites/resources/deployment_test.go index 396ddea5..09ab1b57 100644 --- a/examples/component-prerequisites/resources/deployment_test.go +++ b/examples/component-prerequisites/resources/deployment_test.go @@ -62,7 +62,9 @@ func TestDeploymentShape(t *testing.T) { res, err := resources.NewDeploymentResource(owner) require.NoError(t, err) - golden.AssertYAML(t, tt.golden, res.(golden.Previewer), + previewer, ok := res.(golden.Previewer) + require.True(t, ok, "resource does not implement golden.Previewer") + golden.AssertYAML(t, tt.golden, previewer, golden.WithScheme(scheme), golden.Update(*update)) }) } diff --git a/examples/custom-resource/resources/certificate_test.go b/examples/custom-resource/resources/certificate_test.go index fed35b59..7bee9272 100644 --- a/examples/custom-resource/resources/certificate_test.go +++ b/examples/custom-resource/resources/certificate_test.go @@ -49,7 +49,9 @@ func TestCertificateShape(t *testing.T) { res, err := resources.NewCertificateResource(testOwner()) require.NoError(t, err) - golden.AssertYAML(t, "testdata/certificate.yaml", res.(golden.Previewer), golden.Update(*update)) + previewer, ok := res.(golden.Previewer) + require.True(t, ok, "resource does not implement golden.Previewer") + golden.AssertYAML(t, "testdata/certificate.yaml", previewer, golden.Update(*update)) } // TestCertificateBaseShape pins the bare base object before any mutations. diff --git a/examples/extraction-and-guards/app/controller_test.go b/examples/extraction-and-guards/app/controller_test.go deleted file mode 100644 index 17e6201f..00000000 --- a/examples/extraction-and-guards/app/controller_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package app_test - -import ( - "flag" - "testing" - - "github.com/sourcehawk/operator-component-framework/examples/extraction-and-guards/app" - "github.com/sourcehawk/operator-component-framework/examples/extraction-and-guards/resources" - "github.com/sourcehawk/operator-component-framework/pkg/testing/golden" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" -) - -var update = flag.Bool("update", false, "update golden files") - -func testOwner() *app.ExampleApp { - owner := &app.ExampleApp{ - Spec: app.ExampleAppSpec{Version: "1.0.0"}, - } - owner.Name = "my-app" - owner.Namespace = "default" - return owner -} - -// TestComponentShape goldens the database component the controller reconciles. -// The ConfigMap (extractor) and Secret (guard) are wired to a shared dbHost -// pointer through BuildComponent, the same assembly the controller uses, so the -// snapshot reflects the real desired state of both resources in registration -// order. -func TestComponentShape(t *testing.T) { - scheme := runtime.NewScheme() - require.NoError(t, corev1.AddToScheme(scheme)) - - controller := &app.Controller{ - NewConfigMapResource: resources.NewConfigMapResource, - NewSecretResource: resources.NewSecretResource, - } - - comp, err := controller.BuildComponent(testOwner()) - require.NoError(t, err) - - golden.AssertComponentYAML(t, "testdata/component.yaml", comp, - golden.WithScheme(scheme), golden.Update(*update)) -} diff --git a/examples/extraction-and-guards/resources/configmap_test.go b/examples/extraction-and-guards/resources/configmap_test.go index fa59f3e4..0f6d1310 100644 --- a/examples/extraction-and-guards/resources/configmap_test.go +++ b/examples/extraction-and-guards/resources/configmap_test.go @@ -36,6 +36,8 @@ func TestConfigMapShape(t *testing.T) { res, err := resources.NewConfigMapResource(owner, &dbHost) require.NoError(t, err) - golden.AssertYAML(t, "testdata/configmap.yaml", res.(golden.Previewer), + previewer, ok := res.(golden.Previewer) + require.True(t, ok, "resource does not implement golden.Previewer") + golden.AssertYAML(t, "testdata/configmap.yaml", previewer, golden.WithScheme(scheme), golden.Update(*update)) } diff --git a/examples/extraction-and-guards/resources/secret_test.go b/examples/extraction-and-guards/resources/secret_test.go index 6fc151a8..0a7fa60a 100644 --- a/examples/extraction-and-guards/resources/secret_test.go +++ b/examples/extraction-and-guards/resources/secret_test.go @@ -23,6 +23,8 @@ func TestSecretShape(t *testing.T) { res, err := resources.NewSecretResource(owner, &dbHost) require.NoError(t, err) - golden.AssertYAML(t, "testdata/secret.yaml", res.(golden.Previewer), + previewer, ok := res.(golden.Previewer) + require.True(t, ok, "resource does not implement golden.Previewer") + golden.AssertYAML(t, "testdata/secret.yaml", previewer, golden.WithScheme(scheme), golden.Update(*update)) } diff --git a/examples/grace-inconsistency/app/controller_test.go b/examples/grace-inconsistency/app/controller_test.go deleted file mode 100644 index 3ae2beb5..00000000 --- a/examples/grace-inconsistency/app/controller_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package app_test - -import ( - "flag" - "testing" - - "github.com/sourcehawk/operator-component-framework/examples/grace-inconsistency/app" - "github.com/sourcehawk/operator-component-framework/examples/grace-inconsistency/resources" - "github.com/sourcehawk/operator-component-framework/pkg/testing/golden" - "github.com/stretchr/testify/require" - appsv1 "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/runtime" -) - -var update = flag.Bool("update", false, "update golden files") - -func testOwner() *app.ExampleApp { - owner := &app.ExampleApp{ - Spec: app.ExampleAppSpec{Version: "1.0.0"}, - } - owner.Name = "my-app" - owner.Namespace = "default" - return owner -} - -// TestComponentShape goldens the monitoring component the controller reconciles. -// The grace period and SuppressGraceInconsistencyWarning option are the point of -// this example; the component is built through BuildComponent, the same assembly -// the controller uses, so the snapshot reflects the real desired state. -func TestComponentShape(t *testing.T) { - scheme := runtime.NewScheme() - require.NoError(t, appsv1.AddToScheme(scheme)) - - controller := &app.Controller{ - NewDeploymentResource: resources.NewDeploymentResource, - } - - comp, err := controller.BuildComponent(testOwner()) - require.NoError(t, err) - - golden.AssertComponentYAML(t, "testdata/component.yaml", comp, - golden.WithScheme(scheme), golden.Update(*update)) -} diff --git a/examples/grace-inconsistency/resources/deployment_test.go b/examples/grace-inconsistency/resources/deployment_test.go index 9e279a08..b7be792d 100644 --- a/examples/grace-inconsistency/resources/deployment_test.go +++ b/examples/grace-inconsistency/resources/deployment_test.go @@ -38,6 +38,8 @@ func TestDeploymentShape(t *testing.T) { res, err := resources.NewDeploymentResource(owner) require.NoError(t, err) - golden.AssertYAML(t, "testdata/deployment.yaml", res.(golden.Previewer), + previewer, ok := res.(golden.Previewer) + require.True(t, ok, "resource does not implement golden.Previewer") + golden.AssertYAML(t, "testdata/deployment.yaml", previewer, golden.WithScheme(testScheme()), golden.Update(*update)) }