SEBA-688 implement mock gRPC server and unit test the version command

Change-Id: Ia10a8ea5b00ce5d5100f8fffbefba96f234d4b32
diff --git a/commands/command.go b/commands/command.go
index 6d975cf..4a38d5b 100644
--- a/commands/command.go
+++ b/commands/command.go
@@ -22,6 +22,7 @@
 	"github.com/opencord/cordctl/format"
 	"google.golang.org/grpc"
 	"gopkg.in/yaml.v2"
+	"io"
 	"io/ioutil"
 	"log"
 	"os"
@@ -37,6 +38,9 @@
 	OUTPUT_YAML
 )
 
+// Make it easy to override output stream for testing
+var OutputStream io.Writer = os.Stdout
+
 var CharReplacer = strings.NewReplacer("\\t", "\t", "\\n", "\n")
 
 type GrpcConfigSpec struct {
@@ -55,6 +59,7 @@
 	Server   string        `yaml:"server"`
 	Username string        `yaml:"username"`
 	Password string        `yaml:"password"`
+	Protoset string        `yaml:"protoset"`
 	Tls      TlsConfigSpec `yaml:"tls"`
 	Grpc     GrpcConfigSpec
 }
@@ -74,6 +79,7 @@
 	Server   string `short:"s" long:"server" default:"" value-name:"SERVER:PORT" description:"IP/Host and port of XOS"`
 	Username string `short:"u" long:"username" value-name:"USERNAME" default:"" description:"Username to authenticate with XOS"`
 	Password string `short:"p" long:"password" value-name:"PASSWORD" default:"" description:"Password to authenticate with XOS"`
+	Protoset string `long:"protoset" value-name:"FILENAME" description:"Load protobuf definitions from protoset instead of reflection api"`
 	Debug    bool   `short:"d" long:"debug" description:"Enable debug mode"`
 	UseTLS   bool   `long:"tls" description:"Use TLS"`
 	CACert   string `long:"tlscacert" value-name:"CA_CERT_FILE" description:"Trust certs signed only by this CA"`
@@ -133,6 +139,25 @@
 		}
 	}
 
+	// Override from environment
+	//    in particualr, for passing env vars via `go test`
+	env_server, present := os.LookupEnv("CORDCTL_SERVER")
+	if present {
+		GlobalConfig.Server = env_server
+	}
+	env_username, present := os.LookupEnv("CORDCTL_USERNAME")
+	if present {
+		GlobalConfig.Username = env_username
+	}
+	env_password, present := os.LookupEnv("CORDCTL_PASSWORD")
+	if present {
+		GlobalConfig.Password = env_password
+	}
+	env_protoset, present := os.LookupEnv("CORDCTL_PROTOSET")
+	if present {
+		GlobalConfig.Protoset = env_protoset
+	}
+
 	// Override from command line
 	if GlobalOptions.Server != "" {
 		GlobalConfig.Server = GlobalOptions.Server
@@ -143,6 +168,9 @@
 	if GlobalOptions.Password != "" {
 		GlobalConfig.Password = GlobalOptions.Password
 	}
+	if GlobalOptions.Protoset != "" {
+		GlobalConfig.Protoset = GlobalOptions.Protoset
+	}
 
 	// Generate error messages for required settings
 	if GlobalConfig.Server == "" {
@@ -165,19 +193,19 @@
 	if result != nil && result.Data != nil {
 		if result.OutputAs == OUTPUT_TABLE {
 			tableFormat := format.Format(result.Format)
-			tableFormat.Execute(os.Stdout, true, result.Data)
+			tableFormat.Execute(OutputStream, true, result.Data)
 		} else if result.OutputAs == OUTPUT_JSON {
 			asJson, err := json.Marshal(&result.Data)
 			if err != nil {
 				panic(err)
 			}
-			fmt.Printf("%s", asJson)
+			fmt.Fprintf(OutputStream, "%s", asJson)
 		} else if result.OutputAs == OUTPUT_YAML {
 			asYaml, err := yaml.Marshal(&result.Data)
 			if err != nil {
 				panic(err)
 			}
-			fmt.Printf("%s", asYaml)
+			fmt.Fprintf(OutputStream, "%s", asYaml)
 		}
 	}
 }
diff --git a/commands/common.go b/commands/common.go
index 9810300..f07e705 100644
--- a/commands/common.go
+++ b/commands/common.go
@@ -47,7 +47,19 @@
 	refClient := grpcreflect.NewClient(context.Background(), reflectpb.NewServerReflectionClient(conn))
 	defer refClient.Reset()
 
-	descriptor := grpcurl.DescriptorSourceFromServer(context.Background(), refClient)
+	// Intended method of use is to download the protos via reflection API. Loading the
+	// protos from a file is supported for unit testing, as the mock server does not
+	// support the reflection API.
+
+	var descriptor grpcurl.DescriptorSource
+	if GlobalConfig.Protoset != "" {
+		descriptor, err = grpcurl.DescriptorSourceFromProtoSets(GlobalConfig.Protoset)
+		if err != nil {
+			return nil, nil, err
+		}
+	} else {
+		descriptor = grpcurl.DescriptorSourceFromServer(context.Background(), refClient)
+	}
 
 	return conn, descriptor, nil
 }
