github.com/supabase/cli@v1.168.1/internal/db/diff/diff_test.go (about) 1 package diff 2 3 import ( 4 "context" 5 "errors" 6 "io" 7 "net/http" 8 "os" 9 "path/filepath" 10 "testing" 11 "time" 12 13 "github.com/docker/docker/api/types" 14 "github.com/jackc/pgconn" 15 "github.com/jackc/pgerrcode" 16 "github.com/spf13/afero" 17 "github.com/stretchr/testify/assert" 18 "github.com/stretchr/testify/require" 19 "github.com/supabase/cli/internal/db/reset" 20 "github.com/supabase/cli/internal/db/start" 21 "github.com/supabase/cli/internal/migration/history" 22 "github.com/supabase/cli/internal/testing/apitest" 23 "github.com/supabase/cli/internal/testing/fstest" 24 "github.com/supabase/cli/internal/testing/pgtest" 25 "github.com/supabase/cli/internal/utils" 26 "gopkg.in/h2non/gock.v1" 27 ) 28 29 var dbConfig = pgconn.Config{ 30 Host: "db.supabase.co", 31 Port: 5432, 32 User: "admin", 33 Password: "password", 34 Database: "postgres", 35 } 36 37 var escapedSchemas = []string{ 38 "pgbouncer", 39 "pgsodium", 40 "pgtle", 41 `supabase\_migrations`, 42 "vault", 43 `information\_schema`, 44 `pg\_%`, 45 } 46 47 func TestRun(t *testing.T) { 48 t.Run("runs migra diff", func(t *testing.T) { 49 // Setup in-memory fs 50 fsys := afero.NewMemMapFs() 51 require.NoError(t, utils.WriteConfig(fsys, false)) 52 project := apitest.RandomProjectRef() 53 require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(project), 0644)) 54 // Setup mock docker 55 require.NoError(t, apitest.MockDocker(utils.Docker)) 56 defer gock.OffAll() 57 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-shadow-db") 58 gock.New(utils.Docker.DaemonHost()). 59 Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). 60 Reply(http.StatusOK) 61 gock.New(utils.Docker.DaemonHost()). 62 Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json"). 63 Reply(http.StatusOK). 64 JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ 65 State: &types.ContainerState{ 66 Running: true, 67 Health: &types.Health{Status: "healthy"}, 68 }, 69 }}) 70 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.RealtimeImage), "test-shadow-realtime") 71 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-shadow-realtime", "")) 72 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.StorageImage), "test-shadow-storage") 73 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-shadow-storage", "")) 74 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.GotrueImage), "test-shadow-auth") 75 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-shadow-auth", "")) 76 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.MigraImage), "test-migra") 77 diff := "create table test();" 78 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-migra", diff)) 79 // Setup mock postgres 80 conn := pgtest.NewConn() 81 defer conn.Close(t) 82 // Run test 83 err := Run(context.Background(), []string{"public"}, "file", dbConfig, DiffSchemaMigra, fsys, conn.Intercept) 84 // Check error 85 assert.NoError(t, err) 86 assert.Empty(t, apitest.ListUnmatchedRequests()) 87 // Check diff file 88 files, err := afero.ReadDir(fsys, utils.MigrationsDir) 89 assert.NoError(t, err) 90 assert.Equal(t, 1, len(files)) 91 diffPath := filepath.Join(utils.MigrationsDir, files[0].Name()) 92 contents, err := afero.ReadFile(fsys, diffPath) 93 assert.NoError(t, err) 94 assert.Equal(t, []byte(diff), contents) 95 }) 96 97 t.Run("throws error on missing config", func(t *testing.T) { 98 // Setup in-memory fs 99 fsys := afero.NewMemMapFs() 100 // Run test 101 err := Run(context.Background(), []string{"public"}, "", pgconn.Config{}, DiffSchemaMigra, fsys) 102 // Check error 103 assert.ErrorIs(t, err, os.ErrNotExist) 104 }) 105 106 t.Run("throws error on failure to load user schemas", func(t *testing.T) { 107 // Setup in-memory fs 108 fsys := afero.NewMemMapFs() 109 require.NoError(t, utils.WriteConfig(fsys, false)) 110 project := apitest.RandomProjectRef() 111 require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(project), 0644)) 112 // Setup mock postgres 113 conn := pgtest.NewConn() 114 defer conn.Close(t) 115 conn.Query(reset.ListSchemas, escapedSchemas). 116 ReplyError(pgerrcode.DuplicateTable, `relation "test" already exists`) 117 // Run test 118 err := Run(context.Background(), []string{}, "", dbConfig, DiffSchemaMigra, fsys, conn.Intercept) 119 // Check error 120 assert.ErrorContains(t, err, `ERROR: relation "test" already exists (SQLSTATE 42P07)`) 121 }) 122 123 t.Run("throws error on failure to diff target", func(t *testing.T) { 124 // Setup in-memory fs 125 fsys := afero.NewMemMapFs() 126 require.NoError(t, utils.WriteConfig(fsys, false)) 127 project := apitest.RandomProjectRef() 128 require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(project), 0644)) 129 // Setup mock docker 130 require.NoError(t, apitest.MockDocker(utils.Docker)) 131 defer gock.OffAll() 132 gock.New(utils.Docker.DaemonHost()). 133 Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Pg15Image) + "/json"). 134 ReplyError(errors.New("network error")) 135 // Run test 136 err := Run(context.Background(), []string{"public"}, "file", dbConfig, DiffSchemaMigra, fsys) 137 // Check error 138 assert.ErrorContains(t, err, "network error") 139 assert.Empty(t, apitest.ListUnmatchedRequests()) 140 }) 141 } 142 143 func TestMigrateShadow(t *testing.T) { 144 utils.Config.Db.MajorVersion = 14 145 146 t.Run("migrates shadow database", func(t *testing.T) { 147 utils.Config.Db.ShadowPort = 54320 148 utils.GlobalsSql = "create schema public" 149 utils.InitialSchemaSql = "create schema private" 150 // Setup in-memory fs 151 fsys := afero.NewMemMapFs() 152 path := filepath.Join(utils.MigrationsDir, "0_test.sql") 153 sql := "create schema test" 154 require.NoError(t, afero.WriteFile(fsys, path, []byte(sql), 0644)) 155 // Setup mock postgres 156 conn := pgtest.NewConn() 157 defer conn.Close(t) 158 conn.Query(utils.GlobalsSql). 159 Reply("CREATE SCHEMA"). 160 Query(utils.InitialSchemaSql). 161 Reply("CREATE SCHEMA") 162 pgtest.MockMigrationHistory(conn) 163 conn.Query(sql). 164 Reply("CREATE SCHEMA"). 165 Query(history.INSERT_MIGRATION_VERSION, "0", "test", []string{sql}). 166 Reply("INSERT 0 1") 167 // Run test 168 err := MigrateShadowDatabase(context.Background(), "test-shadow-db", fsys, conn.Intercept) 169 // Check error 170 assert.NoError(t, err) 171 }) 172 173 t.Run("throws error on timeout", func(t *testing.T) { 174 utils.Config.Db.ShadowPort = 54320 175 // Setup in-memory fs 176 fsys := afero.NewMemMapFs() 177 // Setup cancelled context 178 ctx, cancel := context.WithCancel(context.Background()) 179 cancel() 180 // Run test 181 err := MigrateShadowDatabase(ctx, "", fsys) 182 // Check error 183 assert.ErrorIs(t, err, context.Canceled) 184 }) 185 186 t.Run("throws error on permission denied", func(t *testing.T) { 187 // Setup in-memory fs 188 fsys := &fstest.OpenErrorFs{DenyPath: utils.MigrationsDir} 189 // Run test 190 err := MigrateShadowDatabase(context.Background(), "", fsys) 191 // Check error 192 assert.ErrorIs(t, err, os.ErrPermission) 193 }) 194 195 t.Run("throws error on globals schema", func(t *testing.T) { 196 utils.Config.Db.ShadowPort = 54320 197 utils.GlobalsSql = "create schema public" 198 // Setup in-memory fs 199 fsys := afero.NewMemMapFs() 200 // Setup mock postgres 201 conn := pgtest.NewConn() 202 defer conn.Close(t) 203 conn.Query(utils.GlobalsSql). 204 ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`) 205 // Run test 206 err := MigrateShadowDatabase(context.Background(), "test-shadow-db", fsys, conn.Intercept) 207 // Check error 208 assert.ErrorContains(t, err, `ERROR: schema "public" already exists (SQLSTATE 42P06)`) 209 }) 210 } 211 212 func TestDiffDatabase(t *testing.T) { 213 utils.Config.Db.MajorVersion = 14 214 utils.Config.Db.Image = utils.Pg14Image 215 utils.Config.Db.ShadowPort = 54320 216 utils.GlobalsSql = "create schema public" 217 utils.InitialSchemaSql = "create schema private" 218 219 t.Run("throws error on failure to create shadow", func(t *testing.T) { 220 // Setup in-memory fs 221 fsys := afero.NewMemMapFs() 222 // Setup mock docker 223 require.NoError(t, apitest.MockDocker(utils.Docker)) 224 defer gock.OffAll() 225 gock.New(utils.Docker.DaemonHost()). 226 Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Pg14Image) + "/json"). 227 ReplyError(errors.New("network error")) 228 // Run test 229 diff, err := DiffDatabase(context.Background(), []string{"public"}, dbConfig, io.Discard, fsys, DiffSchemaMigra) 230 // Check error 231 assert.Empty(t, diff) 232 assert.ErrorContains(t, err, "network error") 233 assert.Empty(t, apitest.ListUnmatchedRequests()) 234 }) 235 236 t.Run("throws error on health check failure", func(t *testing.T) { 237 start.HealthTimeout = time.Millisecond 238 // Setup in-memory fs 239 fsys := afero.NewMemMapFs() 240 // Setup mock docker 241 require.NoError(t, apitest.MockDocker(utils.Docker)) 242 defer gock.OffAll() 243 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg14Image), "test-shadow-db") 244 gock.New(utils.Docker.DaemonHost()). 245 Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json"). 246 Reply(http.StatusServiceUnavailable) 247 gock.New(utils.Docker.DaemonHost()). 248 Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). 249 Reply(http.StatusOK) 250 // Run test 251 diff, err := DiffDatabase(context.Background(), []string{"public"}, dbConfig, io.Discard, fsys, DiffSchemaMigra) 252 // Check error 253 assert.Empty(t, diff) 254 assert.ErrorIs(t, err, start.ErrDatabase) 255 assert.Empty(t, apitest.ListUnmatchedRequests()) 256 }) 257 258 t.Run("throws error on failure to migrate shadow", func(t *testing.T) { 259 // Setup in-memory fs 260 fsys := afero.NewMemMapFs() 261 // Setup mock docker 262 require.NoError(t, apitest.MockDocker(utils.Docker)) 263 defer gock.OffAll() 264 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg14Image), "test-shadow-db") 265 gock.New(utils.Docker.DaemonHost()). 266 Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json"). 267 Reply(http.StatusOK). 268 JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ 269 State: &types.ContainerState{ 270 Running: true, 271 Health: &types.Health{Status: "healthy"}, 272 }, 273 }}) 274 gock.New(utils.Docker.DaemonHost()). 275 Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). 276 Reply(http.StatusOK) 277 // Setup mock postgres 278 conn := pgtest.NewConn() 279 defer conn.Close(t) 280 conn.Query(utils.GlobalsSql). 281 ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`) 282 // Run test 283 diff, err := DiffDatabase(context.Background(), []string{"public"}, dbConfig, io.Discard, fsys, DiffSchemaMigra, conn.Intercept) 284 // Check error 285 assert.Empty(t, diff) 286 assert.ErrorContains(t, err, `ERROR: schema "public" already exists (SQLSTATE 42P06) 287 At statement 0: create schema public`) 288 assert.Empty(t, apitest.ListUnmatchedRequests()) 289 }) 290 291 t.Run("throws error on failure to diff target", func(t *testing.T) { 292 // Setup in-memory fs 293 fsys := afero.NewMemMapFs() 294 path := filepath.Join(utils.MigrationsDir, "0_test.sql") 295 sql := "create schema test" 296 require.NoError(t, afero.WriteFile(fsys, path, []byte(sql), 0644)) 297 // Setup mock docker 298 require.NoError(t, apitest.MockDocker(utils.Docker)) 299 defer gock.OffAll() 300 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg14Image), "test-shadow-db") 301 gock.New(utils.Docker.DaemonHost()). 302 Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json"). 303 Reply(http.StatusOK). 304 JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ 305 State: &types.ContainerState{ 306 Running: true, 307 Health: &types.Health{Status: "healthy"}, 308 }, 309 }}) 310 gock.New(utils.Docker.DaemonHost()). 311 Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). 312 Reply(http.StatusOK) 313 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.MigraImage), "test-migra") 314 gock.New(utils.Docker.DaemonHost()). 315 Get("/v" + utils.Docker.ClientVersion() + "/containers/test-migra/logs"). 316 ReplyError(errors.New("network error")) 317 gock.New(utils.Docker.DaemonHost()). 318 Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-migra"). 319 Reply(http.StatusOK) 320 // Setup mock postgres 321 conn := pgtest.NewConn() 322 defer conn.Close(t) 323 conn.Query(utils.GlobalsSql). 324 Reply("CREATE SCHEMA"). 325 Query(utils.InitialSchemaSql). 326 Reply("CREATE SCHEMA") 327 pgtest.MockMigrationHistory(conn) 328 conn.Query(sql). 329 Reply("CREATE SCHEMA"). 330 Query(history.INSERT_MIGRATION_VERSION, "0", "test", []string{sql}). 331 Reply("INSERT 0 1") 332 // Run test 333 diff, err := DiffDatabase(context.Background(), []string{"public"}, dbConfig, io.Discard, fsys, DiffSchemaMigra, conn.Intercept) 334 // Check error 335 assert.Empty(t, diff) 336 assert.ErrorContains(t, err, "error diffing schema") 337 assert.Empty(t, apitest.ListUnmatchedRequests()) 338 }) 339 } 340 341 func TestDropStatements(t *testing.T) { 342 drops := findDropStatements("create table t(); drop table t; alter table t drop column c") 343 assert.Equal(t, []string{"drop table t", "alter table t drop column c"}, drops) 344 }