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

     1  package reset
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"os"
     8  	"path/filepath"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/docker/docker/api/types"
    13  	"github.com/docker/docker/client"
    14  	"github.com/jackc/pgerrcode"
    15  	"github.com/spf13/afero"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  	"github.com/Redstoneguy129/cli/internal/testing/apitest"
    19  	"github.com/Redstoneguy129/cli/internal/testing/pgtest"
    20  	"github.com/Redstoneguy129/cli/internal/utils"
    21  	"gopkg.in/h2non/gock.v1"
    22  )
    23  
    24  func TestResetCommand(t *testing.T) {
    25  	t.Run("throws error on missing config", func(t *testing.T) {
    26  		err := Run(context.Background(), afero.NewMemMapFs())
    27  		assert.ErrorIs(t, err, os.ErrNotExist)
    28  	})
    29  
    30  	t.Run("throws error on db is not started", func(t *testing.T) {
    31  		// Setup in-memory fs
    32  		fsys := afero.NewMemMapFs()
    33  		require.NoError(t, utils.WriteConfig(fsys, false))
    34  		// Setup mock docker
    35  		require.NoError(t, apitest.MockDocker(utils.Docker))
    36  		defer gock.OffAll()
    37  		gock.New(utils.Docker.DaemonHost()).
    38  			Get("/v" + utils.Docker.ClientVersion() + "/containers").
    39  			Reply(http.StatusServiceUnavailable)
    40  		// Run test
    41  		err := Run(context.Background(), fsys)
    42  		// Check error
    43  		assert.ErrorContains(t, err, "supabase start is not running.")
    44  		assert.Empty(t, apitest.ListUnmatchedRequests())
    45  	})
    46  
    47  	t.Run("throws error on failure to recreate", func(t *testing.T) {
    48  		// Setup in-memory fs
    49  		fsys := afero.NewMemMapFs()
    50  		require.NoError(t, utils.WriteConfig(fsys, false))
    51  		// Setup mock docker
    52  		require.NoError(t, client.WithHTTPClient(http.DefaultClient)(utils.Docker))
    53  		defer gock.OffAll()
    54  		gock.New(utils.Docker.DaemonHost()).
    55  			Get("/v" + utils.Docker.ClientVersion() + "/containers").
    56  			Reply(http.StatusOK).
    57  			JSON(types.ContainerJSON{})
    58  		// Setup mock postgres
    59  		conn := pgtest.NewConn()
    60  		defer conn.Close(t)
    61  		conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false;").
    62  			ReplyError(pgerrcode.InvalidParameterValue, `cannot disallow connections for current database`)
    63  		// Run test
    64  		err := Run(context.Background(), fsys, conn.Intercept)
    65  		// Check error
    66  		assert.ErrorContains(t, err, "ERROR: cannot disallow connections for current database (SQLSTATE 22023)")
    67  		assert.Empty(t, apitest.ListUnmatchedRequests())
    68  	})
    69  }
    70  
    71  func TestResetDatabase(t *testing.T) {
    72  	t.Run("initialises postgres database", func(t *testing.T) {
    73  		utils.Config.Db.Port = 54322
    74  		utils.InitialSchemaSql = "CREATE SCHEMA public"
    75  		// Setup in-memory fs
    76  		fsys := afero.NewMemMapFs()
    77  		// Setup mock postgres
    78  		conn := pgtest.NewConn()
    79  		defer conn.Close(t)
    80  		conn.Query(utils.InitialSchemaSql).
    81  			Reply("CREATE SCHEMA")
    82  		// Run test
    83  		assert.NoError(t, resetDatabase(context.Background(), fsys, conn.Intercept))
    84  	})
    85  
    86  	t.Run("throws error on connect failure", func(t *testing.T) {
    87  		utils.Config.Db.Port = 0
    88  		// Setup in-memory fs
    89  		fsys := afero.NewMemMapFs()
    90  		// Run test
    91  		err := resetDatabase(context.Background(), fsys)
    92  		// Check error
    93  		assert.ErrorContains(t, err, "invalid port")
    94  	})
    95  
    96  	t.Run("throws error on duplicate schema", func(t *testing.T) {
    97  		utils.Config.Db.Port = 54322
    98  		utils.InitialSchemaSql = "CREATE SCHEMA public"
    99  		// Setup in-memory fs
   100  		fsys := afero.NewMemMapFs()
   101  		// Setup mock postgres
   102  		conn := pgtest.NewConn()
   103  		defer conn.Close(t)
   104  		conn.Query(utils.InitialSchemaSql).
   105  			ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`)
   106  		// Run test
   107  		err := resetDatabase(context.Background(), fsys, conn.Intercept)
   108  		// Check error
   109  		assert.ErrorContains(t, err, `ERROR: schema "public" already exists (SQLSTATE 42P06)`)
   110  	})
   111  
   112  	t.Run("throws error on migration failure", func(t *testing.T) {
   113  		utils.Config.Db.Port = 54322
   114  		utils.InitialSchemaSql = "CREATE SCHEMA public"
   115  		// Setup in-memory fs
   116  		fsys := afero.NewMemMapFs()
   117  		path := filepath.Join(utils.MigrationsDir, "0_table.sql")
   118  		sql := "CREATE TABLE example()"
   119  		require.NoError(t, afero.WriteFile(fsys, path, []byte(sql), 0644))
   120  		// Setup mock postgres
   121  		conn := pgtest.NewConn()
   122  		defer conn.Close(t)
   123  		conn.Query(utils.InitialSchemaSql).
   124  			Reply("CREATE SCHEMA").
   125  			Query(sql).
   126  			ReplyError(pgerrcode.DuplicateObject, `table "example" already exists`)
   127  		// Run test
   128  		err := resetDatabase(context.Background(), fsys, conn.Intercept)
   129  		// Check error
   130  		assert.ErrorContains(t, err, `ERROR: table "example" already exists (SQLSTATE 42710)`)
   131  	})
   132  }
   133  
   134  func TestSeedDatabase(t *testing.T) {
   135  	t.Run("seeds from file", func(t *testing.T) {
   136  		// Setup in-memory fs
   137  		fsys := afero.NewMemMapFs()
   138  		// Setup seed file
   139  		sql := "INSERT INTO employees(name) VALUES ('Alice')"
   140  		require.NoError(t, afero.WriteFile(fsys, utils.SeedDataPath, []byte(sql), 0644))
   141  		// Setup mock postgres
   142  		conn := pgtest.NewConn()
   143  		defer conn.Close(t)
   144  		conn.Query(sql).
   145  			Reply("INSERT 0 1")
   146  		// Connect to mock
   147  		ctx := context.Background()
   148  		mock, err := utils.ConnectLocalPostgres(ctx, utils.Config.Hostname, 54322, "postgres", conn.Intercept)
   149  		require.NoError(t, err)
   150  		defer mock.Close(ctx)
   151  		// Run test
   152  		assert.NoError(t, SeedDatabase(ctx, mock, fsys))
   153  	})
   154  
   155  	t.Run("ignores missing seed", func(t *testing.T) {
   156  		assert.NoError(t, SeedDatabase(context.Background(), nil, afero.NewMemMapFs()))
   157  	})
   158  
   159  	t.Run("throws error on insert failure", func(t *testing.T) {
   160  		// Setup in-memory fs
   161  		fsys := afero.NewMemMapFs()
   162  		// Setup seed file
   163  		sql := "INSERT INTO employees(name) VALUES ('Alice')"
   164  		require.NoError(t, afero.WriteFile(fsys, utils.SeedDataPath, []byte(sql), 0644))
   165  		// Setup mock postgres
   166  		conn := pgtest.NewConn()
   167  		defer conn.Close(t)
   168  		conn.Query(sql).
   169  			ReplyError(pgerrcode.NotNullViolation, `null value in column "age" of relation "employees"`)
   170  		// Connect to mock
   171  		ctx := context.Background()
   172  		mock, err := utils.ConnectLocalPostgres(ctx, utils.Config.Hostname, 54322, "postgres", conn.Intercept)
   173  		require.NoError(t, err)
   174  		defer mock.Close(ctx)
   175  		// Run test
   176  		err = SeedDatabase(ctx, mock, fsys)
   177  		// Check error
   178  		assert.ErrorContains(t, err, `ERROR: null value in column "age" of relation "employees" (SQLSTATE 23502)`)
   179  	})
   180  }
   181  
   182  func TestRecreateDatabase(t *testing.T) {
   183  	t.Run("resets postgres database", func(t *testing.T) {
   184  		utils.Config.Db.Port = 54322
   185  		// Setup mock postgres
   186  		conn := pgtest.NewConn()
   187  		defer conn.Close(t)
   188  		conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false;").
   189  			Reply("ALTER DATABASE").
   190  			Query(fmt.Sprintf(utils.TerminateDbSqlFmt, "postgres")).
   191  			Reply("DO").
   192  			Query("DROP DATABASE IF EXISTS postgres WITH (FORCE);").
   193  			Reply("DROP DATABASE").
   194  			Query("CREATE DATABASE postgres;").
   195  			Reply("CREATE DATABASE")
   196  		// Run test
   197  		assert.NoError(t, RecreateDatabase(context.Background(), conn.Intercept))
   198  	})
   199  
   200  	t.Run("throws error on invalid port", func(t *testing.T) {
   201  		utils.Config.Db.Port = 0
   202  		assert.ErrorContains(t, RecreateDatabase(context.Background()), "invalid port")
   203  	})
   204  
   205  	t.Run("continues on disconnecting missing database", func(t *testing.T) {
   206  		utils.Config.Db.Port = 54322
   207  		// Setup mock postgres
   208  		conn := pgtest.NewConn()
   209  		defer conn.Close(t)
   210  		conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false;").
   211  			ReplyError(pgerrcode.InvalidCatalogName, `database "postgres" does not exist`).
   212  			Query(fmt.Sprintf(utils.TerminateDbSqlFmt, "postgres")).
   213  			ReplyError(pgerrcode.UndefinedTable, `relation "pg_stat_activity" does not exist`)
   214  		// Run test
   215  		err := RecreateDatabase(context.Background(), conn.Intercept)
   216  		// Check error
   217  		assert.ErrorContains(t, err, `ERROR: relation "pg_stat_activity" does not exist (SQLSTATE 42P01)`)
   218  	})
   219  
   220  	t.Run("throws error on failure to disconnect", func(t *testing.T) {
   221  		utils.Config.Db.Port = 54322
   222  		// Setup mock postgres
   223  		conn := pgtest.NewConn()
   224  		defer conn.Close(t)
   225  		conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false;").
   226  			ReplyError(pgerrcode.InvalidParameterValue, `cannot disallow connections for current database`)
   227  		// Run test
   228  		err := RecreateDatabase(context.Background(), conn.Intercept)
   229  		// Check error
   230  		assert.ErrorContains(t, err, "ERROR: cannot disallow connections for current database (SQLSTATE 22023)")
   231  	})
   232  
   233  	t.Run("throws error on failure to drop", func(t *testing.T) {
   234  		utils.Config.Db.Port = 54322
   235  		// Setup mock postgres
   236  		conn := pgtest.NewConn()
   237  		defer conn.Close(t)
   238  		conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false;").
   239  			Reply("ALTER DATABASE").
   240  			Query(fmt.Sprintf(utils.TerminateDbSqlFmt, "postgres")).
   241  			Reply("DO").
   242  			Query("DROP DATABASE IF EXISTS postgres WITH (FORCE);").
   243  			ReplyError(pgerrcode.ObjectInUse, `database "postgres" is used by an active logical replication slot`)
   244  		// Run test
   245  		err := RecreateDatabase(context.Background(), conn.Intercept)
   246  		// Check error
   247  		assert.ErrorContains(t, err, `ERROR: database "postgres" is used by an active logical replication slot (SQLSTATE 55006)`)
   248  	})
   249  }
   250  
   251  func TestRestartDatabase(t *testing.T) {
   252  	t.Run("restarts affected services", func(t *testing.T) {
   253  		utils.DbId = "test-reset"
   254  		// Setup mock docker
   255  		require.NoError(t, apitest.MockDocker(utils.Docker))
   256  		defer gock.OffAll()
   257  		// Restarts storage api
   258  		gock.New(utils.Docker.DaemonHost()).
   259  			Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart").
   260  			Reply(http.StatusOK)
   261  		gock.New(utils.Docker.DaemonHost()).
   262  			Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json").
   263  			Reply(http.StatusOK).
   264  			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
   265  				State: &types.ContainerState{
   266  					Running: true,
   267  					Health:  &types.Health{Status: "healthy"},
   268  				},
   269  			}})
   270  		// Restarts postgREST
   271  		utils.RestId = "test-rest"
   272  		gock.New(utils.Docker.DaemonHost()).
   273  			Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.RestId + "/kill").
   274  			Reply(http.StatusOK)
   275  		utils.StorageId = "test-storage"
   276  		gock.New(utils.Docker.DaemonHost()).
   277  			Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.StorageId + "/restart").
   278  			Reply(http.StatusServiceUnavailable)
   279  		// Run test
   280  		RestartDatabase(context.Background())
   281  		// Check error
   282  		assert.Empty(t, apitest.ListUnmatchedRequests())
   283  	})
   284  
   285  	t.Run("timeout health check", func(t *testing.T) {
   286  		utils.DbId = "test-reset"
   287  		healthTimeout = 0 * time.Second
   288  		// Setup mock docker
   289  		require.NoError(t, apitest.MockDocker(utils.Docker))
   290  		defer gock.OffAll()
   291  		gock.New(utils.Docker.DaemonHost()).
   292  			Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart").
   293  			Reply(http.StatusOK)
   294  		// Run test
   295  		RestartDatabase(context.Background())
   296  		// Check error
   297  		assert.Empty(t, apitest.ListUnmatchedRequests())
   298  	})
   299  }