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
This commit is contained in:
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user