/* Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resourcelock import ( "context" "encoding/json" "fmt" "testing" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" ) // mockRecorder implements record.EventRecorder for testing type mockRecorder struct { events []string } func (r *mockRecorder) Event(object runtime.Object, eventtype, reason, message string) { r.events = append(r.events, message) } func (r *mockRecorder) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) { r.events = append(r.events, fmt.Sprintf(messageFmt, args...)) } func (r *mockRecorder) AnnotatedEventf(object runtime.Object, annotations map[string]string, eventtype, reason, messageFmt string, args ...interface{}) { r.events = append(r.events, fmt.Sprintf(messageFmt, args...)) } const ( testNamespace = "test-namespace" testName = "test-name" testIdentity = "test-identity" ) var ( testTime = time.Now() client *fake.Clientset leaseLock *LeaseLock recorder *mockRecorder testRecord LeaderElectionRecord ) // setup initializes the test variables before each test func setup() { client = fake.NewClientset() recorder = &mockRecorder{} leaseLock = &LeaseLock{ LeaseMeta: metav1.ObjectMeta{ Namespace: testNamespace, Name: testName, }, Client: client.CoordinationV1(), LockConfig: ResourceLockConfig{ Identity: testIdentity, }, } testRecord = LeaderElectionRecord{ HolderIdentity: testIdentity, LeaseDurationSeconds: 15, AcquireTime: metav1.Time{Time: testTime}, RenewTime: metav1.Time{Time: testTime}, LeaderTransitions: 1, PreferredHolder: "preferred-holder", Strategy: "strategy", } } func TestGetError(t *testing.T) { setup() // Test Get on non-existent lease _, _, err := leaseLock.Get(context.Background()) if err == nil { t.Error("Expected error getting non-existent lease, got nil") } if !apierrors.IsNotFound(err) { t.Errorf("Expected NotFound error, got %v", err) } } func TestGet(t *testing.T) { setup() // Create a lease first if err := leaseLock.Create(context.Background(), testRecord); err != nil { t.Fatalf("Failed to create lease: %v", err) } // Test Get record, recordBytes, err := leaseLock.Get(context.Background()) if err != nil { t.Fatalf("Failed to get lease: %v", err) } // Verify record matches what we created if record.HolderIdentity != testRecord.HolderIdentity { t.Errorf("HolderIdentity mismatch, got %q want %q", record.HolderIdentity, testRecord.HolderIdentity) } if record.LeaseDurationSeconds != testRecord.LeaseDurationSeconds { t.Errorf("LeaseDurationSeconds mismatch, got %d want %d", record.LeaseDurationSeconds, testRecord.LeaseDurationSeconds) } if record.LeaderTransitions != testRecord.LeaderTransitions { t.Errorf("LeaderTransitions mismatch, got %d want %d", record.LeaderTransitions, testRecord.LeaderTransitions) } if record.PreferredHolder != testRecord.PreferredHolder { t.Errorf("PreferredHolder mismatch, got %q want %q", record.PreferredHolder, testRecord.PreferredHolder) } if record.Strategy != testRecord.Strategy { t.Errorf("Strategy mismatch, got %q want %q", record.Strategy, testRecord.Strategy) } // Verify we got valid JSON bytes var unmarshalled LeaderElectionRecord if err := json.Unmarshal(recordBytes, &unmarshalled); err != nil { t.Errorf("Failed to unmarshal record bytes: %v", err) } } func TestRecordEventSuccess(t *testing.T) { setup() leaseLock.LockConfig.EventRecorder = recorder // Create a lease first if err := leaseLock.Create(context.Background(), testRecord); err != nil { t.Fatalf("Failed to create lease: %v", err) } // Test recording an event leaseLock.RecordEvent("test event") // Verify event was recorded if len(recorder.events) != 1 { t.Errorf("Expected 1 event, got %d", len(recorder.events)) } expectedEvent := fmt.Sprintf("%v %v", testIdentity, "test event") if recorder.events[0] != expectedEvent { t.Errorf("Event mismatch, got %q want %q", recorder.events[0], expectedEvent) } } func TestRecordEventNilRecorder(t *testing.T) { setup() // Create a lease first if err := leaseLock.Create(context.Background(), testRecord); err != nil { t.Fatalf("Failed to create lease: %v", err) } // Verify nil recorder doesn't panic leaseLock.LockConfig.EventRecorder = nil leaseLock.RecordEvent("should not panic") } func TestCreateError(t *testing.T) { setup() // Create initial lease if err := leaseLock.Create(context.Background(), testRecord); err != nil { t.Fatalf("Failed to create lease: %v", err) } // Attempt to create same lease again err := leaseLock.Create(context.Background(), testRecord) if err == nil { t.Error("Expected error creating duplicate lease, got nil") } if !apierrors.IsAlreadyExists(err) { t.Errorf("Expected AlreadyExists error, got %v", err) } } func TestUpdateError(t *testing.T) { setup() // Attempt to update without initializing lease err := leaseLock.Update(context.Background(), testRecord) if err == nil { t.Error("Expected error updating uninitialized lease, got nil") } } func TestDescribe(t *testing.T) { setup() expected := fmt.Sprintf("%v/%v", testNamespace, testName) if got := leaseLock.Describe(); got != expected { t.Errorf("Describe() = %q, want %q", got, expected) } } func TestLeaseConversion(t *testing.T) { setup() // Convert LeaderElectionRecord to LeaseSpec leaseSpec := LeaderElectionRecordToLeaseSpec(&testRecord) // Verify all fields were converted correctly if *leaseSpec.HolderIdentity != testRecord.HolderIdentity { t.Errorf("HolderIdentity mismatch, got %q want %q", *leaseSpec.HolderIdentity, testRecord.HolderIdentity) } if *leaseSpec.LeaseDurationSeconds != int32(testRecord.LeaseDurationSeconds) { t.Errorf("LeaseDurationSeconds mismatch, got %d want %d", *leaseSpec.LeaseDurationSeconds, testRecord.LeaseDurationSeconds) } if leaseSpec.AcquireTime.Time != testRecord.AcquireTime.Time { t.Errorf("AcquireTime mismatch, got %v want %v", leaseSpec.AcquireTime.Time, testRecord.AcquireTime.Time) } if leaseSpec.RenewTime.Time != testRecord.RenewTime.Time { t.Errorf("RenewTime mismatch, got %v want %v", leaseSpec.RenewTime.Time, testRecord.RenewTime.Time) } if *leaseSpec.LeaseTransitions != int32(testRecord.LeaderTransitions) { t.Errorf("LeaseTransitions mismatch, got %d want %d", *leaseSpec.LeaseTransitions, testRecord.LeaderTransitions) } if *leaseSpec.PreferredHolder != testRecord.PreferredHolder { t.Errorf("PreferredHolder mismatch, got %q want %q", *leaseSpec.PreferredHolder, testRecord.PreferredHolder) } if *leaseSpec.Strategy != testRecord.Strategy { t.Errorf("Strategy mismatch, got %q want %q", *leaseSpec.Strategy, testRecord.Strategy) } // Convert back to LeaderElectionRecord convertedRecord := LeaseSpecToLeaderElectionRecord(&leaseSpec) // Verify round-trip conversion preserved all fields if convertedRecord.HolderIdentity != testRecord.HolderIdentity { t.Errorf("HolderIdentity lost in conversion, got %q want %q", convertedRecord.HolderIdentity, testRecord.HolderIdentity) } if convertedRecord.LeaseDurationSeconds != testRecord.LeaseDurationSeconds { t.Errorf("LeaseDurationSeconds lost in conversion, got %d want %d", convertedRecord.LeaseDurationSeconds, testRecord.LeaseDurationSeconds) } if !convertedRecord.AcquireTime.Equal(&testRecord.AcquireTime) { t.Errorf("AcquireTime lost in conversion, got %v want %v", convertedRecord.AcquireTime, testRecord.AcquireTime) } if !convertedRecord.RenewTime.Equal(&testRecord.RenewTime) { t.Errorf("RenewTime lost in conversion, got %v want %v", convertedRecord.RenewTime, testRecord.RenewTime) } if convertedRecord.LeaderTransitions != testRecord.LeaderTransitions { t.Errorf("LeaderTransitions lost in conversion, got %d want %d", convertedRecord.LeaderTransitions, testRecord.LeaderTransitions) } if convertedRecord.PreferredHolder != testRecord.PreferredHolder { t.Errorf("PreferredHolder lost in conversion, got %q want %q", convertedRecord.PreferredHolder, testRecord.PreferredHolder) } if convertedRecord.Strategy != testRecord.Strategy { t.Errorf("Strategy lost in conversion, got %q want %q", convertedRecord.Strategy, testRecord.Strategy) } } func TestUpdateWithNilLabelsOnLease(t *testing.T) { setup() // Create initial lease if err := leaseLock.Create(context.Background(), testRecord); err != nil { t.Fatalf("Failed to create lease: %v", err) } // Get the lease to initialize leaseLock.lease if _, _, err := leaseLock.Get(context.Background()); err != nil { t.Fatalf("Failed to get lease: %v", err) } leaseLock.lease.Labels = nil leaseLock.Labels = map[string]string{"custom-key": "custom-val"} // Update should succeed even with nil Labels on the lease itself if err := leaseLock.Update(context.Background(), testRecord); err != nil { t.Errorf("Update failed with nil Labels: %v", err) } } func TestUpdateWithNilLabelsOnLeaseLock(t *testing.T) { setup() // Create initial lease if err := leaseLock.Create(context.Background(), testRecord); err != nil { t.Fatalf("Failed to create lease: %v", err) } // Get the lease to initialize leaseLock.lease if _, _, err := leaseLock.Get(context.Background()); err != nil { t.Fatalf("Failed to get lease: %v", err) } leaseLock.Labels = nil leaseLock.lease.Labels = map[string]string{"custom-key": "custom-val"} // Update should succeed even with nil Labels on the leaselock if err := leaseLock.Update(context.Background(), testRecord); err != nil { t.Errorf("Update failed with nil Labels: %v", err) } } func TestLabelUpdate(t *testing.T) { setup() labels := map[string]string{"custom-key": "custom-val"} leaseLock.Labels = labels // Create initial lease if err := leaseLock.Create(context.Background(), testRecord); err != nil { t.Fatalf("Failed to create lease: %v", err) } // Update label on actual lease with new value leaseLock.lease.Labels["custom-key"] = "new-custom-val" // Update extra label on lease leaseLock.lease.Labels["custom-key-2"] = "custom-val-2" // Update labels lease, err := leaseLock.Client.Leases(testNamespace).Update(context.Background(), leaseLock.lease, metav1.UpdateOptions{}) if err != nil { t.Fatalf("Failed to update lease labels: %v", err) } // verify the labels were updated val, exists := lease.Labels["custom-key"] if !exists { t.Error("Label was not set on the lease") } if val != "new-custom-val" { t.Errorf("Label value mismatch, got %q want %q", val, "custom-val") } val, exists = lease.Labels["custom-key-2"] if !exists { t.Error("Label was not set on the lease") } if val != "custom-val-2" { t.Errorf("Label value mismatch, got %q want %q", val, "custom-val") } // Update the lease if err := leaseLock.Update(context.Background(), testRecord); err != nil { t.Fatalf("Failed to update lease: %v", err) } // Verify labels are preserved lease, err = leaseLock.Client.Leases(testNamespace).Get(context.Background(), testName, metav1.GetOptions{}) if err != nil { t.Fatalf("Failed to update lease labels: %v", err) } val, exists = lease.Labels["custom-key"] if !exists { t.Error("Label was not set on the lease") } if val != "custom-val" { t.Errorf("Label value mismatch, got %q want %q", val, "custom-val") } val, exists = lease.Labels["custom-key-2"] if !exists { t.Error("Label was not set on the lease") } if val != "custom-val-2" { t.Errorf("Label value mismatch, got %q want %q", val, "custom-val-2") } }