package reset import ( "context" "errors" "io" "net/http" "path/filepath" "testing" "time" "github.com/docker/docker/api/types" "github.com/h2non/gock" "github.com/jackc/pgconn" "github.com/jackc/pgerrcode" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/supabase/cli/internal/db/start" "github.com/supabase/cli/internal/testing/apitest" "github.com/supabase/cli/internal/testing/fstest" "github.com/supabase/cli/internal/testing/helper" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/pkg/migration" "github.com/supabase/cli/pkg/pgtest" "github.com/supabase/cli/pkg/storage" ) func TestResetCommand(t *testing.T) { utils.Config.Hostname = "127.0.0.1" utils.Config.Db.Port = 5432 var dbConfig = pgconn.Config{ Host: utils.Config.Hostname, Port: utils.Config.Db.Port, User: "admin", Password: "password", Database: "postgres", } t.Run("seeds storage after reset", func(t *testing.T) { utils.DbId = "test-reset" utils.ConfigId = "test-config" utils.Config.Db.MajorVersion = 15 // Setup in-memory fs fsys := afero.NewMemMapFs() // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) defer gock.OffAll() gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId). Reply(http.StatusOK). JSON(types.ContainerJSON{}) gock.New(utils.Docker.DaemonHost()). Delete("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId). Reply(http.StatusOK) gock.New(utils.Docker.DaemonHost()). Delete("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId). Reply(http.StatusOK) apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), utils.DbId) gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json"). Reply(http.StatusOK). JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ State: &types.ContainerState{ Running: true, Health: &types.Health{Status: types.Healthy}, }, }}) // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) // Restarts services utils.StorageId = "test-storage" utils.GotrueId = "test-auth" utils.RealtimeId = "test-realtime" utils.PoolerId = "test-pooler" for _, container := range listServicesToRestart() { gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/containers/" + container + "/restart"). Reply(http.StatusOK) } // Seeds storage gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.StorageId + "/json"). Reply(http.StatusOK). JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ State: &types.ContainerState{ Running: true, Health: &types.Health{Status: types.Healthy}, }, }}) gock.New(utils.Config.Api.ExternalUrl). Get("/storage/v1/bucket"). Reply(http.StatusOK). JSON([]storage.BucketResponse{}) // Run test err := Run(context.Background(), "", dbConfig, fsys, conn.Intercept) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on context canceled", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Run test err := Run(context.Background(), "", pgconn.Config{Host: "db.supabase.co"}, fsys) // Check error assert.ErrorIs(t, err, context.Canceled) }) t.Run("throws error on invalid port", func(t *testing.T) { t.Cleanup(fstest.MockStdin(t, "y")) // Setup in-memory fs fsys := afero.NewMemMapFs() // Run test err := Run(context.Background(), "", pgconn.Config{Host: "db.supabase.co"}, fsys) // Check error assert.ErrorContains(t, err, "invalid port (outside range)") }) t.Run("throws error on db is not started", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) defer gock.OffAll() gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers"). Reply(http.StatusNotFound) // Run test err := Run(context.Background(), "", dbConfig, fsys) // Check error assert.ErrorIs(t, err, utils.ErrNotRunning) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on failure to recreate", func(t *testing.T) { utils.DbId = "test-reset" utils.Config.Db.MajorVersion = 15 // Setup in-memory fs fsys := afero.NewMemMapFs() // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) defer gock.OffAll() gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId). Reply(http.StatusOK). JSON(types.ContainerJSON{}) gock.New(utils.Docker.DaemonHost()). Delete("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId). ReplyError(errors.New("network error")) // Run test err := Run(context.Background(), "", dbConfig, fsys) // Check error assert.ErrorContains(t, err, "network error") assert.Empty(t, apitest.ListUnmatchedRequests()) }) } func TestInitDatabase(t *testing.T) { t.Run("initializes postgres database", func(t *testing.T) { utils.Config.Db.Port = 54322 utils.InitialSchemaPg14Sql = "create schema private" // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query(utils.InitialSchemaPg14Sql). Reply("CREATE SCHEMA") // Run test assert.NoError(t, initDatabase(context.Background(), conn.Intercept)) }) t.Run("throws error on connect failure", func(t *testing.T) { utils.Config.Db.Port = 0 // Run test err := initDatabase(context.Background()) // Check error assert.ErrorContains(t, err, "invalid port (outside range)") }) t.Run("throws error on duplicate schema", func(t *testing.T) { utils.Config.Db.Port = 54322 utils.InitialSchemaPg14Sql = "create schema private" // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query(utils.InitialSchemaPg14Sql). ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`) // Run test err := initDatabase(context.Background(), conn.Intercept) // Check error assert.ErrorContains(t, err, `ERROR: schema "public" already exists (SQLSTATE 42P06)`) }) } func TestRecreateDatabase(t *testing.T) { t.Run("resets postgres database", func(t *testing.T) { utils.Config.Db.Port = 54322 // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false"). Reply("ALTER DATABASE"). Query("ALTER DATABASE _supabase ALLOW_CONNECTIONS false"). Reply("ALTER DATABASE"). Query(TERMINATE_BACKENDS). Reply("SELECT 1"). Query(COUNT_REPLICATION_SLOTS). Reply("SELECT 1", []interface{}{0}). Query("DROP DATABASE IF EXISTS postgres WITH (FORCE)"). Reply("DROP DATABASE"). Query("CREATE DATABASE postgres WITH OWNER postgres"). Reply("CREATE DATABASE"). Query("DROP DATABASE IF EXISTS _supabase WITH (FORCE)"). Reply("DROP DATABASE"). Query("CREATE DATABASE _supabase WITH OWNER postgres"). Reply("CREATE DATABASE") // Run test assert.NoError(t, recreateDatabase(context.Background(), conn.Intercept)) }) t.Run("throws error on invalid port", func(t *testing.T) { utils.Config.Db.Port = 0 assert.ErrorContains(t, recreateDatabase(context.Background()), "invalid port") }) t.Run("continues on disconnecting missing database", func(t *testing.T) { utils.Config.Db.Port = 54322 // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false"). Reply("ALTER DATABASE"). Query("ALTER DATABASE _supabase ALLOW_CONNECTIONS false"). ReplyError(pgerrcode.InvalidCatalogName, `database "_supabase" does not exist`). Query(TERMINATE_BACKENDS). Query(COUNT_REPLICATION_SLOTS). ReplyError(pgerrcode.UndefinedTable, `relation "pg_replication_slots" does not exist`) // Run test err := recreateDatabase(context.Background(), conn.Intercept) // Check error assert.ErrorContains(t, err, `ERROR: relation "pg_replication_slots" does not exist (SQLSTATE 42P01)`) }) t.Run("throws error on failure to disconnect", func(t *testing.T) { utils.Config.Db.Port = 54322 // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false"). ReplyError(pgerrcode.InvalidParameterValue, `cannot disallow connections for current database`). Query("ALTER DATABASE _supabase ALLOW_CONNECTIONS false"). Query(TERMINATE_BACKENDS) // Run test err := recreateDatabase(context.Background(), conn.Intercept) // Check error assert.ErrorContains(t, err, "ERROR: cannot disallow connections for current database (SQLSTATE 22023)") }) t.Run("throws error on failure to drop", func(t *testing.T) { utils.Config.Db.Port = 54322 // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query("ALTER DATABASE postgres ALLOW_CONNECTIONS false"). Reply("ALTER DATABASE"). Query("ALTER DATABASE _supabase ALLOW_CONNECTIONS false"). Reply("ALTER DATABASE"). Query(TERMINATE_BACKENDS). Reply("SELECT 1"). Query(COUNT_REPLICATION_SLOTS). Reply("SELECT 1", []interface{}{0}). Query("DROP DATABASE IF EXISTS postgres WITH (FORCE)"). ReplyError(pgerrcode.ObjectInUse, `database "postgres" is used by an active logical replication slot`). Query("CREATE DATABASE postgres WITH OWNER postgres"). Query("DROP DATABASE IF EXISTS _supabase WITH (FORCE)"). Reply("DROP DATABASE"). Query("CREATE DATABASE _supabase WITH OWNER postgres"). Reply("CREATE DATABASE") err := recreateDatabase(context.Background(), conn.Intercept) // Check error assert.ErrorContains(t, err, `ERROR: database "postgres" is used by an active logical replication slot (SQLSTATE 55006)`) }) } func TestRestartDatabase(t *testing.T) { t.Run("restarts affected services", func(t *testing.T) { utils.DbId = "test-reset" // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) defer gock.OffAll() // Restarts postgres gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart"). Reply(http.StatusOK) gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json"). Reply(http.StatusOK). JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ State: &types.ContainerState{ Running: true, Health: &types.Health{Status: types.Healthy}, }, }}) // Restarts services utils.StorageId = "test-storage" utils.GotrueId = "test-auth" utils.RealtimeId = "test-realtime" utils.PoolerId = "test-pooler" for _, container := range listServicesToRestart() { gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/containers/" + container + "/restart"). Reply(http.StatusOK) } // Run test err := RestartDatabase(context.Background(), io.Discard) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on service restart failure", func(t *testing.T) { utils.DbId = "test-reset" // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) defer gock.OffAll() // Restarts postgres gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart"). Reply(http.StatusOK) gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json"). Reply(http.StatusOK). JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ State: &types.ContainerState{ Running: true, Health: &types.Health{Status: types.Healthy}, }, }}) // Restarts services utils.StorageId = "test-storage" utils.GotrueId = "test-auth" utils.RealtimeId = "test-realtime" utils.PoolerId = "test-pooler" for _, container := range []string{utils.StorageId, utils.GotrueId, utils.RealtimeId} { gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/containers/" + container + "/restart"). Reply(http.StatusServiceUnavailable) } gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.PoolerId + "/restart"). Reply(http.StatusNotFound) // Run test err := RestartDatabase(context.Background(), io.Discard) // Check error assert.ErrorContains(t, err, "failed to restart "+utils.StorageId) assert.ErrorContains(t, err, "failed to restart "+utils.GotrueId) assert.ErrorContains(t, err, "failed to restart "+utils.RealtimeId) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on db restart failure", func(t *testing.T) { utils.DbId = "test-reset" // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) defer gock.OffAll() // Restarts postgres gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart"). Reply(http.StatusServiceUnavailable) // Run test err := RestartDatabase(context.Background(), io.Discard) // Check error assert.ErrorContains(t, err, "failed to restart container") assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on health check timeout", func(t *testing.T) { utils.DbId = "test-reset" start.HealthTimeout = 0 * time.Second // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) defer gock.OffAll() gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/containers/test-reset/restart"). Reply(http.StatusOK) gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/test-reset/json"). Reply(http.StatusOK). JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ State: &types.ContainerState{ Running: false, Status: "exited", }, }}) gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/test-reset/logs"). Reply(http.StatusServiceUnavailable) // Run test err := RestartDatabase(context.Background(), io.Discard) // Check error assert.ErrorContains(t, err, "test-reset container is not running: exited") assert.Empty(t, apitest.ListUnmatchedRequests()) }) } var escapedSchemas = append(migration.ManagedSchemas, "extensions", "public") func TestResetRemote(t *testing.T) { dbConfig := pgconn.Config{ Host: "db.supabase.co", Port: 5432, User: "admin", Password: "password", Database: "postgres", } t.Run("resets remote database", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() path := filepath.Join(utils.MigrationsDir, "0_schema.sql") require.NoError(t, afero.WriteFile(fsys, path, nil, 0644)) // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.ListSchemas, escapedSchemas). Reply("SELECT 1", []interface{}{"private"}). Query("DROP SCHEMA IF EXISTS private CASCADE"). Reply("DROP SCHEMA"). Query(migration.DropObjects). Reply("INSERT 0") helper.MockMigrationHistory(conn). Query(migration.INSERT_MIGRATION_VERSION, "0", "schema", nil). Reply("INSERT 0 1") // Run test err := resetRemote(context.Background(), "", dbConfig, fsys, conn.Intercept) // Check error assert.NoError(t, err) }) t.Run("resets remote database with seed config disabled", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() path := filepath.Join(utils.MigrationsDir, "0_schema.sql") require.NoError(t, afero.WriteFile(fsys, path, nil, 0644)) seedPath := filepath.Join(utils.SupabaseDirPath, "seed.sql") // Will raise an error when seeding require.NoError(t, afero.WriteFile(fsys, seedPath, []byte("INSERT INTO test_table;"), 0644)) // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.ListSchemas, escapedSchemas). Reply("SELECT 1", []interface{}{"private"}). Query("DROP SCHEMA IF EXISTS private CASCADE"). Reply("DROP SCHEMA"). Query(migration.DropObjects). Reply("INSERT 0") helper.MockMigrationHistory(conn). Query(migration.INSERT_MIGRATION_VERSION, "0", "schema", nil). Reply("INSERT 0 1") utils.Config.Db.Seed.Enabled = false // Run test err := resetRemote(context.Background(), "", dbConfig, fsys, conn.Intercept) // No error should be raised since we're skipping the seed assert.NoError(t, err) }) t.Run("throws error on connect failure", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Run test err := resetRemote(context.Background(), "", pgconn.Config{}, fsys) // Check error assert.ErrorContains(t, err, "invalid port (outside range)") }) t.Run("throws error on drop schema failure", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.ListSchemas, escapedSchemas). Reply("SELECT 0"). Query(migration.DropObjects). ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation supabase_migrations") // Run test err := resetRemote(context.Background(), "", dbConfig, fsys, conn.Intercept) // Check error assert.ErrorContains(t, err, "ERROR: permission denied for relation supabase_migrations (SQLSTATE 42501)") }) }