github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/internal/db/diff/migra_test.go (about) 1 package diff 2 3 import ( 4 "context" 5 "errors" 6 "io" 7 "net/http" 8 "os" 9 "path/filepath" 10 "strings" 11 "testing" 12 13 "github.com/docker/docker/api/types" 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/Redstoneguy129/cli/internal/testing/apitest" 19 "github.com/Redstoneguy129/cli/internal/testing/pgtest" 20 "github.com/Redstoneguy129/cli/internal/utils" 21 "github.com/Redstoneguy129/cli/internal/utils/parser" 22 "gopkg.in/h2non/gock.v1" 23 ) 24 25 func TestRunMigra(t *testing.T) { 26 t.Run("runs migra diff", func(t *testing.T) { 27 utils.GlobalsSql = "create schema public" 28 utils.InitialSchemaPg15Sql = "create schema private" 29 // Setup in-memory fs 30 fsys := afero.NewMemMapFs() 31 require.NoError(t, utils.WriteConfig(fsys, false)) 32 project := apitest.RandomProjectRef() 33 require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(project), 0644)) 34 // Setup mock docker 35 require.NoError(t, apitest.MockDocker(utils.Docker)) 36 defer gock.OffAll() 37 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-shadow-db") 38 gock.New(utils.Docker.DaemonHost()). 39 Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). 40 Reply(http.StatusOK) 41 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.MigraImage), "test-migra") 42 diff := "create table test();" 43 require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-migra", diff)) 44 // Setup mock postgres 45 conn := pgtest.NewConn() 46 defer conn.Close(t) 47 conn.Query(utils.GlobalsSql). 48 Reply("CREATE SCHEMA"). 49 Query(utils.InitialSchemaPg15Sql). 50 Reply("CREATE SCHEMA") 51 // Run test 52 err := RunMigra(context.Background(), []string{"public"}, "file", "password", fsys, conn.Intercept) 53 // Check error 54 assert.NoError(t, err) 55 assert.Empty(t, apitest.ListUnmatchedRequests()) 56 // Check diff file 57 files, err := afero.ReadDir(fsys, utils.MigrationsDir) 58 assert.NoError(t, err) 59 assert.Equal(t, 1, len(files)) 60 diffPath := filepath.Join(utils.MigrationsDir, files[0].Name()) 61 contents, err := afero.ReadFile(fsys, diffPath) 62 assert.NoError(t, err) 63 assert.Equal(t, []byte(diff), contents) 64 }) 65 66 t.Run("throws error on missing config", func(t *testing.T) { 67 // Setup in-memory fs 68 fsys := afero.NewMemMapFs() 69 // Run test 70 err := RunMigra(context.Background(), []string{"public"}, "", "", fsys) 71 // Check error 72 assert.ErrorIs(t, err, os.ErrNotExist) 73 }) 74 75 t.Run("throws error on missing project", func(t *testing.T) { 76 // Setup in-memory fs 77 fsys := afero.NewMemMapFs() 78 require.NoError(t, utils.WriteConfig(fsys, false)) 79 // Run test 80 err := RunMigra(context.Background(), []string{"public"}, "", "password", fsys) 81 // Check error 82 assert.ErrorContains(t, err, "Cannot find project ref. Have you run supabase link?") 83 }) 84 85 t.Run("throws error on missing database", func(t *testing.T) { 86 // Setup in-memory fs 87 fsys := afero.NewMemMapFs() 88 require.NoError(t, utils.WriteConfig(fsys, false)) 89 // Setup mock docker 90 require.NoError(t, apitest.MockDocker(utils.Docker)) 91 defer gock.OffAll() 92 gock.New(utils.Docker.DaemonHost()). 93 Get("/v" + utils.Docker.ClientVersion() + "/containers/supabase_db_"). 94 ReplyError(errors.New("network error")) 95 // Run test 96 err := RunMigra(context.Background(), []string{"public"}, "", "", fsys) 97 // Check error 98 assert.ErrorContains(t, err, "supabase start is not running.") 99 assert.Empty(t, apitest.ListUnmatchedRequests()) 100 }) 101 102 t.Run("throws error on failure to load user schemas", func(t *testing.T) { 103 // Setup in-memory fs 104 fsys := afero.NewMemMapFs() 105 require.NoError(t, utils.WriteConfig(fsys, false)) 106 project := apitest.RandomProjectRef() 107 require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(project), 0644)) 108 // Setup mock postgres 109 conn := pgtest.NewConn() 110 defer conn.Close(t) 111 conn.Query("SELECT schema_name FROM information_schema.schemata WHERE NOT schema_name = ANY('{pgbouncer,realtime,_realtime,supabase_functions,supabase_migrations,information_schema,pg_catalog,pg_toast,cron,graphql,graphql_public,net,pgsodium,pgsodium_masks,vault}') ORDER BY schema_name"). 112 ReplyError(pgerrcode.DuplicateTable, `relation "test" already exists`) 113 // Run test 114 err := RunMigra(context.Background(), []string{}, "", "password", fsys, conn.Intercept) 115 // Check error 116 assert.ErrorContains(t, err, `ERROR: relation "test" already exists (SQLSTATE 42P07)`) 117 }) 118 119 t.Run("throws error on failure to diff target", func(t *testing.T) { 120 // Setup in-memory fs 121 fsys := afero.NewMemMapFs() 122 require.NoError(t, utils.WriteConfig(fsys, false)) 123 project := apitest.RandomProjectRef() 124 require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(project), 0644)) 125 // Setup mock docker 126 require.NoError(t, apitest.MockDocker(utils.Docker)) 127 defer gock.OffAll() 128 gock.New(utils.Docker.DaemonHost()). 129 Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Pg15Image) + "/json"). 130 ReplyError(errors.New("network error")) 131 // Run test 132 err := RunMigra(context.Background(), []string{"public"}, "file", "password", fsys) 133 // Check error 134 assert.ErrorContains(t, err, "network error") 135 assert.Empty(t, apitest.ListUnmatchedRequests()) 136 }) 137 } 138 139 func TestBuildTarget(t *testing.T) { 140 t.Run("builds remote url", func(t *testing.T) { 141 // Setup in-memory fs 142 fsys := afero.NewMemMapFs() 143 project := apitest.RandomProjectRef() 144 require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(project), 0644)) 145 // Run test 146 url, err := buildTargetUrl("password", fsys) 147 // Check output 148 assert.NoError(t, err) 149 assert.Equal(t, "postgresql://postgres:password@db."+project+".supabase.co:6543/postgres", url) 150 }) 151 152 t.Run("builds local url", func(t *testing.T) { 153 utils.DbId = "postgres" 154 // Setup in-memory fs 155 fsys := afero.NewMemMapFs() 156 // Setup mock docker 157 require.NoError(t, apitest.MockDocker(utils.Docker)) 158 defer gock.OffAll() 159 gock.New(utils.Docker.DaemonHost()). 160 Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json"). 161 Reply(http.StatusOK). 162 JSON(types.ContainerJSON{}) 163 // Run test 164 url, err := buildTargetUrl("", fsys) 165 // Check output 166 assert.NoError(t, err) 167 assert.Equal(t, "postgresql://postgres:postgres@postgres:5432/postgres", url) 168 assert.Empty(t, apitest.ListUnmatchedRequests()) 169 }) 170 } 171 172 func TestApplyMigrations(t *testing.T) { 173 var postgresUrl = "postgresql://postgres:password@" + utils.Config.Hostname + ":5432/postgres" 174 175 t.Run("applies migrations from local directory", func(t *testing.T) { 176 // Setup in-memory fs 177 fsys := afero.NewMemMapFs() 178 // Setup initial migration 179 migrations := map[string]string{ 180 filepath.Join(utils.MigrationsDir, "20220727064247_init.sql"): "create table test", 181 filepath.Join(utils.MigrationsDir, "20220727064248_drop.sql"): "drop table test;\n-- ignore me", 182 } 183 for path, query := range migrations { 184 require.NoError(t, afero.WriteFile(fsys, path, []byte(query), 0644)) 185 } 186 // Setup mock postgres 187 conn := pgtest.NewConn() 188 defer conn.Close(t) 189 conn.Query("create table test"). 190 Reply("SELECT 0"). 191 Query("drop table test"). 192 Reply("SELECT 0"). 193 Query("-- ignore me"). 194 Reply("") 195 // Run test 196 assert.NoError(t, ApplyMigrations(context.Background(), postgresUrl, fsys, conn.Intercept)) 197 }) 198 199 t.Run("throws error on invalid postgres url", func(t *testing.T) { 200 assert.Error(t, ApplyMigrations(context.Background(), "invalid", afero.NewMemMapFs())) 201 }) 202 203 t.Run("throws error on failture to connect", func(t *testing.T) { 204 assert.Error(t, ApplyMigrations(context.Background(), postgresUrl, afero.NewMemMapFs())) 205 }) 206 207 t.Run("throws error on failture to send batch", func(t *testing.T) { 208 // Setup in-memory fs 209 fsys := afero.NewMemMapFs() 210 // Setup initial migration 211 name := "20220727064247_create_table.sql" 212 path := filepath.Join(utils.MigrationsDir, name) 213 query := "create table test" 214 require.NoError(t, afero.WriteFile(fsys, path, []byte(query), 0644)) 215 // Setup mock postgres 216 conn := pgtest.NewConn() 217 defer conn.Close(t) 218 conn.Query(query). 219 ReplyError(pgerrcode.DuplicateTable, `relation "test" already exists`) 220 // Run test 221 err := ApplyMigrations(context.Background(), postgresUrl, fsys, conn.Intercept) 222 // Check error 223 assert.ErrorContains(t, err, "ERROR: relation \"test\" already exists (SQLSTATE 42P07)\nAt statement 0: create table test") 224 }) 225 } 226 227 func TestMigrateDatabase(t *testing.T) { 228 t.Run("ignores empty local directory", func(t *testing.T) { 229 assert.NoError(t, MigrateDatabase(context.Background(), nil, afero.NewMemMapFs())) 230 }) 231 232 t.Run("ignores outdated migrations", func(t *testing.T) { 233 // Setup in-memory fs 234 fsys := afero.NewMemMapFs() 235 // Setup initial migration 236 name := "20211208000000_init.sql" 237 path := filepath.Join(utils.MigrationsDir, name) 238 query := "create table test" 239 require.NoError(t, afero.WriteFile(fsys, path, []byte(query), 0644)) 240 // Run test 241 err := MigrateDatabase(context.Background(), nil, fsys) 242 // Check error 243 assert.NoError(t, err) 244 }) 245 246 t.Run("throws error on failture to scan token", func(t *testing.T) { 247 // Setup in-memory fs 248 fsys := afero.NewMemMapFs() 249 // Setup initial migration 250 name := "20220727064247_create_table.sql" 251 path := filepath.Join(utils.MigrationsDir, name) 252 query := "BEGIN; " + strings.Repeat("a", parser.MaxScannerCapacity) 253 require.NoError(t, afero.WriteFile(fsys, path, []byte(query), 0644)) 254 // Run test 255 err := MigrateDatabase(context.Background(), nil, fsys) 256 // Check error 257 assert.ErrorContains(t, err, "bufio.Scanner: token too long\nAfter statement 1: BEGIN;") 258 }) 259 } 260 261 func TestMigrateShadow(t *testing.T) { 262 t.Run("throws error on timeout", func(t *testing.T) { 263 utils.Config.Db.ShadowPort = 54320 264 // Setup in-memory fs 265 fsys := afero.NewMemMapFs() 266 // Setup cancelled context 267 ctx, cancel := context.WithCancel(context.Background()) 268 cancel() 269 // Run test 270 err := MigrateShadowDatabase(ctx, fsys) 271 // Check error 272 assert.ErrorContains(t, err, "operation was canceled") 273 }) 274 275 t.Run("throws error on globals schema", func(t *testing.T) { 276 utils.Config.Db.ShadowPort = 54320 277 utils.GlobalsSql = "create schema public" 278 // Setup in-memory fs 279 fsys := afero.NewMemMapFs() 280 // Setup mock postgres 281 conn := pgtest.NewConn() 282 defer conn.Close(t) 283 conn.Query(utils.GlobalsSql). 284 ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`) 285 // Run test 286 err := MigrateShadowDatabase(context.Background(), fsys, conn.Intercept) 287 // Check error 288 assert.ErrorContains(t, err, `ERROR: schema "public" already exists (SQLSTATE 42P06)`) 289 }) 290 291 t.Run("throws error on initial schema", func(t *testing.T) { 292 utils.Config.Db.ShadowPort = 54320 293 utils.GlobalsSql = "create schema public" 294 utils.InitialSchemaSql = "create schema private" 295 // Setup in-memory fs 296 fsys := afero.NewMemMapFs() 297 // Setup mock postgres 298 conn := pgtest.NewConn() 299 defer conn.Close(t) 300 conn.Query(utils.GlobalsSql). 301 Reply("CREATE SCHEMA"). 302 Query(utils.InitialSchemaSql). 303 ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`) 304 // Run test 305 err := MigrateShadowDatabase(context.Background(), fsys, conn.Intercept) 306 // Check error 307 assert.ErrorContains(t, err, `ERROR: schema "public" already exists (SQLSTATE 42P06)`) 308 }) 309 } 310 311 func TestDiffDatabase(t *testing.T) { 312 utils.DbImage = utils.Pg15Image 313 utils.Config.Db.ShadowPort = 54320 314 utils.GlobalsSql = "create schema public" 315 utils.InitialSchemaSql = "create schema private" 316 317 t.Run("throws error on failure to create shadow", func(t *testing.T) { 318 // Setup in-memory fs 319 fsys := afero.NewMemMapFs() 320 // Setup mock docker 321 require.NoError(t, apitest.MockDocker(utils.Docker)) 322 defer gock.OffAll() 323 gock.New(utils.Docker.DaemonHost()). 324 Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Pg15Image) + "/json"). 325 ReplyError(errors.New("network error")) 326 // Run test 327 diff, err := DiffDatabase(context.Background(), []string{"public"}, "", io.Discard, fsys) 328 // Check error 329 assert.Empty(t, diff) 330 assert.ErrorContains(t, err, "network error") 331 assert.Empty(t, apitest.ListUnmatchedRequests()) 332 }) 333 334 t.Run("throws error on failure to migrate shadow", func(t *testing.T) { 335 // Setup in-memory fs 336 fsys := afero.NewMemMapFs() 337 // Setup mock docker 338 require.NoError(t, apitest.MockDocker(utils.Docker)) 339 defer gock.OffAll() 340 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-shadow-db") 341 gock.New(utils.Docker.DaemonHost()). 342 Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). 343 Reply(http.StatusOK) 344 // Setup mock postgres 345 conn := pgtest.NewConn() 346 defer conn.Close(t) 347 conn.Query(utils.GlobalsSql). 348 ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`) 349 // Run test 350 diff, err := DiffDatabase(context.Background(), []string{"public"}, "", io.Discard, fsys, conn.Intercept) 351 // Check error 352 assert.Empty(t, diff) 353 assert.ErrorContains(t, err, `ERROR: schema "public" already exists (SQLSTATE 42P06) 354 At statement 0: create schema public`) 355 assert.Empty(t, apitest.ListUnmatchedRequests()) 356 }) 357 358 t.Run("throws error on failure to diff target", func(t *testing.T) { 359 // Setup in-memory fs 360 fsys := afero.NewMemMapFs() 361 // Setup mock docker 362 require.NoError(t, apitest.MockDocker(utils.Docker)) 363 defer gock.OffAll() 364 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-shadow-db") 365 gock.New(utils.Docker.DaemonHost()). 366 Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). 367 Reply(http.StatusOK) 368 apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.MigraImage), "test-migra") 369 gock.New(utils.Docker.DaemonHost()). 370 Get("/v" + utils.Docker.ClientVersion() + "/containers/test-migra/logs"). 371 ReplyError(errors.New("network error")) 372 gock.New(utils.Docker.DaemonHost()). 373 Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-migra"). 374 Reply(http.StatusOK) 375 // Setup mock postgres 376 conn := pgtest.NewConn() 377 defer conn.Close(t) 378 conn.Query(utils.GlobalsSql). 379 Reply("CREATE SCHEMA"). 380 Query(utils.InitialSchemaSql). 381 Reply("CREATE SCHEMA") 382 // Run test 383 diff, err := DiffDatabase(context.Background(), []string{"public"}, "", io.Discard, fsys, conn.Intercept) 384 // Check error 385 assert.Empty(t, diff) 386 assert.ErrorContains(t, err, "error diffing schema") 387 assert.Empty(t, apitest.ListUnmatchedRequests()) 388 }) 389 } 390 391 func TestUserSchema(t *testing.T) { 392 // Setup mock postgres 393 conn := pgtest.NewConn() 394 defer conn.Close(t) 395 conn.Query(strings.ReplaceAll(LIST_SCHEMAS, "$1", "'{public}'")). 396 Reply("SELECT 1", []interface{}{"test"}) 397 // Connect to mock 398 ctx := context.Background() 399 mock, err := utils.ConnectRemotePostgres(ctx, "admin", "pass", "postgres", utils.Config.Hostname, conn.Intercept) 400 require.NoError(t, err) 401 defer mock.Close(ctx) 402 // Run test 403 schemas, err := LoadUserSchemas(ctx, mock, "public") 404 // Check error 405 assert.NoError(t, err) 406 assert.ElementsMatch(t, []string{"test"}, schemas) 407 }