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 }