From e2a38f8c1e85cb8e0c1dab7fde11aaa405431525 Mon Sep 17 00:00:00 2001 From: attiasas Date: Thu, 18 Jun 2026 15:32:18 +0300 Subject: [PATCH 1/9] Add VSC tests for build tools commands --- buildinfo_test.go | 36 + conan_test.go | 45 ++ go_test.go | 69 +- gradle_test.go | 106 ++- maven_test.go | 39 + nix_test.go | 55 ++ npm_test.go | 40 ++ pnpm_test.go | 46 ++ utils/tests/artifact_props.go | 88 +++ utils/tests/artifact_props_test.go | 57 ++ utils/tests/utils copy.go | 1060 ++++++++++++++++++++++++++++ utils/tests/utils_test.go | 31 + utils/tests/vcs_fixtures.go | 92 +++ utils/tests/vcs_fixtures_test.go | 27 + uv_test.go | 33 + 15 files changed, 1814 insertions(+), 10 deletions(-) create mode 100644 utils/tests/artifact_props.go create mode 100644 utils/tests/artifact_props_test.go create mode 100644 utils/tests/utils copy.go create mode 100644 utils/tests/utils_test.go create mode 100644 utils/tests/vcs_fixtures.go create mode 100644 utils/tests/vcs_fixtures_test.go diff --git a/buildinfo_test.go b/buildinfo_test.go index 6c95699c2..5fb686c25 100644 --- a/buildinfo_test.go +++ b/buildinfo_test.go @@ -1234,6 +1234,42 @@ func TestBuildPublishWithCIVcsProps(t *testing.T) { cleanArtifactoryTest() } +// TestBuildPublishWithLocalGitVcsProps verifies build-publish sets local git VCS props +// when CI env is absent but VCS collection is enabled. +func TestBuildPublishWithLocalGitVcsProps(t *testing.T) { + initArtifactoryTest(t, "") + buildName := tests.RtBuildName1 + "-local-git" + buildNumber := "1" + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + testDir := tests.CopyVcsGitFixture(t, tests.Temp) + runRt(t, "upload", filepath.Join(testDir, "a1.in"), tests.RtRepo1+"/local-git-bp/", "--flat=true", + "--build-name="+buildName, "--build-number="+buildNumber) + + runRt(t, "build-publish", buildName, buildNumber, "--dot-git-path", testDir) + + resultItems := getResultItemsFromArtifactory(tests.SearchAllRepo1, t) + require.Greater(t, len(resultItems), 0) + + var uploaded []rtutils.ResultItem + for _, item := range resultItems { + if item.Name == "a1.in" { + uploaded = append(uploaded, item) + } + } + require.NotEmpty(t, uploaded) + + tests.ValidateLocalGitVcsPropsOnArtifacts(t, uploaded, + tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + + cleanArtifactoryTest() +} + // TestBuildPublishWithoutCI tests that CI VCS properties are NOT set on artifacts // when running build-publish outside of a CI environment. func TestBuildPublishWithoutCI(t *testing.T) { diff --git a/conan_test.go b/conan_test.go index a7334b185..6d4e4d7eb 100644 --- a/conan_test.go +++ b/conan_test.go @@ -1162,3 +1162,48 @@ func TestConanBuildPublishWithCIVcsProps(t *testing.T) { assert.Greater(t, artifactCount, 0, "No artifacts were validated for CI VCS properties") } + +// TestConanUploadWithLocalGitVcsProps verifies civcs local git fallback on conan upload. +func TestConanUploadWithLocalGitVcsProps(t *testing.T) { + initConanTest(t) + + buildName := tests.ConanBuildName + "-local-git" + buildNumber := "1" + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + projectPath := createConanProject(t, "conan-local-git") + tests.CopyGitFixtureIntoProject(t, projectPath) + + wd, err := os.Getwd() + require.NoError(t, err) + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) + defer chdirCallback() + + configureConanRemote(t) + defer cleanupConanRemote() + + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + require.NoError(t, jfrogCli.Exec("conan", "create", ".", "--build=missing", + "--build-name="+buildName, "--build-number="+buildNumber)) + require.NoError(t, jfrogCli.Exec("conan", "upload", "cli-test-package/*", + "-r", tests.ConanLocalRepo, "--confirm", + "--build-name="+buildName, "--build-number="+buildNumber)) + + require.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + require.NoError(t, err) + require.True(t, found) + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + require.NoError(t, err) + + count := tests.ValidateLocalGitVcsPropsOnBuildInfoArtifacts(t, serviceManager, publishedBuildInfo, tests.ConanLocalRepo, + tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + assert.Greater(t, count, 0) +} diff --git a/go_test.go b/go_test.go index 1c9a2e304..79a7a7520 100644 --- a/go_test.go +++ b/go_test.go @@ -469,23 +469,20 @@ func TestGoBuildPublishWithCIVcsProps(t *testing.T) { assert.NoError(t, err) // Verify VCS properties on each artifact from build info - // Use same fallback logic as CI VCS: OriginalDeploymentRepo + Path, or Path directly artifactCount := 0 for _, module := range publishedBuildInfo.BuildInfo.Modules { for _, artifact := range module.Artifacts { - var fullPath string - switch { - case artifact.OriginalDeploymentRepo != "": - fullPath = artifact.OriginalDeploymentRepo + "/" + artifact.Path - case artifact.Path != "": - fullPath = artifact.Path - default: - continue // Skip artifacts without any path info + fullPath := tests.ArtifactFullPath(artifact, tests.GoRepo) + if fullPath == "" { + continue } props, err := serviceManager.GetItemProps(fullPath) assert.NoError(t, err, "Failed to get properties for artifact: %s", fullPath) assert.NotNil(t, props, "Properties are nil for artifact: %s", fullPath) + if props == nil { + continue + } // Validate VCS properties assert.Contains(t, props.Properties, "vcs.provider", "Missing vcs.provider on %s", artifact.Name) @@ -503,3 +500,57 @@ func TestGoBuildPublishWithCIVcsProps(t *testing.T) { assert.Greater(t, artifactCount, 0, "No artifacts were validated for CI VCS properties") } + +// TestGoPublishWithLocalGitVcsProps tests that local git VCS properties are set on Go artifacts +// when running go-publish followed by build-publish with VCS collection enabled and no CI env. +func TestGoPublishWithLocalGitVcsProps(t *testing.T) { + _, cleanUpFunc := initGoTest(t) + defer cleanUpFunc() + + buildName := tests.GoBuildName + "-local-git" + buildNumber := "1" + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + wd, err := os.Getwd() + assert.NoError(t, err, "Failed to get current dir") + + projectPath := createGoProject(t, "project1", true) + testdataTarget := filepath.Join(tests.Out, "testdata") + testdataSrc := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "go", "testdata") + require.NoError(t, biutils.CopyDir(testdataSrc, testdataTarget, true, nil)) + configFileDir := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "go", "project1", ".jfrog", "projects") + _, err = tests.ReplaceTemplateVariables(filepath.Join(configFileDir, "go.yaml"), filepath.Join(projectPath, ".jfrog", "projects")) + require.NoError(t, err) + + tests.CopyGitFixtureIntoProject(t, projectPath) + require.FileExists(t, filepath.Join(projectPath, ".git", "HEAD")) + clientTestUtils.ChangeDirAndAssert(t, projectPath) + defer clientTestUtils.ChangeDirAndAssert(t, wd) + log.Info("Using Go project located at", projectPath) + + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + err = execGo(jfrogCli, "go", "build", "--mod=mod", "--build-name="+buildName, "--build-number="+buildNumber) + assert.NoError(t, err) + + err = execGo(jfrogCli, "gp", "--build-name="+buildName, "--build-number="+buildNumber, "v1.0.0") + assert.NoError(t, err) + + err = execGo(artifactoryCli, "bp", buildName, buildNumber) + assert.NoError(t, err) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found, "Build info was not found") + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + assert.NoError(t, err) + + artifactCount := tests.ValidateLocalGitVcsPropsOnBuildInfoArtifacts(t, serviceManager, publishedBuildInfo, + tests.GoRepo, tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + assert.Greater(t, artifactCount, 0) +} diff --git a/gradle_test.go b/gradle_test.go index 74c0a60a8..725c2641d 100644 --- a/gradle_test.go +++ b/gradle_test.go @@ -707,11 +707,14 @@ func TestGradleBuildPublishWithCIVcsProps(t *testing.T) { artifactCount := 0 for _, module := range publishedBuildInfo.BuildInfo.Modules { for _, artifact := range module.Artifacts { - fullPath := artifact.OriginalDeploymentRepo + "/" + artifact.Path + fullPath := tests.ArtifactFullPath(artifact, tests.GradleRepo) props, err := serviceManager.GetItemProps(fullPath) assert.NoError(t, err, "Failed to get properties for artifact: %s", fullPath) assert.NotNil(t, props, "Properties are nil for artifact: %s", fullPath) + if props == nil { + continue + } // Validate VCS properties assert.Contains(t, props.Properties, "vcs.provider", "Missing vcs.provider on %s", artifact.Name) @@ -730,3 +733,104 @@ func TestGradleBuildPublishWithCIVcsProps(t *testing.T) { cleanGradleTest(t) } + +// TestGradleBuildPublishWithLocalGitVcsProps tests that local git VCS properties are set on Gradle artifacts +// when running build-publish with VCS collection enabled and no CI env. +// Uses the traditional Gradle extractor path (not FlexPack) because SetCIVcsPropsToConfig +// injects local git props into the extractor config; FlexPack only sets build.* props on publish. +func TestGradleBuildPublishWithLocalGitVcsProps(t *testing.T) { + initGradleTest(t) + buildName := "gradle-local-git-test" + buildNumber := "1" + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + _ = os.Unsetenv("JFROG_RUN_NATIVE") + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + buildGradlePath := createGradleProject(t, "gradleproject") + projectDir := filepath.Dir(buildGradlePath) + tests.CopyGitFixtureIntoProject(t, projectDir) + require.FileExists(t, filepath.Join(projectDir, ".git", "HEAD")) + + configFilePath := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "buildspecs", tests.GradleConfig) + createConfigFile(filepath.Join(projectDir, ".jfrog", "projects"), configFilePath, t) + + oldHomeDir := changeWD(t, projectDir) + defer clientTestUtils.ChangeDirAndAssert(t, oldHomeDir) + + buildGradlePath = strings.ReplaceAll(buildGradlePath, `\`, "/") + runJfrogCli(t, "gradle", "clean", "artifactoryPublish", "-b"+buildGradlePath, "--build-name="+buildName, "--build-number="+buildNumber) + + assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + clientTestUtils.ChangeDirAndAssert(t, oldHomeDir) + + var publishedBuildInfo *buildinfo.PublishedBuildInfo + var found bool + assert.Eventuallyf(t, func() bool { + var biErr error + publishedBuildInfo, found, biErr = tests.GetBuildInfo(serverDetails, buildName, buildNumber) + return biErr == nil && found + }, 30*time.Second, 2*time.Second, "Build info was not found for %s/%s", buildName, buildNumber) + if !found || publishedBuildInfo == nil { + return + } + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + assert.NoError(t, err) + + artifactCount := tests.ValidateLocalGitVcsPropsOnBuildInfoArtifacts(t, serviceManager, publishedBuildInfo, + tests.GradleRepo, tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + assert.Greater(t, artifactCount, 0) + + cleanGradleTest(t) +} + +// TestGradleFlexPackPublishWithLocalGitVcsProps verifies local git VCS on FlexPack publish path. +func TestGradleFlexPackPublishWithLocalGitVcsProps(t *testing.T) { + initGradleTest(t) + buildName := "gradle-flexpack-local-git" + buildNumber := "1" + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + setEnvCallBack := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallBack() + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + buildGradlePath := createGradleProject(t, "civcsproject") + projectDir := filepath.Dir(buildGradlePath) + tests.CopyGitFixtureIntoProject(t, projectDir) + + oldHomeDir := changeWD(t, projectDir) + defer clientTestUtils.ChangeDirAndAssert(t, oldHomeDir) + + runJfrogCli(t, "gradle", "clean", "publish", "--build-name="+buildName, "--build-number="+buildNumber) + require.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + clientTestUtils.ChangeDirAndAssert(t, oldHomeDir) + + var publishedBuildInfo *buildinfo.PublishedBuildInfo + var found bool + require.Eventually(t, func() bool { + var biErr error + publishedBuildInfo, found, biErr = tests.GetBuildInfo(serverDetails, buildName, buildNumber) + return biErr == nil && found + }, 30*time.Second, 2*time.Second) + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + require.NoError(t, err) + + count := tests.ValidateLocalGitVcsPropsOnBuildInfoArtifacts(t, serviceManager, publishedBuildInfo, tests.GradleRepo, + tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + assert.Greater(t, count, 0) + + cleanGradleTest(t) +} diff --git a/maven_test.go b/maven_test.go index 54a55ee51..8417cfe47 100644 --- a/maven_test.go +++ b/maven_test.go @@ -839,3 +839,42 @@ func TestMavenBuildPublishWithCIVcsProps(t *testing.T) { cleanMavenTest(t) } + +// TestMavenBuildPublishWithLocalGitVcsProps verifies local git VCS props on Maven artifacts +// when running build-publish with VCS collection enabled and no CI env. +func TestMavenBuildPublishWithLocalGitVcsProps(t *testing.T) { + initMavenTest(t, false) + buildName := tests.MvnBuildName + "-local-git" + buildNumber := "1" + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + pomDir := createSimpleMavenProject(t) + tests.CopyGitFixtureIntoProject(t, pomDir) + + oldHomeDir := changeWD(t, pomDir) + defer clientTestUtils.ChangeDirAndAssert(t, oldHomeDir) + + err := runMaven(t, func(t *testing.T) string { return pomDir }, tests.MavenConfig, + "install", "--build-name="+buildName, "--build-number="+buildNumber) + require.NoError(t, err) + + runRt(t, "build-publish", buildName, buildNumber) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + require.NoError(t, err) + require.True(t, found, "Build info was not found") + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + require.NoError(t, err) + + count := tests.ValidateLocalGitVcsPropsOnBuildInfoArtifacts(t, serviceManager, publishedBuildInfo, tests.MvnRepo1, + tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + assert.Greater(t, count, 0) + + cleanMavenTest(t) +} diff --git a/nix_test.go b/nix_test.go index 9eac5a39d..4380ab43e 100644 --- a/nix_test.go +++ b/nix_test.go @@ -10,6 +10,7 @@ import ( buildinfo "github.com/jfrog/build-info-go/entities" biutils "github.com/jfrog/build-info-go/utils" + "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" coretests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" @@ -495,6 +496,60 @@ func TestNixCopy_VirtualToLocalResolution(t *testing.T) { inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) } +func TestNixCopyWithLocalGitVcsProps(t *testing.T) { + initNixTest(t) + + oldHomeDir, newHomeDir := prepareHomeDir(t) + defer func() { + clientTestUtils.SetEnvAndAssert(t, coreutils.HomeDir, oldHomeDir) + clientTestUtils.RemoveAllAndAssert(t, newHomeDir) + }() + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + buildName := "nix-copy-local-git" + buildNumber := "1" + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + projectDir, cleanupProject := createNixProject(t, "nix-local-git", "channelproject") + defer cleanupProject() + tests.CopyGitFixtureIntoProject(t, projectDir) + + wd, err := os.Getwd() + require.NoError(t, err) + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectDir) + defer chdirCallback() + + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + err = jfrogCli.Exec("nix", "nix-build", "", "-A", "hello", + "--build-name="+buildName, "--build-number="+buildNumber) + if err != nil { + t.Skipf("nix-build not available: %v", err) + } + + toURL := fmt.Sprintf("https://%s:%s@%s/api/nix/%s/", + *tests.JfrogUser, *tests.JfrogPassword, + strings.TrimPrefix(strings.TrimPrefix(*tests.JfrogUrl, "https://"), "http://"), + tests.NixLocalRepo) + require.NoError(t, jfrogCli.Exec("nix", "copy", "--to", toURL, "./result", + "--build-name="+buildName, "--build-number="+buildNumber)) + + require.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + require.NoError(t, err) + require.True(t, found) + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + require.NoError(t, err) + + count := tests.ValidateLocalGitVcsPropsOnBuildInfoArtifacts(t, serviceManager, publishedBuildInfo, tests.NixLocalRepo, + tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + assert.Greater(t, count, 0) +} + func TestNixBuild_BuildOnlyNoCopy(t *testing.T) { initNixTest(t) diff --git a/npm_test.go b/npm_test.go index c5e679664..70df1a14d 100644 --- a/npm_test.go +++ b/npm_test.go @@ -1648,3 +1648,43 @@ func TestNpmBuildPublishWithCIVcsProps(t *testing.T) { } assert.Greater(t, artifactCount, 0, "No artifacts in build info") } + +// TestNpmPublishWithLocalGitVcsProps verifies local git VCS props on npm artifacts +// when running publish followed by build-publish with VCS collection enabled and no CI env. +func TestNpmPublishWithLocalGitVcsProps(t *testing.T) { + initNpmTest(t) + defer cleanNpmTest(t) + + buildName := "npm-local-git-test" + buildNumber := "1" + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + wd, err := os.Getwd() + require.NoError(t, err) + + npmPath := initNpmProjectTest(t) + tests.CopyGitFixtureIntoProject(t, npmPath) + chdirCallBack := clientTestUtils.ChangeDirWithCallback(t, wd, npmPath) + defer chdirCallBack() + + runJfrogCli(t, "npm", "publish", "--build-name="+buildName, "--build-number="+buildNumber) + require.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + clientTestUtils.ChangeDirAndAssert(t, wd) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + require.NoError(t, err) + require.True(t, found) + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + require.NoError(t, err) + + count := tests.ValidateLocalGitVcsPropsOnBuildInfoArtifacts(t, serviceManager, publishedBuildInfo, tests.NpmRepo, + tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + assert.Greater(t, count, 0) +} diff --git a/pnpm_test.go b/pnpm_test.go index b471a894d..f87da7f69 100644 --- a/pnpm_test.go +++ b/pnpm_test.go @@ -891,6 +891,52 @@ func TestPnpmBuildPublishWithCIVcsProps(t *testing.T) { assert.Greater(t, artifactCount, 0, "No artifacts in build info") } +// TestPnpmPublishWithLocalGitVcsProps verifies local git VCS props on pnpm artifacts +// when running publish followed by build-publish with VCS collection enabled and no CI env. +func TestPnpmPublishWithLocalGitVcsProps(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + + buildName := "pnpm-local-git-test" + buildNumber := "1" + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + wd, err := os.Getwd() + assert.NoError(t, err) + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + pnpmProjectPath := createPnpmProject(t, "pnpm-local-git") + projectDir := filepath.Dir(pnpmProjectPath) + tests.CopyGitFixtureIntoProject(t, projectDir) + prepareArtifactoryForPnpmBuild(t, projectDir) + clientTestUtils.ChangeDirAndAssert(t, projectDir) + + cleanupAuth := setupPnpmPublishAuth(t, tests.NpmRepo) + defer cleanupAuth() + + runJfrogCli(t, "pnpm", "publish", "--no-git-checks", + "--build-name="+buildName, "--build-number="+buildNumber) + assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + clientTestUtils.ChangeDirAndAssert(t, wd) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found, "Build info was not found") + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + assert.NoError(t, err) + + count := tests.ValidateLocalGitVcsPropsOnBuildInfoArtifacts(t, serviceManager, publishedBuildInfo, tests.NpmRepo, + tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + assert.Greater(t, count, 0) +} + // TestPnpmInstallAndPublishWithProject verifies that pnpm install and publish work correctly // when targeting a non-default Artifactory project (RTECO-924). // The test uses --project flag with install, publish, and build-publish to verify that diff --git a/utils/tests/artifact_props.go b/utils/tests/artifact_props.go new file mode 100644 index 000000000..172e2aadf --- /dev/null +++ b/utils/tests/artifact_props.go @@ -0,0 +1,88 @@ +package tests + +import ( + "strings" + "testing" + + buildinfo "github.com/jfrog/build-info-go/entities" + "github.com/jfrog/jfrog-client-go/artifactory" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// ArtifactFullPath builds the Artifactory item path for GetItemProps. +// When OriginalDeploymentRepo is empty (common with Gradle extractor build-info), +// defaultRepo is used as the repository prefix. +func ArtifactFullPath(a buildinfo.Artifact, defaultRepo string) string { + path := strings.TrimPrefix(a.Path, "/") + repo := a.OriginalDeploymentRepo + if repo == "" { + repo = defaultRepo + } + if repo != "" { + return repo + "/" + path + } + return path +} + +// ArtifactItemPath returns the Artifactory item path for GetItemProps. +// When Name is set and not already part of Path (e.g. UV stores Path as a directory), +// Name is appended as the filename segment. +func ArtifactItemPath(a buildinfo.Artifact, defaultRepo string) string { + fullPath := ArtifactFullPath(a, defaultRepo) + if a.Name == "" { + return fullPath + } + if strings.HasSuffix(fullPath, "/"+a.Name) || strings.HasSuffix(fullPath, a.Name) { + return fullPath + } + return fullPath + "/" + a.Name +} + +// ValidateLocalGitVcsPropsOnBuildInfoArtifacts fetches props for each build-info artifact +// and asserts local-git VCS fields. Returns the number of artifacts validated. +func ValidateLocalGitVcsPropsOnBuildInfoArtifacts( + t *testing.T, + serviceManager artifactory.ArtifactoryServicesManager, + publishedBuildInfo *buildinfo.PublishedBuildInfo, + defaultRepo string, + expectedURL, expectedRevision, expectedBranch string, +) int { + t.Helper() + require.NotNil(t, publishedBuildInfo) + + count := 0 + for _, module := range publishedBuildInfo.BuildInfo.Modules { + for _, artifact := range module.Artifacts { + fullPath := ArtifactItemPath(artifact, defaultRepo) + if fullPath == "" { + continue + } + + props, err := serviceManager.GetItemProps(fullPath) + require.NoError(t, err, "GetItemProps failed for %s", fullPath) + if props == nil { + assert.Fail(t, "Properties are nil for artifact: %s", fullPath) + continue + } + + assert.Contains(t, props.Properties, "vcs.url", "Missing vcs.url on %s", artifact.Name) + assert.Contains(t, props.Properties["vcs.url"], expectedURL, "Wrong vcs.url on %s", artifact.Name) + + assert.Contains(t, props.Properties, "vcs.revision", "Missing vcs.revision on %s", artifact.Name) + assert.Contains(t, props.Properties["vcs.revision"], expectedRevision, "Wrong vcs.revision on %s", artifact.Name) + + if expectedBranch != "" { + assert.Contains(t, props.Properties, "vcs.branch", "Missing vcs.branch on %s", artifact.Name) + assert.Contains(t, props.Properties["vcs.branch"], expectedBranch, "Wrong vcs.branch on %s", artifact.Name) + } + + // Local-git-only: provider/org/repo must NOT appear when CI is cleared + _, hasProvider := props.Properties["vcs.provider"] + assert.False(t, hasProvider, "vcs.provider should not be set on %s in local-git-only mode", artifact.Name) + + count++ + } + } + return count +} diff --git a/utils/tests/artifact_props_test.go b/utils/tests/artifact_props_test.go new file mode 100644 index 000000000..aaf8bbe01 --- /dev/null +++ b/utils/tests/artifact_props_test.go @@ -0,0 +1,57 @@ +package tests + +import ( + "testing" + + buildinfo "github.com/jfrog/build-info-go/entities" + "github.com/stretchr/testify/assert" +) + +func TestArtifactFullPath(t *testing.T) { + t.Run("uses OriginalDeploymentRepo when set", func(t *testing.T) { + a := buildinfo.Artifact{OriginalDeploymentRepo: "cli-gradle-123", Path: "com/foo/1.0/foo.jar"} + assert.Equal(t, "cli-gradle-123/com/foo/1.0/foo.jar", ArtifactFullPath(a, "fallback-repo")) + }) + + t.Run("falls back to defaultRepo when OriginalDeploymentRepo empty", func(t *testing.T) { + a := buildinfo.Artifact{Path: "com/foo/1.0/foo.jar"} + assert.Equal(t, "cli-gradle-123/com/foo/1.0/foo.jar", ArtifactFullPath(a, "cli-gradle-123")) + }) + + t.Run("falls back to Path when repo empty and no default", func(t *testing.T) { + a := buildinfo.Artifact{Path: "com/foo/1.0/foo.jar"} + assert.Equal(t, "com/foo/1.0/foo.jar", ArtifactFullPath(a, "")) + }) + + t.Run("strips leading slash from Path", func(t *testing.T) { + a := buildinfo.Artifact{Path: "/minimal-example/1.0/minimal-example-1.0.jar"} + assert.Equal(t, "cli-gradle-123/minimal-example/1.0/minimal-example-1.0.jar", ArtifactFullPath(a, "cli-gradle-123")) + }) +} + +func TestValidateLocalGitVcsPropsOnBuildInfoArtifacts_UsesArtifactFullPath(t *testing.T) { + // Smoke-test ArtifactFullPath integration used by the helper (no Artifactory call). + a := buildinfo.Artifact{ + OriginalDeploymentRepo: "", + Path: "/com/foo/1.0/foo.jar", + } + assert.Equal(t, "my-repo/com/foo/1.0/foo.jar", ArtifactFullPath(a, "my-repo")) +} + +func TestArtifactItemPath_AppendsNameForDirectoryPath(t *testing.T) { + a := buildinfo.Artifact{ + OriginalDeploymentRepo: "uv-local", + Path: "my-pkg/0.1.0", + Name: "my_pkg-0.1.0-py3-none-any.whl", + } + assert.Equal(t, "uv-local/my-pkg/0.1.0/my_pkg-0.1.0-py3-none-any.whl", ArtifactItemPath(a, "")) +} + +func TestArtifactItemPath_DoesNotDoubleAppendName(t *testing.T) { + a := buildinfo.Artifact{ + OriginalDeploymentRepo: "mvn-local", + Path: "com/foo/1.0/foo.jar", + Name: "foo.jar", + } + assert.Equal(t, "mvn-local/com/foo/1.0/foo.jar", ArtifactItemPath(a, "")) +} diff --git a/utils/tests/utils copy.go b/utils/tests/utils copy.go new file mode 100644 index 000000000..e8653d1cf --- /dev/null +++ b/utils/tests/utils copy.go @@ -0,0 +1,1060 @@ +package tests + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "math/rand" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "testing" + "time" + + ioutils "github.com/jfrog/gofrog/io" + "github.com/jfrog/jfrog-client-go/utils/tests" + + "github.com/urfave/cli" + + buildinfo "github.com/jfrog/build-info-go/entities" + "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/generic" + commandutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils" + artUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" + "github.com/jfrog/jfrog-cli-core/v2/common/spec" + commonCliUtils "github.com/jfrog/jfrog-cli-core/v2/common/tests" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + corelog "github.com/jfrog/jfrog-cli-core/v2/utils/log" + coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/jfrog/jfrog-cli/utils/summary" + "github.com/jfrog/jfrog-client-go/artifactory/services" + "github.com/jfrog/jfrog-client-go/artifactory/services/utils" + "github.com/jfrog/jfrog-client-go/auth" + clientutils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/stretchr/testify/assert" +) + +var ( + JfrogUrl *string + JfrogUser *string + JfrogPassword *string + JfrogSshKeyPath *string + JfrogSshPassphrase *string + JfrogAccessToken *string + JfrogTargetUrl *string + JfrogTargetAccessToken *string + JfrogHome *string + TestArtifactoryProject *bool + TestArtifactory *bool + TestArtifactoryProxy *bool + TestDistribution *bool + TestDocker *bool + TestPodman *bool + TestDockerScan *bool + ContainerRegistry *string + TestGo *bool + TestNpm *bool + TestPnpm *bool + TestGradle *bool + TestMaven *bool + TestNuget *bool + TestPip *bool + TestPipenv *bool + TestPoetry *bool + TestUv *bool + TestNix *bool + TestConan *bool + TestHelm *bool + TestHuggingFace *bool + TestPlugins *bool + TestXray *bool + TestAccess *bool + TestTransfer *bool + TestLifecycle *bool + TestEvidence *bool + TestApi *bool + TestGhostFrog *bool + HideUnitTestLog *bool + ciRunId *string + InstallDataTransferPlugin *bool + timestampAdded bool +) + +func init() { + JfrogUrl = flag.String("jfrog.url", "http://localhost:8081/", "JFrog platform url") + JfrogUser = flag.String("jfrog.user", "admin", "JFrog platform username") + JfrogPassword = flag.String("jfrog.password", "password", "JFrog platform password") + JfrogSshKeyPath = flag.String("jfrog.sshKeyPath", "", "Ssh key file path") + JfrogSshPassphrase = flag.String("jfrog.sshPassphrase", "", "Ssh key passphrase") + JfrogAccessToken = flag.String("jfrog.adminToken", tests.GetLocalArtifactoryTokenIfNeeded(*JfrogUrl), "JFrog platform admin token") + JfrogTargetUrl = flag.String("jfrog.targetUrl", "", "JFrog target platform url for transfer tests") + JfrogTargetAccessToken = flag.String("jfrog.targetAdminToken", "", "JFrog target platform admin token for transfer tests") + JfrogHome = flag.String("jfrog.home", "", "The JFrog home directory of the local Artifactory installation") + TestArtifactory = flag.Bool("test.artifactory", false, "Test Artifactory") + TestArtifactoryProject = flag.Bool("test.artifactoryProject", false, "Test Artifactory project") + TestArtifactoryProxy = flag.Bool("test.artifactoryProxy", false, "Test Artifactory proxy") + TestDistribution = flag.Bool("test.distribution", false, "Test distribution") + TestDocker = flag.Bool("test.docker", false, "Test Docker build") + TestDockerScan = flag.Bool("test.dockerScan", false, "Test Docker scan") + TestPodman = flag.Bool("test.podman", false, "Test Podman build") + TestGo = flag.Bool("test.go", false, "Test Go") + TestNpm = flag.Bool("test.npm", false, "Test Npm") + TestPnpm = flag.Bool("test.pnpm", false, "Test Pnpm") + TestGradle = flag.Bool("test.gradle", false, "Test Gradle") + TestMaven = flag.Bool("test.maven", false, "Test Maven") + TestNuget = flag.Bool("test.nuget", false, "Test Nuget") + TestPip = flag.Bool("test.pip", false, "Test Pip") + TestPipenv = flag.Bool("test.pipenv", false, "Test Pipenv") + TestPoetry = flag.Bool("test.poetry", false, "Test Poetry") + TestUv = flag.Bool("test.uv", false, "Test UV") + TestNix = flag.Bool("test.nix", false, "Test Nix") + TestConan = flag.Bool("test.conan", false, "Test Conan") + TestHelm = flag.Bool("test.helm", false, "Test Helm") + TestHuggingFace = flag.Bool("test.huggingface", false, "Test HuggingFace") + TestPlugins = flag.Bool("test.plugins", false, "Test Plugins") + TestXray = flag.Bool("test.xray", false, "Test Xray") + TestAccess = flag.Bool("test.access", false, "Test Access") + TestTransfer = flag.Bool("test.transfer", false, "Test files transfer") + TestLifecycle = flag.Bool("test.lifecycle", false, "Test lifecycle") + TestEvidence = flag.Bool("test.evidence", false, "Test evidence") + TestApi = flag.Bool("test.api", false, "Test api command") + TestGhostFrog = flag.Bool("test.ghostFrog", false, "Test Ghost Frog package alias") + ContainerRegistry = flag.String("test.containerRegistry", "localhost:8082", "Container registry") + HideUnitTestLog = flag.Bool("test.hideUnitTestLog", false, "Hide unit tests logs and print it in a file") + InstallDataTransferPlugin = flag.Bool("test.installDataTransferPlugin", false, "Install data-transfer plugin on the source Artifactory server") + ciRunId = flag.String("ci.runId", "", "A unique identifier used as a suffix to create repositories and builds in the tests") +} + +func CleanFileSystem() { + removeDirs(Out, Temp) +} + +func removeDirs(dirs ...string) { + for _, dir := range dirs { + isExist, err := fileutils.IsDirExists(dir, false) + if err != nil { + log.Error(err) + } + if isExist { + err = fileutils.RemoveTempDir(dir) + if err != nil { + log.Error(errors.New("Cannot remove path: " + dir + " due to: " + err.Error())) + } + } + } +} + +func VerifyExistLocally(expected, actual []string, t *testing.T) { + if len(actual) == 0 && len(expected) != 0 { + t.Error("Couldn't find all expected files, expected: " + strconv.Itoa(len(expected)) + ", found: " + strconv.Itoa(len(actual))) + } + err := compare(expected, actual) + assert.NoError(t, err) +} + +func ValidateListsIdentical(expected, actual []string) error { + if len(actual) != len(expected) { + return fmt.Errorf("unexpected behavior, \nexpected: [%s], \nfound: [%s]", strings.Join(expected, ", "), strings.Join(actual, ", ")) + } + err := compare(expected, actual) + return err +} + +func ValidateChecksums(filePath string, expectedChecksum buildinfo.Checksum, t *testing.T) { + localFileDetails, err := fileutils.GetFileDetails(filePath, true) + if err != nil { + t.Error("Couldn't calculate sha1, " + err.Error()) + } + if localFileDetails.Checksum.Sha1 != expectedChecksum.Sha1 { + t.Error("sha1 mismatch for "+filePath+", expected: "+expectedChecksum.Sha1, "found: "+localFileDetails.Checksum.Sha1) + } + if localFileDetails.Checksum.Md5 != expectedChecksum.Md5 { + t.Error("md5 mismatch for "+filePath+", expected: "+expectedChecksum.Md5, "found: "+localFileDetails.Checksum.Sha1) + } + if localFileDetails.Checksum.Sha256 != expectedChecksum.Sha256 { + t.Error("sha256 mismatch for "+filePath+", expected: "+expectedChecksum.Sha256, "found: "+localFileDetails.Checksum.Sha1) + } +} + +func compare(expected, actual []string) error { + for _, v := range expected { + for i, r := range actual { + if v == r { + break + } + if i == len(actual)-1 { + return errors.New("Missing file : " + v) + } + } + } + return nil +} + +func getPathsFromSearchResults(searchResults []artUtils.SearchResult) []string { + var paths []string + for _, result := range searchResults { + paths = append(paths, result.Path) + } + return paths +} + +func CompareExpectedVsActual(expected []string, actual []artUtils.SearchResult, t *testing.T) { + actualPaths := getPathsFromSearchResults(actual) + assert.ElementsMatch(t, expected, actualPaths, fmt.Sprintf("Expected: %v \nActual: %v", expected, actualPaths)) +} + +func GetTestResourcesPath() string { + dir, _ := os.Getwd() + return filepath.ToSlash(dir + "/testdata/") +} + +func GetFilePathForArtifactory(fileName string) string { + return GetTestResourcesPath() + "filespecs/" + fileName +} + +func GetTestsLogsDir() (string, error) { + tempDirPath := filepath.Join(coreutils.GetCliPersistentTempDirPath(), "jfrog_tests_logs") + return tempDirPath, fileutils.CreateDirIfNotExist(tempDirPath) +} + +type PackageSearchResultItem struct { + Name string + Path string + Package string + Version string + Repo string + Owner string + Created string + Size int64 + Sha1 string + Published bool +} + +func DeleteFiles(deleteSpec *spec.SpecFiles, serverDetails *config.ServerDetails) (successCount, failCount int, err error) { + deleteCommand := generic.NewDeleteCommand() + deleteCommand.SetThreads(3).SetSpec(deleteSpec).SetServerDetails(serverDetails).SetDryRun(false) + reader, err := deleteCommand.GetPathsToDelete() + if err != nil { + return 0, 0, err + } + defer ioutils.Close(reader, &err) + return deleteCommand.DeleteFiles(reader) +} + +// This function makes no assertion, caller is responsible to assert as needed. +func GetBuildInfo(serverDetails *config.ServerDetails, buildName, buildNumber string) (pbi *buildinfo.PublishedBuildInfo, found bool, err error) { + servicesManager, err := artUtils.CreateServiceManager(serverDetails, -1, 0, false) + if err != nil { + return nil, false, err + } + params := services.NewBuildInfoParams() + params.BuildName = buildName + params.BuildNumber = buildNumber + return servicesManager.GetBuildInfo(params) +} + +func GetBuildRuns(serverDetails *config.ServerDetails, buildName string) (pbi *buildinfo.BuildRuns, found bool, err error) { + servicesManager, err := artUtils.CreateServiceManager(serverDetails, -1, 0, false) + if err != nil { + return nil, false, err + } + params := services.NewBuildInfoParams() + params.BuildName = buildName + return servicesManager.GetBuildRuns(params) +} + +var reposConfigMap = map[*string]string{ + &DistRepo1: DistributionRepoConfig1, + &DistRepo2: DistributionRepoConfig2, + &GoRepo: GoLocalRepositoryConfig, + &GoRemoteRepo: GoRemoteRepositoryConfig, + &GoVirtualRepo: GoVirtualRepositoryConfig, + &GradleRepo: GradleRepositoryConfig, + &MvnRepo1: MavenRepositoryConfig1, + &MvnRepo2: MavenRepositoryConfig2, + &MvnRemoteRepo: MavenRemoteRepositoryConfig, + &GradleRemoteRepo: GradleRemoteRepositoryConfig, + &NpmRepo: NpmLocalRepositoryConfig, + &NpmScopedRepo: NpmLocalScopedRespositoryConfig, + &NpmRemoteRepo: NpmRemoteRepositoryConfig, + &NugetRemoteRepo: NugetRemoteRepositoryConfig, + &YarnRemoteRepo: YarnRemoteRepositoryConfig, + &PypiLocalRepo: PypiLocalRepositoryConfig, + &PypiRemoteRepo: PypiRemoteRepositoryConfig, + &PypiVirtualRepo: PypiVirtualRepositoryConfig, + &PipenvRemoteRepo: PipenvRemoteRepositoryConfig, + &PipenvVirtualRepo: PipenvVirtualRepositoryConfig, + &PoetryLocalRepo: PoetryLocalRepositoryConfig, + &PoetryRemoteRepo: PoetryRemoteRepositoryConfig, + &PoetryVirtualRepo: PoetryVirtualRepositoryConfig, + &UvLocalRepo: UvLocalRepositoryConfig, + &UvRemoteRepo: UvRemoteRepositoryConfig, + &UvVirtualRepo: UvVirtualRepositoryConfig, + &NixLocalRepo: NixLocalRepositoryConfig, + &NixRemoteRepo: NixRemoteRepositoryConfig, + &NixVirtualRepo: NixVirtualRepositoryConfig, + &ConanLocalRepo: ConanLocalRepositoryConfig, + &ConanRemoteRepo: ConanRemoteRepositoryConfig, + &ConanVirtualRepo: ConanVirtualRepositoryConfig, + &HelmLocalRepo: HelmLocalRepositoryConfig, + &HuggingFaceLocalRepo: HuggingFaceLocalRepositoryConfig, + &RtDebianRepo: DebianTestRepositoryConfig, + &RtLfsRepo: GitLfsTestRepositoryConfig, + &RtRepo1: Repo1RepositoryConfig, + &RtRepo2: Repo2RepositoryConfig, + &RtVirtualRepo: VirtualRepositoryConfig, + &TerraformRepo: TerraformLocalRepositoryConfig, + &DockerLocalRepo: DockerLocalRepositoryConfig, + &DockerLocalPromoteRepo: DockerLocalPromoteRepositoryConfig, + &DockerRemoteRepo: DockerRemoteRepositoryConfig, + &DockerVirtualRepo: DockerVirtualRepositoryConfig, + &OciLocalRepo: OciLocalRepositoryConfig, + &OciRemoteRepo: OciRemoteRepositoryConfig, + &RtDevRepo: DevRepoRepositoryConfig, + &RtProdRepo1: ProdRepo1RepositoryConfig, + &RtProdRepo2: ProdRepo2RepositoryConfig, + &ReleaseLifecycleDependencyRepo: ReleaseLifecycleImportDependencySpec, +} + +var ( + CreatedNonVirtualRepositories map[*string]string + CreatedVirtualRepositories map[*string]string +) + +func getNeededRepositories(reposMap map[*bool][]*string) map[*string]string { + reposToCreate := map[*string]string{} + for needed, testRepos := range reposMap { + if *needed { + for _, repo := range testRepos { + reposToCreate[repo] = reposConfigMap[repo] + } + } + } + return reposToCreate +} + +func getNeededBuildNames(buildNamesMap map[*bool][]*string) []string { + var neededBuildNames []string + for needed, buildNames := range buildNamesMap { + if *needed { + for _, buildName := range buildNames { + neededBuildNames = append(neededBuildNames, *buildName) + } + } + } + return neededBuildNames +} + +// Return local and remote repositories for the test suites, respectfully +func GetNonVirtualRepositories() map[*string]string { + nonVirtualReposMap := map[*bool][]*string{ + TestArtifactory: {&RtRepo1, &RtRepo2, &RtLfsRepo, &RtDebianRepo, &TerraformRepo, &ReleaseLifecycleDependencyRepo}, + TestArtifactoryProject: {&RtRepo1, &RtRepo2, &RtLfsRepo, &RtDebianRepo, &HelmLocalRepo}, + TestDistribution: {&DistRepo1, &DistRepo2}, + TestDocker: {&DockerLocalRepo, &DockerLocalPromoteRepo, &DockerRemoteRepo, &OciLocalRepo, &OciRemoteRepo}, + TestDockerScan: {&DockerLocalRepo, &DockerLocalPromoteRepo, &DockerRemoteRepo}, + TestPodman: {&DockerLocalRepo, &DockerLocalPromoteRepo, &DockerRemoteRepo}, + TestGo: {&GoRepo, &GoRemoteRepo}, + TestGradle: {&GradleRepo, &GradleRemoteRepo}, + TestMaven: {&MvnRepo1, &MvnRepo2, &MvnRemoteRepo}, + TestNpm: {&NpmRepo, &NpmScopedRepo, &NpmRemoteRepo}, + TestPnpm: {&NpmRepo, &NpmScopedRepo, &NpmRemoteRepo}, + TestNuget: {&NugetRemoteRepo}, + TestPip: {&PypiLocalRepo, &PypiRemoteRepo}, + TestPipenv: {&PipenvRemoteRepo}, + TestPoetry: {&PoetryLocalRepo, &PoetryRemoteRepo}, + TestUv: {&UvLocalRepo, &UvRemoteRepo}, + TestNix: {&NixLocalRepo, &NixRemoteRepo}, + TestConan: {&ConanLocalRepo, &ConanRemoteRepo}, + TestHelm: {&HelmLocalRepo}, + TestHuggingFace: {&HuggingFaceLocalRepo}, + TestPlugins: {&RtRepo1}, + TestXray: {&NpmRemoteRepo, &NugetRemoteRepo, &YarnRemoteRepo, &GradleRemoteRepo, &MvnRemoteRepo, &GoRepo, &GoRemoteRepo, &PypiRemoteRepo}, + TestAccess: {&RtRepo1}, + TestTransfer: {&RtRepo1, &RtRepo2, &MvnRepo1, &MvnRemoteRepo, &DockerRemoteRepo}, + TestLifecycle: {&RtDevRepo, &RtProdRepo1, &RtProdRepo2}, + TestHelm: {&RtRepo1}, + } + return getNeededRepositories(nonVirtualReposMap) +} + +// Return virtual repositories for the test suites, respectfully +func GetVirtualRepositories() map[*string]string { + virtualReposMap := map[*bool][]*string{ + TestArtifactory: {&RtVirtualRepo}, + TestDistribution: {}, + TestDocker: {&DockerVirtualRepo}, + TestDockerScan: {&DockerVirtualRepo}, + TestPodman: {&DockerVirtualRepo}, + TestGo: {&GoVirtualRepo}, + TestGradle: {}, + TestMaven: {}, + TestNpm: {}, + TestPnpm: {}, + TestNuget: {}, + TestPip: {&PypiVirtualRepo}, + TestPipenv: {&PipenvVirtualRepo}, + TestPoetry: {&PoetryVirtualRepo}, + TestUv: {&UvVirtualRepo}, + TestNix: {&NixVirtualRepo}, + TestConan: {&ConanVirtualRepo}, + TestHelm: {}, + TestHuggingFace: {}, + TestPlugins: {}, + TestXray: {&GoVirtualRepo}, + TestAccess: {}, + TestHelm: {}, + } + return getNeededRepositories(virtualReposMap) +} + +func GetAllRepositoriesNames() []string { + var baseRepoNames []string + for repoName := range GetNonVirtualRepositories() { + baseRepoNames = append(baseRepoNames, *repoName) + } + for repoName := range GetVirtualRepositories() { + baseRepoNames = append(baseRepoNames, *repoName) + } + return baseRepoNames +} + +func GetTestUsersNames() []string { + return []string{UserName1, UserName2} +} + +func GetBuildNames() []string { + buildNamesMap := map[*bool][]*string{ + TestArtifactory: {&RtBuildName1, &RtBuildName2, &RtBuildNameWithSpecialChars}, + TestDistribution: {}, + TestDocker: {&DockerBuildName}, + TestPodman: {&DockerBuildName}, + TestGo: {&GoBuildName}, + TestGradle: {&GradleBuildName}, + TestMaven: {&MvnBuildName}, + TestNpm: {&NpmBuildName, &YarnBuildName}, + TestPnpm: {&PnpmBuildName}, + TestNuget: {&NuGetBuildName}, + TestPip: {&PipBuildName}, + TestPipenv: {&PipenvBuildName}, + TestPoetry: {&PoetryBuildName}, + TestUv: {&UvBuildName}, + TestNix: {&NixBuildName}, + TestConan: {&ConanBuildName}, + TestHelm: {&HelmBuildName}, + TestHuggingFace: {&HuggingFaceBuildName}, + TestPlugins: {}, + TestXray: {}, + TestAccess: {}, + TestTransfer: {&MvnBuildName}, + TestLifecycle: {&LcBuildName1, &LcBuildName2, &LcBuildName3}, + TestHelm: {}, + } + return getNeededBuildNames(buildNamesMap) +} + +// Builds and repositories names to replace in the test files. +// We use substitution map to set repositories and builds with timestamp. +func getSubstitutionMap() map[string]string { + return map[string]string{ + "${REPO1}": RtRepo1, + "${REPO2}": RtRepo2, + "${REPO_1_AND_2}": RtRepo1And2, + "${VIRTUAL_REPO}": RtVirtualRepo, + "${LFS_REPO}": RtLfsRepo, + "${DEBIAN_REPO}": RtDebianRepo, + "${DOCKER_REPO}": DockerLocalRepo, + "${DOCKER_PROMOTE_REPO}": DockerLocalPromoteRepo, + "${DOCKER_REMOTE_REPO}": DockerRemoteRepo, + "${DOCKER_VIRTUAL_REPO}": DockerVirtualRepo, + "${OCI_LOCAL_REPO}": OciLocalRepo, + "${OCI_REMOTE_REPO}": OciRemoteRepo, + "${DOCKER_IMAGE_NAME}": DockerImageName, + "${CONTAINER_REGISTRY_DOMAIN}": RtContainerHostName, + "${MAVEN_REPO1}": MvnRepo1, + "${MAVEN_REPO2}": MvnRepo2, + "${MAVEN_REMOTE_REPO}": MvnRemoteRepo, + "${GRADLE_REMOTE_REPO}": GradleRemoteRepo, + "${GRADLE_REPO}": GradleRepo, + "${NPM_REPO}": NpmRepo, + "${NPM_SCOPED_REPO}": NpmScopedRepo, + "${NPM_REMOTE_REPO}": NpmRemoteRepo, + "${PNPM_BUILD_NAME}": PnpmBuildName, + "${NUGET_REMOTE_REPO}": NugetRemoteRepo, + "${YARN_REMOTE_REPO}": YarnRemoteRepo, + "${GO_REPO}": GoRepo, + "${GO_REMOTE_REPO}": GoRemoteRepo, + "${GO_VIRTUAL_REPO}": GoVirtualRepo, + "${TERRAFORM_REPO}": TerraformRepo, + "${SERVER_ID}": ServerId, + "${URL}": *JfrogUrl, + "${USERNAME}": *JfrogUser, + "${PASSWORD}": *JfrogPassword, + "${RT_CREDENTIALS_BASIC_AUTH}": base64.StdEncoding.EncodeToString([]byte(*JfrogUser + ":" + *JfrogPassword)), + "${ACCESS_TOKEN}": *JfrogAccessToken, + "${PYPI_LOCAL_REPO}": PypiLocalRepo, + "${PYPI_REMOTE_REPO}": PypiRemoteRepo, + "${PYPI_VIRTUAL_REPO}": PypiVirtualRepo, + "${PIPENV_REMOTE_REPO}": PipenvRemoteRepo, + "${PIPENV_VIRTUAL_REPO}": PipenvVirtualRepo, + "${POETRY_LOCAL_REPO}": PoetryLocalRepo, + "${POETRY_REMOTE_REPO}": PoetryRemoteRepo, + "${POETRY_VIRTUAL_REPO}": PoetryVirtualRepo, + "${UV_LOCAL_REPO}": UvLocalRepo, + "${UV_REMOTE_REPO}": UvRemoteRepo, + "${UV_VIRTUAL_REPO}": UvVirtualRepo, + "${NIX_LOCAL_REPO}": NixLocalRepo, + "${NIX_REMOTE_REPO}": NixRemoteRepo, + "${NIX_VIRTUAL_REPO}": NixVirtualRepo, + "${CONAN_LOCAL_REPO}": ConanLocalRepo, + "${CONAN_REMOTE_REPO}": ConanRemoteRepo, + "${CONAN_VIRTUAL_REPO}": ConanVirtualRepo, + "${HELM_REPO}": HelmLocalRepo, + "${HUGGINGFACE_LOCAL_REPO}": HuggingFaceLocalRepo, + "${BUILD_NAME1}": RtBuildName1, + "${BUILD_NAME2}": RtBuildName2, + "${BUNDLE_NAME}": BundleName, + "${DIST_REPO1}": DistRepo1, + "${DIST_REPO2}": DistRepo2, + "{USER_NAME_1}": UserName1, + "{PASSWORD_1}": Password1, + "{USER_NAME_2}": UserName2, + "{PASSWORD_2}": Password2, + "${LC_BUILD_NAME1}": LcBuildName1, + "${LC_BUILD_NAME2}": LcBuildName2, + "${LC_BUILD_NAME3}": LcBuildName3, + "${RB_NAME1}": LcRbName1, + "${RB_NAME2}": LcRbName2, + "${DEV_REPO}": RtDevRepo, + "${PROD_REPO1}": RtProdRepo1, + "${PROD_REPO2}": RtProdRepo2, + } +} + +// Add timestamp to builds and repositories names +func AddTimestampToGlobalVars() { + // Make sure the global timestamp is added only once even in case of multiple tests flags + if timestampAdded { + return + } + timestamp := strconv.FormatInt(time.Now().Unix(), 10) + uniqueSuffix := "-" + timestamp + if *ciRunId != "" { + uniqueSuffix = "-" + *ciRunId + uniqueSuffix + } + // Artifactory accepts only lowercase repository names + uniqueSuffix = strings.ToLower(uniqueSuffix) + + // Repositories + DistRepo1 += uniqueSuffix + DistRepo2 += uniqueSuffix + GoRepo += uniqueSuffix + GoRemoteRepo += uniqueSuffix + GoVirtualRepo += uniqueSuffix + DockerLocalRepo += uniqueSuffix + DockerLocalPromoteRepo += uniqueSuffix + DockerRemoteRepo += uniqueSuffix + DockerVirtualRepo += uniqueSuffix + OciLocalRepo += uniqueSuffix + OciRemoteRepo += uniqueSuffix + TerraformRepo += uniqueSuffix + GradleRemoteRepo += uniqueSuffix + GradleRepo += uniqueSuffix + MvnRemoteRepo += uniqueSuffix + MvnRepo1 += uniqueSuffix + MvnRepo2 += uniqueSuffix + NpmRepo += uniqueSuffix + NpmScopedRepo += uniqueSuffix + NpmRemoteRepo += uniqueSuffix + NugetRemoteRepo += uniqueSuffix + YarnRemoteRepo += uniqueSuffix + PypiLocalRepo += uniqueSuffix + PypiRemoteRepo += uniqueSuffix + PypiVirtualRepo += uniqueSuffix + PipenvRemoteRepo += uniqueSuffix + PipenvVirtualRepo += uniqueSuffix + PoetryLocalRepo += uniqueSuffix + PoetryRemoteRepo += uniqueSuffix + PoetryVirtualRepo += uniqueSuffix + UvLocalRepo += uniqueSuffix + UvRemoteRepo += uniqueSuffix + UvVirtualRepo += uniqueSuffix + ConanLocalRepo += uniqueSuffix + ConanRemoteRepo += uniqueSuffix + ConanVirtualRepo += uniqueSuffix + HelmLocalRepo += uniqueSuffix + HuggingFaceLocalRepo += uniqueSuffix + RtDebianRepo += uniqueSuffix + RtLfsRepo += uniqueSuffix + RtRepo1 += uniqueSuffix + RtRepo1And2 += uniqueSuffix + RtRepo1And2Placeholder += uniqueSuffix + RtRepo2 += uniqueSuffix + RtVirtualRepo += uniqueSuffix + RtDevRepo += uniqueSuffix + RtProdRepo1 += uniqueSuffix + RtProdRepo2 += uniqueSuffix + + // Builds/bundles/images + BundleName += uniqueSuffix + DockerBuildName += uniqueSuffix + DockerImageName += uniqueSuffix + DotnetBuildName += uniqueSuffix + GoBuildName += uniqueSuffix + GradleBuildName += uniqueSuffix + NpmBuildName += uniqueSuffix + PnpmBuildName += uniqueSuffix + YarnBuildName += uniqueSuffix + MvnBuildName += uniqueSuffix + NuGetBuildName += uniqueSuffix + PipBuildName += uniqueSuffix + PipenvBuildName += uniqueSuffix + PoetryBuildName += uniqueSuffix + ConanBuildName += uniqueSuffix + HelmBuildName += uniqueSuffix + HuggingFaceBuildName += uniqueSuffix + RtBuildName1 += uniqueSuffix + RtBuildName2 += uniqueSuffix + RtBuildNameWithSpecialChars += uniqueSuffix + RtPermissionTargetName += uniqueSuffix + LcBuildName1 += uniqueSuffix + LcBuildName2 += uniqueSuffix + LcBuildName3 += uniqueSuffix + LcRbName1 += uniqueSuffix + LcRbName2 += uniqueSuffix + LcRbName3 += uniqueSuffix + + // Users + UserName1 += uniqueSuffix + UserName2 += uniqueSuffix + + randomSequence := rand.New(rand.NewSource(time.Now().Unix())) + Password1 += uniqueSuffix + strconv.FormatFloat(randomSequence.Float64(), 'f', 2, 32) + Password2 += uniqueSuffix + strconv.FormatFloat(randomSequence.Float64(), 'f', 2, 32) + + // Projects + ProjectKey += timestamp[len(timestamp)-7:] + + timestampAdded = true +} + +// Replace all variables in the form of ${VARIABLE} in the input file, according to the substitution map (see getSubstitutionMap()). +// path - Path to the input file. +// destPath - Path to the output file. If empty, the output file will be under ${CWD}/tmp/. +func ReplaceTemplateVariables(path, destPath string) (string, error) { + return commonCliUtils.ReplaceTemplateVariables(path, destPath, getSubstitutionMap()) +} + +func CreateSpec(fileName string) (string, error) { + searchFilePath := GetFilePathForArtifactory(fileName) + searchFilePath, err := ReplaceTemplateVariables(searchFilePath, "") + return searchFilePath, err +} + +func ConvertSliceToMap(props []utils.Property) map[string][]string { + propsMap := make(map[string][]string) + for _, item := range props { + propsMap[item.Key] = append(propsMap[item.Key], item.Value) + } + return propsMap +} + +// Set user and password from access token. +// Return the original user and password to allow restoring them in the end of the test. +func SetBasicAuthFromAccessToken() (string, string) { + origUser := *JfrogUser + origPassword := *JfrogPassword + *JfrogUser = auth.ExtractUsernameFromAccessToken(*JfrogAccessToken) + *JfrogPassword = *JfrogAccessToken + return origUser, origPassword +} + +// Clean items with timestamp older than 24 hours. Used to delete old repositories, builds, release bundles and Docker images. +// baseItemNames - The items to delete without timestamp, i.e. [cli-rt1, cli-rt2, ...] +// getActualItems - Function that returns all actual items in the remote server, i.e. [cli-rt1-1592990748, cli-rt2-1592990748, ...] +// deleteItem - Function that deletes the item by name +func CleanUpOldItems(baseItemNames []string, getActualItems func() ([]string, error), deleteItem func(string)) { + actualItems, err := getActualItems() + if err != nil { + log.Warn("Couldn't retrieve items", err) + return + } + now := time.Now() + for _, baseItemName := range baseItemNames { + itemPattern := regexp.MustCompile(`^` + baseItemName + `[\w-]*-(\d*)$`) + for _, item := range actualItems { + regexGroups := itemPattern.FindStringSubmatch(item) + if regexGroups == nil { + // Item does not match + continue + } + + itemTimestamp, err := strconv.ParseInt(regexGroups[len(regexGroups)-1], 10, 64) + if err != nil { + log.Warn("Error while parsing timestamp of", item, err) + continue + } + + itemTime := time.Unix(itemTimestamp, 0) + if now.Sub(itemTime).Hours() > 24 { + deleteItem(item) + } + } + } +} + +// Set new logger with output redirection to a null logger. This is useful for negative tests. +// Caller is responsible to set the old log back. +func RedirectLogOutputToNil() (previousLog log.Log) { + previousLog = log.Logger + newLog := log.NewLogger(corelog.GetCliLogLevel(), nil) + newLog.SetOutputWriter(io.Discard) + newLog.SetLogsWriter(io.Discard, 0) + log.SetLogger(newLog) + return previousLog +} + +// Redirect output to a file, execute the command and read output. +// The reason for redirecting to a file and not to a buffer is the limited +// size of the buffer while using os.Pipe. +func GetCmdOutput(t *testing.T, jfrogCli *coreTests.JfrogCli, cmd ...string) ([]byte, []byte, error) { + oldStdout := os.Stdout + oldStdErr := os.Stderr + temp, err := os.CreateTemp("", "output") + assert.NoError(t, err) + tempErr, err := os.CreateTemp("", "output") + assert.NoError(t, err) + os.Stdout = temp + os.Stderr = tempErr + defer func() { + os.Stdout = oldStdout + os.Stderr = oldStdErr + assert.NoError(t, temp.Close()) + assert.NoError(t, os.Remove(temp.Name())) + }() + err = jfrogCli.Exec(cmd...) + assert.NoError(t, err) + content, err := os.ReadFile(temp.Name()) + assert.NoError(t, err) + errContent, err := os.ReadFile(tempErr.Name()) + return content, errContent, err +} + +func VerifySha256DetailedSummaryFromBuffer(t *testing.T, buffer *bytes.Buffer, logger log.Log) { + content := buffer.Bytes() + buffer.Reset() + logger.Output(string(content)) + + var result summary.BuildInfoSummary + err := json.Unmarshal(content, &result) + assert.NoError(t, err) + + assert.Equal(t, summary.Success, result.Status) + assert.True(t, result.Totals.Success > 0) + assert.Equal(t, 0, result.Totals.Failure) + // Verify a sha256 was returned + assert.NotEmpty(t, result.Sha256Array, "Summary validation failed - no sha256 has returned from Artifactory.") + for _, sha256 := range result.Sha256Array { + // Verify sha256 is valid (a string size 256 characters) and not an empty string. + assert.Equal(t, 64, len(sha256.Sha256Str), "Summary validation failed - invalid sha256 has returned from artifactory") + } +} + +func VerifySha256DetailedSummaryFromResult(t *testing.T, result *commandutils.Result) { + if assert.NotNil(t, result) { + reader := result.Reader() + defer func() { + assert.NoError(t, reader.Close()) + }() + assert.NoError(t, reader.GetError()) + for transferDetails := new(clientutils.FileTransferDetails); reader.NextRecord(transferDetails) == nil; transferDetails = new(clientutils.FileTransferDetails) { + assert.Equal(t, 64, len(transferDetails.Sha256), "Summary validation failed - invalid sha256 has returned from artifactory") + } + } +} + +func SkipKnownFailingTest(t *testing.T) { + skipDate := time.Date(2023, time.November, 1, 0, 0, 0, 0, time.UTC) + if time.Now().Before(skipDate) { + t.Skip("Skipping a known failing test, will resume testing after ", skipDate.String()) + } else { + t.Error("Not skipping test. Please fix the test or delay the skipMonth") + } +} + +func CreateContext(t *testing.T, testFlags, testArgs []string) (*cli.Context, *bytes.Buffer) { + flagSet := createFlagSet(t, testFlags, testArgs) + app := cli.NewApp() + app.Writer = &bytes.Buffer{} + return cli.NewContext(app, flagSet, nil), &bytes.Buffer{} +} + +// Create flag set with input flags and arguments. +func createFlagSet(t *testing.T, flags []string, args []string) *flag.FlagSet { + flagSet := flag.NewFlagSet("TestFlagSet", flag.ContinueOnError) + flags = append(flags, "url=http://127.0.0.1:8081/artifactory") + var cmdFlags []string + for _, curFlag := range flags { + flagSet.String(strings.Split(curFlag, "=")[0], "", "") + cmdFlags = append(cmdFlags, "--"+curFlag) + } + cmdFlags = append(cmdFlags, args...) + assert.NoError(t, flagSet.Parse(cmdFlags)) + return flagSet +} + +func SkipTest(reason string) { + log.Info(reason) + os.Exit(0) +} + +// SetupGitHubActionsEnv enables CI VCS property collection for a test. +// When running on GitHub Actions, it uses the real environment variables. +// When running locally, it sets mock CI environment variables. +// Returns a cleanup function and the actual org/repo values to use for validation. +func SetupGitHubActionsEnv(t *testing.T) (cleanup func(), actualOrg, actualRepo string) { + callbacks := []func(){} + + // Enable CI VCS property collection for this test (unset the disable flag) + callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, "JFROG_CLI_CI_VCS_PROPS_DISABLED", "")) + + // Check if we're running on GitHub Actions + if os.Getenv("GITHUB_ACTIONS") == "true" { + // Running on CI - use real environment variables + ghRepo := os.Getenv("GITHUB_REPOSITORY") + if ghRepo != "" { + parts := strings.Split(ghRepo, "/") + if len(parts) == 2 { + actualOrg = parts[0] + actualRepo = parts[1] + } + } + if actualOrg == "" { + actualOrg = os.Getenv("GITHUB_REPOSITORY_OWNER") + } + } else { + // Running locally - set mock CI environment variables + actualOrg = "test-org" + actualRepo = "test-repo" + + // Set the required CI environment variables for cienv.GetCIVcsInfo() to detect CI + callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, "CI", "true")) + callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, "GITHUB_ACTIONS", "true")) + callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, "GITHUB_WORKFLOW", "test")) + callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, "GITHUB_RUN_ID", "12345")) + callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, "GITHUB_REPOSITORY_OWNER", actualOrg)) + callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, "GITHUB_REPOSITORY", actualOrg+"/"+actualRepo)) + } + + cleanup = func() { + for _, cb := range callbacks { + cb() + } + } + return cleanup, actualOrg, actualRepo +} + +// SetupGitHubActionsEnvForLocalGitMerge enables CI VCS collection with provider/org/repo +// but clears url/revision/branch CI env vars so local git fallback is exercised. +func SetupGitHubActionsEnvForLocalGitMerge(t *testing.T) (cleanup func(), actualOrg, actualRepo string) { + t.Helper() + cleanupBase, actualOrg, actualRepo := SetupGitHubActionsEnv(t) + + var callbacks []func() + for _, key := range []string{ + "GITHUB_SERVER_URL", + "GITHUB_SHA", + "GITHUB_REF", + "GITHUB_REF_NAME", + "GITHUB_HEAD_REF", + } { + callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, key, "")) + } + + return func() { + for _, cb := range callbacks { + cb() + } + cleanupBase() + }, actualOrg, actualRepo +} + +// ValidateCIVcsPropsOnArtifacts validates that CI VCS properties are set on artifacts. +func ValidateCIVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem, expectedProvider, expectedOrg, expectedRepo string) { + for _, item := range resultItems { + propertiesMap := ConvertPropertiesToMap(item.Properties) + + // Validate vcs.provider + if expectedProvider != "" { + vals, ok := propertiesMap["vcs.provider"] + assert.True(t, ok, "Missing vcs.provider on %s", item.Name) + assert.Contains(t, vals, expectedProvider, "Wrong vcs.provider on %s", item.Name) + } + + // Validate vcs.org + if expectedOrg != "" { + vals, ok := propertiesMap["vcs.org"] + assert.True(t, ok, "Missing vcs.org on %s", item.Name) + assert.Contains(t, vals, expectedOrg, "Wrong vcs.org on %s", item.Name) + } + + // Validate vcs.repo + if expectedRepo != "" { + vals, ok := propertiesMap["vcs.repo"] + assert.True(t, ok, "Missing vcs.repo on %s", item.Name) + assert.Contains(t, vals, expectedRepo, "Wrong vcs.repo on %s", item.Name) + } + } +} + +// ConvertPropertiesToMap converts a slice of Property to a map for easier lookup. +func ConvertPropertiesToMap(properties []utils.Property) map[string][]string { + propsMap := make(map[string][]string) + for _, prop := range properties { + propsMap[prop.Key] = append(propsMap[prop.Key], prop.Value) + } + return propsMap +} + +// ValidateNoCIVcsPropsOnArtifacts validates that CI VCS properties are NOT set on artifacts. +func ValidateNoCIVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem) { + for _, item := range resultItems { + propertiesMap := ConvertPropertiesToMap(item.Properties) + _, hasProvider := propertiesMap["vcs.provider"] + _, hasOrg := propertiesMap["vcs.org"] + _, hasRepo := propertiesMap["vcs.repo"] + assert.False(t, hasProvider, "vcs.provider should not be set when not in CI on %s", item.Name) + assert.False(t, hasOrg, "vcs.org should not be set when not in CI on %s", item.Name) + assert.False(t, hasRepo, "vcs.repo should not be set when not in CI on %s", item.Name) + } +} + +// ValidateCIVcsPropsIfPresent validates CI VCS properties only if at least one artifact has them. +// This is useful for build tools where OriginalDeploymentRepo may not always be set. +// Logs a warning if no artifacts have CI VCS properties. +func ValidateCIVcsPropsIfPresent(t *testing.T, resultItems []utils.ResultItem, expectedProvider, expectedOrg, expectedRepo string) { + // Check if any artifact has CI VCS properties + hasProps := false + for _, item := range resultItems { + propertiesMap := ConvertPropertiesToMap(item.Properties) + if _, ok := propertiesMap["vcs.provider"]; ok { + hasProps = true + break + } + } + + if !hasProps { + t.Log("Warning: No artifacts have CI VCS properties set. " + + "This may indicate OriginalDeploymentRepo is not populated in build-info.") + return + } + + // Validate all artifacts that have properties + for _, item := range resultItems { + propertiesMap := ConvertPropertiesToMap(item.Properties) + + // Only validate if the artifact has any VCS property + if _, hasAny := propertiesMap["vcs.provider"]; !hasAny { + continue + } + + if expectedProvider != "" { + vals, ok := propertiesMap["vcs.provider"] + assert.True(t, ok, "Missing vcs.provider on %s", item.Name) + assert.Contains(t, vals, expectedProvider, "Wrong vcs.provider on %s", item.Name) + } + + if expectedOrg != "" { + vals, ok := propertiesMap["vcs.org"] + assert.True(t, ok, "Missing vcs.org on %s", item.Name) + assert.Contains(t, vals, expectedOrg, "Wrong vcs.org on %s", item.Name) + } + + if expectedRepo != "" { + vals, ok := propertiesMap["vcs.repo"] + assert.True(t, ok, "Missing vcs.repo on %s", item.Name) + assert.Contains(t, vals, expectedRepo, "Wrong vcs.repo on %s", item.Name) + } + } +} + +// SetupLocalGitVcsEnv enables VCS property collection and clears CI detection +// so only local git fallback is exercised. +func SetupLocalGitVcsEnv(t *testing.T) (cleanup func()) { + t.Helper() + var callbacks []func() + + for _, key := range []string{ + "JFROG_CLI_CI_VCS_PROPS_DISABLED", // set to "" to enable + "CI", "GITHUB_ACTIONS", "GITHUB_WORKFLOW", "GITHUB_RUN_ID", + "GITHUB_REPOSITORY", "GITHUB_REPOSITORY_OWNER", + "GITHUB_SERVER_URL", "GITHUB_SHA", "GITHUB_REF", "GITHUB_REF_NAME", "GITHUB_HEAD_REF", + } { + callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, key, "")) + } + + return func() { + for _, cb := range callbacks { + cb() + } + } +} + +// ValidateLocalGitVcsPropsOnArtifacts asserts vcs.url, vcs.revision, vcs.branch on every item. +func ValidateLocalGitVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem, expectedURL, expectedRevision, expectedBranch string) { + t.Helper() + for _, item := range resultItems { + propertiesMap := ConvertPropertiesToMap(item.Properties) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.url", expectedURL) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.revision", expectedRevision) + if expectedBranch != "" { + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.branch", expectedBranch) + } + } +} + +func assertLocalGitProp(t *testing.T, itemName string, props map[string][]string, key, expected string) { + t.Helper() + vals, ok := props[key] + assert.True(t, ok, "Missing %s on %s", key, itemName) + assert.Contains(t, vals, expected, "Wrong %s on %s", key, itemName) +} + +// ValidateNoLocalGitVcsPropsOnArtifacts asserts url/revision/branch are absent. +func ValidateNoLocalGitVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem) { + t.Helper() + for _, item := range resultItems { + propertiesMap := ConvertPropertiesToMap(item.Properties) + _, hasURL := propertiesMap["vcs.url"] + _, hasRev := propertiesMap["vcs.revision"] + _, hasBranch := propertiesMap["vcs.branch"] + assert.False(t, hasURL, "vcs.url should not be set on %s", item.Name) + assert.False(t, hasRev, "vcs.revision should not be set on %s", item.Name) + assert.False(t, hasBranch, "vcs.branch should not be set on %s", item.Name) + } +} + +// ValidateCIAndLocalGitVcsPropsOnArtifacts asserts CI props plus local git props coexist. +func ValidateCIAndLocalGitVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem, + expectedProvider, expectedOrg, expectedRepo, expectedURL, expectedRevision, expectedBranch string) { + t.Helper() + for _, item := range resultItems { + propertiesMap := ConvertPropertiesToMap(item.Properties) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.provider", expectedProvider) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.org", expectedOrg) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.repo", expectedRepo) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.url", expectedURL) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.revision", expectedRevision) + if expectedBranch != "" { + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.branch", expectedBranch) + } + } +} diff --git a/utils/tests/utils_test.go b/utils/tests/utils_test.go new file mode 100644 index 000000000..d384b9e8c --- /dev/null +++ b/utils/tests/utils_test.go @@ -0,0 +1,31 @@ +package tests + +import ( + "testing" + + "github.com/jfrog/build-info-go/utils/cienv" + "github.com/stretchr/testify/assert" +) + +func TestSetupGitHubActionsEnvForLocalGitMerge_ClearsUrlRevisionBranch(t *testing.T) { + t.Setenv("CI", "true") + t.Setenv("GITHUB_ACTIONS", "true") + t.Setenv("GITHUB_WORKFLOW", "wf") + t.Setenv("GITHUB_RUN_ID", "99") + t.Setenv("GITHUB_REPOSITORY_OWNER", "jfrog") + t.Setenv("GITHUB_REPOSITORY", "jfrog/jfrog-cli") + t.Setenv("GITHUB_SERVER_URL", "https://github.com") + t.Setenv("GITHUB_SHA", "abc123") + t.Setenv("GITHUB_REF", "refs/heads/feature") + + cleanup, _, _ := SetupGitHubActionsEnvForLocalGitMerge(t) + defer cleanup() + + info := cienv.GetCIVcsInfo() + assert.Equal(t, "github", info.Provider) + assert.Equal(t, "jfrog", info.Org) + assert.Equal(t, "jfrog-cli", info.Repo) + assert.Empty(t, info.Url) + assert.Empty(t, info.Revision) + assert.Empty(t, info.Branch) +} diff --git a/utils/tests/vcs_fixtures.go b/utils/tests/vcs_fixtures.go new file mode 100644 index 000000000..2b94d5cbf --- /dev/null +++ b/utils/tests/vcs_fixtures.go @@ -0,0 +1,92 @@ +package tests + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + biutils "github.com/jfrog/build-info-go/utils" + coretests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + VcsFixtureMainURL = "https://github.com/jfrog/jfrog-cli.git" + VcsFixtureMainRevision = "d63c5957ad6819f4c02a817abe757f210d35ff92" + VcsFixtureMainBranch = "master" + + VcsFixtureOtherURL = "https://github.com/jfrog/jfrog-client-go.git" + VcsFixtureOtherRevision = "ad99b6c068283878fde4d49423728f0bdc00544a" + VcsFixtureOtherBranch = "InnerGit" +) + +// testResourcesDir returns the absolute path to the repo's testdata/ directory. +// It is resolved from this source file's location, not os.Getwd(). +func testResourcesDir() string { + _, filename, _, ok := runtime.Caller(0) + if !ok { + abs, err := filepath.Abs(filepath.FromSlash(GetTestResourcesPath())) + if err != nil { + return filepath.FromSlash(GetTestResourcesPath()) + } + return abs + } + abs, err := filepath.Abs(filepath.Join(filepath.Dir(filename), "..", "..", "testdata")) + if err != nil { + return filepath.Join(filepath.Dir(filename), "..", "..", "testdata") + } + return abs +} + +func vcsFixtureSrcDir() string { + return filepath.Join(testResourcesDir(), "vcs") +} + +func vcsGitdataSrcDir() string { + return filepath.Join(vcsFixtureSrcDir(), "gitdata") +} + +// CopyVcsGitFixture copies testdata/vcs into destDir and renames gitdata -> .git. +// Returns the absolute path to destDir. +func CopyVcsGitFixture(t *testing.T, destDir string) string { + t.Helper() + src := vcsFixtureSrcDir() + assert.NoError(t, biutils.CopyDir(src, destDir, true, nil)) + if found, err := fileutils.IsDirExists(filepath.Join(destDir, "gitdata"), false); found { + assert.NoError(t, err) + coretests.RenamePath(filepath.Join(destDir, "gitdata"), filepath.Join(destDir, ".git"), t) + } + if found, err := fileutils.IsDirExists(filepath.Join(destDir, "OtherGit", "gitdata"), false); found { + assert.NoError(t, err) + coretests.RenamePath( + filepath.Join(destDir, "OtherGit", "gitdata"), + filepath.Join(destDir, "OtherGit", ".git"), + t, + ) + } + abs, err := filepath.Abs(destDir) + assert.NoError(t, err) + return abs +} + +// CopyGitFixtureIntoProject installs testdata/vcs/gitdata as projectDir/.git. +func CopyGitFixtureIntoProject(t *testing.T, projectDir string) { + t.Helper() + src := vcsGitdataSrcDir() + gitDir := filepath.Join(projectDir, ".git") + stagingDir := filepath.Join(projectDir, "gitdata-staging") + + if fileutils.IsPathExists(gitDir, false) { + require.NoError(t, os.RemoveAll(gitDir)) + } + require.NoError(t, os.RemoveAll(stagingDir)) + + require.NoError(t, biutils.CopyDir(src, stagingDir, true, nil)) + coretests.RenamePath(stagingDir, gitDir, t) + + require.FileExists(t, filepath.Join(gitDir, "HEAD")) + require.FileExists(t, filepath.Join(gitDir, "config")) +} diff --git a/utils/tests/vcs_fixtures_test.go b/utils/tests/vcs_fixtures_test.go new file mode 100644 index 000000000..a9968101a --- /dev/null +++ b/utils/tests/vcs_fixtures_test.go @@ -0,0 +1,27 @@ +package tests + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCopyGitFixtureIntoProject_WorksAfterChdir(t *testing.T) { + repoRoot, err := os.Getwd() + require.NoError(t, err) + + projectDir := t.TempDir() + subDir := filepath.Join(projectDir, "nested") + require.NoError(t, os.MkdirAll(subDir, 0o755)) + + // Simulate prepareGoProject leaving cwd inside the project tree. + require.NoError(t, os.Chdir(subDir)) + t.Cleanup(func() { _ = os.Chdir(repoRoot) }) + + CopyGitFixtureIntoProject(t, projectDir) + + require.FileExists(t, filepath.Join(projectDir, ".git", "HEAD")) + require.FileExists(t, filepath.Join(projectDir, ".git", "config")) +} diff --git a/uv_test.go b/uv_test.go index afe08fffa..94132fe67 100644 --- a/uv_test.go +++ b/uv_test.go @@ -1490,6 +1490,39 @@ func TestUvBuildPublishWithCIVcsProps(t *testing.T) { assert.Greater(t, artifactCount, 0, "no artifacts were validated for CI VCS properties") } +func TestUvPublishWithLocalGitVcsProps(t *testing.T) { + initUvTest(t) + defer cleanUvTest(t) + + buildName := tests.UvBuildName + "-local-git" + buildNumber := "1" + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + projectPath := createUvProject(t, "uv-local-git", "uvproject") + tests.CopyGitFixtureIntoProject(t, projectPath) + + require.NoError(t, runUvCmd(t, projectPath, "build")) + require.NoError(t, runUvCmd(t, projectPath, "publish", + "--build-name="+buildName, "--build-number="+buildNumber)) + require.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + require.NoError(t, err) + require.True(t, found) + + serviceManager, err := artUtils.CreateServiceManager(serverDetails, 3, 1000, false) + require.NoError(t, err) + + count := tests.ValidateLocalGitVcsPropsOnBuildInfoArtifacts(t, serviceManager, publishedBuildInfo, tests.UvLocalRepo, + tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + assert.Greater(t, count, 0) +} + // --------------------------------------------------------------------------- // P0 — Artifact sha256 not "untrusted" in Artifactory (#Cat7 NEW requirement) // --------------------------------------------------------------------------- From c0e91133178c83bee0ec428ece4def73928b51d0 Mon Sep 17 00:00:00 2001 From: attiasas Date: Thu, 18 Jun 2026 15:33:01 +0300 Subject: [PATCH 2/9] fix changes --- utils/tests/utils copy.go | 1060 ------------------------------------- utils/tests/utils.go | 102 +++- 2 files changed, 100 insertions(+), 1062 deletions(-) delete mode 100644 utils/tests/utils copy.go diff --git a/utils/tests/utils copy.go b/utils/tests/utils copy.go deleted file mode 100644 index e8653d1cf..000000000 --- a/utils/tests/utils copy.go +++ /dev/null @@ -1,1060 +0,0 @@ -package tests - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "math/rand" - "os" - "path/filepath" - "regexp" - "strconv" - "strings" - "testing" - "time" - - ioutils "github.com/jfrog/gofrog/io" - "github.com/jfrog/jfrog-client-go/utils/tests" - - "github.com/urfave/cli" - - buildinfo "github.com/jfrog/build-info-go/entities" - "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/generic" - commandutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils" - artUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" - "github.com/jfrog/jfrog-cli-core/v2/common/spec" - commonCliUtils "github.com/jfrog/jfrog-cli-core/v2/common/tests" - "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - corelog "github.com/jfrog/jfrog-cli-core/v2/utils/log" - coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" - "github.com/jfrog/jfrog-cli/utils/summary" - "github.com/jfrog/jfrog-client-go/artifactory/services" - "github.com/jfrog/jfrog-client-go/artifactory/services/utils" - "github.com/jfrog/jfrog-client-go/auth" - clientutils "github.com/jfrog/jfrog-client-go/utils" - "github.com/jfrog/jfrog-client-go/utils/io/fileutils" - "github.com/jfrog/jfrog-client-go/utils/log" - "github.com/stretchr/testify/assert" -) - -var ( - JfrogUrl *string - JfrogUser *string - JfrogPassword *string - JfrogSshKeyPath *string - JfrogSshPassphrase *string - JfrogAccessToken *string - JfrogTargetUrl *string - JfrogTargetAccessToken *string - JfrogHome *string - TestArtifactoryProject *bool - TestArtifactory *bool - TestArtifactoryProxy *bool - TestDistribution *bool - TestDocker *bool - TestPodman *bool - TestDockerScan *bool - ContainerRegistry *string - TestGo *bool - TestNpm *bool - TestPnpm *bool - TestGradle *bool - TestMaven *bool - TestNuget *bool - TestPip *bool - TestPipenv *bool - TestPoetry *bool - TestUv *bool - TestNix *bool - TestConan *bool - TestHelm *bool - TestHuggingFace *bool - TestPlugins *bool - TestXray *bool - TestAccess *bool - TestTransfer *bool - TestLifecycle *bool - TestEvidence *bool - TestApi *bool - TestGhostFrog *bool - HideUnitTestLog *bool - ciRunId *string - InstallDataTransferPlugin *bool - timestampAdded bool -) - -func init() { - JfrogUrl = flag.String("jfrog.url", "http://localhost:8081/", "JFrog platform url") - JfrogUser = flag.String("jfrog.user", "admin", "JFrog platform username") - JfrogPassword = flag.String("jfrog.password", "password", "JFrog platform password") - JfrogSshKeyPath = flag.String("jfrog.sshKeyPath", "", "Ssh key file path") - JfrogSshPassphrase = flag.String("jfrog.sshPassphrase", "", "Ssh key passphrase") - JfrogAccessToken = flag.String("jfrog.adminToken", tests.GetLocalArtifactoryTokenIfNeeded(*JfrogUrl), "JFrog platform admin token") - JfrogTargetUrl = flag.String("jfrog.targetUrl", "", "JFrog target platform url for transfer tests") - JfrogTargetAccessToken = flag.String("jfrog.targetAdminToken", "", "JFrog target platform admin token for transfer tests") - JfrogHome = flag.String("jfrog.home", "", "The JFrog home directory of the local Artifactory installation") - TestArtifactory = flag.Bool("test.artifactory", false, "Test Artifactory") - TestArtifactoryProject = flag.Bool("test.artifactoryProject", false, "Test Artifactory project") - TestArtifactoryProxy = flag.Bool("test.artifactoryProxy", false, "Test Artifactory proxy") - TestDistribution = flag.Bool("test.distribution", false, "Test distribution") - TestDocker = flag.Bool("test.docker", false, "Test Docker build") - TestDockerScan = flag.Bool("test.dockerScan", false, "Test Docker scan") - TestPodman = flag.Bool("test.podman", false, "Test Podman build") - TestGo = flag.Bool("test.go", false, "Test Go") - TestNpm = flag.Bool("test.npm", false, "Test Npm") - TestPnpm = flag.Bool("test.pnpm", false, "Test Pnpm") - TestGradle = flag.Bool("test.gradle", false, "Test Gradle") - TestMaven = flag.Bool("test.maven", false, "Test Maven") - TestNuget = flag.Bool("test.nuget", false, "Test Nuget") - TestPip = flag.Bool("test.pip", false, "Test Pip") - TestPipenv = flag.Bool("test.pipenv", false, "Test Pipenv") - TestPoetry = flag.Bool("test.poetry", false, "Test Poetry") - TestUv = flag.Bool("test.uv", false, "Test UV") - TestNix = flag.Bool("test.nix", false, "Test Nix") - TestConan = flag.Bool("test.conan", false, "Test Conan") - TestHelm = flag.Bool("test.helm", false, "Test Helm") - TestHuggingFace = flag.Bool("test.huggingface", false, "Test HuggingFace") - TestPlugins = flag.Bool("test.plugins", false, "Test Plugins") - TestXray = flag.Bool("test.xray", false, "Test Xray") - TestAccess = flag.Bool("test.access", false, "Test Access") - TestTransfer = flag.Bool("test.transfer", false, "Test files transfer") - TestLifecycle = flag.Bool("test.lifecycle", false, "Test lifecycle") - TestEvidence = flag.Bool("test.evidence", false, "Test evidence") - TestApi = flag.Bool("test.api", false, "Test api command") - TestGhostFrog = flag.Bool("test.ghostFrog", false, "Test Ghost Frog package alias") - ContainerRegistry = flag.String("test.containerRegistry", "localhost:8082", "Container registry") - HideUnitTestLog = flag.Bool("test.hideUnitTestLog", false, "Hide unit tests logs and print it in a file") - InstallDataTransferPlugin = flag.Bool("test.installDataTransferPlugin", false, "Install data-transfer plugin on the source Artifactory server") - ciRunId = flag.String("ci.runId", "", "A unique identifier used as a suffix to create repositories and builds in the tests") -} - -func CleanFileSystem() { - removeDirs(Out, Temp) -} - -func removeDirs(dirs ...string) { - for _, dir := range dirs { - isExist, err := fileutils.IsDirExists(dir, false) - if err != nil { - log.Error(err) - } - if isExist { - err = fileutils.RemoveTempDir(dir) - if err != nil { - log.Error(errors.New("Cannot remove path: " + dir + " due to: " + err.Error())) - } - } - } -} - -func VerifyExistLocally(expected, actual []string, t *testing.T) { - if len(actual) == 0 && len(expected) != 0 { - t.Error("Couldn't find all expected files, expected: " + strconv.Itoa(len(expected)) + ", found: " + strconv.Itoa(len(actual))) - } - err := compare(expected, actual) - assert.NoError(t, err) -} - -func ValidateListsIdentical(expected, actual []string) error { - if len(actual) != len(expected) { - return fmt.Errorf("unexpected behavior, \nexpected: [%s], \nfound: [%s]", strings.Join(expected, ", "), strings.Join(actual, ", ")) - } - err := compare(expected, actual) - return err -} - -func ValidateChecksums(filePath string, expectedChecksum buildinfo.Checksum, t *testing.T) { - localFileDetails, err := fileutils.GetFileDetails(filePath, true) - if err != nil { - t.Error("Couldn't calculate sha1, " + err.Error()) - } - if localFileDetails.Checksum.Sha1 != expectedChecksum.Sha1 { - t.Error("sha1 mismatch for "+filePath+", expected: "+expectedChecksum.Sha1, "found: "+localFileDetails.Checksum.Sha1) - } - if localFileDetails.Checksum.Md5 != expectedChecksum.Md5 { - t.Error("md5 mismatch for "+filePath+", expected: "+expectedChecksum.Md5, "found: "+localFileDetails.Checksum.Sha1) - } - if localFileDetails.Checksum.Sha256 != expectedChecksum.Sha256 { - t.Error("sha256 mismatch for "+filePath+", expected: "+expectedChecksum.Sha256, "found: "+localFileDetails.Checksum.Sha1) - } -} - -func compare(expected, actual []string) error { - for _, v := range expected { - for i, r := range actual { - if v == r { - break - } - if i == len(actual)-1 { - return errors.New("Missing file : " + v) - } - } - } - return nil -} - -func getPathsFromSearchResults(searchResults []artUtils.SearchResult) []string { - var paths []string - for _, result := range searchResults { - paths = append(paths, result.Path) - } - return paths -} - -func CompareExpectedVsActual(expected []string, actual []artUtils.SearchResult, t *testing.T) { - actualPaths := getPathsFromSearchResults(actual) - assert.ElementsMatch(t, expected, actualPaths, fmt.Sprintf("Expected: %v \nActual: %v", expected, actualPaths)) -} - -func GetTestResourcesPath() string { - dir, _ := os.Getwd() - return filepath.ToSlash(dir + "/testdata/") -} - -func GetFilePathForArtifactory(fileName string) string { - return GetTestResourcesPath() + "filespecs/" + fileName -} - -func GetTestsLogsDir() (string, error) { - tempDirPath := filepath.Join(coreutils.GetCliPersistentTempDirPath(), "jfrog_tests_logs") - return tempDirPath, fileutils.CreateDirIfNotExist(tempDirPath) -} - -type PackageSearchResultItem struct { - Name string - Path string - Package string - Version string - Repo string - Owner string - Created string - Size int64 - Sha1 string - Published bool -} - -func DeleteFiles(deleteSpec *spec.SpecFiles, serverDetails *config.ServerDetails) (successCount, failCount int, err error) { - deleteCommand := generic.NewDeleteCommand() - deleteCommand.SetThreads(3).SetSpec(deleteSpec).SetServerDetails(serverDetails).SetDryRun(false) - reader, err := deleteCommand.GetPathsToDelete() - if err != nil { - return 0, 0, err - } - defer ioutils.Close(reader, &err) - return deleteCommand.DeleteFiles(reader) -} - -// This function makes no assertion, caller is responsible to assert as needed. -func GetBuildInfo(serverDetails *config.ServerDetails, buildName, buildNumber string) (pbi *buildinfo.PublishedBuildInfo, found bool, err error) { - servicesManager, err := artUtils.CreateServiceManager(serverDetails, -1, 0, false) - if err != nil { - return nil, false, err - } - params := services.NewBuildInfoParams() - params.BuildName = buildName - params.BuildNumber = buildNumber - return servicesManager.GetBuildInfo(params) -} - -func GetBuildRuns(serverDetails *config.ServerDetails, buildName string) (pbi *buildinfo.BuildRuns, found bool, err error) { - servicesManager, err := artUtils.CreateServiceManager(serverDetails, -1, 0, false) - if err != nil { - return nil, false, err - } - params := services.NewBuildInfoParams() - params.BuildName = buildName - return servicesManager.GetBuildRuns(params) -} - -var reposConfigMap = map[*string]string{ - &DistRepo1: DistributionRepoConfig1, - &DistRepo2: DistributionRepoConfig2, - &GoRepo: GoLocalRepositoryConfig, - &GoRemoteRepo: GoRemoteRepositoryConfig, - &GoVirtualRepo: GoVirtualRepositoryConfig, - &GradleRepo: GradleRepositoryConfig, - &MvnRepo1: MavenRepositoryConfig1, - &MvnRepo2: MavenRepositoryConfig2, - &MvnRemoteRepo: MavenRemoteRepositoryConfig, - &GradleRemoteRepo: GradleRemoteRepositoryConfig, - &NpmRepo: NpmLocalRepositoryConfig, - &NpmScopedRepo: NpmLocalScopedRespositoryConfig, - &NpmRemoteRepo: NpmRemoteRepositoryConfig, - &NugetRemoteRepo: NugetRemoteRepositoryConfig, - &YarnRemoteRepo: YarnRemoteRepositoryConfig, - &PypiLocalRepo: PypiLocalRepositoryConfig, - &PypiRemoteRepo: PypiRemoteRepositoryConfig, - &PypiVirtualRepo: PypiVirtualRepositoryConfig, - &PipenvRemoteRepo: PipenvRemoteRepositoryConfig, - &PipenvVirtualRepo: PipenvVirtualRepositoryConfig, - &PoetryLocalRepo: PoetryLocalRepositoryConfig, - &PoetryRemoteRepo: PoetryRemoteRepositoryConfig, - &PoetryVirtualRepo: PoetryVirtualRepositoryConfig, - &UvLocalRepo: UvLocalRepositoryConfig, - &UvRemoteRepo: UvRemoteRepositoryConfig, - &UvVirtualRepo: UvVirtualRepositoryConfig, - &NixLocalRepo: NixLocalRepositoryConfig, - &NixRemoteRepo: NixRemoteRepositoryConfig, - &NixVirtualRepo: NixVirtualRepositoryConfig, - &ConanLocalRepo: ConanLocalRepositoryConfig, - &ConanRemoteRepo: ConanRemoteRepositoryConfig, - &ConanVirtualRepo: ConanVirtualRepositoryConfig, - &HelmLocalRepo: HelmLocalRepositoryConfig, - &HuggingFaceLocalRepo: HuggingFaceLocalRepositoryConfig, - &RtDebianRepo: DebianTestRepositoryConfig, - &RtLfsRepo: GitLfsTestRepositoryConfig, - &RtRepo1: Repo1RepositoryConfig, - &RtRepo2: Repo2RepositoryConfig, - &RtVirtualRepo: VirtualRepositoryConfig, - &TerraformRepo: TerraformLocalRepositoryConfig, - &DockerLocalRepo: DockerLocalRepositoryConfig, - &DockerLocalPromoteRepo: DockerLocalPromoteRepositoryConfig, - &DockerRemoteRepo: DockerRemoteRepositoryConfig, - &DockerVirtualRepo: DockerVirtualRepositoryConfig, - &OciLocalRepo: OciLocalRepositoryConfig, - &OciRemoteRepo: OciRemoteRepositoryConfig, - &RtDevRepo: DevRepoRepositoryConfig, - &RtProdRepo1: ProdRepo1RepositoryConfig, - &RtProdRepo2: ProdRepo2RepositoryConfig, - &ReleaseLifecycleDependencyRepo: ReleaseLifecycleImportDependencySpec, -} - -var ( - CreatedNonVirtualRepositories map[*string]string - CreatedVirtualRepositories map[*string]string -) - -func getNeededRepositories(reposMap map[*bool][]*string) map[*string]string { - reposToCreate := map[*string]string{} - for needed, testRepos := range reposMap { - if *needed { - for _, repo := range testRepos { - reposToCreate[repo] = reposConfigMap[repo] - } - } - } - return reposToCreate -} - -func getNeededBuildNames(buildNamesMap map[*bool][]*string) []string { - var neededBuildNames []string - for needed, buildNames := range buildNamesMap { - if *needed { - for _, buildName := range buildNames { - neededBuildNames = append(neededBuildNames, *buildName) - } - } - } - return neededBuildNames -} - -// Return local and remote repositories for the test suites, respectfully -func GetNonVirtualRepositories() map[*string]string { - nonVirtualReposMap := map[*bool][]*string{ - TestArtifactory: {&RtRepo1, &RtRepo2, &RtLfsRepo, &RtDebianRepo, &TerraformRepo, &ReleaseLifecycleDependencyRepo}, - TestArtifactoryProject: {&RtRepo1, &RtRepo2, &RtLfsRepo, &RtDebianRepo, &HelmLocalRepo}, - TestDistribution: {&DistRepo1, &DistRepo2}, - TestDocker: {&DockerLocalRepo, &DockerLocalPromoteRepo, &DockerRemoteRepo, &OciLocalRepo, &OciRemoteRepo}, - TestDockerScan: {&DockerLocalRepo, &DockerLocalPromoteRepo, &DockerRemoteRepo}, - TestPodman: {&DockerLocalRepo, &DockerLocalPromoteRepo, &DockerRemoteRepo}, - TestGo: {&GoRepo, &GoRemoteRepo}, - TestGradle: {&GradleRepo, &GradleRemoteRepo}, - TestMaven: {&MvnRepo1, &MvnRepo2, &MvnRemoteRepo}, - TestNpm: {&NpmRepo, &NpmScopedRepo, &NpmRemoteRepo}, - TestPnpm: {&NpmRepo, &NpmScopedRepo, &NpmRemoteRepo}, - TestNuget: {&NugetRemoteRepo}, - TestPip: {&PypiLocalRepo, &PypiRemoteRepo}, - TestPipenv: {&PipenvRemoteRepo}, - TestPoetry: {&PoetryLocalRepo, &PoetryRemoteRepo}, - TestUv: {&UvLocalRepo, &UvRemoteRepo}, - TestNix: {&NixLocalRepo, &NixRemoteRepo}, - TestConan: {&ConanLocalRepo, &ConanRemoteRepo}, - TestHelm: {&HelmLocalRepo}, - TestHuggingFace: {&HuggingFaceLocalRepo}, - TestPlugins: {&RtRepo1}, - TestXray: {&NpmRemoteRepo, &NugetRemoteRepo, &YarnRemoteRepo, &GradleRemoteRepo, &MvnRemoteRepo, &GoRepo, &GoRemoteRepo, &PypiRemoteRepo}, - TestAccess: {&RtRepo1}, - TestTransfer: {&RtRepo1, &RtRepo2, &MvnRepo1, &MvnRemoteRepo, &DockerRemoteRepo}, - TestLifecycle: {&RtDevRepo, &RtProdRepo1, &RtProdRepo2}, - TestHelm: {&RtRepo1}, - } - return getNeededRepositories(nonVirtualReposMap) -} - -// Return virtual repositories for the test suites, respectfully -func GetVirtualRepositories() map[*string]string { - virtualReposMap := map[*bool][]*string{ - TestArtifactory: {&RtVirtualRepo}, - TestDistribution: {}, - TestDocker: {&DockerVirtualRepo}, - TestDockerScan: {&DockerVirtualRepo}, - TestPodman: {&DockerVirtualRepo}, - TestGo: {&GoVirtualRepo}, - TestGradle: {}, - TestMaven: {}, - TestNpm: {}, - TestPnpm: {}, - TestNuget: {}, - TestPip: {&PypiVirtualRepo}, - TestPipenv: {&PipenvVirtualRepo}, - TestPoetry: {&PoetryVirtualRepo}, - TestUv: {&UvVirtualRepo}, - TestNix: {&NixVirtualRepo}, - TestConan: {&ConanVirtualRepo}, - TestHelm: {}, - TestHuggingFace: {}, - TestPlugins: {}, - TestXray: {&GoVirtualRepo}, - TestAccess: {}, - TestHelm: {}, - } - return getNeededRepositories(virtualReposMap) -} - -func GetAllRepositoriesNames() []string { - var baseRepoNames []string - for repoName := range GetNonVirtualRepositories() { - baseRepoNames = append(baseRepoNames, *repoName) - } - for repoName := range GetVirtualRepositories() { - baseRepoNames = append(baseRepoNames, *repoName) - } - return baseRepoNames -} - -func GetTestUsersNames() []string { - return []string{UserName1, UserName2} -} - -func GetBuildNames() []string { - buildNamesMap := map[*bool][]*string{ - TestArtifactory: {&RtBuildName1, &RtBuildName2, &RtBuildNameWithSpecialChars}, - TestDistribution: {}, - TestDocker: {&DockerBuildName}, - TestPodman: {&DockerBuildName}, - TestGo: {&GoBuildName}, - TestGradle: {&GradleBuildName}, - TestMaven: {&MvnBuildName}, - TestNpm: {&NpmBuildName, &YarnBuildName}, - TestPnpm: {&PnpmBuildName}, - TestNuget: {&NuGetBuildName}, - TestPip: {&PipBuildName}, - TestPipenv: {&PipenvBuildName}, - TestPoetry: {&PoetryBuildName}, - TestUv: {&UvBuildName}, - TestNix: {&NixBuildName}, - TestConan: {&ConanBuildName}, - TestHelm: {&HelmBuildName}, - TestHuggingFace: {&HuggingFaceBuildName}, - TestPlugins: {}, - TestXray: {}, - TestAccess: {}, - TestTransfer: {&MvnBuildName}, - TestLifecycle: {&LcBuildName1, &LcBuildName2, &LcBuildName3}, - TestHelm: {}, - } - return getNeededBuildNames(buildNamesMap) -} - -// Builds and repositories names to replace in the test files. -// We use substitution map to set repositories and builds with timestamp. -func getSubstitutionMap() map[string]string { - return map[string]string{ - "${REPO1}": RtRepo1, - "${REPO2}": RtRepo2, - "${REPO_1_AND_2}": RtRepo1And2, - "${VIRTUAL_REPO}": RtVirtualRepo, - "${LFS_REPO}": RtLfsRepo, - "${DEBIAN_REPO}": RtDebianRepo, - "${DOCKER_REPO}": DockerLocalRepo, - "${DOCKER_PROMOTE_REPO}": DockerLocalPromoteRepo, - "${DOCKER_REMOTE_REPO}": DockerRemoteRepo, - "${DOCKER_VIRTUAL_REPO}": DockerVirtualRepo, - "${OCI_LOCAL_REPO}": OciLocalRepo, - "${OCI_REMOTE_REPO}": OciRemoteRepo, - "${DOCKER_IMAGE_NAME}": DockerImageName, - "${CONTAINER_REGISTRY_DOMAIN}": RtContainerHostName, - "${MAVEN_REPO1}": MvnRepo1, - "${MAVEN_REPO2}": MvnRepo2, - "${MAVEN_REMOTE_REPO}": MvnRemoteRepo, - "${GRADLE_REMOTE_REPO}": GradleRemoteRepo, - "${GRADLE_REPO}": GradleRepo, - "${NPM_REPO}": NpmRepo, - "${NPM_SCOPED_REPO}": NpmScopedRepo, - "${NPM_REMOTE_REPO}": NpmRemoteRepo, - "${PNPM_BUILD_NAME}": PnpmBuildName, - "${NUGET_REMOTE_REPO}": NugetRemoteRepo, - "${YARN_REMOTE_REPO}": YarnRemoteRepo, - "${GO_REPO}": GoRepo, - "${GO_REMOTE_REPO}": GoRemoteRepo, - "${GO_VIRTUAL_REPO}": GoVirtualRepo, - "${TERRAFORM_REPO}": TerraformRepo, - "${SERVER_ID}": ServerId, - "${URL}": *JfrogUrl, - "${USERNAME}": *JfrogUser, - "${PASSWORD}": *JfrogPassword, - "${RT_CREDENTIALS_BASIC_AUTH}": base64.StdEncoding.EncodeToString([]byte(*JfrogUser + ":" + *JfrogPassword)), - "${ACCESS_TOKEN}": *JfrogAccessToken, - "${PYPI_LOCAL_REPO}": PypiLocalRepo, - "${PYPI_REMOTE_REPO}": PypiRemoteRepo, - "${PYPI_VIRTUAL_REPO}": PypiVirtualRepo, - "${PIPENV_REMOTE_REPO}": PipenvRemoteRepo, - "${PIPENV_VIRTUAL_REPO}": PipenvVirtualRepo, - "${POETRY_LOCAL_REPO}": PoetryLocalRepo, - "${POETRY_REMOTE_REPO}": PoetryRemoteRepo, - "${POETRY_VIRTUAL_REPO}": PoetryVirtualRepo, - "${UV_LOCAL_REPO}": UvLocalRepo, - "${UV_REMOTE_REPO}": UvRemoteRepo, - "${UV_VIRTUAL_REPO}": UvVirtualRepo, - "${NIX_LOCAL_REPO}": NixLocalRepo, - "${NIX_REMOTE_REPO}": NixRemoteRepo, - "${NIX_VIRTUAL_REPO}": NixVirtualRepo, - "${CONAN_LOCAL_REPO}": ConanLocalRepo, - "${CONAN_REMOTE_REPO}": ConanRemoteRepo, - "${CONAN_VIRTUAL_REPO}": ConanVirtualRepo, - "${HELM_REPO}": HelmLocalRepo, - "${HUGGINGFACE_LOCAL_REPO}": HuggingFaceLocalRepo, - "${BUILD_NAME1}": RtBuildName1, - "${BUILD_NAME2}": RtBuildName2, - "${BUNDLE_NAME}": BundleName, - "${DIST_REPO1}": DistRepo1, - "${DIST_REPO2}": DistRepo2, - "{USER_NAME_1}": UserName1, - "{PASSWORD_1}": Password1, - "{USER_NAME_2}": UserName2, - "{PASSWORD_2}": Password2, - "${LC_BUILD_NAME1}": LcBuildName1, - "${LC_BUILD_NAME2}": LcBuildName2, - "${LC_BUILD_NAME3}": LcBuildName3, - "${RB_NAME1}": LcRbName1, - "${RB_NAME2}": LcRbName2, - "${DEV_REPO}": RtDevRepo, - "${PROD_REPO1}": RtProdRepo1, - "${PROD_REPO2}": RtProdRepo2, - } -} - -// Add timestamp to builds and repositories names -func AddTimestampToGlobalVars() { - // Make sure the global timestamp is added only once even in case of multiple tests flags - if timestampAdded { - return - } - timestamp := strconv.FormatInt(time.Now().Unix(), 10) - uniqueSuffix := "-" + timestamp - if *ciRunId != "" { - uniqueSuffix = "-" + *ciRunId + uniqueSuffix - } - // Artifactory accepts only lowercase repository names - uniqueSuffix = strings.ToLower(uniqueSuffix) - - // Repositories - DistRepo1 += uniqueSuffix - DistRepo2 += uniqueSuffix - GoRepo += uniqueSuffix - GoRemoteRepo += uniqueSuffix - GoVirtualRepo += uniqueSuffix - DockerLocalRepo += uniqueSuffix - DockerLocalPromoteRepo += uniqueSuffix - DockerRemoteRepo += uniqueSuffix - DockerVirtualRepo += uniqueSuffix - OciLocalRepo += uniqueSuffix - OciRemoteRepo += uniqueSuffix - TerraformRepo += uniqueSuffix - GradleRemoteRepo += uniqueSuffix - GradleRepo += uniqueSuffix - MvnRemoteRepo += uniqueSuffix - MvnRepo1 += uniqueSuffix - MvnRepo2 += uniqueSuffix - NpmRepo += uniqueSuffix - NpmScopedRepo += uniqueSuffix - NpmRemoteRepo += uniqueSuffix - NugetRemoteRepo += uniqueSuffix - YarnRemoteRepo += uniqueSuffix - PypiLocalRepo += uniqueSuffix - PypiRemoteRepo += uniqueSuffix - PypiVirtualRepo += uniqueSuffix - PipenvRemoteRepo += uniqueSuffix - PipenvVirtualRepo += uniqueSuffix - PoetryLocalRepo += uniqueSuffix - PoetryRemoteRepo += uniqueSuffix - PoetryVirtualRepo += uniqueSuffix - UvLocalRepo += uniqueSuffix - UvRemoteRepo += uniqueSuffix - UvVirtualRepo += uniqueSuffix - ConanLocalRepo += uniqueSuffix - ConanRemoteRepo += uniqueSuffix - ConanVirtualRepo += uniqueSuffix - HelmLocalRepo += uniqueSuffix - HuggingFaceLocalRepo += uniqueSuffix - RtDebianRepo += uniqueSuffix - RtLfsRepo += uniqueSuffix - RtRepo1 += uniqueSuffix - RtRepo1And2 += uniqueSuffix - RtRepo1And2Placeholder += uniqueSuffix - RtRepo2 += uniqueSuffix - RtVirtualRepo += uniqueSuffix - RtDevRepo += uniqueSuffix - RtProdRepo1 += uniqueSuffix - RtProdRepo2 += uniqueSuffix - - // Builds/bundles/images - BundleName += uniqueSuffix - DockerBuildName += uniqueSuffix - DockerImageName += uniqueSuffix - DotnetBuildName += uniqueSuffix - GoBuildName += uniqueSuffix - GradleBuildName += uniqueSuffix - NpmBuildName += uniqueSuffix - PnpmBuildName += uniqueSuffix - YarnBuildName += uniqueSuffix - MvnBuildName += uniqueSuffix - NuGetBuildName += uniqueSuffix - PipBuildName += uniqueSuffix - PipenvBuildName += uniqueSuffix - PoetryBuildName += uniqueSuffix - ConanBuildName += uniqueSuffix - HelmBuildName += uniqueSuffix - HuggingFaceBuildName += uniqueSuffix - RtBuildName1 += uniqueSuffix - RtBuildName2 += uniqueSuffix - RtBuildNameWithSpecialChars += uniqueSuffix - RtPermissionTargetName += uniqueSuffix - LcBuildName1 += uniqueSuffix - LcBuildName2 += uniqueSuffix - LcBuildName3 += uniqueSuffix - LcRbName1 += uniqueSuffix - LcRbName2 += uniqueSuffix - LcRbName3 += uniqueSuffix - - // Users - UserName1 += uniqueSuffix - UserName2 += uniqueSuffix - - randomSequence := rand.New(rand.NewSource(time.Now().Unix())) - Password1 += uniqueSuffix + strconv.FormatFloat(randomSequence.Float64(), 'f', 2, 32) - Password2 += uniqueSuffix + strconv.FormatFloat(randomSequence.Float64(), 'f', 2, 32) - - // Projects - ProjectKey += timestamp[len(timestamp)-7:] - - timestampAdded = true -} - -// Replace all variables in the form of ${VARIABLE} in the input file, according to the substitution map (see getSubstitutionMap()). -// path - Path to the input file. -// destPath - Path to the output file. If empty, the output file will be under ${CWD}/tmp/. -func ReplaceTemplateVariables(path, destPath string) (string, error) { - return commonCliUtils.ReplaceTemplateVariables(path, destPath, getSubstitutionMap()) -} - -func CreateSpec(fileName string) (string, error) { - searchFilePath := GetFilePathForArtifactory(fileName) - searchFilePath, err := ReplaceTemplateVariables(searchFilePath, "") - return searchFilePath, err -} - -func ConvertSliceToMap(props []utils.Property) map[string][]string { - propsMap := make(map[string][]string) - for _, item := range props { - propsMap[item.Key] = append(propsMap[item.Key], item.Value) - } - return propsMap -} - -// Set user and password from access token. -// Return the original user and password to allow restoring them in the end of the test. -func SetBasicAuthFromAccessToken() (string, string) { - origUser := *JfrogUser - origPassword := *JfrogPassword - *JfrogUser = auth.ExtractUsernameFromAccessToken(*JfrogAccessToken) - *JfrogPassword = *JfrogAccessToken - return origUser, origPassword -} - -// Clean items with timestamp older than 24 hours. Used to delete old repositories, builds, release bundles and Docker images. -// baseItemNames - The items to delete without timestamp, i.e. [cli-rt1, cli-rt2, ...] -// getActualItems - Function that returns all actual items in the remote server, i.e. [cli-rt1-1592990748, cli-rt2-1592990748, ...] -// deleteItem - Function that deletes the item by name -func CleanUpOldItems(baseItemNames []string, getActualItems func() ([]string, error), deleteItem func(string)) { - actualItems, err := getActualItems() - if err != nil { - log.Warn("Couldn't retrieve items", err) - return - } - now := time.Now() - for _, baseItemName := range baseItemNames { - itemPattern := regexp.MustCompile(`^` + baseItemName + `[\w-]*-(\d*)$`) - for _, item := range actualItems { - regexGroups := itemPattern.FindStringSubmatch(item) - if regexGroups == nil { - // Item does not match - continue - } - - itemTimestamp, err := strconv.ParseInt(regexGroups[len(regexGroups)-1], 10, 64) - if err != nil { - log.Warn("Error while parsing timestamp of", item, err) - continue - } - - itemTime := time.Unix(itemTimestamp, 0) - if now.Sub(itemTime).Hours() > 24 { - deleteItem(item) - } - } - } -} - -// Set new logger with output redirection to a null logger. This is useful for negative tests. -// Caller is responsible to set the old log back. -func RedirectLogOutputToNil() (previousLog log.Log) { - previousLog = log.Logger - newLog := log.NewLogger(corelog.GetCliLogLevel(), nil) - newLog.SetOutputWriter(io.Discard) - newLog.SetLogsWriter(io.Discard, 0) - log.SetLogger(newLog) - return previousLog -} - -// Redirect output to a file, execute the command and read output. -// The reason for redirecting to a file and not to a buffer is the limited -// size of the buffer while using os.Pipe. -func GetCmdOutput(t *testing.T, jfrogCli *coreTests.JfrogCli, cmd ...string) ([]byte, []byte, error) { - oldStdout := os.Stdout - oldStdErr := os.Stderr - temp, err := os.CreateTemp("", "output") - assert.NoError(t, err) - tempErr, err := os.CreateTemp("", "output") - assert.NoError(t, err) - os.Stdout = temp - os.Stderr = tempErr - defer func() { - os.Stdout = oldStdout - os.Stderr = oldStdErr - assert.NoError(t, temp.Close()) - assert.NoError(t, os.Remove(temp.Name())) - }() - err = jfrogCli.Exec(cmd...) - assert.NoError(t, err) - content, err := os.ReadFile(temp.Name()) - assert.NoError(t, err) - errContent, err := os.ReadFile(tempErr.Name()) - return content, errContent, err -} - -func VerifySha256DetailedSummaryFromBuffer(t *testing.T, buffer *bytes.Buffer, logger log.Log) { - content := buffer.Bytes() - buffer.Reset() - logger.Output(string(content)) - - var result summary.BuildInfoSummary - err := json.Unmarshal(content, &result) - assert.NoError(t, err) - - assert.Equal(t, summary.Success, result.Status) - assert.True(t, result.Totals.Success > 0) - assert.Equal(t, 0, result.Totals.Failure) - // Verify a sha256 was returned - assert.NotEmpty(t, result.Sha256Array, "Summary validation failed - no sha256 has returned from Artifactory.") - for _, sha256 := range result.Sha256Array { - // Verify sha256 is valid (a string size 256 characters) and not an empty string. - assert.Equal(t, 64, len(sha256.Sha256Str), "Summary validation failed - invalid sha256 has returned from artifactory") - } -} - -func VerifySha256DetailedSummaryFromResult(t *testing.T, result *commandutils.Result) { - if assert.NotNil(t, result) { - reader := result.Reader() - defer func() { - assert.NoError(t, reader.Close()) - }() - assert.NoError(t, reader.GetError()) - for transferDetails := new(clientutils.FileTransferDetails); reader.NextRecord(transferDetails) == nil; transferDetails = new(clientutils.FileTransferDetails) { - assert.Equal(t, 64, len(transferDetails.Sha256), "Summary validation failed - invalid sha256 has returned from artifactory") - } - } -} - -func SkipKnownFailingTest(t *testing.T) { - skipDate := time.Date(2023, time.November, 1, 0, 0, 0, 0, time.UTC) - if time.Now().Before(skipDate) { - t.Skip("Skipping a known failing test, will resume testing after ", skipDate.String()) - } else { - t.Error("Not skipping test. Please fix the test or delay the skipMonth") - } -} - -func CreateContext(t *testing.T, testFlags, testArgs []string) (*cli.Context, *bytes.Buffer) { - flagSet := createFlagSet(t, testFlags, testArgs) - app := cli.NewApp() - app.Writer = &bytes.Buffer{} - return cli.NewContext(app, flagSet, nil), &bytes.Buffer{} -} - -// Create flag set with input flags and arguments. -func createFlagSet(t *testing.T, flags []string, args []string) *flag.FlagSet { - flagSet := flag.NewFlagSet("TestFlagSet", flag.ContinueOnError) - flags = append(flags, "url=http://127.0.0.1:8081/artifactory") - var cmdFlags []string - for _, curFlag := range flags { - flagSet.String(strings.Split(curFlag, "=")[0], "", "") - cmdFlags = append(cmdFlags, "--"+curFlag) - } - cmdFlags = append(cmdFlags, args...) - assert.NoError(t, flagSet.Parse(cmdFlags)) - return flagSet -} - -func SkipTest(reason string) { - log.Info(reason) - os.Exit(0) -} - -// SetupGitHubActionsEnv enables CI VCS property collection for a test. -// When running on GitHub Actions, it uses the real environment variables. -// When running locally, it sets mock CI environment variables. -// Returns a cleanup function and the actual org/repo values to use for validation. -func SetupGitHubActionsEnv(t *testing.T) (cleanup func(), actualOrg, actualRepo string) { - callbacks := []func(){} - - // Enable CI VCS property collection for this test (unset the disable flag) - callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, "JFROG_CLI_CI_VCS_PROPS_DISABLED", "")) - - // Check if we're running on GitHub Actions - if os.Getenv("GITHUB_ACTIONS") == "true" { - // Running on CI - use real environment variables - ghRepo := os.Getenv("GITHUB_REPOSITORY") - if ghRepo != "" { - parts := strings.Split(ghRepo, "/") - if len(parts) == 2 { - actualOrg = parts[0] - actualRepo = parts[1] - } - } - if actualOrg == "" { - actualOrg = os.Getenv("GITHUB_REPOSITORY_OWNER") - } - } else { - // Running locally - set mock CI environment variables - actualOrg = "test-org" - actualRepo = "test-repo" - - // Set the required CI environment variables for cienv.GetCIVcsInfo() to detect CI - callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, "CI", "true")) - callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, "GITHUB_ACTIONS", "true")) - callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, "GITHUB_WORKFLOW", "test")) - callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, "GITHUB_RUN_ID", "12345")) - callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, "GITHUB_REPOSITORY_OWNER", actualOrg)) - callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, "GITHUB_REPOSITORY", actualOrg+"/"+actualRepo)) - } - - cleanup = func() { - for _, cb := range callbacks { - cb() - } - } - return cleanup, actualOrg, actualRepo -} - -// SetupGitHubActionsEnvForLocalGitMerge enables CI VCS collection with provider/org/repo -// but clears url/revision/branch CI env vars so local git fallback is exercised. -func SetupGitHubActionsEnvForLocalGitMerge(t *testing.T) (cleanup func(), actualOrg, actualRepo string) { - t.Helper() - cleanupBase, actualOrg, actualRepo := SetupGitHubActionsEnv(t) - - var callbacks []func() - for _, key := range []string{ - "GITHUB_SERVER_URL", - "GITHUB_SHA", - "GITHUB_REF", - "GITHUB_REF_NAME", - "GITHUB_HEAD_REF", - } { - callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, key, "")) - } - - return func() { - for _, cb := range callbacks { - cb() - } - cleanupBase() - }, actualOrg, actualRepo -} - -// ValidateCIVcsPropsOnArtifacts validates that CI VCS properties are set on artifacts. -func ValidateCIVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem, expectedProvider, expectedOrg, expectedRepo string) { - for _, item := range resultItems { - propertiesMap := ConvertPropertiesToMap(item.Properties) - - // Validate vcs.provider - if expectedProvider != "" { - vals, ok := propertiesMap["vcs.provider"] - assert.True(t, ok, "Missing vcs.provider on %s", item.Name) - assert.Contains(t, vals, expectedProvider, "Wrong vcs.provider on %s", item.Name) - } - - // Validate vcs.org - if expectedOrg != "" { - vals, ok := propertiesMap["vcs.org"] - assert.True(t, ok, "Missing vcs.org on %s", item.Name) - assert.Contains(t, vals, expectedOrg, "Wrong vcs.org on %s", item.Name) - } - - // Validate vcs.repo - if expectedRepo != "" { - vals, ok := propertiesMap["vcs.repo"] - assert.True(t, ok, "Missing vcs.repo on %s", item.Name) - assert.Contains(t, vals, expectedRepo, "Wrong vcs.repo on %s", item.Name) - } - } -} - -// ConvertPropertiesToMap converts a slice of Property to a map for easier lookup. -func ConvertPropertiesToMap(properties []utils.Property) map[string][]string { - propsMap := make(map[string][]string) - for _, prop := range properties { - propsMap[prop.Key] = append(propsMap[prop.Key], prop.Value) - } - return propsMap -} - -// ValidateNoCIVcsPropsOnArtifacts validates that CI VCS properties are NOT set on artifacts. -func ValidateNoCIVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem) { - for _, item := range resultItems { - propertiesMap := ConvertPropertiesToMap(item.Properties) - _, hasProvider := propertiesMap["vcs.provider"] - _, hasOrg := propertiesMap["vcs.org"] - _, hasRepo := propertiesMap["vcs.repo"] - assert.False(t, hasProvider, "vcs.provider should not be set when not in CI on %s", item.Name) - assert.False(t, hasOrg, "vcs.org should not be set when not in CI on %s", item.Name) - assert.False(t, hasRepo, "vcs.repo should not be set when not in CI on %s", item.Name) - } -} - -// ValidateCIVcsPropsIfPresent validates CI VCS properties only if at least one artifact has them. -// This is useful for build tools where OriginalDeploymentRepo may not always be set. -// Logs a warning if no artifacts have CI VCS properties. -func ValidateCIVcsPropsIfPresent(t *testing.T, resultItems []utils.ResultItem, expectedProvider, expectedOrg, expectedRepo string) { - // Check if any artifact has CI VCS properties - hasProps := false - for _, item := range resultItems { - propertiesMap := ConvertPropertiesToMap(item.Properties) - if _, ok := propertiesMap["vcs.provider"]; ok { - hasProps = true - break - } - } - - if !hasProps { - t.Log("Warning: No artifacts have CI VCS properties set. " + - "This may indicate OriginalDeploymentRepo is not populated in build-info.") - return - } - - // Validate all artifacts that have properties - for _, item := range resultItems { - propertiesMap := ConvertPropertiesToMap(item.Properties) - - // Only validate if the artifact has any VCS property - if _, hasAny := propertiesMap["vcs.provider"]; !hasAny { - continue - } - - if expectedProvider != "" { - vals, ok := propertiesMap["vcs.provider"] - assert.True(t, ok, "Missing vcs.provider on %s", item.Name) - assert.Contains(t, vals, expectedProvider, "Wrong vcs.provider on %s", item.Name) - } - - if expectedOrg != "" { - vals, ok := propertiesMap["vcs.org"] - assert.True(t, ok, "Missing vcs.org on %s", item.Name) - assert.Contains(t, vals, expectedOrg, "Wrong vcs.org on %s", item.Name) - } - - if expectedRepo != "" { - vals, ok := propertiesMap["vcs.repo"] - assert.True(t, ok, "Missing vcs.repo on %s", item.Name) - assert.Contains(t, vals, expectedRepo, "Wrong vcs.repo on %s", item.Name) - } - } -} - -// SetupLocalGitVcsEnv enables VCS property collection and clears CI detection -// so only local git fallback is exercised. -func SetupLocalGitVcsEnv(t *testing.T) (cleanup func()) { - t.Helper() - var callbacks []func() - - for _, key := range []string{ - "JFROG_CLI_CI_VCS_PROPS_DISABLED", // set to "" to enable - "CI", "GITHUB_ACTIONS", "GITHUB_WORKFLOW", "GITHUB_RUN_ID", - "GITHUB_REPOSITORY", "GITHUB_REPOSITORY_OWNER", - "GITHUB_SERVER_URL", "GITHUB_SHA", "GITHUB_REF", "GITHUB_REF_NAME", "GITHUB_HEAD_REF", - } { - callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, key, "")) - } - - return func() { - for _, cb := range callbacks { - cb() - } - } -} - -// ValidateLocalGitVcsPropsOnArtifacts asserts vcs.url, vcs.revision, vcs.branch on every item. -func ValidateLocalGitVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem, expectedURL, expectedRevision, expectedBranch string) { - t.Helper() - for _, item := range resultItems { - propertiesMap := ConvertPropertiesToMap(item.Properties) - assertLocalGitProp(t, item.Name, propertiesMap, "vcs.url", expectedURL) - assertLocalGitProp(t, item.Name, propertiesMap, "vcs.revision", expectedRevision) - if expectedBranch != "" { - assertLocalGitProp(t, item.Name, propertiesMap, "vcs.branch", expectedBranch) - } - } -} - -func assertLocalGitProp(t *testing.T, itemName string, props map[string][]string, key, expected string) { - t.Helper() - vals, ok := props[key] - assert.True(t, ok, "Missing %s on %s", key, itemName) - assert.Contains(t, vals, expected, "Wrong %s on %s", key, itemName) -} - -// ValidateNoLocalGitVcsPropsOnArtifacts asserts url/revision/branch are absent. -func ValidateNoLocalGitVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem) { - t.Helper() - for _, item := range resultItems { - propertiesMap := ConvertPropertiesToMap(item.Properties) - _, hasURL := propertiesMap["vcs.url"] - _, hasRev := propertiesMap["vcs.revision"] - _, hasBranch := propertiesMap["vcs.branch"] - assert.False(t, hasURL, "vcs.url should not be set on %s", item.Name) - assert.False(t, hasRev, "vcs.revision should not be set on %s", item.Name) - assert.False(t, hasBranch, "vcs.branch should not be set on %s", item.Name) - } -} - -// ValidateCIAndLocalGitVcsPropsOnArtifacts asserts CI props plus local git props coexist. -func ValidateCIAndLocalGitVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem, - expectedProvider, expectedOrg, expectedRepo, expectedURL, expectedRevision, expectedBranch string) { - t.Helper() - for _, item := range resultItems { - propertiesMap := ConvertPropertiesToMap(item.Properties) - assertLocalGitProp(t, item.Name, propertiesMap, "vcs.provider", expectedProvider) - assertLocalGitProp(t, item.Name, propertiesMap, "vcs.org", expectedOrg) - assertLocalGitProp(t, item.Name, propertiesMap, "vcs.repo", expectedRepo) - assertLocalGitProp(t, item.Name, propertiesMap, "vcs.url", expectedURL) - assertLocalGitProp(t, item.Name, propertiesMap, "vcs.revision", expectedRevision) - if expectedBranch != "" { - assertLocalGitProp(t, item.Name, propertiesMap, "vcs.branch", expectedBranch) - } - } -} diff --git a/utils/tests/utils.go b/utils/tests/utils.go index 4bacf7ffc..e8653d1cf 100644 --- a/utils/tests/utils.go +++ b/utils/tests/utils.go @@ -114,8 +114,8 @@ func init() { TestPip = flag.Bool("test.pip", false, "Test Pip") TestPipenv = flag.Bool("test.pipenv", false, "Test Pipenv") TestPoetry = flag.Bool("test.poetry", false, "Test Poetry") - TestUv = flag.Bool("test.uv", false, "Test UV") - TestNix = flag.Bool("test.nix", false, "Test Nix") + TestUv = flag.Bool("test.uv", false, "Test UV") + TestNix = flag.Bool("test.nix", false, "Test Nix") TestConan = flag.Bool("test.conan", false, "Test Conan") TestHelm = flag.Bool("test.helm", false, "Test Helm") TestHuggingFace = flag.Bool("test.huggingface", false, "Test HuggingFace") @@ -862,6 +862,31 @@ func SetupGitHubActionsEnv(t *testing.T) (cleanup func(), actualOrg, actualRepo return cleanup, actualOrg, actualRepo } +// SetupGitHubActionsEnvForLocalGitMerge enables CI VCS collection with provider/org/repo +// but clears url/revision/branch CI env vars so local git fallback is exercised. +func SetupGitHubActionsEnvForLocalGitMerge(t *testing.T) (cleanup func(), actualOrg, actualRepo string) { + t.Helper() + cleanupBase, actualOrg, actualRepo := SetupGitHubActionsEnv(t) + + var callbacks []func() + for _, key := range []string{ + "GITHUB_SERVER_URL", + "GITHUB_SHA", + "GITHUB_REF", + "GITHUB_REF_NAME", + "GITHUB_HEAD_REF", + } { + callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, key, "")) + } + + return func() { + for _, cb := range callbacks { + cb() + } + cleanupBase() + }, actualOrg, actualRepo +} + // ValidateCIVcsPropsOnArtifacts validates that CI VCS properties are set on artifacts. func ValidateCIVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem, expectedProvider, expectedOrg, expectedRepo string) { for _, item := range resultItems { @@ -960,3 +985,76 @@ func ValidateCIVcsPropsIfPresent(t *testing.T, resultItems []utils.ResultItem, e } } } + +// SetupLocalGitVcsEnv enables VCS property collection and clears CI detection +// so only local git fallback is exercised. +func SetupLocalGitVcsEnv(t *testing.T) (cleanup func()) { + t.Helper() + var callbacks []func() + + for _, key := range []string{ + "JFROG_CLI_CI_VCS_PROPS_DISABLED", // set to "" to enable + "CI", "GITHUB_ACTIONS", "GITHUB_WORKFLOW", "GITHUB_RUN_ID", + "GITHUB_REPOSITORY", "GITHUB_REPOSITORY_OWNER", + "GITHUB_SERVER_URL", "GITHUB_SHA", "GITHUB_REF", "GITHUB_REF_NAME", "GITHUB_HEAD_REF", + } { + callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, key, "")) + } + + return func() { + for _, cb := range callbacks { + cb() + } + } +} + +// ValidateLocalGitVcsPropsOnArtifacts asserts vcs.url, vcs.revision, vcs.branch on every item. +func ValidateLocalGitVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem, expectedURL, expectedRevision, expectedBranch string) { + t.Helper() + for _, item := range resultItems { + propertiesMap := ConvertPropertiesToMap(item.Properties) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.url", expectedURL) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.revision", expectedRevision) + if expectedBranch != "" { + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.branch", expectedBranch) + } + } +} + +func assertLocalGitProp(t *testing.T, itemName string, props map[string][]string, key, expected string) { + t.Helper() + vals, ok := props[key] + assert.True(t, ok, "Missing %s on %s", key, itemName) + assert.Contains(t, vals, expected, "Wrong %s on %s", key, itemName) +} + +// ValidateNoLocalGitVcsPropsOnArtifacts asserts url/revision/branch are absent. +func ValidateNoLocalGitVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem) { + t.Helper() + for _, item := range resultItems { + propertiesMap := ConvertPropertiesToMap(item.Properties) + _, hasURL := propertiesMap["vcs.url"] + _, hasRev := propertiesMap["vcs.revision"] + _, hasBranch := propertiesMap["vcs.branch"] + assert.False(t, hasURL, "vcs.url should not be set on %s", item.Name) + assert.False(t, hasRev, "vcs.revision should not be set on %s", item.Name) + assert.False(t, hasBranch, "vcs.branch should not be set on %s", item.Name) + } +} + +// ValidateCIAndLocalGitVcsPropsOnArtifacts asserts CI props plus local git props coexist. +func ValidateCIAndLocalGitVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem, + expectedProvider, expectedOrg, expectedRepo, expectedURL, expectedRevision, expectedBranch string) { + t.Helper() + for _, item := range resultItems { + propertiesMap := ConvertPropertiesToMap(item.Properties) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.provider", expectedProvider) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.org", expectedOrg) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.repo", expectedRepo) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.url", expectedURL) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.revision", expectedRevision) + if expectedBranch != "" { + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.branch", expectedBranch) + } + } +} From fcab876876631f63c30efd5c8cc65d53430cddcd Mon Sep 17 00:00:00 2001 From: attiasas Date: Thu, 18 Jun 2026 15:33:57 +0300 Subject: [PATCH 3/9] format --- npm_test.go | 2 -- uv_test.go | 11 ++++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/npm_test.go b/npm_test.go index 70df1a14d..38c0ddf37 100644 --- a/npm_test.go +++ b/npm_test.go @@ -309,7 +309,6 @@ func appendRegistryAuthToNpmrc(t *testing.T, registryURL string) error { return err } - func readModuleId(t *testing.T, wd string, npmVersion *version.Version) string { packageInfo, err := buildutils.ReadPackageInfoFromPackageJsonIfExists(filepath.Dir(wd), npmVersion) assert.NoError(t, err) @@ -483,7 +482,6 @@ func initNpmProjectTest(t *testing.T) (npmProjectPath string) { return } - func initNpmWorkspacesProjectTest(t *testing.T) (npmProjectPath string) { npmProjectPath = filepath.Dir(createNpmProject(t, "npmworkspaces")) err := createConfigFileForTest([]string{npmProjectPath}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Npm, false) diff --git a/uv_test.go b/uv_test.go index 94132fe67..532c53e72 100644 --- a/uv_test.go +++ b/uv_test.go @@ -11,8 +11,8 @@ import ( buildinfo "github.com/jfrog/build-info-go/entities" biutils "github.com/jfrog/build-info-go/utils" - coreBuild "github.com/jfrog/jfrog-cli-core/v2/common/build" artUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" + coreBuild "github.com/jfrog/jfrog-cli-core/v2/common/build" coretests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" @@ -422,9 +422,9 @@ func TestUvBuildFlags(t *testing.T) { expectErr bool // jfrog-cli errors if only one of name/number is set }{ {"both-set", tests.UvBuildName, "1", true, false}, - {"name-only", tests.UvBuildName, "", false, true}, // missing number → CLI error - {"number-only", "", "1", false, true}, // missing name → CLI error - {"neither", "", "", false, false}, // no flags → runs fine, no BI + {"name-only", tests.UvBuildName, "", false, true}, // missing number → CLI error + {"number-only", "", "1", false, true}, // missing name → CLI error + {"neither", "", "", false, false}, // no flags → runs fine, no BI } projectPath := createUvProject(t, "uv-flags", "uvproject") @@ -736,7 +736,8 @@ func TestUvNoPyprojectToml(t *testing.T) { // the dependencies declared in pyproject.toml — no more, no less. // // The test project (uvproject) declares one direct dependency: -// certifi>=2024.0.0 +// +// certifi>=2024.0.0 // // After `jf uv sync` the build info module must contain: // - Exactly 1 dependency (certifi; project itself is excluded) From 8295316daf7c33c290ff14d6dba29f58cba4f399 Mon Sep 17 00:00:00 2001 From: attiasas Date: Thu, 18 Jun 2026 15:35:40 +0300 Subject: [PATCH 4/9] update to dependant dpe --- go.mod | 3 +++ go.sum | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 01570e815..08dd346be 100644 --- a/go.mod +++ b/go.mod @@ -242,6 +242,9 @@ require ( sigs.k8s.io/yaml v1.6.0 // indirect ) +// attiasas:expend_vsc_detection +replace github.com/jfrog/jfrog-cli-artifactory => github.com/attiasas/jfrog-cli-artifactory v0.0.0-20260618090014-e1d2d84ca368 + //replace github.com/gfleury/go-bitbucket-v1 => github.com/gfleury/go-bitbucket-v1 v0.0.0-20230825095122-9bc1711434ab //replace github.com/ktrysmt/go-bitbucket => github.com/ktrysmt/go-bitbucket v0.9.80 diff --git a/go.sum b/go.sum index 381622e55..92600cc74 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/attiasas/jfrog-cli-artifactory v0.0.0-20260618090014-e1d2d84ca368 h1:wyPEN7aNxxwN7VDxdj3x98XP3Wp8Hrcv5xzufTaw9/4= +github.com/attiasas/jfrog-cli-artifactory v0.0.0-20260618090014-e1d2d84ca368/go.mod h1:VqV0Bed11HoBlugAEGa3RumbwnDVslEf0gKocTzLs9s= github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= @@ -406,8 +408,6 @@ 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.2-0.20260617073349-d68ee3120aa8 h1:FG+SfgPgrIuBHSos4sw4KNZq2MKxebbCZ6KZZRfaYcs= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260617073349-d68ee3120aa8/go.mod h1:p8yLtbmCxxQucIbLZKnWu0F+EDtj6NLXbRQCEK/nb6o= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260618051529-1b76b6ad2606 h1:hlc8XoqySjbrvKKjxswyXQ/q5I0Px9FcZpVZUTd+T3M= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260618051529-1b76b6ad2606/go.mod h1:VqV0Bed11HoBlugAEGa3RumbwnDVslEf0gKocTzLs9s= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260615072209-8ccac4f0072e h1:E3B8OyEkCsdEdGsZifTphBDUPrd00yKoemL9+l25Qj8= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260615072209-8ccac4f0072e/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE= github.com/jfrog/jfrog-cli-evidence v0.9.5-0.20260601141509-8df6c9a4bc9b h1:V0FxnU3xh29y8yJHWymm6rPr1MrjG1DdPQlr3ckImwk= From aaa05b148320cd1ad380cd88a0f48fb9a14cbb2a Mon Sep 17 00:00:00 2001 From: attiasas Date: Thu, 18 Jun 2026 15:47:04 +0300 Subject: [PATCH 5/9] add twine tests --- pip_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pip_test.go b/pip_test.go index 4ee8ac9d6..3a39bd1ab 100644 --- a/pip_test.go +++ b/pip_test.go @@ -777,3 +777,50 @@ func TestTwineBuildPublishWithCIVcsProps(t *testing.T) { assert.Greater(t, artifactCount, 0, "No artifacts were validated for CI VCS properties") } + +func TestTwinePublishWithLocalGitVcsProps(t *testing.T) { + initPipTest(t) + + buildName := tests.PipBuildName + "-local-git" + buildNumber := "1" + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + oldHomeDir, newHomeDir := prepareHomeDir(t) + defer func() { + clientTestUtils.SetEnvAndAssert(t, coreutils.HomeDir, oldHomeDir) + clientTestUtils.RemoveAllAndAssert(t, newHomeDir) + }() + + cleanVirtualEnv, err := prepareVirtualEnv(t) + require.NoError(t, err) + defer cleanVirtualEnv() + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + projectPath := createPypiProject(t, "twine-local-git", "pyproject", "twine") + tests.CopyGitFixtureIntoProject(t, projectPath) + + wd, err := os.Getwd() + require.NoError(t, err) + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) + defer chdirCallback() + + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + require.NoError(t, jfrogCli.Exec("twine", "upload", "dist/*", + "--build-name="+buildName, "--build-number="+buildNumber)) + require.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + require.NoError(t, err) + require.True(t, found) + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + require.NoError(t, err) + + count := tests.ValidateLocalGitVcsPropsOnBuildInfoArtifacts(t, serviceManager, publishedBuildInfo, tests.PypiVirtualRepo, + tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + assert.Greater(t, count, 0) +} From 05f964543572588924baa228a9291108f6bf381b Mon Sep 17 00:00:00 2001 From: attiasas Date: Tue, 23 Jun 2026 11:07:06 +0300 Subject: [PATCH 6/9] fix tests --- conan_test.go | 9 ++++++++- maven_test.go | 12 ++++++------ pip_test.go | 14 ++++++++++++++ pnpm_test.go | 2 +- uv_test.go | 7 +++++++ 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/conan_test.go b/conan_test.go index 6d4e4d7eb..e4b62ee26 100644 --- a/conan_test.go +++ b/conan_test.go @@ -7,6 +7,7 @@ import ( "os/exec" "path/filepath" "strconv" + "strings" "testing" buildinfo "github.com/jfrog/build-info-go/entities" @@ -1179,6 +1180,12 @@ func TestConanUploadWithLocalGitVcsProps(t *testing.T) { projectPath := createConanProject(t, "conan-local-git") tests.CopyGitFixtureIntoProject(t, projectPath) + conanfile := filepath.Join(projectPath, "conanfile.py") + data, err := os.ReadFile(conanfile) + require.NoError(t, err) + patched := strings.ReplaceAll(string(data), `name = "cli-test-package"`, `name = "cli-test-package-local-git"`) + require.NoError(t, os.WriteFile(conanfile, []byte(patched), 0o644)) + wd, err := os.Getwd() require.NoError(t, err) chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) @@ -1190,7 +1197,7 @@ func TestConanUploadWithLocalGitVcsProps(t *testing.T) { jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") require.NoError(t, jfrogCli.Exec("conan", "create", ".", "--build=missing", "--build-name="+buildName, "--build-number="+buildNumber)) - require.NoError(t, jfrogCli.Exec("conan", "upload", "cli-test-package/*", + require.NoError(t, jfrogCli.Exec("conan", "upload", "cli-test-package-local-git/*", "-r", tests.ConanLocalRepo, "--confirm", "--build-name="+buildName, "--build-number="+buildNumber)) diff --git a/maven_test.go b/maven_test.go index 8417cfe47..20145528c 100644 --- a/maven_test.go +++ b/maven_test.go @@ -853,13 +853,13 @@ func TestMavenBuildPublishWithLocalGitVcsProps(t *testing.T) { inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) - pomDir := createSimpleMavenProject(t) - tests.CopyGitFixtureIntoProject(t, pomDir) - - oldHomeDir := changeWD(t, pomDir) - defer clientTestUtils.ChangeDirAndAssert(t, oldHomeDir) + createMavenProjectWithGit := func(t *testing.T) string { + pomDir := createSimpleMavenProject(t) + tests.CopyGitFixtureIntoProject(t, pomDir) + return pomDir + } - err := runMaven(t, func(t *testing.T) string { return pomDir }, tests.MavenConfig, + err := runMaven(t, createMavenProjectWithGit, tests.MavenConfig, "install", "--build-name="+buildName, "--build-number="+buildNumber) require.NoError(t, err) diff --git a/pip_test.go b/pip_test.go index 3a39bd1ab..746676087 100644 --- a/pip_test.go +++ b/pip_test.go @@ -803,6 +803,20 @@ func TestTwinePublishWithLocalGitVcsProps(t *testing.T) { projectPath := createPypiProject(t, "twine-local-git", "pyproject", "twine") tests.CopyGitFixtureIntoProject(t, projectPath) + pyproject := filepath.Join(projectPath, "pyproject.toml") + content, err := os.ReadFile(pyproject) + require.NoError(t, err) + patched := strings.ReplaceAll(string(content), `version = "1.0"`, `version = "1.0.1-local-git"`) + require.NoError(t, os.WriteFile(pyproject, []byte(patched), 0o644)) + + distDir := filepath.Join(projectPath, "dist") + require.NoError(t, os.RemoveAll(distDir)) + require.NoError(t, os.MkdirAll(distDir, 0o755)) + buildCmd := exec.Command("python", "-m", "build", "--outdir", distDir) + buildCmd.Dir = projectPath + buildOut, err := buildCmd.CombinedOutput() + require.NoError(t, err, "python build failed: %s", buildOut) + wd, err := os.Getwd() require.NoError(t, err) chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) diff --git a/pnpm_test.go b/pnpm_test.go index f87da7f69..c0379f036 100644 --- a/pnpm_test.go +++ b/pnpm_test.go @@ -910,7 +910,7 @@ func TestPnpmPublishWithLocalGitVcsProps(t *testing.T) { assert.NoError(t, err) defer clientTestUtils.ChangeDirAndAssert(t, wd) - pnpmProjectPath := createPnpmProject(t, "pnpm-local-git") + pnpmProjectPath := createPnpmProject(t, "pnpmproject") projectDir := filepath.Dir(pnpmProjectPath) tests.CopyGitFixtureIntoProject(t, projectDir) prepareArtifactoryForPnpmBuild(t, projectDir) diff --git a/uv_test.go b/uv_test.go index 532c53e72..b0f2acc7f 100644 --- a/uv_test.go +++ b/uv_test.go @@ -1507,6 +1507,13 @@ func TestUvPublishWithLocalGitVcsProps(t *testing.T) { projectPath := createUvProject(t, "uv-local-git", "uvproject") tests.CopyGitFixtureIntoProject(t, projectPath) + pyproject := filepath.Join(projectPath, "pyproject.toml") + data, err := os.ReadFile(pyproject) + require.NoError(t, err) + patched := strings.ReplaceAll(string(data), `version = "0.1.0"`, `version = "0.1.1-local-git"`) + require.NoError(t, os.WriteFile(pyproject, []byte(patched), 0o644)) + require.NoError(t, runUvCmd(t, projectPath, "lock")) + require.NoError(t, runUvCmd(t, projectPath, "build")) require.NoError(t, runUvCmd(t, projectPath, "publish", "--build-name="+buildName, "--build-number="+buildNumber)) From bc0c84be0c2649cc79bac5414b301326a6355d4b Mon Sep 17 00:00:00 2001 From: attiasas Date: Tue, 23 Jun 2026 12:52:12 +0300 Subject: [PATCH 7/9] fix e2e tests --- conan_test.go | 2 +- maven_test.go | 24 ++++++++++++++++-------- pip_test.go | 8 +++++++- utils/tests/artifact_props.go | 6 ++++++ uv_test.go | 23 +++++++++++++++-------- 5 files changed, 45 insertions(+), 18 deletions(-) diff --git a/conan_test.go b/conan_test.go index e4b62ee26..032e5e00e 100644 --- a/conan_test.go +++ b/conan_test.go @@ -1184,7 +1184,7 @@ func TestConanUploadWithLocalGitVcsProps(t *testing.T) { data, err := os.ReadFile(conanfile) require.NoError(t, err) patched := strings.ReplaceAll(string(data), `name = "cli-test-package"`, `name = "cli-test-package-local-git"`) - require.NoError(t, os.WriteFile(conanfile, []byte(patched), 0o644)) + require.NoError(t, os.WriteFile(conanfile, []byte(patched), 0o644)) //#nosec G703 -- test code, path built from createConanProject temp dir wd, err := os.Getwd() require.NoError(t, err) diff --git a/maven_test.go b/maven_test.go index 20145528c..cf98f710d 100644 --- a/maven_test.go +++ b/maven_test.go @@ -853,16 +853,24 @@ func TestMavenBuildPublishWithLocalGitVcsProps(t *testing.T) { inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) - createMavenProjectWithGit := func(t *testing.T) string { - pomDir := createSimpleMavenProject(t) - tests.CopyGitFixtureIntoProject(t, pomDir) - return pomDir - } + pomDir := createSimpleMavenProject(t) + tests.CopyGitFixtureIntoProject(t, pomDir) + require.FileExists(t, filepath.Join(pomDir, ".git", "HEAD")) - err := runMaven(t, createMavenProjectWithGit, tests.MavenConfig, - "install", "--build-name="+buildName, "--build-number="+buildNumber) - require.NoError(t, err) + configFilePath := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "buildspecs", tests.MavenConfig) + destPath := filepath.Join(pomDir, ".jfrog", "projects") + createConfigFile(destPath, configFilePath, t) + require.NoError(t, os.Rename(filepath.Join(destPath, tests.MavenConfig), filepath.Join(destPath, "maven.yaml"))) + + oldHomeDir := changeWD(t, pomDir) + defer clientTestUtils.ChangeDirAndAssert(t, oldHomeDir) + + repoLocalSystemProp := localRepoSystemProperty + localRepoDir + args := []string{"mvn", "clean", "install", "-B", repoLocalSystemProp, + "--build-name=" + buildName, "--build-number=" + buildNumber} + require.NoError(t, runJfrogCliWithoutAssertion(args...)) + // Must run build-publish from project dir so GetLocalGitVcsInfo finds the fixture .git runRt(t, "build-publish", buildName, buildNumber) publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) diff --git a/pip_test.go b/pip_test.go index 746676087..112216ea2 100644 --- a/pip_test.go +++ b/pip_test.go @@ -807,11 +807,17 @@ func TestTwinePublishWithLocalGitVcsProps(t *testing.T) { content, err := os.ReadFile(pyproject) require.NoError(t, err) patched := strings.ReplaceAll(string(content), `version = "1.0"`, `version = "1.0.1-local-git"`) - require.NoError(t, os.WriteFile(pyproject, []byte(patched), 0o644)) + require.NoError(t, os.WriteFile(pyproject, []byte(patched), 0o644)) //#nosec G703 -- test code, path built from createPypiProject temp dir distDir := filepath.Join(projectPath, "dist") require.NoError(t, os.RemoveAll(distDir)) require.NoError(t, os.MkdirAll(distDir, 0o755)) + + installBuild := exec.Command("python", "-m", "pip", "install", "build") + installBuild.Dir = projectPath + installOut, err := installBuild.CombinedOutput() + require.NoError(t, err, "pip install build failed: %s", installOut) + buildCmd := exec.Command("python", "-m", "build", "--outdir", distDir) buildCmd.Dir = projectPath buildOut, err := buildCmd.CombinedOutput() diff --git a/utils/tests/artifact_props.go b/utils/tests/artifact_props.go index 172e2aadf..cb205c343 100644 --- a/utils/tests/artifact_props.go +++ b/utils/tests/artifact_props.go @@ -1,6 +1,7 @@ package tests import ( + "path/filepath" "strings" "testing" @@ -36,6 +37,11 @@ func ArtifactItemPath(a buildinfo.Artifact, defaultRepo string) string { if strings.HasSuffix(fullPath, "/"+a.Name) || strings.HasSuffix(fullPath, a.Name) { return fullPath } + // npm/pypi: Path is the full tarball path (e.g. pkg/-/pkg-1.0.0.tgz) + pathPart := strings.TrimPrefix(a.Path, "/") + if filepath.Ext(pathPart) != "" { + return fullPath + } return fullPath + "/" + a.Name } diff --git a/uv_test.go b/uv_test.go index b0f2acc7f..d657d1f10 100644 --- a/uv_test.go +++ b/uv_test.go @@ -50,11 +50,17 @@ func cleanUvTest(_ *testing.T) { tests.CleanFileSystem() } +const uvLocalGitVersion = "0.1.1-local-git" + // createUvProject copies a test UV project to a temp dir, injects Artifactory // URLs into pyproject.toml, then generates a fresh uv.lock against the test // Artifactory instance. The lock file is not committed to avoid embedding // instance-specific URLs in source. func createUvProject(t *testing.T, outputFolder, projectName string) string { + return createUvProjectWithVersion(t, outputFolder, projectName, "") +} + +func createUvProjectWithVersion(t *testing.T, outputFolder, projectName, version string) string { projectSrc := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "uv", projectName) tmpDir, cleanup := coretests.CreateTempDirWithCallbackAndAssert(t) t.Cleanup(cleanup) @@ -72,6 +78,14 @@ func createUvProject(t *testing.T, outputFolder, projectName string) string { // Patch pyproject.toml with real Artifactory URLs for this test run patchUvPyprojectToml(t, projectPath) + if version != "" { + pyprojectPath := filepath.Join(projectPath, "pyproject.toml") + data, err := os.ReadFile(pyprojectPath) + require.NoError(t, err) + patched := strings.ReplaceAll(string(data), `version = "0.1.0"`, `version = "`+version+`"`) + require.NoError(t, os.WriteFile(filepath.Clean(pyprojectPath), []byte(patched), 0o644)) // #nosec G703 -- path built from filepath.Join, not user input + } + // Generate uv.lock against the patched index so UV resolves through // Artifactory (required for dependency checksum enrichment tests). // Convert the index name to the UV env var suffix format: @@ -1504,16 +1518,9 @@ func TestUvPublishWithLocalGitVcsProps(t *testing.T) { inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) - projectPath := createUvProject(t, "uv-local-git", "uvproject") + projectPath := createUvProjectWithVersion(t, "uv-local-git", "uvproject", uvLocalGitVersion) tests.CopyGitFixtureIntoProject(t, projectPath) - pyproject := filepath.Join(projectPath, "pyproject.toml") - data, err := os.ReadFile(pyproject) - require.NoError(t, err) - patched := strings.ReplaceAll(string(data), `version = "0.1.0"`, `version = "0.1.1-local-git"`) - require.NoError(t, os.WriteFile(pyproject, []byte(patched), 0o644)) - require.NoError(t, runUvCmd(t, projectPath, "lock")) - require.NoError(t, runUvCmd(t, projectPath, "build")) require.NoError(t, runUvCmd(t, projectPath, "publish", "--build-name="+buildName, "--build-number="+buildNumber)) From 4aeacf1d6e167ca6304ceb3c918edb71f0290ffe Mon Sep 17 00:00:00 2001 From: attiasas Date: Tue, 23 Jun 2026 15:14:00 +0300 Subject: [PATCH 8/9] try to fix tests --- pip_test.go | 2 +- utils/tests/artifact_props.go | 14 +++++++++++++- utils/tests/artifact_props_test.go | 9 +++++++++ uv_test.go | 2 +- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pip_test.go b/pip_test.go index 112216ea2..66113d173 100644 --- a/pip_test.go +++ b/pip_test.go @@ -806,7 +806,7 @@ func TestTwinePublishWithLocalGitVcsProps(t *testing.T) { pyproject := filepath.Join(projectPath, "pyproject.toml") content, err := os.ReadFile(pyproject) require.NoError(t, err) - patched := strings.ReplaceAll(string(content), `version = "1.0"`, `version = "1.0.1-local-git"`) + patched := strings.ReplaceAll(string(content), `version = "1.0"`, `version = "1.0.1+localgit"`) require.NoError(t, os.WriteFile(pyproject, []byte(patched), 0o644)) //#nosec G703 -- test code, path built from createPypiProject temp dir distDir := filepath.Join(projectPath, "dist") diff --git a/utils/tests/artifact_props.go b/utils/tests/artifact_props.go index cb205c343..e763da591 100644 --- a/utils/tests/artifact_props.go +++ b/utils/tests/artifact_props.go @@ -26,6 +26,18 @@ func ArtifactFullPath(a buildinfo.Artifact, defaultRepo string) string { return path } +// pathBasenameLooksLikeArchive reports whether the last segment of path is a +// deployable archive file (npm .tgz, Python .whl, Java .jar, etc.). +func pathBasenameLooksLikeArchive(path string) bool { + base := filepath.Base(strings.TrimSuffix(path, "/")) + for _, ext := range []string{".tgz", ".tar.gz", ".whl", ".jar", ".pom", ".zip", ".tar", ".tar.bz2"} { + if strings.HasSuffix(base, ext) { + return true + } + } + return false +} + // ArtifactItemPath returns the Artifactory item path for GetItemProps. // When Name is set and not already part of Path (e.g. UV stores Path as a directory), // Name is appended as the filename segment. @@ -39,7 +51,7 @@ func ArtifactItemPath(a buildinfo.Artifact, defaultRepo string) string { } // npm/pypi: Path is the full tarball path (e.g. pkg/-/pkg-1.0.0.tgz) pathPart := strings.TrimPrefix(a.Path, "/") - if filepath.Ext(pathPart) != "" { + if pathBasenameLooksLikeArchive(pathPart) { return fullPath } return fullPath + "/" + a.Name diff --git a/utils/tests/artifact_props_test.go b/utils/tests/artifact_props_test.go index aaf8bbe01..a88844006 100644 --- a/utils/tests/artifact_props_test.go +++ b/utils/tests/artifact_props_test.go @@ -55,3 +55,12 @@ func TestArtifactItemPath_DoesNotDoubleAppendName(t *testing.T) { } assert.Equal(t, "mvn-local/com/foo/1.0/foo.jar", ArtifactItemPath(a, "")) } + +func TestArtifactItemPath_NpmTarballPathDoesNotAppendName(t *testing.T) { + a := buildinfo.Artifact{ + OriginalDeploymentRepo: "cli-npm-123", + Path: "jfrog-cli-tests/-/jfrog-cli-tests-1.0.0.tgz", + Name: "jfrog-cli-tests-v1.0.0.tgz", + } + assert.Equal(t, "cli-npm-123/jfrog-cli-tests/-/jfrog-cli-tests-1.0.0.tgz", ArtifactItemPath(a, "")) +} diff --git a/uv_test.go b/uv_test.go index d657d1f10..97e85635c 100644 --- a/uv_test.go +++ b/uv_test.go @@ -50,7 +50,7 @@ func cleanUvTest(_ *testing.T) { tests.CleanFileSystem() } -const uvLocalGitVersion = "0.1.1-local-git" +const uvLocalGitVersion = "0.1.1+localgit" // createUvProject copies a test UV project to a temp dir, injects Artifactory // URLs into pyproject.toml, then generates a fresh uv.lock against the test From 51b21a19c372e2ed01733767895dee4fb3652afe Mon Sep 17 00:00:00 2001 From: attiasas Date: Tue, 23 Jun 2026 15:57:25 +0300 Subject: [PATCH 9/9] try to fix pip tests --- pip_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pip_test.go b/pip_test.go index 66113d173..1f1e2dacb 100644 --- a/pip_test.go +++ b/pip_test.go @@ -818,11 +818,16 @@ func TestTwinePublishWithLocalGitVcsProps(t *testing.T) { installOut, err := installBuild.CombinedOutput() require.NoError(t, err, "pip install build failed: %s", installOut) - buildCmd := exec.Command("python", "-m", "build", "--outdir", distDir) + // --outdir is relative to buildCmd.Dir (projectPath), not the process CWD. + buildCmd := exec.Command("python", "-m", "build", "--outdir", "dist") buildCmd.Dir = projectPath buildOut, err := buildCmd.CombinedOutput() require.NoError(t, err, "python build failed: %s", buildOut) + entries, err := os.ReadDir(distDir) + require.NoError(t, err) + require.NotEmpty(t, entries, "dist must contain built artifacts after python -m build") + wd, err := os.Getwd() require.NoError(t, err) chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath)