github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/transferfiles/transfer_test.go (about) 1 package transferfiles 2 3 import ( 4 "context" 5 "encoding/json" 6 "io" 7 "net/http" 8 "net/http/httptest" 9 "os" 10 "path/filepath" 11 "sync" 12 "testing" 13 "time" 14 15 "github.com/gocarina/gocsv" 16 "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/api" 17 "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/state" 18 "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils" 19 coreUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" 20 commonTests "github.com/jfrog/jfrog-cli-core/v2/common/tests" 21 coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" 22 "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" 23 "github.com/jfrog/jfrog-cli-core/v2/utils/tests" 24 "github.com/jfrog/jfrog-client-go/artifactory" 25 "github.com/jfrog/jfrog-client-go/artifactory/services" 26 artifactoryUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" 27 clientutils "github.com/jfrog/jfrog-client-go/utils" 28 "github.com/stretchr/testify/assert" 29 ) 30 31 func TestHandleStopInitAndClose(t *testing.T) { 32 transferFilesCommand, err := NewTransferFilesCommand(nil, nil) 33 assert.NoError(t, err) 34 finishStopping, _ := transferFilesCommand.handleStop(nil) 35 finishStopping() 36 } 37 38 func TestCancelFunc(t *testing.T) { 39 transferFilesCommand, err := NewTransferFilesCommand(nil, nil) 40 assert.NoError(t, err) 41 assert.False(t, transferFilesCommand.shouldStop()) 42 43 transferFilesCommand.cancelFunc() 44 assert.True(t, transferFilesCommand.shouldStop()) 45 } 46 47 func TestSignalStop(t *testing.T) { 48 cleanUpJfrogHome, err := tests.SetJfrogHome() 49 assert.NoError(t, err) 50 defer cleanUpJfrogHome() 51 52 // Create transfer files command and mark the transfer as started 53 transferFilesCommand, err := NewTransferFilesCommand(nil, nil) 54 assert.NoError(t, err) 55 assert.NoError(t, transferFilesCommand.initTransferDir()) 56 assert.NoError(t, transferFilesCommand.stateManager.TryLockTransferStateManager()) 57 58 // Make sure that the '.jfrog/transfer/stop' doesn't exist 59 transferDir, err := coreutils.GetJfrogTransferDir() 60 assert.NoError(t, err) 61 assert.NoFileExists(t, filepath.Join(transferDir, StopFileName)) 62 63 // Run signalStop and make sure that the '.jfrog/transfer/stop' exists 64 assert.NoError(t, transferFilesCommand.signalStop()) 65 assert.FileExists(t, filepath.Join(transferDir, StopFileName)) 66 } 67 68 func TestSignalStopError(t *testing.T) { 69 cleanUpJfrogHome, err := tests.SetJfrogHome() 70 assert.NoError(t, err) 71 defer cleanUpJfrogHome() 72 73 // Create transfer files command and mark the transfer as started 74 transferFilesCommand, err := NewTransferFilesCommand(nil, nil) 75 assert.NoError(t, err) 76 77 // Check "not active file transfer" error 78 assert.EqualError(t, transferFilesCommand.signalStop(), "There is no active file transfer process.") 79 80 // Mock start transfer 81 assert.NoError(t, transferFilesCommand.initTransferDir()) 82 assert.NoError(t, transferFilesCommand.stateManager.TryLockTransferStateManager()) 83 84 // Check "already in progress" error 85 assert.NoError(t, transferFilesCommand.signalStop()) 86 assert.EqualError(t, transferFilesCommand.signalStop(), "Graceful stop is already in progress. Please wait...") 87 } 88 89 /* #nosec G101 -- Not credentials. */ 90 const ( 91 firstUuidTokenForTest = "347cd3e9-86b6-4bec-9be9-e053a485f327" 92 secondUuidTokenForTest = "af14706e-e0c1-4b7d-8791-6a18bd1fd339" 93 nodeIdForTest = "nodea0gwihu76sk5g-artifactory-primary-0" 94 ) 95 96 func TestValidateDataTransferPluginMinimumVersion(t *testing.T) { 97 t.Run("valid version", func(t *testing.T) { testValidateDataTransferPluginMinimumVersion(t, "9.9.9", false) }) 98 t.Run("exact version", func(t *testing.T) { 99 testValidateDataTransferPluginMinimumVersion(t, dataTransferPluginMinVersion, false) 100 }) 101 t.Run("invalid version", func(t *testing.T) { testValidateDataTransferPluginMinimumVersion(t, "1.0.0", true) }) 102 t.Run("snapshot version", func(t *testing.T) { testValidateDataTransferPluginMinimumVersion(t, "1.0.x-SNAPSHOT", false) }) 103 } 104 105 func testValidateDataTransferPluginMinimumVersion(t *testing.T, curVersion string, errorExpected bool) { 106 var pluginVersion string 107 testServer, serverDetails, _ := commonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { 108 if r.RequestURI == "/"+utils.PluginsExecuteRestApi+"verifyCompatibility" { 109 content, err := json.Marshal(utils.VersionResponse{Version: pluginVersion}) 110 assert.NoError(t, err) 111 _, err = w.Write(content) 112 assert.NoError(t, err) 113 } 114 }) 115 defer testServer.Close() 116 srcPluginManager := initSrcUserPluginServiceManager(t, serverDetails) 117 118 pluginVersion = curVersion 119 err := getAndValidateDataTransferPlugin(srcPluginManager) 120 if errorExpected { 121 assert.EqualError(t, err, clientutils.ValidateMinimumVersion(clientutils.DataTransfer, curVersion, dataTransferPluginMinVersion).Error()) 122 return 123 } 124 assert.NoError(t, err) 125 } 126 127 func TestVerifySourceTargetConnectivity(t *testing.T) { 128 testServer, serverDetails, _ := commonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { 129 if r.RequestURI == "/"+utils.PluginsExecuteRestApi+"verifySourceTargetConnectivity" { 130 w.WriteHeader(http.StatusOK) 131 } 132 }) 133 defer testServer.Close() 134 srcPluginManager := initSrcUserPluginServiceManager(t, serverDetails) 135 transferFilesCommand, err := NewTransferFilesCommand(serverDetails, serverDetails) 136 assert.NoError(t, err) 137 err = transferFilesCommand.verifySourceTargetConnectivity(srcPluginManager) 138 assert.NoError(t, err) 139 } 140 141 func TestVerifySourceTargetConnectivityError(t *testing.T) { 142 testServer, serverDetails, _ := commonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { 143 if r.RequestURI == "/"+utils.PluginsExecuteRestApi+"verifySourceTargetConnectivity" { 144 w.WriteHeader(http.StatusBadRequest) 145 _, err := w.Write([]byte("No connection to target")) 146 assert.NoError(t, err) 147 } 148 }) 149 defer testServer.Close() 150 srcPluginManager := initSrcUserPluginServiceManager(t, serverDetails) 151 transferFilesCommand, err := NewTransferFilesCommand(serverDetails, serverDetails) 152 assert.NoError(t, err) 153 err = transferFilesCommand.verifySourceTargetConnectivity(srcPluginManager) 154 assert.ErrorContains(t, err, "No connection to target") 155 } 156 157 func initSrcUserPluginServiceManager(t *testing.T, serverDetails *coreConfig.ServerDetails) *srcUserPluginService { 158 srcPluginManager, err := createSrcRtUserPluginServiceManager(context.Background(), serverDetails) 159 assert.NoError(t, err) 160 return srcPluginManager 161 } 162 163 func TestVerifyConfigImportPluginNotInstalled(t *testing.T) { 164 testServer, serverDetails, _ := commonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { 165 if r.RequestURI == "/"+utils.PluginsExecuteRestApi+"dataTransferVersion" { 166 w.WriteHeader(http.StatusNotFound) 167 _, err := w.Write([]byte("Not found")) 168 assert.NoError(t, err) 169 } 170 }) 171 defer testServer.Close() 172 srcPluginManager := initSrcUserPluginServiceManager(t, serverDetails) 173 174 _, err := srcPluginManager.version() 175 assert.ErrorContains(t, err, "Response from Artifactory: 404 Not Found.") 176 } 177 178 func TestUploadChunkAndPollUploads(t *testing.T) { 179 stateManager, cleanUp := state.InitStateTest(t) 180 defer cleanUp() 181 182 totalChunkStatusVisits := 0 183 totalUploadChunkVisits := 0 184 fileSample := api.FileRepresentation{ 185 Repo: repo1Key, 186 Path: "rel-path", 187 Name: "name-demo", 188 } 189 190 testServer, serverDetails, _ := initPollUploadsTestMockServer(t, &totalChunkStatusVisits, &totalUploadChunkVisits, fileSample) 191 defer testServer.Close() 192 srcPluginManager := initSrcUserPluginServiceManager(t, serverDetails) 193 194 assert.NoError(t, stateManager.SetRepoState(repo1Key, 0, 0, false, true)) 195 phaseBase := &phaseBase{context: context.Background(), stateManager: stateManager, srcUpService: srcPluginManager, repoKey: repo1Key} 196 uploadChunkAndPollTwice(t, phaseBase, fileSample) 197 198 // Assert that exactly 2 requests to chunk status were made 199 // First request - get one DONE chunk and one IN PROGRESS 200 // Second Request - get DONE for the other chunk 201 assert.Equal(t, 2, totalChunkStatusVisits) 202 } 203 204 // Sends chunk to upload, polls on chunk three times - once when it is still in progress, once after done received and once to notify back to the source. 205 func uploadChunkAndPollTwice(t *testing.T, phaseBase *phaseBase, fileSample api.FileRepresentation) { 206 curChunkUploaderThreads = coreUtils.DefaultThreads 207 curChunkBuilderThreads = coreUtils.DefaultThreads 208 uploadChunksChan := make(chan UploadedChunk, 3) 209 doneChan := make(chan bool, 1) 210 var runWaitGroup sync.WaitGroup 211 212 pcWrapper := newProducerConsumerWrapper() 213 chunk := api.UploadChunk{} 214 chunk.AppendUploadCandidateIfNeeded(fileSample, false) 215 stopped := uploadChunkWhenPossible(&pcWrapper, phaseBase, chunk, uploadChunksChan, nil) 216 assert.False(t, stopped) 217 stopped = uploadChunkWhenPossible(&pcWrapper, phaseBase, chunk, uploadChunksChan, nil) 218 assert.False(t, stopped) 219 assert.Equal(t, 2, pcWrapper.totalProcessedUploadChunks) 220 221 runWaitGroup.Add(1) 222 go func() { 223 defer runWaitGroup.Done() 224 pollUploads(&pcWrapper, phaseBase, phaseBase.srcUpService, uploadChunksChan, doneChan, nil) 225 }() 226 // Let the whole process run for a few chunk status checks, then mark it as done. 227 time.Sleep(5 * waitTimeBetweenChunkStatusSeconds * time.Second) 228 doneChan <- true 229 // Wait for the go routine to return. 230 runWaitGroup.Wait() 231 } 232 233 func getUploadChunkMockResponse(t *testing.T, w http.ResponseWriter, totalUploadChunkVisits *int) { 234 w.WriteHeader(http.StatusAccepted) 235 var resp api.UploadChunkResponse 236 if *totalUploadChunkVisits == 1 { 237 resp = api.UploadChunkResponse{UuidTokenResponse: api.UuidTokenResponse{UuidToken: firstUuidTokenForTest}, NodeIdResponse: api.NodeIdResponse{NodeId: nodeIdForTest}} 238 } else { 239 resp = api.UploadChunkResponse{UuidTokenResponse: api.UuidTokenResponse{UuidToken: secondUuidTokenForTest}, NodeIdResponse: api.NodeIdResponse{NodeId: nodeIdForTest}} 240 } 241 writeMockResponse(t, w, resp) 242 } 243 244 func validateChunkStatusBody(t *testing.T, r *http.Request) { 245 // Read body 246 content, err := io.ReadAll(r.Body) 247 assert.NoError(t, err) 248 var actual api.UploadChunksStatusBody 249 assert.NoError(t, json.Unmarshal(content, &actual)) 250 251 // Make sure all parameters as expected 252 if len(actual.ChunksToDelete) == 0 { 253 assert.Len(t, actual.AwaitingStatusChunks, 2) 254 assert.ElementsMatch(t, []api.ChunkId{firstUuidTokenForTest, secondUuidTokenForTest}, actual.AwaitingStatusChunks) 255 } else { 256 assert.Len(t, actual.ChunksToDelete, 1) 257 assert.Len(t, actual.AwaitingStatusChunks, 1) 258 assert.Equal(t, api.ChunkId(firstUuidTokenForTest), actual.ChunksToDelete[0]) 259 assert.Equal(t, api.ChunkId(secondUuidTokenForTest), actual.AwaitingStatusChunks[0]) 260 } 261 262 } 263 264 func getChunkStatusMockFirstResponse(t *testing.T, w http.ResponseWriter) { 265 resp := getSampleChunkStatus() 266 resp.ChunksStatus[0].Status = api.Done 267 writeMockResponse(t, w, resp) 268 } 269 270 func getChunkStatusMockSecondResponse(t *testing.T, w http.ResponseWriter, file api.FileRepresentation) { 271 resp := api.UploadChunksStatusResponse{ 272 ChunksStatus: []api.ChunkStatus{ 273 { 274 UuidTokenResponse: api.UuidTokenResponse{UuidToken: secondUuidTokenForTest}, 275 Status: api.Done, 276 Files: []api.FileUploadStatusResponse{{FileRepresentation: file, Status: api.Success, StatusCode: http.StatusOK}}, 277 }, 278 }, 279 DeletedChunks: []string{ 280 firstUuidTokenForTest, 281 }, 282 NodeIdResponse: api.NodeIdResponse{ 283 NodeId: nodeIdForTest, 284 }, 285 } 286 287 writeMockResponse(t, w, resp) 288 } 289 290 func writeMockResponse(t *testing.T, w http.ResponseWriter, resp interface{}) { 291 content, err := json.Marshal(resp) 292 assert.NoError(t, err) 293 _, err = w.Write(content) 294 assert.NoError(t, err) 295 } 296 297 func initPollUploadsTestMockServer(t *testing.T, totalChunkStatusVisits *int, totalUploadChunkVisits *int, file api.FileRepresentation) (*httptest.Server, *coreConfig.ServerDetails, artifactory.ArtifactoryServicesManager) { 298 return commonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { 299 if r.RequestURI == "/"+utils.PluginsExecuteRestApi+"uploadChunk" { 300 *totalUploadChunkVisits++ 301 getUploadChunkMockResponse(t, w, totalUploadChunkVisits) 302 } else if r.RequestURI == "/"+utils.PluginsExecuteRestApi+syncChunks { 303 *totalChunkStatusVisits++ 304 validateChunkStatusBody(t, r) 305 // If already visited chunk status, return status done this time. 306 if *totalChunkStatusVisits == 1 { 307 getChunkStatusMockFirstResponse(t, w) 308 } else { 309 getChunkStatusMockSecondResponse(t, w, file) 310 } 311 } 312 }) 313 } 314 315 func TestGetAllLocalRepositories(t *testing.T) { 316 // Prepare mock server 317 testServer, serverDetails, _ := commonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { 318 switch r.RequestURI { 319 case "/api/storageinfo/calculate": 320 // Response for CalculateStorageInfo 321 w.WriteHeader(http.StatusAccepted) 322 case "/api/storageinfo": 323 // Response for GetStorageInfo 324 w.WriteHeader(http.StatusOK) 325 response := &artifactoryUtils.StorageInfo{RepositoriesSummaryList: []artifactoryUtils.RepositorySummary{ 326 {RepoKey: "repo-1"}, {RepoKey: "repo-2"}, 327 {RepoKey: "federated-repo-1"}, {RepoKey: "federated-repo-2"}, 328 {RepoKey: "artifactory-build-info", PackageType: "BuildInfo"}, {RepoKey: "proj-build-info", PackageType: "BuildInfo"}}, 329 } 330 bytes, err := json.Marshal(response) 331 assert.NoError(t, err) 332 _, err = w.Write(bytes) 333 assert.NoError(t, err) 334 case "/api/repositories?type=local&packageType=": 335 // Response for GetWithFilter 336 w.WriteHeader(http.StatusOK) 337 response := &[]services.RepositoryDetails{{Key: "repo-1"}, {Key: "repo-2"}} 338 bytes, err := json.Marshal(response) 339 assert.NoError(t, err) 340 _, err = w.Write(bytes) 341 assert.NoError(t, err) 342 case "/api/repositories?type=federated&packageType=": 343 // Response for GetWithFilter 344 w.WriteHeader(http.StatusOK) 345 // We add a build info repository to the response to cover cases whereby a federated build-info repository is returned 346 response := &[]services.RepositoryDetails{{Key: "federated-repo-1"}, {Key: "federated-repo-2"}, {Key: "proj-build-info"}} 347 bytes, err := json.Marshal(response) 348 assert.NoError(t, err) 349 _, err = w.Write(bytes) 350 assert.NoError(t, err) 351 } 352 }) 353 defer testServer.Close() 354 355 // Get and assert regular local and build info repositories 356 transferFilesCommand, err := NewTransferFilesCommand(nil, nil) 357 assert.NoError(t, err) 358 storageInfoManager, err := coreUtils.NewStorageInfoManager(context.Background(), serverDetails) 359 assert.NoError(t, err) 360 localRepos, localBuildInfoRepo, err := transferFilesCommand.getAllLocalRepos(serverDetails, storageInfoManager) 361 assert.NoError(t, err) 362 assert.ElementsMatch(t, []string{"repo-1", "repo-2", "federated-repo-1", "federated-repo-2"}, localRepos) 363 assert.ElementsMatch(t, []string{"artifactory-build-info", "proj-build-info"}, localBuildInfoRepo) 364 } 365 366 func TestInitStorageInfoManagers(t *testing.T) { 367 sourceServerCalculated, targetServerCalculated := false, false 368 // Prepare source mock server 369 sourceTestServer, sourceServerDetails, _ := commonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { 370 if r.RequestURI == "/api/storageinfo/calculate" { 371 w.WriteHeader(http.StatusAccepted) 372 sourceServerCalculated = true 373 } 374 }) 375 defer sourceTestServer.Close() 376 377 // Prepare target mock server 378 targetTestServer, targetServerDetails, _ := commonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { 379 if r.RequestURI == "/api/storageinfo/calculate" { 380 w.WriteHeader(http.StatusAccepted) 381 targetServerCalculated = true 382 } 383 }) 384 defer targetTestServer.Close() 385 386 // Init and assert storage info managers 387 transferFilesCommand, err := NewTransferFilesCommand(sourceServerDetails, targetServerDetails) 388 assert.NoError(t, err) 389 err = transferFilesCommand.initStorageInfoManagers() 390 assert.NoError(t, err) 391 assert.True(t, sourceServerCalculated) 392 assert.True(t, targetServerCalculated) 393 } 394 395 func getSampleChunkStatus() api.UploadChunksStatusResponse { 396 return api.UploadChunksStatusResponse{ 397 NodeIdResponse: api.NodeIdResponse{NodeId: nodeIdForTest}, 398 ChunksStatus: []api.ChunkStatus{ 399 { 400 UuidTokenResponse: api.UuidTokenResponse{UuidToken: firstUuidTokenForTest}, 401 Status: api.InProgress, 402 Files: []api.FileUploadStatusResponse{ 403 { 404 FileRepresentation: api.FileRepresentation{ 405 Repo: "my-repo-local-2", 406 Path: "rel-path-2", 407 Name: "name-demo-2", 408 }, 409 }, 410 }, 411 }, 412 { 413 UuidTokenResponse: api.UuidTokenResponse{UuidToken: secondUuidTokenForTest}, 414 Status: api.InProgress, 415 Files: []api.FileUploadStatusResponse{ 416 { 417 FileRepresentation: api.FileRepresentation{ 418 Repo: "my-repo-local-1", 419 Path: "rel-path-1", 420 Name: "name-demo-1", 421 }, 422 }, 423 }, 424 }, 425 }, 426 } 427 } 428 429 func TestCheckChunkStatusSync(t *testing.T) { 430 chunkStatus := getSampleChunkStatus() 431 manager := ChunksLifeCycleManager{ 432 nodeToChunksMap: map[api.NodeId]map[api.ChunkId]UploadedChunkData{}, 433 } 434 manager.nodeToChunksMap[nodeIdForTest] = map[api.ChunkId]UploadedChunkData{} 435 manager.nodeToChunksMap[nodeIdForTest][firstUuidTokenForTest] = UploadedChunkData{} 436 manager.nodeToChunksMap[nodeIdForTest][secondUuidTokenForTest] = UploadedChunkData{} 437 pcWrapper := newProducerConsumerWrapper() 438 errChanMng := createErrorsChannelMng() 439 checkChunkStatusSync(&pcWrapper, &chunkStatus, &manager, &errChanMng) 440 assert.Len(t, manager.nodeToChunksMap[nodeIdForTest], 2) 441 chunkStatus.ChunksStatus = chunkStatus.ChunksStatus[:len(chunkStatus.ChunksStatus)-1] 442 checkChunkStatusSync(&pcWrapper, &chunkStatus, &manager, &errChanMng) 443 assert.Len(t, manager.nodeToChunksMap[nodeIdForTest], 1) 444 chunkStatus.ChunksStatus = chunkStatus.ChunksStatus[:len(chunkStatus.ChunksStatus)-1] 445 checkChunkStatusSync(&pcWrapper, &chunkStatus, &manager, &errChanMng) 446 assert.Len(t, manager.nodeToChunksMap[nodeIdForTest], 0) 447 } 448 449 func TestCreateErrorsSummaryFile(t *testing.T) { 450 cleanUpJfrogHome, err := tests.SetJfrogHome() 451 assert.NoError(t, err) 452 defer cleanUpJfrogHome() 453 454 testDataDir := filepath.Join("..", "testdata", "transfer_summary") 455 logFiles := []string{filepath.Join(testDataDir, "logs1.json"), filepath.Join(testDataDir, "logs2.json")} 456 allErrors, err := parseErrorsFromLogFiles(logFiles) 457 assert.NoError(t, err) 458 // Create Errors Summary Csv File from given JSON log files 459 createdCsvPath, err := utils.CreateCSVFile("transfer-files-logs", allErrors.Errors, time.Now()) 460 assert.NoError(t, err) 461 assert.NotEmpty(t, createdCsvPath) 462 createdFile, err := os.Open(createdCsvPath) 463 assert.NoError(t, err) 464 defer func() { 465 assert.NoError(t, createdFile.Close()) 466 }() 467 actualFileErrors := new([]api.FileUploadStatusResponse) 468 assert.NoError(t, gocsv.UnmarshalFile(createdFile, actualFileErrors)) 469 470 // Create expected csv file 471 expectedFile, err := os.Open(filepath.Join(testDataDir, "logs.csv")) 472 assert.NoError(t, err) 473 defer func() { 474 assert.NoError(t, expectedFile.Close()) 475 }() 476 expectedFileErrors := new([]api.FileUploadStatusResponse) 477 assert.NoError(t, gocsv.UnmarshalFile(expectedFile, expectedFileErrors)) 478 assert.ElementsMatch(t, *expectedFileErrors, *actualFileErrors) 479 }