github.com/0chain/gosdk@v1.17.11/zboxcore/sdk/copyworker_test.go (about) 1 package sdk 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "io" 8 "io/ioutil" 9 "mime" 10 "mime/multipart" 11 "net/http" 12 "strconv" 13 "strings" 14 "sync" 15 "testing" 16 17 "github.com/0chain/errors" 18 "github.com/0chain/gosdk/core/zcncrypto" 19 "github.com/0chain/gosdk/dev" 20 devMock "github.com/0chain/gosdk/dev/mock" 21 "github.com/0chain/gosdk/sdks/blobber" 22 "github.com/0chain/gosdk/zboxcore/blockchain" 23 zclient "github.com/0chain/gosdk/zboxcore/client" 24 "github.com/0chain/gosdk/zboxcore/fileref" 25 "github.com/0chain/gosdk/zboxcore/mocks" 26 "github.com/0chain/gosdk/zboxcore/zboxutil" 27 "github.com/stretchr/testify/mock" 28 "github.com/stretchr/testify/require" 29 ) 30 31 func TestCopyRequest_copyBlobberObject(t *testing.T) { 32 const ( 33 mockAllocationTxId = "mock transaction id" 34 mockClientId = "mock client id" 35 mockClientKey = "mock client key" 36 mockRemoteFilePath = "mock/remote/file/path" 37 mockDestPath = "mock/dest/path" 38 mockAllocationId = "mock allocation id" 39 mockType = "f" 40 mockConnectionId = "1234567890" 41 ) 42 43 var mockClient = mocks.HttpClient{} 44 zboxutil.Client = &mockClient 45 46 client := zclient.GetClient() 47 client.Wallet = &zcncrypto.Wallet{ 48 ClientID: mockClientId, 49 ClientKey: mockClientKey, 50 } 51 52 type parameters struct { 53 referencePathToRetrieve fileref.ReferencePath 54 requestFields map[string]string 55 } 56 57 tests := []struct { 58 name string 59 parameters parameters 60 setup func(*testing.T, string, parameters) 61 wantErr bool 62 errMsg string 63 wantFunc func(*require.Assertions, *CopyRequest) 64 }{ 65 { 66 name: "Test_Error_New_HTTP_Failed_By_Containing_" + string([]byte{0x7f, 0, 0}), 67 setup: func(t *testing.T, testName string, p parameters) {}, 68 wantErr: true, 69 errMsg: `net/url: invalid control character in URL`, 70 }, 71 { 72 name: "Test_Error_Get_Object_Tree_From_Blobber_Failed", 73 setup: func(t *testing.T, testName string, p parameters) { 74 mockClient.On("Do", mock.MatchedBy(func(req *http.Request) bool { 75 return strings.HasPrefix(req.URL.Path, testName) 76 })).Return(&http.Response{ 77 StatusCode: http.StatusBadRequest, 78 Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), 79 }, nil) 80 }, 81 wantErr: true, 82 errMsg: "400: Object tree error response: Body:", 83 }, 84 { 85 name: "Test_Copy_Blobber_Object_Failed", 86 parameters: parameters{ 87 referencePathToRetrieve: fileref.ReferencePath{ 88 Meta: map[string]interface{}{ 89 "type": mockType, 90 }, 91 }, 92 }, 93 setup: func(t *testing.T, testName string, p parameters) { 94 mockClient.On("Do", mock.MatchedBy(func(req *http.Request) bool { 95 return strings.HasPrefix(req.URL.Path, testName) && 96 req.Method == "GET" && 97 req.Header.Get("X-App-Client-ID") == mockClientId && 98 req.Header.Get("X-App-Client-Key") == mockClientKey 99 })).Return(&http.Response{ 100 StatusCode: http.StatusOK, 101 Body: func() io.ReadCloser { 102 jsonFR, err := json.Marshal(p.referencePathToRetrieve) 103 require.NoError(t, err) 104 return ioutil.NopCloser(bytes.NewReader([]byte(jsonFR))) 105 }(), 106 }, nil) 107 108 mockClient.On("Do", mock.MatchedBy(func(req *http.Request) bool { 109 return strings.HasPrefix(req.URL.Path, testName) && 110 req.Method == "POST" && 111 req.Header.Get("X-App-Client-ID") == mockClientId && 112 req.Header.Get("X-App-Client-Key") == mockClientKey 113 })).Return(&http.Response{ 114 StatusCode: http.StatusBadRequest, 115 Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), 116 }, nil) 117 }, 118 wantErr: true, 119 errMsg: "response_error", 120 wantFunc: func(require *require.Assertions, req *CopyRequest) { 121 require.NotNil(req) 122 require.Equal(0, req.copyMask.CountOnes()) 123 require.Equal(0, req.consensus) 124 }, 125 }, 126 { 127 name: "Test_Copy_Blobber_Object_Success", 128 parameters: parameters{ 129 referencePathToRetrieve: fileref.ReferencePath{ 130 Meta: map[string]interface{}{ 131 "type": mockType, 132 }, 133 }, 134 requestFields: map[string]string{ 135 "connection_id": mockConnectionId, 136 "path": mockRemoteFilePath, 137 "dest": mockDestPath, 138 }, 139 }, 140 setup: func(t *testing.T, testName string, p parameters) { 141 mockClient.On("Do", mock.MatchedBy(func(req *http.Request) bool { 142 return strings.HasPrefix(req.URL.Path, testName) && 143 req.Method == "GET" && 144 req.Header.Get("X-App-Client-ID") == mockClientId && 145 req.Header.Get("X-App-Client-Key") == mockClientKey 146 })).Return(&http.Response{ 147 StatusCode: http.StatusOK, 148 Body: func() io.ReadCloser { 149 jsonFR, err := json.Marshal(p.referencePathToRetrieve) 150 require.NoError(t, err) 151 return ioutil.NopCloser(bytes.NewReader([]byte(jsonFR))) 152 }(), 153 }, nil) 154 155 mockClient.On("Do", mock.MatchedBy(func(req *http.Request) bool { 156 mediaType, params, err := mime.ParseMediaType(req.Header.Get("Content-Type")) 157 require.NoError(t, err) 158 require.True(t, strings.HasPrefix(mediaType, "multipart/")) 159 reader := multipart.NewReader(req.Body, params["boundary"]) 160 161 err = nil 162 for { 163 var part *multipart.Part 164 part, err = reader.NextPart() 165 if err != nil { 166 break 167 } 168 expected, ok := p.requestFields[part.FormName()] 169 require.True(t, ok) 170 actual, err := ioutil.ReadAll(part) 171 require.NoError(t, err) 172 require.EqualValues(t, expected, string(actual)) 173 } 174 require.Error(t, err) 175 require.EqualValues(t, "EOF", errors.Top(err)) 176 177 return strings.HasPrefix(req.URL.Path, testName) && 178 req.Method == "POST" && 179 req.Header.Get("X-App-Client-ID") == mockClientId && 180 req.Header.Get("X-App-Client-Key") == mockClientKey 181 })).Return(&http.Response{ 182 StatusCode: http.StatusOK, 183 Body: func() io.ReadCloser { 184 jsonFR, err := json.Marshal(p.referencePathToRetrieve) 185 require.NoError(t, err) 186 return ioutil.NopCloser(bytes.NewReader([]byte(jsonFR))) 187 }(), 188 }, nil) 189 }, 190 wantFunc: func(require *require.Assertions, req *CopyRequest) { 191 require.NotNil(req) 192 require.Equal(1, req.copyMask.CountOnes()) 193 require.Equal(1, req.consensus) 194 }, 195 }, 196 } 197 for _, tt := range tests { 198 t.Run(tt.name, func(t *testing.T) { 199 require := require.New(t) 200 tt.setup(t, tt.name, tt.parameters) 201 req := &CopyRequest{ 202 allocationID: mockAllocationId, 203 allocationTx: mockAllocationTxId, 204 remotefilepath: mockRemoteFilePath, 205 destPath: mockDestPath, 206 Consensus: Consensus{ 207 RWMutex: &sync.RWMutex{}, 208 consensusThresh: 2, 209 fullconsensus: 4, 210 }, 211 maskMU: &sync.Mutex{}, 212 ctx: context.TODO(), 213 connectionID: mockConnectionId, 214 } 215 req.blobbers = append(req.blobbers, &blockchain.StorageNode{ 216 Baseurl: tt.name, 217 }) 218 req.copyMask = zboxutil.NewUint128(1).Lsh(uint64(len(req.blobbers))).Sub64(1) 219 _, err := req.copyBlobberObject(req.blobbers[0], 0) 220 require.EqualValues(tt.wantErr, err != nil) 221 if err != nil { 222 require.Contains(errors.Top(err), tt.errMsg) 223 return 224 } 225 require.NoErrorf(err, "expected no error but got %v", err) 226 if tt.wantFunc != nil { 227 tt.wantFunc(require, req) 228 } 229 }) 230 } 231 } 232 233 func TestCopyRequest_ProcessCopy(t *testing.T) { 234 const ( 235 mockAllocationTxId = "mock transaction id" 236 mockClientId = "mock client id" 237 mockClientKey = "mock client key" 238 mockRemoteFilePath = "mock/remote/file/path" 239 mockDestPath = "mock/dest/path" 240 mockAllocationId = "mock allocation id" 241 mockBlobberId = "mock blobber id" 242 mockBlobberUrl = "mockblobberurl" 243 mockType = "f" 244 mockConnectionId = "1234567890" 245 ) 246 247 rawClient := zboxutil.Client 248 249 var mockClient = mocks.HttpClient{} 250 zboxutil.Client = &mockClient 251 252 client := zclient.GetClient() 253 client.Wallet = &zcncrypto.Wallet{ 254 ClientID: mockClientId, 255 ClientKey: mockClientKey, 256 } 257 258 zboxutil.Client = &mockClient 259 260 defer func() { 261 zboxutil.Client = rawClient 262 }() 263 264 setupHttpResponses := func(t *testing.T, testName string, numBlobbers int, numCorrect int, req *CopyRequest) { 265 for i := 0; i < numBlobbers; i++ { 266 url := mockBlobberUrl + strconv.Itoa(i) 267 mockClient.On("Do", mock.MatchedBy(func(req *http.Request) bool { 268 return req.Method == "GET" && 269 strings.Contains(req.URL.String(), testName+url) 270 })).Return(&http.Response{ 271 StatusCode: http.StatusOK, 272 Body: func() io.ReadCloser { 273 jsonFR, err := json.Marshal(&fileref.ReferencePath{ 274 Meta: map[string]interface{}{ 275 "type": mockType, 276 }, 277 }) 278 require.NoError(t, err) 279 return ioutil.NopCloser(bytes.NewReader([]byte(jsonFR))) 280 }(), 281 }, nil) 282 283 mockClient.On("Do", mock.MatchedBy(func(req *http.Request) bool { 284 return req.Method == "POST" && 285 strings.Contains(req.URL.String(), zboxutil.COPY_ENDPOINT) && 286 strings.Contains(req.URL.String(), testName+url) 287 })).Return(&http.Response{ 288 StatusCode: func() int { 289 if i < numCorrect { 290 return http.StatusOK 291 } 292 return http.StatusBadRequest 293 }(), 294 Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), 295 }, nil) 296 297 mockClient.On("Do", mock.MatchedBy(func(req *http.Request) bool { 298 return req.Method == "POST" && 299 strings.Contains(req.URL.String(), zboxutil.WM_LOCK_ENDPOINT) && 300 strings.Contains(req.URL.String(), testName+url) 301 })).Return(&http.Response{ 302 StatusCode: func() int { 303 if i < numCorrect { 304 return http.StatusOK 305 } 306 return http.StatusBadRequest 307 }(), 308 Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"status":2}`))), 309 }, nil) 310 311 mockClient.On("Do", mock.MatchedBy(func(req *http.Request) bool { 312 return req.Method == "POST" && 313 strings.Contains(req.URL.String(), zboxutil.COMMIT_ENDPOINT) && 314 strings.Contains(req.URL.String(), testName+url) 315 })).Return(&http.Response{ 316 StatusCode: func() int { 317 if i < numCorrect { 318 return http.StatusOK 319 } 320 return http.StatusBadRequest 321 }(), 322 Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), 323 }, nil) 324 325 mockClient.On("Do", mock.MatchedBy(func(req *http.Request) bool { 326 return req.Method == "DELETE" && 327 strings.Contains(req.URL.String(), testName+url) 328 })).Return(&http.Response{ 329 StatusCode: func() int { 330 if i < numCorrect { 331 return http.StatusOK 332 } 333 return http.StatusBadRequest 334 }(), 335 }, nil) 336 } 337 338 commitChan = make(map[string]chan *CommitRequest) 339 for _, blobber := range req.blobbers { 340 if _, ok := commitChan[blobber.ID]; !ok { 341 commitChan[blobber.ID] = make(chan *CommitRequest, 1) 342 } 343 } 344 blobberChan := commitChan 345 go func() { 346 cm0 := <-blobberChan[req.blobbers[0].ID] 347 require.EqualValues(t, cm0.blobber.ID, testName+mockBlobberId+strconv.Itoa(0)) 348 cm0.result = &CommitResult{ 349 Success: true, 350 } 351 if cm0.wg != nil { 352 cm0.wg.Done() 353 } 354 }() 355 go func() { 356 cm1 := <-blobberChan[req.blobbers[1].ID] 357 require.EqualValues(t, cm1.blobber.ID, testName+mockBlobberId+strconv.Itoa(1)) 358 cm1.result = &CommitResult{ 359 Success: true, 360 } 361 if cm1.wg != nil { 362 cm1.wg.Done() 363 } 364 }() 365 go func() { 366 cm2 := <-blobberChan[req.blobbers[2].ID] 367 require.EqualValues(t, cm2.blobber.ID, testName+mockBlobberId+strconv.Itoa(2)) 368 cm2.result = &CommitResult{ 369 Success: true, 370 } 371 if cm2.wg != nil { 372 cm2.wg.Done() 373 } 374 }() 375 go func() { 376 cm3 := <-blobberChan[req.blobbers[3].ID] 377 require.EqualValues(t, cm3.blobber.ID, testName+mockBlobberId+strconv.Itoa(3)) 378 cm3.result = &CommitResult{ 379 Success: true, 380 } 381 if cm3.wg != nil { 382 cm3.wg.Done() 383 } 384 }() 385 } 386 387 tests := []struct { 388 name string 389 numBlobbers int 390 numCorrect int 391 setup func(*testing.T, string, int, int, *CopyRequest) 392 wantErr bool 393 errMsg string 394 wantFunc func(*require.Assertions, *CopyRequest) 395 }{ 396 { 397 name: "Test_All_Blobber_Copy_Success", 398 numBlobbers: 4, 399 numCorrect: 4, 400 setup: setupHttpResponses, 401 wantErr: false, 402 wantFunc: func(require *require.Assertions, req *CopyRequest) { 403 require.NotNil(req) 404 require.Equal(4, req.copyMask.CountOnes()) 405 require.Equal(4, req.consensus) 406 }, 407 }, 408 { 409 name: "Test_Blobber_Index_3_Error_On_Copy_Success", 410 numBlobbers: 4, 411 numCorrect: 3, 412 setup: setupHttpResponses, 413 wantErr: false, 414 wantFunc: func(require *require.Assertions, req *CopyRequest) { 415 require.NotNil(req) 416 require.Equal(3, req.copyMask.CountOnes()) 417 require.Equal(3, req.consensus) 418 }, 419 }, 420 { 421 name: "Test_Blobber_Index_2_3_Error_On_Copy_Failure", 422 numBlobbers: 4, 423 numCorrect: 2, 424 setup: setupHttpResponses, 425 wantErr: true, 426 errMsg: "copy_failed", 427 }, 428 { 429 name: "Test_All_Blobber_Error_On_Copy_Failure", 430 numBlobbers: 4, 431 numCorrect: 0, 432 setup: setupHttpResponses, 433 wantErr: true, 434 errMsg: "copy_failed", 435 }, 436 } 437 for _, tt := range tests { 438 t.Run(tt.name, func(t *testing.T) { 439 require := require.New(t) 440 441 a := &Allocation{ 442 Tx: "TestCopyRequest_ProcessCopy", 443 DataShards: numBlobbers, 444 FileOptions: 63, 445 } 446 a.InitAllocation() 447 448 setupMockAllocation(t, a) 449 450 resp := &WMLockResult{ 451 Status: WMLockStatusOK, 452 } 453 454 respBuf, _ := json.Marshal(resp) 455 m := make(devMock.ResponseMap) 456 457 server := dev.NewBlobberServer(m) 458 defer server.Close() 459 460 for i := 0; i < numBlobbers; i++ { 461 path := "/TestCopyRequest_ProcessCopy" + tt.name + mockBlobberUrl + strconv.Itoa(i) 462 463 m[http.MethodPost+":"+path+blobber.EndpointWriteMarkerLock+a.ID] = devMock.Response{ 464 StatusCode: http.StatusOK, 465 Body: respBuf, 466 } 467 468 a.Blobbers = append(a.Blobbers, &blockchain.StorageNode{ 469 ID: tt.name + mockBlobberId + strconv.Itoa(i), 470 Baseurl: server.URL + path, 471 }) 472 } 473 474 setupMockRollback(a, &mockClient) 475 476 req := &CopyRequest{ 477 allocationObj: a, 478 blobbers: a.Blobbers, 479 allocationID: mockAllocationId, 480 allocationTx: mockAllocationTxId, 481 remotefilepath: mockRemoteFilePath, 482 destPath: mockDestPath, 483 Consensus: Consensus{ 484 RWMutex: &sync.RWMutex{}, 485 consensusThresh: 3, 486 fullconsensus: 4, 487 }, 488 copyMask: zboxutil.NewUint128(1).Lsh(uint64(len(a.Blobbers))).Sub64(1), 489 maskMU: &sync.Mutex{}, 490 connectionID: mockConnectionId, 491 } 492 sig, err := zclient.Sign(mockAllocationTxId) 493 require.NoError(err) 494 req.sig = sig 495 req.ctx, req.ctxCncl = context.WithCancel(context.TODO()) 496 497 tt.setup(t, tt.name, tt.numBlobbers, tt.numCorrect, req) 498 err = req.ProcessCopy() 499 if tt.wantErr { 500 require.Contains(errors.Top(err), tt.errMsg) 501 } else { 502 require.Nil(err) 503 } 504 505 if tt.wantFunc != nil { 506 tt.wantFunc(require, req) 507 } 508 }) 509 } 510 }