git.frostfs.info/TrueCloudLab/frostfs-sdk-go@v0.0.0-20241022124111-5361f0ecebd3/client/object_patch_test.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/ecdsa"
     7  	"crypto/elliptic"
     8  	"crypto/rand"
     9  	"testing"
    10  
    11  	v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
    12  	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
    13  	oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
    14  	"github.com/stretchr/testify/require"
    15  )
    16  
    17  type mockPatchStream struct {
    18  	streamedPayloadPatches []*object.PayloadPatch
    19  }
    20  
    21  func (m *mockPatchStream) Write(r *v2object.PatchRequest) error {
    22  	pp := new(object.PayloadPatch)
    23  	pp.FromV2(r.GetBody().GetPatch())
    24  
    25  	if r.GetBody().GetPatch() != nil {
    26  		bodyChunk := r.GetBody().GetPatch().Chunk
    27  		pp.Chunk = make([]byte, len(bodyChunk))
    28  		copy(pp.Chunk, bodyChunk)
    29  	}
    30  
    31  	m.streamedPayloadPatches = append(m.streamedPayloadPatches, pp)
    32  
    33  	return nil
    34  }
    35  
    36  func (m *mockPatchStream) Close() error {
    37  	return nil
    38  }
    39  
    40  func TestObjectPatcher(t *testing.T) {
    41  	type part struct {
    42  		offset int
    43  		length int
    44  		chunk  string
    45  	}
    46  
    47  	for _, test := range []struct {
    48  		name         string
    49  		patchPayload string
    50  		rng          *object.Range
    51  		maxChunkLen  int
    52  		expectParts  []part
    53  	}{
    54  		{
    55  			name:         "no split payload patch",
    56  			patchPayload: "011111",
    57  			rng:          newRange(0, 6),
    58  			maxChunkLen:  defaultGRPCPayloadChunkLen,
    59  			expectParts: []part{
    60  				{
    61  					offset: 0,
    62  					length: 6,
    63  					chunk:  "011111",
    64  				},
    65  			},
    66  		},
    67  		{
    68  			name:         "splitted payload patch",
    69  			patchPayload: "012345",
    70  			rng:          newRange(0, 6),
    71  			maxChunkLen:  2,
    72  			expectParts: []part{
    73  				{
    74  					offset: 0,
    75  					length: 6,
    76  					chunk:  "01",
    77  				},
    78  				{
    79  					offset: 6,
    80  					length: 0,
    81  					chunk:  "23",
    82  				},
    83  				{
    84  					offset: 6,
    85  					length: 0,
    86  					chunk:  "45",
    87  				},
    88  			},
    89  		},
    90  		{
    91  			name:         "splitted payload patch with zero-length subpatches",
    92  			patchPayload: "0123456789!@",
    93  			rng:          newRange(0, 4),
    94  			maxChunkLen:  2,
    95  			expectParts: []part{
    96  				{
    97  					offset: 0,
    98  					length: 4,
    99  					chunk:  "01",
   100  				},
   101  				{
   102  					offset: 4,
   103  					length: 0,
   104  					chunk:  "23",
   105  				},
   106  				{
   107  					offset: 4,
   108  					length: 0,
   109  					chunk:  "45",
   110  				},
   111  				{
   112  					offset: 4,
   113  					length: 0,
   114  					chunk:  "67",
   115  				},
   116  				{
   117  					offset: 4,
   118  					length: 0,
   119  					chunk:  "89",
   120  				},
   121  				{
   122  					offset: 4,
   123  					length: 0,
   124  					chunk:  "!@",
   125  				},
   126  			},
   127  		},
   128  		{
   129  			name:         "splitted payload patch with zero-length subpatches only",
   130  			patchPayload: "0123456789!@",
   131  			rng:          newRange(0, 0),
   132  			maxChunkLen:  2,
   133  			expectParts: []part{
   134  				{
   135  					offset: 0,
   136  					length: 0,
   137  					chunk:  "01",
   138  				},
   139  				{
   140  					offset: 0,
   141  					length: 0,
   142  					chunk:  "23",
   143  				},
   144  				{
   145  					offset: 0,
   146  					length: 0,
   147  					chunk:  "45",
   148  				},
   149  				{
   150  					offset: 0,
   151  					length: 0,
   152  					chunk:  "67",
   153  				},
   154  				{
   155  					offset: 0,
   156  					length: 0,
   157  					chunk:  "89",
   158  				},
   159  				{
   160  					offset: 0,
   161  					length: 0,
   162  					chunk:  "!@",
   163  				},
   164  			},
   165  		},
   166  	} {
   167  		t.Run(test.name, func(t *testing.T) {
   168  			m := &mockPatchStream{}
   169  
   170  			pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   171  
   172  			patcher := objectPatcher{
   173  				client:      &Client{},
   174  				stream:      m,
   175  				addr:        oidtest.Address(),
   176  				key:         pk,
   177  				maxChunkLen: test.maxChunkLen,
   178  			}
   179  
   180  			success := patcher.PatchAttributes(context.Background(), nil, false)
   181  			require.True(t, success)
   182  
   183  			success = patcher.PatchPayload(context.Background(), test.rng, bytes.NewReader([]byte(test.patchPayload)))
   184  			require.True(t, success)
   185  
   186  			require.Len(t, m.streamedPayloadPatches, len(test.expectParts)+1)
   187  
   188  			// m.streamedPayloadPatches[0] is attribute patch, so skip it
   189  			for i, part := range test.expectParts {
   190  				requireRangeChunk(t, m.streamedPayloadPatches[i+1], part.offset, part.length, part.chunk)
   191  			}
   192  		})
   193  	}
   194  }
   195  
   196  func TestRepeatPayloadPatch(t *testing.T) {
   197  	t.Run("no payload patch partioning", func(t *testing.T) {
   198  		m := &mockPatchStream{}
   199  
   200  		pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   201  
   202  		const maxChunkLen = 20
   203  
   204  		patcher := objectPatcher{
   205  			client:      &Client{},
   206  			stream:      m,
   207  			addr:        oidtest.Address(),
   208  			key:         pk,
   209  			maxChunkLen: maxChunkLen,
   210  		}
   211  
   212  		for _, pp := range []struct {
   213  			patchPayload string
   214  			rng          *object.Range
   215  		}{
   216  			{
   217  				patchPayload: "xxxxxxxxxx",
   218  				rng:          newRange(1, 6),
   219  			},
   220  			{
   221  				patchPayload: "yyyyyyyyyy",
   222  				rng:          newRange(5, 9),
   223  			},
   224  			{
   225  				patchPayload: "zzzzzzzzzz",
   226  				rng:          newRange(10, 0),
   227  			},
   228  		} {
   229  			success := patcher.PatchPayload(context.Background(), pp.rng, bytes.NewReader([]byte(pp.patchPayload)))
   230  			require.True(t, success)
   231  		}
   232  
   233  		requireRangeChunk(t, m.streamedPayloadPatches[0], 1, 6, "xxxxxxxxxx")
   234  		requireRangeChunk(t, m.streamedPayloadPatches[1], 5, 9, "yyyyyyyyyy")
   235  		requireRangeChunk(t, m.streamedPayloadPatches[2], 10, 0, "zzzzzzzzzz")
   236  	})
   237  
   238  	t.Run("payload patch partioning", func(t *testing.T) {
   239  		m := &mockPatchStream{}
   240  
   241  		pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   242  
   243  		const maxChunkLen = 5
   244  
   245  		patcher := objectPatcher{
   246  			client:      &Client{},
   247  			stream:      m,
   248  			addr:        oidtest.Address(),
   249  			key:         pk,
   250  			maxChunkLen: maxChunkLen,
   251  		}
   252  
   253  		for _, pp := range []struct {
   254  			patchPayload string
   255  			rng          *object.Range
   256  		}{
   257  			{
   258  				patchPayload: "xxxxxxxxxx",
   259  				rng:          newRange(1, 6),
   260  			},
   261  			{
   262  				patchPayload: "yyyyyyyyyy",
   263  				rng:          newRange(5, 9),
   264  			},
   265  			{
   266  				patchPayload: "zzzzzzzzzz",
   267  				rng:          newRange(10, 0),
   268  			},
   269  		} {
   270  			success := patcher.PatchPayload(context.Background(), pp.rng, bytes.NewReader([]byte(pp.patchPayload)))
   271  			require.True(t, success)
   272  		}
   273  
   274  		requireRangeChunk(t, m.streamedPayloadPatches[0], 1, 6, "xxxxx")
   275  		requireRangeChunk(t, m.streamedPayloadPatches[1], 7, 0, "xxxxx")
   276  		requireRangeChunk(t, m.streamedPayloadPatches[2], 5, 9, "yyyyy")
   277  		requireRangeChunk(t, m.streamedPayloadPatches[3], 14, 0, "yyyyy")
   278  		requireRangeChunk(t, m.streamedPayloadPatches[4], 10, 0, "zzzzz")
   279  		requireRangeChunk(t, m.streamedPayloadPatches[5], 10, 0, "zzzzz")
   280  	})
   281  }
   282  
   283  func requireRangeChunk(t *testing.T, pp *object.PayloadPatch, offset, length int, chunk string) {
   284  	require.NotNil(t, pp)
   285  	require.Equal(t, uint64(offset), pp.Range.GetOffset())
   286  	require.Equal(t, uint64(length), pp.Range.GetLength())
   287  	require.Equal(t, []byte(chunk), pp.Chunk)
   288  }
   289  
   290  func newRange(offest, length uint64) *object.Range {
   291  	rng := &object.Range{}
   292  	rng.SetOffset(offest)
   293  	rng.SetLength(length)
   294  	return rng
   295  }