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 }