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  }