| package runtimeconfig |
| |
| import ( |
| "context" |
| "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" |
| ) |
| |
| // 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") |
| } |
| 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 |
| } |
| |
| func setUp(t *testing.T, fs *fakeServer) (*Client, func()) { |
| // Set up gRPC server. |
| lis, err := net.Listen("tcp", "localhost:0") |
| if err != nil { |
| t.Fatalf("tcp listen failed: %v", err) |
| } |
| s := grpc.NewServer() |
| pb.RegisterRuntimeConfigManagerServer(s, fs) |
| // Run gRPC server on a background goroutine. |
| go s.Serve(lis) |
| |
| // Set up client. |
| conn, err := grpc.Dial(lis.Addr().String(), 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) |
| } |
| } |