| // Copyright IBM Corp. 2014, 2026 |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package configs |
| |
| import ( |
| "strings" |
| "testing" |
| |
| version "github.com/hashicorp/go-version" |
| "github.com/hashicorp/hcl/v2" |
| "github.com/hashicorp/hcl/v2/hclsyntax" |
| tfaddr "github.com/hashicorp/terraform-registry-address" |
| "github.com/hashicorp/terraform/internal/configs/configschema" |
| "github.com/hashicorp/terraform/internal/getproviders" |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| // The Hash method assumes that the state_store schema doesn't include a provider block, |
| // and it requires calling code to remove the nested provider block from state_store config data. |
| func TestStateStore_Hash(t *testing.T) { |
| // Normally these schemas would come from a provider's GetProviderSchema data |
| exampleStateStoreSchema := &configschema.Block{ |
| Attributes: map[string]*configschema.Attribute{ |
| "path": { |
| Type: cty.String, |
| Required: true, |
| }, |
| "workspace_dir": { |
| Type: cty.String, |
| Optional: true, |
| }, |
| }, |
| } |
| exampleProviderSchema := &configschema.Block{ |
| Attributes: map[string]*configschema.Attribute{ |
| "foobar": { |
| Type: cty.String, |
| Required: true, |
| }, |
| }, |
| } |
| |
| // These values are all coupled. |
| // The test case below asserts that given these inputs, the expected hash is returned. |
| exampleProviderVersion := version.Must(version.NewSemver("1.2.3")) |
| exampleProviderAddr := tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "foobar") |
| exampleConfig := configBodyForTest(t, `state_store "foobar_fs" { |
| provider "foobar" { |
| foobar = "foobar" |
| } |
| path = "mystate.tfstate" |
| workspace_dir = "foobar" |
| }`) |
| exampleHash := 614398732 |
| t.Run("example happy path with all attrs set in the configuration", func(t *testing.T) { |
| // Construct a configs.StateStore for the test. |
| content, _, cfgDiags := exampleConfig.PartialContent(terraformBlockSchema) |
| if len(cfgDiags) > 0 { |
| t.Fatalf("unexpected diagnostics: %s", cfgDiags) |
| } |
| var ssDiags hcl.Diagnostics |
| s, ssDiags := decodeStateStoreBlock(content.Blocks.OfType("state_store")[0]) |
| if len(ssDiags) > 0 { |
| t.Fatalf("unexpected diagnostics: %s", ssDiags) |
| } |
| s.ProviderAddr = exampleProviderAddr |
| s.ProviderSupplyMode = getproviders.ManagedByTerraform |
| |
| // Test Hash method. |
| gotHash, diags := s.Hash(exampleStateStoreSchema, exampleProviderSchema, exampleProviderVersion) |
| if diags.HasErrors() { |
| t.Fatalf("unexpected error: %s", diags.Err()) |
| } |
| |
| if gotHash != exampleHash { |
| t.Fatalf("expected hash for state_store to be %d, but got %d", exampleHash, gotHash) |
| } |
| }) |
| |
| // Test cases each change a single input that affects the output hash |
| // Assertions check that the output hash doesn't match the hash above, following the changed input. |
| cases := map[string]struct { |
| config hcl.Body |
| stateStoreSchema *configschema.Block |
| providerVersion *version.Version |
| providerAddr tfaddr.Provider |
| }{ |
| "changing the state store type affects the hash value": { |
| config: configBodyForTest(t, `state_store "foobar_CHANGED_VALUE_HERE" { |
| provider "foobar" { |
| foobar = "foobar" |
| } |
| path = "mystate.tfstate" |
| workspace_dir = "foobar" |
| }`), |
| }, |
| "changing the provider affects the hash value": { |
| providerAddr: tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "different-provider"), |
| config: configBodyForTest(t, `state_store "different-provider_fs" { |
| provider "different-provider" { |
| foobar = "foobar" |
| } |
| path = "mystate.tfstate" |
| workspace_dir = "foobar" |
| }`), |
| }, |
| "changing the provider version affects the hash value": { |
| providerVersion: version.Must(version.NewSemver("9.9.9")), |
| }, |
| } |
| |
| for tn, tc := range cases { |
| t.Run(tn, func(t *testing.T) { |
| // If a test case doesn't set an override for these inputs, |
| // instead use a default value from the example above. |
| var config hcl.Body |
| var schema *configschema.Block |
| var providerVersion *version.Version |
| var providerAddr tfaddr.Provider |
| if tc.config == nil { |
| config = exampleConfig |
| } else { |
| config = tc.config |
| } |
| if tc.stateStoreSchema == nil { |
| schema = exampleStateStoreSchema |
| } else { |
| schema = tc.stateStoreSchema |
| } |
| if tc.providerVersion == nil { |
| providerVersion = exampleProviderVersion |
| } else { |
| providerVersion = tc.providerVersion |
| } |
| if tc.providerAddr.IsZero() { |
| providerAddr = exampleProviderAddr |
| } else { |
| providerAddr = tc.providerAddr |
| } |
| |
| // Construct a configs.StateStore for the test. |
| content, _, cfgDiags := config.PartialContent(terraformBlockSchema) |
| if len(cfgDiags) > 0 { |
| t.Fatalf("unexpected diagnostics: %s", cfgDiags) |
| } |
| var ssDiags hcl.Diagnostics |
| s, ssDiags := decodeStateStoreBlock(content.Blocks.OfType("state_store")[0]) |
| if len(ssDiags) > 0 { |
| t.Fatalf("unexpected diagnostics: %s", ssDiags) |
| } |
| s.ProviderAddr = providerAddr |
| s.ProviderSupplyMode = getproviders.ManagedByTerraform |
| |
| // Test Hash method. |
| gotHash, diags := s.Hash(schema, exampleProviderSchema, providerVersion) |
| if diags.HasErrors() { |
| t.Fatalf("unexpected error: %s", diags.Err()) |
| } |
| if gotHash == exampleHash { |
| t.Fatal("expected hash for state_store to be different from the example due to a changed input, but it matched.") |
| } |
| }) |
| } |
| } |
| |
| func TestStateStore_Hash_edgeCases(t *testing.T) { |
| // Normally these schemas would come from a provider's GetProviderSchema data |
| stateStoreSchema := &configschema.Block{ |
| Attributes: map[string]*configschema.Attribute{ |
| "path": { |
| Type: cty.String, |
| Required: true, |
| }, |
| "workspace_dir": { |
| Type: cty.String, |
| Optional: true, |
| }, |
| }, |
| } |
| providerSchema := &configschema.Block{ |
| Attributes: map[string]*configschema.Attribute{ |
| "foobar": { |
| Type: cty.String, |
| Required: true, |
| }, |
| }, |
| } |
| providerAddr := tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "foobar") |
| providerVersion := version.Must(version.NewSemver("1.2.3")) |
| config := configBodyForTest(t, `state_store "foobar_fs" { |
| provider "foobar" { |
| foobar = "foobar" |
| } |
| path = "mystate.tfstate" |
| workspace_dir = "foobar" |
| }`) |
| |
| cases := map[string]struct { |
| config hcl.Body |
| providerAddr tfaddr.Provider |
| providerVersion *version.Version |
| providerSupplyMode getproviders.ProviderSupplyMode |
| }{ |
| "tolerates empty config block for the provider even when schema has Required field(s)": { |
| config: configBodyForTest(t, `state_store "foobar_fs" { |
| provider "foobar" { |
| # required field "foobar" is missing |
| } |
| path = "mystate.tfstate" |
| workspace_dir = "foobar" |
| }`), |
| providerAddr: providerAddr, |
| providerVersion: providerVersion, |
| }, |
| "tolerates missing Required field(s) in state_store config": { |
| config: configBodyForTest(t, `state_store "foobar_fs" { |
| provider "foobar" { |
| foobar = "foobar" |
| } |
| |
| # required field "path" is missing |
| workspace_dir = "foobar" |
| }`), |
| providerAddr: providerAddr, |
| providerVersion: providerVersion, |
| }, |
| "tolerates missing provider version data when using a builtin provider": { |
| config: config, |
| providerAddr: tfaddr.NewProvider(tfaddr.BuiltInProviderHost, "hashicorp", "foobar"), |
| providerVersion: nil, // No version |
| providerSupplyMode: getproviders.BuiltIn, // Builtin |
| }, |
| "tolerates missing provider version data when using a reattached provider": { |
| config: config, |
| providerAddr: providerAddr, |
| providerVersion: nil, // No version |
| providerSupplyMode: getproviders.Reattached, |
| }, |
| "tolerates missing provider version data when using a dev_override provider": { |
| config: config, |
| providerAddr: providerAddr, |
| providerVersion: nil, // No version |
| providerSupplyMode: getproviders.DevOverride, |
| }, |
| } |
| |
| for tn, tc := range cases { |
| t.Run(tn, func(t *testing.T) { |
| // Construct a configs.StateStore for the test. |
| content, _, cfgDiags := config.PartialContent(terraformBlockSchema) |
| if len(cfgDiags) > 0 { |
| t.Fatalf("unexpected diagnostics: %s", cfgDiags) |
| } |
| var ssDiags hcl.Diagnostics |
| s, ssDiags := decodeStateStoreBlock(content.Blocks.OfType("state_store")[0]) |
| if len(ssDiags) > 0 { |
| t.Fatalf("unexpected diagnostics: %s", ssDiags) |
| } |
| s.ProviderAddr = tc.providerAddr |
| |
| if tc.providerSupplyMode != "" { |
| s.ProviderSupplyMode = tc.providerSupplyMode |
| } else { |
| s.ProviderSupplyMode = getproviders.ManagedByTerraform |
| } |
| |
| // Test Hash method. |
| _, diags := s.Hash(stateStoreSchema, providerSchema, tc.providerVersion) |
| if diags.HasErrors() { |
| t.Fatalf("unexpected error: %s", diags.Err()) |
| } |
| }) |
| } |
| } |
| |
| func TestStateStore_Hash_errorConditions(t *testing.T) { |
| // Normally these schemas would come from a provider's GetProviderSchema data |
| exampleStateStoreSchema := &configschema.Block{ |
| Attributes: map[string]*configschema.Attribute{ |
| "path": { |
| Type: cty.String, |
| Required: true, |
| }, |
| "workspace_dir": { |
| Type: cty.String, |
| Optional: true, |
| }, |
| }, |
| } |
| exampleProviderSchema := &configschema.Block{ |
| Attributes: map[string]*configschema.Attribute{ |
| "foobar": { |
| Type: cty.String, |
| Required: true, |
| }, |
| }, |
| } |
| exampleProviderVersion := version.Must(version.NewSemver("1.2.3")) |
| |
| // Cases where an error would occur |
| cases := map[string]struct { |
| config hcl.Body |
| stateStoreSchema *configschema.Block |
| providerVersion *version.Version |
| providerSupplyMode getproviders.ProviderSupplyMode |
| wantErrorString string |
| }{ |
| "returns errors when the state_store config doesn't match the schema": { |
| providerVersion: exampleProviderVersion, |
| stateStoreSchema: exampleStateStoreSchema, |
| config: configBodyForTest(t, `state_store "foobar_fs" { |
| provider "foobar" { |
| foobar = "foobar" |
| } |
| unexpected_block { |
| foobar = "foobar" |
| } |
| unexpected_attr = "foobar" |
| path = "mystate.tfstate" |
| workspace_dir = "foobar" |
| }`), |
| wantErrorString: "Unsupported argument", |
| }, |
| "returns errors when the provider config doesn't match the schema": { |
| providerVersion: exampleProviderVersion, |
| stateStoreSchema: exampleStateStoreSchema, |
| config: configBodyForTest(t, `state_store "foobar_fs" { |
| provider "foobar" { |
| foobar = "foobar" |
| unexpected_attr = "foobar" |
| unexpected_block { |
| foobar = "foobar" |
| } |
| } |
| path = "mystate.tfstate" |
| workspace_dir = "foobar" |
| }`), |
| wantErrorString: "Unsupported argument", |
| }, |
| "returns an error if the state_store schema includes a provider block": { |
| providerVersion: exampleProviderVersion, |
| stateStoreSchema: &configschema.Block{ |
| BlockTypes: map[string]*configschema.NestedBlock{ |
| "provider": { |
| Block: configschema.Block{ |
| Attributes: map[string]*configschema.Attribute{ |
| "foo": { |
| Type: cty.String, |
| Optional: true, |
| }, |
| }, |
| }, |
| Nesting: configschema.NestingSingle, |
| }, |
| }, |
| }, |
| config: configBodyForTest(t, `state_store "foobar_fs" { |
| provider "foobar" { |
| foobar = "foobar" |
| } |
| path = "mystate.tfstate" |
| workspace_dir = "foobar" |
| }`), |
| wantErrorString: `Protected block name "provider" in state store schema`, |
| }, |
| "returns an error if the state_store schema includes a provider attribute": { |
| providerVersion: exampleProviderVersion, |
| stateStoreSchema: &configschema.Block{ |
| Attributes: map[string]*configschema.Attribute{ |
| "provider": { |
| Type: cty.String, |
| Optional: true, |
| }, |
| }, |
| }, |
| config: configBodyForTest(t, `state_store "foobar_fs" { |
| provider "foobar" { |
| foobar = "foobar" |
| } |
| path = "mystate.tfstate" |
| workspace_dir = "foobar" |
| }`), |
| wantErrorString: `Protected argument name "provider" in state store schema`, |
| }, |
| "returns an error if the provider version is missing when using a managed provider": { |
| providerVersion: nil, // No value provided in this test case |
| providerSupplyMode: getproviders.ManagedByTerraform, |
| stateStoreSchema: exampleStateStoreSchema, |
| config: configBodyForTest(t, `state_store "foobar_fs" { |
| provider "foobar" { |
| foobar = "foobar" |
| } |
| path = "mystate.tfstate" |
| workspace_dir = "foobar" |
| }`), |
| wantErrorString: `Provider version data was missing during hash generation`, |
| }, |
| } |
| |
| for tn, tc := range cases { |
| t.Run(tn, func(t *testing.T) { |
| // Construct a configs.StateStore for the test. |
| content, _, cfgDiags := tc.config.PartialContent(terraformBlockSchema) |
| if len(cfgDiags) > 0 { |
| t.Fatalf("unexpected diagnostics: %s", cfgDiags) |
| } |
| var ssDiags hcl.Diagnostics |
| s, ssDiags := decodeStateStoreBlock(content.Blocks.OfType("state_store")[0]) |
| if len(ssDiags) > 0 { |
| t.Fatalf("unexpected diagnostics: %s", ssDiags) |
| } |
| s.ProviderAddr = tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "foobar") |
| |
| if tc.providerSupplyMode != "" { |
| s.ProviderSupplyMode = tc.providerSupplyMode |
| } else { |
| s.ProviderSupplyMode = getproviders.ManagedByTerraform |
| } |
| |
| // Test Hash method. |
| _, diags := s.Hash(tc.stateStoreSchema, exampleProviderSchema, tc.providerVersion) |
| if !diags.HasErrors() { |
| t.Fatal("expected error but got none") |
| } |
| if !strings.Contains(diags.Err().Error(), tc.wantErrorString) { |
| t.Fatalf("expected error to contain %q but got: %s", tc.wantErrorString, diags.Err()) |
| } |
| }) |
| } |
| } |
| |
| func configBodyForTest(t *testing.T, config string) hcl.Body { |
| t.Helper() |
| f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1}) |
| if diags.HasErrors() { |
| t.Fatalf("failure creating hcl.Body during test setup: %s", diags.Error()) |
| } |
| return f.Body |
| } |