diff --git a/go.mod b/go.mod index a54125431..f10e08810 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.6 replace ( // Should not be updated to 0.2.6 due to a bug (https://github.com/jfrog/jfrog-cli-core/pull/372) github.com/c-bata/go-prompt => github.com/c-bata/go-prompt v0.2.5 + github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.7.3-0.20250930060931-d9c7efa90fbb // Should not be updated to 0.2.0-beta.2 due to a bug (https://github.com/jfrog/jfrog-cli-core/pull/372) github.com/pkg/term => github.com/pkg/term v1.1.0 diff --git a/go.sum b/go.sum index 6f060d5f5..41d8c9993 100644 --- a/go.sum +++ b/go.sum @@ -361,8 +361,8 @@ github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYLipdsOFMY= github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= -github.com/jfrog/jfrog-cli-artifactory v0.7.3-0.20250924123948-4fce9d68fabe h1:RTOKkKrAjAn/bV5phpCUgOJY+4LHLpjK1YdwpBSTwGs= -github.com/jfrog/jfrog-cli-artifactory v0.7.3-0.20250924123948-4fce9d68fabe/go.mod h1:N1nn6tNbyAjtdhLp89tO1CstT+iWcQSzAJGvAml7Deg= +github.com/jfrog/jfrog-cli-artifactory v0.7.3-0.20250930060931-d9c7efa90fbb h1:wrl9zkkzfc2lFgEY+LhPbLK5quRX6/N8utq1WVdiM7I= +github.com/jfrog/jfrog-cli-artifactory v0.7.3-0.20250930060931-d9c7efa90fbb/go.mod h1:N1nn6tNbyAjtdhLp89tO1CstT+iWcQSzAJGvAml7Deg= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20250925124759-72e2205de3f6 h1:BA6o4MYduG3IxZbSxqIQQ7maTjUszgK3YOXQrZdP/Rk= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20250925124759-72e2205de3f6/go.mod h1:Z1nBKbz0ZOZrOIS9maGp5LZpHaxuufZAABVF+DAz9wo= github.com/jfrog/jfrog-cli-platform-services v1.10.0 h1:O+N/VAF+QjFvq9xkHpmzKLcdl9aJu3IP204Su0L14rw= diff --git a/pip_test.go b/pip_test.go index 23ca2d55f..a59a6b000 100644 --- a/pip_test.go +++ b/pip_test.go @@ -8,12 +8,16 @@ import ( "os/exec" "path/filepath" "strconv" + "strings" "testing" + "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" + biutils "github.com/jfrog/build-info-go/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" coretests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/python" + "github.com/jfrog/jfrog-client-go/artifactory" "github.com/jfrog/jfrog-client-go/http/httpclient" clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" @@ -369,6 +373,228 @@ func testTwineCmd(t *testing.T, projectPath, buildNumber, expectedModuleId strin assert.Equal(t, expectedModuleId, twineModule.Id) } +func TestTwineWithBuildNameAndNumberAndTimeStampProperties(t *testing.T) { + if !*tests.TestPip { + t.Skip("Skipping test. Requires '-test.pip=true' options.") + } + initPipTest(t) + + oldHomeDir, newHomeDir := prepareHomeDir(t) + defer func() { + clientTestUtils.SetEnvAndAssert(t, coreutils.HomeDir, oldHomeDir) + clientTestUtils.RemoveAllAndAssert(t, newHomeDir) + }() + + allTests := []struct { + name string + project string + outputFolder string + expectedModuleId string + args []string + expectedArtifacts int + }{ + {"twineWithProps", "pyproject", "twine", "jfrog-python-example:1.0", []string{}, 2}, + } + + for testNumber, test := range allTests { + t.Run(test.name, func(t *testing.T) { + cleanVirtualEnv, err := prepareVirtualEnv(t) + assert.NoError(t, err) + + buildNumber := strconv.Itoa(100 + testNumber) + test.args = append([]string{"twine", "upload", "dist/*", "--build-name=" + tests.PipBuildName, "--build-number=" + buildNumber}, test.args...) + verifyBuildNameNumberTimestampPropertiesOnTwineArtifact(t, createPypiProject(t, test.outputFolder, test.project, "twine"), buildNumber, test.expectedModuleId, test.expectedArtifacts, test.args) + + cleanVirtualEnv() + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.PipBuildName, artHttpDetails) + }) + } +} + +func verifyBuildNameNumberTimestampPropertiesOnTwineArtifact(t *testing.T, projectPath, buildNumber, expectedModuleId string, expectedArtifacts int, args []string) { + wd, err := os.Getwd() + assert.NoError(t, err, "Failed to get current dir") + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) + defer chdirCallback() + + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + err = jfrogCli.Exec(args...) + if err != nil { + assert.Fail(t, "Failed executing twine upload command", err.Error()) + return + } + + assert.NoError(t, artifactoryCli.Exec("bp", tests.PipBuildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, tests.PipBuildName, buildNumber) + if err != nil { + assert.NoError(t, err) + return + } + if !found { + assert.True(t, found, "build info was expected to be found") + return + } + buildInfo := publishedBuildInfo.BuildInfo + require.Len(t, buildInfo.Modules, 1) + twineModule := buildInfo.Modules[0] + assert.Equal(t, buildinfo.Python, twineModule.Type) + assert.Len(t, twineModule.Artifacts, expectedArtifacts) + assert.Equal(t, expectedModuleId, twineModule.Id) + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + if err != nil { + return + } + + var allErrors []string + for _, artifact := range twineModule.Artifacts { + errors := verifyBuildProperties(serviceManager, artifact, buildinfo.Python, tests.PipBuildName, buildNumber) + allErrors = append(allErrors, errors...) + } + + if len(allErrors) > 0 { + assert.Fail(t, "Missing build properties for the artifacts:\n"+strings.Join(allErrors, "\n")) + } +} + +func TestTwineAndGenericUploadSameBuildInfo(t *testing.T) { + if !*tests.TestArtifactory || !*tests.TestPip { + t.Skip("Skipping test. Requires both '-test.artifactory=true' and '-test.pip=true' options.") + } + + oldHomeDir, newHomeDir := prepareHomeDir(t) + defer func() { + clientTestUtils.SetEnvAndAssert(t, coreutils.HomeDir, oldHomeDir) + clientTestUtils.RemoveAllAndAssert(t, newHomeDir) + }() + + cleanVirtualEnv, err := prepareVirtualEnv(t) + assert.NoError(t, err) + defer cleanVirtualEnv() + + projectPath := createPypiProject(t, "twine-generic-test", "pyproject", "twine") + wd, err := os.Getwd() + assert.NoError(t, err, "Failed to get current dir") + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) + defer chdirCallback() + + buildName := "test-twine-generic-build" + buildNumber := "1" + + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + twineArgs := []string{"twine", "upload", "dist/*", "--build-name=" + buildName, "--build-number=" + buildNumber} + err = jfrogCli.Exec(twineArgs...) + assert.NoError(t, err, "Failed executing twine upload command") + + chdirCallback() + + testFileName := "test-artifact.zip" + testFilePath := filepath.Join(t.TempDir(), testFileName) + err = os.WriteFile(testFilePath, []byte("test content for generic upload"), 0644) + assert.NoError(t, err) + + uploadArgs := []string{"upload", testFilePath, tests.RtRepo1 + "/", "--build-name=" + buildName, "--build-number=" + buildNumber} + err = artifactoryCli.Exec(uploadArgs...) + assert.NoError(t, err, "Failed executing generic upload command") + + err = artifactoryCli.Exec("bp", buildName, buildNumber) + assert.NoError(t, err, "Failed publishing build info") + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found, "build info was expected to be found") + + buildInfo := publishedBuildInfo.BuildInfo + + require.Len(t, buildInfo.Modules, 2, "Expected 2 modules in build info") + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + assert.NoError(t, err) + + var allErrors []string + for _, module := range buildInfo.Modules { + for _, artifact := range module.Artifacts { + errors := verifyBuildProperties(serviceManager, artifact, module.Type, buildName, buildNumber) + allErrors = append(allErrors, errors...) + } + } + + if len(allErrors) > 0 { + assert.Fail(t, "Missing build properties for the artifacts:\n"+strings.Join(allErrors, "\n")) + } + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) +} + +func verifyBuildProperties(serviceManager artifactory.ArtifactoryServicesManager, artifact buildinfo.Artifact, + moduleType buildinfo.ModuleType, expectedBuildName, expectedBuildNumber string) []string { + + var errors []string + + relativePath := artifact.OriginalDeploymentRepo + "/" + artifact.Path + props, err := serviceManager.GetItemProps(relativePath) + if err != nil { + return []string{fmt.Sprintf("Failed to get properties for %s artifact '%s': %v", + moduleType, artifact.Name, err)} + } + + if props == nil { + return []string{fmt.Sprintf("Properties are nil for %s artifact '%s'", + moduleType, artifact.Name)} + } + + errors = append(errors, validateBuildNameProperty(props.Properties, moduleType, artifact.Name, expectedBuildName)...) + errors = append(errors, validateBuildNumberProperty(props.Properties, moduleType, artifact.Name, expectedBuildNumber)...) + errors = append(errors, validateBuildTimestampProperty(props.Properties, moduleType, artifact.Name)...) + + return errors +} + +func validateBuildNameProperty(properties map[string][]string, moduleType buildinfo.ModuleType, artifactName, expectedBuildName string) []string { + buildNameProp, exists := properties["build.name"] + if !exists { + return []string{fmt.Sprintf("Missing build.name property for %s artifact '%s'", moduleType, artifactName)} + } + if !contains(buildNameProp, expectedBuildName) { + return []string{fmt.Sprintf("Incorrect build.name for %s artifact '%s': expected %s, got %v", + moduleType, artifactName, expectedBuildName, buildNameProp)} + } + return nil +} + +func validateBuildNumberProperty(properties map[string][]string, moduleType buildinfo.ModuleType, artifactName, expectedBuildNumber string) []string { + buildNumberProp, exists := properties["build.number"] + if !exists { + return []string{fmt.Sprintf("Missing build.number property for %s artifact '%s'", moduleType, artifactName)} + } + if !contains(buildNumberProp, expectedBuildNumber) { + return []string{fmt.Sprintf("Incorrect build.number for %s artifact '%s': expected %s, got %v", + moduleType, artifactName, expectedBuildNumber, buildNumberProp)} + } + return nil +} + +func validateBuildTimestampProperty(properties map[string][]string, moduleType buildinfo.ModuleType, artifactName string) []string { + buildTimestampProp, exists := properties["build.timestamp"] + if !exists { + return []string{fmt.Sprintf("Missing build.timestamp property for %s artifact '%s'", moduleType, artifactName)} + } + + if len(buildTimestampProp) == 0 || buildTimestampProp[0] == "" { + return []string{fmt.Sprintf("Empty build.timestamp property for %s artifact '%s'", moduleType, artifactName)} + } + + timestampStr := buildTimestampProp[0] + _, err := strconv.ParseInt(timestampStr, 10, 64) + if err != nil { + return []string{fmt.Sprintf("Invalid build.timestamp format for %s artifact '%s': %s", + moduleType, artifactName, timestampStr)} + } + + return nil +} + func TestSetupPipCommand(t *testing.T) { if !*tests.TestPip { t.Skip("Skipping Pip test. To run Pip test add the '-test.pip=true' option.")