terraform/command/validate_test.go

268 lines
8.2 KiB
Go

package command
import (
"encoding/json"
"io/ioutil"
"os"
"path"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/providers"
)
func setupTest(t *testing.T, fixturepath string, args ...string) (*terminal.TestOutput, int) {
view, done := testView(t)
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: &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},
},
},
},
},
},
},
},
}
c := &ValidateCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
args = append(args, "-no-color")
args = append(args, testFixturePath(fixturepath))
code := c.Run(args)
return done(t), code
}
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 := tempDir(t)
testCopyDir(t, testFixturePath("validate-valid/with-tfvars-file"), td)
defer os.RemoveAll(td)
defer testChdir(t, 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 TestSameProviderMutipleTimesShouldFail(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 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())
}
wantError = `Error: Variables not allowed`
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 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/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]interface{}
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 := ioutil.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)
}
})
}
}