diff --git a/commands/version.go b/commands/version.go
index 1941589..38d2f7a 100644
--- a/commands/version.go
+++ b/commands/version.go
@@ -101,26 +101,20 @@
 const DefaultFormat = ClientFormat + ServerFormat
 
 func (options *VersionOpts) Execute(args []string) error {
-
 	if !options.ClientOnly {
-		conn, err := NewConnection()
+		conn, descriptor, err := InitReflectionClient()
 		if err != nil {
 			return err
 		}
 		defer conn.Close()
 
-		descriptor, method, err := GetReflectionMethod(conn, "xos.utility.GetVersion")
-		if err != nil {
-			return err
-		}
-
 		ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
 		defer cancel()
 
 		headers := GenerateHeaders()
 
 		h := &RpcEventHandler{}
-		err = grpcurl.InvokeRPC(ctx, descriptor, conn, method, headers, h, h.GetParams)
+		err = grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.utility.GetVersion", headers, h, h.GetParams)
 		if err != nil {
 			return err
 		}
diff --git a/commands/version_test.go b/commands/version_test.go
new file mode 100644
index 0000000..1ebe7c3
--- /dev/null
+++ b/commands/version_test.go
@@ -0,0 +1,100 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original copyright 2019-present Ciena Corporation
+ *
+ * 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 commands
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/opencord/cordctl/testutils"
+	"os"
+	"testing"
+)
+
+func TestVersionClientOnly(t *testing.T) {
+	expected := "" +
+		"Client:\n" +
+		" Version         unknown-version\n" +
+		" Go version:     unknown-goversion\n" +
+		" Git commit:     unknown-gitcommit\n" +
+		" Git dirty:      unknown-gitdirty\n" +
+		" Built:          unknown-buildtime\n" +
+		" OS/Arch:        unknown-os/unknown-arch\n" +
+		"\n"
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options VersionOpts
+	options.ClientOnly = true
+	err := options.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	if got.String() != expected {
+		t.Logf("RECEIVED:\n%s\n", got.String())
+		t.Logf("EXPECTED:\n%s\n", expected)
+		t.Errorf("%s: expected and received did not match", t.Name())
+	}
+}
+
+func TestVersionClientAndServer(t *testing.T) {
+	expected := "" +
+		"Client:\n" +
+		" Version         unknown-version\n" +
+		" Go version:     unknown-goversion\n" +
+		" Git commit:     unknown-gitcommit\n" +
+		" Git dirty:      unknown-gitdirty\n" +
+		" Built:          unknown-buildtime\n" +
+		" OS/Arch:        unknown-os/unknown-arch\n" +
+		"\n" +
+		"Server:\n" +
+		" Version         3.2.6\n" +
+		" Python version: 2.7.16 (default, May  6 2019, 19:35:26)\n" +
+		" Git commit:     b0df1bf6ed1698285eda6a6725c5da0c80aa4aee\n" +
+		" Built:          2019-05-20T17:04:14Z\n" +
+		" OS/Arch:        linux/x86_64\n" +
+		"\n"
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options VersionOpts
+	err := options.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	if got.String() != expected {
+		t.Logf("RECEIVED:\n%s\n", got.String())
+		t.Logf("EXPECTED:\n%s\n", expected)
+		t.Errorf("%s: expected and received did not match", t.Name())
+	}
+}
+
+func TestMain(m *testing.M) {
+	err := testutils.StartMockServer("data.json")
+	if err != nil {
+		fmt.Printf("Error when initializing mock server %v", err)
+		os.Exit(-1)
+	}
+	os.Exit(m.Run())
+}