SEBA-688 add model tests

Change-Id: Ia50dc7aae5529a6e005645bc7461944caa82a329
diff --git a/commands/backup.go b/commands/backup.go
index 9d11db9..287236f 100644
--- a/commands/backup.go
+++ b/commands/backup.go
@@ -17,6 +17,7 @@
 package commands
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	flags "github.com/jessevdk/go-flags"
@@ -67,6 +68,8 @@
 	}
 	defer conn.Close()
 
+	ctx := context.Background() // TODO: Implement a sync timeout
+
 	// We might close and reopen the connection befor we do the DownloadFile,
 	// so make sure we've downloaded the service descriptor.
 	_, err = descriptor.FindSymbol("xos.filetransfer")
@@ -90,7 +93,7 @@
 	// STEP 2: Wait for the operation to complete
 
 	flags := GM_UNTIL_ENACTED | GM_UNTIL_FOUND | Ternary_uint32(options.Quiet, GM_QUIET, 0)
-	conn, completed_backupop, err := GetModelWithRetry(conn, descriptor, "BackupOperation", backupop["id"].(int32), flags)
+	conn, completed_backupop, err := GetModelWithRetry(ctx, conn, descriptor, "BackupOperation", backupop["id"].(int32), flags)
 	if err != nil {
 		return err
 	}
@@ -111,7 +114,7 @@
 		return errors.New("BackupOp.file_id is not set")
 	}
 
-	completed_backupfile, err := GetModel(conn, descriptor, "BackupFile", backupfile_id)
+	completed_backupfile, err := GetModel(ctx, conn, descriptor, "BackupFile", backupfile_id)
 	if err != nil {
 		return err
 	}
@@ -156,6 +159,8 @@
 	}
 	defer conn.Close()
 
+	ctx := context.Background() // TODO: Implement a sync timeout
+
 	local_name := options.Args.LocalFileName
 	remote_name := "cordctl-restore-" + time.Now().Format("20060102T150405Z")
 	uri := "file:///var/run/xos/backup/local/" + remote_name
@@ -209,7 +214,7 @@
 
 	flags := GM_UNTIL_ENACTED | GM_UNTIL_FOUND | GM_UNTIL_STATUS | Ternary_uint32(options.Quiet, GM_QUIET, 0)
 	queries := map[string]string{"uuid": backupop["uuid"].(string)}
-	conn, completed_backupop, err := FindModelWithRetry(conn, descriptor, "BackupOperation", queries, flags)
+	conn, completed_backupop, err := FindModelWithRetry(ctx, conn, descriptor, "BackupOperation", queries, flags)
 	if err != nil {
 		return err
 	}
