Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ require (
github.com/buger/jsonparser v1.3.0
github.com/gocarina/gocsv v0.0.0-20260607070740-0735908c6461
github.com/jfrog/archiver/v3 v3.6.4
github.com/jfrog/build-info-go v1.13.1-0.20260828071122-bb92ab7ba69b
github.com/jfrog/build-info-go v1.13.1-0.20260903111226-2e7b6001aade
github.com/jfrog/gofrog v1.7.6
github.com/jfrog/jfrog-cli-application v1.0.2-0.20260621072921-cadb78770a3e
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260828154930-3b7d100d5390
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260827111619-bee4d60fbdc7
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260903113804-415ec69b09dc
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260831061529-c6dd293bccca
github.com/jfrog/jfrog-cli-evidence v0.9.5-0.20260618135203-4d2bdd4ee35f
github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260618062042-6053ab368cab
github.com/jfrog/jfrog-cli-security v1.35.0
github.com/jfrog/jfrog-client-go v1.55.1-0.20260827094947-e7a90ebc8049
github.com/jfrog/jfrog-client-go v1.55.1-0.20260901090904-78d68f83abec
github.com/jszwec/csvutil v1.10.0
github.com/moby/moby/api v1.55.0
github.com/spf13/viper v1.21.0
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,8 @@ github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP
github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=
github.com/jfrog/archiver/v3 v3.6.4 h1:qHAWCLKwo3+ocHNNoWzGZ8ESl8QQk/lR3W09Pt+ROvE=
github.com/jfrog/archiver/v3 v3.6.4/go.mod h1:5V9l+Fte30Y4qe9dUOAd3yNTf8lmtVNuhKNrvI8PMhg=
github.com/jfrog/build-info-go v1.13.1-0.20260828071122-bb92ab7ba69b h1:kQRepoHjiJWwDx14CkrfBlfRHaHWf77XXWogqoMsVzU=
github.com/jfrog/build-info-go v1.13.1-0.20260828071122-bb92ab7ba69b/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE=
github.com/jfrog/build-info-go v1.13.1-0.20260903111226-2e7b6001aade h1:hIeC9PUxVgEcQnxGVPguhcnpp2dfRsCWvRIHg6Fa/2E=
github.com/jfrog/build-info-go v1.13.1-0.20260903111226-2e7b6001aade/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE=
github.com/jfrog/froggit-go v1.23.1 h1:4wmaHeuptxVINbovMaeITzVhi3+VQoc/FFIjF4axzu0=
github.com/jfrog/froggit-go v1.23.1/go.mod h1:wRDryqyp3oe+eHgME2mpnEQmO8XBECIPagFwj0nHmdI=
github.com/jfrog/go-mockhttp v0.3.1 h1:/wac8v4GMZx62viZmv4wazB5GNKs+GxawuS1u3maJH8=
Expand All @@ -402,18 +402,18 @@ github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYL
github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w=
github.com/jfrog/jfrog-cli-application v1.0.2-0.20260621072921-cadb78770a3e h1:jUfQzLCVbUazw7FEXf3+57vQheDSHa/Px/Gp4pf/sNI=
github.com/jfrog/jfrog-cli-application v1.0.2-0.20260621072921-cadb78770a3e/go.mod h1:p8yLtbmCxxQucIbLZKnWu0F+EDtj6NLXbRQCEK/nb6o=
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260828154930-3b7d100d5390 h1:pfoT3lcjqRcX7csf70OaxaE7IEGatnsyW2D68Xw6YHk=
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260828154930-3b7d100d5390/go.mod h1:we3sXBDY283lkB0Szd0q1oO1iKYqDsJFPpL/8Ze4P+Q=
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260827111619-bee4d60fbdc7 h1:4ytBkQB+iBS/KbG+a974hiZbmTith6KuWa5g0Zvw+z4=
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260827111619-bee4d60fbdc7/go.mod h1:vuARjRZopsCqVcZmWzCgw5Pr9QD1FWvwFxijV4bvJJI=
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260903113804-415ec69b09dc h1:5jshOGTzAJeRMlnBhvQBqE1bTmr1FT1AmP8VKXLYm8o=
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260903113804-415ec69b09dc/go.mod h1:viy6JELO1G/bJ3qkwiKoDmTI3lvtrIgcIt5DxtD6nno=
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260831061529-c6dd293bccca h1:/Ox4k56Pbiow4qbkNrBOmgcnAHwIBjZOsJmS7dURJng=
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260831061529-c6dd293bccca/go.mod h1:vuARjRZopsCqVcZmWzCgw5Pr9QD1FWvwFxijV4bvJJI=
github.com/jfrog/jfrog-cli-evidence v0.9.5-0.20260618135203-4d2bdd4ee35f h1:MV4BATdkEoUYJmdPDvaB9EBb8JQZg28n/K4X7dcmyAY=
github.com/jfrog/jfrog-cli-evidence v0.9.5-0.20260618135203-4d2bdd4ee35f/go.mod h1:t2luv7YHtrKe/Yf1xLZgLOkkiPtk1DsKj0OLXL2GwYo=
github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260618062042-6053ab368cab h1:Zn/qB8LYhSu82YDtbqXwErN1RPHTHe/a3gQY6Ti/OBE=
github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260618062042-6053ab368cab/go.mod h1:lVUeZtlvrLKJRsoSu8OPN9mJ+bfeq9zSESNYao2Jgo8=
github.com/jfrog/jfrog-cli-security v1.35.0 h1:6pz+WH4Zqbl5xXvf9YEidYUn1Iz0SbdLQtwuOCPQyoI=
github.com/jfrog/jfrog-cli-security v1.35.0/go.mod h1:T1LXsW+LORDBJbZGiEPxdWeOn+zzaOGwoBFQwvEL37A=
github.com/jfrog/jfrog-client-go v1.55.1-0.20260827094947-e7a90ebc8049 h1:eogwWAzZFir1suYEgZ4ZrYrch8fhWs7ma2dxv06p/z8=
github.com/jfrog/jfrog-client-go v1.55.1-0.20260827094947-e7a90ebc8049/go.mod h1:7B7eMRKuMhZ0rOdMItbJVpWjRUe1L//J3Jq+PgjiNxI=
github.com/jfrog/jfrog-client-go v1.55.1-0.20260901090904-78d68f83abec h1:fotFisxAbONFCpvjMniD30XbK+h92TpspLcqb258Z04=
github.com/jfrog/jfrog-client-go v1.55.1-0.20260901090904-78d68f83abec/go.mod h1:7B7eMRKuMhZ0rOdMItbJVpWjRUe1L//J3Jq+PgjiNxI=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/jszwec/csvutil v1.10.0 h1:upMDUxhQKqZ5ZDCs/wy+8Kib8rZR8I8lOR34yJkdqhI=
Expand Down
244 changes: 244 additions & 0 deletions npm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1686,3 +1686,247 @@ func TestNpmPublishWithLocalGitVcsProps(t *testing.T) {
tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch)
assert.Greater(t, count, 0)
}

