github.com/supabase/cli@v1.168.1/internal/db/pull/pull_test.go (about)

     1  package pull
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"os"
     7  	"path/filepath"
     8  	"testing"
     9  
    10  	"github.com/jackc/pgconn"
    11  	"github.com/jackc/pgerrcode"
    12  	"github.com/spf13/afero"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  	"github.com/supabase/cli/internal/db/reset"
    16  	"github.com/supabase/cli/internal/migration/list"
    17  	"github.com/supabase/cli/internal/testing/apitest"
    18  	"github.com/supabase/cli/internal/testing/fstest"
    19  	"github.com/supabase/cli/internal/testing/pgtest"
    20  	"github.com/supabase/cli/internal/utils"
    21  	"gopkg.in/h2non/gock.v1"
    22  )
    23  
    24  var dbConfig = pgconn.Config{
    25  	Host:     "db.supabase.co",
    26  	Port:     5432,
    27  	User:     "admin",
    28  	Password: "password",
    29  	Database: "postgres",
    30  }
    31  
    32  var escapedSchemas = []string{
    33  	"pgbouncer",
    34  	"pgsodium",
    35  	"pgtle",
    36  	`supabase\_migrations`,
    37  	"vault",
    38  	`information\_schema`,
    39  	`pg\_%`,
    40  }
    41  
    42  func TestPullCommand(t *testing.T) {
    43  	t.Run("throws error on missing config", func(t *testing.T) {
    44  		// Setup in-memory fs
    45  		fsys := afero.NewMemMapFs()
    46  		// Run test
    47  		err := Run(context.Background(), nil, pgconn.Config{}, "", fsys)
    48  		// Check error
    49  		assert.ErrorIs(t, err, os.ErrNotExist)
    50  		assert.Empty(t, apitest.ListUnmatchedRequests())
    51  	})
    52  
    53  	t.Run("throws error on connect failure", func(t *testing.T) {
    54  		// Setup in-memory fs
    55  		fsys := afero.NewMemMapFs()
    56  		require.NoError(t, utils.WriteConfig(fsys, false))
    57  		// Run test
    58  		err := Run(context.Background(), nil, pgconn.Config{}, "", fsys)
    59  		// Check error
    60  		assert.ErrorContains(t, err, "invalid port (outside range)")
    61  		assert.Empty(t, apitest.ListUnmatchedRequests())
    62  	})
    63  
    64  	t.Run("throws error on sync failure", func(t *testing.T) {
    65  		// Setup in-memory fs
    66  		fsys := afero.NewMemMapFs()
    67  		require.NoError(t, utils.WriteConfig(fsys, false))
    68  		// Setup mock postgres
    69  		conn := pgtest.NewConn()
    70  		defer conn.Close(t)
    71  		conn.Query(list.LIST_MIGRATION_VERSION).
    72  			ReplyError(pgerrcode.InvalidCatalogName, `database "postgres" does not exist`)
    73  		// Run test
    74  		err := Run(context.Background(), nil, dbConfig, "", fsys, conn.Intercept)
    75  		// Check error
    76  		assert.ErrorContains(t, err, `ERROR: database "postgres" does not exist (SQLSTATE 3D000)`)
    77  		assert.Empty(t, apitest.ListUnmatchedRequests())
    78  	})
    79  }
    80  
    81  func TestPullSchema(t *testing.T) {
    82  	t.Run("dumps remote schema", func(t *testing.T) {
    83  		// Setup in-memory fs
    84  		fsys := afero.NewMemMapFs()
    85  		// Setup mock docker
    86  		require.NoError(t, apitest.MockDocker(utils.Docker))
    87  		defer gock.OffAll()
    88  		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-db")
    89  		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-db", "test"))
    90  		// Setup mock postgres
    91  		conn := pgtest.NewConn()
    92  		defer conn.Close(t)
    93  		conn.Query(list.LIST_MIGRATION_VERSION).
    94  			Reply("SELECT 0")
    95  		// Connect to mock
    96  		ctx := context.Background()
    97  		mock, err := utils.ConnectByConfig(ctx, dbConfig, conn.Intercept)
    98  		require.NoError(t, err)
    99  		defer mock.Close(ctx)
   100  		// Run test
   101  		err = utils.RunProgram(context.Background(), func(p utils.Program, ctx context.Context) error {
   102  			return run(p, ctx, nil, "0_test.sql", mock, fsys)
   103  		})
   104  		// Check error
   105  		assert.NoError(t, err)
   106  		assert.Empty(t, apitest.ListUnmatchedRequests())
   107  		contents, err := afero.ReadFile(fsys, "0_test.sql")
   108  		assert.NoError(t, err)
   109  		assert.Equal(t, []byte("test"), contents)
   110  	})
   111  
   112  	t.Run("throws error on load user schema failure", func(t *testing.T) {
   113  		// Setup in-memory fs
   114  		fsys := afero.NewMemMapFs()
   115  		path := filepath.Join(utils.MigrationsDir, "0_test.sql")
   116  		require.NoError(t, afero.WriteFile(fsys, path, []byte(""), 0644))
   117  		// Setup mock postgres
   118  		conn := pgtest.NewConn()
   119  		defer conn.Close(t)
   120  		conn.Query(list.LIST_MIGRATION_VERSION).
   121  			Reply("SELECT 1", []interface{}{"0"}).
   122  			Query(reset.ListSchemas, escapedSchemas).
   123  			ReplyError(pgerrcode.DuplicateTable, `relation "test" already exists`)
   124  		// Connect to mock
   125  		ctx := context.Background()
   126  		mock, err := utils.ConnectByConfig(ctx, dbConfig, conn.Intercept)
   127  		require.NoError(t, err)
   128  		defer mock.Close(ctx)
   129  		// Run test
   130  		err = utils.RunProgram(context.Background(), func(p utils.Program, ctx context.Context) error {
   131  			return run(p, ctx, nil, "", mock, fsys)
   132  		})
   133  		// Check error
   134  		assert.ErrorContains(t, err, `ERROR: relation "test" already exists (SQLSTATE 42P07)`)
   135  	})
   136  
   137  	t.Run("throws error on diff failure", func(t *testing.T) {
   138  		// Setup in-memory fs
   139  		fsys := afero.NewMemMapFs()
   140  		path := filepath.Join(utils.MigrationsDir, "0_test.sql")
   141  		require.NoError(t, afero.WriteFile(fsys, path, []byte(""), 0644))
   142  		// Setup mock docker
   143  		require.NoError(t, apitest.MockDocker(utils.Docker))
   144  		defer gock.OffAll()
   145  		gock.New(utils.Docker.DaemonHost()).
   146  			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Config.Db.Image) + "/json").
   147  			ReplyError(errors.New("network error"))
   148  		// Setup mock postgres
   149  		conn := pgtest.NewConn()
   150  		defer conn.Close(t)
   151  		conn.Query(list.LIST_MIGRATION_VERSION).
   152  			Reply("SELECT 1", []interface{}{"0"})
   153  		// Connect to mock
   154  		ctx := context.Background()
   155  		mock, err := utils.ConnectByConfig(ctx, dbConfig, conn.Intercept)
   156  		require.NoError(t, err)
   157  		defer mock.Close(ctx)
   158  		// Run test
   159  		err = utils.RunProgram(context.Background(), func(p utils.Program, ctx context.Context) error {
   160  			return run(p, ctx, []string{"public"}, "", mock, fsys)
   161  		})
   162  		// Check error
   163  		assert.ErrorContains(t, err, "network error")
   164  		assert.Empty(t, apitest.ListUnmatchedRequests())
   165  	})
   166  }
   167  
   168  func TestSyncRemote(t *testing.T) {
   169  	t.Run("throws error on permission denied", func(t *testing.T) {
   170  		// Setup in-memory fs
   171  		fsys := &fstest.OpenErrorFs{DenyPath: utils.MigrationsDir}
   172  		// Setup mock postgres
   173  		conn := pgtest.NewConn()
   174  		defer conn.Close(t)
   175  		conn.Query(list.LIST_MIGRATION_VERSION).
   176  			Reply("SELECT 0")
   177  		// Connect to mock
   178  		ctx := context.Background()
   179  		mock, err := utils.ConnectByConfig(ctx, dbConfig, conn.Intercept)
   180  		require.NoError(t, err)
   181  		defer mock.Close(ctx)
   182  		// Run test
   183  		err = assertRemoteInSync(ctx, mock, fsys)
   184  		// Check error
   185  		assert.ErrorIs(t, err, os.ErrPermission)
   186  		assert.Empty(t, apitest.ListUnmatchedRequests())
   187  	})
   188  
   189  	t.Run("throws error on mismatched length", func(t *testing.T) {
   190  		// Setup in-memory fs
   191  		fsys := afero.NewMemMapFs()
   192  		path := filepath.Join(utils.MigrationsDir, "0_test.sql")
   193  		require.NoError(t, afero.WriteFile(fsys, path, []byte(""), 0644))
   194  		// Setup mock postgres
   195  		conn := pgtest.NewConn()
   196  		defer conn.Close(t)
   197  		conn.Query(list.LIST_MIGRATION_VERSION).
   198  			Reply("SELECT 0")
   199  		// Connect to mock
   200  		ctx := context.Background()
   201  		mock, err := utils.ConnectByConfig(ctx, dbConfig, conn.Intercept)
   202  		require.NoError(t, err)
   203  		defer mock.Close(ctx)
   204  		// Run test
   205  		err = assertRemoteInSync(ctx, mock, fsys)
   206  		// Check error
   207  		assert.ErrorIs(t, err, errConflict)
   208  		assert.Empty(t, apitest.ListUnmatchedRequests())
   209  	})
   210  
   211  	t.Run("throws error on mismatched migration", func(t *testing.T) {
   212  		// Setup in-memory fs
   213  		fsys := afero.NewMemMapFs()
   214  		path := filepath.Join(utils.MigrationsDir, "0_test.sql")
   215  		require.NoError(t, afero.WriteFile(fsys, path, []byte(""), 0644))
   216  		// Setup mock postgres
   217  		conn := pgtest.NewConn()
   218  		defer conn.Close(t)
   219  		conn.Query(list.LIST_MIGRATION_VERSION).
   220  			Reply("SELECT 1", []interface{}{"20220727064247"})
   221  		// Connect to mock
   222  		ctx := context.Background()
   223  		mock, err := utils.ConnectByConfig(ctx, dbConfig, conn.Intercept)
   224  		require.NoError(t, err)
   225  		defer mock.Close(ctx)
   226  		// Run test
   227  		err = assertRemoteInSync(ctx, mock, fsys)
   228  		// Check error
   229  		assert.ErrorIs(t, err, errConflict)
   230  		assert.Empty(t, apitest.ListUnmatchedRequests())
   231  	})
   232  
   233  	t.Run("throws error on missing migration", func(t *testing.T) {
   234  		// Setup in-memory fs
   235  		fsys := afero.NewMemMapFs()
   236  		// Setup mock postgres
   237  		conn := pgtest.NewConn()
   238  		defer conn.Close(t)
   239  		conn.Query(list.LIST_MIGRATION_VERSION).
   240  			Reply("SELECT 0")
   241  		// Connect to mock
   242  		ctx := context.Background()
   243  		mock, err := utils.ConnectByConfig(ctx, dbConfig, conn.Intercept)
   244  		require.NoError(t, err)
   245  		defer mock.Close(ctx)
   246  		// Run test
   247  		err = assertRemoteInSync(ctx, mock, fsys)
   248  		// Check error
   249  		assert.ErrorIs(t, err, errMissing)
   250  		assert.Empty(t, apitest.ListUnmatchedRequests())
   251  	})
   252  }