Files
Reloader/internal/pkg/handler/update_test.go
2026-01-10 13:42:10 +01:00

532 lines
15 KiB
Go

package handler
import (
"testing"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/metrics"
)
func TestResourceUpdatedHandler_GetConfig(t *testing.T) {
tests := []struct {
name string
oldResource any
newResource any
expectedName string
expectedNS string
expectedType string
expectSHANotEmpty bool
expectSHAChanged bool
}{
{
name: "ConfigMap data changed",
oldResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"},
Data: map[string]string{"key": "old-value"},
},
newResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"},
Data: map[string]string{"key": "new-value"},
},
expectedName: "my-cm",
expectedNS: "default",
expectedType: constants.ConfigmapEnvVarPostfix,
expectSHANotEmpty: true,
expectSHAChanged: true,
},
{
name: "ConfigMap data unchanged",
oldResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"},
Data: map[string]string{"key": "same-value"},
},
newResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"},
Data: map[string]string{"key": "same-value"},
},
expectedName: "my-cm",
expectedNS: "default",
expectedType: constants.ConfigmapEnvVarPostfix,
expectSHANotEmpty: true,
expectSHAChanged: false,
},
{
name: "ConfigMap key added",
oldResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"},
Data: map[string]string{"key1": "value1"},
},
newResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"},
Data: map[string]string{"key1": "value1", "key2": "value2"},
},
expectedName: "my-cm",
expectedNS: "default",
expectedType: constants.ConfigmapEnvVarPostfix,
expectSHANotEmpty: true,
expectSHAChanged: true,
},
{
name: "ConfigMap key removed",
oldResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"},
Data: map[string]string{"key1": "value1", "key2": "value2"},
},
newResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "my-cm", Namespace: "default"},
Data: map[string]string{"key1": "value1"},
},
expectedName: "my-cm",
expectedNS: "default",
expectedType: constants.ConfigmapEnvVarPostfix,
expectSHANotEmpty: true,
expectSHAChanged: true,
},
{
name: "ConfigMap only labels changed - SHA unchanged",
oldResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "my-cm",
Namespace: "default",
Labels: map[string]string{"version": "v1"},
},
Data: map[string]string{"key": "value"},
},
newResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "my-cm",
Namespace: "default",
Labels: map[string]string{"version": "v2"},
},
Data: map[string]string{"key": "value"},
},
expectedName: "my-cm",
expectedNS: "default",
expectedType: constants.ConfigmapEnvVarPostfix,
expectSHANotEmpty: true,
expectSHAChanged: false, // Only data affects SHA, not labels
},
{
name: "ConfigMap only annotations changed - SHA unchanged",
oldResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "my-cm",
Namespace: "default",
Annotations: map[string]string{"note": "old"},
},
Data: map[string]string{"key": "value"},
},
newResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "my-cm",
Namespace: "default",
Annotations: map[string]string{"note": "new"},
},
Data: map[string]string{"key": "value"},
},
expectedName: "my-cm",
expectedNS: "default",
expectedType: constants.ConfigmapEnvVarPostfix,
expectSHANotEmpty: true,
expectSHAChanged: false, // Only data affects SHA, not annotations
},
{
name: "Secret data changed",
oldResource: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "my-secret", Namespace: "default"},
Data: map[string][]byte{"password": []byte("old-pass")},
},
newResource: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "my-secret", Namespace: "default"},
Data: map[string][]byte{"password": []byte("new-pass")},
},
expectedName: "my-secret",
expectedNS: "default",
expectedType: constants.SecretEnvVarPostfix,
expectSHANotEmpty: true,
expectSHAChanged: true,
},
{
name: "Secret data unchanged",
oldResource: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "my-secret", Namespace: "default"},
Data: map[string][]byte{"password": []byte("same-pass")},
},
newResource: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "my-secret", Namespace: "default"},
Data: map[string][]byte{"password": []byte("same-pass")},
},
expectedName: "my-secret",
expectedNS: "default",
expectedType: constants.SecretEnvVarPostfix,
expectSHANotEmpty: true,
expectSHAChanged: false,
},
{
name: "Secret key added",
oldResource: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "my-secret", Namespace: "default"},
Data: map[string][]byte{"key1": []byte("value1")},
},
newResource: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "my-secret", Namespace: "default"},
Data: map[string][]byte{"key1": []byte("value1"), "key2": []byte("value2")},
},
expectedName: "my-secret",
expectedNS: "default",
expectedType: constants.SecretEnvVarPostfix,
expectSHANotEmpty: true,
expectSHAChanged: true,
},
{
name: "Secret only labels changed - SHA unchanged",
oldResource: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret",
Namespace: "default",
Labels: map[string]string{"env": "dev"},
},
Data: map[string][]byte{"key": []byte("value")},
},
newResource: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret",
Namespace: "default",
Labels: map[string]string{"env": "prod"},
},
Data: map[string][]byte{"key": []byte("value")},
},
expectedName: "my-secret",
expectedNS: "default",
expectedType: constants.SecretEnvVarPostfix,
expectSHANotEmpty: true,
expectSHAChanged: false,
},
{
name: "Invalid resource type",
oldResource: "invalid",
newResource: "invalid",
expectedName: "",
expectedNS: "",
expectedType: "",
expectSHANotEmpty: false,
expectSHAChanged: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handler := ResourceUpdatedHandler{
Resource: tt.newResource,
OldResource: tt.oldResource,
Collectors: metrics.NewCollectors(),
}
config, oldSHA := handler.GetConfig()
assert.Equal(t, tt.expectedName, config.ResourceName)
assert.Equal(t, tt.expectedNS, config.Namespace)
assert.Equal(t, tt.expectedType, config.Type)
if tt.expectSHANotEmpty {
assert.NotEmpty(t, config.SHAValue, "new SHA should not be empty")
assert.NotEmpty(t, oldSHA, "old SHA should not be empty")
}
if tt.expectSHAChanged {
assert.NotEqual(t, config.SHAValue, oldSHA, "SHA should have changed")
} else if tt.expectSHANotEmpty {
assert.Equal(t, config.SHAValue, oldSHA, "SHA should not have changed")
}
})
}
}
func TestResourceUpdatedHandler_Handle(t *testing.T) {
tests := []struct {
name string
oldResource any
newResource any
expectError bool
}{
{
name: "Both resources nil",
oldResource: nil,
newResource: nil,
expectError: false, // logs error but returns nil
},
{
name: "Old resource nil",
oldResource: nil,
newResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"},
Data: map[string]string{"key": "value"},
},
expectError: false,
},
{
name: "New resource nil",
oldResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"},
Data: map[string]string{"key": "value"},
},
newResource: nil,
expectError: false,
},
{
name: "ConfigMap unchanged - no action",
oldResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"},
Data: map[string]string{"key": "same"},
},
newResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"},
Data: map[string]string{"key": "same"},
},
expectError: false,
},
{
name: "ConfigMap changed - triggers update",
oldResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"},
Data: map[string]string{"key": "old"},
},
newResource: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"},
Data: map[string]string{"key": "new"},
},
expectError: false, // No error, but no workloads to update in test
},
{
name: "Secret unchanged - no action",
oldResource: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "secret", Namespace: "default"},
Data: map[string][]byte{"key": []byte("same")},
},
newResource: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "secret", Namespace: "default"},
Data: map[string][]byte{"key": []byte("same")},
},
expectError: false,
},
{
name: "Secret changed - triggers update",
oldResource: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "secret", Namespace: "default"},
Data: map[string][]byte{"key": []byte("old")},
},
newResource: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "secret", Namespace: "default"},
Data: map[string][]byte{"key": []byte("new")},
},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handler := ResourceUpdatedHandler{
Resource: tt.newResource,
OldResource: tt.oldResource,
Collectors: metrics.NewCollectors(),
}
err := handler.Handle()
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestResourceUpdatedHandler_GetConfig_Annotations(t *testing.T) {
// Test that annotations from the new resource are captured
oldCM := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "cm",
Namespace: "default",
Annotations: map[string]string{
"old-annotation": "old-value",
},
},
Data: map[string]string{"key": "value"},
}
newCM := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "cm",
Namespace: "default",
Annotations: map[string]string{
"new-annotation": "new-value",
},
},
Data: map[string]string{"key": "value"},
}
handler := ResourceUpdatedHandler{
Resource: newCM,
OldResource: oldCM,
Collectors: metrics.NewCollectors(),
}
config, _ := handler.GetConfig()
// Should have new annotations
assert.Equal(t, "new-value", config.ResourceAnnotations["new-annotation"])
// Should not have old annotations
_, hasOld := config.ResourceAnnotations["old-annotation"]
assert.False(t, hasOld)
}
func TestResourceUpdatedHandler_GetConfig_Labels(t *testing.T) {
// Test that labels from the new resource are captured
oldSecret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "default",
Labels: map[string]string{"version": "v1"},
},
Data: map[string][]byte{"key": []byte("value")},
}
newSecret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "default",
Labels: map[string]string{"version": "v2"},
},
Data: map[string][]byte{"key": []byte("value")},
}
handler := ResourceUpdatedHandler{
Resource: newSecret,
OldResource: oldSecret,
Collectors: metrics.NewCollectors(),
}
config, _ := handler.GetConfig()
// Should have new labels
assert.Equal(t, "v2", config.Labels["version"])
}
func TestResourceUpdatedHandler_EmptyToNonEmpty(t *testing.T) {
// Test transition from empty data to non-empty data
oldCM := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"},
Data: map[string]string{},
}
newCM := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"},
Data: map[string]string{"key": "value"},
}
handler := ResourceUpdatedHandler{
Resource: newCM,
OldResource: oldCM,
Collectors: metrics.NewCollectors(),
}
config, oldSHA := handler.GetConfig()
assert.NotEqual(t, config.SHAValue, oldSHA, "SHA should change when data is added")
}
func TestResourceUpdatedHandler_NonEmptyToEmpty(t *testing.T) {
// Test transition from non-empty data to empty data
oldCM := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"},
Data: map[string]string{"key": "value"},
}
newCM := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"},
Data: map[string]string{},
}
handler := ResourceUpdatedHandler{
Resource: newCM,
OldResource: oldCM,
Collectors: metrics.NewCollectors(),
}
config, oldSHA := handler.GetConfig()
assert.NotEqual(t, config.SHAValue, oldSHA, "SHA should change when data is removed")
}
func TestResourceUpdatedHandler_BinaryDataChange(t *testing.T) {
// Test ConfigMap binary data change
oldCM := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"},
BinaryData: map[string][]byte{"binary": []byte("old-binary")},
}
newCM := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"},
BinaryData: map[string][]byte{"binary": []byte("new-binary")},
}
handler := ResourceUpdatedHandler{
Resource: newCM,
OldResource: oldCM,
Collectors: metrics.NewCollectors(),
}
config, oldSHA := handler.GetConfig()
assert.NotEqual(t, config.SHAValue, oldSHA, "SHA should change when binary data changes")
}
func TestResourceUpdatedHandler_MixedDataAndBinaryData(t *testing.T) {
// Test ConfigMap with both Data and BinaryData
oldCM := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"},
Data: map[string]string{"text": "value"},
BinaryData: map[string][]byte{"binary": []byte("binary-value")},
}
newCM := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "default"},
Data: map[string]string{"text": "value"},
BinaryData: map[string][]byte{"binary": []byte("new-binary-value")},
}
handler := ResourceUpdatedHandler{
Resource: newCM,
OldResource: oldCM,
Collectors: metrics.NewCollectors(),
}
config, oldSHA := handler.GetConfig()
assert.NotEqual(t, config.SHAValue, oldSHA, "SHA should change when binary data changes")
}
func TestResourceUpdatedHandler_DifferentNamespaces(t *testing.T) {
// Edge case: what if namespaces are different (shouldn't happen in practice)
oldCM := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns1"},
Data: map[string]string{"key": "value"},
}
newCM := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns2"},
Data: map[string]string{"key": "value"},
}
handler := ResourceUpdatedHandler{
Resource: newCM,
OldResource: oldCM,
Collectors: metrics.NewCollectors(),
}
config, _ := handler.GetConfig()
// Should use new resource's namespace
assert.Equal(t, "ns2", config.Namespace)
}