diff --git a/stackit/internal/services/postgresflexalpha/utils/planModifiers.go b/stackit/internal/utils/planModifiers.go similarity index 100% rename from stackit/internal/services/postgresflexalpha/utils/planModifiers.go rename to stackit/internal/utils/planModifiers.go diff --git a/stackit/internal/utils/planModifiers_test.go b/stackit/internal/utils/planModifiers_test.go new file mode 100644 index 00000000..337ea36f --- /dev/null +++ b/stackit/internal/utils/planModifiers_test.go @@ -0,0 +1,224 @@ +package utils + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" +) + +func TestReadModifiersConfig(t *testing.T) { + testcases := []struct { + name string + content []byte + wantErr bool + }{ + { + name: "valid yaml", + content: []byte(` +fields: + - name: 'id' + modifiers: + - 'UseStateForUnknown' +`), + wantErr: false, + }, + { + name: "invalid yaml", + content: []byte(`invalid: yaml: :`), + wantErr: true, + }, + } + + for _, tc := range testcases { + t.Run( + tc.name, func(t *testing.T) { + _, err := ReadModifiersConfig(tc.content) + if (err != nil) != tc.wantErr { + t.Errorf("ReadModifiersConfig() error = %v, wantErr %v", err, tc.wantErr) + } + }, + ) + } +} + +func TestAddPlanModifiersToResourceSchema(t *testing.T) { + testcases := []struct { + name string + fields *Fields + sch *schema.Schema + wantErr bool + }{ + { + name: "full coverage - all types and nested structures", + fields: &Fields{ + Fields: []*Field{ + { + Name: "string_attr", + Modifiers: []*string{utils.Ptr("RequiresReplace"), utils.Ptr("UseStateForUnknown")}, + }, + {Name: "bool_attr", Modifiers: []*string{utils.Ptr("RequiresReplace")}}, + {Name: "int_attr", Modifiers: []*string{utils.Ptr("UseStateForUnknown")}}, + {Name: "list_attr", Modifiers: []*string{utils.Ptr("RequiresReplace")}}, + {Name: "Nested.sub_string", Modifiers: []*string{utils.Ptr("RequiresReplace")}}, + }, + }, + sch: &schema.Schema{ + Attributes: map[string]schema.Attribute{ + "StringAttr": schema.StringAttribute{}, + "BoolAttr": schema.BoolAttribute{}, + "IntAttr": schema.Int64Attribute{}, + "ListAttr": schema.ListAttribute{}, + "Nested": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "SubString": schema.StringAttribute{}, + }, + }, + "Unsupported": schema.MapAttribute{ElementType: types.StringType}, // Triggers default/warn case + }, + }, + wantErr: false, + }, + { + name: "validation error - invalid modifier", + fields: &Fields{ + Fields: []*Field{ + {Name: "id", Modifiers: []*string{utils.Ptr("InvalidModifier")}}, + }, + }, + sch: &schema.Schema{ + Attributes: map[string]schema.Attribute{"id": schema.StringAttribute{}}, + }, + wantErr: true, + }, + { + name: "validation error - empty modifier", + fields: &Fields{ + Fields: []*Field{ + {Name: "id", Modifiers: []*string{utils.Ptr("")}}, + }, + }, + sch: &schema.Schema{}, + wantErr: true, + }, + { + name: "nil fields - should return nil", + fields: nil, + sch: &schema.Schema{}, + wantErr: false, + }, + } + + for _, tc := range testcases { + t.Run( + tc.name, func(t *testing.T) { + err := AddPlanModifiersToResourceSchema(tc.fields, tc.sch) + + if (err != nil) != tc.wantErr { + t.Fatalf("AddPlanModifiersToResourceSchema() error = %v, wantErr %v", err, tc.wantErr) + } + + if !tc.wantErr && tc.name == "full coverage - all types and nested structures" { + // Check StringAttr + if sAttr, ok := tc.sch.Attributes["StringAttr"].(schema.StringAttribute); ok { + if len(sAttr.PlanModifiers) != 2 { + t.Errorf("StringAttr: expected 2 modifiers, got %d", len(sAttr.PlanModifiers)) + } + } + + // Check Nested Sub-Attribute + if nested, ok := tc.sch.Attributes["Nested"].(schema.SingleNestedAttribute); ok { + if subAttr, ok := nested.Attributes["SubString"].(schema.StringAttribute); ok { + if len(subAttr.PlanModifiers) != 1 { + // Dies schlug vorher fehl, weil der Prefix "Nested" statt "nested" war + t.Errorf("Nested SubString: expected 1 modifier, got %d", len(subAttr.PlanModifiers)) + } + } else { + t.Error("SubString attribute not found in Nested") + } + } else { + t.Error("Nested attribute not found") + } + } + }, + ) + } +} + +func TestFieldListToMap(t *testing.T) { + testcases := []struct { + name string + fields *Fields + want map[string][]*string + }{ + { + name: "convert list to map", + fields: &Fields{ + Fields: []*Field{ + {Name: "test", Modifiers: []*string{utils.Ptr("mod")}}, + }, + }, + want: map[string][]*string{ + "test": {utils.Ptr("mod")}, + }, + }, + { + name: "nil fields", + fields: nil, + want: map[string][]*string{}, + }, + } + + for _, tc := range testcases { + t.Run( + tc.name, func(t *testing.T) { + got := fieldListToMap(tc.fields) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("fieldListToMap() mismatch (-want +got):\n%s", diff) + } + }, + ) + } +} + +func TestHandleTypeMismatches(t *testing.T) { + modifiers := []*string{utils.Ptr("RequiresReplace")} + + t.Run( + "bool type mismatch", func(t *testing.T) { + _, err := handleBoolPlanModifiers(schema.StringAttribute{}, modifiers) + if err == nil { + t.Error("expected error for type mismatch in handleBoolPlanModifiers") + } + }, + ) + + t.Run( + "string type mismatch", func(t *testing.T) { + _, err := handleStringPlanModifiers(schema.BoolAttribute{}, modifiers) + if err == nil { + t.Error("expected error for type mismatch in handleStringPlanModifiers") + } + }, + ) + + t.Run( + "int64 type mismatch", func(t *testing.T) { + _, err := handleInt64PlanModifiers(schema.StringAttribute{}, modifiers) + if err == nil { + t.Error("expected error for type mismatch in handleInt64PlanModifiers") + } + }, + ) + + t.Run( + "list type mismatch", func(t *testing.T) { + _, err := handleListPlanModifiers(schema.StringAttribute{}, modifiers) + if err == nil { + t.Error("expected error for type mismatch in handleListPlanModifiers") + } + }, + ) +}