diff --git a/internal/terraform/context_plan_actions_test.go b/internal/terraform/context_plan_actions_test.go index ab4888428d..9a87a4b5af 100644 --- a/internal/terraform/context_plan_actions_test.go +++ b/internal/terraform/context_plan_actions_test.go @@ -29,10 +29,17 @@ func TestContextPlan_actions(t *testing.T) { planActionResponse *providers.PlanActionResponse planOpts *PlanOpts - expectPlanActionCalled bool + expectPlanActionCalled bool + + // Some tests can produce race-conditions in the error messages, so we + // have two ways of checking the diagnostics. Use expectValidateDiagnostics + // by default, if there is a race condition and you want to allow multiple + // versions, please use assertValidateDiagnostics. expectValidateDiagnostics func(m *configs.Config) tfdiags.Diagnostics - expectPlanDiagnostics func(m *configs.Config) tfdiags.Diagnostics - assertPlan func(*testing.T, *plans.Plan) + assertValidateDiagnostics func(*testing.T, tfdiags.Diagnostics) + + expectPlanDiagnostics func(m *configs.Config) tfdiags.Diagnostics + assertPlan func(*testing.T, *plans.Plan) }{ "unreferenced": { module: map[string]string{ @@ -1116,6 +1123,70 @@ resource "other_object" "a" { } }, }, + + "action config refers to before triggering resource leads to circular dependency": { + module: map[string]string{ + "main.tf": ` +action "test_unlinked" "hello" { + config { + attr = test_object.a.name + } +} +resource "test_object" "a" { + lifecycle { + action_trigger { + events = [before_create] + actions = [action.test_unlinked.hello] + } + } +} +`, + }, + expectPlanActionCalled: false, + assertValidateDiagnostics: func(t *testing.T, diags tfdiags.Diagnostics) { + if !diags.HasErrors() { + t.Fatalf("expected diagnostics to have errors, but it does not") + } + if len(diags) != 1 { + t.Fatalf("expected diagnostics to have 1 error, but it has %d", len(diags)) + } + if diags[0].Description().Summary != "Cycle: test_object.a, action.test_unlinked.hello (expand)" && diags[0].Description().Summary != "Cycle: action.test_unlinked.hello (expand), test_object.a" { + t.Fatalf("expected diagnostic to have summary 'Cycle: test_object.a, action.test_unlinked.hello (expand)' or 'Cycle: action.test_unlinked.hello (expand), test_object.a', but got '%s'", diags[0].Description().Summary) + } + }, + }, + + "action config refers to after triggering resource leads to circular dependency": { + module: map[string]string{ + "main.tf": ` +action "test_unlinked" "hello" { + config { + attr = test_object.a.name + } +} +resource "test_object" "a" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.test_unlinked.hello] + } + } +} +`, + }, + expectPlanActionCalled: false, + assertValidateDiagnostics: func(t *testing.T, diags tfdiags.Diagnostics) { + if !diags.HasErrors() { + t.Fatalf("expected diagnostics to have errors, but it does not") + } + if len(diags) != 1 { + t.Fatalf("expected diagnostics to have 1 error, but it has %d", len(diags)) + } + if diags[0].Description().Summary != "Cycle: test_object.a, action.test_unlinked.hello (expand)" && diags[0].Description().Summary != "Cycle: action.test_unlinked.hello (expand), test_object.a" { + t.Fatalf("expected diagnostic to have summary 'Cycle: test_object.a, action.test_unlinked.hello (expand)' or 'Cycle: action.test_unlinked.hello (expand), test_object.a', but got '%s'", diags[0].Description().Summary) + } + }, + }, } { t.Run(name, func(t *testing.T) { if tc.toBeImplemented { @@ -1248,6 +1319,8 @@ resource "other_object" "a" { diags := ctx.Validate(m, &ValidateOpts{}) if tc.expectValidateDiagnostics != nil { tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectValidateDiagnostics(m)) + } else if tc.assertValidateDiagnostics != nil { + tc.assertValidateDiagnostics(t, diags) } else { tfdiags.AssertNoDiagnostics(t, diags) }