diff --git a/artifactory_test.go b/artifactory_test.go index 9b05ff675..d334d33ec 100644 --- a/artifactory_test.go +++ b/artifactory_test.go @@ -5766,3 +5766,121 @@ func deleteReceivedReleaseBundle(t *testing.T, url, bundleName, bundleVersion st _, _, err = client.SendDelete(*tests.JfrogUrl+deleteApi, []byte{}, artHttpDetails, "Deleting release bundle") assert.NoError(t, err) } + +var buildAqlSearchQueryDataProvider = []struct { + pattern string + recursive bool + expectedAql string +}{ + {"**/a-.*.tar.gz", false, `{"$or":[{"$and":[{"path":{"$match":"**"},"name":{"$match":"a-.*.tar.gz"}}]},{"$and":[{"path":".","name":{"$match":"*a-.*.tar.gz"}}]}]}`}, +} + +// TestArtifactorySearchByPattern is an integration test that validates search pattern functionality +// by creating test repositories, uploading files, and verifying that searches return correct results. +// This test validates the unit test functionality of CreateAqlBodyForSpecWithPattern. +func TestArtifactorySearchByPattern(t *testing.T) { + initArtifactoryCli() + initArtifactoryTest(t, "") + defer cleanArtifactoryTest() + testRepos := []string{"repo-local", "repo-wildcard", "repo-local2", "repo-local3"} + for _, repoName := range testRepos { + if !isRepoExist(repoName) { + repoConfig := fmt.Sprintf(`{"key":"%s","rclass":"local","packageType":"generic"}`, repoName) + execCreateRepoRestFromString(repoConfig, repoName) + } else { + runRt(t, "delete", repoName+"/*", "--quiet") + } + } + setupTestFilesForSearchPatterns(t) + defer cleanupTestFilesForSearchPatterns(t) + for _, searchSample := range buildAqlSearchQueryDataProvider { + testName := fmt.Sprintf("Pattern_%s_Recursive_%s", + strings.ReplaceAll(strings.ReplaceAll(searchSample.pattern, "/", "_"), "*", "star"), + strconv.FormatBool(searchSample.recursive)) + t.Run(testName, func(t *testing.T) { + args := []string{"search", searchSample.pattern} + if searchSample.recursive { + args = append(args, "--recursive") + } + err := artifactoryCli.Exec(args...) + assert.NoError(t, err, "Search command should execute successfully for pattern: %s", searchSample.pattern) + validateSearchPatternWithAPI(t, searchSample.pattern, searchSample.recursive) + }) + } +} + +// setupTestFilesForSearchPatterns uploads test files to match the search patterns +func setupTestFilesForSearchPatterns(t *testing.T) { + tmpFile, err := os.CreateTemp("", "test-file-*.txt") + assert.NoError(t, err) + defer func(name string) { + _ = os.Remove(name) + }(tmpFile.Name()) + _, err = tmpFile.WriteString("test content") + if err != nil { + return + } + err = tmpFile.Close() + if err != nil { + return + } + runRt(t, "upload", tmpFile.Name(), "repo-local/test-file.txt") + runRt(t, "upload", tmpFile.Name(), "repo-local/subdir/nested-file.txt") + runRt(t, "upload", tmpFile.Name(), "repo-wildcard/test-file.txt") + runRt(t, "upload", tmpFile.Name(), "repo-local2/a1b2c3/dd/test-file.txt") + runRt(t, "upload", tmpFile.Name(), "repo-local2/a1b2c3/dd/subdir/nested-file.txt") + runRt(t, "upload", tmpFile.Name(), "repo-local2/other-path/test-file.txt") + runRt(t, "upload", tmpFile.Name(), "repo-local3/a1b2c3/dd/test-file.txt") + runRt(t, "upload", tmpFile.Name(), "repo-local/path/to/a-1.2.3.tar.gz") + runRt(t, "upload", tmpFile.Name(), "repo-local/a-2.3.4.tar.gz") +} + +// cleanupTestFilesForSearchPatterns removes test files uploaded during the test +func cleanupTestFilesForSearchPatterns(t *testing.T) { + testRepos := []string{"repo-local", "repo-wildcard", "repo-local2", "repo-local3"} + for _, repoName := range testRepos { + if isRepoExist(repoName) { + runRt(t, "delete", repoName+"/*", "--quiet") + } + } +} + +// execCreateRepoRestFromString creates a repository from a JSON string +func execCreateRepoRestFromString(repoConfig, repoName string) { + rtutils.AddHeader("Content-Type", "application/json", &artHttpDetails.Headers) + client, err := httpclient.ClientBuilder().Build() + if err != nil { + log.Error(err) + os.Exit(1) + } + resp, body, err := client.SendPut(serverDetails.ArtifactoryUrl+"api/repositories/"+repoName, []byte(repoConfig), artHttpDetails, "") + if err != nil { + log.Error(err) + os.Exit(1) + } + if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK, http.StatusCreated); err != nil { + log.Error(err) + os.Exit(1) + } + log.Info("Repository", repoName, "created.") +} + +// validateSearchPatternWithAPI validates the search pattern using the search API directly +func validateSearchPatternWithAPI(t *testing.T, pattern string, recursive bool) { + searchSpec := spec.NewBuilder().Pattern(pattern).Recursive(recursive).BuildSpec() + searchCmd := generic.NewSearchCommand() + searchCmd.SetServerDetails(serverDetails).SetSpec(searchSpec) + reader, err := searchCmd.Search() + assert.NoError(t, err, "Search API should work for pattern: %s", pattern) + var resultItems []utils.SearchResult + readerNoDate, err := utils.SearchResultNoDate(reader) + assert.NoError(t, err) + for resultItem := new(utils.SearchResult); readerNoDate.NextRecord(resultItem) == nil; resultItem = new(utils.SearchResult) { + resultItems = append(resultItems, *resultItem) + } + if strings.HasPrefix(pattern, "repo-local") && !strings.Contains(pattern, "*repo-local") { + assert.True(t, len(resultItems) > 0, "Pattern %s should return results", pattern) + } + readerGetErrorAndAssert(t, readerNoDate) + readerCloseAndAssert(t, readerNoDate) +} diff --git a/go.mod b/go.mod index ad36eec28..7b318df11 100644 --- a/go.mod +++ b/go.mod @@ -19,12 +19,12 @@ require ( github.com/jfrog/build-info-go v1.12.2 github.com/jfrog/gofrog v1.7.6 github.com/jfrog/jfrog-cli-application v1.0.1 - github.com/jfrog/jfrog-cli-artifactory v0.7.3-0.20251029093809-3aaa4e93a359 + github.com/jfrog/jfrog-cli-artifactory v0.7.3-0.20251107104809-61e0eb1318a5 github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251028101519-8c79bb06de65 github.com/jfrog/jfrog-cli-evidence v0.8.2 github.com/jfrog/jfrog-cli-platform-services v1.10.0 - github.com/jfrog/jfrog-cli-security v1.21.10 - github.com/jfrog/jfrog-client-go v1.55.1-0.20251028101538-d0d16f019d67 + github.com/jfrog/jfrog-cli-security v1.22.0 + github.com/jfrog/jfrog-client-go v1.55.1-0.20251106114658-e01e86b037c8 github.com/jszwec/csvutil v1.10.0 github.com/manifoldco/promptui v0.9.0 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index 9f338b041..80d4b3148 100644 --- a/go.sum +++ b/go.sum @@ -1159,18 +1159,18 @@ github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYL github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= github.com/jfrog/jfrog-cli-application v1.0.1 h1:PiiSVKFFs8VLAwjCe2JrIKlX7LmARFt+r8BLTupWIDc= github.com/jfrog/jfrog-cli-application v1.0.1/go.mod h1:gjV6Q2hhoMe3FF77GFiTyIRLSHokpDrkVj0GvYM+1Y4= -github.com/jfrog/jfrog-cli-artifactory v0.7.3-0.20251029093809-3aaa4e93a359 h1:r6rrXnFJYbdpy/mFRcSXxj4pcusZlE0552hj3exv2GY= -github.com/jfrog/jfrog-cli-artifactory v0.7.3-0.20251029093809-3aaa4e93a359/go.mod h1:4D19dyt67m1wtAojNhSYR34xpOSzTyucs2iHEJG9tF8= +github.com/jfrog/jfrog-cli-artifactory v0.7.3-0.20251107104809-61e0eb1318a5 h1:pzx0AdO31G/9bOOukZ/kNF7Ria4x3vwo4eaie45EKqE= +github.com/jfrog/jfrog-cli-artifactory v0.7.3-0.20251107104809-61e0eb1318a5/go.mod h1:l9orAXG293Sx8pyk8nwZyHtnMXmPoVehUKZATIAy14U= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251028101519-8c79bb06de65 h1:9HDaoQXzP2EcSL7JML3j6qQ18tDvpKRuonFe1DEKvb8= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251028101519-8c79bb06de65/go.mod h1:qGHIijD2doUdFaRFRhudtCmlt0ENDaBvM3dnUClpmHc= github.com/jfrog/jfrog-cli-evidence v0.8.2 h1:GQi2BjIruGzTWJ9AJM59frRgiPQoLLDo4IVfwiye0tc= github.com/jfrog/jfrog-cli-evidence v0.8.2/go.mod h1:6KohlDOtLswaJ4YYi/FqflJEZTvt7yP7SA1cjGG0If4= github.com/jfrog/jfrog-cli-platform-services v1.10.0 h1:O+N/VAF+QjFvq9xkHpmzKLcdl9aJu3IP204Su0L14rw= github.com/jfrog/jfrog-cli-platform-services v1.10.0/go.mod h1:qbu4iqBST9x8LgD8HhzUm91iOB3vHqtoGmaxOnmw0ok= -github.com/jfrog/jfrog-cli-security v1.21.10 h1:g5wTAWXJE4V7Ly+AoxU6HzuBaxDj9U+z8aKmQasPOi0= -github.com/jfrog/jfrog-cli-security v1.21.10/go.mod h1:XoJ6YBf/TrwkqC/r9+x/WKDIwnZogTF6+2wos1PJBUs= -github.com/jfrog/jfrog-client-go v1.55.1-0.20251028101538-d0d16f019d67 h1:SpzS1xdPY/N0a4KYlQoyA9UVC3o57vj7mbq7EJTYsD8= -github.com/jfrog/jfrog-client-go v1.55.1-0.20251028101538-d0d16f019d67/go.mod h1:wsMEtoyAu/1bARUHxFdmgz83g96ml7ZWcFioIPiuz/U= +github.com/jfrog/jfrog-cli-security v1.22.0 h1:KNovA+BA1IpE0c0jI6jWF33fOimgylR9a7T84ZmgNJI= +github.com/jfrog/jfrog-cli-security v1.22.0/go.mod h1:uoACrGyWZViNPU0STC0fF38bVKtNXjm3hzWW/DKI0DY= +github.com/jfrog/jfrog-client-go v1.55.1-0.20251106114658-e01e86b037c8 h1:kSbJw3FRy7/tnQ25CSGnbut4iYQ8jXibRfBwUYrl6Vg= +github.com/jfrog/jfrog-client-go v1.55.1-0.20251106114658-e01e86b037c8/go.mod h1:wsMEtoyAu/1bARUHxFdmgz83g96ml7ZWcFioIPiuz/U= github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= diff --git a/lifecycle_test.go b/lifecycle_test.go index 8f2d37213..e4d9ad7d8 100644 --- a/lifecycle_test.go +++ b/lifecycle_test.go @@ -754,7 +754,10 @@ func createRbIfDoesNotExists(t *testing.T, rbName, rbVersion string, lcManager * isExist, err := lcManager.IsReleaseBundleExist(rbName, rbVersion, "") assert.NoError(t, err) if isExist { - return + _, statusErr := getStatus(lcManager, rbName, rbVersion, "") + if statusErr == nil { + return + } } createRbFromSpec(t, tests.LifecycleBuilds12, rbName, rbVersion, true, true) } @@ -773,30 +776,57 @@ func TestReleaseBundlesSearchGroups(t *testing.T) { const rbNameB = rbPrefix + "-beta" const rbNameC = rbPrefix + "-core" const rbNameD = "another-app" - const version1 = "1.0.0" - // Create Release Bundle A createRbIfDoesNotExists(t, rbNameA, version1, lcManager) defer deleteReleaseBundle(t, lcManager, rbNameA, version1) assertStatusCompleted(t, lcManager, rbNameA, version1, "") - // Create Release Bundle B createRbIfDoesNotExists(t, rbNameB, version1, lcManager) defer deleteReleaseBundle(t, lcManager, rbNameB, version1) assertStatusCompleted(t, lcManager, rbNameB, version1, "") - // Create Release Bundle C createRbIfDoesNotExists(t, rbNameC, version1, lcManager) defer deleteReleaseBundle(t, lcManager, rbNameC, version1) assertStatusCompleted(t, lcManager, rbNameC, version1, "") - // Create Release Bundle D (for filter/exclusion) createRbIfDoesNotExists(t, rbNameD, version1, lcManager) defer deleteReleaseBundle(t, lcManager, rbNameD, version1) assertStatusCompleted(t, lcManager, rbNameD, version1, "") - time.Sleep(3 * time.Second) + const pollTimeout = 45 * time.Second + const pollInterval = 3 * time.Second + + startTime := time.Now() + found := false + var lastSearchError error + + for time.Since(startTime) < pollTimeout { + resp, err := lcManager.ReleaseBundlesSearchGroup(services.GetSearchOptionalQueryParams{}) + if err != nil { + lastSearchError = fmt.Errorf("error while polling: %w", err) + time.Sleep(pollInterval) + continue + } + for _, rb := range resp.ReleaseBundleSearchGroup { + if rb.ReleaseBundleName == rbNameA { + found = true + break + } + } + if found { + log.Info(fmt.Sprintf("Found '%s' in search index after %s", rbNameA, time.Since(startTime))) + break + } + time.Sleep(pollInterval) + } + + if !found { + if lastSearchError != nil { + t.Fatalf("Failed to find '%s' in search index after %s. Last error: %v", rbNameA, pollTimeout, lastSearchError) + } + t.Fatalf("Failed to find '%s' in search index after %s. Test cannot continue.", rbNameA, pollTimeout) + } testCases := []struct { name string @@ -871,18 +901,15 @@ func TestReleaseBundlesSearchGroups(t *testing.T) { return } assert.NoError(t, err, fmt.Sprintf("Expected no error for test case: %s", tc.name)) - if tc.queryParams.FilterBy != "" { - assert.Equal(t, tc.expectedTotal, len(resp.ReleaseBundleSearchGroup), "Total count mismatch for filtered search") - } else { - assert.GreaterOrEqual(t, resp.Total, tc.expectedTotal, "Total count should be at least expected for unfiltered search") - } var actualNames []string for _, rb := range resp.ReleaseBundleSearchGroup { actualNames = append(actualNames, rb.ReleaseBundleName) } if tc.queryParams.FilterBy != "" { - assert.Equal(t, tc.expectedRbNames, actualNames, "Release bundle names order mismatch") + assert.Equal(t, tc.expectedTotal, len(resp.ReleaseBundleSearchGroup), "Total count mismatch for filtered search") + assert.ElementsMatch(t, tc.expectedRbNames, actualNames, "Release bundle names mismatch") } else { + assert.GreaterOrEqual(t, resp.Total, tc.expectedTotal, "Total count should be at least expected for unfiltered search") assert.Subset(t, actualNames, tc.expectedRbNames, "Actual names should contain all expected names") } }) diff --git a/testdata/mixed_repository_and_pkg_types_config.json b/testdata/mixed_repository_and_pkg_types_config.json index bad5ecf5a..98eefd3f4 100644 --- a/testdata/mixed_repository_and_pkg_types_config.json +++ b/testdata/mixed_repository_and_pkg_types_config.json @@ -15,8 +15,8 @@ "key": "${DOCKER_VIRTUAL_REPO}", "rclass": "virtual", "packageType": "docker", - "repositories": ["${REPO1}", "${DEBIAN_REPO}"], - "defaultDeploymentRepo": "${REPO1}", + "repositories": ["${DEBIAN_REPO}"], + "defaultDeploymentRepo": "${DEBIAN_REPO}", "blockPushingSchema1": "false" } ] \ No newline at end of file diff --git a/testdata/mixed_repository_and_pkg_types_update_config.json b/testdata/mixed_repository_and_pkg_types_update_config.json index 625113fcb..17f58808b 100644 --- a/testdata/mixed_repository_and_pkg_types_update_config.json +++ b/testdata/mixed_repository_and_pkg_types_update_config.json @@ -17,8 +17,8 @@ "key": "${DOCKER_VIRTUAL_REPO}", "rclass": "virtual", "packageType": "docker", - "repositories": ["${REPO1}", "${DEBIAN_REPO}"], - "defaultDeploymentRepo": "${REPO1}", + "repositories": ["${DEBIAN_REPO}"], + "defaultDeploymentRepo": "${DEBIAN_REPO}", "blockPushingSchema1": "false", "description": "UPDATED: Virtual docker repo for batch ops" }