github.com/StackExchange/blackbox/v2@v2.0.1-0.20220331193400-d84e904973ab/integrationTest/ithelpers.go (about) 1 package main 2 3 import ( 4 "flag" 5 "fmt" 6 "io/ioutil" 7 "log" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "runtime" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/StackExchange/blackbox/v2/pkg/bblog" 17 "github.com/StackExchange/blackbox/v2/pkg/bbutil" 18 "github.com/StackExchange/blackbox/v2/pkg/vcs" 19 _ "github.com/StackExchange/blackbox/v2/pkg/vcs/_all" 20 21 "github.com/andreyvit/diff" 22 ) 23 24 var verbose = flag.Bool("verbose", false, "reveal stderr") 25 var nocleanup = flag.Bool("nocleanup", false, "do not delete the tmp directory") 26 27 type userinfo struct { 28 name string 29 dir string // .gnupg-$name 30 agentInfo string // GPG_AGENT_INFO 31 email string 32 fullname string 33 } 34 35 var users = map[string]*userinfo{} 36 37 func init() { 38 testing.Init() 39 flag.Parse() 40 } 41 42 var logErr *log.Logger 43 var logDebug *log.Logger 44 45 func init() { 46 logErr = bblog.GetErr() 47 logDebug = bblog.GetDebug(*verbose) 48 } 49 50 func getVcs(t *testing.T, name string) vcs.Vcs { 51 t.Helper() 52 // Set up the vcs 53 for _, v := range vcs.Catalog { 54 logDebug.Printf("Testing vcs: %v == %v", name, v.Name) 55 if strings.ToLower(v.Name) == strings.ToLower(name) { 56 h, err := v.New() 57 if err != nil { 58 return nil // No idea how that would happen. 59 } 60 return h 61 } 62 logDebug.Println("...Nope.") 63 64 } 65 return nil 66 } 67 68 // TestBasicCommands's helpers 69 70 func makeHomeDir(t *testing.T, testname string) { 71 t.Helper() 72 var homedir string 73 var err error 74 75 if *nocleanup { 76 // Make a predictable location; don't deleted. 77 homedir = "/tmp/bbhome-" + testname 78 os.RemoveAll(homedir) 79 err = os.Mkdir(homedir, 0770) 80 if err != nil { 81 t.Fatal(fmt.Errorf("mk-home %q: %v", homedir, err)) 82 } 83 } else { 84 // Make a random location that is deleted automatically 85 homedir, err = ioutil.TempDir("", filepath.Join("bbhome-"+testname)) 86 defer os.RemoveAll(homedir) // clean up 87 if err != nil { 88 t.Fatal(err) 89 } 90 } 91 92 err = os.Setenv("HOME", homedir) 93 if err != nil { 94 t.Fatal(err) 95 } 96 logDebug.Printf("TESTING DIR HOME: cd %v\n", homedir) 97 98 repodir := filepath.Join(homedir, "repo") 99 err = os.Mkdir(repodir, 0770) 100 if err != nil { 101 t.Fatal(fmt.Errorf("mk-repo %q: %v", repodir, err)) 102 } 103 err = os.Chdir(repodir) 104 if err != nil { 105 t.Fatal(err) 106 } 107 } 108 109 func createDummyFilesAdmin(t *testing.T) { 110 // This creates a repo with real data, except any .gpg file 111 // is just garbage. 112 addLineSorted(t, ".blackbox/blackbox-admins.txt", "user1@example.com") 113 addLineSorted(t, ".blackbox/blackbox-admins.txt", "user2@example.com") 114 addLineSorted(t, ".blackbox/blackbox-files.txt", "foo.txt") 115 addLineSorted(t, ".blackbox/blackbox-files.txt", "bar.txt") 116 makeFile(t, "foo.txt", "I am the foo.txt file!") 117 makeFile(t, "bar.txt", "I am the foo.txt file!") 118 makeFile(t, "foo.txt.gpg", "V nz gur sbb.gkg svyr!") 119 makeFile(t, "bar.txt.gpg", "V nz gur one.gkg svyr!") 120 } 121 122 func createFilesStatus(t *testing.T) { 123 // This creates a few files with real plaintext but fake cyphertext. 124 // There are a variety of timestamps to enable many statuses. 125 t.Helper() 126 127 // DECRYPTED: File is decrypted and ready to edit (unknown if it has been edited). 128 // ENCRYPTED: GPG file is newer than plaintext. Indicates recented edited then encrypted. 129 // SHREDDED: Plaintext is missing. 130 // GPGMISSING: The .gpg file is missing. Oops? 131 // PLAINERROR: Can't access the plaintext file to determine status. 132 // GPGERROR: Can't access .gpg file to determine status. 133 134 addLineSorted(t, ".blackbox/blackbox-files.txt", "status-DECRYPTED.txt") 135 addLineSorted(t, ".blackbox/blackbox-files.txt", "status-ENCRYPTED.txt") 136 addLineSorted(t, ".blackbox/blackbox-files.txt", "status-SHREDDED.txt") 137 addLineSorted(t, ".blackbox/blackbox-files.txt", "status-GPGMISSING.txt") 138 // addLineSorted(t, ".blackbox/blackbox-files.txt", "status-PLAINERROR.txt") 139 // addLineSorted(t, ".blackbox/blackbox-files.txt", "status-GPGERROR.txt") 140 addLineSorted(t, ".blackbox/blackbox-files.txt", "status-BOTHMISSING.txt") 141 142 // Combination of age difference either missing, file error, both missing. 143 makeFile(t, "status-DECRYPTED.txt", "File with DECRYPTED in it.") 144 makeFile(t, "status-DECRYPTED.txt.gpg", "Svyr jvgu QRPELCGRQ va vg.") 145 146 makeFile(t, "status-ENCRYPTED.txt", "File with ENCRYPTED in it.") 147 makeFile(t, "status-ENCRYPTED.txt.gpg", "Svyr jvgu RAPELCGRQ va vg.") 148 149 // Plaintext intentionally missing. 150 makeFile(t, "status-SHREDDED.txt.gpg", "Svyr jvgu FUERQQRQ va vg.") 151 152 makeFile(t, "status-GPGMISSING.txt", "File with GPGMISSING in it.") 153 // gpg file intentionally missing. 154 155 // Plaintext intentionally missing. ("status-BOTHMISSING.txt") 156 // gpg file intentionally missing. ("status-BOTHMISSING.txt.gpg") 157 158 // NB(tlim): commented out. I can't think of an error I can reproduce. 159 // makeFile(t, "status-PLAINERROR.txt", "File with PLAINERROR in it.") 160 // makeFile(t, "status-PLAINERROR.txt.gpg", "Svyr jvgu CYNVAREEBE va vg.") 161 // setFilePerms(t, "status-PLAINERROR.txt", 0000) 162 163 // NB(tlim): commented out. I can't think of an error I can reproduce. 164 // makeFile(t, "status-GPGERROR.txt", "File with GPGERROR in it.") 165 // makeFile(t, "status-GPGERROR.txt.gpg", "Svyr jvgu TCTREEBE va vg.") 166 // setFilePerms(t, "status-GPGERROR.txt.gpg", 0000) 167 168 time.Sleep(200 * time.Millisecond) 169 170 if err := bbutil.Touch("status-DECRYPTED.txt"); err != nil { 171 t.Fatal(err) 172 } 173 if err := bbutil.Touch("status-ENCRYPTED.txt.gpg"); err != nil { 174 t.Fatal(err) 175 } 176 } 177 178 func addLineSorted(t *testing.T, filename, line string) { 179 err := bbutil.AddLinesToSortedFile(filename, line) 180 if err != nil { 181 t.Fatalf("addLineSorted failed: %v", err) 182 } 183 } 184 185 func removeFile(t *testing.T, name string) { 186 os.RemoveAll(name) 187 } 188 189 func makeFile(t *testing.T, name string, content string) { 190 t.Helper() 191 192 err := ioutil.WriteFile(name, []byte(content), 0666) 193 if err != nil { 194 t.Fatalf("makeFile can't create %q: %v", name, err) 195 } 196 } 197 198 func setFilePerms(t *testing.T, name string, perms int) { 199 t.Helper() 200 201 err := os.Chmod(name, os.FileMode(perms)) 202 if err != nil { 203 t.Fatalf("setFilePerms can't chmod %q: %v", name, err) 204 } 205 } 206 207 var originPath string // CWD when program started. 208 209 // checkOutput runs blackbox with args, the last arg is the filename 210 // of the expected output. Error if output is not expected. 211 func checkOutput(name string, t *testing.T, args ...string) { 212 t.Helper() 213 214 cmd := exec.Command(PathToBlackBox(), args...) 215 cmd.Stdin = nil 216 cmd.Stdout = nil 217 cmd.Stderr = os.Stderr 218 var gb []byte 219 gb, err := cmd.Output() 220 if err != nil { 221 t.Fatal(fmt.Errorf("checkOutput(%q): %w", args, err)) 222 } 223 got := string(gb) 224 225 wb, err := ioutil.ReadFile(filepath.Join(originPath, "test_data", name)) 226 if err != nil { 227 t.Fatalf("checkOutput can't read %v: %v", name, err) 228 } 229 want := string(wb) 230 231 //fmt.Printf("CHECKOUTPUT g: %v\n", got) 232 //fmt.Printf("CHECKOUTPUT w: %v\n", want) 233 234 if g, w := got, want; g != w { 235 t.Errorf("checkOutput(%q) mismatch (-got +want):\n%s", 236 args, diff.LineDiff(g, w)) 237 } 238 239 } 240 241 func invalidArgs(t *testing.T, args ...string) { 242 t.Helper() 243 244 logDebug.Printf("invalidArgs(%q): \n", args) 245 cmd := exec.Command(PathToBlackBox(), args...) 246 cmd.Stdin = nil 247 if *verbose { 248 cmd.Stdout = os.Stdout 249 cmd.Stderr = os.Stderr 250 } 251 err := cmd.Run() 252 if err == nil { 253 logDebug.Println("BAD") 254 t.Fatal(fmt.Errorf("invalidArgs(%q): wanted failure but got success", args)) 255 } 256 logDebug.Printf("^^^^ (correct error received): err=%q\n", err) 257 } 258 259 // TestAliceAndBob's helpers. 260 261 func setupUser(t *testing.T, user, passphrase string) { 262 t.Helper() 263 logDebug.Printf("DEBUG: setupUser %q %q\n", user, passphrase) 264 } 265 266 var pathToBlackBox string 267 268 // PathToBlackBox returns the path to the executable we compile for integration testing. 269 func PathToBlackBox() string { return pathToBlackBox } 270 271 // SetPathToBlackBox sets the path. 272 func SetPathToBlackBox(n string) { 273 logDebug.Printf("PathToBlackBox=%q\n", n) 274 pathToBlackBox = n 275 } 276 277 func runBB(t *testing.T, args ...string) { 278 t.Helper() 279 280 logDebug.Printf("runBB(%q)\n", args) 281 cmd := exec.Command(PathToBlackBox(), args...) 282 cmd.Stdin = nil 283 cmd.Stdout = os.Stdout 284 cmd.Stderr = os.Stderr 285 err := cmd.Run() 286 if err != nil { 287 t.Fatal(fmt.Errorf("runBB(%q): %w", args, err)) 288 } 289 } 290 291 func phase(msg string) { 292 logDebug.Println("********************") 293 logDebug.Println("********************") 294 logDebug.Printf("********* %v\n", msg) 295 logDebug.Println("********************") 296 logDebug.Println("********************") 297 } 298 299 func makeAdmin(t *testing.T, name, fullname, email string) string { 300 testing.Init() 301 302 dir, err := filepath.Abs(filepath.Join(os.Getenv("HOME"), ".gnupg-"+name)) 303 if err != nil { 304 t.Fatal(err) 305 } 306 os.Mkdir(dir, 0700) 307 308 u := &userinfo{ 309 name: name, 310 dir: dir, 311 fullname: fullname, 312 email: email, 313 } 314 users[name] = u 315 316 // GNUPGHOME=u.dir 317 // echo 'pinentry-program' "$(which pinentry-tty)" >> "$GNUPGHOME/gpg-agent.conf" 318 os.Setenv("GNUPGHOME", u.dir) 319 if runtime.GOOS != "darwin" { 320 ai, err := bbutil.RunBashOutput("gpg-agent", "--homedir", u.dir, "--daemon") 321 // NB(tlim): It should return something like: 322 // `GPG_AGENT_INFO=/home/tlimoncelli/.gnupg/S.gpg-agent:18548:1; export GPG_AGENT_INFO;` 323 if err != nil { 324 //t.Fatal(err) 325 } 326 if !strings.HasPrefix(ai, "GPG_AGENT_INFO=") { 327 fmt.Println("WARNING: gpg-agent didn't output what we expected. Assumed dead.") 328 } else { 329 u.agentInfo = ai[15:strings.Index(ai, ";")] 330 os.Setenv("GPG_AGENT_INFO", u.agentInfo) 331 fmt.Printf("GPG_AGENT_INFO=%q (was %q)\n", ai, u.agentInfo) 332 } 333 } 334 335 os.Setenv("GNUPGHOME", u.dir) 336 // Generate key: 337 if hasQuick(t) { 338 fmt.Println("DISCOVERED: NEW GPG") 339 fmt.Printf("Generating %q using --qgk\n", u.email) 340 bbutil.RunBash("gpg", 341 "--homedir", u.dir, 342 "--batch", 343 "--passphrase", "", 344 "--quick-generate-key", u.email, 345 ) 346 if err != nil { 347 t.Fatal(err) 348 } 349 350 } else { 351 352 fmt.Println("DISCOVERED: OLD GPG") 353 fmt.Println("MAKING KEY") 354 355 tmpfile, err := ioutil.TempFile("", "example") 356 if err != nil { 357 log.Fatal(err) 358 } 359 defer os.Remove(tmpfile.Name()) // clean up 360 361 batch := `%echo Generating a basic OpenPGP key 362 Key-Type: RSA 363 Key-Length: 2048 364 Subkey-Type: RSA 365 Subkey-Length: 2048 366 Name-Real: ` + u.fullname + ` 367 Name-Comment: Not for actual use 368 Name-Email: ` + u.email + ` 369 Expire-Date: 0 370 %pubring ` + filepath.Join(u.dir, `pubring.gpg`) + ` 371 %secring ` + filepath.Join(u.dir, `secring.gpg`) + ` 372 # Do a commit here, so that we can later print "done" 373 %commit 374 %echo done` 375 //fmt.Printf("BATCH START\n%s\nBATCH END\n", batch) 376 fmt.Fprintln(tmpfile, batch) 377 378 // FIXME(tlim): The batch file should include a password, but then 379 // we need to figure out how to get "blackbox encrypt" and other 380 // commands to input a password in an automated way. 381 // To experiment with this, add after "Expire-Date:" a line like: 382 // Passphrase: kljfhslfjkhsaljkhsdflgjkhsd 383 // Current status: without that line GPG keys have no passphrase 384 // and none is requested. 385 386 bbutil.RunBash("gpg", 387 "--homedir", u.dir, 388 "--verbose", 389 "--batch", 390 "--gen-key", 391 tmpfile.Name(), 392 ) 393 if err != nil { 394 t.Fatal(err) 395 } 396 if err := tmpfile.Close(); err != nil { 397 log.Fatal(err) 398 } 399 400 // We do this just to for gpg to create trustdb.gpg 401 bbutil.RunBash("gpg", 402 "--homedir", u.dir, 403 "--list-keys", 404 ) 405 if err != nil { 406 t.Fatal(err) 407 } 408 409 bbutil.RunBash("gpg", 410 "--homedir", u.dir, 411 "--list-secret-keys", 412 ) 413 if err != nil { 414 t.Fatal(err) 415 } 416 417 } 418 419 return u.dir 420 } 421 422 func hasQuick(t *testing.T) bool { 423 testing.Init() 424 fmt.Println("========== Do we have --quick-generate-key?") 425 err := bbutil.RunBash("gpg2", 426 "--dry-run", 427 "--quick-generate-key", 428 "--batch", 429 "--passphrase", "", 430 "foo", "rsa", "encr") 431 fmt.Println("========== Done") 432 if err == nil { 433 return true 434 } 435 //fmt.Printf("DISCOVER GPG: %d", err.ExitCode()) 436 if exitError, ok := err.(*exec.ExitError); ok { 437 if exitError.ExitCode() == 0 { 438 return true 439 } 440 } 441 return false 442 } 443 444 func become(t *testing.T, name string) { 445 testing.Init() 446 u := users[name] 447 448 os.Setenv("GNUPGHOME", u.dir) 449 os.Setenv("GPG_AGENT_INFO", u.agentInfo) 450 bbutil.RunBash("git", "config", "user.name", u.name) 451 bbutil.RunBash("git", "config", "user.email", u.fullname) 452 } 453 454 // // Get fingerprint: 455 // // Retrieve fingerprint of generated key. 456 // // Use it to extract the secret/public keys. 457 // // (stolen from https://raymii.org/s/articles/GPG_noninteractive_batch_sign_trust_and_send_gnupg_keys.html) 458 // 459 // // fpr=`gpg --homedir /tmp/blackbox_createrole --fingerprint --with-colons "$ROLE_NAME" | awk -F: '/fpr:/ {print $10}' | head -n 1` 460 // var fpr string 461 // bbutil.RunBashOutput("gpg", 462 // "--homedir", "/tmp/blackbox_createrole", 463 // "--fingerprint", 464 // "--with-colons", 465 // u.email, 466 // ) 467 // for i, l := range string.Split(out, "\n") { 468 // if string.HasPrefix(l, "fpr:") { 469 // fpr = strings.Split(l, ":")[9] 470 // } 471 // break 472 // } 473 // 474 // // Create key key: 475 // // gpg --homedir "$gpghomedir" --batch --passphrase '' --quick-add-key "$fpr" rsa encr 476 // bbutil.RunBash("gpg", 477 // "--homedir", u.dir, 478 // "--batch", 479 // "--passphrase", "", 480 // "--quick-add-key", fpr, 481 // "rsa", "encr", 482 // ) 483 484 // function md5sum_file() { 485 // # Portably generate the MD5 hash of file $1. 486 // case $(uname -s) in 487 // Darwin | FreeBSD ) 488 // md5 -r "$1" | awk '{ print $1 }' 489 // ;; 490 // NetBSD ) 491 // md5 -q "$1" 492 // ;; 493 // SunOS ) 494 // digest -a md5 "$1" 495 // ;; 496 // Linux ) 497 // md5sum "$1" | awk '{ print $1 }' 498 // ;; 499 // CYGWIN* ) 500 // md5sum "$1" | awk '{ print $1 }' 501 // ;; 502 // * ) 503 // echo 'ERROR: Unknown OS. Exiting.' 504 // exit 1 505 // ;; 506 // esac 507 // } 508 // 509 // function assert_file_missing() { 510 // if [[ -e "$1" ]]; then 511 // echo "ASSERT FAILED: ${1} should not exist." 512 // exit 1 513 // fi 514 // } 515 // 516 // function assert_file_exists() { 517 // if [[ ! -e "$1" ]]; then 518 // echo "ASSERT FAILED: ${1} should exist." 519 // echo "PWD=$(/usr/bin/env pwd -P)" 520 // #echo "LS START" 521 // #ls -la 522 // #echo "LS END" 523 // exit 1 524 // fi 525 // } 526 // function assert_file_md5hash() { 527 // local file="$1" 528 // local wanted="$2" 529 // assert_file_exists "$file" 530 // local found 531 // found=$(md5sum_file "$file") 532 // if [[ "$wanted" != "$found" ]]; then 533 // echo "ASSERT FAILED: $file hash wanted=$wanted found=$found" 534 // exit 1 535 // fi 536 // } 537 // function assert_file_group() { 538 // local file="$1" 539 // local wanted="$2" 540 // local found 541 // assert_file_exists "$file" 542 // 543 // case $(uname -s) in 544 // Darwin | FreeBSD | NetBSD ) 545 // found=$(stat -f '%Dg' "$file") 546 // ;; 547 // Linux | SunOS ) 548 // found=$(stat -c '%g' "$file") 549 // ;; 550 // CYGWIN* ) 551 // echo "ASSERT_FILE_GROUP: Running on Cygwin. Not being tested." 552 // return 0 553 // ;; 554 // * ) 555 // echo 'ERROR: Unknown OS. Exiting.' 556 // exit 1 557 // ;; 558 // esac 559 // 560 // echo "DEBUG: assert_file_group X${wanted}X vs. X${found}X" 561 // echo "DEBUG:" $(which stat) 562 // if [[ "$wanted" != "$found" ]]; then 563 // echo "ASSERT FAILED: $file chgrp group wanted=$wanted found=$found" 564 // exit 1 565 // fi 566 // } 567 // function assert_file_perm() { 568 // local wanted="$1" 569 // local file="$2" 570 // local found 571 // assert_file_exists "$file" 572 // 573 // case $(uname -s) in 574 // Darwin | FreeBSD | NetBSD ) 575 // found=$(stat -f '%Sp' "$file") 576 // ;; 577 // # NB(tlim): CYGWIN hasn't been tested. It might be more like Darwin. 578 // Linux | CYGWIN* | SunOS ) 579 // found=$(stat -c '%A' "$file") 580 // ;; 581 // * ) 582 // echo 'ERROR: Unknown OS. Exiting.' 583 // exit 1 584 // ;; 585 // esac 586 // 587 // echo "DEBUG: assert_file_perm X${wanted}X vs. X${found}X" 588 // echo "DEBUG:" $(which stat) 589 // if [[ "$wanted" != "$found" ]]; then 590 // echo "ASSERT FAILED: $file chgrp perm wanted=$wanted found=$found" 591 // exit 1 592 // fi 593 // } 594 // function assert_line_not_exists() { 595 // local target="$1" 596 // local file="$2" 597 // assert_file_exists "$file" 598 // if grep -F -x -s -q >/dev/null "$target" "$file" ; then 599 // echo "ASSERT FAILED: line '$target' should not exist in file $file" 600 // echo "==== file contents: START $file" 601 // cat "$file" 602 // echo "==== file contents: END $file" 603 // exit 1 604 // fi 605 // } 606 // function assert_line_exists() { 607 // local target="$1" 608 // local file="$2" 609 // assert_file_exists "$file" 610 // if ! grep -F -x -s -q >/dev/null "$target" "$file" ; then 611 // echo "ASSERT FAILED: line '$target' should exist in file $file" 612 // echo "==== file contents: START $file" 613 // cat "$file" 614 // echo "==== file contents: END $file" 615 // exit 1 616 // fi 617 // }