// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package backendbase import ( "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcltest" "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/tfdiags" ) func TestBase_coerceError(t *testing.T) { // This tests that we return errors if type coersion fails. // This doesn't thoroughly test all cases because we're just delegating // to the configschema package's coersion function, which is already // tested in its own package. b := Base{ Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": { Type: cty.String, Optional: true, }, }, }, } // This is a fake body just to give us something to correlate the // diagnostic attribute paths against so we can test that the // errors are properly annotated. In the real implementation // the command package logic would evaluate the diagnostics against // the real HCL body written by the end-user. // // Because we're using MockExprLiteral for the expressions here, // the source range for each expression is just the fake filename // "MockExprLiteral". If the PrepareConfig function fails to properly // annotate its diagnostics then the source range won't be populated // at all. body := hcltest.MockBody(&hcl.BodyContent{ Attributes: hcl.Attributes{ "foo": { Expr: hcltest.MockExprLiteral(cty.StringVal("")), }, }, }) t.Run("error", func(t *testing.T) { _, diags := b.PrepareConfig(cty.ObjectVal(map[string]cty.Value{ // This is incorrect because the schema wants a string "foo": cty.MapValEmpty(cty.String), })) gotDiags := diags.InConfigBody(body, "") var wantDiags tfdiags.Diagnostics wantDiags = wantDiags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid backend configuration", Detail: "The backend configuration is incorrect: .foo: string required, but have map of string.", Subject: &hcl.Range{Filename: "MockExprLiteral"}, }) tfdiags.AssertDiagnosticsMatch(t, gotDiags, wantDiags) }) } func TestBase_deprecatedArg(t *testing.T) { b := Base{ Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "not_deprecated": { Type: cty.String, Optional: true, }, "deprecated": { Type: cty.String, Optional: true, Deprecated: true, }, }, BlockTypes: map[string]*configschema.NestedBlock{ "nested": { Nesting: configschema.NestingList, Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "deprecated": { Type: cty.String, Optional: true, Deprecated: true, }, }, }, }, }, }, } // This is a fake body just to give us something to correlate the // diagnostic attribute paths against so we can test that the // warnings are properly annotated. In the real implementation // the command package logic would evaluate the diagnostics against // the real HCL body written by the end-user. // // Because we're using MockExprLiteral for the expressions here, // the source range for each expression is just the fake filename // "MockExprLiteral". If the PrepareConfig function fails to properly // annotate its diagnostics then the source range won't be populated // at all. body := hcltest.MockBody(&hcl.BodyContent{ Attributes: hcl.Attributes{ "deprecated": { Expr: hcltest.MockExprLiteral(cty.StringVal("")), }, }, Blocks: hcl.Blocks{ { Type: "nested", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcl.Attributes{ "deprecated": { Expr: hcltest.MockExprLiteral(cty.StringVal("")), }, }, }), }, { Type: "nested", Body: hcltest.MockBody(&hcl.BodyContent{ Attributes: hcl.Attributes{ "deprecated": { Expr: hcltest.MockExprLiteral(cty.StringVal("")), }, }, }), }, }, }) t.Run("nothing deprecated", func(t *testing.T) { got, diags := b.PrepareConfig(cty.ObjectVal(map[string]cty.Value{ "not_deprecated": cty.StringVal("hello"), })) if len(diags) != 0 { t.Errorf("unexpected diagnostics: %s", diags.ErrWithWarnings().Error()) } want := cty.ObjectVal(map[string]cty.Value{ "deprecated": cty.NullVal(cty.String), "not_deprecated": cty.StringVal("hello"), "nested": cty.ListValEmpty(cty.Object(map[string]cty.Type{ "deprecated": cty.String, })), }) if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { t.Errorf("wrong result\n%s", diff) } }) t.Run("toplevel deprecated", func(t *testing.T) { _, diags := b.PrepareConfig(cty.ObjectVal(map[string]cty.Value{ "deprecated": cty.StringVal("hello"), })) gotDiags := diags.InConfigBody(body, "") var wantDiags tfdiags.Diagnostics wantDiags = wantDiags.Append(&hcl.Diagnostic{ Severity: hcl.DiagWarning, Summary: "Deprecated provider argument", Detail: "The argument .deprecated is deprecated. Refer to the backend documentation for more information.", Subject: &hcl.Range{Filename: "MockExprLiteral"}, }) tfdiags.AssertDiagnosticsMatch(t, wantDiags, gotDiags) }) t.Run("nested deprecated", func(t *testing.T) { _, diags := b.PrepareConfig(cty.ObjectVal(map[string]cty.Value{ "nested": cty.TupleVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "deprecated": cty.StringVal("hello"), }), cty.ObjectVal(map[string]cty.Value{ "deprecated": cty.StringVal("hello"), }), }), })) gotDiags := diags.InConfigBody(body, "") var wantDiags tfdiags.Diagnostics wantDiags = wantDiags.Append(&hcl.Diagnostic{ Severity: hcl.DiagWarning, Summary: "Deprecated provider argument", Detail: "The argument .nested[0].deprecated is deprecated. Refer to the backend documentation for more information.", Subject: &hcl.Range{Filename: "MockExprLiteral"}, }) wantDiags = wantDiags.Append(&hcl.Diagnostic{ Severity: hcl.DiagWarning, Summary: "Deprecated provider argument", Detail: "The argument .nested[1].deprecated is deprecated. Refer to the backend documentation for more information.", Subject: &hcl.Range{Filename: "MockExprLiteral"}, }) tfdiags.AssertDiagnosticsMatch(t, wantDiags, gotDiags) }) } func TestBase_nullCrash(t *testing.T) { // This test ensures that we don't crash while applying defaults to // a null value b := Base{ Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": { Type: cty.String, Required: true, }, }, }, SDKLikeDefaults: SDKLikeDefaults{ "foo": { Fallback: "fallback", }, }, } t.Run("error", func(t *testing.T) { // We pass an explicit null value here to simulate an interrupt _, gotDiags := b.PrepareConfig(cty.NullVal(cty.Object(map[string]cty.Type{ "foo": cty.String, }))) var wantDiags tfdiags.Diagnostics wantDiags = wantDiags.Append( &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid backend configuration", Detail: "The backend configuration is incorrect: attribute \"foo\" is required.", }) if diff := cmp.Diff(wantDiags.ForRPC(), gotDiags.ForRPC()); diff != "" { t.Errorf("wrong diagnostics\n%s", diff) } }) }