storj.io/uplink@v1.13.0/private/piecestore/client_test.go (about) 1 // Copyright (C) 2024 Storj Labs, Inc. 2 // See LICENSE for copying information. 3 4 package piecestore 5 6 import ( 7 "context" 8 "crypto/rand" 9 "fmt" 10 "io" 11 "strings" 12 "sync" 13 "testing" 14 "time" 15 16 "github.com/stretchr/testify/require" 17 18 "storj.io/common/pb" 19 "storj.io/drpc" 20 "storj.io/drpc/drpcconn" 21 "storj.io/drpc/drpcmux" 22 "storj.io/drpc/drpcserver" 23 "storj.io/drpc/drpctest" 24 ) 25 26 func TestSendRetain(t *testing.T) { 27 mock := &MockPieceStore{} 28 c1, c2 := pipe() 29 30 ctx := drpctest.NewTracker(t) 31 mux := drpcmux.New() 32 33 err := pb.DRPCRegisterPiecestore(mux, mock) 34 require.NoError(t, err) 35 36 srv := drpcserver.NewWithOptions(mux, drpcserver.Options{}) 37 ctx.Run(func(ctx context.Context) { _ = srv.ServeOne(ctx, c1) }) 38 conn := drpcconn.NewWithOptions(c2, drpcconn.Options{}) 39 40 c := Client{} 41 c.client = pb.NewDRPCPiecestoreClient(conn) 42 43 for _, size := range []int{10, 100, retainMessageLimit, retainMessageLimit + 1, retainMessageLimit - 1, retainMessageLimit*2 - 1, retainMessageLimit * 2, retainMessageLimit*2 + 1} { 44 t.Run(fmt.Sprintf("with_size_%d", size), func(t *testing.T) { 45 mock.lastReq = nil 46 data := make([]byte, size) 47 _, err := rand.Read(data) 48 require.NoError(t, err) 49 created := time.Now() 50 err = c.Retain(ctx, &pb.RetainRequest{ 51 Filter: data, 52 CreationDate: created, 53 }) 54 require.NoError(t, err) 55 require.Eventuallyf(t, func() bool { 56 return !mock.Unused() 57 }, 10*time.Second, 100*time.Millisecond, "Message is not received") 58 require.Equal(t, created.UTC(), mock.lastReq.CreationDate.UTC()) 59 require.Equal(t, data, mock.lastReq.Filter) 60 }) 61 } 62 t.Run("wrong hash", func(t *testing.T) { 63 stream, err := c.client.RetainBig(ctx) 64 require.NoError(t, err) 65 err = stream.Send(&pb.RetainRequest{ 66 CreationDate: time.Now(), 67 Filter: []byte{1, 2, 3, 4}, 68 Hash: make([]byte, 4), 69 }) 70 require.NoError(t, err) 71 _, err = stream.CloseAndRecv() 72 require.ErrorContains(t, err, "Hash mismatch") 73 }) 74 } 75 76 func TestCompatibility(t *testing.T) { 77 mock := &MockPieceStore{} 78 c1, c2 := pipe() 79 80 ctx := drpctest.NewTracker(t) 81 mux := drpcmux.New() 82 83 // this is a tricky server which excludes RetainBig from the available RPC methods 84 // emulating servers before RetaingBig is implemented 85 err := mux.Register(mock, LegacyPieceStoreDescription{}) 86 require.NoError(t, err) 87 88 srv := drpcserver.NewWithOptions(mux, drpcserver.Options{}) 89 ctx.Run(func(ctx context.Context) { _ = srv.ServeOne(ctx, c1) }) 90 conn := drpcconn.NewWithOptions(c2, drpcconn.Options{}) 91 92 c := Client{} 93 c.client = pb.NewDRPCPiecestoreClient(conn) 94 t.Run("small filter", func(t *testing.T) { 95 mock.lastReq = nil 96 97 data := make([]byte, 10) 98 _, err := rand.Read(data) 99 require.NoError(t, err) 100 101 created := time.Now() 102 err = c.Retain(ctx, &pb.RetainRequest{ 103 Filter: data, 104 CreationDate: created, 105 }) 106 107 // should work, with calling the original Retain 108 require.NoError(t, err) 109 require.Eventuallyf(t, func() bool { 110 return !mock.Unused() 111 }, 10*time.Second, 100*time.Millisecond, "Message is not received") 112 require.Equal(t, created.UTC(), mock.lastReq.CreationDate.UTC()) 113 require.Equal(t, data, mock.lastReq.Filter) 114 115 }) 116 t.Run("big filter", func(t *testing.T) { 117 mock.lastReq = nil 118 119 data := make([]byte, 10*1024*1024) 120 _, err := rand.Read(data) 121 require.NoError(t, err) 122 123 created := time.Now() 124 err = c.Retain(ctx, &pb.RetainRequest{ 125 Filter: data, 126 CreationDate: created, 127 }) 128 129 // workaround couldn't work as message is too big 130 require.Error(t, err) 131 132 }) 133 } 134 135 // MockPieceStore is a partial DRPC Piecestore implementation. 136 type MockPieceStore struct { 137 mu sync.Mutex 138 lastReq *pb.RetainRequest 139 pb.DRPCPiecestoreUnimplementedServer 140 } 141 142 // Retain implements pb.DRPCPiecestoreServer. 143 func (s *MockPieceStore) Retain(ctx context.Context, req *pb.RetainRequest) (*pb.RetainResponse, error) { 144 s.mu.Lock() 145 defer s.mu.Unlock() 146 s.lastReq = req 147 return &pb.RetainResponse{}, nil 148 } 149 150 func (s *MockPieceStore) Unused() bool { 151 s.mu.Lock() 152 defer s.mu.Unlock() 153 return s.lastReq == nil 154 } 155 156 // RetainBig implements pb.DRPCPiecestoreServer. 157 func (s *MockPieceStore) RetainBig(stream pb.DRPCPiecestore_RetainBigStream) error { 158 s.mu.Lock() 159 defer s.mu.Unlock() 160 lastReq, err := RetainRequestFromStream(stream) 161 if err != nil { 162 return err 163 } 164 s.lastReq = &lastReq 165 return err 166 } 167 168 func pipe() (drpc.Transport, drpc.Transport) { 169 type rwc struct { 170 io.Reader 171 io.Writer 172 io.Closer 173 } 174 c1r, c1w := io.Pipe() 175 c2r, c2w := io.Pipe() 176 177 return rwc{c1r, c2w, c2w}, rwc{c2r, c1w, c1w} 178 } 179 180 // LegacyPieceStoreDescription is like the existing pb.DRPCPiecestoreDescription, but the RetainBig method is filtered out. 181 type LegacyPieceStoreDescription struct { 182 Current pb.DRPCPiecestoreDescription 183 } 184 185 var _ drpc.Description = LegacyPieceStoreDescription{} 186 187 // NumMethods implements drpc.Description. 188 func (l LegacyPieceStoreDescription) NumMethods() int { 189 return l.Current.NumMethods() - 1 190 } 191 192 // Method implements drpc.Description. 193 func (l LegacyPieceStoreDescription) Method(n int) (rpc string, encoding drpc.Encoding, receiver drpc.Receiver, method interface{}, ok bool) { 194 index := 0 195 for i := 0; i < l.Current.NumMethods(); i++ { 196 rpc, encoding, receiver, method, ok := l.Current.Method(i) 197 if strings.Contains(rpc, "RetainBig") { 198 continue 199 } 200 if index == n { 201 return rpc, encoding, receiver, method, ok 202 } 203 index++ 204 } 205 panic("index was too hight") 206 }