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  }