diff --git a/internal/acceptance/helpers/is_not_resource_action.go b/internal/acceptance/helpers/is_not_resource_action.go new file mode 100644 index 000000000000..2d245691c636 --- /dev/null +++ b/internal/acceptance/helpers/is_not_resource_action.go @@ -0,0 +1,89 @@ +package helpers + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-testing/plancheck" +) + +var _ plancheck.PlanCheck = isNotResourceAction{} + +type isNotResourceAction struct { + resourceAddress string + actionType plancheck.ResourceActionType +} + +// CheckPlan implements the plan check logic. +func (e isNotResourceAction) CheckPlan(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) { + foundResource := false + + for _, rc := range req.Plan.ResourceChanges { + if e.resourceAddress != rc.Address { + continue + } + + switch e.actionType { + case plancheck.ResourceActionNoop: + if rc.Change.Actions.NoOp() { + resp.Error = fmt.Errorf("'%s' - expected action to not be %s", rc.Address, e.actionType) + return + } + case plancheck.ResourceActionCreate: + if rc.Change.Actions.Create() { + resp.Error = fmt.Errorf("'%s' - expected action to not be %s", rc.Address, e.actionType) + return + } + case plancheck.ResourceActionRead: + if rc.Change.Actions.Read() { + resp.Error = fmt.Errorf("'%s' - expected action to not be %s", rc.Address, e.actionType) + return + } + case plancheck.ResourceActionUpdate: + if rc.Change.Actions.Update() { + resp.Error = fmt.Errorf("'%s' - expected action to not be %s", rc.Address, e.actionType) + return + } + case plancheck.ResourceActionDestroy: + if rc.Change.Actions.Delete() { + resp.Error = fmt.Errorf("'%s' - expected action to not be %s", rc.Address, e.actionType) + return + } + case plancheck.ResourceActionDestroyBeforeCreate: + if rc.Change.Actions.DestroyBeforeCreate() { + resp.Error = fmt.Errorf("'%s' - expected action to not be %s", rc.Address, e.actionType) + return + } + case plancheck.ResourceActionCreateBeforeDestroy: + if rc.Change.Actions.CreateBeforeDestroy() { + resp.Error = fmt.Errorf("'%s' - expected action to not be %s", rc.Address, e.actionType) + return + } + case plancheck.ResourceActionReplace: + if rc.Change.Actions.Replace() { + resp.Error = fmt.Errorf("'%s' - expected action to not be %s", rc.Address, e.actionType) + return + } + default: + resp.Error = fmt.Errorf("%s - unexpected ResourceActionType: %s", rc.Address, e.actionType) + return + } + + foundResource = true + break + } + + if !foundResource { + resp.Error = fmt.Errorf("%s - Resource not found in plan ResourceChanges", e.resourceAddress) + return + } +} + +// IsNotResourceAction returns a plan check that asserts that a given resource will not have a specific resource change type in the plan. +// Valid actionType are an enum of type plancheck.ResourceActionType, examples: NoOp, DestroyBeforeCreate, Update (in-place), etc. +func IsNotResourceAction(resourceAddress string, actionType plancheck.ResourceActionType) plancheck.PlanCheck { + return isNotResourceAction{ + resourceAddress: resourceAddress, + actionType: actionType, + } +} diff --git a/internal/acceptance/testcase.go b/internal/acceptance/testcase.go index 539ecc66c1b3..b6e8ef66f974 100644 --- a/internal/acceptance/testcase.go +++ b/internal/acceptance/testcase.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/helpers" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/testclient" @@ -48,6 +49,48 @@ func (td TestData) ResourceTest(t *testing.T, testResource types.TestResource, s RefreshState: true, } + newSteps := make([]TestStep, 0) + for _, step := range steps { + // This block adds a check to make sure tests aren't recreating a resource + if (step.Config != "" || step.ConfigDirectory != nil || step.ConfigFile != nil) && !step.PlanOnly { + step.ConfigPlanChecks = resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + helpers.IsNotResourceAction(td.ResourceName, plancheck.ResourceActionReplace), + }, + } + } + + if !step.ImportState { + newSteps = append(newSteps, step) + } else { + newSteps = append(newSteps, refreshStep) + newSteps = append(newSteps, step) + } + } + steps = newSteps + + testCase := resource.TestCase{ + PreCheck: func() { PreCheck(t) }, + CheckDestroy: func(s *terraform.State) error { + client, err := testclient.Build() + if err != nil { + return fmt.Errorf("building client: %+v", err) + } + return helpers.CheckDestroyedFunc(client, testResource, td.ResourceType, td.ResourceName)(s) + }, + Steps: steps, + } + td.runAcceptanceTest(t, testCase) +} + +// ResourceTestIgnoreRecreate should be used when checking that a resource should be recreated during a test. +func (td TestData) ResourceTestIgnoreRecreate(t *testing.T, testResource types.TestResource, steps []TestStep) { + // Testing framework as of 1.6.0 no longer auto-refreshes state, so adding it back in here for all steps that update + // the config rather than having to modify 1000's of tests individually to add a refresh-only step + var refreshStep = TestStep{ + RefreshState: true, + } + newSteps := make([]TestStep, 0) for _, step := range steps { if !step.ImportState {