github.com/supabase/cli@v1.168.1/internal/migration/squash/squash_test.go (about) 1 package squash 2 3 import ( 4 "bytes" 5 "context" 6 "embed" 7 "errors" 8 "fmt" 9 "net/http" 10 "os" 11 "path/filepath" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/docker/docker/api/types" 17 "github.com/jackc/pgconn" 18 "github.com/jackc/pgerrcode" 19 "github.com/jackc/pgx/v4" 20 "github.com/spf13/afero" 21 "github.com/stretchr/testify/assert" 22 "github.com/stretchr/testify/require" 23 "github.com/supabase/cli/internal/db/start" 24 "github.com/supabase/cli/internal/migration/history" 25 "github.com/supabase/cli/internal/migration/repair" 26 "github.com/supabase/cli/internal/testing/apitest" 27 "github.com/supabase/cli/internal/testing/fstest" 28 "github.com/supabase/cli/internal/testing/pgtest" 29 "github.com/supabase/cli/internal/utils" 30 "gopkg.in/h2non/gock.v1" 31 ) 32 33 var dbConfig = pgconn.Config{ 34 Host: "db.supabase.co", 35 Port: 5432, 36 User: "admin", 37 Password: "password", 38 Database: "postgres", 39 } 40 41 func TestSquashCommand(t *testing.T) { 42 t.Run("squashes local migrations", func(t *testing.T) { 43 // Setup in-memory fs 44 fsys := afero.NewMemMapFs() 45 require.NoError(t, utils.WriteConfig(fsys, false)) 46 paths := []string{ 47 filepath.Join(utils.MigrationsDir, "0_init.sql"), 48 filepath.Join(utils.MigrationsDir, "1_target.sql"), 49 } 50 sql := "create schema test" 51 require.NoError(t, afero.WriteFile(fsys, paths[0], []byte(sql), 0644)) 52 require.NoError(t, afero.WriteFile(fsys, paths[1], []byte{}, 0644)) 53 // Setup mock docker 54 require.NoError(t, apitest.MockDocker(utils.Docker)) 55 defer gock.OffAll() 56 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-shadow-db") 57 gock.New(utils.Docker.DaemonHost()). 58 Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json"). 59 Reply(http.StatusOK). 60 JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ 61 State: &types.ContainerState{ 62 Running: true, 63 Health: &types.Health{Status: "healthy"}, 64 }, 65 }}) 66 gock.New(utils.Docker.DaemonHost()). 67 Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). 68 Reply(http.StatusOK) 69 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.RealtimeImage), "test-realtime") 70 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-realtime", "")) 71 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.StorageImage), "test-storage") 72 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-storage", "")) 73 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.GotrueImage), "test-auth") 74 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-auth", "")) 75 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-db") 76 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-db", sql)) 77 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-db") 78 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-db", sql)) 79 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-db") 80 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-db", sql)) 81 // Setup mock postgres 82 conn := pgtest.NewConn() 83 defer conn.Close(t) 84 pgtest.MockMigrationHistory(conn) 85 conn.Query(sql). 86 Reply("CREATE SCHEMA"). 87 Query(history.INSERT_MIGRATION_VERSION, "0", "init", []string{sql}). 88 Reply("INSERT 0 1"). 89 Query(history.INSERT_MIGRATION_VERSION, "1", "target", nil). 90 Reply("INSERT 0 1") 91 // Run test 92 err := Run(context.Background(), "", pgconn.Config{ 93 Host: "127.0.0.1", 94 Port: 54322, 95 }, fsys, conn.Intercept) 96 // Check error 97 assert.NoError(t, err) 98 assert.Empty(t, apitest.ListUnmatchedRequests()) 99 exists, err := afero.Exists(fsys, paths[0]) 100 assert.NoError(t, err) 101 assert.False(t, exists) 102 match, err := afero.FileContainsBytes(fsys, paths[1], []byte(sql)) 103 assert.NoError(t, err) 104 assert.True(t, match) 105 }) 106 107 t.Run("baselines migration history", func(t *testing.T) { 108 // Setup in-memory fs 109 fsys := afero.NewMemMapFs() 110 require.NoError(t, utils.WriteConfig(fsys, false)) 111 path := filepath.Join(utils.MigrationsDir, "0_init.sql") 112 sql := "create schema test" 113 require.NoError(t, afero.WriteFile(fsys, path, []byte(sql), 0644)) 114 // Setup mock postgres 115 conn := pgtest.NewConn() 116 defer conn.Close(t) 117 pgtest.MockMigrationHistory(conn) 118 conn.Query(fmt.Sprintf("DELETE FROM supabase_migrations.schema_migrations WHERE version <= '0' ;INSERT INTO supabase_migrations.schema_migrations(version, name, statements) VALUES( '0' , 'init' , '{%s}' )", sql)). 119 Reply("INSERT 0 1") 120 // Run test 121 err := Run(context.Background(), "0", dbConfig, fsys, conn.Intercept, func(cc *pgx.ConnConfig) { 122 cc.PreferSimpleProtocol = true 123 }) 124 // Check error 125 assert.NoError(t, err) 126 match, err := afero.FileContainsBytes(fsys, path, []byte(sql)) 127 assert.NoError(t, err) 128 assert.True(t, match) 129 }) 130 131 t.Run("throws error on invalid version", func(t *testing.T) { 132 // Setup in-memory fs 133 fsys := afero.NewMemMapFs() 134 // Run test 135 err := Run(context.Background(), "0_init", pgconn.Config{}, fsys) 136 // Check error 137 assert.ErrorIs(t, err, repair.ErrInvalidVersion) 138 }) 139 140 t.Run("throws error on missing config", func(t *testing.T) { 141 // Setup in-memory fs 142 fsys := afero.NewMemMapFs() 143 // Run test 144 err := Run(context.Background(), "0", pgconn.Config{}, fsys) 145 // Check error 146 assert.ErrorIs(t, err, os.ErrNotExist) 147 }) 148 } 149 150 func TestSquashVersion(t *testing.T) { 151 t.Run("throws error on permission denied", func(t *testing.T) { 152 // Setup in-memory fs 153 fsys := &fstest.OpenErrorFs{DenyPath: utils.MigrationsDir} 154 // Run test 155 err := squashToVersion(context.Background(), "0", fsys) 156 // Check error 157 assert.ErrorIs(t, err, os.ErrPermission) 158 }) 159 160 t.Run("throws error on missing version", func(t *testing.T) { 161 // Setup in-memory fs 162 fsys := afero.NewMemMapFs() 163 // Run test 164 err := squashToVersion(context.Background(), "0", fsys) 165 // Check error 166 assert.ErrorIs(t, err, ErrMissingVersion) 167 }) 168 169 t.Run("throws error on shadow create failure", func(t *testing.T) { 170 // Setup in-memory fs 171 fsys := afero.NewMemMapFs() 172 path := filepath.Join(utils.MigrationsDir, "0_init.sql") 173 require.NoError(t, afero.WriteFile(fsys, path, []byte{}, 0644)) 174 path = filepath.Join(utils.MigrationsDir, "1_target.sql") 175 require.NoError(t, afero.WriteFile(fsys, path, []byte{}, 0644)) 176 // Setup mock docker 177 require.NoError(t, apitest.MockDocker(utils.Docker)) 178 defer gock.OffAll() 179 gock.New(utils.Docker.DaemonHost()). 180 Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Config.Db.Image) + "/json"). 181 ReplyError(errors.New("network error")) 182 // Run test 183 err := squashToVersion(context.Background(), "1", fsys) 184 // Check error 185 assert.ErrorContains(t, err, "network error") 186 assert.Empty(t, apitest.ListUnmatchedRequests()) 187 }) 188 } 189 190 func TestSquashMigrations(t *testing.T) { 191 utils.Config.Db.MajorVersion = 15 192 utils.Config.Db.Image = utils.Pg15Image 193 utils.Config.Db.ShadowPort = 54320 194 195 t.Run("throws error on shadow create failure", func(t *testing.T) { 196 // Setup in-memory fs 197 fsys := afero.NewMemMapFs() 198 // Setup mock docker 199 require.NoError(t, apitest.MockDocker(utils.Docker)) 200 defer gock.OffAll() 201 gock.New(utils.Docker.DaemonHost()). 202 Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Config.Db.Image) + "/json"). 203 ReplyError(errors.New("network error")) 204 // Run test 205 err := squashMigrations(context.Background(), nil, fsys) 206 // Check error 207 assert.ErrorContains(t, err, "network error") 208 assert.Empty(t, apitest.ListUnmatchedRequests()) 209 }) 210 211 t.Run("throws error on health check failure", func(t *testing.T) { 212 start.HealthTimeout = time.Millisecond 213 // Setup in-memory fs 214 fsys := afero.NewMemMapFs() 215 // Setup mock docker 216 require.NoError(t, apitest.MockDocker(utils.Docker)) 217 defer gock.OffAll() 218 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-shadow-db") 219 gock.New(utils.Docker.DaemonHost()). 220 Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json"). 221 Reply(http.StatusServiceUnavailable) 222 gock.New(utils.Docker.DaemonHost()). 223 Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). 224 Reply(http.StatusOK) 225 // Run test 226 err := squashMigrations(context.Background(), nil, fsys) 227 // Check error 228 assert.ErrorIs(t, err, start.ErrDatabase) 229 assert.Empty(t, apitest.ListUnmatchedRequests()) 230 }) 231 232 t.Run("throws error on shadow migrate failure", func(t *testing.T) { 233 // Setup in-memory fs 234 fsys := afero.NewMemMapFs() 235 // Setup mock docker 236 require.NoError(t, apitest.MockDocker(utils.Docker)) 237 defer gock.OffAll() 238 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-shadow-db") 239 gock.New(utils.Docker.DaemonHost()). 240 Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json"). 241 Reply(http.StatusOK). 242 JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ 243 State: &types.ContainerState{ 244 Running: true, 245 Health: &types.Health{Status: "healthy"}, 246 }, 247 }}) 248 gock.New(utils.Docker.DaemonHost()). 249 Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). 250 Reply(http.StatusOK) 251 gock.New(utils.Docker.DaemonHost()). 252 Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.RealtimeImage) + "/json"). 253 ReplyError(errors.New("network error")) 254 // Setup mock postgres 255 conn := pgtest.NewConn() 256 defer conn.Close(t) 257 // Run test 258 err := squashMigrations(context.Background(), nil, fsys, conn.Intercept) 259 // Check error 260 assert.ErrorContains(t, err, "network error") 261 assert.Empty(t, apitest.ListUnmatchedRequests()) 262 }) 263 264 t.Run("throws error on permission denied", func(t *testing.T) { 265 // Setup in-memory fs 266 fsys := afero.NewMemMapFs() 267 path := filepath.Join(utils.MigrationsDir, "0_init.sql") 268 sql := "create schema test" 269 require.NoError(t, afero.WriteFile(fsys, path, []byte(sql), 0644)) 270 // Setup mock docker 271 require.NoError(t, apitest.MockDocker(utils.Docker)) 272 defer gock.OffAll() 273 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-shadow-db") 274 gock.New(utils.Docker.DaemonHost()). 275 Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json"). 276 Reply(http.StatusOK). 277 JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ 278 State: &types.ContainerState{ 279 Running: true, 280 Health: &types.Health{Status: "healthy"}, 281 }, 282 }}) 283 gock.New(utils.Docker.DaemonHost()). 284 Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). 285 Reply(http.StatusOK) 286 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.RealtimeImage), "test-realtime") 287 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-realtime", "")) 288 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.StorageImage), "test-storage") 289 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-storage", "")) 290 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.GotrueImage), "test-auth") 291 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-auth", "")) 292 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-db") 293 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-db", sql)) 294 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-db") 295 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-db", sql)) 296 // Setup mock postgres 297 conn := pgtest.NewConn() 298 defer conn.Close(t) 299 pgtest.MockMigrationHistory(conn) 300 conn.Query(sql). 301 Reply("CREATE SCHEMA"). 302 Query(history.INSERT_MIGRATION_VERSION, "0", "init", []string{sql}). 303 Reply("INSERT 0 1") 304 // Run test 305 err := squashMigrations(context.Background(), []string{filepath.Base(path)}, afero.NewReadOnlyFs(fsys), conn.Intercept) 306 // Check error 307 assert.ErrorIs(t, err, os.ErrPermission) 308 assert.Empty(t, apitest.ListUnmatchedRequests()) 309 }) 310 } 311 312 func TestBaselineMigration(t *testing.T) { 313 t.Run("baselines earliest version", func(t *testing.T) { 314 // Setup in-memory fs 315 fsys := afero.NewMemMapFs() 316 paths := []string{ 317 filepath.Join(utils.MigrationsDir, "0_init.sql"), 318 filepath.Join(utils.MigrationsDir, "1_target.sql"), 319 } 320 sql := "create schema test" 321 require.NoError(t, afero.WriteFile(fsys, paths[0], []byte(sql), 0644)) 322 require.NoError(t, afero.WriteFile(fsys, paths[1], []byte{}, 0644)) 323 // Setup mock postgres 324 conn := pgtest.NewConn() 325 defer conn.Close(t) 326 pgtest.MockMigrationHistory(conn) 327 conn.Query(fmt.Sprintf("DELETE FROM supabase_migrations.schema_migrations WHERE version <= '0' ;INSERT INTO supabase_migrations.schema_migrations(version, name, statements) VALUES( '0' , 'init' , '{%s}' )", sql)). 328 Reply("INSERT 0 1") 329 // Run test 330 err := baselineMigrations(context.Background(), dbConfig, "", fsys, conn.Intercept, func(cc *pgx.ConnConfig) { 331 cc.PreferSimpleProtocol = true 332 }) 333 // Check error 334 assert.NoError(t, err) 335 }) 336 337 t.Run("throws error on connect failure", func(t *testing.T) { 338 // Setup in-memory fs 339 fsys := afero.NewMemMapFs() 340 // Run test 341 err := baselineMigrations(context.Background(), pgconn.Config{}, "0", fsys) 342 // Check error 343 assert.ErrorContains(t, err, "invalid port (outside range)") 344 }) 345 346 t.Run("throws error on query failure", func(t *testing.T) { 347 // Setup in-memory fs 348 fsys := afero.NewMemMapFs() 349 path := filepath.Join(utils.MigrationsDir, "0_init.sql") 350 require.NoError(t, afero.WriteFile(fsys, path, []byte(""), 0644)) 351 // Setup mock postgres 352 conn := pgtest.NewConn() 353 defer conn.Close(t) 354 pgtest.MockMigrationHistory(conn) 355 conn.Query(fmt.Sprintf("DELETE FROM supabase_migrations.schema_migrations WHERE version <= '%[1]s' ;INSERT INTO supabase_migrations.schema_migrations(version, name, statements) VALUES( '%[1]s' , 'init' , null )", "0")). 356 ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation supabase_migrations") 357 // Run test 358 err := baselineMigrations(context.Background(), dbConfig, "0", fsys, conn.Intercept, func(cc *pgx.ConnConfig) { 359 cc.PreferSimpleProtocol = true 360 }) 361 // Check error 362 assert.ErrorContains(t, err, `ERROR: permission denied for relation supabase_migrations (SQLSTATE 42501)`) 363 }) 364 365 t.Run("throws error on missing file", func(t *testing.T) { 366 // Setup in-memory fs 367 fsys := afero.NewMemMapFs() 368 // Setup mock postgres 369 conn := pgtest.NewConn() 370 defer conn.Close(t) 371 pgtest.MockMigrationHistory(conn) 372 // Run test 373 err := baselineMigrations(context.Background(), dbConfig, "0", fsys, conn.Intercept) 374 // Check error 375 assert.ErrorIs(t, err, os.ErrNotExist) 376 }) 377 } 378 379 //go:embed testdata/*.sql 380 var testdata embed.FS 381 382 func TestLineByLine(t *testing.T) { 383 t.Run("diffs output from pg_dump", func(t *testing.T) { 384 before, err := testdata.Open("testdata/before.sql") 385 require.NoError(t, err) 386 after, err := testdata.Open("testdata/after.sql") 387 require.NoError(t, err) 388 expected, err := testdata.ReadFile("testdata/diff.sql") 389 require.NoError(t, err) 390 // Run test 391 var out bytes.Buffer 392 err = lineByLineDiff(before, after, &out) 393 // Check error 394 assert.NoError(t, err) 395 assert.Equal(t, expected, out.Bytes()) 396 }) 397 398 t.Run("diffs shorter before", func(t *testing.T) { 399 before := strings.NewReader("select 1;") 400 after := strings.NewReader("select 0;\nselect 1;\nselect 2;") 401 // Run test 402 var out bytes.Buffer 403 err := lineByLineDiff(before, after, &out) 404 // Check error 405 assert.NoError(t, err) 406 assert.Equal(t, "select 0;\nselect 2;\n", out.String()) 407 }) 408 409 t.Run("diffs shorter after", func(t *testing.T) { 410 before := strings.NewReader("select 1;\nselect 2;") 411 after := strings.NewReader("select 1;") 412 // Run test 413 var out bytes.Buffer 414 err := lineByLineDiff(before, after, &out) 415 // Check error 416 assert.NoError(t, err) 417 assert.Equal(t, "", out.String()) 418 }) 419 420 t.Run("diffs no match", func(t *testing.T) { 421 before := strings.NewReader("select 0;\nselect 1;") 422 after := strings.NewReader("select 1;") 423 // Run test 424 var out bytes.Buffer 425 err := lineByLineDiff(before, after, &out) 426 // Check error 427 assert.NoError(t, err) 428 assert.Equal(t, "select 1;\n", out.String()) 429 }) 430 }