internal/runtimeconfig: add tests.

Allow user to set any minimum value for wait time > 0.

Change-Id: I61592707f840029c6f401e383d66fd9608b993f5
Reviewed-on: https://go-review.googlesource.com/85655
Reviewed-by: Ross Light <light@google.com>
diff --git a/internal/runtimeconfig/example_test.go b/internal/runtimeconfig/example_test.go
new file mode 100644
index 0000000..dcf0517
--- /dev/null
+++ b/internal/runtimeconfig/example_test.go
@@ -0,0 +1,45 @@
+package runtimeconfig_test
+
+import (
+	"context"
+	"log"
+
+	"github.com/golang/gddo/internal/runtimeconfig"
+)
+
+func Example() {
+	// Create a Client object.
+	ctx := context.Background()
+	client, err := runtimeconfig.NewClient(ctx)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer client.Close()
+
+	// Create a Watcher object.
+	w, err := client.NewWatcher(ctx, "project", "config-name", "food", nil)
+	// Use retrieved Variable and apply to configurations accordingly.
+	log.Printf("value: %s\n", string(w.Variable().Value))
+
+	// Optionally, get a Context with cancel func to stop the Watch call.
+	ctx, cancel := context.WithCancel(ctx)
+	defer cancel()
+
+	// Have a separate goroutine that waits for changes.
+	go func() {
+		for {
+			select {
+			case <-ctx.Done():
+				// Cancelled or timed out.
+				return
+			default:
+				if err := w.Watch(ctx); err != nil {
+					// Log or handle other errors
+					continue
+				}
+				// Use updated variable accordingly.
+				log.Printf("value: %s\n", string(w.Variable().Value))
+			}
+		}
+	}()
+}
diff --git a/internal/runtimeconfig/runtimeconfig.go b/internal/runtimeconfig/runtimeconfig.go
index a29d1f6..9cc8c61 100644
--- a/internal/runtimeconfig/runtimeconfig.go
+++ b/internal/runtimeconfig/runtimeconfig.go
@@ -23,8 +23,12 @@
 	"google.golang.org/grpc/status"
 )
 
-// endpoint is the address of the GCP Runtime Configurator API.
-const endPoint = "runtimeconfig.googleapis.com:443"
+const (
+	// endpoint is the address of the GCP Runtime Configurator API.
+	endPoint = "runtimeconfig.googleapis.com:443"
+	// defaultWaitTimeout is the default value for WatchOptions.WaitTime if not set.
+	defaultWaitTimeout = 10 * time.Minute
+)
 
 // List of authentication scopes required for using the Runtime Configurator API.
 var authScopes = []string{
@@ -32,11 +36,6 @@
 	"https://www.googleapis.com/auth/cloudruntimeconfig",
 }
 
