github.com/cornelk/go-cloud@v0.17.1/docstore/gcpfirestore/native_codec_test.go (about)

     1  // Copyright 2019 The Go Cloud Development Kit Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package gcpfirestore
    16  
    17  import (
    18  	"context"
    19  	"io"
    20  	"net"
    21  	"testing"
    22  
    23  	"cloud.google.com/go/firestore"
    24  	ts "github.com/golang/protobuf/ptypes/timestamp"
    25  	"github.com/google/go-cmp/cmp"
    26  	"google.golang.org/api/option"
    27  	pb "google.golang.org/genproto/googleapis/firestore/v1"
    28  	"google.golang.org/grpc"
    29  	"google.golang.org/grpc/metadata"
    30  )
    31  
    32  // A nativeCodec encodes and decodes structs using the cloud.google.com/go/firestore
    33  // client. Since that package doesn't export its codec, we have to go behind the
    34  // scenes and intercept traffic at the gRPC level. We use interceptors to do that. (A
    35  // mock server would have worked too.)
    36  type nativeCodec struct {
    37  	client *firestore.Client
    38  	doc    *pb.Document
    39  }
    40  
    41  func newNativeCodec() (*nativeCodec, error) {
    42  	// Establish a gRPC server, just so we have a connection to hang the interceptors on.
    43  	srv := grpc.NewServer()
    44  	l, err := net.Listen("tcp", "127.0.0.1:0")
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	go func() {
    49  		if err := srv.Serve(l); err != nil {
    50  			panic(err) // we should never get an error because we just connect and stop
    51  		}
    52  	}()
    53  	nc := &nativeCodec{}
    54  
    55  	conn, err := grpc.Dial(l.Addr().String(),
    56  		grpc.WithInsecure(),
    57  		grpc.WithBlock(),
    58  		grpc.WithUnaryInterceptor(nc.interceptUnary),
    59  		grpc.WithStreamInterceptor(nc.interceptStream))
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  	conn.Close()
    64  	srv.Stop()
    65  	nc.client, err = firestore.NewClient(context.Background(), "P", option.WithGRPCConn(conn))
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  	return nc, nil
    70  }
    71  
    72  // Intercept all unary (non-streaming) RPCs. The only one we should ever get is a Commit, for
    73  // the Create call in Encode.
    74  // If this completes successfully, the encoded *pb.Document will be in c.doc.
    75  func (c *nativeCodec) interceptUnary(_ context.Context, method string, req, res interface{}, _ *grpc.ClientConn, _ grpc.UnaryInvoker, _ ...grpc.CallOption) error {
    76  	c.doc = req.(*pb.CommitRequest).Writes[0].GetUpdate()
    77  	res.(*pb.CommitResponse).WriteResults = []*pb.WriteResult{{}}
    78  	return nil
    79  }
    80  
    81  // Intercept all streaming RPCs. The only one we should ever get is a BatchGet, for the Get
    82  // call in Decode.
    83  // Before this is called, c.doc must be set to the *pb.Document to be returned from the call.
    84  func (c *nativeCodec) interceptStream(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
    85  	return &clientStream{ctx: ctx, doc: c.doc}, nil
    86  }
    87  
    88  // clientStream is a fake client stream. It returns a single document, then terminates.
    89  type clientStream struct {
    90  	ctx context.Context
    91  	doc *pb.Document
    92  }
    93  
    94  func (cs *clientStream) RecvMsg(m interface{}) error {
    95  	if cs.doc != nil {
    96  		cs.doc.CreateTime = &ts.Timestamp{}
    97  		cs.doc.UpdateTime = &ts.Timestamp{}
    98  		m.(*pb.BatchGetDocumentsResponse).Result = &pb.BatchGetDocumentsResponse_Found{Found: cs.doc}
    99  		cs.doc = nil
   100  		return nil
   101  	}
   102  	return io.EOF
   103  }
   104  
   105  func (cs *clientStream) Context() context.Context     { return cs.ctx }
   106  func (cs *clientStream) SendMsg(m interface{}) error  { return nil }
   107  func (cs *clientStream) Header() (metadata.MD, error) { return nil, nil }
   108  func (cs *clientStream) Trailer() metadata.MD         { return nil }
   109  func (cs *clientStream) CloseSend() error             { return nil }
   110  
   111  // Encode a Go value into a Firestore proto document.
   112  func (c *nativeCodec) Encode(x interface{}) (*pb.Document, error) {
   113  	_, err := c.client.Collection("C").Doc("D").Create(context.Background(), x)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	return c.doc, nil
   118  }
   119  
   120  // Decode value, which must be a *pb.Document, into dest.
   121  func (c *nativeCodec) Decode(value *pb.Document, dest interface{}) error {
   122  	c.doc = value
   123  	docsnap, err := c.client.Collection("C").Doc("D").Get(context.Background())
   124  	if err != nil {
   125  		return err
   126  	}
   127  	return docsnap.DataTo(dest)
   128  }
   129  
   130  func TestNativeCodec(t *testing.T) {
   131  	nc, err := newNativeCodec()
   132  	if err != nil {
   133  		t.Fatal(err)
   134  	}
   135  	type S struct {
   136  		A int
   137  	}
   138  	want := S{3}
   139  	fields, err := nc.Encode(&want)
   140  	if err != nil {
   141  		t.Fatal(err)
   142  	}
   143  	var got S
   144  	if err := nc.Decode(fields, &got); err != nil {
   145  		t.Fatal(err)
   146  	}
   147  	if !cmp.Equal(got, want) {
   148  		t.Errorf("got %+v, want %+v", got, want)
   149  	}
   150  }