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  }