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  }