Repository: danvergara/dblab (go)

Issue:
SSL connection related flags for Postgres
**Is your feature request related to a problem? Please describe.**
When it comes to SSL connection, `dblab` assumes that you have the default files set in your system (e.g., `~/.postgresql/root.crt`) and there's no way to customize this behavior right now.

**Describe the solution you'd like**
dblab should provide flags like `--sslcert`, `--sslkey` and -`-sslrootcert` at least.


**Describe alternatives you've considered**
Another way to solve would adding a flag that receives a string with custom values like `--custom='sslrootcert=/path/to/root.crt'`


Pull request description:
Add ssl flags
# Add SSL connection flags/query params

## Description
When it comes to SSL connection, dblab assumes that you have the default files set in your system (e.g., ~/.postgresql/root.crt) and there's no way to customize this behavior right now.

Fixes #153 

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [x] This change requires a documentation update

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

Ran the new feature in my system, QA'ing the app by trying multiple scenarios 

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have checked my code and corrected any misspellings


Patch:
diff --git a/cmd/root.go b/cmd/root.go
index ae31ab2..6d42e9e 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -12,19 +12,23 @@ import (
 
 // Define all the global flags.
 var (
-	cfg     bool
-	cfgName string
-	driver  string
-	url     string
-	host    string
-	port    string
-	user    string
-	pass    string
-	schema  string
-	db      string
-	ssl     string
-	limit   uint
-	socket  string
+	cfg         bool
+	cfgName     string
+	driver      string
+	url         string
+	host        string
+	port        string
+	user        string
+	pass        string
+	schema      string
+	db          string
+	ssl         string
+	limit       uint
+	socket      string
+	sslcert     string
+	sslkey      string
+	sslpassword string
+	sslrootcert string
 )
 
 // NewRootCmd returns the root command.
@@ -44,17 +48,21 @@ func NewRootCmd() *cobra.Command {
 				}
 			} else {
 				opts = command.Options{
-					Driver: driver,
-					URL:    url,
-					Host:   host,
-					Port:   port,
-					User:   user,
-					Pass:   pass,
-					DBName: db,
-					Schema: schema,
-					SSL:    ssl,
-					Limit:  limit,
-					Socket: socket,
+					Driver:      driver,
+					URL:         url,
+					Host:        host,
+					Port:        port,
+					User:        user,
+					Pass:        pass,
+					DBName:      db,
+					Schema:      schema,
+					SSL:         ssl,
+					Limit:       limit,
+					Socket:      socket,
+					SSLCert:     sslcert,
+					SSLKey:      sslkey,
+					SSLPassword: sslpassword,
+					SSLRootcert: sslrootcert,
 				}
 
 				if form.IsEmpty(opts) {
@@ -121,4 +129,26 @@ func init() {
 	rootCmd.Flags().StringVarP(&ssl, "ssl", "", "", "SSL mode")
 	rootCmd.Flags().UintVarP(&limit, "limit", "", 100, "Size of the result set from the table content query (should be greater than zero, otherwise the app will error out)")
 	rootCmd.Flags().StringVarP(&socket, "socket", "", "", "Path to a Unix socket file")
+	rootCmd.Flags().StringVarP(
+		&sslcert,
+		"sslcert",
+		"",
+		"",
+		"This parameter specifies the file name of the client SSL certificate, replacing the default ~/.postgresql/postgresql.crt",
+	)
+	rootCmd.Flags().StringVarP(
+		&sslkey,
+		"sslkey",
+		"",
+		"",
+		"This parameter specifies the location for the secret key used for the client certificate. It can either specify a file name that will be used instead of the default ~/.postgresql/postgresql.key, or it can specify a key obtained from an external “engine”",
+	)
+	rootCmd.Flags().StringVarP(&sslpassword, "sslpassword", "", "", "This parameter specifies the password for the secret key specified in sslkey")
+	rootCmd.Flags().StringVarP(
+		&sslrootcert,
+		"sslrootcert",
+		"",
+		"",
+		"This parameter specifies the name of a file containing SSL certificate authority (CA) certificate(s) The default is ~/.postgresql/root.crt",
+	)
 }
diff --git a/pkg/command/command.go b/pkg/command/command.go
index 25af048..f82a4c2 100644
--- a/pkg/command/command.go
+++ b/pkg/command/command.go
@@ -11,11 +11,16 @@ type Options struct {
 	User   string
 	Pass   string
 	DBName string
-	SSL    string
 	// PostgreSQL only.
 	Schema string
 	Limit  uint
 	Socket string
+	SSL    string
+	// SSL connection params.
+	SSLCert     string
+	SSLKey      string
+	SSLPassword string
+	SSLRootcert string
 }
 
 // SetDefault returns a Options struct and fills the empty
diff --git a/pkg/connection/connection.go b/pkg/connection/connection.go
index e67f7c5..1eb9efd 100644
--- a/pkg/connection/connection.go
+++ b/pkg/connection/connection.go
@@ -87,6 +87,22 @@ func BuildConnectionFromOpts(opts command.Options) (string, command.Options, err
 			}
 		}
 
+		if opts.SSLCert != "" {
+			query.Add("sslcert", opts.SSLCert)
+		}
+
+		if opts.SSLKey != "" {
+			query.Add("sslkey", opts.SSLKey)
+		}
+
+		if opts.SSLPassword != "" {
+			query.Add("sslpassword", opts.SSLPassword)
+		}
+
+		if opts.SSLRootcert != "" {
+			query.Add("sslrootcert", opts.SSLRootcert)
+		}
+
 		connDB := url.URL{
 			Scheme:   opts.Driver,
 			Host:     fmt.Sprintf("%v:%v", opts.Host, opts.Port),
diff --git a/pkg/form/form.go b/pkg/form/form.go
index bf5ad9d..8795ebf 100644
--- a/pkg/form/form.go
+++ b/pkg/form/form.go
@@ -33,6 +33,12 @@ type Model struct {
 	drivers []string
 	driver  string
 
+	// ssl connection params.
+	sslCertInput     textinput.Model
+	sslKeyInput      textinput.Model
+	sslPasswordInput textinput.Model
+	sslRootcertInput textinput.Model
+
 	// std data.
 	hostInput     textinput.Model
 	portInput     textinput.Model
@@ -43,8 +49,10 @@ type Model struct {
 	limitInput    textinput.Model
 
 	// ssl.
-	modes []string
-	ssl   string
+	postgreSQLSSLModes []string
+	mySQLSSLModes      []string
+	sqliteSSLModes     []string
+	sslMode            string
 }
 
 // Init initialize the meta-model.
@@ -68,7 +76,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case 1:
 		return updateStd(msg, m)
 	case 2:
-		return updateSSL(msg, m)
+		return updateSSLMode(msg, m)
+	case 3:
+		return updateSSLConn(msg, m)
 	}
 
 	return m, tea.Quit
@@ -85,6 +95,8 @@ func (m *Model) View() string {
 		s = standardView(m)
 	case 2:
 		s = sslView(m)
+	case 3:
+		s = sslConnView(m)
 	}
 
 	return fmt.Sprint(s)
@@ -115,9 +127,25 @@ func (m *Model) Database() string {
 	return m.databaseInput.Value()
 }
 
-// SSL returns the ssl name value.
-func (m *Model) SSL() string {
-	return m.ssl
+// SSLMode returns the ssl mode name value.
+func (m *Model) SSLMode() string {
+	return m.sslMode
+}
+
+func (m *Model) SSLCert() string {
+	return m.sslCertInput.Value()
+}
+
+func (m *Model) SSLKey() string {
+	return m.sslKeyInput.Value()
+}
+
+func (m *Model) SSLPassword() string {
+	return m.sslPasswordInput.Value()
+}
+
+func (m *Model) SSLRootcert() string {
+	return m.sslRootcertInput.Value()
 }
 
 // Limit returns the limit input value from the user.
@@ -190,19 +218,45 @@ func initModel() Model {
 	filePath.CharLimit = 1000
 	filePath.Focus()
 
+	sslCert := textinput.NewModel()
+	sslCert.Placeholder = "Client SSL certificate"
+	sslCert.CharLimit = 1000
+	sslCert.Focus()
+
+	sslKey := textinput.NewModel()
+	sslKey.Placeholder = "The location for the secret key used for the client certificate"
+	sslKey.CharLimit = 1000
+
+	sslPassord := textinput.NewModel()
+	sslPassord.Placeholder = "The password for the secret key"
+	sslPassord.CharLimit = 1000
+
+	sslRootCert := textinput.NewModel()
+	sslRootCert.Placeholder = "The name of a file containing SSL certificate authority (CA) certificate(s)"
+	sslRootCert.CharLimit = 1000
+
 	m := Model{
 		// the supported drivers by the client.
 		drivers: []string{"postgres", "mysql", "sqlite"},
 		// our default value.
 		driver: "postgres",
 
-		hostInput:     host,
-		portInput:     port,
-		userInput:     user,
-		passwordInput: password,
-		databaseInput: database,
-		limitInput:    limit,
-		filePathInput: filePath,
+		sslMode:            "disable",
+		postgreSQLSSLModes: []string{"disable", "require", "verify-full", "verify-ca"},
+		mySQLSSLModes:      []string{"true", "false", "skip-verify", "preferred"},
+		sqliteSSLModes:     []string{},
+
+		hostInput:        host,
+		portInput:        port,
+		userInput:        user,
+		passwordInput:    password,
+		databaseInput:    database,
+		limitInput:       limit,
+		filePathInput:    filePath,
+		sslCertInput:     sslCert,
+		sslKeyInput:      sslKey,
+		sslPasswordInput: sslPassord,
+		sslRootcertInput: sslRootCert,
 	}
 
 	return m
@@ -221,14 +275,18 @@ func Run() (command.Options, error) {
 	}
 
 	opts := command.Options{
-		Driver: m.driver,
-		Host:   m.Host(),
-		Port:   m.Port(),
-		User:   m.User(),
-		Pass:   m.Password(),
-		DBName: m.Database(),
-		SSL:    m.SSL(),
-		Limit:  limit,
+		Driver:      m.driver,
+		Host:        m.Host(),
+		Port:        m.Port(),
+		User:        m.User(),
+		Pass:        m.Password(),
+		DBName:      m.Database(),
+		SSL:         m.SSLMode(),
+		SSLCert:     m.SSLCert(),
+		SSLKey:      m.SSLKey(),
+		SSLPassword: m.SSLPassword(),
+		SSLRootcert: m.SSLRootcert(),
+		Limit:       limit,
 	}
 
 	if m.driver == "sqlite" {
@@ -240,5 +298,19 @@ func Run() (command.Options, error) {
 
 // IsEmpty checks if the given options objects is empty.
 func IsEmpty(opts command.Options) bool {
-	return cmp.Equal(opts, command.Options{}, cmpopts.IgnoreFields(command.Options{}, "SSL", "Limit"))
+	return cmp.Equal(
+		opts,
+		command.Options{},
+		cmpopts.IgnoreFields(
+			command.Options{},
+			"SSL",
+			"Limit",
+			"Socket",
+			"SSL",
+			"SSLCert",
+			"SSLKey",
+			"SSLPassword",
+			"SSLRootcert",
+		),
+	)
 }
diff --git a/pkg/form/update.go b/pkg/form/update.go
index c104634..856afb7 100644
--- a/pkg/form/update.go
+++ b/pkg/form/update.go
@@ -10,7 +10,6 @@ func updateDriver(msg tea.Msg, m *Model) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
 	// Is it a key press?
 	case tea.KeyMsg:
-
 		switch msg.String() {
 		// the "up" and "k" keys mve the cursor up.
 		case "up", "k":
@@ -48,10 +47,14 @@ func updateStd(msg tea.Msg, m *Model) (tea.Model, tea.Cmd) {
 
 			s := msg.String()
 
-			// If so, exit.
 			if s == "enter" && m.cursor == len(inputs)-1 {
 				m.steps = 2
 				m.cursor = 0
+
+				if m.driver == drivers.SQLite {
+					return m, tea.Quit
+				}
+
 				return m, nil
 			}
 
@@ -129,11 +132,42 @@ func assignStdInputValues(m *Model, inputs []textinput.Model) {
 	}
 }
 
-func updateSSL(msg tea.Msg, m *Model) (tea.Model, tea.Cmd) {
+func updateInputs(msg tea.Msg, m *Model) (*Model, tea.Cmd) {
+	var (
+		cmd  tea.Cmd
+		cmds []tea.Cmd
+	)
+
+	if m.driver == drivers.SQLite {
+		m.filePathInput, cmd = m.filePathInput.Update(msg)
+		cmds = append(cmds, cmd)
+	} else {
+		m.hostInput, cmd = m.hostInput.Update(msg)
+		cmds = append(cmds, cmd)
+
+		m.portInput, cmd = m.portInput.Update(msg)
+		cmds = append(cmds, cmd)
+
+		m.userInput, cmd = m.userInput.Update(msg)
+		cmds = append(cmds, cmd)
+
+		m.passwordInput, cmd = m.passwordInput.Update(msg)
+		cmds = append(cmds, cmd)
+
+		m.databaseInput, cmd = m.databaseInput.Update(msg)
+		cmds = append(cmds, cmd)
+	}
+
+	m.limitInput, cmd = m.limitInput.Update(msg)
+	cmds = append(cmds, cmd)
+
+	return m, tea.Batch(cmds...)
+}
+
+func updateSSLMode(msg tea.Msg, m *Model) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
 	// Is it a key press?
 	case tea.KeyMsg:
-
 		switch msg.String() {
 		// These keys should exit the program.
 		// the "up" and "k" keys mve the cursor up.
@@ -143,50 +177,138 @@ func updateSSL(msg tea.Msg, m *Model) (tea.Model, tea.Cmd) {
 			}
 		// the "down" and "j" keys move the cursor down.
 		case "down", "j":
-			if m.cursor < len(m.modes)-1 {
-				m.cursor++
+			switch m.driver {
+			case drivers.Postgres:
+				if m.cursor < len(m.postgreSQLSSLModes)-1 {
+					m.cursor++
+				}
+			case drivers.MySQL:
+				if m.cursor < len(m.mySQLSSLModes)-1 {
+					m.cursor++
+				}
 			}
 		case "enter":
-			if len(m.modes) > 0 {
-				m.ssl = m.modes[m.cursor]
+			switch m.driver {
+			case drivers.Postgres:
+				m.sslMode = m.postgreSQLSSLModes[m.cursor]
+			case drivers.MySQL:
+				m.sslMode = m.mySQLSSLModes[m.cursor]
 			}
+
 			m.steps = 3
 			m.cursor = 0
-			return m, tea.Quit
+
+			if m.driver != drivers.Postgres || m.sslMode == "disable" {
+				return m, tea.Quit
+			}
+
+			return m, nil
 		}
 	}
 
 	return m, nil
 }
 
-func updateInputs(msg tea.Msg, m *Model) (*Model, tea.Cmd) {
+func updateSSLConn(msg tea.Msg, m *Model) (tea.Model, tea.Cmd) {
+	var (
+		cmd    tea.Cmd
+		inputs []textinput.Model
+	)
+
+	switch msg := msg.(type) {
+	case tea.KeyMsg:
+		switch msg.String() {
+		case "tab", "shift+tab", "enter", "up", "down":
+			inputs = sslConnInputs(m)
+
+			s := msg.String()
+
+			if s == "enter" && m.cursor == len(inputs)-1 {
+				m.steps = 4
+				m.cursor = 0
+
+				return m, tea.Quit
+			}
+
+			if s == "up" || s == "shift+tab" {
+				m.cursor--
+			} else {
+				m.cursor++
+			}
+
+			if m.cursor > len(inputs) {
+				m.cursor = 0
+			} else if m.cursor < 0 {
+				m.cursor = len(inputs)
+			}
+
+			for i := 0; i <= len(inputs)-1; i++ {
+				if i == m.cursor {
+					// Set focused state.
+					inputs[i].Focus()
+					inputs[i].PromptStyle = focusedStyle
+					inputs[i].TextStyle = focusedStyle
+					continue
+				}
+				// Remove focused state.
+				inputs[i].Blur()
+				inputs[i].PromptStyle = noStyle
+				inputs[i].TextStyle = noStyle
+			}
+
+			assignSSLConnInputValues(m, inputs)
+
+			return m, nil
+		}
+	}
+
+	m, cmd = updateSSLConnInputs(msg, m)
+	return m, cmd
+}
+
+func sslConnInputs(m *Model) []textinput.Model {
+	var inputs []textinput.Model
+
+	if m.driver == drivers.Postgres {
+		inputs = []textinput.Model{
+			m.sslCertInput,
+			m.sslKeyInput,
+			m.sslPasswordInput,
+			m.sslRootcertInput,
+		}
+	}
+
+	return inputs
+}
+
+func assignSSLConnInputValues(m *Model, inputs []textinput.Model) {
+	if m.driver == drivers.Postgres && len(inputs) == 4 {
+		m.sslCertInput = inputs[0]
+		m.sslKeyInput = inputs[1]
+		m.sslPasswordInput = inputs[2]
+		m.sslRootcertInput = inputs[3]
+	}
+}
+
+func updateSSLConnInputs(msg tea.Msg, m *Model) (*Model, tea.Cmd) {
 	var (
 		cmd  tea.Cmd
 		cmds []tea.Cmd
 	)
 
-	if m.driver == drivers.SQLite {
-		m.filePathInput, cmd = m.filePathInput.Update(msg)
-		cmds = append(cmds, cmd)
-	} else {
-		m.hostInput, cmd = m.hostInput.Update(msg)
+	if m.driver == drivers.Postgres {
+		m.sslCertInput, cmd = m.sslCertInput.Update(msg)
 		cmds = append(cmds, cmd)
 
-		m.portInput, cmd = m.portInput.Update(msg)
+		m.sslKeyInput, cmd = m.sslKeyInput.Update(msg)
 		cmds = append(cmds, cmd)
 
-		m.userInput, cmd = m.userInput.Update(msg)
+		m.sslPasswordInput, cmd = m.sslPasswordInput.Update(msg)
 		cmds = append(cmds, cmd)
 
-		m.passwordInput, cmd = m.passwordInput.Update(msg)
-		cmds = append(cmds, cmd)
-
-		m.databaseInput, cmd = m.databaseInput.Update(msg)
+		m.sslRootcertInput, cmd = m.sslRootcertInput.Update(msg)
 		cmds = append(cmds, cmd)
 	}
 
-	m.limitInput, cmd = m.limitInput.Update(msg)
-	cmds = append(cmds, cmd)
-
 	return m, tea.Batch(cmds...)
 }
diff --git a/pkg/form/view.go b/pkg/form/view.go
index a3753fb..7dde242 100644
--- a/pkg/form/view.go
+++ b/pkg/form/view.go
@@ -10,7 +10,7 @@ func driverView(m *Model) string {
 	// The header.
 	s := "Select the database driver:"
 	var choices string
-	// Iterate over the driver.
+	// Iterate over the drivers.
 	for i, driver := range m.drivers {
 		choices += fmt.Sprintf("%s\n", checkbox(driver, m.cursor == i))
 	}
@@ -57,24 +57,58 @@ func viewInputs(m *Model) []string {
 }
 
 func sslView(m *Model) string {
+	var sslModes []string
+
 	switch m.driver {
 	case drivers.Postgres:
-		m.modes = []string{"disable", "require", "verify-full"}
+		sslModes = m.postgreSQLSSLModes
 	case drivers.MySQL:
-		m.modes = []string{"true", "false", "skip-verify", "preferred"}
+		sslModes = m.mySQLSSLModes
 	case drivers.SQLite:
-		m.modes = []string{}
+		sslModes = m.sqliteSSLModes
 	default:
-		m.modes = []string{"disable", "require", "verify-full"}
+		sslModes = m.postgreSQLSSLModes
 	}
 
 	// The header.
 	s := "\nSelect the ssl mode (just press enter if you selected sqlite3):"
 	var choices string
 	// Iterate over the driver.
-	for i, mode := range m.modes {
+	for i, mode := range sslModes {
 		choices += fmt.Sprintf("%s\n", checkbox(mode, m.cursor == i))
 	}
 
 	return fmt.Sprintf("%s\n\n%s", s, choices)
 }
+
+func sslConnView(m *Model) string {
+	s := "Introduce the SSL connection params:\n\n"
+
+	inputs := sslConnViewInputs(m)
+
+	for i := 0; i < len(inputs); i++ {
+		s += inputs[i]
+		if i < len(inputs)-1 {
+			s += "\n"
+		}
+	}
+
+	s += "\n"
+
+	return s
+}
+
+func sslConnViewInputs(m *Model) []string {
+	var inputs []string
+
+	if m.driver == drivers.Postgres && (m.sslMode == "require" || m.sslMode == "verify-full" || m.sslMode == "verify-ca") {
+		inputs = []string{
+			m.sslCertInput.View(),
+			m.sslKeyInput.View(),
+			m.sslPasswordInput.View(),
+			m.sslRootcertInput.View(),
+		}
+	}
+
+	return inputs
+}


Test patch:
diff --git a/pkg/connection/connection_test.go b/pkg/connection/connection_test.go
index a193c5c..1fc7627 100644
--- a/pkg/connection/connection_test.go
+++ b/pkg/connection/connection_test.go
@@ -82,6 +82,19 @@ func TestBuildConnectionFromOptsFromURL(t *testing.T) {
 				err:      ErrInvalidURLFormat,
 			},
 		},
+		{
+			name: "valid postgres url with sslmode equals to require",
+			given: given{
+				opts: command.Options{
+					// keep url params in alphetic orden
+					// see: https://github.com/golang/go/issues/29985
+					URL: "postgres://user:password@localhost:5432/db?sslcert=client-cert.pem&sslkey=client-key.pem&sslmode=require&sslrootcert=server-ca.crt",
+				},
+			},
+			want: want{
+				uri: "postgres://user:password@localhost:5432/db?sslcert=client-cert.pem&sslkey=client-key.pem&sslmode=require&sslrootcert=server-ca.crt",
+			},
+		},
 		// mysql
 		{
 			name: "valid mysql localhost",
@@ -236,6 +249,46 @@ func TestBuildConnectionFromOptsUserData(t *testing.T) {
 				uri: "postgres://user:password@127.0.0.1:5432/db?sslmode=disable",
 			},
 		},
+		{
+			name: "success  ssl mode - require",
+			given: given{
+				opts: command.Options{
+					Driver: drivers.Postgres,
+					User:   "user",
+					Pass:   "password",
+					// fake instance.
+					Host:        "db-postgresql-nyc1-12345-do-user-123456-0.b.db.ondigitalocean.com",
+					Port:        "5432",
+					DBName:      "db",
+					SSL:         "require",
+					SSLCert:     "client-cert.pem",
+					SSLKey:      "client-key.pem",
+					SSLRootcert: "server-ca.crt",
+				},
+			},
+			want: want{
+				uri: "postgres://user:password@db-postgresql-nyc1-12345-do-user-123456-0.b.db.ondigitalocean.com:5432/db?sslcert=client-cert.pem&sslkey=client-key.pem&sslmode=require&sslrootcert=server-ca.crt",
+			},
+		},
+		{
+			name: "success  ssl mode - require",
+			given: given{
+				opts: command.Options{
+					Driver: drivers.Postgres,
+					User:   "user",
+					Pass:   "password",
+					// fake instance.
+					Host:        "db-postgresql-nyc1-12345-do-user-123456-0.b.db.ondigitalocean.com",
+					Port:        "5432",
+					DBName:      "db",
+					SSL:         "require",
+					SSLRootcert: "server-ca.crt",
+				},
+			},
+			want: want{
+				uri: "postgres://user:password@db-postgresql-nyc1-12345-do-user-123456-0.b.db.ondigitalocean.com:5432/db?sslmode=require&sslrootcert=server-ca.crt",
+			},
+		},
 		{
 			name: "success - remote host - postgres",
 			given: given{
@@ -461,6 +514,28 @@ func TestFormatPostgresURL(t *testing.T) {
 				uri: "postgresql://user:password@localhost:5432/db?sslmode=disable",
 			},
 		},
+		{
+			name: "valid postgres url ssl mode require all params",
+			given: given{
+				opts: command.Options{
+					URL: "postgres://user:password@localhost:5432/db?sslcert=client-cert.pem&sslkey=client-key.pem&sslmode=require&sslrootcert=server-ca.pem",
+				},
+			},
+			want: want{
+				uri: "postgres://user:password@localhost:5432/db?sslcert=client-cert.pem&sslkey=client-key.pem&sslmode=require&sslrootcert=server-ca.pem",
+			},
+		},
+		{
+			name: "valid postgres url ssl mode require missing params",
+			given: given{
+				opts: command.Options{
+					URL: "postgres://user:password@localhost:5432/db?sslcert=client-cert.pem&sslkey=client-key.pem&sslmode=require",
+				},
+			},
+			want: want{
+				uri: "postgres://user:password@localhost:5432/db?sslcert=client-cert.pem&sslkey=client-key.pem&sslmode=require",
+			},
+		},
 		{
 			name: "error misspelled postgres",
 			given: given{