| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package command | |
| import ( | |
| "encoding/json" | |
| "io" | |
| "os" | |
| "path" | |
| "strings" | |
| "testing" | |
| "github.com/google/go-cmp/cmp" | |
| "github.com/zclconf/go-cty/cty" | |
| "github.com/hashicorp/terraform/internal/backend" | |
| backendInit "github.com/hashicorp/terraform/internal/backend/init" | |
| testing_command "github.com/hashicorp/terraform/internal/command/testing" | |
| "github.com/hashicorp/terraform/internal/command/views" | |
| "github.com/hashicorp/terraform/internal/configs/configschema" | |
| "github.com/hashicorp/terraform/internal/providers" | |
| testing_provider "github.com/hashicorp/terraform/internal/providers/testing" | |
| "github.com/hashicorp/terraform/internal/terminal" | |
| ) | |
| func setupTest(t *testing.T, fixturepath string, args ...string) (*terminal.TestOutput, int) { | |
| view, done := testView(t) | |
| c := &ValidateCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(validateTestProvider()), | |
| View: view, | |
| }, | |
| } | |
| args = append(args, "-no-color") | |
| args = append(args, testFixturePath(fixturepath)) | |
| code := c.Run(args) | |
| return done(t), code | |
| } | |
| func validateTestProvider() *testing_provider.MockProvider { | |
| p := testProvider() | |
| p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ | |
| ResourceTypes: map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "ami": {Type: cty.String, Optional: true}, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "network_interface": { | |
| Nesting: configschema.NestingList, | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "device_index": {Type: cty.String, Optional: true}, | |
| "description": {Type: cty.String, Optional: true}, | |
| "name": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| return p | |
| } | |
| func TestValidateCommand(t *testing.T) { | |
| if output, code := setupTest(t, "validate-valid"); code != 0 { | |
| t.Fatalf("unexpected non-successful exit code %d\n\n%s", code, output.Stderr()) | |
| } | |
| } | |
| func TestValidateCommandWithTfvarsFile(t *testing.T) { | |
| // Create a temporary working directory that is empty because this test | |
| // requires scanning the current working directory by validate command. | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("validate-valid/with-tfvars-file"), td) | |
| t.Chdir(td) | |
| view, done := testView(t) | |
| c := &ValidateCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad %d\n\n%s", code, output.Stderr()) | |
| } | |
| } | |
| func TestValidateFailingCommand(t *testing.T) { | |
| if output, code := setupTest(t, "validate-invalid"); code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| } | |
| func TestValidateFailingCommandMissingQuote(t *testing.T) { | |
| output, code := setupTest(t, "validate-invalid/missing_quote") | |
| if code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| wantError := "Error: Invalid reference" | |
| if !strings.Contains(output.Stderr(), wantError) { | |
| t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) | |
| } | |
| } | |
| func TestValidateFailingCommandMissingVariable(t *testing.T) { | |
| output, code := setupTest(t, "validate-invalid/missing_var") | |
| if code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| wantError := "Error: Reference to undeclared input variable" | |
| if !strings.Contains(output.Stderr(), wantError) { | |
| t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) | |
| } | |
| } | |
| func TestSameProviderMultipleTimesShouldFail(t *testing.T) { | |
| output, code := setupTest(t, "validate-invalid/multiple_providers") | |
| if code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| wantError := "Error: Duplicate provider configuration" | |
| if !strings.Contains(output.Stderr(), wantError) { | |
| t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) | |
| } | |
| } | |
| func TestSameModuleMultipleTimesShouldFail(t *testing.T) { | |
| output, code := setupTest(t, "validate-invalid/multiple_modules") | |
| if code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| wantError := "Error: Duplicate module call" | |
| if !strings.Contains(output.Stderr(), wantError) { | |
| t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) | |
| } | |
| } | |
| func TestSameResourceMultipleTimesShouldFail(t *testing.T) { | |
| output, code := setupTest(t, "validate-invalid/multiple_resources") | |
| if code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| wantError := `Error: Duplicate resource "aws_instance" configuration` | |
| if !strings.Contains(output.Stderr(), wantError) { | |
| t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) | |
| } | |
| } | |
| func TestSameImportTargetMultipleTimesShouldFail(t *testing.T) { | |
| output, code := setupTest(t, "validate-invalid/duplicate_import_targets") | |
| if code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| wantError := `Error: Duplicate import configuration for "aws_instance.web"` | |
| if !strings.Contains(output.Stderr(), wantError) { | |
| t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) | |
| } | |
| } | |
| func TestOutputWithoutValueShouldFail(t *testing.T) { | |
| output, code := setupTest(t, "validate-invalid/outputs") | |
| if code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| wantError := `The argument "value" is required, but no definition was found.` | |
| if !strings.Contains(output.Stderr(), wantError) { | |
| t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) | |
| } | |
| wantError = `An argument named "values" is not expected here. Did you mean "value"?` | |
| if !strings.Contains(output.Stderr(), wantError) { | |
| t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) | |
| } | |
| } | |
| func TestModuleWithIncorrectNameShouldFail(t *testing.T) { | |
| output, code := setupTest(t, "validate-invalid/incorrectmodulename") | |
| if code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| wantError := `Error: Invalid module instance name` | |
| if !strings.Contains(output.Stderr(), wantError) { | |
| t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) | |
| } | |
| } | |
| func TestWronglyUsedInterpolationShouldFail(t *testing.T) { | |
| output, code := setupTest(t, "validate-invalid/interpolation") | |
| if code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| wantError := `Error: Variables not allowed` | |
| if !strings.Contains(output.Stderr(), wantError) { | |
| t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) | |
| } | |
| wantError = `A single static variable reference is required` | |
| if !strings.Contains(output.Stderr(), wantError) { | |
| t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) | |
| } | |
| } | |
| func TestMissingDefinedVar(t *testing.T) { | |
| output, code := setupTest(t, "validate-invalid/missing_defined_var") | |
| // This is allowed because validate tests only that variables are referenced | |
| // correctly, not that they all have defined values. | |
| if code != 0 { | |
| t.Fatalf("Should have passed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| } | |
| func TestValidateWithInvalidTestFile(t *testing.T) { | |
| // We're reusing some testing configs that were written for testing the | |
| // test command here, so we have to initalise things slightly differently | |
| // to the other tests. | |
| view, done := testView(t) | |
| provider := testing_command.NewProvider(nil) | |
| c := &ValidateCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(provider.Provider), | |
| View: view, | |
| }, | |
| } | |
| var args []string | |
| args = append(args, "-no-color") | |
| args = append(args, testFixturePath("test/invalid")) | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| wantError := "Error: Invalid `expect_failures` reference" | |
| if !strings.Contains(output.Stderr(), wantError) { | |
| t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) | |
| } | |
| } | |
| func TestValidateWithInvalidTestModule(t *testing.T) { | |
| // We're reusing some testing configs that were written for testing the | |
| // test command here, so we have to initalise things slightly differently | |
| // to the other tests. | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath(path.Join("test", "invalid-module")), td) | |
| t.Chdir(td) | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := views.NewView(streams) | |
| ui := testUiWrapped(t) | |
| provider := testing_command.NewProvider(nil) | |
| providerSource := newMockProviderSource(t, map[string][]string{ | |
| "test": {"1.0.0"}, | |
| }) | |
| meta := Meta{ | |
| testingOverrides: metaOverridesForProvider(provider.Provider), | |
| Ui: ui, | |
| View: view, | |
| Streams: streams, | |
| ProviderSource: providerSource, | |
| } | |
| init := &InitCommand{ | |
| Meta: meta, | |
| } | |
| if code := init.Run(nil); code != 0 { | |
| t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter) | |
| } | |
| c := &ValidateCommand{ | |
| Meta: meta, | |
| } | |
| var args []string | |
| args = append(args, "-no-color") | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| wantError := "Error: Reference to undeclared input variable" | |
| if !strings.Contains(output.Stderr(), wantError) { | |
| t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) | |
| } | |
| } | |
| func TestValidate_constVariable(t *testing.T) { | |
| t.Run("missing value", func(t *testing.T) { | |
| wd := tempWorkingDirFixture(t, "dynamic-module-sources/command-with-const-var") | |
| t.Chdir(wd.RootModuleDir()) | |
| view, done := testView(t) | |
| c := &ValidateCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| View: view, | |
| WorkingDir: wd, | |
| }, | |
| } | |
| args := []string{"-no-color"} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| wantError := "Error: No value for required variable" | |
| if !strings.Contains(output.Stderr(), wantError) { | |
| t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) | |
| } | |
| }) | |
| t.Run("value via cli", func(t *testing.T) { | |
| wd := tempWorkingDirFixture(t, "dynamic-module-sources/command-with-const-var") | |
| t.Chdir(wd.RootModuleDir()) | |
| view, done := testView(t) | |
| c := &ValidateCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(validateTestProvider()), | |
| View: view, | |
| WorkingDir: wd, | |
| }, | |
| } | |
| args := []string{ | |
| "-no-color", | |
| "-var", "module_name=child", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("unexpected non-successful exit code %d\n\n%s", code, output.Stderr()) | |
| } | |
| }) | |
| t.Run("value via backend", func(t *testing.T) { | |
| mockBackend := TestNewVariableBackend(map[string]string{ | |
| "module_name": "child", | |
| }) | |
| backendInit.Set("local-vars", func() backend.Backend { return mockBackend }) | |
| defer backendInit.Set("local-vars", nil) | |
| wd := tempWorkingDirFixture(t, "dynamic-module-sources/command-with-const-var-backend") | |
| t.Chdir(wd.RootModuleDir()) | |
| view, done := testView(t) | |
| c := &ValidateCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(validateTestProvider()), | |
| View: view, | |
| WorkingDir: wd, | |
| }, | |
| } | |
| args := []string{"-no-color"} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("unexpected non-successful exit code %d\n\n%s", code, output.Stderr()) | |
| } | |
| }) | |
| } | |
| func TestValidateWithInvalidOverrides(t *testing.T) { | |
| // We're reusing some testing configs that were written for testing the | |
| // test command here, so we have to initalise things slightly differently | |
| // to the other tests. | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath(path.Join("test", "invalid-overrides")), td) | |
| t.Chdir(td) | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := views.NewView(streams) | |
| ui := testUiWrapped(t) | |
| provider := testing_command.NewProvider(nil) | |
| providerSource := newMockProviderSource(t, map[string][]string{ | |
| "test": {"1.0.0"}, | |
| }) | |
| meta := Meta{ | |
| testingOverrides: metaOverridesForProvider(provider.Provider), | |
| Ui: ui, | |
| View: view, | |
| Streams: streams, | |
| ProviderSource: providerSource, | |
| } | |
| init := &InitCommand{ | |
| Meta: meta, | |
| } | |
| output := done(t) | |
| if code := init.Run(nil); code != 0 { | |
| t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) | |
| } | |
| // reset streams for next command | |
| streams, done = terminal.StreamsForTesting(t) | |
| meta.View = views.NewView(streams) | |
| meta.Streams = streams | |
| c := &ValidateCommand{ | |
| Meta: meta, | |
| } | |
| var args []string | |
| args = append(args, "-no-color") | |
| code := c.Run(args) | |
| output = done(t) | |
| if code != 0 { | |
| t.Errorf("Should have passed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| actual := output.All() | |
| expected := ` | |
| Warning: Invalid override target | |
| on main.tftest.hcl line 4, in mock_provider "test": | |
| 4: target = test_resource.absent_one | |
| The override target test_resource.absent_one does not exist within the | |
| configuration under test. This could indicate a typo in the target address or | |
| an unnecessary override. | |
| (and 5 more similar warnings elsewhere) | |
| Success! The configuration is valid, but there were some validation warnings | |
| as shown above. | |
| ` | |
| if diff := cmp.Diff(expected, actual); len(diff) > 0 { | |
| t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) | |
| } | |
| } | |
| func TestValidate_json(t *testing.T) { | |
| tests := []struct { | |
| path string | |
| valid bool | |
| }{ | |
| {"validate-valid", true}, | |
| {"validate-invalid", false}, | |
| {"validate-invalid/missing_quote", false}, | |
| {"validate-invalid/missing_var", false}, | |
| {"validate-invalid/multiple_providers", false}, | |
| {"validate-invalid/multiple_modules", false}, | |
| {"validate-invalid/multiple_resources", false}, | |
| {"validate-invalid/duplicate_import_targets", false}, | |
| {"validate-invalid/outputs", false}, | |
| {"validate-invalid/incorrectmodulename", false}, | |
| {"validate-invalid/interpolation", false}, | |
| {"validate-invalid/missing_defined_var", true}, | |
| } | |
| for _, tc := range tests { | |
| t.Run(tc.path, func(t *testing.T) { | |
| var want, got map[string]any | |
| wantFile, err := os.Open(path.Join(testFixturePath(tc.path), "output.json")) | |
| if err != nil { | |
| t.Fatalf("failed to open output file: %s", err) | |
| } | |
| defer wantFile.Close() | |
| wantBytes, err := io.ReadAll(wantFile) | |
| if err != nil { | |
| t.Fatalf("failed to read output file: %s", err) | |
| } | |
| err = json.Unmarshal([]byte(wantBytes), &want) | |
| if err != nil { | |
| t.Fatalf("failed to unmarshal expected JSON: %s", err) | |
| } | |
| output, code := setupTest(t, tc.path, "-json") | |
| gotString := output.Stdout() | |
| err = json.Unmarshal([]byte(gotString), &got) | |
| if err != nil { | |
| t.Fatalf("failed to unmarshal actual JSON: %s", err) | |
| } | |
| if !cmp.Equal(got, want) { | |
| t.Errorf("wrong output:\n %v\n", cmp.Diff(got, want)) | |
| t.Errorf("raw output:\n%s\n", gotString) | |
| } | |
| if tc.valid && code != 0 { | |
| t.Errorf("wrong exit code: want 0, got %d", code) | |
| } else if !tc.valid && code != 1 { | |
| t.Errorf("wrong exit code: want 1, got %d", code) | |
| } | |
| if errorOutput := output.Stderr(); errorOutput != "" { | |
| t.Errorf("unexpected error output:\n%s", errorOutput) | |
| } | |
| }) | |
| } | |
| } | |
| func TestValidate_ensure_json_diags(t *testing.T) { | |
| t.Run("-var error diags are rendered using the correct view ", func(t *testing.T) { | |
| output, code := setupTest(t, "validate-invalid", "-json", "-var", "foo") | |
| if code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| if output.Stderr() != "" { | |
| t.Fatalf("Expected output, all json output should go to stdout, got stderr: %s", output.Stderr()) | |
| } | |
| gotString := output.Stdout() | |
| var got map[string]any | |
| err := json.Unmarshal([]byte(gotString), &got) | |
| if err != nil { | |
| t.Fatalf("Test did not produce valid JSON: %s", err) | |
| } | |
| }) | |
| t.Run("Flag/argument parsing error diags are rendered using the correct view ", func(t *testing.T) { | |
| output, code := setupTest(t, "validate-invalid", "-json", "-foobar") | |
| if code != 1 { | |
| t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) | |
| } | |
| if output.Stderr() != "" { | |
| t.Fatalf("Expected output, all json output should go to stdout, got stderr: %s", output.Stderr()) | |
| } | |
| gotString := output.Stdout() | |
| var got map[string]any | |
| err := json.Unmarshal([]byte(gotString), &got) | |
| if err != nil { | |
| t.Fatalf("Test did not produce valid JSON: %s", err) | |
| } | |
| }) | |
| } | |
| func TestValidateWithInvalidListResource(t *testing.T) { | |
| td := t.TempDir() | |
| cases := []struct { | |
| name string | |
| path string | |
| wantError string | |
| args []string | |
| code int | |
| }{ | |
| { | |
| name: "invalid-traversal with validate -query command", | |
| path: "query/invalid-traversal", | |
| wantError: ` | |
| Error: Invalid list resource traversal | |
| on main.tfquery.hcl line 19, in list "test_instance" "test2": | |
| 19: ami = list.test_instance.test.state.instance_type | |
| The first step in the traversal for a list resource must be an attribute | |
| "data". | |
| `, | |
| args: []string{"-query"}, | |
| code: 1, | |
| }, | |
| { | |
| name: "invalid-traversal with no -query", | |
| path: "query/invalid-traversal", | |
| }, | |
| } | |
| for _, tc := range cases { | |
| t.Run(tc.name, func(t *testing.T) { | |
| testCopyDir(t, testFixturePath(tc.path), td) | |
| t.Chdir(td) | |
| streams, done := terminal.StreamsForTesting(t) | |
| view := views.NewView(streams) | |
| ui := testUiWrapped(t) | |
| provider := queryFixtureProvider() | |
| providerSource := newMockProviderSource(t, map[string][]string{ | |
| "test": {"1.0.0"}, | |
| }) | |
| meta := Meta{ | |
| testingOverrides: metaOverridesForProvider(provider), | |
| Ui: ui, | |
| View: view, | |
| Streams: streams, | |
| ProviderSource: providerSource, | |
| } | |
| init := &InitCommand{ | |
| Meta: meta, | |
| } | |
| if code := init.Run(nil); code != 0 { | |
| t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter) | |
| } | |
| c := &ValidateCommand{ | |
| Meta: meta, | |
| } | |
| var args []string | |
| args = append(args, "-no-color") | |
| args = append(args, tc.args...) | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != tc.code { | |
| t.Fatalf("Expected status code %d but got %d: %s", tc.code, code, output.Stderr()) | |
| } | |
| if diff := cmp.Diff(tc.wantError, output.Stderr()); diff != "" { | |
| t.Fatalf("Expected error string %q but got %q\n\ndiff: \n%s", tc.wantError, output.Stderr(), diff) | |
| } | |
| }) | |
| } | |
| } | |
| func TestValidate_backendBlocks(t *testing.T) { | |
| // This type of error is detected when parsing hcl, and isn't validation | |
| // specific to backend blocks. | |
| t.Run("invalid when block contains a repeated attribute", func(t *testing.T) { | |
| output, code := setupTest(t, "invalid-backend-configuration/repeated-attr") | |
| if code != 1 { | |
| t.Fatalf("unexpected successful exit code %d\n\n%s", code, output.Stdout()) | |
| } | |
| expectedErr := "Error: Attribute redefined" | |
| if !strings.Contains(output.Stderr(), expectedErr) { | |
| t.Fatalf("unexpected error content: wanted %q, got: %s", | |
| expectedErr, | |
| output.Stderr(), | |
| ) | |
| } | |
| }) | |
| t.Run("invalid when the backend type is unknown", func(t *testing.T) { | |
| output, code := setupTest(t, "invalid-backend-configuration/unknown-backend-type") | |
| if code != 1 { | |
| t.Fatalf("expected an unsuccessful exit code %d\n\n%s", code, output.Stderr()) | |
| } | |
| expectedErr := "Error: Unsupported backend type" | |
| if !strings.Contains(output.Stderr(), expectedErr) { | |
| t.Fatalf("unexpected error content: wanted %q, got: %s", | |
| expectedErr, | |
| output.Stderr(), | |
| ) | |
| } | |
| }) | |
| t.Run("removed backends cause errors with extra info", func(t *testing.T) { | |
| output, code := setupTest(t, "invalid-backend-configuration/removed-backend-type") | |
| if code != 1 { | |
| t.Fatalf("expected an unsuccessful exit code %d\n\n%s", code, output.Stderr()) | |
| } | |
| expectedErrMsgs := []string{ | |
| "Error: Unsupported backend type", | |
| "The \"artifactory\" backend is not supported in Terraform v1.3 or later.", | |
| } | |
| for _, msg := range expectedErrMsgs { | |
| if !strings.Contains(output.Stderr(), msg) { | |
| t.Fatalf("unexpected error content: wanted to include %q, got: %s", | |
| msg, | |
| output.Stderr(), | |
| ) | |
| } | |
| } | |
| }) | |
| // We don't validate using the backend's schema due to potential use of the -backend-config flag. | |
| t.Run("unknown attributes are not detected by validate command", func(t *testing.T) { | |
| output, code := setupTest(t, "invalid-backend-configuration/unknown-attr") | |
| if code != 0 { | |
| t.Fatalf("expected a successful exit code %d\n\n%s", code, output.Stderr()) | |
| } | |
| expected := "Success! The configuration is valid." | |
| if !strings.Contains(output.Stdout(), expected) { | |
| t.Fatalf("unexpected error content: wanted %q, got: %s", | |
| expected, | |
| output.Stdout(), | |
| ) | |
| } | |
| }) | |
| // We don't validate using the backend's schema due to potential use of the -backend-config flag. | |
| t.Run("unset required attributes are not detected by validate command", func(t *testing.T) { | |
| output, code := setupTest(t, "invalid-backend-configuration/missing-required-attr") | |
| if code != 0 { | |
| t.Fatalf("expected a successful exit code %d\n\n%s", code, output.Stderr()) | |
| } | |
| expected := "Success! The configuration is valid." | |
| if !strings.Contains(output.Stdout(), expected) { | |
| t.Fatalf("unexpected error content: wanted %q, got: %s", | |
| expected, | |
| output.Stdout(), | |
| ) | |
| } | |
| }) | |
| } | |
| // Resources are validated using their schemas, so unknown or missing required attributes are identified. | |
| func TestValidate_resourceBlock(t *testing.T) { | |
| t.Run("invalid when block contains a repeated attribute", func(t *testing.T) { | |
| output, code := setupTest(t, "invalid-resource-configuration/repeated-attr") | |
| if code != 1 { | |
| t.Fatalf("unexpected successful exit code %d\n\n%s", code, output.Stdout()) | |
| } | |
| expectedErr := "Error: Attribute redefined" | |
| if !strings.Contains(output.Stderr(), expectedErr) { | |
| t.Fatalf("unexpected error content: wanted %q, got: %s", | |
| expectedErr, | |
| output.Stderr(), | |
| ) | |
| } | |
| }) | |
| t.Run("invalid when there's an unknown attribute present", func(t *testing.T) { | |
| output, code := setupTest(t, "invalid-resource-configuration/unknown-attr") | |
| if code != 1 { | |
| t.Fatalf("unexpected successful exit code %d\n\n%s", code, output.Stdout()) | |
| } | |
| expectedErr := "Error: Unsupported argument" | |
| if !strings.Contains(output.Stderr(), expectedErr) { | |
| t.Fatalf("unexpected error content: wanted %q, got: %s", | |
| expectedErr, | |
| output.Stderr(), | |
| ) | |
| } | |
| }) | |
| t.Run("invalid when a required attribute is unset", func(t *testing.T) { | |
| view, done := testView(t) | |
| p := testProvider() | |
| p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ | |
| ResourceTypes: map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "ami": {Type: cty.String, Required: true}, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "network_interface": { | |
| Nesting: configschema.NestingList, | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "device_index": {Type: cty.String, Optional: true}, | |
| "description": {Type: cty.String, Optional: true}, | |
| "name": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| c := &ValidateCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{"-no-color", testFixturePath("invalid-resource-configuration/missing-required-attr")} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("expected non-successful exit code %d\n\n%s", code, output.Stdout()) | |
| } | |
| expectedErr := "Error: Missing required argument" | |
| if !strings.Contains(output.Stderr(), expectedErr) { | |
| t.Fatalf("unexpected error content: wanted %q, got: %s", | |
| expectedErr, | |
| output.Stderr(), | |
| ) | |
| } | |
| }) | |
| } | |
| func TestValidate_child_module_backend_warning(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("invalid-child-modules/with-backend"), td) | |
| t.Chdir(td) | |
| view, done := testView(t) | |
| c := &ValidateCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(validateTestProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{"-no-color"} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("expected successful exit code, got: %d\n\n%s", code, output.Stdout()) | |
| } | |
| expectedWarning := "Warning: Backend configuration ignored" | |
| if !strings.Contains(output.Stdout(), expectedWarning) { | |
| t.Fatalf("unexpected stdout content: wanted %q, got: %s", | |
| expectedWarning, | |
| output.Stdout(), | |
| ) | |
| } | |
| } | |
| func TestValidate_child_module_cloud_block_warning(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("invalid-child-modules/with-cloud-block"), td) | |
| t.Chdir(td) | |
| view, done := testView(t) | |
| c := &ValidateCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(validateTestProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{"-no-color"} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("expected successful exit code, got: %d\n\n%s", code, output.Stdout()) | |
| } | |
| expectedWarning := "Warning: Cloud configuration ignored" | |
| if !strings.Contains(output.Stdout(), expectedWarning) { | |
| t.Fatalf("unexpected stdout content: wanted %q, got: %s", | |
| expectedWarning, | |
| output.Stdout(), | |
| ) | |
| } | |
| } | |
| func TestValidate_child_module_import(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("valid-child-modules/with-import"), td) | |
| t.Chdir(td) | |
| view, done := testView(t) | |
| c := &ValidateCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(validateTestProvider()), | |
| View: view, | |
| }, | |
| } | |
| args := []string{"-no-color"} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("unexpected non-successful exit code %d\n\n%s", code, output.Stderr()) | |
| } | |
| } |