github.com/DelineaXPM/dsv-cli@v1.40.6/tests/e2e/e2e_test.go (about) 1 //go:build endtoend 2 // +build endtoend 3 4 package e2e 5 6 import ( 7 "fmt" 8 "log" 9 "os" 10 "os/exec" 11 "path" 12 "path/filepath" 13 "runtime" 14 "strings" 15 "testing" 16 "time" 17 18 "github.com/AlecAivazis/survey/v2/terminal" 19 expect "github.com/Netflix/go-expect" 20 pseudotty "github.com/creack/pty" 21 "github.com/hinshun/vt10x" 22 ) 23 24 const info = ` 25 ______ ___ ______ 26 | ____|__ \| ____| Go version: %s 27 | |__ ) | |__ OS/Arch: %s/%s 28 | __| / /| __| Working dir: %s 29 | |____ / /_| |____ Time: %s 30 |______|____|______| 31 ` 32 33 const help = ` 34 =================================================================================================== 35 DevOps Secrets Vault CLI End-to-End testing requires a tenant and an access to it. 36 37 A template with all required data is defined in the <project-root>/tests/e2e/.env.example file. 38 39 How to run E2E tests: 40 - from project root directory create a copy of the example file: 41 $ cp ./tests/e2e/.env.example ./tests/e2e/.env 42 - fill in all variables in the created .env file 43 - execute it: 44 $ source ./tests/e2e/.env 45 - run E2E tests: 46 $ make e2e-test 47 =================================================================================================== 48 49 %s 50 51 ` 52 53 const ( 54 domainEnvName = "DSV_CLI_E2E_DOMAIN" 55 tenantEnvName = "DSV_CLI_E2E_TENANT" 56 usernameEnvName = "DSV_CLI_E2E_USERNAME" 57 passwordEnvName = "DSV_CLI_E2E_PASSWORD" 58 certificateEnvName = "DSV_CLI_E2E_CERTIFICATE" 59 privateKeyEnvName = "DSV_CLI_E2E_PRIVKEY" 60 ) 61 62 // Vars managed by TestMain function. 63 var ( 64 binPath = "" 65 tmpDirPath = "" 66 cliConfigPath = "" 67 cliConfigProfile = "" 68 targetArtifactDirectory = "" 69 ) 70 71 func TestMain(m *testing.M) { 72 workDir, err := os.Getwd() 73 74 targetArtifactDirectory = filepath.Join("..", "..", ".artifacts", "coverage", "e2e") 75 if _, err := os.Stat(targetArtifactDirectory); os.IsNotExist(err) { 76 err = os.MkdirAll(targetArtifactDirectory, 0o755) 77 } 78 if err != nil { 79 fmt.Fprintf(os.Stderr, "[TestMain] Unable to determine working directory: %v.\n", err) 80 os.Exit(1) 81 } 82 fmt.Fprintf(os.Stderr, info, 83 runtime.Version(), runtime.GOOS, runtime.GOARCH, 84 workDir, time.Now().Format(time.ANSIC), 85 ) 86 87 if os.Getenv(domainEnvName) == "" { 88 fmt.Fprintf(os.Stderr, help, "Error: domain is not set.") 89 os.Exit(1) 90 } 91 if os.Getenv(tenantEnvName) == "" { 92 fmt.Fprintf(os.Stderr, help, "Error: tenant is not set.") 93 os.Exit(1) 94 } 95 if os.Getenv(usernameEnvName) == "" { 96 fmt.Fprintf(os.Stderr, help, "Error: username is not set.") 97 os.Exit(1) 98 } 99 if os.Getenv(passwordEnvName) == "" { 100 fmt.Fprintf(os.Stderr, help, "Error: password is not set.") 101 os.Exit(1) 102 } 103 104 // Binary. 105 fmt.Fprintln(os.Stderr, "[TestMain] Compiling test binary.") 106 // compile (-c) the test binary with coverage enabled (-covermode=count) 107 cmd := exec.Command("go", "test", "-c", "-covermode=count", "-coverpkg=./...", "-o=./tests/e2e/dsv.test") 108 cmd.Env = append(os.Environ(), "CGO_ENABLED=0") 109 cmd.Dir = filepath.Join(workDir, "..", "..") 110 if err := cmd.Run(); err != nil { 111 fmt.Fprintf(os.Stderr, "[TestMain] Error: Failed to compile test binary: %v.\n", err) 112 os.Exit(1) 113 } 114 binPath = "./dsv.test" 115 116 // Temp directory. 117 fmt.Fprintln(os.Stderr, "[TestMain] Creating directory for temporary files.") 118 tDir, err := os.MkdirTemp("", "cli-config-*") 119 if err != nil { 120 fmt.Fprintf(os.Stderr, "[TestMain] Error: Failed to create temp directory: %v.\n", err) 121 os.Exit(1) 122 } 123 tmpDirPath = tDir 124 fmt.Fprintf(os.Stderr, "[TestMain] Temp directory path: %s.\n", tmpDirPath) 125 126 // Coverage directory. 127 fmt.Fprintf(os.Stderr, "[TestMain] Coverage directory path: %s.\n", targetArtifactDirectory) 128 fmt.Fprintln(os.Stderr, "[TestMain] Removing directory with old coverage data.") 129 err = os.RemoveAll(targetArtifactDirectory) 130 if err != nil { 131 fmt.Fprintf(os.Stderr, "[TestMain] Error: Failed to delete directory with old coverage data: %v.\n", err) 132 os.Exit(1) 133 } 134 fmt.Fprintln(os.Stderr, "[TestMain] Creating directory where new coverage data will be collected.") 135 err = os.Mkdir(targetArtifactDirectory, 0o755) 136 if err != nil { 137 fmt.Fprintf(os.Stderr, "[TestMain] Error: Failed to create directory for coverage data: %v.\n", err) 138 os.Exit(1) 139 } 140 141 // Prepare CLI profile for tests. 142 e := newEnv() 143 cliConfigPath = path.Join(tmpDirPath, ".dsv.yml") 144 cliConfigProfile = "e2e-profile" 145 fmt.Fprintf(os.Stderr, "[TestMain] Creating a new profile '%s' in '%s' config file.\n", cliConfigProfile, cliConfigPath) 146 147 args := []string{ 148 "init", 149 fmt.Sprintf("--config=%s", cliConfigPath), 150 fmt.Sprintf("--profile=%s", cliConfigProfile), 151 fmt.Sprintf("--tenant=%s", e.tenant), 152 fmt.Sprintf("--domain=%s", e.domain), 153 "--store-type=file", 154 fmt.Sprintf("--store-path=%s", tmpDirPath), 155 "--cache-strategy=server", 156 "--auth-type=password", 157 fmt.Sprintf("--auth-username=%s", e.username), 158 fmt.Sprintf("--auth-password=%s", e.password), 159 } 160 cmd = exec.Command(binPath, args...) 161 cmd.Env = append(os.Environ(), "IS_SYSTEM_TEST=true") 162 output, err := cmd.CombinedOutput() 163 if err != nil { 164 fmt.Fprintf(os.Stderr, "[TestMain] Error: Failed to prepare CLI config profile: %v.\n\t%s\n", err, output) 165 os.Exit(1) 166 } 167 if strings.Contains(string(output), "Failed to authenticate") { 168 fmt.Fprintf(os.Stderr, "[TestMain] Error: Failed to prepare CLI config profile: failed to authenticate.\n") 169 os.Exit(1) 170 } 171 172 // Run tests. 173 var code int 174 err = resilienceBefore() 175 if err != nil { 176 fmt.Fprintf(os.Stderr, "[TestMain] resilienceBefore() failed: %v.\n", err) 177 code = 1 178 } else { 179 code = m.Run() 180 err = resilienceAfter() 181 if err != nil { 182 fmt.Fprintf(os.Stderr, "[TestMain] resilienceAfter() failed: %v.\n", err) 183 } 184 } 185 186 // Clean up. 187 fmt.Fprintln(os.Stderr, "[TestMain] Removing test binary.") 188 err = os.Remove(binPath) 189 if err != nil { 190 fmt.Fprintf(os.Stderr, "[TestMain] Error: Failed to delete test binary: %v.\n", err) 191 } 192 fmt.Fprintf(os.Stderr, "[TestMain] Removing temp directory at path %s.\n", tmpDirPath) 193 err = os.RemoveAll(tmpDirPath) 194 if err != nil { 195 fmt.Fprintf(os.Stderr, "[TestMain] Error: Failed to delete temp directory: %v.\n", err) 196 } 197 198 os.Exit(code) 199 } 200 201 type environment struct { 202 domain string 203 tenant string 204 username string 205 password string 206 certificate string 207 privateKey string 208 tmpDirPath string 209 } 210 211 func newEnv() *environment { 212 e := &environment{ 213 domain: os.Getenv(domainEnvName), 214 tenant: os.Getenv(tenantEnvName), 215 username: os.Getenv(usernameEnvName), 216 password: os.Getenv(passwordEnvName), 217 certificate: os.Getenv(certificateEnvName), 218 privateKey: os.Getenv(privateKeyEnvName), 219 220 tmpDirPath: tmpDirPath, 221 } 222 223 return e 224 } 225 226 type console interface { 227 ExpectString(string) 228 ExpectEOF() 229 SendLine(string) 230 Send(string) 231 SendKeyEnter() 232 SendKeyArrowDown() 233 } 234 235 type consoleWithErrorHandling struct { 236 console *expect.Console 237 t *testing.T 238 } 239 240 func (c *consoleWithErrorHandling) ExpectString(s string) { 241 c.t.Helper() 242 c.t.Logf("Expecting %q", s) 243 if buf, err := c.console.ExpectString(s); err != nil { 244 c.t.Logf("ExpectString(%q) buffer:\n%s", s, buf) 245 c.t.Fatalf("ExpectString(%q) = %v", s, err) 246 } 247 } 248 249 func (c *consoleWithErrorHandling) ExpectEOF() { 250 if buf, err := c.console.ExpectEOF(); err != nil { 251 c.t.Helper() 252 if strings.Contains(err.Error(), "use of closed file") { 253 return // Ignore. 254 } 255 c.t.Logf("ExpectEOF() buffer:\n%s", buf) 256 c.t.Fatalf("ExpectEOF() = %v", err) 257 } 258 } 259 260 func (c *consoleWithErrorHandling) SendLine(s string) { 261 if _, err := c.console.SendLine(s); err != nil { 262 c.t.Helper() 263 c.t.Fatalf("SendLine(%q) = %v", s, err) 264 } 265 } 266 267 func (c *consoleWithErrorHandling) Send(s string) { 268 if _, err := c.console.Send(s); err != nil { 269 c.t.Helper() 270 c.t.Fatalf("Send(%q) = %v", s, err) 271 } 272 } 273 274 func (c *consoleWithErrorHandling) SendKeyEnter() { 275 if _, err := c.console.Send(string(terminal.KeyEnter)); err != nil { 276 c.t.Helper() 277 c.t.Fatalf("Send(terminal.KeyEnter) = %v", err) 278 } 279 } 280 281 func (c *consoleWithErrorHandling) SendKeyArrowDown() { 282 if _, err := c.console.Send(string(terminal.KeyArrowDown)); err != nil { 283 c.t.Helper() 284 c.t.Fatalf("Send(terminal.KeyArrowDown) = %v", err) 285 } 286 } 287 288 func prepCmd(t *testing.T, args []string) *exec.Cmd { 289 var covName string 290 if len(args) > 1 && !strings.HasPrefix(args[1], "-") { 291 covName = fmt.Sprintf("%s-%s-%d.out", args[0], args[1], time.Now().UnixNano()) 292 } else { 293 covName = fmt.Sprintf("%s-%d.out", args[0], time.Now().UnixNano()) 294 } 295 covPath := filepath.Join(targetArtifactDirectory, covName) 296 t.Logf("Coverage report: %s", covPath) 297 binArgs := append( 298 []string{ 299 fmt.Sprintf("-test.coverprofile=%s", covPath), 300 "-test.timeout=2m", 301 }, 302 args..., 303 ) 304 cmd := exec.Command(binPath, binArgs...) 305 cmd.Env = append(os.Environ(), "IS_SYSTEM_TEST=true") 306 return cmd 307 } 308 309 func run(t *testing.T, command []string) string { 310 t.Helper() 311 cmd := prepCmd(t, command) 312 output, err := cmd.CombinedOutput() 313 if err != nil { 314 log.Fatalf("cmd.CombinedOutput() = %v", err) 315 } 316 317 out := string(output) 318 319 // Remove lines like this added at the end of the output: 320 // > PASS 321 // > coverage: 6.8% of statements in ./... 322 // > 323 return out[:strings.Index(out, `PASS`)] 324 } 325 326 func runWithProfile(t *testing.T, command string) string { 327 t.Helper() 328 args := strings.Split(command, " ") 329 args = append(args, 330 fmt.Sprintf("--config=%s", cliConfigPath), 331 fmt.Sprintf("--profile=%s", cliConfigProfile), 332 ) 333 return run(t, args) 334 } 335 336 func runFlow(t *testing.T, command []string, flow func(c console)) { 337 t.Helper() 338 339 pty, tty, err := pseudotty.Open() 340 if err != nil { 341 t.Fatalf("failed to open pseudotty: %v", err) 342 } 343 344 term := vt10x.New(vt10x.WithWriter(tty)) 345 c, err := expect.NewConsole( 346 expect.WithStdin(pty), 347 expect.WithStdout(term), 348 expect.WithCloser(pty, tty), 349 // 15 seconds should be enough even for high API response time. 350 expect.WithDefaultTimeout(15*time.Second), 351 ) 352 if err != nil { 353 t.Fatalf("failed to create console: %v", err) 354 } 355 defer c.Close() 356 357 cmd := prepCmd(t, command) 358 cmd.Stdin = c.Tty() 359 cmd.Stdout = c.Tty() 360 cmd.Stderr = c.Tty() 361 362 go func() { 363 flow(&consoleWithErrorHandling{console: c, t: t}) 364 }() 365 366 err = cmd.Start() 367 if err != nil { 368 log.Fatalf("cmd.Start() = %v", err) 369 } 370 371 err = cmd.Wait() 372 if err != nil { 373 log.Fatalf("cmd.Wait() = %v", err) 374 } 375 } 376 377 func createFile(t *testing.T, path string) { 378 t.Helper() 379 f, err := os.Create(path) 380 if err != nil { 381 t.Fatalf("os.Create(%q) = %v", path, err) 382 } 383 f.Close() 384 } 385 386 func writeFile(t *testing.T, data []byte, path string) { 387 t.Helper() 388 err := os.WriteFile(path, data, os.ModePerm) 389 if err != nil { 390 t.Fatalf("os.WriteFile(%q) = %v", path, err) 391 } 392 } 393 394 func readFile(t *testing.T, path string) string { 395 t.Helper() 396 bytes, err := os.ReadFile(path) 397 if err != nil { 398 t.Fatalf("os.ReadFile(%q) = %v", path, err) 399 } 400 return string(bytes) 401 } 402 403 func deleteFile(t *testing.T, path string) { 404 t.Helper() 405 err := os.Remove(path) 406 if err != nil { 407 t.Fatalf("os.Remove(%q) = %v", path, err) 408 } 409 } 410 411 func requireLine(t *testing.T, lines string, line string) { 412 t.Helper() 413 for _, l := range strings.Split(lines, "\n") { 414 if strings.TrimSpace(l) == line { 415 return 416 } 417 } 418 t.Fatalf("Line %q not found in lines:\n%s", line, lines) 419 } 420 421 func requireContains(t *testing.T, s string, substr string) { 422 t.Helper() 423 if !strings.Contains(s, substr) { 424 t.Fatalf("String %q not found in:\n%s", substr, s) 425 } 426 } 427 428 func requireNotContains(t *testing.T, s string, substr string) { 429 t.Helper() 430 if strings.Contains(s, substr) { 431 t.Fatalf("Sould not contain %q in:\n%s", substr, s) 432 } 433 } 434 435 func requireEmpty(t *testing.T, s string) { 436 t.Helper() 437 if s != "" { 438 t.Fatalf("Not empty: %q", s) 439 } 440 }