github.com/pix4d/terravalet@v0.8.1-0.20240131132849-abcd6a79eeeb/cmdmoverename_test.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/google/go-cmp/cmp"
    13  )
    14  
    15  func TestRunRenameSuccess(t *testing.T) {
    16  	testCases := []struct {
    17  		name         string
    18  		options      []string
    19  		planPath     string
    20  		wantUpPath   string
    21  		wantDownPath string
    22  	}{
    23  		{
    24  			name:         "exact match",
    25  			options:      []string{},
    26  			planPath:     "testdata/rename/01_exact-match.plan.txt",
    27  			wantUpPath:   "testdata/rename/01_exact-match.up.sh",
    28  			wantDownPath: "testdata/rename/01_exact-match.down.sh",
    29  		},
    30  		{
    31  			name:         "q-gram fuzzy match simple",
    32  			options:      []string{"--fuzzy-match"},
    33  			planPath:     "testdata/rename/02_fuzzy-match.plan.txt",
    34  			wantUpPath:   "testdata/rename/02_fuzzy-match.up.sh",
    35  			wantDownPath: "testdata/rename/02_fuzzy-match.down.sh",
    36  		},
    37  		{
    38  			name:         "q-gram fuzzy match complicated",
    39  			options:      []string{"--fuzzy-match"},
    40  			planPath:     "testdata/rename/03_fuzzy-match.plan.txt",
    41  			wantUpPath:   "testdata/rename/03_fuzzy-match.up.sh",
    42  			wantDownPath: "testdata/rename/03_fuzzy-match.down.sh",
    43  		},
    44  		{
    45  			name:         "q-gram fuzzy match complicated (regression)",
    46  			options:      []string{"--fuzzy-match"},
    47  			planPath:     "testdata/rename/07_fuzzy-match.plan.txt",
    48  			wantUpPath:   "testdata/rename/07_fuzzy-match.up.sh",
    49  			wantDownPath: "testdata/rename/07_fuzzy-match.down.sh",
    50  		},
    51  	}
    52  
    53  	for _, tc := range testCases {
    54  		t.Run(tc.name, func(t *testing.T) {
    55  			args := []string{"terravalet", "rename", "--plan", tc.planPath}
    56  			args = append(args, tc.options...)
    57  
    58  			runSuccess(t, args, tc.wantUpPath, tc.wantDownPath)
    59  		})
    60  	}
    61  }
    62  
    63  func TestRunRenameFailure(t *testing.T) {
    64  	testCases := []struct {
    65  		name     string
    66  		planPath string
    67  		wantErr  string
    68  	}{
    69  		{
    70  			name:     "plan file doesn't exist",
    71  			planPath: "nonexisting",
    72  			wantErr:  "opening the terraform plan file: open nonexisting: no such file or directory",
    73  		},
    74  		{
    75  			name:     "matchExact failure",
    76  			planPath: "testdata/rename/02_fuzzy-match.plan.txt",
    77  			wantErr: `matchExact:
    78  unmatched create:
    79    aws_route53_record.localhostnames_public["artifactory"]
    80    aws_route53_record.loopback["artifactory"]
    81    aws_route53_record.private["artifactory"]
    82  unmatched destroy:
    83    aws_route53_record.artifactory
    84    aws_route53_record.artifactory_loopback
    85    aws_route53_record.artifactory_private`,
    86  		},
    87  	}
    88  
    89  	for _, tc := range testCases {
    90  		t.Run(tc.name, func(t *testing.T) {
    91  			args := []string{"terravalet", "rename", "--plan", tc.planPath}
    92  
    93  			runFailure(t, args, tc.wantErr)
    94  		})
    95  	}
    96  }
    97  
    98  func TestRunMoveAfterSuccess(t *testing.T) {
    99  	testCases := []struct {
   100  		name       string
   101  		before     string
   102  		after      string
   103  		wantScript string
   104  	}{
   105  		{
   106  			name:       "exact match",
   107  			before:     "testdata/move-after/04-before",
   108  			after:      "testdata/move-after/04-after",
   109  			wantScript: "testdata/move-after/04-want",
   110  		},
   111  	}
   112  
   113  	for _, tc := range testCases {
   114  		t.Run(tc.name, func(t *testing.T) {
   115  			args := []string{"terravalet", "move-after"}
   116  
   117  			runMoveSuccess(t, args, tc.before, tc.after, tc.wantScript)
   118  		})
   119  	}
   120  }
   121  
   122  func TestRunMoveAfterFailure(t *testing.T) {
   123  	testCases := []struct {
   124  		name    string
   125  		before  string // special value: "non-existing"
   126  		after   string // special value: "non-existing"
   127  		wantErr string
   128  	}{
   129  		{
   130  			name:    "non existing before tfplan",
   131  			before:  "non-existing",
   132  			after:   "non-existing",
   133  			wantErr: "opening the terraform BEFORE plan file: open non-existing.tfplan: no such file or directory",
   134  		},
   135  		{
   136  			name:    "non existing after tfplan",
   137  			before:  "testdata/move-after/05-before",
   138  			after:   "non-existing",
   139  			wantErr: "opening the terraform AFTER plan file: open non-existing.tfplan: no such file or directory",
   140  		},
   141  		{
   142  			name:    "before tfplan must only destroy",
   143  			before:  "testdata/move-after/05-before",
   144  			after:   "testdata/move-after/05-after",
   145  			wantErr: "BEFORE plan contains resources to create: [aws_batch_job_definition.foo]",
   146  		},
   147  		{
   148  			name:    "after tfplan must only create",
   149  			before:  "testdata/move-after/06-before",
   150  			after:   "testdata/move-after/06-after",
   151  			wantErr: "AFTER plan contains resources to destroy: [aws_batch_job_definition.foo]",
   152  		},
   153  	}
   154  
   155  	for _, tc := range testCases {
   156  		t.Run(tc.name, func(t *testing.T) {
   157  			args := []string{"terravalet", "move-after"}
   158  
   159  			runMoveFailure(t, args, tc.before, tc.after, tc.wantErr)
   160  		})
   161  	}
   162  }
   163  
   164  func TestRunMoveBeforeSuccess(t *testing.T) {
   165  	testCases := []struct {
   166  		name       string
   167  		before     string
   168  		after      string // special prefix: dummy
   169  		wantScript string
   170  	}{
   171  		{
   172  			name:       "happy path simple",
   173  			before:     "testdata/move-before/01-before",
   174  			after:      "dummy-01-after",
   175  			wantScript: "testdata/move-before/01-want",
   176  		},
   177  	}
   178  
   179  	for _, tc := range testCases {
   180  		t.Run(tc.name, func(t *testing.T) {
   181  			args := []string{"terravalet", "move-before"}
   182  
   183  			runMoveSuccess(t, args, tc.before, tc.after, tc.wantScript)
   184  		})
   185  	}
   186  }
   187  
   188  func TestRunMoveBeforeFailure(t *testing.T) {
   189  	testCases := []struct {
   190  		name    string
   191  		before  string // special value: "non-existing"
   192  		wantErr string
   193  	}{
   194  		{
   195  			name:    "non existing BEFORE plan",
   196  			before:  "non-existing",
   197  			wantErr: "opening the terraform BEFORE plan file: open non-existing.tfplan: no such file or directory",
   198  		},
   199  		{
   200  			name:    "BEFORE plan must not contain resources to destroy",
   201  			before:  "testdata/move-before/02-before",
   202  			wantErr: "BEFORE plan contains resources to destroy: [aws_batch_compute_environment.foo_batch]",
   203  		},
   204  	}
   205  
   206  	for _, tc := range testCases {
   207  		t.Run(tc.name, func(t *testing.T) {
   208  			args := []string{"terravalet", "move-before",
   209  				"--before=" + tc.before, "--after=testdata/move-before/dummy-after",
   210  			}
   211  
   212  			runMoveFailure(t, args, tc.before, "non-existing", tc.wantErr)
   213  		})
   214  	}
   215  }
   216  
   217  // If after has special prefix "dummy", it will not attempt to copy the
   218  // corresponding tfplan files, to accomodate for move-before.
   219  func runMoveSuccess(t *testing.T, args []string, before, after, wantScript string) {
   220  	wantUpPath := wantScript + "_up.sh"
   221  	wantUp, err := os.ReadFile(wantUpPath)
   222  	if err != nil {
   223  		t.Fatalf("reading want up file: %v", err)
   224  	}
   225  
   226  	wantDownPath := wantScript + "_down.sh"
   227  	wantDown, err := os.ReadFile(wantDownPath)
   228  	if err != nil {
   229  		t.Fatalf("reading want down file: %v", err)
   230  	}
   231  
   232  	tmpDir, err := os.MkdirTemp("", "terravalet")
   233  	if err != nil {
   234  		t.Fatalf("creating temporary dir: %v", err)
   235  	}
   236  	defer os.RemoveAll(tmpDir)
   237  
   238  	// Copy the required input files to the tmpdir.
   239  	if err := copyfile(before+".tfplan",
   240  		filepath.Join(tmpDir, path.Base(before)+".tfplan")); err != nil {
   241  		t.Fatal(err)
   242  	}
   243  	if !strings.HasPrefix(after, "dummy") {
   244  		if err := copyfile(after+".tfplan",
   245  			filepath.Join(tmpDir, path.Base(after)+".tfplan")); err != nil {
   246  			t.Fatal(err)
   247  		}
   248  	}
   249  
   250  	// Change directory to the tmpdir.
   251  	cwd, err := os.Getwd()
   252  	if err != nil {
   253  		t.Fatal("getwd:", err)
   254  	}
   255  	if err := os.Chdir(tmpDir); err != nil {
   256  		t.Fatal("chdir:", err)
   257  	}
   258  	defer func() {
   259  		if err := os.Chdir(cwd); err != nil {
   260  			panic(err)
   261  		}
   262  	}()
   263  
   264  	tmpScript := path.Base(wantScript)
   265  	tmpUpPath := tmpScript + "_up.sh"
   266  	tmpDownPath := tmpScript + "_down.sh"
   267  
   268  	args = append(args, "--before="+path.Base(before), "--after="+path.Base(after),
   269  		"--script="+tmpScript)
   270  	os.Args = args
   271  
   272  	if err := run(); err != nil {
   273  		t.Fatalf("run: args: %s\nhave: %q\nwant: no error", args, err)
   274  	}
   275  	t.Log("SUT ran successfully")
   276  
   277  	tmpUp, err := os.ReadFile(tmpUpPath)
   278  	if err != nil {
   279  		t.Fatalf("reading tmp up file: %s", err)
   280  	}
   281  	tmpDown, err := os.ReadFile(tmpDownPath)
   282  	if err != nil {
   283  		t.Fatalf("reading tmp down file: %s", err)
   284  	}
   285  
   286  	if diff := cmp.Diff(string(wantUp), string(tmpUp)); diff != "" {
   287  		t.Errorf("\nup script: mismatch (-want +have):\n"+
   288  			"(want path: %s)\n"+
   289  			"%s", wantUpPath, diff)
   290  	}
   291  	if diff := cmp.Diff(string(wantDown), string(tmpDown)); diff != "" {
   292  		t.Errorf("\ndown script: mismatch (-want +have):\n"+
   293  			"(want path: %s)\n"+
   294  			"%s", wantDownPath, diff)
   295  	}
   296  }
   297  
   298  // If before or after have the special value "non-existing", it will not attempt to copy the
   299  // corresponding tfplan files, allowing to test a missing file.
   300  func runMoveFailure(t *testing.T, args []string, before, after, wantErr string) {
   301  	tmpDir, err := os.MkdirTemp("", "terravalet")
   302  	if err != nil {
   303  		t.Fatalf("creating temporary dir: %v", err)
   304  	}
   305  	defer os.RemoveAll(tmpDir)
   306  
   307  	// Copy the required input files to the tmpdir.
   308  	if before != "non-existing" {
   309  		if err := copyfile(before+".tfplan",
   310  			filepath.Join(tmpDir, path.Base(before)+".tfplan")); err != nil {
   311  			t.Fatal(err)
   312  		}
   313  	}
   314  	if after != "non-existing" {
   315  		if err := copyfile(after+".tfplan",
   316  			filepath.Join(tmpDir, path.Base(after)+".tfplan")); err != nil {
   317  			t.Fatal(err)
   318  		}
   319  	}
   320  
   321  	// Change directory to the tmpdir.
   322  	cwd, err := os.Getwd()
   323  	if err != nil {
   324  		t.Fatal("getwd:", err)
   325  	}
   326  	if err := os.Chdir(tmpDir); err != nil {
   327  		t.Fatal("chdir:", err)
   328  	}
   329  	defer func() {
   330  		if err := os.Chdir(cwd); err != nil {
   331  			panic(err)
   332  		}
   333  	}()
   334  
   335  	args = append(args, "--before="+path.Base(before), "--after="+path.Base(after),
   336  		"--script=dummy-script")
   337  	os.Args = args
   338  
   339  	err = run()
   340  
   341  	if err == nil {
   342  		t.Fatalf("run: args: %s\nhave: no error\nwant: %q", args, wantErr)
   343  	}
   344  	if diff := cmp.Diff(wantErr, err.Error()); diff != "" {
   345  		t.Errorf("error message mismatch (-want +have):\n%s", diff)
   346  	}
   347  }
   348  
   349  // copyfile copies file src to dst. It is not robust for production use (a lot of OS-dependent
   350  // corner cases) but is good enough for tests.
   351  func copyfile(src, dst string) error {
   352  	srcFile, err := os.Open(src)
   353  	if err != nil {
   354  		return fmt.Errorf("opening src file: %s", err)
   355  	}
   356  	defer srcFile.Close()
   357  
   358  	// Create (or truncate) the dst file
   359  	dstFile, err := os.Create(dst)
   360  	if err != nil {
   361  		return fmt.Errorf("creating dst file: %s", err)
   362  	}
   363  	defer dstFile.Close()
   364  
   365  	if _, err := io.Copy(dstFile, srcFile); err != nil {
   366  		return err
   367  	}
   368  
   369  	return nil
   370  }