From b1fe5527123875b156a67952db10f5d22f6eb8f7 Mon Sep 17 00:00:00 2001 From: Marco Molteni Date: Mon, 29 Jan 2024 11:27:35 +0100 Subject: [PATCH 1/3] README: mention the "moved" block --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 40c6534..dd21b49 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,10 @@ For this reason Terravalet operates on local state and leaves to the operator th Be careful when using Terraform workspaces, since they are invisible and persistent global state :-(. Remember to always explicitly run `terraform workspace select` before anything else. +### Interactions with the "moved" block + +After the creation of Terravalet, Terraform introduced the `moved` block, which can be seen as an alternative to certain usages of Terravalet. See [Terraform: refactoring](https://developer.hashicorp.com/terraform/language/modules/develop/refactoring)) for more information. + ## Install ### Install from binary package From 3a12124de2273816902d7109f8048ced4736b892 Mon Sep 17 00:00:00 2001 From: Marco Molteni Date: Mon, 29 Jan 2024 14:26:13 +0100 Subject: [PATCH 2/3] main: uniform switch on subcommand --- main.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 26c7549..44380a1 100644 --- a/main.go +++ b/main.go @@ -74,8 +74,9 @@ func run() error { switch { case args.Rename != nil: - return doRename(args.Rename.Up, args.Rename.Down, - args.Rename.PlanPath, args.Rename.LocalStatePath, args.Rename.FuzzyMatch) + cmd := args.Rename + return doRename(cmd.Up, cmd.Down, cmd.PlanPath, cmd.LocalStatePath, + cmd.FuzzyMatch) case args.MoveAfter != nil: cmd := args.MoveAfter return doMoveAfter(cmd.Script, cmd.Before, cmd.After) @@ -83,12 +84,13 @@ func run() error { cmd := args.MoveBefore return doMoveBefore(cmd.Script, cmd.Before, cmd.After) case args.Import != nil: - return doImport(args.Import.Up, args.Import.Down, - args.Import.SrcPlanPath, args.Import.ResourceDefs) + cmd := args.Import + return doImport(cmd.Up, cmd.Down, cmd.SrcPlanPath, cmd.ResourceDefs) case args.Version != nil: fmt.Println("terravalet", fullVersion) return nil default: - return fmt.Errorf("internal error: unwired command: %s", parser.SubcommandNames()[0]) + return fmt.Errorf("internal error: unwired command: %s", + parser.SubcommandNames()[0]) } } From ce7bc1ffff6a86295aa348868cac3881320ab294 Mon Sep 17 00:00:00 2001 From: Marco Molteni Date: Mon, 29 Jan 2024 14:50:56 +0100 Subject: [PATCH 3/3] refactor: move tests to per-command test files --- cmdimport_test.go | 95 +++++++ cmdmoverename_test.go | 370 +++++++++++++++++++++++++++ main_test.go | 568 +++++------------------------------------- 3 files changed, 523 insertions(+), 510 deletions(-) create mode 100644 cmdimport_test.go create mode 100644 cmdmoverename_test.go diff --git a/cmdimport_test.go b/cmdimport_test.go new file mode 100644 index 0000000..4bdecdc --- /dev/null +++ b/cmdimport_test.go @@ -0,0 +1,95 @@ +package main + +import "testing" + +func TestRunImportSuccess(t *testing.T) { + testCases := []struct { + name string + resDefs string + srcPlanPath string + wantUpPath string + wantDownPath string + }{ + { + name: "import resources", + resDefs: "testdata/import/terravalet_imports_definitions.json", + srcPlanPath: "testdata/import/08_import_src-plan.json", + wantUpPath: "testdata/import/08_import_up.sh", + wantDownPath: "testdata/import/08_import_down.sh", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + args := []string{"terravalet", "import", + "--res-defs", tc.resDefs, + "--src-plan", tc.srcPlanPath, + } + + runSuccess(t, args, tc.wantUpPath, tc.wantDownPath) + }) + } +} + +func TestRunImportFailure(t *testing.T) { + testCases := []struct { + name string + resDefs string + srcPlanPath string + wantErr string + }{ + { + name: "non existing src-plan", + resDefs: "testdata/import/terravalet_imports_definitions.json", + srcPlanPath: "src-plan-path-dummy", + wantErr: "opening the terraform plan file: open src-plan-path-dummy: no such file or directory", + }, + { + name: "src-plan is invalid json", + resDefs: "testdata/import/terravalet_imports_definitions.json", + srcPlanPath: "testdata/import/09_import_empty_src-plan.json", + wantErr: "parse src-plan: parsing the plan: unexpected end of JSON input", + }, + { + name: "src-plan must create resource", + resDefs: "testdata/import/terravalet_imports_definitions.json", + srcPlanPath: "testdata/import/10_import_no-new-resources.json", + wantErr: "parse src-plan: src-plan doesn't contains resources to create", + }, + { + name: "src-plan contains only undefined resources", + resDefs: "testdata/import/terravalet_imports_definitions.json", + srcPlanPath: "testdata/import/11_import_src-plan_undefined_resources.json", + wantErr: "parse src-plan: src-plan contains only undefined resources", + }, + { + name: "src-plan contains a not existing resource parameter", + resDefs: "testdata/import/terravalet_imports_definitions.json", + srcPlanPath: "testdata/import/12_import_src-plan_invalid_resource_param.json", + wantErr: "parse src-plan: error in resources definition dummy_resource2: field 'long_name' doesn't exist in plan", + }, + { + name: "terravalet missing resources definitions file", + resDefs: "testdata/import/missing.file", + srcPlanPath: "testdata/import/08_import_src-plan.json", + wantErr: "opening the definitions file: open testdata/import/missing.file: no such file or directory", + }, + { + name: "terravalet invalid resources definitions file", + resDefs: "testdata/import/invalid_imports_definitions.json", + srcPlanPath: "testdata/import/08_import_src-plan.json", + wantErr: "parse src-plan: parsing resources definitions: invalid character '}' after object key", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + args := []string{"terravalet", "import", + "--res-defs", tc.resDefs, + "--src-plan", tc.srcPlanPath, + } + + runFailure(t, args, tc.wantErr) + }) + } +} diff --git a/cmdmoverename_test.go b/cmdmoverename_test.go new file mode 100644 index 0000000..1d37e8e --- /dev/null +++ b/cmdmoverename_test.go @@ -0,0 +1,370 @@ +package main + +import ( + "fmt" + "io" + "os" + "path" + "path/filepath" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestRunRenameSuccess(t *testing.T) { + testCases := []struct { + name string + options []string + planPath string + wantUpPath string + wantDownPath string + }{ + { + name: "exact match", + options: []string{}, + planPath: "testdata/rename/01_exact-match.plan.txt", + wantUpPath: "testdata/rename/01_exact-match.up.sh", + wantDownPath: "testdata/rename/01_exact-match.down.sh", + }, + { + name: "q-gram fuzzy match simple", + options: []string{"--fuzzy-match"}, + planPath: "testdata/rename/02_fuzzy-match.plan.txt", + wantUpPath: "testdata/rename/02_fuzzy-match.up.sh", + wantDownPath: "testdata/rename/02_fuzzy-match.down.sh", + }, + { + name: "q-gram fuzzy match complicated", + options: []string{"--fuzzy-match"}, + planPath: "testdata/rename/03_fuzzy-match.plan.txt", + wantUpPath: "testdata/rename/03_fuzzy-match.up.sh", + wantDownPath: "testdata/rename/03_fuzzy-match.down.sh", + }, + { + name: "q-gram fuzzy match complicated (regression)", + options: []string{"--fuzzy-match"}, + planPath: "testdata/rename/07_fuzzy-match.plan.txt", + wantUpPath: "testdata/rename/07_fuzzy-match.up.sh", + wantDownPath: "testdata/rename/07_fuzzy-match.down.sh", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + args := []string{"terravalet", "rename", "--plan", tc.planPath} + args = append(args, tc.options...) + + runSuccess(t, args, tc.wantUpPath, tc.wantDownPath) + }) + } +} + +func TestRunRenameFailure(t *testing.T) { + testCases := []struct { + name string + planPath string + wantErr string + }{ + { + name: "plan file doesn't exist", + planPath: "nonexisting", + wantErr: "opening the terraform plan file: open nonexisting: no such file or directory", + }, + { + name: "matchExact failure", + planPath: "testdata/rename/02_fuzzy-match.plan.txt", + wantErr: `matchExact: +unmatched create: + aws_route53_record.localhostnames_public["artifactory"] + aws_route53_record.loopback["artifactory"] + aws_route53_record.private["artifactory"] +unmatched destroy: + aws_route53_record.artifactory + aws_route53_record.artifactory_loopback + aws_route53_record.artifactory_private`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + args := []string{"terravalet", "rename", "--plan", tc.planPath} + + runFailure(t, args, tc.wantErr) + }) + } +} + +func TestRunMoveAfterSuccess(t *testing.T) { + testCases := []struct { + name string + before string + after string + wantScript string + }{ + { + name: "exact match", + before: "testdata/move-after/04-before", + after: "testdata/move-after/04-after", + wantScript: "testdata/move-after/04-want", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + args := []string{"terravalet", "move-after"} + + runMoveSuccess(t, args, tc.before, tc.after, tc.wantScript) + }) + } +} + +func TestRunMoveAfterFailure(t *testing.T) { + testCases := []struct { + name string + before string // special value: "non-existing" + after string // special value: "non-existing" + wantErr string + }{ + { + name: "non existing before tfplan", + before: "non-existing", + after: "non-existing", + wantErr: "opening the terraform BEFORE plan file: open non-existing.tfplan: no such file or directory", + }, + { + name: "non existing after tfplan", + before: "testdata/move-after/05-before", + after: "non-existing", + wantErr: "opening the terraform AFTER plan file: open non-existing.tfplan: no such file or directory", + }, + { + name: "before tfplan must only destroy", + before: "testdata/move-after/05-before", + after: "testdata/move-after/05-after", + wantErr: "BEFORE plan contains resources to create: [aws_batch_job_definition.foo]", + }, + { + name: "after tfplan must only create", + before: "testdata/move-after/06-before", + after: "testdata/move-after/06-after", + wantErr: "AFTER plan contains resources to destroy: [aws_batch_job_definition.foo]", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + args := []string{"terravalet", "move-after"} + + runMoveFailure(t, args, tc.before, tc.after, tc.wantErr) + }) + } +} + +func TestRunMoveBeforeSuccess(t *testing.T) { + testCases := []struct { + name string + before string + after string // special prefix: dummy + wantScript string + }{ + { + name: "happy path simple", + before: "testdata/move-before/01-before", + after: "dummy-01-after", + wantScript: "testdata/move-before/01-want", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + args := []string{"terravalet", "move-before"} + + runMoveSuccess(t, args, tc.before, tc.after, tc.wantScript) + }) + } +} + +func TestRunMoveBeforeFailure(t *testing.T) { + testCases := []struct { + name string + before string // special value: "non-existing" + wantErr string + }{ + { + name: "non existing BEFORE plan", + before: "non-existing", + wantErr: "opening the terraform BEFORE plan file: open non-existing.tfplan: no such file or directory", + }, + { + name: "BEFORE plan must not contain resources to destroy", + before: "testdata/move-before/02-before", + wantErr: "BEFORE plan contains resources to destroy: [aws_batch_compute_environment.foo_batch]", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + args := []string{"terravalet", "move-before", + "--before=" + tc.before, "--after=testdata/move-before/dummy-after", + } + + runMoveFailure(t, args, tc.before, "non-existing", tc.wantErr) + }) + } +} + +// If after has special prefix "dummy", it will not attempt to copy the +// corresponding tfplan files, to accomodate for move-before. +func runMoveSuccess(t *testing.T, args []string, before, after, wantScript string) { + wantUpPath := wantScript + "_up.sh" + wantUp, err := os.ReadFile(wantUpPath) + if err != nil { + t.Fatalf("reading want up file: %v", err) + } + + wantDownPath := wantScript + "_down.sh" + wantDown, err := os.ReadFile(wantDownPath) + if err != nil { + t.Fatalf("reading want down file: %v", err) + } + + tmpDir, err := os.MkdirTemp("", "terravalet") + if err != nil { + t.Fatalf("creating temporary dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Copy the required input files to the tmpdir. + if err := copyfile(before+".tfplan", + filepath.Join(tmpDir, path.Base(before)+".tfplan")); err != nil { + t.Fatal(err) + } + if !strings.HasPrefix(after, "dummy") { + if err := copyfile(after+".tfplan", + filepath.Join(tmpDir, path.Base(after)+".tfplan")); err != nil { + t.Fatal(err) + } + } + + // Change directory to the tmpdir. + cwd, err := os.Getwd() + if err != nil { + t.Fatal("getwd:", err) + } + if err := os.Chdir(tmpDir); err != nil { + t.Fatal("chdir:", err) + } + defer func() { + if err := os.Chdir(cwd); err != nil { + panic(err) + } + }() + + tmpScript := path.Base(wantScript) + tmpUpPath := tmpScript + "_up.sh" + tmpDownPath := tmpScript + "_down.sh" + + args = append(args, "--before="+path.Base(before), "--after="+path.Base(after), + "--script="+tmpScript) + os.Args = args + + if err := run(); err != nil { + t.Fatalf("run: args: %s\nhave: %q\nwant: no error", args, err) + } + t.Log("SUT ran successfully") + + tmpUp, err := os.ReadFile(tmpUpPath) + if err != nil { + t.Fatalf("reading tmp up file: %s", err) + } + tmpDown, err := os.ReadFile(tmpDownPath) + if err != nil { + t.Fatalf("reading tmp down file: %s", err) + } + + if diff := cmp.Diff(string(wantUp), string(tmpUp)); diff != "" { + t.Errorf("\nup script: mismatch (-want +have):\n"+ + "(want path: %s)\n"+ + "%s", wantUpPath, diff) + } + if diff := cmp.Diff(string(wantDown), string(tmpDown)); diff != "" { + t.Errorf("\ndown script: mismatch (-want +have):\n"+ + "(want path: %s)\n"+ + "%s", wantDownPath, diff) + } +} + +// If before or after have the special value "non-existing", it will not attempt to copy the +// corresponding tfplan files, allowing to test a missing file. +func runMoveFailure(t *testing.T, args []string, before, after, wantErr string) { + tmpDir, err := os.MkdirTemp("", "terravalet") + if err != nil { + t.Fatalf("creating temporary dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Copy the required input files to the tmpdir. + if before != "non-existing" { + if err := copyfile(before+".tfplan", + filepath.Join(tmpDir, path.Base(before)+".tfplan")); err != nil { + t.Fatal(err) + } + } + if after != "non-existing" { + if err := copyfile(after+".tfplan", + filepath.Join(tmpDir, path.Base(after)+".tfplan")); err != nil { + t.Fatal(err) + } + } + + // Change directory to the tmpdir. + cwd, err := os.Getwd() + if err != nil { + t.Fatal("getwd:", err) + } + if err := os.Chdir(tmpDir); err != nil { + t.Fatal("chdir:", err) + } + defer func() { + if err := os.Chdir(cwd); err != nil { + panic(err) + } + }() + + args = append(args, "--before="+path.Base(before), "--after="+path.Base(after), + "--script=dummy-script") + os.Args = args + + err = run() + + if err == nil { + t.Fatalf("run: args: %s\nhave: no error\nwant: %q", args, wantErr) + } + if diff := cmp.Diff(wantErr, err.Error()); diff != "" { + t.Errorf("error message mismatch (-want +have):\n%s", diff) + } +} + +// copyfile copies file src to dst. It is not robust for production use (a lot of OS-dependent +// corner cases) but is good enough for tests. +func copyfile(src, dst string) error { + srcFile, err := os.Open(src) + if err != nil { + return fmt.Errorf("opening src file: %s", err) + } + defer srcFile.Close() + + // Create (or truncate) the dst file + dstFile, err := os.Create(dst) + if err != nil { + return fmt.Errorf("creating dst file: %s", err) + } + defer dstFile.Close() + + if _, err := io.Copy(dstFile, srcFile); err != nil { + return err + } + + return nil +} diff --git a/main_test.go b/main_test.go index f54db82..6cb9d9b 100644 --- a/main_test.go +++ b/main_test.go @@ -2,10 +2,7 @@ package main import ( "fmt" - "io" "os" - "path" - "path/filepath" "strings" "testing" @@ -25,415 +22,6 @@ func Example_version() { // terravalet unknown } -func TestRunRenameSuccess(t *testing.T) { - testCases := []struct { - name string - options []string - planPath string - wantUpPath string - wantDownPath string - }{ - { - name: "exact match", - options: []string{}, - planPath: "testdata/rename/01_exact-match.plan.txt", - wantUpPath: "testdata/rename/01_exact-match.up.sh", - wantDownPath: "testdata/rename/01_exact-match.down.sh", - }, - { - name: "q-gram fuzzy match simple", - options: []string{"--fuzzy-match"}, - planPath: "testdata/rename/02_fuzzy-match.plan.txt", - wantUpPath: "testdata/rename/02_fuzzy-match.up.sh", - wantDownPath: "testdata/rename/02_fuzzy-match.down.sh", - }, - { - name: "q-gram fuzzy match complicated", - options: []string{"--fuzzy-match"}, - planPath: "testdata/rename/03_fuzzy-match.plan.txt", - wantUpPath: "testdata/rename/03_fuzzy-match.up.sh", - wantDownPath: "testdata/rename/03_fuzzy-match.down.sh", - }, - { - name: "q-gram fuzzy match complicated (regression)", - options: []string{"--fuzzy-match"}, - planPath: "testdata/rename/07_fuzzy-match.plan.txt", - wantUpPath: "testdata/rename/07_fuzzy-match.up.sh", - wantDownPath: "testdata/rename/07_fuzzy-match.down.sh", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - args := []string{"terravalet", "rename", "--plan", tc.planPath} - args = append(args, tc.options...) - - runSuccess(t, args, tc.wantUpPath, tc.wantDownPath) - }) - } -} - -func TestRunRenameFailure(t *testing.T) { - testCases := []struct { - name string - planPath string - wantErr string - }{ - { - name: "plan file doesn't exist", - planPath: "nonexisting", - wantErr: "opening the terraform plan file: open nonexisting: no such file or directory", - }, - { - name: "matchExact failure", - planPath: "testdata/rename/02_fuzzy-match.plan.txt", - wantErr: `matchExact: -unmatched create: - aws_route53_record.localhostnames_public["artifactory"] - aws_route53_record.loopback["artifactory"] - aws_route53_record.private["artifactory"] -unmatched destroy: - aws_route53_record.artifactory - aws_route53_record.artifactory_loopback - aws_route53_record.artifactory_private`, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - args := []string{"terravalet", "rename", "--plan", tc.planPath} - - runFailure(t, args, tc.wantErr) - }) - } -} - -func TestRunMoveAfterSuccess(t *testing.T) { - testCases := []struct { - name string - before string - after string - wantScript string - }{ - { - name: "exact match", - before: "testdata/move-after/04-before", - after: "testdata/move-after/04-after", - wantScript: "testdata/move-after/04-want", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - args := []string{"terravalet", "move-after"} - - runMoveSuccess(t, args, tc.before, tc.after, tc.wantScript) - }) - } -} - -func TestRunMoveAfterFailure(t *testing.T) { - testCases := []struct { - name string - before string // special value: "non-existing" - after string // special value: "non-existing" - wantErr string - }{ - { - name: "non existing before tfplan", - before: "non-existing", - after: "non-existing", - wantErr: "opening the terraform BEFORE plan file: open non-existing.tfplan: no such file or directory", - }, - { - name: "non existing after tfplan", - before: "testdata/move-after/05-before", - after: "non-existing", - wantErr: "opening the terraform AFTER plan file: open non-existing.tfplan: no such file or directory", - }, - { - name: "before tfplan must only destroy", - before: "testdata/move-after/05-before", - after: "testdata/move-after/05-after", - wantErr: "BEFORE plan contains resources to create: [aws_batch_job_definition.foo]", - }, - { - name: "after tfplan must only create", - before: "testdata/move-after/06-before", - after: "testdata/move-after/06-after", - wantErr: "AFTER plan contains resources to destroy: [aws_batch_job_definition.foo]", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - args := []string{"terravalet", "move-after"} - - runMoveFailure(t, args, tc.before, tc.after, tc.wantErr) - }) - } -} - -func TestRunMoveBeforeSuccess(t *testing.T) { - testCases := []struct { - name string - before string - after string // special prefix: dummy - wantScript string - }{ - { - name: "happy path simple", - before: "testdata/move-before/01-before", - after: "dummy-01-after", - wantScript: "testdata/move-before/01-want", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - args := []string{"terravalet", "move-before"} - - runMoveSuccess(t, args, tc.before, tc.after, tc.wantScript) - }) - } -} - -func TestRunMoveBeforeFailure(t *testing.T) { - testCases := []struct { - name string - before string // special value: "non-existing" - wantErr string - }{ - { - name: "non existing BEFORE plan", - before: "non-existing", - wantErr: "opening the terraform BEFORE plan file: open non-existing.tfplan: no such file or directory", - }, - { - name: "BEFORE plan must not contain resources to destroy", - before: "testdata/move-before/02-before", - wantErr: "BEFORE plan contains resources to destroy: [aws_batch_compute_environment.foo_batch]", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - args := []string{"terravalet", "move-before", - "--before=" + tc.before, "--after=testdata/move-before/dummy-after", - } - - runMoveFailure(t, args, tc.before, "non-existing", tc.wantErr) - }) - } -} - -// If after has special prefix "dummy", it will not attempt to copy the -// corresponding tfplan files, to accomodate for move-before. -func runMoveSuccess(t *testing.T, args []string, before, after, wantScript string) { - wantUpPath := wantScript + "_up.sh" - wantUp, err := os.ReadFile(wantUpPath) - if err != nil { - t.Fatalf("reading want up file: %v", err) - } - - wantDownPath := wantScript + "_down.sh" - wantDown, err := os.ReadFile(wantDownPath) - if err != nil { - t.Fatalf("reading want down file: %v", err) - } - - tmpDir, err := os.MkdirTemp("", "terravalet") - if err != nil { - t.Fatalf("creating temporary dir: %v", err) - } - defer os.RemoveAll(tmpDir) - - // Copy the required input files to the tmpdir. - if err := copyfile(before+".tfplan", - filepath.Join(tmpDir, path.Base(before)+".tfplan")); err != nil { - t.Fatal(err) - } - if !strings.HasPrefix(after, "dummy") { - if err := copyfile(after+".tfplan", - filepath.Join(tmpDir, path.Base(after)+".tfplan")); err != nil { - t.Fatal(err) - } - } - - // Change directory to the tmpdir. - cwd, err := os.Getwd() - if err != nil { - t.Fatal("getwd:", err) - } - if err := os.Chdir(tmpDir); err != nil { - t.Fatal("chdir:", err) - } - defer func() { - if err := os.Chdir(cwd); err != nil { - panic(err) - } - }() - - tmpScript := path.Base(wantScript) - tmpUpPath := tmpScript + "_up.sh" - tmpDownPath := tmpScript + "_down.sh" - - args = append(args, "--before="+path.Base(before), "--after="+path.Base(after), - "--script="+tmpScript) - os.Args = args - - if err := run(); err != nil { - t.Fatalf("run: args: %s\nhave: %q\nwant: no error", args, err) - } - t.Log("SUT ran successfully") - - tmpUp, err := os.ReadFile(tmpUpPath) - if err != nil { - t.Fatalf("reading tmp up file: %s", err) - } - tmpDown, err := os.ReadFile(tmpDownPath) - if err != nil { - t.Fatalf("reading tmp down file: %s", err) - } - - if diff := cmp.Diff(string(wantUp), string(tmpUp)); diff != "" { - t.Errorf("\nup script: mismatch (-want +have):\n"+ - "(want path: %s)\n"+ - "%s", wantUpPath, diff) - } - if diff := cmp.Diff(string(wantDown), string(tmpDown)); diff != "" { - t.Errorf("\ndown script: mismatch (-want +have):\n"+ - "(want path: %s)\n"+ - "%s", wantDownPath, diff) - } -} - -// If before or after have the special value "non-existing", it will not attempt to copy the -// corresponding tfplan files, allowing to test a missing file. -func runMoveFailure(t *testing.T, args []string, before, after, wantErr string) { - tmpDir, err := os.MkdirTemp("", "terravalet") - if err != nil { - t.Fatalf("creating temporary dir: %v", err) - } - defer os.RemoveAll(tmpDir) - - // Copy the required input files to the tmpdir. - if before != "non-existing" { - if err := copyfile(before+".tfplan", - filepath.Join(tmpDir, path.Base(before)+".tfplan")); err != nil { - t.Fatal(err) - } - } - if after != "non-existing" { - if err := copyfile(after+".tfplan", - filepath.Join(tmpDir, path.Base(after)+".tfplan")); err != nil { - t.Fatal(err) - } - } - - // Change directory to the tmpdir. - cwd, err := os.Getwd() - if err != nil { - t.Fatal("getwd:", err) - } - if err := os.Chdir(tmpDir); err != nil { - t.Fatal("chdir:", err) - } - defer func() { - if err := os.Chdir(cwd); err != nil { - panic(err) - } - }() - - args = append(args, "--before="+path.Base(before), "--after="+path.Base(after), - "--script=dummy-script") - os.Args = args - - err = run() - - if err == nil { - t.Fatalf("run: args: %s\nhave: no error\nwant: %q", args, wantErr) - } - if diff := cmp.Diff(wantErr, err.Error()); diff != "" { - t.Errorf("error message mismatch (-want +have):\n%s", diff) - } -} - -func runSuccess(t *testing.T, args []string, wantUpPath string, wantDownPath string) { - wantUp, err := os.ReadFile(wantUpPath) - if err != nil { - t.Fatalf("reading want up file: %v", err) - } - wantDown, err := os.ReadFile(wantDownPath) - if err != nil { - t.Fatalf("reading want down file: %v", err) - } - - tmpDir, err := os.MkdirTemp("", "terravalet") - if err != nil { - t.Fatalf("creating temporary dir: %v", err) - } - defer os.RemoveAll(tmpDir) - - tmpUpPath := tmpDir + "/up" - tmpDownPath := tmpDir + "/down" - - args = append(args, "--up", tmpUpPath, "--down", tmpDownPath) - os.Args = args - - if err := run(); err != nil { - t.Fatalf("run: args: %s\nhave: %q\nwant: no error", args, err) - } - - tmpUp, err := os.ReadFile(tmpUpPath) - if err != nil { - t.Fatalf("reading tmp up file: %v", err) - } - tmpDown, err := os.ReadFile(tmpDownPath) - if err != nil { - t.Fatalf("reading tmp down file: %v", err) - } - - if diff := cmp.Diff(string(wantUp), string(tmpUp)); diff != "" { - t.Errorf("\nup script: mismatch (-want +have):\n"+ - "(want path: %s)\n"+ - "%s", wantUpPath, diff) - } - if diff := cmp.Diff(string(wantDown), string(tmpDown)); diff != "" { - t.Errorf("\ndown script: mismatch (-want +have):\n"+ - "(want path: %s)\n"+ - "%s", wantDownPath, diff) - } -} - -func runFailure(t *testing.T, args []string, wantErr string) { - tmpDir, err := os.MkdirTemp("", "terravalet") - if err != nil { - t.Fatalf("creating temporary dir: %v", err) - } - defer os.RemoveAll(tmpDir) - - tmpUpPath := tmpDir + "/up" - tmpDownPath := tmpDir + "/down" - - args = append(args, "--up", tmpUpPath, "--down", tmpDownPath) - os.Args = args - - err = run() - - if err == nil { - t.Fatalf("run: args: %s\nhave: no error\nwant: %q", args, wantErr) - } - if diff := cmp.Diff(wantErr, err.Error()); diff != "" { - t.Errorf("error message mismatch (-want +have):\n%s", diff) - } -} - -// Used to compare sets. -var setCmp = cmp.Comparer(func(s1, s2 *strset.Set) bool { - return s1.IsEqual(s2) -}) - func TestParseSuccess(t *testing.T) { testCases := []struct { name string @@ -689,117 +277,77 @@ func TestMatchFuzzyError(t *testing.T) { } } -func TestRunImportSuccess(t *testing.T) { - testCases := []struct { - name string - resDefs string - srcPlanPath string - wantUpPath string - wantDownPath string - }{ - { - name: "import resources", - resDefs: "testdata/import/terravalet_imports_definitions.json", - srcPlanPath: "testdata/import/08_import_src-plan.json", - wantUpPath: "testdata/import/08_import_up.sh", - wantDownPath: "testdata/import/08_import_down.sh", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - args := []string{"terravalet", "import", - "--res-defs", tc.resDefs, - "--src-plan", tc.srcPlanPath, - } +// Used to compare sets. +var setCmp = cmp.Comparer(func(s1, s2 *strset.Set) bool { + return s1.IsEqual(s2) +}) - runSuccess(t, args, tc.wantUpPath, tc.wantDownPath) - }) +func runSuccess(t *testing.T, args []string, wantUpPath string, wantDownPath string) { + wantUp, err := os.ReadFile(wantUpPath) + if err != nil { + t.Fatalf("reading want up file: %v", err) + } + wantDown, err := os.ReadFile(wantDownPath) + if err != nil { + t.Fatalf("reading want down file: %v", err) } -} -func TestRunImportFailure(t *testing.T) { - testCases := []struct { - name string - resDefs string - srcPlanPath string - wantErr string - }{ - { - name: "non existing src-plan", - resDefs: "testdata/import/terravalet_imports_definitions.json", - srcPlanPath: "src-plan-path-dummy", - wantErr: "opening the terraform plan file: open src-plan-path-dummy: no such file or directory", - }, - { - name: "src-plan is invalid json", - resDefs: "testdata/import/terravalet_imports_definitions.json", - srcPlanPath: "testdata/import/09_import_empty_src-plan.json", - wantErr: "parse src-plan: parsing the plan: unexpected end of JSON input", - }, - { - name: "src-plan must create resource", - resDefs: "testdata/import/terravalet_imports_definitions.json", - srcPlanPath: "testdata/import/10_import_no-new-resources.json", - wantErr: "parse src-plan: src-plan doesn't contains resources to create", - }, - { - name: "src-plan contains only undefined resources", - resDefs: "testdata/import/terravalet_imports_definitions.json", - srcPlanPath: "testdata/import/11_import_src-plan_undefined_resources.json", - wantErr: "parse src-plan: src-plan contains only undefined resources", - }, - { - name: "src-plan contains a not existing resource parameter", - resDefs: "testdata/import/terravalet_imports_definitions.json", - srcPlanPath: "testdata/import/12_import_src-plan_invalid_resource_param.json", - wantErr: "parse src-plan: error in resources definition dummy_resource2: field 'long_name' doesn't exist in plan", - }, - { - name: "terravalet missing resources definitions file", - resDefs: "testdata/import/missing.file", - srcPlanPath: "testdata/import/08_import_src-plan.json", - wantErr: "opening the definitions file: open testdata/import/missing.file: no such file or directory", - }, - { - name: "terravalet invalid resources definitions file", - resDefs: "testdata/import/invalid_imports_definitions.json", - srcPlanPath: "testdata/import/08_import_src-plan.json", - wantErr: "parse src-plan: parsing resources definitions: invalid character '}' after object key", - }, + tmpDir, err := os.MkdirTemp("", "terravalet") + if err != nil { + t.Fatalf("creating temporary dir: %v", err) } + defer os.RemoveAll(tmpDir) - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - args := []string{"terravalet", "import", - "--res-defs", tc.resDefs, - "--src-plan", tc.srcPlanPath, - } + tmpUpPath := tmpDir + "/up" + tmpDownPath := tmpDir + "/down" - runFailure(t, args, tc.wantErr) - }) + args = append(args, "--up", tmpUpPath, "--down", tmpDownPath) + os.Args = args + + if err := run(); err != nil { + t.Fatalf("run: args: %s\nhave: %q\nwant: no error", args, err) } -} -// copyfile copies file src to dst. It is not robust for production use (a lot of OS-dependent -// corner cases) but is good enough for tests. -func copyfile(src, dst string) error { - srcFile, err := os.Open(src) + tmpUp, err := os.ReadFile(tmpUpPath) if err != nil { - return fmt.Errorf("opening src file: %s", err) + t.Fatalf("reading tmp up file: %v", err) } - defer srcFile.Close() - - // Create (or truncate) the dst file - dstFile, err := os.Create(dst) + tmpDown, err := os.ReadFile(tmpDownPath) if err != nil { - return fmt.Errorf("creating dst file: %s", err) + t.Fatalf("reading tmp down file: %v", err) + } + + if diff := cmp.Diff(string(wantUp), string(tmpUp)); diff != "" { + t.Errorf("\nup script: mismatch (-want +have):\n"+ + "(want path: %s)\n"+ + "%s", wantUpPath, diff) + } + if diff := cmp.Diff(string(wantDown), string(tmpDown)); diff != "" { + t.Errorf("\ndown script: mismatch (-want +have):\n"+ + "(want path: %s)\n"+ + "%s", wantDownPath, diff) } - defer dstFile.Close() +} - if _, err := io.Copy(dstFile, srcFile); err != nil { - return err +func runFailure(t *testing.T, args []string, wantErr string) { + tmpDir, err := os.MkdirTemp("", "terravalet") + if err != nil { + t.Fatalf("creating temporary dir: %v", err) } + defer os.RemoveAll(tmpDir) - return nil + tmpUpPath := tmpDir + "/up" + tmpDownPath := tmpDir + "/down" + + args = append(args, "--up", tmpUpPath, "--down", tmpDownPath) + os.Args = args + + err = run() + + if err == nil { + t.Fatalf("run: args: %s\nhave: no error\nwant: %q", args, wantErr) + } + if diff := cmp.Diff(wantErr, err.Error()); diff != "" { + t.Errorf("error message mismatch (-want +have):\n%s", diff) + } }