github.com/supabase/cli@v1.168.1/internal/db/reset/reset_test.go (about)

     1  package reset
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/docker/docker/api/types"
    13  	"github.com/jackc/pgconn"
    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/supabase/cli/internal/db/start"
    19  	"github.com/supabase/cli/internal/testing/apitest"
    20  	"github.com/supabase/cli/internal/testing/fstest"
    21  	"github.com/supabase/cli/internal/testing/pgtest"
    22  	"github.com/supabase/cli/internal/utils"
    23  	"gopkg.in/h2non/gock.v1"
    24  )
    25  
    26  func TestResetCommand(t *testing.T) {
    27  	utils.Config.Hostname = "127.0.0.1"
    28  	utils.Config.Db.Port = 5432
    29  
    30  	var dbConfig = pgconn.Config{
    31  		Host:     utils.Config.Hostname,
    32  		Port:     utils.Config.Db.Port,
    33  		User:     "admin",
    34  		Password: "password",
    35  		Database: "postgres",
    36  	}
    37  
    38  	t.Run("throws error on context canceled", func(t *testing.T) {
    39  		// Setup in-memory fs
    40  		fsys := afero.NewMemMapFs()
    41  		// Run test
    42  		err := Run(context.Background(), "", pgconn.Config{Host: "db.supabase.co"}, fsys)
    43  		// Check error
    44  		assert.ErrorIs(t, err, context.Canceled)
    45  	})
    46  
    47  	t.Run("throws error on invalid port", func(t *testing.T) {
    48  		defer fstest.MockStdin(t, "y")()
    49  		// Setup in-memory fs
    50  		fsys := afero.NewMemMapFs()
    51  		// Run test
    52  		err := Run(context.Background(), "", pgconn.Config{Host: "db.supabase.co"}, fsys)
    53  		// Check error
    54  		assert.ErrorContains(t, err, "invalid port (outside range)")
    55  	})
    56  
    57  	t.Run("throws error on db is not started", func(t *testing.T) {
    58  		// Setup in-memory fs
    59  		fsys := afero.NewMemMapFs()
    60  		// Setup mock docker
    61  		require.NoError(t, apitest.MockDocker(utils.Docker))
    62  		defer gock.OffAll()
    63  		gock.New(utils.Docker.DaemonHost()).
    64  			Get("/v" + utils.Docker.ClientVersion() + "/containers").
    65  			Reply(http.StatusNotFound)
    66  		// Run test
    67  		err := Run(context.Background(), "", dbConfig, fsys)
    68  		// Check error
    69  		assert.ErrorIs(t, err, utils.ErrNotRunning)
    70  		assert.Empty(t, apitest.ListUnmatchedRequests())
    71  	})
    72  
    73  	t.Run("throws error on failure to recreate", func(t *testing.T) {
    74  		utils.DbId = "test-reset"
    75  		utils.Config.Db.MajorVersion = 15
    76  		// Setup in-memory fs
    77  		fsys := afero.NewMemMapFs()
    78  		// Setup mock docker
    79  		require.NoError(t, apitest.MockDocker(utils.Docker))
    80  		defer gock.OffAll()
    81  		gock.New(utils.Docker.DaemonHost()).
    82  			Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId).
    83  			Reply(http.StatusOK).
    84  			JSON(types.ContainerJSON{})
    85  		gock.New(utils.Docker.DaemonHost()).
    86  			Delete("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId).
    87  			ReplyError(errors.New("network error"))
    88  		// Run test
    89  		err := Run(context.Background(), "", dbConfig, fsys)
    90  		// Check error
    91  		assert.ErrorContains(t, err, "network error")
    92  		assert.Empty(t, apitest.ListUnmatchedRequests())
    93  	})
    94  }
    95  
    96  func TestInitDatabase(t *testing.T) {
    97  	t.Run("initializes postgres database", func(t *testing.T) {
    98  		utils.Config.Db.Port = 54322
    99  		utils.InitialSchemaSql = "CREATE SCHEMA public"
   100  		// Setup mock postgres
   101  		conn := pgtest.NewConn()
   102  		defer conn.Close(t)
   103  		conn.Query(utils.InitialSchemaSql).
   104  			Reply("CREATE SCHEMA")
   105  		// Run test
   106  		assert.NoError(t, initDatabase(context.Background(), conn.Intercept))
   107  	})
   108  
   109  	t.Run("throws error on connect failure", func(t *testing.T) {
   110  		utils.Config.Db.Port = 0
   111  		// Run test
   112  		err := initDatabase(context.Background())
   113  		// Check error
   114  		assert.ErrorContains(t, err, "invalid port (outside range)")
   115  	})
   116  
   117  	t.Run("throws error on duplicate schema", func(t *testing.T) {
   118  		utils.Config.Db.Port = 54322
   119  		utils.InitialSchemaSql = "CREATE SCHEMA public"
   120  		// Setup mock postgres
   121  		conn := pgtest.NewConn()
   122  		defer conn.Close(t)
   123  		conn.Query(utils.InitialSchemaSql).
   124  			ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`)
   125  		// Run test
   126  		err := initDatabase(context.Background(), conn.Intercept)
   127  		// Check error
   128  		assert.ErrorContains(t, err, `ERROR: schema "public" already exists (SQLSTATE 42P06)`)
   129  	})
   130  }
   131  
   132  func TestRecreateDatabase(t *testing.T) {
   133  	t.Run("resets postgres database", func(t *testing.T) {
   134  		utils.Config.Db.Port = 54322
   135  		// Setup mock postgres
   136  		conn := pgtest.NewConn()
   137  		defer conn.Close(t)
   138  		conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false;").
   139  			Reply("ALTER DATABASE").
   140  			Query(fmt.Sprintf(utils.TerminateDbSqlFmt, "postgres")).
   141  			Reply("DO").
   142  			Query("DROP DATABASE IF EXISTS postgres WITH (FORCE)").
   143  			Reply("DROP DATABASE").
   144  			Query("CREATE DATABASE postgres WITH OWNER postgres").
   145  			Reply("CREATE DATABASE")
   146  		// Run test
   147  		assert.NoError(t, recreateDatabase(context.Background(), conn.Intercept))
   148  	})
   149  
   150  	t.Run("throws error on invalid port", func(t *testing.T) {
   151  		utils.Config.Db.Port = 0
   152  		assert.ErrorContains(t, recreateDatabase(context.Background()), "invalid port")
   153  	})
   154  
   155  	t.Run("continues on disconnecting missing database", func(t *testing.T) {
   156  		utils.Config.Db.Port = 54322
   157  		// Setup mock postgres
   158  		conn := pgtest.NewConn()
   159  		defer conn.Close(t)
   160  		conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false;").
   161  			ReplyError(pgerrcode.InvalidCatalogName, `database "postgres" does not exist`).
   162  			Query(fmt.Sprintf(utils.TerminateDbSqlFmt, "postgres")).
   163  			ReplyError(pgerrcode.UndefinedTable, `relation "pg_stat_activity" does not exist`)
   164  		// Run test
   165  		err := recreateDatabase(context.Background(), conn.Intercept)
   166  		// Check error
   167  		assert.ErrorContains(t, err, `ERROR: relation "pg_stat_activity" does not exist (SQLSTATE 42P01)`)
   168  	})
   169  
   170  	t.Run("throws error on failure to disconnect", func(t *testing.T) {
   171  		utils.Config.Db.Port = 54322
   172  		// Setup mock postgres
   173  		conn := pgtest.NewConn()
   174  		defer conn.Close(t)
   175  		conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false;").
   176  			ReplyError(pgerrcode.InvalidParameterValue, `cannot disallow connections for current database`)
   177  		// Run test
   178  		err := recreateDatabase(context.Background(), conn.Intercept)
   179  		// Check error
   180  		assert.ErrorContains(t, err, "ERROR: cannot disallow connections for current database (SQLSTATE 22023)")
   181  	})
   182  
   183  	t.Run("throws error on failure to drop", 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  			ReplyError(pgerrcode.ObjectInUse, `database "postgres" is used by an active logical replication slot`).
   194  			Query("CREATE DATABASE postgres WITH OWNER postgres")
   195  		// Run test
   196  		err := recreateDatabase(context.Background(), conn.Intercept)
   197  		// Check error
   198  		assert.ErrorContains(t, err, `ERROR: database "postgres" is used by an active logical replication slot (SQLSTATE 55006)`)
   199  	})
   200  }
   201  
   202  func TestRestartDatabase(t *testing.T) {
   203  	t.Run("restarts affected services", func(t *testing.T) {
   204  		utils.DbId = "test-reset"
   205  		// Setup mock docker
   206  		require.NoError(t, apitest.MockDocker(utils.Docker))
   207  		defer gock.OffAll()
   208  		// Restarts postgres
   209  		gock.New(utils.Docker.DaemonHost()).
   210  			Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart").
   211  			Reply(http.StatusOK)
   212  		gock.New(utils.Docker.DaemonHost()).
   213  			Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json").
   214  			Reply(http.StatusOK).
   215  			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
   216  				State: &types.ContainerState{
   217  					Running: true,
   218  					Health:  &types.Health{Status: "healthy"},
   219  				},
   220  			}})
   221  		// Restarts services
   222  		utils.StorageId = "test-storage"
   223  		utils.GotrueId = "test-auth"
   224  		utils.RealtimeId = "test-realtime"
   225  		for _, container := range []string{utils.StorageId, utils.GotrueId, utils.RealtimeId} {
   226  			gock.New(utils.Docker.DaemonHost()).
   227  				Post("/v" + utils.Docker.ClientVersion() + "/containers/" + container + "/restart").
   228  				Reply(http.StatusOK)
   229  		}
   230  		// Run test
   231  		err := RestartDatabase(context.Background(), io.Discard)
   232  		// Check error
   233  		assert.NoError(t, err)
   234  		assert.Empty(t, apitest.ListUnmatchedRequests())
   235  	})
   236  
   237  	t.Run("throws error on service restart failure", func(t *testing.T) {
   238  		utils.DbId = "test-reset"
   239  		// Setup mock docker
   240  		require.NoError(t, apitest.MockDocker(utils.Docker))
   241  		defer gock.OffAll()
   242  		// Restarts postgres
   243  		gock.New(utils.Docker.DaemonHost()).
   244  			Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart").
   245  			Reply(http.StatusOK)
   246  		gock.New(utils.Docker.DaemonHost()).
   247  			Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json").
   248  			Reply(http.StatusOK).
   249  			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
   250  				State: &types.ContainerState{
   251  					Running: true,
   252  					Health:  &types.Health{Status: "healthy"},
   253  				},
   254  			}})
   255  		// Restarts services
   256  		utils.StorageId = "test-storage"
   257  		utils.GotrueId = "test-auth"
   258  		utils.RealtimeId = "test-realtime"
   259  		for _, container := range []string{utils.StorageId, utils.GotrueId, utils.RealtimeId} {
   260  			gock.New(utils.Docker.DaemonHost()).
   261  				Post("/v" + utils.Docker.ClientVersion() + "/containers/" + container + "/restart").
   262  				Reply(http.StatusServiceUnavailable)
   263  		}
   264  		// Run test
   265  		err := RestartDatabase(context.Background(), io.Discard)
   266  		// Check error
   267  		assert.ErrorContains(t, err, "Failed to restart "+utils.StorageId)
   268  		assert.ErrorContains(t, err, "Failed to restart "+utils.GotrueId)
   269  		assert.ErrorContains(t, err, "Failed to restart "+utils.RealtimeId)
   270  		assert.Empty(t, apitest.ListUnmatchedRequests())
   271  	})
   272  
   273  	t.Run("throws error on db restart failure", func(t *testing.T) {
   274  		utils.DbId = "test-reset"
   275  		// Setup mock docker
   276  		require.NoError(t, apitest.MockDocker(utils.Docker))
   277  		defer gock.OffAll()
   278  		// Restarts postgres
   279  		gock.New(utils.Docker.DaemonHost()).
   280  			Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart").
   281  			Reply(http.StatusServiceUnavailable)
   282  		// Run test
   283  		err := RestartDatabase(context.Background(), io.Discard)
   284  		// Check error
   285  		assert.ErrorContains(t, err, "failed to restart container")
   286  		assert.Empty(t, apitest.ListUnmatchedRequests())
   287  	})
   288  
   289  	t.Run("throws error on health check timeout", func(t *testing.T) {
   290  		utils.DbId = "test-reset"
   291  		start.HealthTimeout = 0 * time.Second
   292  		// Setup mock docker
   293  		require.NoError(t, apitest.MockDocker(utils.Docker))
   294  		defer gock.OffAll()
   295  		gock.New(utils.Docker.DaemonHost()).
   296  			Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart").
   297  			Reply(http.StatusOK)
   298  		// Run test
   299  		err := RestartDatabase(context.Background(), io.Discard)
   300  		// Check error
   301  		assert.ErrorIs(t, err, start.ErrDatabase)
   302  		assert.Empty(t, apitest.ListUnmatchedRequests())
   303  	})
   304  }
   305  
   306  var escapedSchemas = []string{
   307  	"extensions",
   308  	"public",
   309  	"pgbouncer",
   310  	"pgsodium",
   311  	"pgtle",
   312  	`supabase\_migrations`,
   313  	"vault",
   314  	`information\_schema`,
   315  	`pg\_%`,
   316  }
   317  
   318  func TestResetRemote(t *testing.T) {
   319  	dbConfig := pgconn.Config{
   320  		Host:     "db.supabase.co",
   321  		Port:     5432,
   322  		User:     "admin",
   323  		Password: "password",
   324  		Database: "postgres",
   325  	}
   326  
   327  	t.Run("resets remote database", func(t *testing.T) {
   328  		// Setup in-memory fs
   329  		fsys := afero.NewMemMapFs()
   330  		// Setup mock postgres
   331  		conn := pgtest.NewConn()
   332  		defer conn.Close(t)
   333  		conn.Query(ListSchemas, escapedSchemas).
   334  			Reply("SELECT 1", []interface{}{"private"}).
   335  			Query("DROP SCHEMA IF EXISTS private CASCADE").
   336  			Reply("DROP SCHEMA").
   337  			Query(dropObjects).
   338  			Reply("INSERT 0")
   339  		// Run test
   340  		err := resetRemote(context.Background(), "", dbConfig, fsys, conn.Intercept)
   341  		// Check error
   342  		assert.NoError(t, err)
   343  	})
   344  
   345  	t.Run("throws error on connect failure", func(t *testing.T) {
   346  		// Setup in-memory fs
   347  		fsys := afero.NewMemMapFs()
   348  		// Run test
   349  		err := resetRemote(context.Background(), "", pgconn.Config{}, fsys)
   350  		// Check error
   351  		assert.ErrorContains(t, err, "invalid port (outside range)")
   352  	})
   353  
   354  	t.Run("throws error on list schema failure", func(t *testing.T) {
   355  		// Setup in-memory fs
   356  		fsys := afero.NewMemMapFs()
   357  		// Setup mock postgres
   358  		conn := pgtest.NewConn()
   359  		defer conn.Close(t)
   360  		conn.Query(ListSchemas, escapedSchemas).
   361  			ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation information_schema")
   362  		// Run test
   363  		err := resetRemote(context.Background(), "", dbConfig, fsys, conn.Intercept)
   364  		// Check error
   365  		assert.ErrorContains(t, err, "ERROR: permission denied for relation information_schema (SQLSTATE 42501)")
   366  	})
   367  
   368  	t.Run("throws error on drop schema failure", func(t *testing.T) {
   369  		// Setup in-memory fs
   370  		fsys := afero.NewMemMapFs()
   371  		// Setup mock postgres
   372  		conn := pgtest.NewConn()
   373  		defer conn.Close(t)
   374  		conn.Query(ListSchemas, escapedSchemas).
   375  			Reply("SELECT 0").
   376  			Query(dropObjects).
   377  			ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation supabase_migrations")
   378  		// Run test
   379  		err := resetRemote(context.Background(), "", dbConfig, fsys, conn.Intercept)
   380  		// Check error
   381  		assert.ErrorContains(t, err, "ERROR: permission denied for relation supabase_migrations (SQLSTATE 42501)")
   382  	})
   383  }