github.com/shoshinnikita/budget-manager@v0.7.1-0.20220131195411-8c46ff1c6778/tests/component.go (about)

     1  package tests
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/stretchr/testify/require"
    15  
    16  	"github.com/ShoshinNikita/budget-manager/internal/app"
    17  	"github.com/ShoshinNikita/budget-manager/internal/pkg/errors"
    18  )
    19  
    20  type Component interface {
    21  	GetName() string
    22  	Cleanup() error
    23  }
    24  
    25  type StartComponentFn func(*testing.T, *app.Config) Component
    26  
    27  // StartPostgreSQL starts a fresh PostgreSQL instance in a docker container.
    28  // It updates PostgreSQL config with a chosen port
    29  func StartPostgreSQL(t *testing.T, cfg *app.Config) Component {
    30  	require := require.New(t)
    31  
    32  	port := getFreePort(t)
    33  	cfg.DB.Postgres.Port = port
    34  
    35  	t.Logf("use port %d for PostgreSQL container", port)
    36  
    37  	c := &DockerComponent{
    38  		ImageName: "postgres:12-alpine",
    39  		Ports: [][2]int{
    40  			{port, 5432},
    41  		},
    42  		Env: []string{
    43  			"POSTGRES_HOST_AUTH_METHOD=trust",
    44  		},
    45  	}
    46  
    47  	err := c.Run()
    48  	require.NoError(err)
    49  
    50  	return c
    51  }
    52  
    53  // StartSQLite generates a random path and updates SQLite config
    54  func StartSQLite(t *testing.T, cfg *app.Config) Component {
    55  	require := require.New(t)
    56  
    57  	dbPath := func() string {
    58  		dir := os.TempDir()
    59  
    60  		b := make([]byte, 4)
    61  		_, err := rand.Read(b)
    62  		require.NoError(err)
    63  
    64  		filename := "budget-manager-" + hex.EncodeToString(b) + ".db"
    65  
    66  		return filepath.Join(dir, filename)
    67  	}()
    68  
    69  	cfg.DB.SQLite.Path = dbPath
    70  	t.Logf("use path %s for SQLite", dbPath)
    71  
    72  	return &CustomComponent{
    73  		Name: "SQLite (" + dbPath + ")",
    74  		CleanupFn: func() error {
    75  			return os.Remove(dbPath)
    76  		},
    77  	}
    78  }
    79  
    80  type CustomComponent struct {
    81  	Name      string
    82  	CleanupFn func() error
    83  }
    84  
    85  func (c *CustomComponent) GetName() string {
    86  	return c.Name
    87  }
    88  
    89  func (c *CustomComponent) Cleanup() error {
    90  	return c.CleanupFn()
    91  }
    92  
    93  // DockerComponent is a dependency (for example, db) that will be run with Docker
    94  type DockerComponent struct {
    95  	ImageName string
    96  	Ports     [][2]int
    97  	Env       []string
    98  
    99  	containerID string
   100  }
   101  
   102  // Run runs a component as a docker container:
   103  //
   104  //	docker run --rm -d [-e ...] [-p ...] image
   105  //
   106  func (c *DockerComponent) Run() error {
   107  	if err := checkDockerImage(c.ImageName); err != nil {
   108  		return err
   109  	}
   110  
   111  	args := []string{
   112  		"run", "--rm", "-d",
   113  	}
   114  	for _, env := range c.Env {
   115  		args = append(args, "-e", env)
   116  	}
   117  	for _, p := range c.Ports {
   118  		args = append(args, "-p", fmt.Sprintf("%d:%d", p[0], p[1]))
   119  	}
   120  	args = append(args, c.ImageName)
   121  
   122  	cmd := exec.Command("docker", args...)
   123  	output := &bytes.Buffer{}
   124  	cmd.Stdout = output
   125  	cmd.Stderr = output
   126  
   127  	if err := cmd.Run(); err != nil {
   128  		return errors.Wrapf(err, "couldn't run component %q", c.ImageName)
   129  	}
   130  
   131  	c.containerID = output.String()
   132  	c.containerID = strings.TrimSpace(c.containerID)
   133  
   134  	if c.containerID == "" {
   135  		return errors.Errorf("couldn't get container id for component %q", c.ImageName)
   136  	}
   137  	return nil
   138  }
   139  
   140  func (c *DockerComponent) GetName() string {
   141  	name := c.ImageName
   142  	if c.containerID != "" {
   143  		name += " (" + c.containerID + ")"
   144  	}
   145  	return name
   146  }
   147  
   148  // Cleanup stops a docker container
   149  func (c *DockerComponent) Cleanup() error {
   150  	if c.containerID == "" {
   151  		return errors.Errorf("component %q is not run", c.ImageName)
   152  	}
   153  	return exec.Command("docker", "stop", c.containerID).Run() //nolint:gosec
   154  }
   155  
   156  func checkDockerImage(image string) error {
   157  	cmd := exec.Command("docker", "images", "-q", image)
   158  	output := &bytes.Buffer{}
   159  	cmd.Stdout = output
   160  	cmd.Stderr = output
   161  
   162  	if err := cmd.Run(); err != nil {
   163  		return errors.Wrapf(err, "couldn't check image %q", image)
   164  	}
   165  	if strings.TrimSpace(output.String()) == "" {
   166  		return errors.Errorf("no image %q", image)
   167  	}
   168  	return nil
   169  }