supabase-cli/internal/db/reset/reset_test.go

495 lines
17 KiB
Go

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)")
})
}