From c354876178bf43d0b1d9b8429579287260255b63 Mon Sep 17 00:00:00 2001 From: Story Crater Bot <19826264+Riotpiaole@users.noreply.github.com> Date: Sat, 11 Jul 2026 07:30:33 -0700 Subject: [PATCH] test: add comprehensive TemporalWorker tests Queue reconciliation: - TemporalWorker creation when label present - Namespace label validation - Kubernetes name validation - Cleanup on Queue deletion Validation helpers: - isValidTemporalNamespace (8 cases) - validateKubernetesName (9 cases) TemporalWorker controller: - Deployment creation and updates - Env var injection including TEMPORAL_TASK_QUEUE - Status tracking - Delete handling --- internal/operator/queue_controller_test.go | 137 +++++++++++++ .../temporal_worker_controller_test.go | 193 ++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 internal/operator/temporal_worker_controller_test.go diff --git a/internal/operator/queue_controller_test.go b/internal/operator/queue_controller_test.go index b0f7ee5..23cb30d 100644 --- a/internal/operator/queue_controller_test.go +++ b/internal/operator/queue_controller_test.go @@ -2,6 +2,7 @@ package operator import ( "context" + "strings" "testing" "time" @@ -40,6 +41,16 @@ func newTestRedis(t *testing.T) *goredis.Client { return goredis.NewClient(&goredis.Options{Addr: mr.Addr()}) } +func newTestSchemeWithAppsV1(t *testing.T) *runtime.Scheme { + t.Helper() + scheme := newTestScheme(t) + appsv1 := runtime.NewScheme() + if err := clientgoscheme.AddToScheme(appsv1); err != nil { + t.Fatalf("add client-go scheme: %v", err) + } + return scheme +} + func newTestReconciler(t *testing.T, objs ...client.Object) (*QueueReconciler, *fakeAdmin) { t.Helper() scheme := newTestScheme(t) @@ -273,3 +284,129 @@ func TestReconcileDrainsClosingShardWhenLagZero(t *testing.T) { t.Errorf("expected drained topic %q to be deleted", topic) } } + +func TestReconcileTemporalWorkerCreatesWhenLabelPresent(t *testing.T) { + queue := baseQueue("orders") + queue.Labels = map[string]string{"temporal.io/namespace": "default"} + r, _ := newTestReconciler(t, queue) + ctx := context.Background() + + if err := r.Reconcile(ctx, "", "orders"); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + var worker kmsvcv1.TemporalWorker + if err := r.Client.Get(ctx, client.ObjectKey{Name: "worker-default", Namespace: "temporal"}, &worker); err != nil { + t.Fatalf("expected TemporalWorker to be created: %v", err) + } + if worker.Spec.Namespace != "default" { + t.Errorf("worker namespace = %q, want default", worker.Spec.Namespace) + } +} + +func TestReconcileTemporalWorkerValidatesNamespaceLabel(t *testing.T) { + queue := baseQueue("orders") + queue.Labels = map[string]string{"temporal.io/namespace": "Foo@Bar"} + r, _ := newTestReconciler(t, queue) + ctx := context.Background() + + err := r.Reconcile(ctx, "", "orders") + if err == nil { + t.Fatal("expected Reconcile to fail with invalid namespace label") + } + if err.Error() == "" || err.Error() == "invalid temporal namespace" { + t.Errorf("error message not descriptive: %v", err) + } +} + +func TestReconcileTemporalWorkerValidatesKubernetesName(t *testing.T) { + queue := baseQueue("orders") + queue.Labels = map[string]string{"temporal.io/namespace": strings.Repeat("a", 250)} + r, _ := newTestReconciler(t, queue) + ctx := context.Background() + + err := r.Reconcile(ctx, "", "orders") + if err == nil { + t.Fatal("expected Reconcile to fail with too-long kubernetes name") + } +} + +func TestReconcileDeleteRemovesTemporalWorker(t *testing.T) { + queue := baseQueue("orders") + queue.Labels = map[string]string{"temporal.io/namespace": "default"} + r, _ := newTestReconciler(t, queue) + ctx := context.Background() + + if err := r.Reconcile(ctx, "", "orders"); err != nil { + t.Fatalf("initial reconcile: %v", err) + } + + var got kmsvcv1.Queue + if err := r.Client.Get(ctx, client.ObjectKey{Name: "orders"}, &got); err != nil { + t.Fatalf("get queue: %v", err) + } + + if err := r.Client.Delete(ctx, &got); err != nil { + t.Fatalf("delete queue: %v", err) + } + if err := r.Reconcile(ctx, "", "orders"); err != nil { + t.Fatalf("delete reconcile: %v", err) + } + + var worker kmsvcv1.TemporalWorker + err := r.Client.Get(ctx, client.ObjectKey{Name: "worker-default", Namespace: "temporal"}, &worker) + if err == nil { + t.Error("expected TemporalWorker to be deleted") + } +} + +func TestIsValidTemporalNamespace(t *testing.T) { + tests := []struct { + name string + ns string + want bool + }{ + {"valid lowercase", "default", true}, + {"valid with underscore", "my_namespace", true}, + {"valid with hyphen", "my-namespace", true}, + {"valid with digits", "ns123", true}, + {"invalid uppercase", "MyNamespace", false}, + {"invalid special chars", "my@namespace", false}, + {"empty", "", false}, + {"too long", strings.Repeat("a", 256), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isValidTemporalNamespace(tt.ns); got != tt.want { + t.Errorf("isValidTemporalNamespace(%q) = %v, want %v", tt.ns, got, tt.want) + } + }) + } +} + +func TestValidateKubernetesName(t *testing.T) { + tests := []struct { + name string + input string + wantOk bool + }{ + {"valid", "worker-default", true}, + {"valid lowercase digits", "worker-123", true}, + {"invalid uppercase", "Worker-default", false}, + {"invalid starts with hyphen", "-worker-default", false}, + {"invalid ends with hyphen", "worker-default-", false}, + {"invalid special char", "worker@default", false}, + {"too long", "worker-" + strings.Repeat("a", 250), false}, + {"starts with digit", "1worker-default", false}, + {"empty", "", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateKubernetesName(tt.input) + got := err == nil + if got != tt.wantOk { + t.Errorf("validateKubernetesName(%q) error = %v, want ok=%v", tt.input, err, tt.wantOk) + } + }) + } +} diff --git a/internal/operator/temporal_worker_controller_test.go b/internal/operator/temporal_worker_controller_test.go new file mode 100644 index 0000000..1b8bc10 --- /dev/null +++ b/internal/operator/temporal_worker_controller_test.go @@ -0,0 +1,193 @@ +package operator + +import ( + "context" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + kmsvcv1 "github.com/rockliang/kafka-management-service/apis/kmsvc/v1" +) + +func newTemporalWorkerTestScheme(t *testing.T) *runtime.Scheme { + t.Helper() + scheme := runtime.NewScheme() + if err := clientgoscheme.AddToScheme(scheme); err != nil { + t.Fatalf("add client-go scheme: %v", err) + } + if err := kmsvcv1.AddToScheme(scheme); err != nil { + t.Fatalf("add kmsvc scheme: %v", err) + } + if err := appsv1.AddToScheme(scheme); err != nil { + t.Fatalf("add apps scheme: %v", err) + } + return scheme +} + +func newTemporalWorkerTestReconciler(t *testing.T, objs ...client.Object) (*TemporalWorkerReconciler, client.Client) { + t.Helper() + scheme := newTemporalWorkerTestScheme(t) + cl := fake.NewClientBuilder(). + WithScheme(scheme). + WithStatusSubresource(&kmsvcv1.TemporalWorker{}). + WithObjects(objs...). + Build() + return &TemporalWorkerReconciler{Client: cl}, cl +} + +func baseTemporalWorker(name, namespace string) *kmsvcv1.TemporalWorker { + replicas := int32(2) + return &kmsvcv1.TemporalWorker{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, + Spec: kmsvcv1.TemporalWorkerSpec{ + Namespace: "default", + Image: "story-crater-backend:v1.0.0", + Replicas: &replicas, + }, + } +} + +func TestTemporalWorkerReconcileCreatesDeployment(t *testing.T) { + worker := baseTemporalWorker("worker-default", "temporal") + r, cl := newTemporalWorkerTestReconciler(t, worker) + ctx := context.Background() + + if err := r.Reconcile(ctx, "temporal", "worker-default"); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + var deploy appsv1.Deployment + if err := cl.Get(ctx, client.ObjectKey{Name: "worker-default", Namespace: "temporal"}, &deploy); err != nil { + t.Fatalf("expected Deployment to be created: %v", err) + } + if *deploy.Spec.Replicas != 2 { + t.Errorf("replicas = %d, want 2", *deploy.Spec.Replicas) + } + if deploy.Spec.Template.Spec.Containers[0].Image != "story-crater-backend:v1.0.0" { + t.Errorf("image = %q, want story-crater-backend:v1.0.0", deploy.Spec.Template.Spec.Containers[0].Image) + } +} + +func TestTemporalWorkerReconcileInjectsEnvVars(t *testing.T) { + worker := baseTemporalWorker("worker-default", "temporal") + r, cl := newTemporalWorkerTestReconciler(t, worker) + ctx := context.Background() + + if err := r.Reconcile(ctx, "temporal", "worker-default"); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + var deploy appsv1.Deployment + if err := cl.Get(ctx, client.ObjectKey{Name: "worker-default", Namespace: "temporal"}, &deploy); err != nil { + t.Fatalf("get Deployment: %v", err) + } + + envVars := deploy.Spec.Template.Spec.Containers[0].Env + envMap := make(map[string]string) + for _, ev := range envVars { + envMap[ev.Name] = ev.Value + } + + if envMap["TEMPORAL_FRONTEND_ADDRESS"] != "temporal-frontend.temporal.svc.cluster.local:7233" { + t.Errorf("TEMPORAL_FRONTEND_ADDRESS = %q, want temporal-frontend.temporal.svc.cluster.local:7233", envMap["TEMPORAL_FRONTEND_ADDRESS"]) + } + if envMap["TEMPORAL_NAMESPACE"] != "default" { + t.Errorf("TEMPORAL_NAMESPACE = %q, want default", envMap["TEMPORAL_NAMESPACE"]) + } + if envMap["TEMPORAL_TASK_QUEUE"] != "worker-default" { + t.Errorf("TEMPORAL_TASK_QUEUE = %q, want worker-default", envMap["TEMPORAL_TASK_QUEUE"]) + } +} + +func TestTemporalWorkerReconcileUpdateDeployment(t *testing.T) { + replicas := int32(2) + worker := baseTemporalWorker("worker-default", "temporal") + deploy := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "worker-default", Namespace: "temporal"}, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app.kubernetes.io/name": "temporal-worker", + "app.kubernetes.io/instance": "worker-default", + "app.kubernetes.io/managed-by": "kmsvc-temporal-operator", + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app.kubernetes.io/name": "temporal-worker", + "app.kubernetes.io/instance": "worker-default", + "app.kubernetes.io/managed-by": "kmsvc-temporal-operator", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "worker", + Image: "story-crater-backend:v0.9.0", + }, + }, + }, + }, + }, + } + + r, cl := newTemporalWorkerTestReconciler(t, worker, deploy) + ctx := context.Background() + + if err := r.Reconcile(ctx, "temporal", "worker-default"); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + var updated appsv1.Deployment + if err := cl.Get(ctx, client.ObjectKey{Name: "worker-default", Namespace: "temporal"}, &updated); err != nil { + t.Fatalf("get Deployment: %v", err) + } + + if updated.Spec.Template.Spec.Containers[0].Image != "story-crater-backend:v1.0.0" { + t.Errorf("image updated to %q, want story-crater-backend:v1.0.0", updated.Spec.Template.Spec.Containers[0].Image) + } +} + +func TestTemporalWorkerReconcileUpdatesStatus(t *testing.T) { + worker := baseTemporalWorker("worker-default", "temporal") + r, cl := newTemporalWorkerTestReconciler(t, worker) + ctx := context.Background() + + if err := r.Reconcile(ctx, "temporal", "worker-default"); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + var updated kmsvcv1.TemporalWorker + if err := cl.Get(ctx, client.ObjectKey{Name: "worker-default", Namespace: "temporal"}, &updated); err != nil { + t.Fatalf("get TemporalWorker: %v", err) + } + + if updated.Status.Phase != kmsvcv1.TemporalWorkerPhasePending { + t.Errorf("phase = %v, want Pending", updated.Status.Phase) + } + if updated.Status.Replicas != 2 { + t.Errorf("status.replicas = %d, want 2", updated.Status.Replicas) + } +} + +func TestTemporalWorkerReconcileDeleteHandlesMarkedForDeletion(t *testing.T) { + worker := baseTemporalWorker("worker-default", "temporal") + now := metav1.Now() + worker.ObjectMeta.DeletionTimestamp = &now + worker.ObjectMeta.Finalizers = []string{temporalWorkerFinalizerName} + + r, _ := newTemporalWorkerTestReconciler(t, worker) + ctx := context.Background() + + if err := r.Reconcile(ctx, "temporal", "worker-default"); err != nil { + t.Fatalf("Reconcile delete should not error: %v", err) + } +}