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 }