github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/transferfiles/utils_test.go (about) 1 package transferfiles 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "net/http/httptest" 10 "os" 11 "path/filepath" 12 "strconv" 13 "strings" 14 "testing" 15 "time" 16 17 "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/api" 18 "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/state" 19 artifactoryutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" 20 "github.com/jfrog/jfrog-cli-core/v2/utils/config" 21 "github.com/jfrog/jfrog-cli-core/v2/utils/tests" 22 "github.com/jfrog/jfrog-client-go/artifactory/services" 23 "github.com/jfrog/jfrog-client-go/artifactory/services/utils" 24 "github.com/jfrog/jfrog-client-go/utils/io/fileutils" 25 "github.com/jfrog/jfrog-client-go/utils/log" 26 clientutilstests "github.com/jfrog/jfrog-client-go/utils/tests" 27 "github.com/stretchr/testify/assert" 28 ) 29 30 type transferFilesHandler func(w http.ResponseWriter, r *http.Request) 31 32 const runningNodesResponse = ` 33 { 34 "isHa": true, 35 "nodes": [ 36 { 37 "id": "node-1", 38 "state": "RUNNING" 39 }, 40 { 41 "id": "node-2", 42 "state": "RUNNING" 43 }, 44 { 45 "id": "node-3", 46 "state": "RUNNING" 47 } 48 ] 49 } 50 ` 51 52 const ( 53 staleChunksNodeIdOne = "node-id-1" 54 staleChunksNodeIdTwo = "node-id-2" 55 staleChunksChunkId = "chunk-id" 56 staleChunksPath = "path-in-repo" 57 staleChunksName = "file-name" 58 ) 59 60 func TestInitTempDir(t *testing.T) { 61 // Create JFrog home 62 cleanUpJfrogHome, err := tests.SetJfrogHome() 63 assert.NoError(t, err) 64 defer cleanUpJfrogHome() 65 66 // Set the temp dir to be <jfrog-home>/transfer/tmp/ 67 unsetTempDir, err := initTempDir() 68 assert.NoError(t, err) 69 70 // Assert temp dir base path contain transfer/tmp 71 assert.Contains(t, fileutils.GetTempDirBase(), filepath.Join("transfer", "tmp")) 72 73 // Unset temp dir and assert that it is not contain transfer/tmp 74 unsetTempDir() 75 assert.NotContains(t, fileutils.GetTempDirBase(), filepath.Join("transfer", "tmp")) 76 } 77 78 func TestGetRunningNodes(t *testing.T) { 79 testServer, serverDetails, _ := createMockServer(t, func(w http.ResponseWriter, _ *http.Request) { 80 w.WriteHeader(http.StatusOK) 81 _, err := w.Write([]byte(runningNodesResponse)) 82 assert.NoError(t, err) 83 }) 84 defer testServer.Close() 85 86 runningNodes, err := getRunningNodes(context.Background(), serverDetails) 87 assert.NoError(t, err) 88 assert.ElementsMatch(t, runningNodes, []string{"node-1", "node-2", "node-3"}) 89 } 90 91 func TestStopTransferOnArtifactoryNodes(t *testing.T) { 92 stoppedNodeOne, stoppedNodeTwo := false, false 93 requestNumber := 0 94 testServer, _, srcUpService := createMockServer(t, func(w http.ResponseWriter, _ *http.Request) { 95 w.WriteHeader(http.StatusOK) 96 var nodeId string 97 if requestNumber == 0 { 98 nodeId = "node-1" 99 stoppedNodeOne = true 100 } else { 101 nodeId = "node-2" 102 stoppedNodeTwo = true 103 } 104 _, err := w.Write([]byte(fmt.Sprintf(`{"node_id": "%s"}`, nodeId))) 105 assert.NoError(t, err) 106 requestNumber++ 107 }) 108 defer testServer.Close() 109 110 stopTransferInArtifactoryNodes(srcUpService, []string{"node-1", "node-2"}) 111 assert.True(t, stoppedNodeOne) 112 assert.True(t, stoppedNodeTwo) 113 } 114 115 const repoConfigurationResponse = ` 116 { 117 "key" : "%[1]s-local", 118 "packageType" : "%[1]s", 119 "description" : "", 120 "notes" : "", 121 "includesPattern" : "**/*", 122 "excludesPattern" : "", 123 "repoLayoutRef" : "simple-default", 124 "enableComposerSupport" : false, 125 "enableNuGetSupport" : false, 126 "enableGemsSupport" : false, 127 "enableNpmSupport" : false, 128 "enableBowerSupport" : false, 129 "enableCocoaPodsSupport" : false, 130 "enableConanSupport" : false, 131 "enableDebianSupport" : false, 132 "debianTrivialLayout" : false, 133 "enablePypiSupport" : false, 134 "enablePuppetSupport" : false, 135 "enableDockerSupport" : false, 136 "dockerApiVersion" : "V2", 137 "blockPushingSchema1" : true, 138 "forceNugetAuthentication" : false, 139 "enableVagrantSupport" : false, 140 "enableGitLfsSupport" : false, 141 "enableDistRepoSupport" : false, 142 "priorityResolution" : false, 143 "checksumPolicyType" : "client-checksums", 144 "handleReleases" : true, 145 "handleSnapshots" : true, 146 "maxUniqueSnapshots" : %[2]d, 147 "maxUniqueTags" : %[3]d, 148 "snapshotVersionBehavior" : "unique", 149 "suppressPomConsistencyChecks" : true, 150 "blackedOut" : false, 151 "propertySets" : [ ], 152 "archiveBrowsingEnabled" : false, 153 "calculateYumMetadata" : false, 154 "enableFileListsIndexing" : false, 155 "yumRootDepth" : 0, 156 "downloadRedirect" : false, 157 "xrayIndex" : false, 158 "enabledChefSupport" : false, 159 "rclass" : "local" 160 } 161 ` 162 163 func TestGetMaxUniqueSnapshots(t *testing.T) { 164 testCases := []struct { 165 packageType string 166 expectedMaxUniqueSnapshots int 167 }{ 168 {conan, -1}, 169 {maven, 5}, 170 {gradle, 5}, 171 {nuget, 5}, 172 {ivy, 5}, 173 {sbt, 5}, 174 {docker, 3}, 175 } 176 177 testServer, serverDetails, _ := createMockServer(t, func(w http.ResponseWriter, r *http.Request) { 178 w.WriteHeader(http.StatusOK) 179 packageType := strings.TrimSuffix(strings.TrimPrefix(r.RequestURI, "/api/repositories/"), "-local") 180 var response string 181 switch packageType { 182 case "docker": 183 response = fmt.Sprintf(repoConfigurationResponse, packageType, 0, 3) 184 case "maven", "gradle", "nuget", "ivy", "sbt": 185 response = fmt.Sprintf(repoConfigurationResponse, packageType, 5, 0) 186 default: 187 assert.Fail(t, "tried to get the Max Unique Snapshots setting of a repository of an unsupported package type") 188 } 189 _, err := w.Write([]byte(response)) 190 assert.NoError(t, err) 191 }) 192 defer testServer.Close() 193 194 for _, testCase := range testCases { 195 t.Run(testCase.packageType, func(t *testing.T) { 196 lowerPackageType := strings.ToLower(testCase.packageType) 197 repoSummary := &utils.RepositorySummary{RepoKey: lowerPackageType + "-local", PackageType: testCase.packageType} 198 maxUniqueSnapshots, err := getMaxUniqueSnapshots(context.Background(), serverDetails, repoSummary) 199 assert.NoError(t, err) 200 assert.Equal(t, testCase.expectedMaxUniqueSnapshots, maxUniqueSnapshots) 201 }) 202 } 203 } 204 205 func TestUpdateMaxUniqueSnapshots(t *testing.T) { 206 packageTypes := []string{conan, maven, gradle, nuget, ivy, sbt, docker} 207 208 testServer, serverDetails, _ := createMockServer(t, func(w http.ResponseWriter, r *http.Request) { 209 w.WriteHeader(http.StatusOK) 210 body, err := io.ReadAll(r.Body) 211 assert.NoError(t, err) 212 repoDetails := &services.RepositoryDetails{} 213 assert.NoError(t, json.Unmarshal(body, repoDetails)) 214 packageType := repoDetails.PackageType 215 216 expectedPackageType := strings.TrimPrefix(r.RequestURI, "/api/repositories/") 217 if strings.HasSuffix(expectedPackageType, "-local") { 218 expectedPackageType = strings.TrimSuffix(expectedPackageType, "-local") 219 assert.Equal(t, services.LocalRepositoryRepoType, repoDetails.Rclass) 220 } else { 221 expectedPackageType = strings.TrimSuffix(expectedPackageType, "-federated") 222 assert.Equal(t, services.FederatedRepositoryRepoType, repoDetails.Rclass) 223 } 224 225 assert.Equal(t, expectedPackageType, packageType) 226 switch repoDetails.PackageType { 227 case "docker": 228 assert.Contains(t, string(body), "\"maxUniqueTags\":5") 229 case "maven", "gradle", "nuget", "ivy", "sbt": 230 assert.Contains(t, string(body), "\"maxUniqueSnapshots\":5") 231 default: 232 assert.Fail(t, "tried to update the Max Unique Snapshots setting of a repository of an unsupported package type") 233 } 234 _, err = w.Write([]byte(fmt.Sprintf("Repository %s-local update successfully.", packageType))) 235 assert.NoError(t, err) 236 }) 237 defer testServer.Close() 238 239 for _, packageType := range packageTypes { 240 t.Run(packageType, func(t *testing.T) { 241 lowerPackageType := strings.ToLower(packageType) 242 repoSummary := &utils.RepositorySummary{RepoKey: lowerPackageType + "-local", PackageType: packageType, RepoType: "LOCAL"} 243 err := updateMaxUniqueSnapshots(context.Background(), serverDetails, repoSummary, 5) 244 assert.NoError(t, err) 245 246 repoSummary = &utils.RepositorySummary{RepoKey: lowerPackageType + "-federated", PackageType: packageType, RepoType: "FEDERATED"} 247 err = updateMaxUniqueSnapshots(context.Background(), serverDetails, repoSummary, 5) 248 assert.NoError(t, err) 249 }) 250 } 251 } 252 253 func TestInterruptIfRequested(t *testing.T) { 254 cleanUpJfrogHome, err := tests.SetJfrogHome() 255 assert.NoError(t, err) 256 defer cleanUpJfrogHome() 257 258 // Create new transfer files command 259 transferFilesCommand, err := NewTransferFilesCommand(nil, nil) 260 assert.NoError(t, err) 261 262 // Run interruptIfRequested and make sure that the interrupted signal wasn't sent to the channel 263 assert.NoError(t, interruptIfRequested(transferFilesCommand.stopSignal)) 264 select { 265 case <-transferFilesCommand.stopSignal: 266 assert.Fail(t, "Signal was sent, but shouldn't be") 267 default: 268 } 269 270 // Create the 'stop' file 271 assert.NoError(t, transferFilesCommand.initTransferDir()) 272 assert.NoError(t, transferFilesCommand.stateManager.TryLockTransferStateManager()) 273 assert.NoError(t, transferFilesCommand.signalStop()) 274 275 // Run interruptIfRequested and make sure that the signal was sent to the channel 276 assert.NoError(t, interruptIfRequested(transferFilesCommand.stopSignal)) 277 actualSignal, ok := <-transferFilesCommand.stopSignal 278 assert.True(t, ok) 279 assert.Equal(t, os.Interrupt, actualSignal) 280 } 281 282 func TestGetNodeIdToChunkIdsMap(t *testing.T) { 283 // Test empty ChunksLifeCycleManager 284 chunksLifeCycleManager := ChunksLifeCycleManager{} 285 assert.Empty(t, chunksLifeCycleManager.GetNodeIdToChunkIdsMap()) 286 287 // Create ChunksLifeCycleManager with 3 nodes 288 chunksLifeCycleManager = ChunksLifeCycleManager{ 289 nodeToChunksMap: make(map[api.NodeId]map[api.ChunkId]UploadedChunkData), 290 } 291 chunksLifeCycleManager.nodeToChunksMap["nodeId-1"] = map[api.ChunkId]UploadedChunkData{"0": {}, "1": {}} 292 chunksLifeCycleManager.nodeToChunksMap["nodeId-2"] = map[api.ChunkId]UploadedChunkData{"2": {}} 293 chunksLifeCycleManager.nodeToChunksMap["nodeId-3"] = map[api.ChunkId]UploadedChunkData{} 294 295 // Generate the map and check response 296 nodeIdToChunkIdsMap := chunksLifeCycleManager.GetNodeIdToChunkIdsMap() 297 assert.ElementsMatch(t, nodeIdToChunkIdsMap["nodeId-1"], []api.ChunkId{"0", "1"}) 298 assert.ElementsMatch(t, nodeIdToChunkIdsMap["nodeId-2"], []api.ChunkId{"2"}) 299 assert.ElementsMatch(t, nodeIdToChunkIdsMap["nodeId-3"], []api.ChunkId{}) 300 } 301 302 func TestStoreStaleChunksEmpty(t *testing.T) { 303 // Init state manager 304 stateManager, cleanUp := state.InitStateTest(t) 305 defer cleanUp() 306 307 // Store empty stale chunks 308 chunksLifeCycleManager := ChunksLifeCycleManager{ 309 nodeToChunksMap: make(map[api.NodeId]map[api.ChunkId]UploadedChunkData), 310 } 311 assert.NoError(t, chunksLifeCycleManager.StoreStaleChunks(stateManager)) 312 313 // Make sure no chunks 314 staleChunks, err := stateManager.GetStaleChunks() 315 assert.NoError(t, err) 316 assert.Empty(t, staleChunks) 317 } 318 319 func TestStoreStaleChunksNoStale(t *testing.T) { 320 // Init state manager 321 stateManager, cleanUp := state.InitStateTest(t) 322 defer cleanUp() 323 324 // Store chunk that is not stale 325 chunksLifeCycleManager := ChunksLifeCycleManager{ 326 nodeToChunksMap: map[api.NodeId]map[api.ChunkId]UploadedChunkData{ 327 staleChunksNodeIdOne: { 328 staleChunksChunkId: { 329 TimeSent: time.Now().Add(-time.Minute), 330 ChunkFiles: []api.FileRepresentation{{Repo: repo1Key, Path: staleChunksPath, Name: staleChunksName}}, 331 }, 332 }, 333 }, 334 } 335 assert.NoError(t, chunksLifeCycleManager.StoreStaleChunks(stateManager)) 336 337 // Make sure no chunks 338 staleChunks, err := stateManager.GetStaleChunks() 339 assert.NoError(t, err) 340 assert.Empty(t, staleChunks) 341 } 342 343 func TestStoreStaleChunksStale(t *testing.T) { 344 // Init state manager 345 stateManager, cleanUp := state.InitStateTest(t) 346 defer cleanUp() 347 348 // Store stale chunk 349 sent := time.Now().Add(-time.Hour) 350 chunksLifeCycleManager := ChunksLifeCycleManager{ 351 nodeToChunksMap: map[api.NodeId]map[api.ChunkId]UploadedChunkData{ 352 staleChunksNodeIdOne: { 353 staleChunksChunkId: { 354 TimeSent: sent, 355 ChunkFiles: []api.FileRepresentation{{Repo: repo1Key, Path: staleChunksPath, Name: staleChunksName, Size: 100}}, 356 }, 357 }, 358 }, 359 } 360 assert.NoError(t, chunksLifeCycleManager.StoreStaleChunks(stateManager)) 361 362 // Make sure the stale chunk was stored in the state 363 staleChunks, err := stateManager.GetStaleChunks() 364 assert.NoError(t, err) 365 assert.Len(t, staleChunks, 1) 366 assert.Equal(t, staleChunksNodeIdOne, staleChunks[0].NodeID) 367 assert.Len(t, staleChunks[0].Chunks, 1) 368 assert.Equal(t, staleChunksChunkId, staleChunks[0].Chunks[0].ChunkID) 369 assert.Equal(t, sent.Unix(), staleChunks[0].Chunks[0].Sent) 370 assert.Len(t, staleChunks[0].Chunks[0].Files, 1) 371 assert.Equal(t, fmt.Sprintf("%s/%s/%s (0.1KB)", repo1Key, staleChunksPath, staleChunksName), staleChunks[0].Chunks[0].Files[0]) 372 } 373 374 func TestStoreStaleChunksTwoNodes(t *testing.T) { 375 // Init state manager 376 stateManager, cleanUp := state.InitStateTest(t) 377 defer cleanUp() 378 379 // Store 1 stale chunk and 1 non-stale chunk 380 chunksLifeCycleManager := ChunksLifeCycleManager{ 381 nodeToChunksMap: map[api.NodeId]map[api.ChunkId]UploadedChunkData{ 382 staleChunksNodeIdOne: { 383 staleChunksChunkId: { 384 TimeSent: time.Now().Add(-time.Hour), // Older than 0.5 hours 385 ChunkFiles: []api.FileRepresentation{{Repo: repo1Key, Path: staleChunksPath, Name: staleChunksName, Size: 1024}}, 386 }, 387 }, 388 staleChunksNodeIdTwo: { 389 staleChunksChunkId: { 390 TimeSent: time.Now(), // Less than 0.5 hours 391 ChunkFiles: []api.FileRepresentation{{Repo: repo2Key, Path: staleChunksPath, Name: staleChunksName, Size: 0}}, 392 }, 393 }, 394 }, 395 } 396 assert.NoError(t, chunksLifeCycleManager.StoreStaleChunks(stateManager)) 397 398 // Make sure only the stale chunk was stored in the state 399 staleChunks, err := stateManager.GetStaleChunks() 400 assert.NoError(t, err) 401 assert.Len(t, staleChunks, 1) 402 assert.Equal(t, staleChunksNodeIdOne, staleChunks[0].NodeID) 403 } 404 405 // Create mock server to test transfer config commands 406 // t - The testing object 407 // testHandler - The HTTP handler of the test 408 func createMockServer(t *testing.T, testHandler transferFilesHandler) (*httptest.Server, *config.ServerDetails, *srcUserPluginService) { 409 testServer := httptest.NewServer(http.HandlerFunc(testHandler)) 410 serverDetails := &config.ServerDetails{ArtifactoryUrl: testServer.URL + "/"} 411 412 serviceManager, err := createSrcRtUserPluginServiceManager(context.Background(), serverDetails) 413 assert.NoError(t, err) 414 return testServer, serverDetails, serviceManager 415 } 416 417 func TestGetUniqueErrorOrDelayFilePath(t *testing.T) { 418 tmpDir, err := os.MkdirTemp("", "unique_file_path_test") 419 assert.NoError(t, err) 420 421 createUniqueFileAndAssertCounter(t, tmpDir, "prefix", 0) 422 // A file with 0 already exists, so new counter should be 1. 423 createUniqueFileAndAssertCounter(t, tmpDir, "prefix", 1) 424 // Unique prefix, so counter should be 0. 425 createUniqueFileAndAssertCounter(t, tmpDir, "new", 0) 426 427 } 428 429 func createUniqueFileAndAssertCounter(t *testing.T, tmpDir, prefix string, expectedCounter int) { 430 filePath, err := getUniqueErrorOrDelayFilePath(tmpDir, func() string { 431 return prefix 432 }) 433 assert.NoError(t, err) 434 assert.NoError(t, os.WriteFile(filePath, nil, 0644)) 435 assert.True(t, strings.HasSuffix(filePath, strconv.Itoa(expectedCounter)+".json")) 436 } 437 438 var updateThreadsProvider = []struct { 439 threadsNumber int 440 expectedChunkBuilderThreads int 441 expectedChunkUploaderThreads int 442 buildInfo bool 443 }{ 444 {artifactoryutils.DefaultThreads - 1, artifactoryutils.DefaultThreads - 1, artifactoryutils.DefaultThreads - 1, false}, 445 {artifactoryutils.DefaultThreads, artifactoryutils.DefaultThreads, artifactoryutils.DefaultThreads, false}, 446 {artifactoryutils.MaxBuildInfoThreads + 1, artifactoryutils.MaxBuildInfoThreads + 1, artifactoryutils.MaxBuildInfoThreads + 1, false}, 447 {artifactoryutils.MaxChunkBuilderThreads + 1, artifactoryutils.MaxChunkBuilderThreads, artifactoryutils.MaxChunkBuilderThreads + 1, false}, 448 449 {artifactoryutils.DefaultThreads - 1, artifactoryutils.DefaultThreads - 1, artifactoryutils.DefaultThreads - 1, true}, 450 {artifactoryutils.DefaultThreads, artifactoryutils.DefaultThreads, artifactoryutils.DefaultThreads, true}, 451 {artifactoryutils.MaxBuildInfoThreads + 1, artifactoryutils.MaxBuildInfoThreads, artifactoryutils.MaxBuildInfoThreads, true}, 452 {artifactoryutils.MaxChunkBuilderThreads + 1, artifactoryutils.MaxBuildInfoThreads, artifactoryutils.MaxBuildInfoThreads, true}, 453 } 454 455 func TestUpdateThreads(t *testing.T) { 456 cleanUpJfrogHome, err := tests.SetJfrogHome() 457 assert.NoError(t, err) 458 defer cleanUpJfrogHome() 459 460 previousLog := clientutilstests.RedirectLogOutputToNil() 461 defer func() { 462 log.SetLogger(previousLog) 463 }() 464 465 for _, testCase := range updateThreadsProvider { 466 t.Run(strconv.Itoa(testCase.threadsNumber)+" Build Info: "+strconv.FormatBool(testCase.buildInfo), func(t *testing.T) { 467 transferSettings := &artifactoryutils.TransferSettings{ThreadsNumber: testCase.threadsNumber} 468 assert.NoError(t, artifactoryutils.SaveTransferSettings(transferSettings)) 469 470 assert.NoError(t, updateThreads(nil, testCase.buildInfo)) 471 assert.Equal(t, testCase.expectedChunkBuilderThreads, curChunkBuilderThreads) 472 assert.Equal(t, testCase.expectedChunkUploaderThreads, curChunkUploaderThreads) 473 }) 474 } 475 }