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 }