github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/internal/db/start/start_test.go (about)

     1  package start
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"io"
     7  	"io/fs"
     8  	"net/http"
     9  	"os"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/Redstoneguy129/cli/internal/testing/apitest"
    14  	"github.com/Redstoneguy129/cli/internal/testing/pgtest"
    15  	"github.com/Redstoneguy129/cli/internal/utils"
    16  	"github.com/Redstoneguy129/cli/internal/utils/parser"
    17  	"github.com/docker/docker/api/types"
    18  	"github.com/jackc/pgerrcode"
    19  	"github.com/spf13/afero"
    20  	"github.com/stretchr/testify/assert"
    21  	"github.com/stretchr/testify/require"
    22  	"gopkg.in/h2non/gock.v1"
    23  )
    24  
    25  type StatErrorFs struct {
    26  	afero.MemMapFs
    27  	DenyPath string
    28  }
    29  
    30  func (m *StatErrorFs) Stat(name string) (fs.FileInfo, error) {
    31  	if strings.HasPrefix(name, m.DenyPath) {
    32  		return nil, fs.ErrPermission
    33  	}
    34  	return m.MemMapFs.Stat(name)
    35  }
    36  
    37  type OpenErrorFs struct {
    38  	afero.MemMapFs
    39  	DenyPath string
    40  }
    41  
    42  func (m *OpenErrorFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
    43  	if strings.HasPrefix(name, m.DenyPath) {
    44  		return nil, fs.ErrPermission
    45  	}
    46  	return m.MemMapFs.OpenFile(name, flag, perm)
    47  }
    48  
    49  func TestInitDatabase(t *testing.T) {
    50  	t.Run("throws error on permission denied", func(t *testing.T) {
    51  		// Setup in-memory fs
    52  		fsys := afero.NewReadOnlyFs(afero.NewMemMapFs())
    53  		// Run test
    54  		err := initDatabase(context.Background(), fsys, io.Discard)
    55  		// Check error
    56  		assert.ErrorContains(t, err, "operation not permitted")
    57  	})
    58  
    59  	t.Run("throws error on stat failure", func(t *testing.T) {
    60  		// Setup in-memory fs
    61  		fsys := &StatErrorFs{DenyPath: utils.CurrBranchPath}
    62  		// Setup mock docker
    63  		// Run test
    64  		err := initDatabase(context.Background(), fsys, io.Discard)
    65  		// Check error
    66  		assert.ErrorContains(t, err, "permission denied")
    67  	})
    68  
    69  	t.Run("throws error on write failure", func(t *testing.T) {
    70  		// Setup in-memory fs
    71  		fsys := &OpenErrorFs{DenyPath: utils.CurrBranchPath}
    72  		// Run test
    73  		err := initDatabase(context.Background(), fsys, io.Discard)
    74  		// Check error
    75  		assert.ErrorContains(t, err, "permission denied")
    76  	})
    77  
    78  	t.Run("throws error on connect failure", func(t *testing.T) {
    79  		utils.Config.Db.Port = 0
    80  		// Setup in-memory fs
    81  		fsys := afero.NewMemMapFs()
    82  		// Run test
    83  		err := initDatabase(context.Background(), fsys, io.Discard)
    84  		// Check error
    85  		assert.ErrorContains(t, err, "invalid port")
    86  	})
    87  
    88  	t.Run("throws error on exec failure", func(t *testing.T) {
    89  		utils.Config.Db.Port = 5432
    90  		// Setup in-memory fs
    91  		fsys := afero.NewMemMapFs()
    92  		// Setup mock postgres
    93  		conn := pgtest.NewConn()
    94  		defer conn.Close(t)
    95  		globals, err := parser.SplitAndTrim(strings.NewReader(utils.GlobalsSql))
    96  		require.NoError(t, err)
    97  		for _, line := range globals {
    98  			trim := strings.TrimSpace(strings.TrimRight(line, ";"))
    99  			if len(trim) > 0 {
   100  				conn.Query(trim)
   101  			}
   102  		}
   103  		conn.ReplyError(pgerrcode.DuplicateObject, `role "postgres" already exists`)
   104  		// Run test
   105  		err = initDatabase(context.Background(), fsys, io.Discard, conn.Intercept)
   106  		// Check error
   107  		assert.ErrorContains(t, err, `ERROR: role "postgres" already exists (SQLSTATE 42710)`)
   108  	})
   109  }
   110  
   111  func TestStartDatabase(t *testing.T) {
   112  	t.Cleanup(func() {
   113  		utils.Containers = []string{}
   114  		utils.Volumes = []string{}
   115  	})
   116  
   117  	t.Run("initialise main branch", func(t *testing.T) {
   118  		utils.DbImage = utils.Pg15Image
   119  		utils.Config.Db.MajorVersion = 15
   120  		utils.DbId = "supabase_db_test"
   121  		utils.Config.Db.Port = 5432
   122  		utils.InitialSchemaSql = "CREATE SCHEMA public"
   123  		// Setup in-memory fs
   124  		fsys := afero.NewMemMapFs()
   125  		// Setup mock docker
   126  		require.NoError(t, apitest.MockDocker(utils.Docker))
   127  		defer gock.OffAll()
   128  		gock.New(utils.Docker.DaemonHost()).
   129  			Get("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId).
   130  			Reply(http.StatusNotFound).
   131  			JSON(types.Volume{})
   132  		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.DbImage), utils.DbId)
   133  		gock.New(utils.Docker.DaemonHost()).
   134  			Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json").
   135  			Reply(http.StatusOK).
   136  			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
   137  				State: &types.ContainerState{
   138  					Running: true,
   139  					Health:  &types.Health{Status: "healthy"},
   140  				},
   141  			}})
   142  		// Setup mock postgres
   143  		conn := pgtest.NewConn()
   144  		defer conn.Close(t)
   145  		globals, err := parser.SplitAndTrim(strings.NewReader(utils.GlobalsSql))
   146  		require.NoError(t, err)
   147  		for _, line := range globals {
   148  			trim := strings.TrimSpace(strings.TrimRight(line, ";"))
   149  			if len(trim) > 0 {
   150  				conn.Query(trim)
   151  			}
   152  		}
   153  		conn.Query(utils.InitialSchemaSql).Reply("CREATE SCHEMA")
   154  		// Run test
   155  		err = StartDatabase(context.Background(), fsys, io.Discard, conn.Intercept)
   156  		// Check error
   157  		assert.NoError(t, err)
   158  		assert.Empty(t, apitest.ListUnmatchedRequests())
   159  		// Check current branch
   160  		contents, err := afero.ReadFile(fsys, utils.CurrBranchPath)
   161  		assert.NoError(t, err)
   162  		assert.Equal(t, []byte("main"), contents)
   163  	})
   164  
   165  	t.Run("recover from backup volume", func(t *testing.T) {
   166  		utils.DbImage = utils.Pg15Image
   167  		utils.Config.Db.MajorVersion = 15
   168  		utils.DbId = "supabase_db_test"
   169  		// Setup in-memory fs
   170  		fsys := afero.NewMemMapFs()
   171  		// Setup mock docker
   172  		require.NoError(t, apitest.MockDocker(utils.Docker))
   173  		defer gock.OffAll()
   174  		gock.New(utils.Docker.DaemonHost()).
   175  			Get("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId).
   176  			Reply(http.StatusOK).
   177  			JSON(types.Volume{})
   178  		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.DbImage), utils.DbId)
   179  		gock.New(utils.Docker.DaemonHost()).
   180  			Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json").
   181  			Reply(http.StatusOK).
   182  			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
   183  				State: &types.ContainerState{
   184  					Running: true,
   185  					Health:  &types.Health{Status: "healthy"},
   186  				},
   187  			}})
   188  		// Run test
   189  		err := StartDatabase(context.Background(), fsys, io.Discard)
   190  		// Check error
   191  		assert.NoError(t, err)
   192  		assert.Empty(t, apitest.ListUnmatchedRequests())
   193  		// Check current branch
   194  		contents, err := afero.ReadFile(fsys, utils.CurrBranchPath)
   195  		assert.NoError(t, err)
   196  		assert.Equal(t, []byte("main"), contents)
   197  	})
   198  
   199  	t.Run("throws error on start failure", func(t *testing.T) {
   200  		utils.DbImage = utils.Pg15Image
   201  		utils.Config.Db.MajorVersion = 15
   202  		utils.DbId = "supabase_db_test"
   203  		// Setup in-memory fs
   204  		fsys := afero.NewMemMapFs()
   205  		// Setup mock docker
   206  		require.NoError(t, apitest.MockDocker(utils.Docker))
   207  		defer gock.OffAll()
   208  		gock.New(utils.Docker.DaemonHost()).
   209  			Get("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId).
   210  			ReplyError(errors.New("network error"))
   211  		gock.New(utils.Docker.DaemonHost()).
   212  			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.DbImage) + "/json").
   213  			Reply(http.StatusInternalServerError)
   214  		// Run test
   215  		err := StartDatabase(context.Background(), fsys, io.Discard)
   216  		// Check error
   217  		assert.ErrorContains(t, err, "request returned Internal Server Error for API route and version")
   218  		assert.Empty(t, apitest.ListUnmatchedRequests())
   219  	})
   220  }
   221  
   222  func TestStartCommand(t *testing.T) {
   223  	t.Run("throws error on missing config", func(t *testing.T) {
   224  		// Setup in-memory fs
   225  		fsys := afero.NewMemMapFs()
   226  		// Run test
   227  		err := Run(context.Background(), fsys)
   228  		// Check error
   229  		assert.ErrorIs(t, err, os.ErrNotExist)
   230  	})
   231  
   232  	t.Run("throws error on missing docker", func(t *testing.T) {
   233  		// Setup in-memory fs
   234  		fsys := afero.NewMemMapFs()
   235  		require.NoError(t, utils.WriteConfig(fsys, false))
   236  		// Setup mock docker
   237  		require.NoError(t, apitest.MockDocker(utils.Docker))
   238  		defer gock.OffAll()
   239  		gock.New(utils.Docker.DaemonHost()).
   240  			Head("/_ping").
   241  			ReplyError(errors.New("network error"))
   242  		gock.New(utils.Docker.DaemonHost()).
   243  			Get("/_ping").
   244  			ReplyError(errors.New("network error"))
   245  		// Run test
   246  		err := Run(context.Background(), fsys)
   247  		// Check error
   248  		assert.ErrorContains(t, err, "network error")
   249  		assert.Empty(t, apitest.ListUnmatchedRequests())
   250  	})
   251  
   252  	t.Run("exits if already started", func(t *testing.T) {
   253  		// Setup in-memory fs
   254  		fsys := afero.NewMemMapFs()
   255  		require.NoError(t, utils.WriteConfig(fsys, false))
   256  		// Setup mock docker
   257  		require.NoError(t, apitest.MockDocker(utils.Docker))
   258  		defer gock.OffAll()
   259  		gock.New("http:///var/run/docker.sock").
   260  			Head("/_ping").
   261  			Reply(http.StatusOK).
   262  			SetHeader("API-Version", utils.Docker.ClientVersion()).
   263  			SetHeader("OSType", "linux")
   264  		gock.New(utils.Docker.DaemonHost()).
   265  			Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json").
   266  			Reply(http.StatusOK).
   267  			JSON(types.ContainerJSON{})
   268  		// Run test
   269  		err := Run(context.Background(), fsys)
   270  		// Check error
   271  		assert.NoError(t, err)
   272  		assert.Empty(t, apitest.ListUnmatchedRequests())
   273  	})
   274  
   275  	t.Run("throws error on start failure", func(t *testing.T) {
   276  		// Setup in-memory fs
   277  		fsys := afero.NewMemMapFs()
   278  		require.NoError(t, utils.WriteConfig(fsys, false))
   279  		// Setup mock docker
   280  		require.NoError(t, apitest.MockDocker(utils.Docker))
   281  		defer gock.OffAll()
   282  		gock.New("http:///var/run/docker.sock").
   283  			Head("/_ping").
   284  			Reply(http.StatusOK).
   285  			SetHeader("API-Version", utils.Docker.ClientVersion()).
   286  			SetHeader("OSType", "linux")
   287  		gock.New(utils.Docker.DaemonHost()).
   288  			Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json").
   289  			Reply(http.StatusNotFound)
   290  		// Fail to start
   291  		gock.New(utils.Docker.DaemonHost()).
   292  			Get("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId).
   293  			ReplyError(errors.New("network error"))
   294  		gock.New(utils.Docker.DaemonHost()).
   295  			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Pg15Image) + "/json").
   296  			ReplyError(errors.New("network error"))
   297  		// Cleanup resources
   298  		gock.New(utils.Docker.DaemonHost()).
   299  			Delete("/v" + utils.Docker.ClientVersion() + "/networks/").
   300  			Reply(http.StatusOK)
   301  		// Run test
   302  		err := Run(context.Background(), fsys)
   303  		// Check error
   304  		assert.ErrorContains(t, err, "network error")
   305  		assert.Empty(t, apitest.ListUnmatchedRequests())
   306  	})
   307  }