From f0b4f4e01abf10ecd3818b819d3aeb5ce8b8ed34 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 30 Sep 2020 17:43:26 +0200 Subject: [PATCH] Some tests are running --- cmd/init.go | 138 +++++++++++++++++++++++++++++++++++++++++++ cmd/list_test.go | 11 ---- cmd/root.go | 137 ++++++++++++------------------------------ cmd/root_test.go | 89 ++++++++++++++++++++++++++-- cmd/snapshot.go | 6 +- cmd/snapshot_test.go | 65 ++++++++++++++++++++ 6 files changed, 328 insertions(+), 118 deletions(-) create mode 100644 cmd/init.go delete mode 100644 cmd/list_test.go create mode 100644 cmd/snapshot_test.go diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 0000000..7abc259 --- /dev/null +++ b/cmd/init.go @@ -0,0 +1,138 @@ +/* +Copyright © 2020 NAME HERE + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "fmt" + "github.com/AlecAivazis/survey/v2" + "github.com/AlecAivazis/survey/v2/terminal" + "github.com/BurntSushi/toml" + "github.com/ttacon/chalk" + "log" + "os" + "strings" + + "github.com/spf13/cobra" +) + +// initCmd represents the init command +var initCmd = &cobra.Command{ + Use: "init", + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("init called") + writeConfigFile(configFileName) + }, +} + +func init() { + rootCmd.AddCommand(initCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // initCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // initCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + + +func writeConfigFile(filename string) { + + err := runWizard(&config) + if err != nil { + log.Fatalf("Error during wizard : %s", err) + } + + f, err := os.Create(filename) + if err != nil { + // failed to create/open the file + log.Fatal(err) + } + if err := toml.NewEncoder(f).Encode(config); err != nil { + // failed to encode + log.Fatal(err) + } + + fmt.Println(chalk.Green.Color(fmt.Sprintf("Config file (%s) created, good to go", configFileName))) + + defer func() { + if err := f.Close(); err != nil { + log.Fatal(err) + } + }() +} + +func runWizard(config *Config) error { + // Get parent dir name as project name + currWorkDirPath, _ := os.Getwd() + breakPath := strings.Split(currWorkDirPath, "/") + maybeDirName := breakPath[len(breakPath)-1] + + // Wizard questions + var qs = []*survey.Question{ + { + Name: "username", + Prompt: &survey.Input{Message: "Postgres user ?"}, + Validate: survey.Required, + }, + { + Name: "password", + Prompt: &survey.Password{Message: "Postgres password ?"}, + Validate: survey.Required, + }, + { + Name: "host", + Prompt: &survey.Input{Message: "Postgres server host ?", Default: "127.0.0.1"}, + Validate: survey.Required, + }, + { + Name: "port", + Prompt: &survey.Input{Message: "Postgres server port ?", Default: "5432"}, + Validate: survey.Required, + }, + { + Name: "database", + Prompt: &survey.Input{Message: "Your working database name"}, + Validate: survey.Required, + }, + { + Name: "project", + Prompt: &survey.Input{Message: "This project name", Default: maybeDirName}, + Validate: survey.Required, + }, + } + + // perform the questions + err := survey.Ask(qs, config) + if err == terminal.InterruptErr { + fmt.Println("User terminated prompt, no config file created") + os.Exit(0) + } else if err != nil { + return err + } + + return nil +} \ No newline at end of file diff --git a/cmd/list_test.go b/cmd/list_test.go deleted file mode 100644 index 1dbfe3e..0000000 --- a/cmd/list_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package cmd - -import ( - "fmt" - "testing" -) - -func TestSomething(t *testing.T) { - fmt.Println("I am running") - // db.Query() -} diff --git a/cmd/root.go b/cmd/root.go index f04d764..38550e8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,17 +2,17 @@ package cmd import ( "context" + "errors" "fmt" - "github.com/AlecAivazis/survey/v2" - "github.com/AlecAivazis/survey/v2/terminal" - "github.com/BurntSushi/toml" + "github.com/jackc/pgx/v4" - "github.com/ttacon/chalk" + "io/ioutil" "log" - "os" + "strings" + "github.com/go-playground/validator/v10" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -29,12 +29,12 @@ var ( ) type Config struct { - Username string `mapstructure:"username" survey:"username"` - Password string `mapstructure:"password" survey:"password"` - Host string `mapstructure:"host" survey:"host"` - Port string `mapstructure:"port" survey:"port"` - Database string `mapstructure:"database" survey:"database"` - Project string `mapstructure:"project" survey:"project"` + Username string `mapstructure:"username" survey:"username" validate:"required"` + Password string `mapstructure:"password" survey:"password" validate:"required"` + Host string `mapstructure:"host" survey:"host" validate:"required"` + Port string `mapstructure:"port" survey:"port" validate:"required"` + Database string `mapstructure:"database" survey:"database" validate:"required"` + Project string `mapstructure:"project" survey:"project" validate:"required"` } // rootCmd represents the base command when called without any subcommands @@ -45,6 +45,7 @@ var rootCmd = &cobra.Command{ Useful when you have git branches containing migrations Heavily (98%) inspired by fastmonkeys/stellar `, + SilenceUsage: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { verbose, err := cmd.Flags().GetBool("verbose") @@ -60,26 +61,30 @@ Heavily (98%) inspired by fastmonkeys/stellar // Some commands does not need a correct config file to be present, return early if we are running // excluded commands runningCmd := cmd.Name() - if runningCmd == "version" || runningCmd == "help" { + if runningCmd == "version" || runningCmd == "help" || runningCmd == "init" { return nil } // Find & load config file if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { - fmt.Println(chalk.Yellow.Color(fmt.Sprintf("Config file (%s) not found in current directory", configFileName))) - fmt.Println(chalk.Yellow.Color(fmt.Sprint("I will create one after asking you a few questions"))) - writeConfigFile(configFileName) + return errors.New(fmt.Sprintf("%s\nRun 'cappa init'", err.(viper.ConfigFileNotFoundError))) } else { - log.Printf("Config file was found but another error was produced : %s", err) - os.Exit(0) + return errors.New(fmt.Sprintf("Config file was found but another error was produced : %s", err)) } } - // Unmarshal config into Config struct + err = viper.Unmarshal(&config) if err != nil { - log.Fatalf("error unmarshall %s", err) + log.Printf("error unmarshall %s", err) } + + valid, errors := isConfigValid(&config) + if !valid { + fmt.Fprintf(cmd.OutOrStdout(), "Some values are missing or are incorrect in your config file (run 'cappa init')\n") + return errors + } + log.Println("Using config file:", viper.ConfigFileUsed()) log.Printf("Config values : %#v", config) @@ -97,12 +102,24 @@ Heavily (98%) inspired by fastmonkeys/stellar }, } +func isConfigValid(config *Config) (bool, error) { + validate := validator.New() + err := validate.Struct(config) + if err != nil { + errorsList := []string{} + for _, err := range err.(validator.ValidationErrors) { + errorsList = append(errorsList, fmt.Sprintf("\n\"%s\" %s", err.Field(), err.Tag())) + } + return false, errors.New(fmt.Sprintf("\n%s", strings.Join(errorsList, ""))) + } + return true, nil +} + // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) + } } @@ -139,84 +156,6 @@ func createConnection(config Config, database string) *pgx.Conn { return conn } -func writeConfigFile(filename string) { - - err := runWizard(&config) - if err != nil { - log.Fatalf("Error during wizard : %s", err) - } - - f, err := os.Create(filename) - if err != nil { - // failed to create/open the file - log.Fatal(err) - } - if err := toml.NewEncoder(f).Encode(config); err != nil { - // failed to encode - log.Fatal(err) - } - - fmt.Println(chalk.Green.Color(fmt.Sprintf("Config file (%s) created, good to go", configFileName))) - - defer func() { - if err := f.Close(); err != nil { - log.Fatal(err) - } - }() -} - -func runWizard(config *Config) error { - // Get parent dir name as project name - currWorkDirPath, _ := os.Getwd() - breakPath := strings.Split(currWorkDirPath, "/") - maybeDirName := breakPath[len(breakPath)-1] - - // Wizard questions - var qs = []*survey.Question{ - { - Name: "username", - Prompt: &survey.Input{Message: "Postgres user ?"}, - Validate: survey.Required, - }, - { - Name: "password", - Prompt: &survey.Password{Message: "Postgres password ?"}, - Validate: survey.Required, - }, - { - Name: "host", - Prompt: &survey.Input{Message: "Postgres server host ?", Default: "127.0.0.1"}, - Validate: survey.Required, - }, - { - Name: "port", - Prompt: &survey.Input{Message: "Postgres server port ?", Default: "5432"}, - Validate: survey.Required, - }, - { - Name: "database", - Prompt: &survey.Input{Message: "Your working database name"}, - Validate: survey.Required, - }, - { - Name: "project", - Prompt: &survey.Input{Message: "This project name", Default: maybeDirName}, - Validate: survey.Required, - }, - } - - // perform the questions - err := survey.Ask(qs, config) - if err == terminal.InterruptErr { - fmt.Println("User terminated prompt, no config file created") - os.Exit(0) - } else if err != nil { - return err - } - - return nil -} - // This function create the database for tracking snapshots func createTrackerDb(conn *pgx.Conn) { structureSql := `CREATE TABLE snapshots (id SERIAL PRIMARY KEY, hash TEXT UNIQUE NOT NULL, name TEXT NOT NULL,project TEXT NOT NULL, created_at timestamp not null default CURRENT_TIMESTAMP);` diff --git a/cmd/root_test.go b/cmd/root_test.go index 6ef305f..e5d83bd 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -7,6 +7,8 @@ import ( "io/ioutil" "log" "os" + "path" + "runtime" "strings" "testing" @@ -20,27 +22,60 @@ var pool *dockertest.Pool var resource *dockertest.Resource var database = "postgres" var err error +var testDir string + +var testConfig = Config{ + Username: "postgres", + Password: "secret", + Host: "localhost", + Port: "5432", + Database: "devproject", + Project: "testproject", +} func TestMain(m *testing.M) { - setup() + + // Go test has path relative to package instead of root package + // So we alter this behavior here + _, filename, _, _ := runtime.Caller(0) + rootdir := path.Join(path.Dir(filename), "..") + err := os.Chdir(rootdir) + if err != nil { + panic(err) + } + + // We create a test dir like a real project situation + testDir, err = ioutil.TempDir(".", "testdir") + if err != nil { + panic(err) + } + os.Chdir(testDir) + + // Setup docker with database + setupDockerDb() + + // Run all tests code := m.Run() + + // Teardown teardown() + os.Exit(code) } -func setup() { +func setupDockerDb() { pool, err = dockertest.NewPool("") if err != nil { log.Fatalf("Could not connect to docker: %s", err) } - resource, err = pool.Run("postgres", "9.6", []string{"POSTGRES_PASSWORD=secret", "POSTGRES_DB=" + database}) + resource, err = pool.Run("postgres", "9.6", []string{"POSTGRES_PASSWORD=" + testConfig.Password, "POSTGRES_DB=" + database}) if err != nil { log.Fatalf("Could not start resource: %s", err) } if err = pool.Retry(func() error { var err error - db, err = sql.Open("postgres", fmt.Sprintf("postgres://postgres:secret@localhost:%s/%s?sslmode=disable", resource.GetPort("5432/tcp"), database)) + db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", testConfig.Username, testConfig.Password, testConfig.Host, resource.GetPort("5432/tcp"), database)) if err != nil { return err } @@ -51,11 +86,28 @@ func setup() { fmt.Printf("\033[1;36m%s\033[0m", "> Setup completed\n") } +func resetDatabases(){ + deleteNonTemplateDat := `select 'drop database "'||datname||'";' from pg_database where datistemplate=false AND datname!='postgres';` + row := db.QueryRow(deleteNonTemplateDat) + switch err := row.Scan(); err { + case sql.ErrNoRows: + log.Println("No rows were returned!") + case nil: + log.Println("success") + default: + panic(err) + } +} + func teardown() { // You can't defer this because os.Exit doesn't care for defer if err := pool.Purge(resource); err != nil { log.Fatalf("Could not purge resource: %s", err) } + err = os.RemoveAll(testDir) + if err != nil { + fmt.Printf("error removing testDir %s",err) + } fmt.Printf("\033[1;36m%s\033[0m", "> Teardown completed\n") } @@ -63,7 +115,34 @@ func NewRootCmd() *cobra.Command { return rootCmd } -func Test_ExecuteCommand(t *testing.T) { +type fakeConfig struct { + Config +} + +func (f *fakeConfig) create() { + _, err := os.Create(".cappa.toml") + if err != nil { + log.Fatal("Error creating empty fake config file") + } +} + +func (f *fakeConfig) createReal() { + _, err := os.Create(".cappa.toml") + if err != nil { + log.Fatal("Error creating empty fake config file") + } +} + + +func (f *fakeConfig) remove() error { + err := os.Remove(".cappa.toml") + if err != nil { + return err + } + return nil +} + +func Test_ExecuteRootCommand(t *testing.T) { cmd := NewRootCmd() b := bytes.NewBufferString("") cmd.SetOut(b) diff --git a/cmd/snapshot.go b/cmd/snapshot.go index bca16c1..5514ab6 100644 --- a/cmd/snapshot.go +++ b/cmd/snapshot.go @@ -18,7 +18,7 @@ import ( var snapshotCmd = &cobra.Command{ Use: "snapshot", Short: "Snapshot database", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { var snapshotName string snapUuid := shortuuid.New() toDatabase := fmt.Sprintf("%s_%s", cliName, strings.ToLower(snapUuid)) @@ -30,7 +30,7 @@ var snapshotCmd = &cobra.Command{ err := survey.AskOne(prompt, &snapshotName, survey.WithValidator(survey.Required)) if err == terminal.InterruptErr { fmt.Println("User terminated prompt") - os.Exit(0) + return nil } else if err != nil { log.Fatal(err) } @@ -60,7 +60,7 @@ var snapshotCmd = &cobra.Command{ } fmt.Printf("Snapshot created from %s to %s\n", config.Database, snapshotName) - + return nil }, } diff --git a/cmd/snapshot_test.go b/cmd/snapshot_test.go new file mode 100644 index 0000000..cf18ff2 --- /dev/null +++ b/cmd/snapshot_test.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "bytes" + "io/ioutil" + "strings" + "testing" +) + +func Test_ExecuteSnapshotWithNoConfigFile(t *testing.T) { + cmd := NewRootCmd() + b := bytes.NewBufferString("") + cmd.SetOut(b) + cmd.SetErr(b) + cmd.SetArgs([]string{"snapshot"}) + cmd.Execute() + out, err := ioutil.ReadAll(b) + if err != nil { + t.Fatal(err) + } + subString := `Error: Config File ".cappa" Not Found in` + if !strings.Contains(string(out), subString) { + t.Fatalf("expected output to contain \"%s\" in \"%s\"", subString, string(out)) + } +} + +func Test_ExecuteSnapshotWithInvalidConfig(t *testing.T) { + cmd := NewRootCmd() + b := bytes.NewBufferString("") + cmd.SetOut(b) + cmd.SetErr(b) + cmd.SetArgs([]string{"snapshot"}) + fakeconf := fakeConfig{} + fakeconf.create() + cmd.Execute() + out, err := ioutil.ReadAll(b) + if err != nil { + t.Fatal(err) + } + fakeconf.remove() + subString := "Some values are missing or are incorrect in your config file (run 'cappa init')" + if !strings.Contains(string(out), subString) { + t.Fatalf("expected output to contain \"%s\" in \"%s\"", subString, string(out)) + } +} + +func Test_ExecuteSnapshot(t *testing.T) { + cmd := NewRootCmd() + b := bytes.NewBufferString("") + cmd.SetOut(b) + cmd.SetErr(b) + cmd.SetArgs([]string{"snapshot"}) + fakeconf := fakeConfig{} + fakeconf.create() + cmd.Execute() + out, err := ioutil.ReadAll(b) + if err != nil { + t.Fatal(err) + } + fakeconf.remove() + subString := "Some values are missing or are incorrect in your config file (run 'cappa init')" + if !strings.Contains(string(out), subString) { + t.Fatalf("expected output to contain \"%s\" in \"%s\"", subString, string(out)) + } +}