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  }