diff --git a/internal/planmodifiers/attribute.go b/internal/planmodifiers/attribute.go index baddc592..77a83f4e 100644 --- a/internal/planmodifiers/attribute.go +++ b/internal/planmodifiers/attribute.go @@ -168,46 +168,88 @@ func (r requiresReplaceIfValuesNotNullModifier) Modify(ctx context.Context, req return } - configMap, ok := req.AttributeConfig.(types.Map) - if !ok { + // If there are no differences, do not mark the resource for replacement + // and ensure the plan matches the configuration. + if req.AttributeConfig.Equal(req.AttributeState) { return } - stateMap, ok := req.AttributeState.(types.Map) - if !ok { - return - } + if req.AttributeState.IsNull() { + // terraform-plugin-sdk would store maps as null if all keys had null + // values. To prevent unintentional replacement plans when migrating + // to terraform-plugin-framework, only trigger replacement when the + // prior state (map) is null and when there are not null map values. + allNullValues := true + + configMap, ok := req.AttributeConfig.(types.Map) - replace := false + if !ok { + return + } + + for _, configValue := range configMap.Elems { + if !configValue.IsNull() { + allNullValues = false + } + } + + if allNullValues { + return + } + } else { + // terraform-plugin-sdk would completely omit storing map keys with + // null values, so this also must prevent unintentional replacement + // in that case as well. + allNewNullValues := true + + configMap, ok := req.AttributeConfig.(types.Map) + + if !ok { + return + } - for k, configValue := range configMap.Elems { - stateValue, ok := stateMap.Elems[k] + stateMap, ok := req.AttributeState.(types.Map) - if !ok && configValue.IsNull() { - continue + if !ok { + return } - if !configValue.Equal(stateValue) { - replace = true + for configKey, configValue := range configMap.Elems { + stateValue, ok := stateMap.Elems[configKey] + + // If the key doesn't exist in state and the config value is + // null, do not trigger replacement. + if !ok && configValue.IsNull() { + continue + } + + // If the state value exists and it is equal to the config value, + // do not trigger replacement. + if configValue.Equal(stateValue) { + continue + } + + allNewNullValues = false break } - } - if replace { - resp.RequiresReplace = true - return + for stateKey, _ := range stateMap.Elems { + _, ok := configMap.Elems[stateKey] + + // If the key doesn't exist in the config, but there is a state + // value, trigger replacement. + if !ok { + allNewNullValues = false + break + } + } + + if allNewNullValues { + return + } } - //respPlan := resp.AttributePlan - // - //pm, ok := respPlan.(types.Map) - //if ok { - // for k, v := range additionalElems { - // pm.Elems[k] = v - // } - //} - // - //resp.AttributePlan = pm + resp.RequiresReplace = true } // Description returns a human-readable description of the plan modifier. diff --git a/internal/provider/resource_integer.go b/internal/provider/resource_integer.go index d6e3d2a5..0e71a835 100644 --- a/internal/provider/resource_integer.go +++ b/internal/provider/resource_integer.go @@ -36,7 +36,6 @@ func (r *integerResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Dia ElemType: types.StringType, }, Optional: true, - Computed: true, PlanModifiers: []tfsdk.AttributePlanModifier{ planmodifiers.RequiresReplaceIfValuesNotNull(), }, @@ -142,7 +141,15 @@ func (r *integerResource) Read(ctx context.Context, req resource.ReadRequest, re // Update is intentionally left blank as all required and optional attributes force replacement of the resource // through the RequiresReplace AttributePlanModifier. func (r *integerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - resp.State = tfsdk.State(req.Plan) + var model integerModelV0 + + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) } // Delete does not need to explicitly call resp.State.RemoveResource() as this is automatically handled by the diff --git a/internal/provider/resource_integer_test.go b/internal/provider/resource_integer_test.go index b8a59ddc..4600c91d 100644 --- a/internal/provider/resource_integer_test.go +++ b/internal/provider/resource_integer_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccResourceInteger(t *testing.T) { @@ -177,6 +178,791 @@ func TestAccResourceInteger_UpgradeFromVersion3_3_2(t *testing.T) { }) } +func TestAccResourceInteger_Keepers_Keep_EmptyMap(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = {} + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = {} + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesEqual(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "0"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_Keep_EmptyMapToNullValue(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = {} + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesEqual(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_Keep_NullMap(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesEqual(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "0"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_Keep_NullMapToNullValue(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesEqual(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_Keep_NullValue(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesEqual(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_Keep_NullValues(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key1" = null + "key2" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "2"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key1" = null + "key2" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesEqual(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "2"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_Keep_Value(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesEqual(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_Keep_Values(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key1" = "123" + "key2" = "456" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "2"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key1" = "123" + "key2" = "456" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesEqual(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "2"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_Replace_EmptyMapToValue(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = {} + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesDiffer(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_Replace_NullMapToValue(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesDiffer(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_Replace_NullValueToValue(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesDiffer(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_Replace_ValueToEmptyMap(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = {} + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesDiffer(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "0"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_Replace_ValueToNullMap(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesDiffer(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "0"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_Replace_ValueToNullValue(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesDiffer(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_Replace_ValueToNewValue(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = "456" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesDiffer(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_FrameworkMigration_NullMapToNullValue(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: providerVersion332(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesEqual(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_FrameworkMigration_NullMapToValue(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: providerVersion332(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesDiffer(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_FrameworkMigration_NullMapToMultipleNullValue(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: providerVersion332(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key1" = null + "key2" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key1" = null + "key2" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesEqual(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "2"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_FrameworkMigration_NullMapToMultipleValue(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: providerVersion332(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key1" = null + "key2" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key1" = "123" + "key2" = "456" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesDiffer(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "2"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_FrameworkMigration_NullMapValue(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: providerVersion332(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key1" = "123" + "key2" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key1" = "123" + "key2" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesEqual(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "2"), + ), + }, + }, + }) +} + +func TestAccResourceInteger_Keepers_FrameworkMigration_NullMapValueToValue(t *testing.T) { + var id1, id2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: providerVersion332(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key1" = "123" + "key2" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id1), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_integer" "test" { + min = 1 + max = 100000000 + keepers = { + "key1" = "123" + "key2" = "456" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_integer.test", "id", &id2), + testCheckAttributeValuesDiffer(&id1, &id2), + resource.TestCheckResourceAttr("random_integer.test", "keepers.%", "2"), + ), + }, + }, + }) +} + func testCheckNotEmptyString(field string) func(input string) error { return func(input string) error { if input == "" { @@ -186,3 +972,51 @@ func testCheckNotEmptyString(field string) func(input string) error { return nil } } + +func testExtractResourceAttr(resourceName string, attributeName string, attributeValue *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + + if !ok { + return fmt.Errorf("resource name %s not found in state", resourceName) + } + + attrValue, ok := rs.Primary.Attributes[attributeName] + + if !ok { + return fmt.Errorf("attribute %s not found in resource %s state", attributeName, resourceName) + } + + *attributeValue = attrValue + + return nil + } +} + +func testCheckAttributeValuesDiffer(i *string, j *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if testStringValue(i) == testStringValue(j) { + return fmt.Errorf("attribute values are the same") + } + + return nil + } +} + +func testCheckAttributeValuesEqual(i *string, j *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if testStringValue(i) != testStringValue(j) { + return fmt.Errorf("attribute values are different, got %s and %s", testStringValue(i), testStringValue(j)) + } + + return nil + } +} + +func testStringValue(sPtr *string) string { + if sPtr == nil { + return "" + } + + return *sPtr +}