From daa30b2ccccc911158c7e27b4c5843ecd3405495 Mon Sep 17 00:00:00 2001 From: lighttiger2505 Date: Sun, 21 Feb 2021 18:16:21 +0900 Subject: [PATCH 1/2] Add config test --- internal/config/config_test.go | 97 ++++++++++++++++++++++++++++++ internal/config/testdata/basic.yml | 45 ++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 internal/config/config_test.go create mode 100644 internal/config/testdata/basic.yml diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..a594c32 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,97 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/lighttiger2505/sqls/internal/database" +) + +func TestGetConfig(t *testing.T) { + type args struct { + fp string + } + tests := []struct { + name string + args args + want *Config + wantErr bool + }{ + { + name: "basic", + args: args{ + fp: "basic.yml", + }, + want: &Config{ + LowercaseKeywords: true, + Connections: []*database.DBConfig{ + { + Alias: "sqls_mysql", + Driver: "mysql", + Proto: "tcp", + User: "root", + Passwd: "root", + Host: "127.0.0.1", + Port: 13306, + DBName: "world", + Params: map[string]string{"autocommit": "true", "tls": "skip-verify"}, + }, + { + Alias: "sqls_sqlite3", + Driver: "sqlite3", + DataSourceName: "file:/home/lighttiger2505/chinook.db", + }, + { + Alias: "sqls_postgresql", + Driver: "postgresql", + Proto: "tcp", + User: "postgres", + Passwd: "mysecretpassword1234", + Host: "127.0.0.1", + Port: 15432, + DBName: "dvdrental", + Params: map[string]string{"sslmode": "disable"}, + }, + { + Alias: "mysql_with_bastion", + Driver: "mysql", + Proto: "tcp", + User: "admin", + Passwd: "Q+ACgv12ABx/", + Host: "192.168.121.163", + Port: 3306, + DBName: "world", + SSHCfg: &database.SSHConfig{ + Host: "192.168.121.168", + Port: 22, + User: "vagrant", + PassPhrase: "passphrase1234", + PrivateKey: "/home/lighttiger2505/.ssh/id_rsa", + }, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + packageDir, err := os.Getwd() + if err != nil { + t.Fatalf("cannot get package path, Err=%v", err) + } + testFile := filepath.Join(packageDir, "testdata", tt.args.fp) + + t.Run(tt.name, func(t *testing.T) { + got, err := GetConfig(testFile) + if (err != nil) != tt.wantErr { + t.Errorf("GetConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("unmatch (- want, + got):\n%s", diff) + } + }) + } +} diff --git a/internal/config/testdata/basic.yml b/internal/config/testdata/basic.yml new file mode 100644 index 0000000..43c225c --- /dev/null +++ b/internal/config/testdata/basic.yml @@ -0,0 +1,45 @@ +lowercaseKeywords: true +connections: + - alias: sqls_mysql + driver: mysql + dataSourceName: "" + proto: tcp + user: root + passwd: root + host: 127.0.0.1 + port: 13306 + path: "" + dbName: world + params: + autocommit: "true" + tls: skip-verify + - alias: sqls_sqlite3 + driver: sqlite3 + dataSourceName: "file:/home/lighttiger2505/chinook.db" + - alias: sqls_postgresql + driver: postgresql + dataSourceName: "" + proto: tcp + user: postgres + passwd: mysecretpassword1234 + host: 127.0.0.1 + port: 15432 + path: "" + dbName: dvdrental + params: + sslmode: disable + - alias: mysql_with_bastion + driver: mysql + dataSourceName: "" + proto: tcp + user: admin + passwd: Q+ACgv12ABx/ + host: 192.168.121.163 + port: 3306 + dbName: world + sshConfig: + host: 192.168.121.168 + port: 22 + user: vagrant + passPhrase: passphrase1234 + privateKey: /home/lighttiger2505/.ssh/id_rsa From deb8478f77fe8010c390368c68a9051e918b7e3a Mon Sep 17 00:00:00 2001 From: lighttiger2505 Date: Sun, 21 Feb 2021 22:40:05 +0900 Subject: [PATCH 2/2] Add config validation --- internal/config/config.go | 13 +++ internal/config/config_test.go | 94 ++++++++++++++++++- internal/config/testdata/invalid_proto.yml | 14 +++ internal/config/testdata/no_connection.yml | 5 + internal/config/testdata/no_driver.yml | 14 +++ internal/config/testdata/no_dsn.yml | 4 + internal/config/testdata/no_host.yml | 14 +++ internal/config/testdata/no_path.yml | 14 +++ internal/config/testdata/no_ssh_host.yml | 16 ++++ .../config/testdata/no_ssh_private_key.yml | 16 ++++ internal/config/testdata/no_ssh_user.yml | 16 ++++ internal/config/testdata/no_user.yml | 15 +++ internal/database/config.go | 55 +++++++++++ 13 files changed, 287 insertions(+), 3 deletions(-) create mode 100644 internal/config/testdata/invalid_proto.yml create mode 100644 internal/config/testdata/no_connection.yml create mode 100644 internal/config/testdata/no_driver.yml create mode 100644 internal/config/testdata/no_dsn.yml create mode 100644 internal/config/testdata/no_host.yml create mode 100644 internal/config/testdata/no_path.yml create mode 100644 internal/config/testdata/no_ssh_host.yml create mode 100644 internal/config/testdata/no_ssh_private_key.yml create mode 100644 internal/config/testdata/no_ssh_user.yml create mode 100644 internal/config/testdata/no_user.yml diff --git a/internal/config/config.go b/internal/config/config.go index 45f6100..915ca94 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "errors" + "fmt" "io/ioutil" "os" "path/filepath" @@ -24,6 +25,14 @@ type Config struct { Connections []*database.DBConfig `json:"connections" yaml:"connections"` } +func (c *Config) Validate() error { + fmt.Println("Validate") + if len(c.Connections) > 0 { + return c.Connections[0].Validate() + } + return nil +} + func NewConfig() *Config { cfg := &Config{} cfg.LowercaseKeywords = false @@ -69,6 +78,10 @@ func (c *Config) Load(fp string) error { if err = yaml.Unmarshal(b, c); err != nil { return xerrors.Errorf("failed unmarshal yaml, %+v", err, string(b)) } + + if err := c.Validate(); err != nil { + return xerrors.Errorf("failed validation, %+v", err) + } return nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a594c32..400baf8 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -18,6 +18,7 @@ func TestGetConfig(t *testing.T) { args args want *Config wantErr bool + errMsg string }{ { name: "basic", @@ -75,6 +76,87 @@ func TestGetConfig(t *testing.T) { }, wantErr: false, }, + { + name: "no driver", + args: args{ + fp: "no_driver.yml", + }, + want: nil, + wantErr: true, + errMsg: "failed validation, required: connections[].driver", + }, + { + name: "no connection", + args: args{ + fp: "no_connection.yml", + }, + want: nil, + wantErr: true, + errMsg: "failed validation, required: connections[].dataSourceName or connections[].proto", + }, + { + name: "no user", + args: args{ + fp: "no_user.yml", + }, + want: nil, + wantErr: true, + errMsg: "failed validation, required: connections[].user", + }, + { + name: "invalid proto", + args: args{ + fp: "invalid_proto.yml", + }, + want: nil, + wantErr: true, + errMsg: "failed validation, invalid: connections[].proto", + }, + { + name: "no path", + args: args{ + fp: "no_path.yml", + }, + want: nil, + wantErr: true, + errMsg: "failed validation, required: connections[].path", + }, + { + name: "no dsn", + args: args{ + fp: "no_dsn.yml", + }, + want: nil, + wantErr: true, + errMsg: "failed validation, required: connections[].dataSourceName", + }, + { + name: "no ssh host", + args: args{ + fp: "no_ssh_host.yml", + }, + want: nil, + wantErr: true, + errMsg: "failed validation, required: connections[]sshConfig.host", + }, + { + name: "no ssh user", + args: args{ + fp: "no_ssh_user.yml", + }, + want: nil, + wantErr: true, + errMsg: "failed validation, required: connections[].sshConfig.user", + }, + { + name: "no ssh private key", + args: args{ + fp: "no_ssh_private_key.yml", + }, + want: nil, + wantErr: true, + errMsg: "failed validation, required: connections[].sshConfig.privateKey", + }, } for _, tt := range tests { packageDir, err := os.Getwd() @@ -85,9 +167,15 @@ func TestGetConfig(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := GetConfig(testFile) - if (err != nil) != tt.wantErr { - t.Errorf("GetConfig() error = %v, wantErr %v", err, tt.wantErr) - return + if err != nil { + if tt.wantErr { + if err.Error() != tt.errMsg { + t.Errorf("unmatch error message, want:%q got:%q", tt.errMsg, err.Error()) + } + } else { + t.Errorf("GetConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } } if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("unmatch (- want, + got):\n%s", diff) diff --git a/internal/config/testdata/invalid_proto.yml b/internal/config/testdata/invalid_proto.yml new file mode 100644 index 0000000..d3b77ad --- /dev/null +++ b/internal/config/testdata/invalid_proto.yml @@ -0,0 +1,14 @@ +connections: + - alias: sqls_mysql + driver: mysql + dataSourceName: "" + proto: invalid + user: root + passwd: root + host: 127.0.0.1 + port: 13306 + path: "" + dbName: world + params: + autocommit: "true" + tls: skip-verify diff --git a/internal/config/testdata/no_connection.yml b/internal/config/testdata/no_connection.yml new file mode 100644 index 0000000..53aada9 --- /dev/null +++ b/internal/config/testdata/no_connection.yml @@ -0,0 +1,5 @@ +connections: + - alias: sqls_mysql + driver: mysql + dataSourceName: "" + proto: "" diff --git a/internal/config/testdata/no_driver.yml b/internal/config/testdata/no_driver.yml new file mode 100644 index 0000000..28eae66 --- /dev/null +++ b/internal/config/testdata/no_driver.yml @@ -0,0 +1,14 @@ +connections: + - alias: sqls_mysql + driver: "" + dataSourceName: "" + proto: tcp + user: root + passwd: root + host: 127.0.0.1 + port: 13306 + path: "" + dbName: world + params: + autocommit: "true" + tls: skip-verify diff --git a/internal/config/testdata/no_dsn.yml b/internal/config/testdata/no_dsn.yml new file mode 100644 index 0000000..89e8106 --- /dev/null +++ b/internal/config/testdata/no_dsn.yml @@ -0,0 +1,4 @@ +connections: + - alias: sqls_sqlite3 + driver: sqlite3 + dataSourceName: "" diff --git a/internal/config/testdata/no_host.yml b/internal/config/testdata/no_host.yml new file mode 100644 index 0000000..29c4a41 --- /dev/null +++ b/internal/config/testdata/no_host.yml @@ -0,0 +1,14 @@ +connections: + - alias: sqls_mysql + driver: mysql + dataSourceName: "" + proto: unix + user: root + passwd: root + host: "" + port: 0 + path: "" + dbName: world + params: + autocommit: "true" + tls: skip-verify diff --git a/internal/config/testdata/no_path.yml b/internal/config/testdata/no_path.yml new file mode 100644 index 0000000..2eb05d1 --- /dev/null +++ b/internal/config/testdata/no_path.yml @@ -0,0 +1,14 @@ +connections: + - alias: sqls_mysql + driver: mysql + dataSourceName: "" + proto: unix + user: root + passwd: root + host: 127.0.0.1 + port: 13306 + path: "" + dbName: world + params: + autocommit: "true" + tls: skip-verify diff --git a/internal/config/testdata/no_ssh_host.yml b/internal/config/testdata/no_ssh_host.yml new file mode 100644 index 0000000..b08ca2b --- /dev/null +++ b/internal/config/testdata/no_ssh_host.yml @@ -0,0 +1,16 @@ +connections: + - alias: mysql_with_bastion + driver: mysql + dataSourceName: "" + proto: tcp + user: admin + passwd: Q+ACgv12ABx/ + host: 192.168.121.163 + port: 3306 + dbName: world + sshConfig: + host: "" + port: 22 + user: vagrant + passPhrase: passphrase1234 + privateKey: /home/lighttiger2505/.ssh/id_rsa diff --git a/internal/config/testdata/no_ssh_private_key.yml b/internal/config/testdata/no_ssh_private_key.yml new file mode 100644 index 0000000..31c7d1e --- /dev/null +++ b/internal/config/testdata/no_ssh_private_key.yml @@ -0,0 +1,16 @@ +connections: + - alias: mysql_with_bastion + driver: mysql + dataSourceName: "" + proto: tcp + user: admin + passwd: Q+ACgv12ABx/ + host: 192.168.121.163 + port: 3306 + dbName: world + sshConfig: + host: 192.168.121.168 + port: 22 + user: vagrant + passPhrase: passphrase1234 + privateKey: "" diff --git a/internal/config/testdata/no_ssh_user.yml b/internal/config/testdata/no_ssh_user.yml new file mode 100644 index 0000000..2fb1e59 --- /dev/null +++ b/internal/config/testdata/no_ssh_user.yml @@ -0,0 +1,16 @@ +connections: + - alias: mysql_with_bastion + driver: mysql + dataSourceName: "" + proto: tcp + user: admin + passwd: Q+ACgv12ABx/ + host: 192.168.121.163 + port: 3306 + dbName: world + sshConfig: + host: 192.168.121.168 + port: 22 + user: "" + passPhrase: passphrase1234 + privateKey: /home/lighttiger2505/.ssh/id_rsa diff --git a/internal/config/testdata/no_user.yml b/internal/config/testdata/no_user.yml new file mode 100644 index 0000000..e277cb1 --- /dev/null +++ b/internal/config/testdata/no_user.yml @@ -0,0 +1,15 @@ +lowercaseKeywords: true +connections: + - alias: sqls_mysql + driver: mysql + dataSourceName: "" + proto: tcp + user: "" + passwd: root + host: 127.0.0.1 + port: 13306 + path: "" + dbName: world + params: + autocommit: "true" + tls: skip-verify diff --git a/internal/database/config.go b/internal/database/config.go index d5e59cb..99ff2c8 100644 --- a/internal/database/config.go +++ b/internal/database/config.go @@ -1,6 +1,7 @@ package database import ( + "errors" "fmt" "io/ioutil" @@ -32,6 +33,47 @@ type DBConfig struct { SSHCfg *SSHConfig `json:"sshConfig" yaml:"sshConfig"` } +func (c *DBConfig) Validate() error { + if c.Driver == "" { + return errors.New("required: connections[].driver") + } + + switch c.Driver { + case dialect.DatabaseDriverMySQL, dialect.DatabaseDriverPostgreSQL: + if c.DataSourceName == "" && c.Proto == "" { + return errors.New("required: connections[].dataSourceName or connections[].proto") + } + + if c.DataSourceName == "" && c.Proto != "" { + if c.User == "" { + return errors.New("required: connections[].user") + } + switch c.Proto { + case ProtoTCP, ProtoUDP: + if c.Host == "" { + return errors.New("required: connections[].host") + } + case ProtoUnix: + if c.Path == "" { + return errors.New("required: connections[].path") + } + default: + return errors.New("invalid: connections[].proto") + } + if c.SSHCfg != nil { + return c.SSHCfg.Validate() + } + } + case dialect.DatabaseDriverSQLite3: + if c.DataSourceName == "" { + return errors.New("required: connections[].dataSourceName") + } + default: + return errors.New("invalid: connections[].driver") + } + return nil +} + type SSHConfig struct { Host string `json:"host" yaml:"host"` Port int `json:"port" yaml:"port"` @@ -40,6 +82,19 @@ type SSHConfig struct { PrivateKey string `json:"privateKey" yaml:"privateKey"` } +func (c *SSHConfig) Validate() error { + if c.Host == "" { + return errors.New("required: connections[]sshConfig.host") + } + if c.User == "" { + return errors.New("required: connections[].sshConfig.user") + } + if c.PrivateKey == "" { + return errors.New("required: connections[].sshConfig.privateKey") + } + return nil +} + func (s *SSHConfig) Endpoint() string { return fmt.Sprintf("%s:%d", s.Host, s.Port) }