diff --git a/commands/models.go b/commands/models.go
index 33c3793..fd820b2 100644
--- a/commands/models.go
+++ b/commands/models.go
@@ -17,10 +17,12 @@
 package commands
 
 import (
+	"context"
 	"fmt"
 	flags "github.com/jessevdk/go-flags"
 	"github.com/jhump/protoreflect/dynamic"
 	"strings"
+	"time"
 )
 
 const (
@@ -45,12 +47,13 @@
 
 type ModelUpdate struct {
 	OutputOptions
-	Unbuffered bool   `short:"u" long:"unbuffered" description:"Do not buffer console output and suppress default output processor"`
-	Filter     string `short:"f" long:"filter" description:"Comma-separated list of filters"`
-	SetFields  string `long:"set-field" description:"Comma-separated list of field=value to set"`
-	SetJSON    string `long:"set-json" description:"JSON dictionary to use for settings fields"`
-	Sync       bool   `long:"sync" description:"Synchronize before returning"`
-	Args       struct {
+	Unbuffered  bool          `short:"u" long:"unbuffered" description:"Do not buffer console output and suppress default output processor"`
+	Filter      string        `short:"f" long:"filter" description:"Comma-separated list of filters"`
+	SetFields   string        `long:"set-field" description:"Comma-separated list of field=value to set"`
+	SetJSON     string        `long:"set-json" description:"JSON dictionary to use for settings fields"`
+	Sync        bool          `long:"sync" description:"Synchronize before returning"`
+	SyncTimeout time.Duration `long:"synctimeout" default:"600s" description:"Timeout for --sync option"`
+	Args        struct {
 		ModelName ModelNameString
 	} `positional-args:"yes" required:"yes"`
 	IDArgs struct {
@@ -72,20 +75,22 @@
 
 type ModelCreate struct {
 	OutputOptions
-	Unbuffered bool   `short:"u" long:"unbuffered" description:"Do not buffer console output"`
-	SetFields  string `long:"set-field" description:"Comma-separated list of field=value to set"`
-	SetJSON    string `long:"set-json" description:"JSON dictionary to use for settings fields"`
-	Sync       bool   `long:"sync" description:"Synchronize before returning"`
-	Args       struct {
+	Unbuffered  bool          `short:"u" long:"unbuffered" description:"Do not buffer console output"`
+	SetFields   string        `long:"set-field" description:"Comma-separated list of field=value to set"`
+	SetJSON     string        `long:"set-json" description:"JSON dictionary to use for settings fields"`
+	Sync        bool          `long:"sync" description:"Synchronize before returning"`
+	SyncTimeout time.Duration `long:"synctimeout" default:"600s" description:"Timeout for --sync option"`
+	Args        struct {
 		ModelName ModelNameString
 	} `positional-args:"yes" required:"yes"`
 }
 
 type ModelSync struct {
 	OutputOptions
-	Unbuffered bool   `short:"u" long:"unbuffered" description:"Do not buffer console output and suppress default output processor"`
-	Filter     string `short:"f" long:"filter" description:"Comma-separated list of filters"`
-	Args       struct {
+	Unbuffered  bool          `short:"u" long:"unbuffered" description:"Do not buffer console output and suppress default output processor"`
+	Filter      string        `short:"f" long:"filter" description:"Comma-separated list of filters"`
+	SyncTimeout time.Duration `long:"synctimeout" default:"600s" description:"Timeout for synchronization"`
+	Args        struct {
 		ModelName ModelNameString
 	} `positional-args:"yes" required:"yes"`
 	IDArgs struct {
@@ -165,7 +170,7 @@
 		return err
 	}
 
-	models, err := ListOrFilterModels(conn, descriptor, string(options.Args.ModelName), queries)
+	models, err := ListOrFilterModels(context.Background(), conn, descriptor, string(options.Args.ModelName), queries)
 	if err != nil {
 		return err
 	}
@@ -255,13 +260,13 @@
 	if len(options.IDArgs.ID) > 0 {
 		models = make([]*dynamic.Message, len(options.IDArgs.ID))
 		for i, id := range options.IDArgs.ID {
-			models[i], err = GetModel(conn, descriptor, modelName, id)
+			models[i], err = GetModel(context.Background(), conn, descriptor, modelName, id)
 			if err != nil {
 				return err
 			}
 		}
 	} else {
-		models, err = ListOrFilterModels(conn, descriptor, modelName, queries)
+		models, err = ListOrFilterModels(context.Background(), conn, descriptor, modelName, queries)
 		if err != nil {
 			return err
 		}
@@ -300,11 +305,13 @@
 	}
 
 	if options.Sync {
+		ctx, cancel := context.WithTimeout(context.Background(), options.SyncTimeout)
+		defer cancel()
 		for i, model := range models {
 			id := model.GetFieldByName("id").(int32)
 			if modelStatusOutput.Rows[i].Message == "Updated" {
 				conditional_printf(!options.Quiet, "Wait for sync: %d ", id)
-				conn, _, err = GetModelWithRetry(conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
+				conn, _, err = GetModelWithRetry(ctx, conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
 				conditional_printf(!options.Quiet, "\n")
 				UpdateModelStatusOutput(&modelStatusOutput, i, id, "Enacted", err, true)
 			}
@@ -348,7 +355,7 @@
 	if len(options.IDArgs.ID) > 0 {
 		ids = options.IDArgs.ID
 	} else {
-		models, err := ListOrFilterModels(conn, descriptor, modelName, queries)
+		models, err := ListOrFilterModels(context.Background(), conn, descriptor, modelName, queries)
 		if err != nil {
 			return err
 		}
@@ -419,10 +426,12 @@
 	UpdateModelStatusOutput(&modelStatusOutput, 0, fields["id"], "Created", err, !options.Sync)
 
 	if options.Sync {
+		ctx, cancel := context.WithTimeout(context.Background(), options.SyncTimeout)
+		defer cancel()
 		if modelStatusOutput.Rows[0].Message == "Created" {
 			id := fields["id"].(int32)
 			conditional_printf(!options.Quiet, "Wait for sync: %d ", id)
-			conn, _, err = GetModelWithRetry(conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
+			conn, _, err = GetModelWithRetry(ctx, conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
 			conditional_printf(!options.Quiet, "\n")
 			UpdateModelStatusOutput(&modelStatusOutput, 0, id, "Enacted", err, true)
 		}
@@ -465,7 +474,7 @@
 	if len(options.IDArgs.ID) > 0 {
 		ids = options.IDArgs.ID
 	} else {
-		models, err := ListOrFilterModels(conn, descriptor, modelName, queries)
+		models, err := ListOrFilterModels(context.Background(), conn, descriptor, modelName, queries)
 		if err != nil {
 			return err
 		}
@@ -482,10 +491,13 @@
 		}
 	}
 
+	ctx, cancel := context.WithTimeout(context.Background(), options.SyncTimeout)
+	defer cancel()
+
 	modelStatusOutput := InitModelStatusOutput(options.Unbuffered, len(ids))
 	for i, id := range ids {
 		conditional_printf(!options.Quiet, "Wait for sync: %d ", id)
-		conn, _, err = GetModelWithRetry(conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
+		conn, _, err = GetModelWithRetry(ctx, conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
 		conditional_printf(!options.Quiet, "\n")
 		UpdateModelStatusOutput(&modelStatusOutput, i, id, "Enacted", err, true)
 	}
diff --git a/commands/models_test.go b/commands/models_test.go
new file mode 100644
index 0000000..2b44f8f
--- /dev/null
+++ b/commands/models_test.go
@@ -0,0 +1,376 @@
+/*
+ * 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"
+	"github.com/opencord/cordctl/testutils"
+	"testing"
+	"time"
+)
+
+func TestModelList(t *testing.T) {
+	// use `python -m json.tool` to pretty-print json
+	expected := `[
+		{
+			"controller_kind": "",
+			"controller_replica_count": 0,
+			"creator_id": 0,
+			"default_flavor_id": 0,
+			"default_image_id": 0,
+			"default_isolation": "",
+			"default_node_id": 0,
+			"description": "",
+			"enabled": false,
+			"exposed_ports": "",
+			"id": 1,
+			"max_instances": 0,
+			"mount_data_sets": "",
+			"name": "mockslice1",
+			"network": "",
+			"principal_id": 0,
+			"service_id": 0,
+			"site_id": 1,
+			"trust_domain_id": 0
+		},
+		{
+			"controller_kind": "",
+			"controller_replica_count": 0,
+			"creator_id": 0,
+			"default_flavor_id": 0,
+			"default_image_id": 0,
+			"default_isolation": "",
+			"default_node_id": 0,
+			"description": "",
+			"enabled": false,
+			"exposed_ports": "",
+			"id": 2,
+			"max_instances": 0,
+			"mount_data_sets": "",
+			"name": "mockslice2",
+			"network": "",
+			"principal_id": 0,
+			"service_id": 0,
+			"site_id": 1,
+			"trust_domain_id": 0
+		}
+	]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.List.Args.ModelName = "Slice"
+	options.List.OutputAs = "json"
+	err := options.List.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelListFilterID(t *testing.T) {
+	// use `python -m json.tool` to pretty-print json
+	expected := `[
+		{
+			"controller_kind": "",
+			"controller_replica_count": 0,
+			"creator_id": 0,
+			"default_flavor_id": 0,
+			"default_image_id": 0,
+			"default_isolation": "",
+			"default_node_id": 0,
+			"description": "",
+			"enabled": false,
+			"exposed_ports": "",
+			"id": 1,
+			"max_instances": 0,
+			"mount_data_sets": "",
+			"name": "mockslice1",
+			"network": "",
+			"principal_id": 0,
+			"service_id": 0,
+			"site_id": 1,
+			"trust_domain_id": 0
+		}
+	]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.List.Args.ModelName = "Slice"
+	options.List.OutputAs = "json"
+	options.List.Filter = "id=1"
+	err := options.List.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelListFilterName(t *testing.T) {
+	// use `python -m json.tool` to pretty-print json
+	expected := `[
+		{
+			"controller_kind": "",
+			"controller_replica_count": 0,
+			"creator_id": 0,
+			"default_flavor_id": 0,
+			"default_image_id": 0,
+			"default_isolation": "",
+			"default_node_id": 0,
+			"description": "",
+			"enabled": false,
+			"exposed_ports": "",
+			"id": 2,
+			"max_instances": 0,
+			"mount_data_sets": "",
+			"name": "mockslice2",
+			"network": "",
+			"principal_id": 0,
+			"service_id": 0,
+			"site_id": 1,
+			"trust_domain_id": 0
+		}
+	]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.List.Args.ModelName = "Slice"
+	options.List.OutputAs = "json"
+	options.List.Filter = "name=mockslice2"
+	err := options.List.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelUpdate(t *testing.T) {
+	expected := `[{"id":1, "message":"Updated"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Update.Args.ModelName = "Slice"
+	options.Update.OutputAs = "json"
+	options.Update.IDArgs.ID = []int32{1}
+	options.Update.SetFields = "name=mockslice1_newname"
+	err := options.Update.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelUpdateUsingFilter(t *testing.T) {
+	expected := `[{"id":1, "message":"Updated"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Update.Args.ModelName = "Slice"
+	options.Update.OutputAs = "json"
+	options.Update.Filter = "id=1"
+	options.Update.SetFields = "name=mockslice1_newname"
+	err := options.Update.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelUpdateNoExist(t *testing.T) {
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Update.Args.ModelName = "Slice"
+	options.Update.OutputAs = "json"
+	options.Update.IDArgs.ID = []int32{77}
+	options.Update.SetFields = "name=mockslice1_newname"
+	err := options.Update.Execute([]string{})
+	testutils.AssertErrorEqual(t, err, "rpc error: code = Unknown desc = Slice matching query does not exist.")
+}
+
+func TestModelUpdateUsingFilterNoExist(t *testing.T) {
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Update.Args.ModelName = "Slice"
+	options.Update.OutputAs = "json"
+	options.Update.Filter = "id=77"
+	options.Update.SetFields = "name=mockslice1_newname"
+	err := options.Update.Execute([]string{})
+
+	testutils.AssertErrorEqual(t, err, "Filter matches no objects")
+}
+
+func TestModelCreate(t *testing.T) {
+	expected := `[{"id":3, "message":"Created"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Create.Args.ModelName = "Slice"
+	options.Create.OutputAs = "json"
+	options.Create.SetFields = "name=mockslice3,site_id=1"
+	err := options.Create.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelDelete(t *testing.T) {
+	expected := `[{"id":1, "message":"Deleted"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Delete.Args.ModelName = "Slice"
+	options.Delete.OutputAs = "json"
+	options.Delete.IDArgs.ID = []int32{1}
+	err := options.Delete.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelDeleteUsingFilter(t *testing.T) {
+	expected := `[{"id":1, "message":"Deleted"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Delete.Args.ModelName = "Slice"
+	options.Delete.OutputAs = "json"
+	options.Delete.Filter = "id=1"
+	err := options.Delete.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelDeleteNoExist(t *testing.T) {
+	expected := `[{"id":77, "message":"Slice matching query does not exist."}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Delete.Args.ModelName = "Slice"
+	options.Delete.OutputAs = "json"
+	options.Delete.IDArgs.ID = []int32{77}
+	err := options.Delete.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelDeleteFilterNoExist(t *testing.T) {
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Delete.Args.ModelName = "Slice"
+	options.Delete.OutputAs = "json"
+	options.Delete.Filter = "id=77"
+	err := options.Delete.Execute([]string{})
+	testutils.AssertErrorEqual(t, err, "Filter matches no objects")
+}
+
+func TestModelSync(t *testing.T) {
+	expected := `[{"id":1, "message":"Enacted"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Sync.Args.ModelName = "Slice"
+	options.Sync.OutputAs = "json"
+	options.Sync.IDArgs.ID = []int32{1}
+	options.Sync.SyncTimeout = 5 * time.Second
+	err := options.Sync.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelSyncTimeout(t *testing.T) {
+	expected := `[{"id":2, "message":"context deadline exceeded"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Sync.Args.ModelName = "Slice"
+	options.Sync.OutputAs = "json"
+	options.Sync.IDArgs.ID = []int32{2}
+	options.Sync.SyncTimeout = 5 * time.Second
+	err := options.Sync.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
diff --git a/commands/orm.go b/commands/orm.go
index 87dee55..a817ea5 100644
--- a/commands/orm.go
+++ b/commands/orm.go
@@ -307,8 +307,8 @@
 }
 
 // Get a model from XOS given its ID
-func GetModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, id int32) (*dynamic.Message, error) {
-	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+func GetModel(ctx context.Context, conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, id int32) (*dynamic.Message, error) {
+	ctx, cancel := context.WithTimeout(ctx, GlobalConfig.Grpc.Timeout)
 	defer cancel()
 
 	headers := GenerateHeaders()
@@ -334,7 +334,7 @@
 }
 
 // Get a model, but retry under a variety of circumstances
-func GetModelWithRetry(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, id int32, flags uint32) (*grpc.ClientConn, *dynamic.Message, error) {
+func GetModelWithRetry(ctx context.Context, conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, id int32, flags uint32) (*grpc.ClientConn, *dynamic.Message, error) {
 	quiet := (flags & GM_QUIET) != 0
 	until_found := (flags & GM_UNTIL_FOUND) != 0
 	until_enacted := (flags & GM_UNTIL_ENACTED) != 0
@@ -350,14 +350,18 @@
 			}
 		}
 
-		model, err := GetModel(conn, descriptor, modelName, id)
+		model, err := GetModel(ctx, conn, descriptor, modelName, id)
 		if err != nil {
 			if strings.Contains(err.Error(), "rpc error: code = Unavailable") ||
 				strings.Contains(err.Error(), "rpc error: code = Internal desc = stream terminated by RST_STREAM") {
 				if !quiet {
 					fmt.Print(".")
 				}
-				time.Sleep(100 * time.Millisecond)
+				select {
+				case <-time.After(100 * time.Millisecond):
+				case <-ctx.Done():
+					return nil, nil, ctx.Err()
+				}
 				conn.Close()
 				conn = nil
 				continue
@@ -367,7 +371,11 @@
 				if !quiet {
 					fmt.Print("x")
 				}
-				time.Sleep(100 * time.Millisecond)
+				select {
+				case <-time.After(100 * time.Millisecond):
+				case <-ctx.Done():
+					return nil, nil, ctx.Err()
+				}
 				continue
 			}
 			return nil, nil, err
@@ -377,7 +385,11 @@
 			if !quiet {
 				fmt.Print("o")
 			}
-			time.Sleep(100 * time.Millisecond)
+			select {
+			case <-time.After(100 * time.Millisecond):
+			case <-ctx.Done():
+				return nil, nil, ctx.Err()
+			}
 			continue
 		}
 
@@ -385,7 +397,11 @@
 			if !quiet {
 				fmt.Print("O")
 			}
-			time.Sleep(100 * time.Millisecond)
+			select {
+			case <-time.After(100 * time.Millisecond):
+			case <-ctx.Done():
+				return nil, nil, ctx.Err()
+			}
 			continue
 		}
 
@@ -402,8 +418,8 @@
 }
 
 // List all objects of a given model
-func ListModels(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string) ([]*dynamic.Message, error) {
-	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+func ListModels(ctx context.Context, conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string) ([]*dynamic.Message, error) {
+	ctx, cancel := context.WithTimeout(ctx, GlobalConfig.Grpc.Timeout)
 	defer cancel()
 
 	headers := GenerateHeaders()
@@ -435,8 +451,8 @@
 //   queries is a map of <field_name> to <operator><query>
 //   For example,
 //     map[string]string{"name": "==mysite"}
-func FilterModels(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) ([]*dynamic.Message, error) {
-	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+func FilterModels(ctx context.Context, conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) ([]*dynamic.Message, error) {
+	ctx, cancel := context.WithTimeout(ctx, GlobalConfig.Grpc.Timeout)
 	defer cancel()
 
 	headers := GenerateHeaders()
@@ -481,17 +497,17 @@
 }
 
 // Call ListModels or FilterModels as appropriate
-func ListOrFilterModels(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) ([]*dynamic.Message, error) {
+func ListOrFilterModels(ctx context.Context, conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) ([]*dynamic.Message, error) {
 	if len(queries) == 0 {
-		return ListModels(conn, descriptor, modelName)
+		return ListModels(ctx, conn, descriptor, modelName)
 	} else {
-		return FilterModels(conn, descriptor, modelName, queries)
+		return FilterModels(ctx, conn, descriptor, modelName, queries)
 	}
 }
 
 // Get a model from XOS given a fieldName/fieldValue
-func FindModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) (*dynamic.Message, error) {
-	models, err := FilterModels(conn, descriptor, modelName, queries)
+func FindModel(ctx context.Context, conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) (*dynamic.Message, error) {
+	models, err := FilterModels(ctx, conn, descriptor, modelName, queries)
 	if err != nil {
 		return nil, err
 	}
@@ -504,7 +520,7 @@
 }
 
 // Find a model, but retry under a variety of circumstances
-func FindModelWithRetry(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string, flags uint32) (*grpc.ClientConn, *dynamic.Message, error) {
+func FindModelWithRetry(ctx context.Context, conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string, flags uint32) (*grpc.ClientConn, *dynamic.Message, error) {
 	quiet := (flags & GM_QUIET) != 0
 	until_found := (flags & GM_UNTIL_FOUND) != 0
 	until_enacted := (flags & GM_UNTIL_ENACTED) != 0
@@ -520,14 +536,18 @@
 			}
 		}
 
-		model, err := FindModel(conn, descriptor, modelName, queries)
+		model, err := FindModel(ctx, conn, descriptor, modelName, queries)
 		if err != nil {
 			if strings.Contains(err.Error(), "rpc error: code = Unavailable") ||
 				strings.Contains(err.Error(), "rpc error: code = Internal desc = stream terminated by RST_STREAM") {
 				if !quiet {
 					fmt.Print(".")
 				}
-				time.Sleep(100 * time.Millisecond)
+				select {
+				case <-time.After(100 * time.Millisecond):
+				case <-ctx.Done():
+					return nil, nil, ctx.Err()
+				}
 				conn.Close()
 				conn = nil
 				continue
@@ -537,7 +557,11 @@
 				if !quiet {
 					fmt.Print("x")
 				}
-				time.Sleep(100 * time.Millisecond)
+				select {
+				case <-time.After(100 * time.Millisecond):
+				case <-ctx.Done():
+					return nil, nil, ctx.Err()
+				}
 				continue
 			}
 			return nil, nil, err
@@ -547,7 +571,11 @@
 			if !quiet {
 				fmt.Print("o")
 			}
-			time.Sleep(100 * time.Millisecond)
+			select {
+			case <-time.After(100 * time.Millisecond):
+			case <-ctx.Done():
+				return nil, nil, ctx.Err()
+			}
 			continue
 		}
 
@@ -555,7 +583,11 @@
 			if !quiet {
 				fmt.Print("O")
 			}
-			time.Sleep(100 * time.Millisecond)
+			select {
+			case <-time.After(100 * time.Millisecond):
+			case <-ctx.Done():
+				return nil, nil, ctx.Err()
+			}
 			continue
 		}
 
diff --git a/commands/setup_test.go b/commands/setup_test.go
new file mode 100644
index 0000000..5c1ec83
--- /dev/null
+++ b/commands/setup_test.go
@@ -0,0 +1,35 @@
+/*
+ * 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 (
+	"fmt"
+	"github.com/opencord/cordctl/testutils"
+	"os"
+	"testing"
+)
+
+// This TestMain is global to all tests in the `commands` package
+
+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())
+}
diff --git a/commands/version_test.go b/commands/version_test.go
index 1ebe7c3..6835fc2 100644
--- a/commands/version_test.go
+++ b/commands/version_test.go
@@ -18,9 +18,6 @@
 
 import (
 	"bytes"
-	"fmt"
-	"github.com/opencord/cordctl/testutils"
-	"os"
 	"testing"
 )
 
@@ -89,12 +86,3 @@
 		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())
-}
diff --git a/mock/data.json b/mock/data.json
index 2c610f9..710f423 100644
--- a/mock/data.json
+++ b/mock/data.json
@@ -13,22 +13,112 @@
     },
     {
         "method": "GetSlice",
-        "input": ".*",
+        "input": {"id": 1},
         "output": {
           "id": 1,
           "name": "mockslice1",
-          "site_id": 1
+          "site_id": 1,
+          "updated": 1234.0,
+          "enacted": 1234.0
         }
     },
     {
+        "method": "GetSlice",
+        "input": {"id": 2},
+        "output": {
+          "id": 2,
+          "name": "mockslice2",
+          "site_id": 1,
+          "updated": 1234.0,
+          "enacted": 900.0
+        }
+    },
+    {
+        "method": "GetSlice",
+        "input": {"id": 77},
+        "error": { "code": 2, "message": "Slice matching query does not exist."}
+    },
+    {
         "method": "ListSlice",
         "input": ".*",
         "output": {
           "items": [{
             "id": 1,
             "name": "mockslice1",
+            "site_id": 1, 
+            "updated": 1234.0,
+            "enacted": 1234.0
+          },
+          {
+            "id": 2,
+            "name": "mockslice2",
+            "site_id": 1,
+            "updated": 1234.0,
+            "enacted": 900.0
+          }]
+        }
+    },
+    {
+        "method": "FilterSlice",
+        "input": {"elements": [{"operator": 0, "name": "id", "iValue": 1}]},
+        "output": {
+          "items": [{
+            "id": 1,
+            "name": "mockslice1",
             "site_id": 1
           }]
         }
+    },
+    {
+        "method": "FilterSlice",
+        "input": {"elements": [{"operator": 0, "name": "name", "sValue": "mockslice2"}]},
+        "output": {
+          "items": [{
+            "id": 2,
+            "name": "mockslice2",
+            "site_id": 1
+          }]
+        }
+    },
+    {
+        "method": "FilterSlice",
+        "input": {"elements": [{"operator": 0, "name": "id", "iValue": 77}]},
+        "output": {
+          "items": []
+        }
+    },
+    {
+        "method": "UpdateSlice",
+        "input": {"id": 1, "name": "mockslice1_newname"},
+        "output": {
+            "id": 1,
+            "name": "mockslice1_newname",
+            "site_id": 1
+        }
+    },
+    {
+        "method": "UpdateSlice",
+        "input": { "id": 77, "name": "mockslice1_newname"},
+        "error": { "code": 2, "message": "Slice matching query does not exist."}
+    },
+    {
+        "method": "CreateSlice",
+        "input": {"name": "mockslice3", "site_id": 1},
+        "output": {
+            "id": 3,
+            "name": "mockslice3",
+            "site_id": 1
+        }
+    },
+    {
+        "method": "DeleteSlice",
+        "input": {"id": 1},
+        "output": {}
+    },
+    {
+        "method": "DeleteSlice",
+        "input": {"id": 77},
+        "error": { "code": 2, "message": "Slice matching query does not exist."}
     }
+
 ]
diff --git a/testutils/testutils.go b/testutils/testutils.go
index da290b8..64fdbcb 100644
--- a/testutils/testutils.go
+++ b/testutils/testutils.go
@@ -16,10 +16,14 @@
 package testutils
 
 import (
+	"encoding/json"
+	"errors"
 	"fmt"
 	"os"
 	"os/exec"
+	"reflect"
 	"strings"
+	"testing"
 )
 
 const (
@@ -91,3 +95,39 @@
 
 	return strings.Contains(string(out), "Listening for requests"), nil
 }
+
+// Assert that two JSON-encoded strings are equal
+func AssertJSONEqual(t *testing.T, actual string, expected string) error {
+	var expected_json interface{}
+	err := json.Unmarshal([]byte(expected), &expected_json)
+	if err != nil {
+		t.Errorf("Failed to unmarshal expected json %s", expected)
+		return err
+	}
+
+	var actual_json interface{}
+	err = json.Unmarshal([]byte(actual), &actual_json)
+	if err != nil {
+		t.Errorf("Failed to unmarshal actual json %s", actual_json)
+		return err
+	}
+
+	if !reflect.DeepEqual(expected_json, actual_json) {
+		t.Errorf("Actual json does not match expected json\nACTUAL:\n%s\nEXPECTED:\n%s", actual, expected)
+	}
+
+	return nil
+}
+
+// Assert that the error string is what we expect
+func AssertErrorEqual(t *testing.T, err error, expected string) error {
+	if err == nil {
+		t.Error("Expected an error, but received nil")
+		return errors.New("AssertErrorEqual")
+	}
+	if err.Error() != expected {
+		t.Errorf("Expected error `%s` but received actual error `%s`", expected, err.Error())
+		return errors.New("AssertErrorEqual")
+	}
+	return nil
+}