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  }