// TestNpmInstallFailOnMissingDepsWithoutBuildInfo tests that --fail-on-missing-deps is accepted
// when build-info collection is off. The flag is stripped before npm runs and has no effect.
func TestNpmInstallFailOnMissingDepsWithoutBuildInfo(t *testing.T) {
initNpmTest(t)
defer cleanNpmTest(t)

wd, err := os.Getwd()
require.NoError(t, err)

npmPath := initNpmProjectTest(t)
chdirCallBack := clientTestUtils.ChangeDirWithCallback(t, wd, npmPath)
defer chdirCallBack()

runJfrogCli(t, "npm", "install", "--fail-on-missing-deps")
clientTestUtils.ChangeDirAndAssert(t, wd)
}

// TestNpmInstallWithoutFailOnMissingDepsFlag collects build-info with xml/json present in cache.
// This is the happy path, not the missing-cache warn path.
func TestNpmInstallWithoutFailOnMissingDepsFlag(t *testing.T) {
initNpmTest(t)
defer cleanNpmTest(t)

buildName := "npm-no-strict-test"
buildNumber := "1"

inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails)
defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails)

wd, err := os.Getwd()
require.NoError(t, err)

npmPath := initNpmProjectTest(t)
chdirCallBack := clientTestUtils.ChangeDirWithCallback(t, wd, npmPath)
defer chdirCallBack()

runJfrogCli(t, "npm", "install", "--build-name="+buildName, "--build-number="+buildNumber)
clientTestUtils.ChangeDirAndAssert(t, wd)
assertPublishedXmlAndJsonDeps(t, buildName, buildNumber)
}

