github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/internal/link/link_test.go (about)

     1  package link
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"os"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/Redstoneguy129/cli/internal/migration/repair"
    11  	"github.com/Redstoneguy129/cli/internal/testing/apitest"
    12  	"github.com/Redstoneguy129/cli/internal/testing/pgtest"
    13  	"github.com/Redstoneguy129/cli/internal/utils"
    14  	"github.com/Redstoneguy129/cli/pkg/api"
    15  	"github.com/jackc/pgerrcode"
    16  	"github.com/jackc/pgx/v4"
    17  	"github.com/spf13/afero"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  	"github.com/zalando/go-keyring"
    21  	"gopkg.in/h2non/gock.v1"
    22  )
    23  
    24  var (
    25  	username = "admin"
    26  	password = "password"
    27  	database = "postgres"
    28  	host     = utils.Config.Hostname
    29  )
    30  
    31  func TestPreRun(t *testing.T) {
    32  	// Reset global variable
    33  	copy := utils.Config
    34  	t.Cleanup(func() {
    35  		utils.Config = copy
    36  	})
    37  
    38  	t.Run("passes sanity check", func(t *testing.T) {
    39  		project := apitest.RandomProjectRef()
    40  		// Setup in-memory fs
    41  		fsys := afero.NewMemMapFs()
    42  		require.NoError(t, utils.WriteConfig(fsys, false))
    43  		// Run test
    44  		err := PreRun(project, fsys)
    45  		// Check error
    46  		assert.NoError(t, err)
    47  	})
    48  
    49  	t.Run("throws error on invalid project ref", func(t *testing.T) {
    50  		// Setup in-memory fs
    51  		fsys := afero.NewMemMapFs()
    52  		// Run test
    53  		err := PreRun("malformed", fsys)
    54  		// Check error
    55  		assert.ErrorContains(t, err, "Invalid project ref format. Must be like `abcdefghijklmnopqrst`.")
    56  	})
    57  
    58  	t.Run("throws error on missing config", func(t *testing.T) {
    59  		project := apitest.RandomProjectRef()
    60  		// Setup in-memory fs
    61  		fsys := afero.NewMemMapFs()
    62  		// Run test
    63  		err := PreRun(project, fsys)
    64  		// Check error
    65  		assert.ErrorIs(t, err, os.ErrNotExist)
    66  	})
    67  }
    68  
    69  func TestPostRun(t *testing.T) {
    70  	t.Run("prints completion message", func(t *testing.T) {
    71  		project := "test-project"
    72  		// Setup in-memory fs
    73  		fsys := afero.NewMemMapFs()
    74  		// Run test
    75  		buf := &strings.Builder{}
    76  		err := PostRun(project, buf, fsys)
    77  		// Check error
    78  		assert.NoError(t, err)
    79  		assert.Equal(t, "Finished supabase link.\n", buf.String())
    80  	})
    81  
    82  	t.Run("prints changed config", func(t *testing.T) {
    83  		project := "test-project"
    84  		updatedConfig["api"] = "test"
    85  		// Setup in-memory fs
    86  		fsys := afero.NewMemMapFs()
    87  		// Run test
    88  		buf := &strings.Builder{}
    89  		err := PostRun(project, buf, fsys)
    90  		// Check error
    91  		assert.NoError(t, err)
    92  		assert.Contains(t, buf.String(), `api = "test"`)
    93  	})
    94  }
    95  
    96  func TestLinkCommand(t *testing.T) {
    97  	project := "test-project"
    98  	// Setup valid access token
    99  	token := apitest.RandomAccessToken(t)
   100  	t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
   101  	// Mock credentials store
   102  	keyring.MockInit()
   103  
   104  	// Reset global variable
   105  	t.Cleanup(func() {
   106  		for k := range updatedConfig {
   107  			delete(updatedConfig, k)
   108  		}
   109  	})
   110  
   111  	t.Run("link valid project", func(t *testing.T) {
   112  		// Setup in-memory fs
   113  		fsys := afero.NewMemMapFs()
   114  		// Setup mock postgres
   115  		conn := pgtest.NewConn()
   116  		defer conn.Close(t)
   117  		conn.Query(repair.CREATE_VERSION_SCHEMA).
   118  			Reply("CREATE SCHEMA").
   119  			Query(repair.CREATE_VERSION_TABLE).
   120  			Reply("CREATE TABLE")
   121  		// Flush pending mocks after test execution
   122  		defer gock.OffAll()
   123  		gock.New("https://api.supabase.io").
   124  			Get("/v1/projects/" + project + "/postgrest").
   125  			Reply(200).
   126  			JSON(api.PostgrestConfigResponse{})
   127  		// Run test
   128  		err := Run(context.Background(), project, username, password, database, fsys, conn.Intercept)
   129  		// Check error
   130  		assert.NoError(t, err)
   131  		assert.Empty(t, apitest.ListUnmatchedRequests())
   132  		// Validate file contents
   133  		content, err := afero.ReadFile(fsys, utils.ProjectRefPath)
   134  		assert.NoError(t, err)
   135  		assert.Equal(t, []byte(project), content)
   136  	})
   137  
   138  	t.Run("throws error on network failure", func(t *testing.T) {
   139  		// Setup in-memory fs
   140  		fsys := afero.NewMemMapFs()
   141  		// Flush pending mocks after test execution
   142  		defer gock.OffAll()
   143  		gock.New("https://api.supabase.io").
   144  			Get("/v1/projects/" + project + "/postgrest").
   145  			ReplyError(errors.New("network error"))
   146  		// Run test
   147  		err := Run(context.Background(), project, username, password, database, fsys)
   148  		// Check error
   149  		assert.ErrorContains(t, err, "network error")
   150  	})
   151  
   152  	t.Run("throws error on connect failure", func(t *testing.T) {
   153  		// Setup in-memory fs
   154  		fsys := afero.NewMemMapFs()
   155  		// Flush pending mocks after test execution
   156  		defer gock.OffAll()
   157  		gock.New("https://api.supabase.io").
   158  			Get("/v1/projects/" + project + "/postgrest").
   159  			Reply(200).
   160  			JSON(api.PostgrestConfigResponse{})
   161  		// Run test
   162  		err := Run(context.Background(), project, username, password, database, fsys, func(cc *pgx.ConnConfig) {
   163  			cc.LookupFunc = func(ctx context.Context, host string) (addrs []string, err error) {
   164  				return nil, errors.New("hostname resolving error")
   165  			}
   166  		})
   167  		// Check error
   168  		assert.ErrorContains(t, err, "hostname resolving error")
   169  	})
   170  
   171  	t.Run("throws error on write failure", func(t *testing.T) {
   172  		// Setup in-memory fs
   173  		fsys := afero.NewReadOnlyFs(afero.NewMemMapFs())
   174  		// Flush pending mocks after test execution
   175  		defer gock.OffAll()
   176  		gock.New("https://api.supabase.io").
   177  			Get("/v1/projects/" + project + "/postgrest").
   178  			Reply(200).
   179  			JSON(api.PostgrestConfigResponse{})
   180  		// Run test
   181  		err := Run(context.Background(), project, username, "", database, fsys)
   182  		// Check error
   183  		assert.ErrorContains(t, err, "operation not permitted")
   184  		assert.Empty(t, apitest.ListUnmatchedRequests())
   185  		// Validate file contents
   186  		exists, err := afero.Exists(fsys, utils.ProjectRefPath)
   187  		assert.NoError(t, err)
   188  		assert.False(t, exists)
   189  	})
   190  }
   191  
   192  func TestLinkPostgrest(t *testing.T) {
   193  	project := "test-project"
   194  	// Setup valid access token
   195  	token := apitest.RandomAccessToken(t)
   196  	t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
   197  
   198  	// Reset global variable
   199  	t.Cleanup(func() {
   200  		for k := range updatedConfig {
   201  			delete(updatedConfig, k)
   202  		}
   203  	})
   204  
   205  	t.Run("ignores matching config", func(t *testing.T) {
   206  		// Flush pending mocks after test execution
   207  		defer gock.OffAll()
   208  		gock.New("https://api.supabase.io").
   209  			Get("/v1/projects/" + project + "/postgrest").
   210  			Reply(200).
   211  			JSON(api.PostgrestConfigResponse{})
   212  		// Run test
   213  		err := linkPostgrest(context.Background(), project)
   214  		// Check error
   215  		assert.NoError(t, err)
   216  		assert.Empty(t, apitest.ListUnmatchedRequests())
   217  		assert.Empty(t, updatedConfig)
   218  	})
   219  
   220  	t.Run("updates api on newer config", func(t *testing.T) {
   221  		// Flush pending mocks after test execution
   222  		defer gock.OffAll()
   223  		gock.New("https://api.supabase.io").
   224  			Get("/v1/projects/" + project + "/postgrest").
   225  			Reply(200).
   226  			JSON(api.PostgrestConfigResponse{
   227  				DbSchema:          "public, storage, graphql_public",
   228  				DbExtraSearchPath: "public, extensions",
   229  				MaxRows:           1000,
   230  			})
   231  		// Run test
   232  		err := linkPostgrest(context.Background(), project)
   233  		// Check error
   234  		assert.NoError(t, err)
   235  		assert.Empty(t, apitest.ListUnmatchedRequests())
   236  		utils.Config.Api.Schemas = []string{"public", "storage", "graphql_public"}
   237  		utils.Config.Api.ExtraSearchPath = []string{"public", "extensions"}
   238  		utils.Config.Api.MaxRows = 1000
   239  		assert.Equal(t, map[string]interface{}{
   240  			"api": utils.Config.Api,
   241  		}, updatedConfig)
   242  	})
   243  
   244  	t.Run("throws error on network failure", func(t *testing.T) {
   245  		// Flush pending mocks after test execution
   246  		defer gock.OffAll()
   247  		gock.New("https://api.supabase.io").
   248  			Get("/v1/projects/" + project + "/postgrest").
   249  			ReplyError(errors.New("network error"))
   250  		// Run test
   251  		err := linkPostgrest(context.Background(), project)
   252  		// Validate api
   253  		assert.ErrorContains(t, err, "network error")
   254  		assert.Empty(t, apitest.ListUnmatchedRequests())
   255  	})
   256  
   257  	t.Run("throws error on server unavailable", func(t *testing.T) {
   258  		// Flush pending mocks after test execution
   259  		defer gock.OffAll()
   260  		gock.New("https://api.supabase.io").
   261  			Get("/v1/projects/" + project + "/postgrest").
   262  			Reply(500).
   263  			JSON(map[string]string{"message": "unavailable"})
   264  		// Run test
   265  		err := linkPostgrest(context.Background(), project)
   266  		// Validate api
   267  		assert.ErrorContains(t, err, `Authorization failed for the access token and project ref pair: {"message":"unavailable"}`)
   268  		assert.Empty(t, apitest.ListUnmatchedRequests())
   269  	})
   270  }
   271  
   272  func TestSliceEqual(t *testing.T) {
   273  	assert.False(t, sliceEqual([]string{"a"}, []string{"b"}))
   274  }
   275  
   276  func TestLinkDatabase(t *testing.T) {
   277  	// Reset global variable
   278  	t.Cleanup(func() {
   279  		for k := range updatedConfig {
   280  			delete(updatedConfig, k)
   281  		}
   282  	})
   283  
   284  	t.Run("throws error on connect failure", func(t *testing.T) {
   285  		// Run test
   286  		err := linkDatabase(context.Background(), username, password, database, "0")
   287  		// Check error
   288  		assert.ErrorContains(t, err, "dial error (dial tcp 0.0.0.0:6543: connect: connection refused)")
   289  		assert.Empty(t, updatedConfig)
   290  	})
   291  
   292  	t.Run("ignores missing server version", func(t *testing.T) {
   293  		// Setup mock postgres
   294  		conn := pgtest.NewWithStatus(map[string]string{
   295  			"standard_conforming_strings": "on",
   296  		})
   297  		defer conn.Close(t)
   298  		conn.Query(repair.CREATE_VERSION_SCHEMA).
   299  			Reply("CREATE SCHEMA").
   300  			Query(repair.CREATE_VERSION_TABLE).
   301  			Reply("CREATE TABLE")
   302  		// Run test
   303  		err := linkDatabase(context.Background(), username, password, database, host, conn.Intercept)
   304  		// Check error
   305  		assert.NoError(t, err)
   306  		assert.Empty(t, updatedConfig)
   307  	})
   308  
   309  	t.Run("updates config to newer db version", func(t *testing.T) {
   310  		utils.Config.Db.MajorVersion = 14
   311  		// Setup mock postgres
   312  		conn := pgtest.NewWithStatus(map[string]string{
   313  			"standard_conforming_strings": "on",
   314  			"server_version":              "15.0",
   315  		})
   316  		defer conn.Close(t)
   317  		conn.Query(repair.CREATE_VERSION_SCHEMA).
   318  			Reply("CREATE SCHEMA").
   319  			Query(repair.CREATE_VERSION_TABLE).
   320  			Reply("CREATE TABLE")
   321  		// Run test
   322  		err := linkDatabase(context.Background(), username, password, database, host, conn.Intercept)
   323  		// Check error
   324  		assert.NoError(t, err)
   325  		utils.Config.Db.MajorVersion = 15
   326  		assert.Equal(t, map[string]interface{}{
   327  			"db": utils.Config.Db,
   328  		}, updatedConfig)
   329  	})
   330  
   331  	t.Run("throws error on query failure", func(t *testing.T) {
   332  		utils.Config.Db.MajorVersion = 14
   333  		// Setup mock postgres
   334  		conn := pgtest.NewConn()
   335  		defer conn.Close(t)
   336  		conn.Query(repair.CREATE_VERSION_SCHEMA).
   337  			Reply("CREATE SCHEMA").
   338  			Query(repair.CREATE_VERSION_TABLE).
   339  			ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation supabase_migrations")
   340  		// Run test
   341  		err := linkDatabase(context.Background(), username, password, database, host, conn.Intercept)
   342  		// Check error
   343  		assert.ErrorContains(t, err, "ERROR: permission denied for relation supabase_migrations (SQLSTATE 42501)")
   344  	})
   345  }