github.com/blend/go-sdk@v1.20220411.3/testutil/connection_test.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package testutil_test 9 10 import ( 11 "context" 12 "fmt" 13 "math/rand" 14 "strconv" 15 "testing" 16 "time" 17 18 "github.com/blend/go-sdk/assert" 19 "github.com/blend/go-sdk/db" 20 "github.com/blend/go-sdk/env" 21 "github.com/blend/go-sdk/testutil" 22 "github.com/blend/go-sdk/uuid" 23 ) 24 25 var ( 26 // actualEnv contains the **actual** / current environment variables when 27 // the tests were started. This enables tests to run in parallel without 28 // needing to globally lock the result of `env.Env()`. 29 actualEnv = env.Env() 30 ) 31 32 const ( 33 dbDefaultUsername = "postgres" 34 ipv4Loopback = "127.0.0.1" 35 ) 36 37 func TestResolveDBConfig_InvalidEnv(t *testing.T) { 38 it := assert.New(t) 39 40 ev := env.New() 41 // Use only the values we set; e.g. in CI `DB_HOST` and `DB_PORT` also get set. 42 ev.Set(db.EnvVarDatabaseURL, "postgres://hi:bye@localhost:9999/any") 43 ev.Set(db.EnvVarDBPassword, "bye") 44 ev.Set(db.EnvVarDBConnectTimeout, "not-int") 45 46 c := db.Config{} 47 ctx := env.WithVars(context.TODO(), ev) 48 err := testutil.ResolveDBConfig(ctx, &c) 49 it.NotNil(err) 50 expected := `Failed to read 'DB_*' environment variables. Error: 51 time: invalid duration "not-int" 52 53 Environment Variables: 54 - DATABASE_URL="postgres://hi:..password-redacted..@localhost:9999/any" 55 - DB_PASSWORD="..password-redacted.." 56 - DB_CONNECT_TIMEOUT="not-int" 57 ` 58 it.Equal(expected, fmt.Sprintf("%v", err)) 59 } 60 61 func TestValidatePool_NotRunning(t *testing.T) { 62 it := assert.New(t) 63 64 c := defaultConfig(it, actualEnv) 65 c.Engine = "pgx" 66 67 // Use a random port and ":fingers_crossed:" it isn't open on the machine. 68 port := getRandomPort() 69 c.Port = fmt.Sprintf("%d", port) 70 71 pool, err := db.New(db.OptConfig(c)) 72 it.Nil(err) 73 err = testutil.ValidatePool(context.TODO(), pool, "\nadvice on restart\n") 74 it.NotNil(err) 75 expected := `Network error: 76 Could not connect to database. 77 78 advice on restart 79 80 Connection String: 81 "postgres://%[1]s:..password-redacted..@%[2]s:%[3]d/%[4]s?connect_timeout=5&search_path=%[5]s&sslmode=disable" 82 ` 83 it.Equal(formatExpectedPort(expected, port, actualEnv), fmt.Sprintf("%v", err)) 84 } 85 86 func TestValidatePool_NoSSL(t *testing.T) { 87 t.Skip("it is not a safe assumption that all local installations of postgres disable ssl") 88 it := assert.New(t) 89 90 c := defaultConfig(it, actualEnv) 91 c.Engine = "pgx" 92 c.SSLMode = db.SSLModeRequire 93 94 pool, err := db.New(db.OptConfig(c)) 95 it.Nil(err) 96 err = testutil.ValidatePool(context.TODO(), pool, "\nadvice on restart\n") 97 expected := `PostgreSQL error when connecting to the database: 98 server refused TLS connection 99 100 advice on restart 101 102 Connection String: 103 "postgres://%[1]s:..password-redacted..@%[2]s:%[3]d/%[4]s?connect_timeout=5&search_path=%[5]s&sslmode=require" 104 ` 105 it.Equal(formatExpected(expected, actualEnv), fmt.Sprintf("%v", err)) 106 } 107 108 func TestValidatePool_MissingPassword(t *testing.T) { 109 t.Skip("it is not a safe assumption that all local installations of postgres require password authentication") 110 it := assert.New(t) 111 112 c := defaultConfig(it, actualEnv) 113 c.Engine = "pgx" 114 c.Password = "" 115 116 pool, err := db.New(db.OptConfig(c)) 117 it.Nil(err) 118 err = testutil.ValidatePool(context.TODO(), pool, "\nadvice on restart\n") 119 it.NotNil(err) 120 expected := `PostgreSQL error when connecting to the database: 121 password authentication failed for user "%[1]s" 122 123 advice on restart 124 125 Connection String: 126 "postgres://%[1]s@%[2]s:%[3]d/%[4]s?connect_timeout=5&search_path=%[5]s&sslmode=disable" 127 ` 128 it.Equal(formatExpected(expected, actualEnv), fmt.Sprintf("%v", err)) 129 } 130 131 func TestValidatePool_WrongPassword(t *testing.T) { 132 t.Skip("it is not a safe assumption that all local installations of postgres require password authentication") 133 it := assert.New(t) 134 135 c := defaultConfig(it, actualEnv) 136 c.Engine = "pgx" 137 c.Password = uuid.V4().String() 138 139 pool, err := db.New(db.OptConfig(c)) 140 it.Nil(err) 141 err = testutil.ValidatePool(context.TODO(), pool, "\nadvice on restart\n") 142 it.NotNil(err) 143 expected := `PostgreSQL error when connecting to the database: 144 password authentication failed for user "%[1]s" 145 146 advice on restart 147 148 Connection String: 149 "postgres://%[1]s:..password-redacted..@%[2]s:%[3]d/%[4]s?connect_timeout=5&search_path=%[5]s&sslmode=disable" 150 ` 151 it.Equal(formatExpected(expected, actualEnv), fmt.Sprintf("%v", err)) 152 } 153 154 func TestValidatePool_UnsupportedDriver(t *testing.T) { 155 it := assert.New(t) 156 157 c := defaultConfig(it, actualEnv) 158 c.Engine = "not-pgx" 159 c.Password = uuid.V4().ToShortString() 160 161 pool, err := db.New(db.OptConfig(c)) 162 it.Nil(err) 163 err = testutil.ValidatePool(context.TODO(), pool, "\nadvice on restart\n") 164 it.NotNil(err) 165 expected := `Error from 'sql' package: 166 unknown driver "not-pgx" (forgotten import?) 167 Database Engine: 168 not-pgx 169 170 advice on restart 171 172 Connection String: 173 "postgres://%[1]s:..password-redacted..@%[2]s:%[3]d/%[4]s?connect_timeout=5&search_path=%[5]s&sslmode=disable" 174 ` 175 it.Equal(formatExpected(expected, actualEnv), fmt.Sprintf("%v", err)) 176 } 177 178 func TestValidatePool_NotAccepting(t *testing.T) { 179 it := assert.New(t) 180 181 c := defaultConfig(it, actualEnv) 182 c.Engine = "pgx" 183 // NOTE: This makes two strong assumptions that are somewhat specific to 184 // the default `postgres` Docker container: 185 // - the `template0` DB exists in the running PostgreSQL instance 186 // - the `template0` DB is not accepting connections. 187 c.Database = "template0" 188 189 pool, err := db.New(db.OptConfig(c)) 190 it.Nil(err) 191 err = testutil.ValidatePool(context.TODO(), pool, "\nadvice on restart\n") 192 it.NotNil(err) 193 expected := `PostgreSQL error when connecting to the database: 194 database "template0" is not currently accepting connections 195 196 advice on restart 197 198 Connection String: 199 "postgres://%[1]s:..password-redacted..@%[2]s:%[3]d/template0?connect_timeout=5&search_path=%[5]s&sslmode=disable" 200 ` 201 it.Equal(formatExpected(expected, actualEnv), fmt.Sprintf("%v", err)) 202 } 203 204 func TestValidatePool_DoesNotExist(t *testing.T) { 205 it := assert.New(t) 206 207 database := fmt.Sprintf("testdb_%s", uuid.V4().String()) 208 overrides := env.New() 209 overrides.Set(db.EnvVarDBName, database) 210 combined := env.Merge(actualEnv, overrides) 211 212 c := defaultConfig(it, combined) 213 c.Engine = "pgx" 214 c.Database = database 215 216 pool, err := db.New(db.OptConfig(c)) 217 it.Nil(err) 218 err = testutil.ValidatePool(context.TODO(), pool, "\nadvice on restart\n") 219 it.NotNil(err) 220 expectedTemplate := `PostgreSQL error when connecting to the database: 221 database "%s" does not exist 222 223 advice on restart 224 225 Connection String: 226 "postgres://%%[1]s:..password-redacted..@%%[2]s:%%[3]d/%%[4]s?connect_timeout=5&search_path=%%[5]s&sslmode=disable" 227 ` 228 expected := fmt.Sprintf(expectedTemplate, c.Database) 229 it.Equal(formatExpected(expected, combined), fmt.Sprintf("%v", err)) 230 } 231 232 func TestValidatePool_InvalidProtocolInDSN(t *testing.T) { 233 it := assert.New(t) 234 235 c := db.Config{Engine: "pgx", DSN: "x://y"} 236 pool, err := db.New(db.OptConfig(c)) 237 it.Nil(err) 238 239 err = testutil.ValidatePool(context.TODO(), pool, "\nadvice on restart\n") 240 it.NotNil(err) 241 expected := `Unexpected Open() failure: 242 invalid connection protocol: x 243 244 advice on restart 245 246 Connection String: 247 "Failed to parse DSN: see DATABASE_URL environment variable" 248 ` 249 it.Equal(expected, fmt.Sprintf("%v", err)) 250 } 251 252 func getRandomPort() int { 253 s := rand.NewSource(time.Now().UnixNano()) 254 r := rand.New(s) 255 return 2048 + r.Intn(4096) 256 } 257 258 // defaultConfig returns a database config that sets a few defaults and then 259 // uses environment variables to populate the rest. 260 func defaultConfig(it *assert.Assertions, ev env.Vars) db.Config { 261 c := db.Config{ 262 Schema: db.DefaultSchema, 263 Username: dbDefaultUsername, 264 Password: "s33kr1t", 265 SSLMode: db.SSLModeDisable, 266 } 267 ctx := env.WithVars(context.TODO(), ev) 268 err := c.Resolve(ctx) 269 it.Nil(err) 270 // Ensure IPv4 address is used; in some environments where IPv6 may be 271 // a problem this can result in unexpected failures of the form 272 // > dial tcp [::1]:5432: connect: cannot assign requested address 273 if c.Host == db.DefaultHost { 274 c.Host = ipv4Loopback 275 } 276 return c 277 } 278 279 func defaultHost(ev env.Vars) string { 280 host, ok := ev[db.EnvVarDBHost] 281 if !ok { 282 // Ensure IPv4 address is used 283 return ipv4Loopback 284 } 285 // Ensure IPv4 address is used 286 if host == db.DefaultHost { 287 return ipv4Loopback 288 } 289 290 return host 291 } 292 293 func defaultUsername(ev env.Vars) string { 294 username, ok := ev[db.EnvVarDBUser] 295 if !ok { 296 return dbDefaultUsername 297 } 298 299 return username 300 } 301 302 func defaultDatabase(ev env.Vars) string { 303 name, ok := ev[db.EnvVarDBName] 304 if !ok { 305 return db.DefaultDatabase 306 } 307 308 return name 309 } 310 311 func defaultSchema(ev env.Vars) string { 312 schema, ok := ev[db.EnvVarDBSchema] 313 if !ok { 314 return db.DefaultSchema 315 } 316 317 return schema 318 } 319 320 // formatExpectedPort determines the DSN parameters to be used in a template string 321 // to populate an expected error template and then populates the template. s 322 func formatExpectedPort(template string, port int, ev env.Vars) string { 323 host := defaultHost(ev) 324 username := defaultUsername(ev) 325 database := defaultDatabase(ev) 326 schema := defaultSchema(ev) 327 return fmt.Sprintf(template, username, host, port, database, schema) 328 } 329 330 func defaultPort(ev env.Vars) int { 331 port, err := strconv.Atoi(ev.String(db.EnvVarDBPort)) 332 if err != nil { 333 return 5432 334 } 335 336 return port 337 } 338 339 // formatExpected determines the DSN parameters to be used in a template string 340 // to populate an expected error template and then populates the template. 341 func formatExpected(template string, ev env.Vars) string { 342 port := defaultPort(ev) 343 return formatExpectedPort(template, port, ev) 344 }