// TestNpmInstallLegacyModeWarnsWithMissingCache installs xml/json from Artifactory,
// then clears npm _cacache tarballs while keeping node_modules.
// Without --fail-on-missing-deps the command succeeds and still publishes partial build-info.
func TestNpmInstallLegacyModeWarnsWithMissingCache(t *testing.T) {
initNpmTest(t)
defer cleanNpmTest(t)

buildName := "npm-legacy-warn-test"
buildNumber := "1"

inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails)
defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails)

wd, err := os.Getwd()
require.NoError(t, err)

npmPath := initNpmProjectTest(t)
chdirCallBack := clientTestUtils.ChangeDirWithCallback(t, wd, npmPath)
defer chdirCallBack()

cacheDir, restoreCache := useIsolatedNpmCache(t)
defer restoreCache()

err = runJfrogCliWithoutAssertion("npm", "install", "--cache="+cacheDir)
assert.NoError(t, err, "Initial npm install should populate node_modules and the isolated cache from Artifactory")

wipeNpmCacacheTarballs(t, cacheDir)

err = runJfrogCliWithoutAssertion("npm", "install",
"--cache="+cacheDir,
"--build-name="+buildName,
"--build-number="+buildNumber)
assert.NoError(t, err, "Without --fail-on-missing-deps, missing xml/json cache tarballs should not fail the command")

clientTestUtils.ChangeDirAndAssert(t, wd)
require.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber))
publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber)
assert.NoError(t, err)
assert.True(t, found, "Partial build-info should still be published in legacy (warn) mode")
require.NotNil(t, publishedBuildInfo)
assertNoXmlOrJsonDeps(t, publishedBuildInfo)
}

// TestNpmInstallWithFailOnMissingDepsFlag collects build-info with --fail-on-missing-deps
// while xml/json tarballs are present. Strict mode must not fail, and both packages must
// appear in the published module.
func TestNpmInstallWithFailOnMissingDepsFlag(t *testing.T) {
initNpmTest(t)
defer cleanNpmTest(t)

buildName := "npm-strict-test"
buildNumber := "1"

inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails)
defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails)

wd, err := os.Getwd()
require.NoError(t, err)

npmPath := initNpmProjectTest(t)
chdirCallBack := clientTestUtils.ChangeDirWithCallback(t, wd, npmPath)
defer chdirCallBack()

runJfrogCli(t, "npm", "install",
"--build-name="+buildName,
"--build-number="+buildNumber,
"--fail-on-missing-deps")
clientTestUtils.ChangeDirAndAssert(t, wd)
assertPublishedXmlAndJsonDeps(t, buildName, buildNumber)
}

// useIsolatedNpmCache points npm at a dedicated cache directory via npm_config_cache.
// Callers must also pass --cache=<dir> to every npm invocation: the env var alone loses to an
// NPM_CONFIG_CACHE already exported by the environment, which makes 'npm config get cache'
// (how the build-info collector locates the cache) report a directory the test never wiped.
func useIsolatedNpmCache(t *testing.T) (cacheDir string, restore func()) {
cacheDir = t.TempDir()
return cacheDir, clientTestUtils.SetEnvWithCallbackAndAssert(t, "npm_config_cache", cacheDir)
}

// npmCachedTarballs returns every tarball currently stored in the cache's content-v2 store.
func npmCachedTarballs(t *testing.T, cacheDir string) []string {
var tarballs []string
contentDir := filepath.Join(cacheDir, "_cacache", "content-v2")
err := filepath.Walk(contentDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
tarballs = append(tarballs, path)
}
return nil
})
if os.IsNotExist(err) {
return nil
}
require.NoError(t, err)
return tarballs
}

// wipeNpmCacacheTarballs removes cached tarballs and index-v5 so xml/json cannot be checksummed.
// GetNpmConfigCache requires _cacache to exist; node_modules is left in place so the next
// npm install stays up to date and does not refill the cache from the registry.
func wipeNpmCacacheTarballs(t *testing.T, cacheDir string) {
cacachePath := filepath.Join(cacheDir, "_cacache")
tarballs := npmCachedTarballs(t, cacheDir)
require.NotEmpty(t, tarballs, "cache should hold tarballs before wiping, otherwise the test proves nothing")
require.NoError(t, os.RemoveAll(filepath.Join(cacachePath, "content-v2")))
require.NoError(t, os.RemoveAll(filepath.Join(cacachePath, "index-v5")))
require.NoError(t, os.MkdirAll(cacachePath, 0755))
}

func assertPublishedXmlAndJsonDeps(t *testing.T, buildName, buildNumber string) {
t.Helper()
require.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber))
publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber)
assert.NoError(t, err)
require.True(t, found)
require.NotNil(t, publishedBuildInfo)
require.NotEmpty(t, publishedBuildInfo.BuildInfo.Modules)
equalDependenciesSlices(t,
[]expectedDependency{
{id: "xml:1.0.1", scopes: []string{"prod"}},
{id: "json:9.0.6", scopes: []string{"dev"}},
},
publishedBuildInfo.BuildInfo.Modules[0].Dependencies)
}

func assertNoXmlOrJsonDeps(t *testing.T, publishedBuildInfo *buildinfo.PublishedBuildInfo) {
t.Helper()
require.NotNil(t, publishedBuildInfo)
for _, module := range publishedBuildInfo.BuildInfo.Modules {
for _, dep := range module.Dependencies {
assert.NotEqual(t, "xml:1.0.1", dep.Id, "wiped xml tarball must not appear in module %s", module.Id)
assert.NotEqual(t, "json:9.0.6", dep.Id, "wiped json tarball must not appear in module %s", module.Id)
}
}
}

func assertMissingCacheStrictError(t *testing.T, err error) {
t.Helper()
require.Error(t, err)
msg := err.Error()
assert.True(t,
strings.Contains(msg, "cannot be 100% resolved") || strings.Contains(msg, "missing in the npm cache"),
"Error should mention unresolved xml/json build-info dependencies, got: %v", err)
assert.Contains(t, msg, "xml")
assert.Contains(t, msg, "json")
}

// TestNpmInstallFailsWithMissingCacheStrict is the same xml/json + wiped-cache setup as
// TestNpmInstallLegacyModeWarnsWithMissingCache, but with --fail-on-missing-deps.
// The warn path already listed xml:1.0.1 and json:9.0.6 as missing; strict mode must error
// on that same list and skip SaveBuildInfo, so no dependencies reach Artifactory.
func TestNpmInstallFailsWithMissingCacheStrict(t *testing.T) {
initNpmTest(t)
defer cleanNpmTest(t)

buildName := "npm-missing-cache-test"
buildNumber := "1"

inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails)
defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails)

wd, err := os.Getwd()
require.NoError(t, err)

npmPath := initNpmProjectTest(t)
chdirCallBack := clientTestUtils.ChangeDirWithCallback(t, wd, npmPath)
defer chdirCallBack()

cacheDir, restoreCache := useIsolatedNpmCache(t)
defer restoreCache()

err = runJfrogCliWithoutAssertion("npm", "install", "--cache="+cacheDir)
assert.NoError(t, err, "Initial npm install should populate node_modules and the isolated cache from Artifactory")

wipeNpmCacacheTarballs(t, cacheDir)

err = runJfrogCliWithoutAssertion("npm", "install",
"--cache="+cacheDir,
"--build-name="+buildName,
"--build-number="+buildNumber,
"--fail-on-missing-deps")
assertMissingCacheStrictError(t, err)

clientTestUtils.ChangeDirAndAssert(t, wd)
publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber)
assert.NoError(t, err)
assert.False(t, found, "Build info should not exist in Artifactory when collection failed")
assert.Nil(t, publishedBuildInfo)

// The build directory is created before dependencies are collected, so 'bp' can still publish an
// empty build-info. What must not happen is xml/json reaching Artifactory after the wipe.
if publishErr := artifactoryCli.Exec("bp", buildName, buildNumber); publishErr == nil {
publishedBuildInfo, found, err = tests.GetBuildInfo(serverDetails, buildName, buildNumber)
assert.NoError(t, err)
if found && publishedBuildInfo != nil {
assertNoXmlOrJsonDeps(t, publishedBuildInfo)
}
}
}
2 changes: 1 addition & 1 deletion testdata/npm/npmproject/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
"devDependencies": {
"json": "9.0.6"
}
}
}
Loading