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  }