-const (
-	defaultWaitTimeout = 10 * time.Minute
-	minWaitTimeout     = 10 * time.Second
-)
-
 // Client is a RuntimeConfigManager client.  It wraps the gRPC client stub and currently exposes
 // only a few APIs primarily for fetching and watching over configuration variables.
 type Client struct {
@@ -82,8 +81,8 @@
 	switch {
 	case waitTime == 0:
 		waitTime = defaultWaitTimeout
-	case waitTime < minWaitTimeout:
-		waitTime = minWaitTimeout
+	case waitTime < 0:
+		return nil, fmt.Errorf("cannot have negative WaitTime option value: %v", waitTime)
 	}
 
 	// Make sure update time is valid before copying.
@@ -105,10 +104,10 @@
 type WatchOptions struct {
 	// WaitTime controls the frequency of making RPC and checking for updates by the Watch method.
 	// A Watcher keeps track of the last time it made an RPC, when Watch is called, it waits for
-	// configured WaitTime from the last RPC before making another RPC.
+	// configured WaitTime from the last RPC before making another RPC. The smaller the value, the
+	// higher the frequency of making RPCs, which also means faster rate of hitting the API quota.
 	//
-	// If this option is not set, it defaults to defaultWaitTimeout.  If option is set to a value
-	// smaller than minWaitTimeout, it uses minWaitTimeout value instead.
+	// If this option is not set or set to 0, it uses defaultWaitTimeout value.
 	WaitTime time.Duration
 }
 
diff --git a/internal/runtimeconfig/runtimeconfig_test.go b/internal/runtimeconfig/runtimeconfig_test.go
index ee8178e..35fb3e8 100644
--- a/internal/runtimeconfig/runtimeconfig_test.go
+++ b/internal/runtimeconfig/runtimeconfig_test.go
@@ -2,42 +2,267 @@
 
 import (
 	"context"
-	"log"
+	"fmt"
+	"net"
+	"strings"
+	"testing"
+	"time"
+
+	tspb "github.com/golang/protobuf/ptypes/timestamp"
+	"github.com/google/go-cmp/cmp"
+	pb "google.golang.org/genproto/googleapis/cloud/runtimeconfig/v1beta1"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
 )
 
-func Example() {
-	// Create a Client object.
-	ctx := context.Background()
-	client, err := NewClient(ctx)
-	if err != nil {
-		log.Fatal(err)
+// Set wait timeout used for tests.
+var watchOpt = &WatchOptions{
+	WaitTime: 500 * time.Millisecond,
+}
+
+// fakeServer partially implements RuntimeConfigManagerServer for Client to connect to.  Prefill
+// responses field with the ordered list of responses to GetVariable calls.
+type fakeServer struct {
+	pb.RuntimeConfigManagerServer
+	responses []response
+	index     int
+}
+
+type response struct {
+	vrbl *pb.Variable
+	err  error
+}
+
+func (s *fakeServer) GetVariable(context.Context, *pb.GetVariableRequest) (*pb.Variable, error) {
+	if len(s.responses) == 0 {
+		return nil, fmt.Errorf("fakeClient missing responses")
 	}
-	defer client.Close()
+	resp := s.responses[s.index]
+	// Adjust index to next response for next call till it gets to last one, then keep using the
+	// last one.
+	if s.index < len(s.responses)-1 {
+		s.index++
+	}
+	return resp.vrbl, resp.err
+}
 
-	// Create a Watcher object.
-	w, err := client.NewWatcher(ctx, "project", "config-name", "food", nil)
-	// Use retrieved Variable and apply to configurations accordingly.
-	log.Printf("value: %s\n", string(w.Variable().Value))
+func setUp(t *testing.T, fs *fakeServer) (*Client, func()) {
+	// TODO: Replace logic to use a port picker.
+	const address = "localhost:8888"
+	// Set up gRPC server.
+	lis, err := net.Listen("tcp", address)
+	if err != nil {
+		t.Fatalf("tcp listen on %s failed: %v", address, err)
+	}
+	s := grpc.NewServer()
+	pb.RegisterRuntimeConfigManagerServer(s, fs)
+	// Run gRPC server on a background goroutine.
+	go s.Serve(lis)
 
-	// Optionally, get a Context with cancel func to stop the Watch call.
-	ctx, cancel := context.WithCancel(ctx)
-	defer cancel()
-
-	// Have a separate goroutine that waits for changes.
-	go func() {
-		for {
-			select {
-			case <-ctx.Done():
-				// Cancelled or timed out.
-				return
-			default:
-				if err := w.Watch(ctx); err != nil {
-					// Log or handle other errors
-					continue
-				}
-				// Use updated variable accordingly.
-				log.Printf("value: %s\n", string(w.Variable().Value))
-			}
+	// Set up client.
+	conn, err := grpc.Dial(address, grpc.WithInsecure())
+	if err != nil {
+		t.Fatalf("did not connect: %v", err)
+	}
+	return &Client{
+			conn:   conn,
+			client: pb.NewRuntimeConfigManagerClient(conn),
+		}, func() {
+			conn.Close()
+			s.Stop()
+			time.Sleep(time.Second * 1)
 		}
+}
+
+func pbToVariable(vpb *pb.Variable) (*Variable, error) {
+	vrbl := &Variable{}
+	tm, err := parseUpdateTime(vpb)
+	if err != nil {
+		return nil, err
+	}
+	copyFromProto(vpb, vrbl, tm)
+	return vrbl, nil
+}
+
+var (
+	startTime = time.Now().Unix()
+	vrbl1     = &pb.Variable{
+		Name:       "greetings",
+		Contents:   &pb.Variable_Text{"hello"},
+		UpdateTime: &tspb.Timestamp{Seconds: startTime},
+	}
+	vrbl2 = &pb.Variable{
+		Name:       "greetings",
+		Contents:   &pb.Variable_Text{"world"},
+		UpdateTime: &tspb.Timestamp{Seconds: startTime + 100},
+	}
+)
+
+func TestNewWatcher(t *testing.T) {
+	client, cleanUp := setUp(t, &fakeServer{
+		responses: []response{
+			{vrbl: vrbl1},
+		},
+	})
+	defer cleanUp()
+
+	ctx := context.Background()
+	w, err := client.NewWatcher(ctx, "projectID", "config", "greetings", watchOpt)
+	if err != nil {
+		t.Fatalf("Client.NewWatcher() returned error: %v", err)
+	}
+
+	got := w.Variable()
+	want, err := pbToVariable(vrbl1)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if diff := cmp.Diff(&got, want); diff != "" {
+		t.Errorf("Watcher.Variable(): %s", diff)
+	}
+}
+
+func TestWatchUpdatesVariable(t *testing.T) {
+	client, cleanUp := setUp(t, &fakeServer{
+		responses: []response{
+			{vrbl: vrbl1},
+			{vrbl: vrbl2},
+		},
+	})
+	defer cleanUp()
+
+	ctx := context.Background()
+	w, err := client.NewWatcher(ctx, "projectID", "config", "greetings", watchOpt)
+	if err != nil {
+		t.Fatalf("Client.NewWatcher() returned error: %v", err)
+	}
+
+	if err := w.Watch(ctx); err != nil {
+		t.Fatalf("Watcher.Watch() returned error: %v", err)
+	}
+	got := w.Variable()
+	want, err := pbToVariable(vrbl2)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if diff := cmp.Diff(&got, want); diff != "" {
+		t.Errorf("Watcher.Variable(): %s", diff)
+	}
+}
+
+func TestWatchVariableDeletedAndReset(t *testing.T) {
+	client, cleanUp := setUp(t, &fakeServer{
+		responses: []response{
+			{vrbl: vrbl1},
+			{err: status.Error(codes.NotFound, "deleted")},
+			{vrbl: vrbl2},
+		},
+	})
+	defer cleanUp()
+
+	ctx := context.Background()
+	w, err := client.NewWatcher(ctx, "projectID", "config", "greetings", watchOpt)
+	if err != nil {
+		t.Fatalf("Client.NewWatcher() returned error: %v", err)
+	}
+
+	// Expect deleted error.
+	if err := w.Watch(ctx); err == nil {
+		t.Fatalf("Watcher.Watch() returned nil, want error")
+	} else {
+		if !IsDeleted(err) {
+			t.Fatalf("Watcher.Watch() returned error %v, want errDeleted", err)
+		}
+	}
+
+	// Variable Name and Value should be the same, IsDeleted and UpdateTime should be updated.
+	got := w.Variable()
+	prev, err := pbToVariable(vrbl1)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if got.Name != prev.Name {
+		t.Errorf("Variable.Name got %v, want %v", got.Name, prev.Name)
+	}
+	if diff := cmp.Diff(got.Value, prev.Value); diff != "" {
+		t.Errorf("Variable.Value: %s", diff)
+	}
+	if !got.IsDeleted {
+		t.Errorf("Variable.IsDeleted got %v, want true", got.IsDeleted)
+	}
+	if !got.UpdateTime.After(prev.UpdateTime) {
+		t.Errorf("Variable.UpdateTime is less than or equal to previous value")
+	}
+
+	// Calling Watch again will produce vrbl2.
+	if err := w.Watch(ctx); err != nil {
+		t.Fatalf("Watcher.Watch() returned error: %v", err)
+	}
+	got = w.Variable()
+	want, err := pbToVariable(vrbl2)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if diff := cmp.Diff(&got, want); diff != "" {
+		t.Errorf("Watcher.Variable(): %s", diff)
+	}
+}
+
+func TestWatchCancelled(t *testing.T) {
+	client, cleanUp := setUp(t, &fakeServer{
+		responses: []response{
+			{vrbl: vrbl1},
+		},
+	})
+	defer cleanUp()
+
+	ctx := context.Background()
+	w, err := client.NewWatcher(ctx, "projectID", "config", "greetings", watchOpt)
+	if err != nil {
+		t.Fatalf("Client.NewWatcher() returned error: %v", err)
+	}
+
+	ctx, cancel := context.WithCancel(ctx)
+	go func() {
+		time.Sleep(1 * time.Second)
+		cancel()
 	}()
+
+	if err := w.Watch(ctx); err != context.Canceled {
+		t.Fatalf("Watcher.Watch() returned %v, want context.Canceled", err)
+	}
+}
+
+func TestWatchRPCError(t *testing.T) {
+	rpcErr := status.Error(codes.Internal, "bad")
+	client, cleanUp := setUp(t, &fakeServer{
+		responses: []response{
+			{vrbl: vrbl1},
+			{err: rpcErr},
+		},
+	})
+	defer cleanUp()
+
+	ctx := context.Background()
+	w, err := client.NewWatcher(ctx, "projectID", "config", "greetings", watchOpt)
+	if err != nil {
+		t.Fatalf("Client.NewWatcher() returned error: %v", err)
+	}
+
+	// Expect RPC error.
+	err = w.Watch(ctx)
+	if !strings.Contains(err.Error(), "bad") {
+		t.Errorf("Watcher.Watch() returned %v, want %v", err, rpcErr)
+	}
+
+	// Variable should still be the same.
+	got := w.Variable()
+	want, err := pbToVariable(vrbl1)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if diff := cmp.Diff(&got, want); diff != "" {
+		t.Errorf("Watcher.Variable(): %s", diff)
+	}
 }