github.com/thiagoyeds/go-cloud@v0.26.0/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 "github.com/google/go-cmp/cmp" 25 "google.golang.org/api/option" 26 pb "google.golang.org/genproto/googleapis/firestore/v1" 27 "google.golang.org/grpc" 28 "google.golang.org/grpc/metadata" 29 tspb "google.golang.org/protobuf/types/known/timestamppb" 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 = &tspb.Timestamp{} 97 cs.doc.UpdateTime = &tspb.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 }