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 }