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 }