github.com/supabase/cli@v1.168.1/internal/link/link_test.go (about)

     1  package link
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/jackc/pgconn"
    10  	"github.com/jackc/pgerrcode"
    11  	"github.com/jackc/pgx/v4"
    12  	"github.com/spf13/afero"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/supabase/cli/internal/migration/history"
    15  	"github.com/supabase/cli/internal/testing/apitest"
    16  	"github.com/supabase/cli/internal/testing/fstest"
    17  	"github.com/supabase/cli/internal/testing/pgtest"
    18  	"github.com/supabase/cli/internal/utils"
    19  	"github.com/supabase/cli/internal/utils/tenant"
    20  	"github.com/supabase/cli/pkg/api"
    21  	"github.com/zalando/go-keyring"
    22  	"gopkg.in/h2non/gock.v1"
    23  )
    24  
    25  var dbConfig = pgconn.Config{
    26  	Host:     "127.0.0.1",
    27  	Port:     5432,
    28  	User:     "admin",
    29  	Password: "password",
    30  	Database: "postgres",
    31  }
    32  
    33  // Reset global variable
    34  func teardown() {
    35  	updatedConfig.Api = nil
    36  	updatedConfig.Db = nil
    37  	updatedConfig.Pooler = nil
    38  }
    39  
    40  func TestPostRun(t *testing.T) {
    41  	t.Run("prints completion message", func(t *testing.T) {
    42  		defer teardown()
    43  		project := "test-project"
    44  		// Setup in-memory fs
    45  		fsys := afero.NewMemMapFs()
    46  		// Run test
    47  		buf := &strings.Builder{}
    48  		err := PostRun(project, buf, fsys)
    49  		// Check error
    50  		assert.NoError(t, err)
    51  		assert.Equal(t, "Finished supabase link.\n", buf.String())
    52  	})
    53  
    54  	t.Run("prints changed config", func(t *testing.T) {
    55  		defer teardown()
    56  		project := "test-project"
    57  		updatedConfig.Api = "test"
    58  		// Setup in-memory fs
    59  		fsys := afero.NewMemMapFs()
    60  		// Run test
    61  		buf := &strings.Builder{}
    62  		err := PostRun(project, buf, fsys)
    63  		// Check error
    64  		assert.NoError(t, err)
    65  		assert.Contains(t, buf.String(), `api = "test"`)
    66  	})
    67  }
    68  
    69  func TestLinkCommand(t *testing.T) {
    70  	project := "test-project"
    71  	// Setup valid access token
    72  	token := apitest.RandomAccessToken(t)
    73  	t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
    74  	// Mock credentials store
    75  	keyring.MockInit()
    76  
    77  	t.Run("link valid project", func(t *testing.T) {
    78  		defer teardown()
    79  		defer fstest.MockStdin(t, "\n")()
    80  		// Setup in-memory fs
    81  		fsys := afero.NewMemMapFs()
    82  		// Setup mock postgres
    83  		conn := pgtest.NewConn()
    84  		defer conn.Close(t)
    85  		pgtest.MockMigrationHistory(conn)
    86  		// Flush pending mocks after test execution
    87  		defer gock.OffAll()
    88  		gock.New(utils.DefaultApiHost).
    89  			Get("/v1/projects/" + project + "/api-keys").
    90  			Reply(200).
    91  			JSON([]api.ApiKeyResponse{{Name: "anon", ApiKey: "anon-key"}})
    92  		// Link configs
    93  		gock.New(utils.DefaultApiHost).
    94  			Get("/v1/projects/" + project + "/postgrest").
    95  			Reply(200).
    96  			JSON(api.V1PostgrestConfigResponse{})
    97  		gock.New(utils.DefaultApiHost).
    98  			Get("/v1/projects/" + project + "/config/database/pgbouncer").
    99  			Reply(200).
   100  			JSON(api.V1PgbouncerConfigResponse{})
   101  		// Link versions
   102  		auth := tenant.HealthResponse{Version: "v2.74.2"}
   103  		gock.New("https://" + utils.GetSupabaseHost(project)).
   104  			Get("/auth/v1/health").
   105  			Reply(200).
   106  			JSON(auth)
   107  		rest := tenant.SwaggerResponse{Info: tenant.SwaggerInfo{Version: "11.1.0"}}
   108  		gock.New("https://" + utils.GetSupabaseHost(project)).
   109  			Get("/rest/v1/").
   110  			Reply(200).
   111  			JSON(rest)
   112  		gock.New("https://" + utils.GetSupabaseHost(project)).
   113  			Get("/storage/v1/version").
   114  			Reply(200).
   115  			BodyString("0.40.4")
   116  		postgres := api.V1DatabaseResponse{
   117  			Host:    utils.GetSupabaseDbHost(project),
   118  			Version: "15.1.0.117",
   119  		}
   120  		gock.New(utils.DefaultApiHost).
   121  			Get("/v1/projects").
   122  			Reply(200).
   123  			JSON([]api.V1ProjectResponse{
   124  				{
   125  					Id:             project,
   126  					Database:       &postgres,
   127  					OrganizationId: "combined-fuchsia-lion",
   128  					Name:           "Test Project",
   129  					Region:         "us-west-1",
   130  					CreatedAt:      "2022-04-25T02:14:55.906498Z",
   131  				},
   132  			})
   133  		// Run test
   134  		err := Run(context.Background(), project, fsys, conn.Intercept)
   135  		// Check error
   136  		assert.NoError(t, err)
   137  		assert.Empty(t, apitest.ListUnmatchedRequests())
   138  		// Validate file contents
   139  		content, err := afero.ReadFile(fsys, utils.ProjectRefPath)
   140  		assert.NoError(t, err)
   141  		assert.Equal(t, []byte(project), content)
   142  		restVersion, err := afero.ReadFile(fsys, utils.RestVersionPath)
   143  		assert.NoError(t, err)
   144  		assert.Equal(t, []byte("v"+rest.Info.Version), restVersion)
   145  		authVersion, err := afero.ReadFile(fsys, utils.GotrueVersionPath)
   146  		assert.NoError(t, err)
   147  		assert.Equal(t, []byte(auth.Version), authVersion)
   148  		postgresVersion, err := afero.ReadFile(fsys, utils.PostgresVersionPath)
   149  		assert.NoError(t, err)
   150  		assert.Equal(t, []byte(postgres.Version), postgresVersion)
   151  	})
   152  
   153  	t.Run("ignores error linking services", func(t *testing.T) {
   154  		defer fstest.MockStdin(t, "\n")()
   155  		// Setup in-memory fs
   156  		fsys := afero.NewMemMapFs()
   157  		// Flush pending mocks after test execution
   158  		defer gock.OffAll()
   159  		gock.New(utils.DefaultApiHost).
   160  			Get("/v1/projects/" + project + "/api-keys").
   161  			Reply(200).
   162  			JSON([]api.ApiKeyResponse{{Name: "anon", ApiKey: "anon-key"}})
   163  		// Link configs
   164  		gock.New(utils.DefaultApiHost).
   165  			Get("/v1/projects/" + project + "/postgrest").
   166  			ReplyError(errors.New("network error"))
   167  		gock.New(utils.DefaultApiHost).
   168  			Get("/v1/projects/" + project + "/config/database/pgbouncer").
   169  			ReplyError(errors.New("network error"))
   170  		// Link versions
   171  		gock.New("https://" + utils.GetSupabaseHost(project)).
   172  			Get("/auth/v1/health").
   173  			ReplyError(errors.New("network error"))
   174  		gock.New("https://" + utils.GetSupabaseHost(project)).
   175  			Get("/rest/v1/").
   176  			ReplyError(errors.New("network error"))
   177  		gock.New("https://" + utils.GetSupabaseHost(project)).
   178  			Get("/storage/v1/version").
   179  			ReplyError(errors.New("network error"))
   180  		gock.New(utils.DefaultApiHost).
   181  			Get("/v1/projects").
   182  			ReplyError(errors.New("network error"))
   183  		// Run test
   184  		err := Run(context.Background(), project, fsys, func(cc *pgx.ConnConfig) {
   185  			cc.LookupFunc = func(ctx context.Context, host string) (addrs []string, err error) {
   186  				return nil, errors.New("hostname resolving error")
   187  			}
   188  		})
   189  		// Check error
   190  		assert.ErrorContains(t, err, "hostname resolving error")
   191  		assert.Empty(t, apitest.ListUnmatchedRequests())
   192  	})
   193  
   194  	t.Run("throws error on write failure", func(t *testing.T) {
   195  		defer teardown()
   196  		// Setup in-memory fs
   197  		fsys := afero.NewReadOnlyFs(afero.NewMemMapFs())
   198  		// Flush pending mocks after test execution
   199  		defer gock.OffAll()
   200  		gock.New(utils.DefaultApiHost).
   201  			Get("/v1/projects/" + project + "/api-keys").
   202  			Reply(200).
   203  			JSON([]api.ApiKeyResponse{{Name: "anon", ApiKey: "anon-key"}})
   204  		// Link configs
   205  		gock.New(utils.DefaultApiHost).
   206  			Get("/v1/projects/" + project + "/postgrest").
   207  			ReplyError(errors.New("network error"))
   208  		gock.New(utils.DefaultApiHost).
   209  			Get("/v1/projects/" + project + "/config/database/pgbouncer").
   210  			ReplyError(errors.New("network error"))
   211  		// Link versions
   212  		gock.New("https://" + utils.GetSupabaseHost(project)).
   213  			Get("/auth/v1/health").
   214  			ReplyError(errors.New("network error"))
   215  		gock.New("https://" + utils.GetSupabaseHost(project)).
   216  			Get("/rest/v1/").
   217  			ReplyError(errors.New("network error"))
   218  		gock.New("https://" + utils.GetSupabaseHost(project)).
   219  			Get("/storage/v1/version").
   220  			ReplyError(errors.New("network error"))
   221  		gock.New(utils.DefaultApiHost).
   222  			Get("/v1/projects").
   223  			ReplyError(errors.New("network error"))
   224  		// Run test
   225  		err := Run(context.Background(), project, fsys)
   226  		// Check error
   227  		assert.ErrorContains(t, err, "operation not permitted")
   228  		assert.Empty(t, apitest.ListUnmatchedRequests())
   229  		// Validate file contents
   230  		exists, err := afero.Exists(fsys, utils.ProjectRefPath)
   231  		assert.NoError(t, err)
   232  		assert.False(t, exists)
   233  	})
   234  }
   235  
   236  func TestLinkPostgrest(t *testing.T) {
   237  	project := "test-project"
   238  	// Setup valid access token
   239  	token := apitest.RandomAccessToken(t)
   240  	t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
   241  
   242  	t.Run("ignores matching config", func(t *testing.T) {
   243  		defer teardown()
   244  		// Flush pending mocks after test execution
   245  		defer gock.OffAll()
   246  		gock.New(utils.DefaultApiHost).
   247  			Get("/v1/projects/" + project + "/postgrest").
   248  			Reply(200).
   249  			JSON(api.V1PostgrestConfigResponse{})
   250  		// Run test
   251  		err := linkPostgrest(context.Background(), project)
   252  		// Check error
   253  		assert.NoError(t, err)
   254  		assert.Empty(t, apitest.ListUnmatchedRequests())
   255  		assert.Empty(t, updatedConfig)
   256  	})
   257  
   258  	t.Run("updates api on newer config", func(t *testing.T) {
   259  		defer teardown()
   260  		// Flush pending mocks after test execution
   261  		defer gock.OffAll()
   262  		gock.New(utils.DefaultApiHost).
   263  			Get("/v1/projects/" + project + "/postgrest").
   264  			Reply(200).
   265  			JSON(api.V1PostgrestConfigResponse{
   266  				DbSchema:          "public, graphql_public",
   267  				DbExtraSearchPath: "public, extensions",
   268  				MaxRows:           1000,
   269  			})
   270  		// Run test
   271  		err := linkPostgrest(context.Background(), project)
   272  		// Check error
   273  		assert.NoError(t, err)
   274  		assert.Empty(t, apitest.ListUnmatchedRequests())
   275  		utils.Config.Api.Schemas = []string{"public", "graphql_public"}
   276  		utils.Config.Api.ExtraSearchPath = []string{"public", "extensions"}
   277  		utils.Config.Api.MaxRows = 1000
   278  		assert.Equal(t, ConfigCopy{
   279  			Api: utils.Config.Api,
   280  		}, updatedConfig)
   281  	})
   282  
   283  	t.Run("throws error on network failure", func(t *testing.T) {
   284  		defer teardown()
   285  		// Flush pending mocks after test execution
   286  		defer gock.OffAll()
   287  		gock.New(utils.DefaultApiHost).
   288  			Get("/v1/projects/" + project + "/postgrest").
   289  			ReplyError(errors.New("network error"))
   290  		// Run test
   291  		err := linkPostgrest(context.Background(), project)
   292  		// Validate api
   293  		assert.ErrorContains(t, err, "network error")
   294  		assert.Empty(t, apitest.ListUnmatchedRequests())
   295  	})
   296  
   297  	t.Run("throws error on server unavailable", func(t *testing.T) {
   298  		defer teardown()
   299  		// Flush pending mocks after test execution
   300  		defer gock.OffAll()
   301  		gock.New(utils.DefaultApiHost).
   302  			Get("/v1/projects/" + project + "/postgrest").
   303  			Reply(500).
   304  			JSON(map[string]string{"message": "unavailable"})
   305  		// Run test
   306  		err := linkPostgrest(context.Background(), project)
   307  		// Validate api
   308  		assert.ErrorIs(t, err, tenant.ErrAuthToken)
   309  		assert.Empty(t, apitest.ListUnmatchedRequests())
   310  	})
   311  }
   312  
   313  func TestLinkDatabase(t *testing.T) {
   314  	t.Run("throws error on connect failure", func(t *testing.T) {
   315  		defer teardown()
   316  		// Run test
   317  		err := linkDatabase(context.Background(), pgconn.Config{})
   318  		// Check error
   319  		assert.ErrorContains(t, err, "invalid port (outside range)")
   320  		assert.Empty(t, updatedConfig)
   321  	})
   322  
   323  	t.Run("ignores missing server version", func(t *testing.T) {
   324  		defer teardown()
   325  		// Setup mock postgres
   326  		conn := pgtest.NewWithStatus(map[string]string{
   327  			"standard_conforming_strings": "on",
   328  		})
   329  		defer conn.Close(t)
   330  		pgtest.MockMigrationHistory(conn)
   331  		// Run test
   332  		err := linkDatabase(context.Background(), dbConfig, conn.Intercept)
   333  		// Check error
   334  		assert.NoError(t, err)
   335  		assert.Empty(t, updatedConfig)
   336  	})
   337  
   338  	t.Run("updates config to newer db version", func(t *testing.T) {
   339  		defer teardown()
   340  		utils.Config.Db.MajorVersion = 14
   341  		// Setup mock postgres
   342  		conn := pgtest.NewWithStatus(map[string]string{
   343  			"standard_conforming_strings": "on",
   344  			"server_version":              "15.0",
   345  		})
   346  		defer conn.Close(t)
   347  		pgtest.MockMigrationHistory(conn)
   348  		// Run test
   349  		err := linkDatabase(context.Background(), dbConfig, conn.Intercept)
   350  		// Check error
   351  		assert.NoError(t, err)
   352  		utils.Config.Db.MajorVersion = 15
   353  		assert.Equal(t, ConfigCopy{
   354  			Db: utils.Config.Db,
   355  		}, updatedConfig)
   356  	})
   357  
   358  	t.Run("throws error on query failure", func(t *testing.T) {
   359  		defer teardown()
   360  		utils.Config.Db.MajorVersion = 14
   361  		// Setup mock postgres
   362  		conn := pgtest.NewConn()
   363  		defer conn.Close(t)
   364  		conn.Query(history.SET_LOCK_TIMEOUT).
   365  			Query(history.CREATE_VERSION_SCHEMA).
   366  			Reply("CREATE SCHEMA").
   367  			Query(history.CREATE_VERSION_TABLE).
   368  			ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation supabase_migrations").
   369  			Query(history.ADD_STATEMENTS_COLUMN).
   370  			Query(history.ADD_NAME_COLUMN)
   371  		// Run test
   372  		err := linkDatabase(context.Background(), dbConfig, conn.Intercept)
   373  		// Check error
   374  		assert.ErrorContains(t, err, "ERROR: permission denied for relation supabase_migrations (SQLSTATE 42501)")
   375  	})
   376  }