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  }