From 6e1118643fe3a01513a02dbd2c2f2766823e31df Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Erik=20Hedenstr=C3=B6m?= <erik@hedenstroem.com>
Date: Tue, 3 Oct 2023 17:31:20 +0000
Subject: [PATCH] Implement go tests

---
 .gitignore               |  3 +++
 .gitlab-ci.yml           | 33 +++++++++++++++++++++++
 .vscode/tasks.json       |  9 +++++++
 cmd/root.go              |  5 ++--
 cmd/root_test.go         | 57 ++++++++++++++++++++++++++++++++++++++++
 main.go                  |  7 +++--
 sonar-project.properties |  8 ++++++
 testing/testing.go       | 16 +++++++++++
 vault/http.go            | 10 +++++--
 9 files changed, 142 insertions(+), 6 deletions(-)
 create mode 100644 cmd/root_test.go
 create mode 100644 testing/testing.go

diff --git a/.gitignore b/.gitignore
index 5f924b6..cec487f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,4 +26,7 @@ _testmain.go
 
 vendor/*/
 .env
+
+coverage.*
+
 vaultenv
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5bc29b5..0a72030 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -9,3 +9,36 @@ include:
     file: 'SonarQube.gitlab-ci.yml'
   - project: 'gitlab/templates'
     file: 'Go-CLI.gitlab-ci.yml'
+
+go-test:
+  image: golang:$GOLANG_VERSION
+  stage: test
+  rules:
+    - if: $CI_COMMIT_BRANCH
+  cache:
+    key:
+      files:
+        - go.mod
+        - go.sum
+    paths:
+      - .cache
+    policy: pull
+  services:
+    - name: hashicorp/vault:latest
+      alias: vault
+      command: ["server","-dev-kv-v1","-dev-root-token-id","00000000-0000-0000-0000-000000000000"]
+  script:
+    - |
+      export VAULT_ADDR=http://vault:8200
+      export VAULT_TOKEN=00000000-0000-0000-0000-000000000000
+      export GOPATH=${CI_PROJECT_DIR}/.cache
+      export PATH=${PATH}:${GOPATH}/bin
+      go test -covermode=count -coverprofile=coverage.txt $(go list ./... | grep -v /vendor/)
+      (gocover-cobertura < coverage.txt > coverage.xml) || true
+  artifacts:
+    paths:
+        - coverage.txt
+    reports:
+      coverage_report:
+        coverage_format: cobertura
+        path: coverage.xml
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index a2883d2..ad8b6c7 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -40,5 +40,14 @@
         "reveal": "silent"
       }
     },
+    {
+      "label": "Start vault dev server",
+      "type": "shell",
+      "command": "docker run --rm -it -p 8200:8200 --name vault-dev hashicorp/vault:latest server -dev-kv-v1 -dev-root-token-id 00000000-0000-0000-0000-000000000000",
+      "group": {
+        "kind": "test",
+        "isDefault": false
+      }
+    },
   ]
 }
diff --git a/cmd/root.go b/cmd/root.go
index 6035e42..1961d99 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -13,6 +13,7 @@ var RootCmd = &cobra.Command{
 	Short:             "Root Short",
 	Long:              `Root Long`,
 	DisableAutoGenTag: true,
+	SilenceUsage:      true,
 }
 
 func init() {
@@ -20,8 +21,8 @@ func init() {
 	RootCmd.PersistentFlags().StringP("addr", "a", "http://127.0.0.1:8200", "Address to the vault server")
 	RootCmd.PersistentFlags().StringP("token", "t", "", "Vault access token")
 	viper.SetEnvPrefix("VAULT")
-	viper.BindPFlag("ADDR", RootCmd.PersistentFlags().Lookup("addr"))
-	viper.BindPFlag("TOKEN", RootCmd.PersistentFlags().Lookup("token"))
+	_ = viper.BindPFlag("ADDR", RootCmd.PersistentFlags().Lookup("addr"))
+	_ = viper.BindPFlag("TOKEN", RootCmd.PersistentFlags().Lookup("token"))
 }
 
 func initEnv() {
diff --git a/cmd/root_test.go b/cmd/root_test.go
new file mode 100644
index 0000000..d1b9221
--- /dev/null
+++ b/cmd/root_test.go
@@ -0,0 +1,57 @@
+package cmd
+
+import (
+	"bytes"
+	"io"
+	"os"
+	"strings"
+	"sync"
+	"testing"
+
+	_ "gitlab.hedenstroem.com/go/vaultenv/testing"
+)
+
+func execute(t *testing.T, args string, input io.Reader) (string, error) {
+
+	osStdout := os.Stdout                   // keep backup of the real stdout
+	defer func() { os.Stdout = osStdout }() // restore the real stdout
+	r, w, _ := os.Pipe()
+	os.Stdout = w
+
+	output := new(bytes.Buffer)
+
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		_, err := io.Copy(output, r)
+		if err != nil {
+			t.Error(err)
+		}
+		wg.Done()
+	}()
+
+	RootCmd.SetIn(input)
+	RootCmd.SetOut(output) // usage messages
+	RootCmd.SetErr(output) // error messages
+	RootCmd.SetArgs(strings.Split(args, " "))
+	err := RootCmd.Execute()
+
+	w.Close()
+	wg.Wait()
+
+	return output.String(), err
+}
+
+func TestWriteCmd(t *testing.T) {
+	_, err := execute(t, "write secret/test hello world", nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	out, err := execute(t, "read secret/test", nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if strings.Contains(out, "\"hello\": \"world\"") == false {
+		t.Errorf("expected \"hello\": \"world\", got '%s'", out)
+	}
+}
diff --git a/main.go b/main.go
index ff9a66f..f5b508d 100644
--- a/main.go
+++ b/main.go
@@ -20,8 +20,11 @@
 
 package main
 
-import "gitlab.hedenstroem.com/go/vaultenv/cmd"
+import (
+	"github.com/spf13/cobra"
+	"gitlab.hedenstroem.com/go/vaultenv/cmd"
+)
 
 func main() {
-	cmd.RootCmd.Execute()
+	cobra.CheckErr(cmd.RootCmd.Execute())
 }
diff --git a/sonar-project.properties b/sonar-project.properties
index ee63546..ac9921a 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -1 +1,9 @@
 sonar.projectKey=go_vaultenv_AXyPJ7AsH35cfvcLwDFS
+
+sonar.sources=.
+sonar.exclusions=**/*_test.go
+
+sonar.tests=.
+sonar.test.inclusions=**/*_test.go
+
+sonar.go.coverage.reportPaths=coverage.txt
diff --git a/testing/testing.go b/testing/testing.go
new file mode 100644
index 0000000..413cb48
--- /dev/null
+++ b/testing/testing.go
@@ -0,0 +1,16 @@
+package testing
+
+import (
+	"os"
+	"path"
+	"runtime"
+)
+
+func init() {
+	_, filename, _, _ := runtime.Caller(0)
+	dir := path.Join(path.Dir(filename), "..")
+	err := os.Chdir(dir)
+	if err != nil {
+		panic(err)
+	}
+}
diff --git a/vault/http.go b/vault/http.go
index d3d76db..4628ab4 100644
--- a/vault/http.go
+++ b/vault/http.go
@@ -40,7 +40,10 @@ func GetSecret(path string) (data map[string]interface{}, err error) {
 	case http.StatusOK:
 		var parsed map[string]interface{}
 		defer res.Body.Close()
-		json.NewDecoder(res.Body).Decode(&parsed)
+		err = json.NewDecoder(res.Body).Decode(&parsed)
+		if err != nil {
+			return
+		}
 		data = parsed["data"].(map[string]interface{})
 	case http.StatusNoContent:
 		data = make(map[string]interface{})
@@ -52,7 +55,10 @@ func GetSecret(path string) (data map[string]interface{}, err error) {
 	default:
 		var parsed map[string]interface{}
 		defer res.Body.Close()
-		json.NewDecoder(res.Body).Decode(&parsed)
+		err = json.NewDecoder(res.Body).Decode(&parsed)
+		if err != nil {
+			return
+		}
 		err = &Error{
 			Status:  res.StatusCode,
 			Message: fmt.Sprint(parsed["errors"]),
-- 
GitLab