Add comprehensive tests for Go and Python version conflict detection and licensing normalization

- Implemented GoVersionConflictDetectorTests to validate pseudo-version detection, conflict analysis, and conflict retrieval for Go modules.
- Created VersionConflictDetectorTests for Python to assess conflict detection across various version scenarios, including major, minor, and patch differences.
- Added SpdxLicenseNormalizerTests to ensure accurate normalization of SPDX license strings and classifiers.
- Developed VendoredPackageDetectorTests to identify vendored packages and extract embedded packages from Python packages, including handling of vendor directories and known vendored packages.
This commit is contained in:
StellaOps Bot
2025-12-07 01:51:37 +02:00
parent 98934170ca
commit e0f6efecce
66 changed files with 7591 additions and 451 deletions

View File

@@ -0,0 +1,6 @@
{
"lockfileVersion": 1,
"packages": {
"@company/internal-pkg@1.0.0": ["https://npm.company.com/@company/internal-pkg/-/internal-pkg-1.0.0.tgz", "sha512-customhash123=="]
}
}

View File

@@ -0,0 +1,2 @@
[install.scopes]
"@company" = "https://npm.company.com/"

View File

@@ -0,0 +1,39 @@
[
{
"analyzerId": "bun",
"componentKey": "purl::pkg:npm/%40company/internal-pkg@1.0.0",
"purl": "pkg:npm/%40company/internal-pkg@1.0.0",
"name": "@company/internal-pkg",
"version": "1.0.0",
"type": "npm",
"usedByEntrypoint": false,
"metadata": {
"customRegistry": "https://npm.company.com/",
"direct": "true",
"integrity": "sha512-customhash123==",
"packageManager": "bun",
"path": "node_modules/@company/internal-pkg",
"resolved": "https://npm.company.com/@company/internal-pkg/-/internal-pkg-1.0.0.tgz",
"source": "node_modules"
},
"evidence": [
{
"kind": "file",
"source": "node_modules",
"locator": "node_modules/@company/internal-pkg/package.json"
},
{
"kind": "metadata",
"source": "integrity",
"locator": "bun.lock",
"value": "sha512-customhash123=="
},
{
"kind": "metadata",
"source": "resolved",
"locator": "bun.lock",
"value": "https://npm.company.com/@company/internal-pkg/-/internal-pkg-1.0.0.tgz"
}
]
}
]

View File

@@ -0,0 +1,7 @@
{
"name": "custom-registry-fixture",
"version": "1.0.0",
"dependencies": {
"@company/internal-pkg": "^1.0.0"
}
}

View File

@@ -0,0 +1,7 @@
{
"lockfileVersion": 1,
"packages": {
"debug@4.3.4": ["https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", {"ms": "^2.1.2"}],
"ms@2.1.3": ["https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="]
}
}

View File

@@ -0,0 +1,73 @@
[
{
"analyzerId": "bun",
"componentKey": "purl::pkg:npm/debug@4.3.4",
"purl": "pkg:npm/debug@4.3.4",
"name": "debug",
"version": "4.3.4",
"type": "npm",
"usedByEntrypoint": false,
"metadata": {
"direct": "true",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"packageManager": "bun",
"path": "node_modules/debug",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"source": "node_modules"
},
"evidence": [
{
"kind": "file",
"source": "node_modules",
"locator": "node_modules/debug/package.json"
},
{
"kind": "metadata",
"source": "integrity",
"locator": "bun.lock",
"value": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="
},
{
"kind": "metadata",
"source": "resolved",
"locator": "bun.lock",
"value": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
}
]
},
{
"analyzerId": "bun",
"componentKey": "purl::pkg:npm/ms@2.1.3",
"purl": "pkg:npm/ms@2.1.3",
"name": "ms",
"version": "2.1.3",
"type": "npm",
"usedByEntrypoint": false,
"metadata": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"packageManager": "bun",
"path": "node_modules/ms",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"source": "node_modules"
},
"evidence": [
{
"kind": "file",
"source": "node_modules",
"locator": "node_modules/ms/package.json"
},
{
"kind": "metadata",
"source": "integrity",
"locator": "bun.lock",
"value": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
{
"kind": "metadata",
"source": "resolved",
"locator": "bun.lock",
"value": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
}
]
}
]

View File

@@ -0,0 +1,7 @@
{
"name": "deep-tree-fixture",
"version": "1.0.0",
"dependencies": {
"debug": "^4.3.4"
}
}

View File

@@ -0,0 +1,6 @@
{
"lockfileVersion": 1,
"packages": {
"my-git-pkg@1.0.0": ["git+https://github.com/user/my-git-pkg.git#abc123def456", null]
}
}

View File

@@ -0,0 +1,34 @@
[
{
"analyzerId": "bun",
"componentKey": "purl::pkg:npm/my-git-pkg@1.0.0",
"purl": "pkg:npm/my-git-pkg@1.0.0",
"name": "my-git-pkg",
"version": "1.0.0",
"type": "npm",
"usedByEntrypoint": false,
"metadata": {
"direct": "true",
"gitCommit": "abc123def456",
"packageManager": "bun",
"path": "node_modules/my-git-pkg",
"resolved": "git+https://github.com/user/my-git-pkg.git#abc123def456",
"source": "node_modules",
"sourceType": "git",
"specifier": "git+https://github.com/user/my-git-pkg.git#abc123def456"
},
"evidence": [
{
"kind": "file",
"source": "node_modules",
"locator": "node_modules/my-git-pkg/package.json"
},
{
"kind": "metadata",
"source": "resolved",
"locator": "bun.lock",
"value": "git+https://github.com/user/my-git-pkg.git#abc123def456"
}
]
}
]

View File

@@ -0,0 +1,7 @@
{
"name": "git-dependencies-fixture",
"version": "1.0.0",
"dependencies": {
"my-git-pkg": "github:user/my-git-pkg#v1.0.0"
}
}

View File

@@ -0,0 +1,6 @@
{
"lockfileVersion": 1,
"packages": {
"lodash@4.17.21": ["https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vz1kAmtILi+8fm9nJMg7b0GN8sMEJz2mxG/S7mNxhWQ7+D9bF8Q=="]
}
}

View File

@@ -0,0 +1,40 @@
[
{
"analyzerId": "bun",
"componentKey": "purl::pkg:npm/lodash@4.17.21",
"purl": "pkg:npm/lodash@4.17.21",
"name": "lodash",
"version": "4.17.21",
"type": "npm",
"usedByEntrypoint": false,
"metadata": {
"direct": "true",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vz1kAmtILi+8fm9nJMg7b0GN8sMEJz2mxG/S7mNxhWQ7+D9bF8Q==",
"packageManager": "bun",
"patchFile": "patches/lodash@4.17.21.patch",
"patched": "true",
"path": "node_modules/lodash",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"source": "node_modules"
},
"evidence": [
{
"kind": "file",
"source": "node_modules",
"locator": "node_modules/lodash/package.json"
},
{
"kind": "metadata",
"source": "integrity",
"locator": "bun.lock",
"value": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vz1kAmtILi+8fm9nJMg7b0GN8sMEJz2mxG/S7mNxhWQ7+D9bF8Q=="
},
{
"kind": "metadata",
"source": "resolved",
"locator": "bun.lock",
"value": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
}
]
}
]

View File

@@ -0,0 +1,10 @@
{
"name": "patched-packages-fixture",
"version": "1.0.0",
"dependencies": {
"lodash": "^4.17.21"
},
"patchedDependencies": {
"lodash@4.17.21": "patches/lodash@4.17.21.patch"
}
}

View File

@@ -0,0 +1,5 @@
--- a/index.js
+++ b/index.js
@@ -1 +1 @@
-module.exports = require('./lodash');
+module.exports = require('./lodash-patched');

View File

@@ -0,0 +1,7 @@
{
"lockfileVersion": 1,
"packages": {
"@babel/core@7.24.0": ["https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw=="],
"@types/node@20.11.0": ["https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz", "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxXjVJa7b8XWCF/wPH2E/0Vz9e+V1B3eXX0WCw+INcAobvUag=="]
}
}

View File

@@ -0,0 +1,74 @@
[
{
"analyzerId": "bun",
"componentKey": "purl::pkg:npm/%40babel/core@7.24.0",
"purl": "pkg:npm/%40babel/core@7.24.0",
"name": "@babel/core",
"version": "7.24.0",
"type": "npm",
"usedByEntrypoint": false,
"metadata": {
"direct": "true",
"integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
"packageManager": "bun",
"path": "node_modules/@babel/core",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
"source": "node_modules"
},
"evidence": [
{
"kind": "file",
"source": "node_modules",
"locator": "node_modules/@babel/core/package.json"
},
{
"kind": "metadata",
"source": "integrity",
"locator": "bun.lock",
"value": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw=="
},
{
"kind": "metadata",
"source": "resolved",
"locator": "bun.lock",
"value": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz"
}
]
},
{
"analyzerId": "bun",
"componentKey": "purl::pkg:npm/%40types/node@20.11.0",
"purl": "pkg:npm/%40types/node@20.11.0",
"name": "@types/node",
"version": "20.11.0",
"type": "npm",
"usedByEntrypoint": false,
"metadata": {
"direct": "true",
"integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxXjVJa7b8XWCF/wPH2E/0Vz9e+V1B3eXX0WCw+INcAobvUag==",
"packageManager": "bun",
"path": "node_modules/@types/node",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz",
"source": "node_modules"
},
"evidence": [
{
"kind": "file",
"source": "node_modules",
"locator": "node_modules/@types/node/package.json"
},
{
"kind": "metadata",
"source": "integrity",
"locator": "bun.lock",
"value": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxXjVJa7b8XWCF/wPH2E/0Vz9e+V1B3eXX0WCw+INcAobvUag=="
},
{
"kind": "metadata",
"source": "resolved",
"locator": "bun.lock",
"value": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz"
}
]
}
]

View File

@@ -0,0 +1,8 @@
{
"name": "scoped-packages-fixture",
"version": "1.0.0",
"dependencies": {
"@babel/core": "^7.24.0",
"@types/node": "^20.11.0"
}
}

View File

@@ -0,0 +1,226 @@
using StellaOps.Scanner.Analyzers.Lang.Bun.Internal;
namespace StellaOps.Scanner.Analyzers.Lang.Bun.Tests.Parsers;
public sealed class BunConfigHelperTests
{
private readonly string _tempDir;
public BunConfigHelperTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"bun-config-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
#region ParseConfig Tests
[Fact]
public void ParseConfig_MissingFile_ReturnsEmpty()
{
var result = BunConfigHelper.ParseConfig(_tempDir);
Assert.Null(result.DefaultRegistry);
Assert.Empty(result.ScopeRegistries);
Assert.False(result.HasCustomRegistry);
}
[Fact]
public void ParseConfig_DefaultRegistry_ReturnsUrl()
{
var bunfig = """
[install]
registry = "https://npm.company.com/"
""";
File.WriteAllText(Path.Combine(_tempDir, "bunfig.toml"), bunfig);
var result = BunConfigHelper.ParseConfig(_tempDir);
Assert.Equal("https://npm.company.com/", result.DefaultRegistry);
Assert.True(result.HasCustomRegistry);
}
[Fact]
public void ParseConfig_ScopedRegistries_ReturnsMappings()
{
var bunfig = """
[install.scopes]
"@company" = "https://npm.company.com/"
"@internal" = "https://internal.registry.com/"
""";
File.WriteAllText(Path.Combine(_tempDir, "bunfig.toml"), bunfig);
var result = BunConfigHelper.ParseConfig(_tempDir);
Assert.Equal(2, result.ScopeRegistries.Count);
Assert.Equal("https://npm.company.com/", result.ScopeRegistries["@company"]);
Assert.Equal("https://internal.registry.com/", result.ScopeRegistries["@internal"]);
Assert.True(result.HasCustomRegistry);
}
[Fact]
public void ParseConfig_InlineTableFormat_ExtractsUrl()
{
var bunfig = """
[install.scopes]
"@company" = { url = "https://npm.company.com/" }
""";
File.WriteAllText(Path.Combine(_tempDir, "bunfig.toml"), bunfig);
var result = BunConfigHelper.ParseConfig(_tempDir);
Assert.Single(result.ScopeRegistries);
Assert.Equal("https://npm.company.com/", result.ScopeRegistries["@company"]);
}
[Fact]
public void ParseConfig_Comments_IgnoresComments()
{
var bunfig = """
# This is a comment
[install]
# registry for npm packages
registry = "https://npm.company.com/"
""";
File.WriteAllText(Path.Combine(_tempDir, "bunfig.toml"), bunfig);
var result = BunConfigHelper.ParseConfig(_tempDir);
Assert.Equal("https://npm.company.com/", result.DefaultRegistry);
}
[Fact]
public void ParseConfig_EmptyFile_ReturnsEmpty()
{
File.WriteAllText(Path.Combine(_tempDir, "bunfig.toml"), "");
var result = BunConfigHelper.ParseConfig(_tempDir);
Assert.Null(result.DefaultRegistry);
Assert.Empty(result.ScopeRegistries);
}
[Fact]
public void ParseConfig_BothDefaultAndScoped_ReturnsBoth()
{
var bunfig = """
[install]
registry = "https://default.registry.com/"
[install.scopes]
"@company" = "https://npm.company.com/"
""";
File.WriteAllText(Path.Combine(_tempDir, "bunfig.toml"), bunfig);
var result = BunConfigHelper.ParseConfig(_tempDir);
Assert.Equal("https://default.registry.com/", result.DefaultRegistry);
Assert.Single(result.ScopeRegistries);
Assert.Equal("https://npm.company.com/", result.ScopeRegistries["@company"]);
}
#endregion
#region StripQuotes Tests
[Fact]
public void StripQuotes_DoubleQuotes_RemovesQuotes()
{
var result = BunConfigHelper.StripQuotes("\"hello world\"");
Assert.Equal("hello world", result);
}
[Fact]
public void StripQuotes_SingleQuotes_RemovesQuotes()
{
var result = BunConfigHelper.StripQuotes("'hello world'");
Assert.Equal("hello world", result);
}
[Fact]
public void StripQuotes_NoQuotes_ReturnsUnchanged()
{
var result = BunConfigHelper.StripQuotes("hello world");
Assert.Equal("hello world", result);
}
[Fact]
public void StripQuotes_MismatchedQuotes_ReturnsUnchanged()
{
var result = BunConfigHelper.StripQuotes("\"hello world'");
Assert.Equal("\"hello world'", result);
}
[Fact]
public void StripQuotes_EmptyString_ReturnsEmpty()
{
var result = BunConfigHelper.StripQuotes("");
Assert.Equal("", result);
}
[Fact]
public void StripQuotes_SingleCharacter_ReturnsUnchanged()
{
var result = BunConfigHelper.StripQuotes("a");
Assert.Equal("a", result);
}
#endregion
#region ExtractRegistryUrl Tests
[Fact]
public void ExtractRegistryUrl_DirectUrl_ReturnsUrl()
{
var result = BunConfigHelper.ExtractRegistryUrl("https://npm.company.com/");
Assert.Equal("https://npm.company.com/", result);
}
[Fact]
public void ExtractRegistryUrl_InlineTable_ExtractsUrl()
{
var result = BunConfigHelper.ExtractRegistryUrl("{ url = \"https://npm.company.com/\" }");
Assert.Equal("https://npm.company.com/", result);
}
[Fact]
public void ExtractRegistryUrl_InlineTableSingleQuotes_ExtractsUrl()
{
var result = BunConfigHelper.ExtractRegistryUrl("{ url = 'https://npm.company.com/' }");
Assert.Equal("https://npm.company.com/", result);
}
[Fact]
public void ExtractRegistryUrl_InvalidFormat_ReturnsNull()
{
var result = BunConfigHelper.ExtractRegistryUrl("not-a-url");
Assert.Null(result);
}
[Fact]
public void ExtractRegistryUrl_HttpUrl_ReturnsUrl()
{
var result = BunConfigHelper.ExtractRegistryUrl("http://internal.registry.local/");
Assert.Equal("http://internal.registry.local/", result);
}
#endregion
}

View File

@@ -0,0 +1,455 @@
using StellaOps.Scanner.Analyzers.Lang.Bun.Internal;
namespace StellaOps.Scanner.Analyzers.Lang.Bun.Tests.Parsers;
public sealed class BunLockParserTests
{
#region ParsePackageKey Tests
[Fact]
public void ParsePackageKey_ScopedPackage_ReturnsCorrectNameAndVersion()
{
var (name, version) = BunLockParser.ParsePackageKey("@babel/core@7.24.0");
Assert.Equal("@babel/core", name);
Assert.Equal("7.24.0", version);
}
[Fact]
public void ParsePackageKey_UnscopedPackage_ReturnsCorrectNameAndVersion()
{
var (name, version) = BunLockParser.ParsePackageKey("lodash@4.17.21");
Assert.Equal("lodash", name);
Assert.Equal("4.17.21", version);
}
[Fact]
public void ParsePackageKey_InvalidFormat_NoAtSymbol_ReturnsEmpty()
{
var (name, version) = BunLockParser.ParsePackageKey("lodash");
Assert.Empty(name);
Assert.Empty(version);
}
[Fact]
public void ParsePackageKey_InvalidFormat_OnlyScope_ReturnsEmpty()
{
var (name, version) = BunLockParser.ParsePackageKey("@babel");
Assert.Empty(name);
Assert.Empty(version);
}
[Fact]
public void ParsePackageKey_ScopedPackageWithComplexVersion_ReturnsCorrectParts()
{
var (name, version) = BunLockParser.ParsePackageKey("@types/node@20.11.24");
Assert.Equal("@types/node", name);
Assert.Equal("20.11.24", version);
}
[Fact]
public void ParsePackageKey_PreReleaseVersion_ReturnsCorrectParts()
{
var (name, version) = BunLockParser.ParsePackageKey("typescript@5.4.0-beta");
Assert.Equal("typescript", name);
Assert.Equal("5.4.0-beta", version);
}
#endregion
#region ClassifyResolvedUrl Tests
[Fact]
public void ClassifyResolvedUrl_GitPlusHttps_ReturnsGit()
{
var (sourceType, gitCommit, specifier) = BunLockParser.ClassifyResolvedUrl("git+https://github.com/user/repo.git#abc123");
Assert.Equal("git", sourceType);
Assert.Equal("abc123", gitCommit);
Assert.Equal("git+https://github.com/user/repo.git#abc123", specifier);
}
[Fact]
public void ClassifyResolvedUrl_GitPlusSsh_ReturnsGit()
{
var (sourceType, gitCommit, specifier) = BunLockParser.ClassifyResolvedUrl("git+ssh://git@github.com/user/repo.git#v1.0.0");
Assert.Equal("git", sourceType);
Assert.Equal("v1.0.0", gitCommit);
}
[Fact]
public void ClassifyResolvedUrl_GithubShorthand_ReturnsGit()
{
var (sourceType, gitCommit, specifier) = BunLockParser.ClassifyResolvedUrl("github:user/repo#main");
Assert.Equal("git", sourceType);
Assert.Equal("main", gitCommit);
Assert.Equal("github:user/repo#main", specifier);
}
[Fact]
public void ClassifyResolvedUrl_GitlabShorthand_ReturnsGit()
{
var (sourceType, _, _) = BunLockParser.ClassifyResolvedUrl("gitlab:user/repo#v2.0.0");
Assert.Equal("git", sourceType);
}
[Fact]
public void ClassifyResolvedUrl_BitbucketShorthand_ReturnsGit()
{
var (sourceType, _, _) = BunLockParser.ClassifyResolvedUrl("bitbucket:user/repo#feature");
Assert.Equal("git", sourceType);
}
[Fact]
public void ClassifyResolvedUrl_TarballUrl_ReturnsTarball()
{
var (sourceType, gitCommit, specifier) = BunLockParser.ClassifyResolvedUrl("https://example.com/pkg-1.0.0.tgz");
Assert.Equal("tarball", sourceType);
Assert.Null(gitCommit);
Assert.Equal("https://example.com/pkg-1.0.0.tgz", specifier);
}
[Fact]
public void ClassifyResolvedUrl_TarGzUrl_ReturnsTarball()
{
var (sourceType, _, _) = BunLockParser.ClassifyResolvedUrl("https://example.com/pkg-1.0.0.tar.gz");
Assert.Equal("tarball", sourceType);
}
[Fact]
public void ClassifyResolvedUrl_NpmRegistryTgz_ReturnsNpm()
{
var (sourceType, _, _) = BunLockParser.ClassifyResolvedUrl("https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz");
Assert.Equal("npm", sourceType);
}
[Fact]
public void ClassifyResolvedUrl_FileProtocol_ReturnsFile()
{
var (sourceType, gitCommit, specifier) = BunLockParser.ClassifyResolvedUrl("file:./local-pkg");
Assert.Equal("file", sourceType);
Assert.Null(gitCommit);
Assert.Equal("file:./local-pkg", specifier);
}
[Fact]
public void ClassifyResolvedUrl_LinkProtocol_ReturnsLink()
{
var (sourceType, _, specifier) = BunLockParser.ClassifyResolvedUrl("link:../packages/shared");
Assert.Equal("link", sourceType);
Assert.Equal("link:../packages/shared", specifier);
}
[Fact]
public void ClassifyResolvedUrl_WorkspaceProtocol_ReturnsWorkspace()
{
var (sourceType, _, specifier) = BunLockParser.ClassifyResolvedUrl("workspace:*");
Assert.Equal("workspace", sourceType);
Assert.Equal("workspace:*", specifier);
}
[Fact]
public void ClassifyResolvedUrl_NpmRegistry_ReturnsNpm()
{
var (sourceType, gitCommit, specifier) = BunLockParser.ClassifyResolvedUrl("https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz");
Assert.Equal("npm", sourceType);
Assert.Null(gitCommit);
Assert.Null(specifier);
}
[Fact]
public void ClassifyResolvedUrl_NullOrEmpty_ReturnsNpm()
{
var (sourceType1, _, _) = BunLockParser.ClassifyResolvedUrl(null);
var (sourceType2, _, _) = BunLockParser.ClassifyResolvedUrl("");
Assert.Equal("npm", sourceType1);
Assert.Equal("npm", sourceType2);
}
#endregion
#region ExtractGitCommit Tests
[Fact]
public void ExtractGitCommit_HashFragment_ReturnsCommit()
{
var commit = BunLockParser.ExtractGitCommit("git+https://github.com/user/repo.git#abc123def");
Assert.Equal("abc123def", commit);
}
[Fact]
public void ExtractGitCommit_NoFragment_ReturnsNull()
{
var commit = BunLockParser.ExtractGitCommit("git+https://github.com/user/repo.git");
Assert.Null(commit);
}
[Fact]
public void ExtractGitCommit_EmptyFragment_ReturnsNull()
{
var commit = BunLockParser.ExtractGitCommit("github:user/repo#");
Assert.Null(commit);
}
[Fact]
public void ExtractGitCommit_TagName_ReturnsTag()
{
var commit = BunLockParser.ExtractGitCommit("github:user/repo#v1.2.3");
Assert.Equal("v1.2.3", commit);
}
#endregion
#region Parse Tests
[Fact]
public void Parse_EmptyContent_ReturnsEmptyData()
{
var result = BunLockParser.Parse("");
Assert.Empty(result.Entries);
}
[Fact]
public void Parse_WhitespaceContent_ReturnsEmptyData()
{
var result = BunLockParser.Parse(" \n\t ");
Assert.Empty(result.Entries);
}
[Fact]
public void Parse_MalformedJson_ReturnsEmptyData()
{
var result = BunLockParser.Parse("{ invalid json }");
Assert.Empty(result.Entries);
}
[Fact]
public void Parse_JsoncComments_IgnoresCommentsAndParses()
{
var content = """
{
// This is a comment
"lockfileVersion": 1,
"packages": {
"lodash@4.17.21": ["https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "sha512-abc"]
}
}
""";
var result = BunLockParser.Parse(content);
Assert.Single(result.Entries);
Assert.Equal("lodash", result.Entries[0].Name);
}
[Fact]
public void Parse_TrailingCommas_ParsesSuccessfully()
{
var content = """
{
"lockfileVersion": 1,
"packages": {
"lodash@4.17.21": ["https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "sha512-abc"],
},
}
""";
var result = BunLockParser.Parse(content);
Assert.Single(result.Entries);
}
[Fact]
public void Parse_ArrayFormat_ExtractsResolvedAndIntegrity()
{
var content = """
{
"lockfileVersion": 1,
"packages": {
"ms@2.1.3": ["https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "sha512-6FlzubTLZG3J2a"]
}
}
""";
var result = BunLockParser.Parse(content);
Assert.Single(result.Entries);
var entry = result.Entries[0];
Assert.Equal("ms", entry.Name);
Assert.Equal("2.1.3", entry.Version);
Assert.Equal("https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", entry.Resolved);
Assert.Equal("sha512-6FlzubTLZG3J2a", entry.Integrity);
}
[Fact]
public void Parse_ArrayFormat_ExtractsDependencies()
{
var content = """
{
"lockfileVersion": 1,
"packages": {
"debug@4.3.4": ["https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "sha512-abc", {"ms": "^2.1.3"}]
}
}
""";
var result = BunLockParser.Parse(content);
Assert.Single(result.Entries);
var entry = result.Entries[0];
Assert.Single(entry.Dependencies);
Assert.Contains("ms", entry.Dependencies);
}
[Fact]
public void Parse_ObjectFormat_ExtractsDevOptionalPeer()
{
var content = """
{
"lockfileVersion": 1,
"packages": {
"typescript@5.4.0": {
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.0.tgz",
"integrity": "sha512-abc",
"dev": true,
"optional": true,
"peer": true
}
}
}
""";
var result = BunLockParser.Parse(content);
Assert.Single(result.Entries);
var entry = result.Entries[0];
Assert.Equal("typescript", entry.Name);
Assert.True(entry.IsDev);
Assert.True(entry.IsOptional);
Assert.True(entry.IsPeer);
}
[Fact]
public void Parse_StringFormat_ExtractsResolved()
{
var content = """
{
"lockfileVersion": 1,
"packages": {
"lodash@4.17.21": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
}
}
""";
var result = BunLockParser.Parse(content);
Assert.Single(result.Entries);
var entry = result.Entries[0];
Assert.Equal("lodash", entry.Name);
Assert.Equal("https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", entry.Resolved);
Assert.Null(entry.Integrity);
}
[Fact]
public void Parse_SkipsRootProjectEntry()
{
var content = """
{
"lockfileVersion": 1,
"packages": {
"": {},
".": {},
"lodash@4.17.21": ["https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "sha512-abc"]
}
}
""";
var result = BunLockParser.Parse(content);
Assert.Single(result.Entries);
Assert.Equal("lodash", result.Entries[0].Name);
}
[Fact]
public void Parse_MultiplePackages_ReturnsAll()
{
var content = """
{
"lockfileVersion": 1,
"packages": {
"lodash@4.17.21": ["https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "sha512-lodash"],
"ms@2.1.3": ["https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "sha512-ms"],
"@babel/core@7.24.0": ["https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", "sha512-babel"]
}
}
""";
var result = BunLockParser.Parse(content);
Assert.Equal(3, result.Entries.Count);
Assert.Contains(result.Entries, e => e.Name == "lodash");
Assert.Contains(result.Entries, e => e.Name == "ms");
Assert.Contains(result.Entries, e => e.Name == "@babel/core");
}
[Fact]
public void Parse_GitDependency_ClassifiesCorrectly()
{
var content = """
{
"lockfileVersion": 1,
"packages": {
"my-pkg@1.0.0": ["git+https://github.com/user/my-pkg.git#abc123", null]
}
}
""";
var result = BunLockParser.Parse(content);
Assert.Single(result.Entries);
var entry = result.Entries[0];
Assert.Equal("git", entry.SourceType);
Assert.Equal("abc123", entry.GitCommit);
Assert.Equal("git+https://github.com/user/my-pkg.git#abc123", entry.Specifier);
}
[Fact]
public void Parse_NoPackagesProperty_ReturnsEmpty()
{
var content = """
{
"lockfileVersion": 1
}
""";
var result = BunLockParser.Parse(content);
Assert.Empty(result.Entries);
}
#endregion
}

View File

@@ -0,0 +1,325 @@
using StellaOps.Scanner.Analyzers.Lang.Bun.Internal;
namespace StellaOps.Scanner.Analyzers.Lang.Bun.Tests.Parsers;
public sealed class BunPackageTests
{
#region Purl Generation Tests
[Fact]
public void FromPackageJson_UnscopedPackage_GeneratesCorrectPurl()
{
var package = BunPackage.FromPackageJson(
name: "lodash",
version: "4.17.21",
logicalPath: "node_modules/lodash",
realPath: null,
isPrivate: false,
lockEntry: null);
Assert.Equal("pkg:npm/lodash@4.17.21", package.Purl);
Assert.Equal("purl::pkg:npm/lodash@4.17.21", package.ComponentKey);
}
[Fact]
public void FromPackageJson_ScopedPackage_EncodesAtSymbol()
{
var package = BunPackage.FromPackageJson(
name: "@babel/core",
version: "7.24.0",
logicalPath: "node_modules/@babel/core",
realPath: null,
isPrivate: false,
lockEntry: null);
Assert.Equal("pkg:npm/%40babel/core@7.24.0", package.Purl);
}
[Fact]
public void FromPackageJson_ScopedPackageWithSlash_EncodesCorrectly()
{
var package = BunPackage.FromPackageJson(
name: "@types/node",
version: "20.11.0",
logicalPath: "node_modules/@types/node",
realPath: null,
isPrivate: false,
lockEntry: null);
Assert.Equal("pkg:npm/%40types/node@20.11.0", package.Purl);
}
#endregion
#region CreateMetadata Tests
[Fact]
public void CreateMetadata_BasicPackage_ReturnsRequiredKeys()
{
var package = BunPackage.FromPackageJson(
name: "lodash",
version: "4.17.21",
logicalPath: "node_modules/lodash",
realPath: null,
isPrivate: false,
lockEntry: null);
var metadata = package.CreateMetadata().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
Assert.True(metadata.ContainsKey("packageManager"));
Assert.Equal("bun", metadata["packageManager"]);
Assert.True(metadata.ContainsKey("source"));
Assert.Equal("node_modules", metadata["source"]);
Assert.True(metadata.ContainsKey("path"));
Assert.Equal("node_modules/lodash", metadata["path"]);
}
[Fact]
public void CreateMetadata_AllFieldsSet_ReturnsAllKeys()
{
var lockEntry = new BunLockEntry
{
Name = "lodash",
Version = "4.17.21",
Resolved = "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
Integrity = "sha512-abc",
IsDev = true,
IsOptional = true,
IsPeer = true,
SourceType = "git",
GitCommit = "abc123",
Specifier = "github:lodash/lodash#abc123"
};
var package = BunPackage.FromPackageJson(
name: "lodash",
version: "4.17.21",
logicalPath: "node_modules/lodash",
realPath: "node_modules/.bun/lodash@4.17.21",
isPrivate: true,
lockEntry: lockEntry);
package.IsDirect = true;
package.IsPatched = true;
package.PatchFile = "patches/lodash.patch";
package.CustomRegistry = "https://npm.company.com/";
var metadata = package.CreateMetadata().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
Assert.Equal("true", metadata["dev"]);
Assert.Equal("true", metadata["direct"]);
Assert.Equal("true", metadata["optional"]);
Assert.Equal("true", metadata["peer"]);
Assert.Equal("true", metadata["private"]);
Assert.Equal("true", metadata["patched"]);
Assert.Equal("patches/lodash.patch", metadata["patchFile"]);
Assert.Equal("https://npm.company.com/", metadata["customRegistry"]);
Assert.Equal("abc123", metadata["gitCommit"]);
Assert.Equal("git", metadata["sourceType"]);
Assert.Equal("github:lodash/lodash#abc123", metadata["specifier"]);
}
[Fact]
public void CreateMetadata_SortedAlphabetically()
{
var lockEntry = new BunLockEntry
{
Name = "lodash",
Version = "4.17.21",
Resolved = "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
Integrity = "sha512-abc",
IsDev = true
};
var package = BunPackage.FromPackageJson(
name: "lodash",
version: "4.17.21",
logicalPath: "node_modules/lodash",
realPath: null,
isPrivate: false,
lockEntry: lockEntry);
package.IsDirect = true;
var keys = package.CreateMetadata().Select(kvp => kvp.Key).ToList();
// Verify keys are sorted alphabetically
var sortedKeys = keys.OrderBy(k => k, StringComparer.Ordinal).ToList();
Assert.Equal(sortedKeys, keys);
}
[Fact]
public void CreateMetadata_NormalizesPathSeparators()
{
var package = BunPackage.FromPackageJson(
name: "lodash",
version: "4.17.21",
logicalPath: "node_modules\\lodash",
realPath: "node_modules\\.bun\\lodash@4.17.21",
isPrivate: false,
lockEntry: null);
var metadata = package.CreateMetadata().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
Assert.Equal("node_modules/lodash", metadata["path"]);
Assert.Equal("node_modules/.bun/lodash@4.17.21", metadata["realPath"]);
}
[Fact]
public void CreateMetadata_MultipleOccurrences_JoinsWithSemicolon()
{
var package = BunPackage.FromPackageJson(
name: "lodash",
version: "4.17.21",
logicalPath: "node_modules/lodash",
realPath: null,
isPrivate: false,
lockEntry: null);
package.AddOccurrence("node_modules/lodash");
package.AddOccurrence("packages/app/node_modules/lodash");
var metadata = package.CreateMetadata().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
Assert.True(metadata.ContainsKey("occurrences"));
Assert.Contains(";", metadata["occurrences"]);
}
#endregion
#region CreateEvidence Tests
[Fact]
public void CreateEvidence_WithResolvedAndIntegrity_ReturnsAll()
{
var lockEntry = new BunLockEntry
{
Name = "lodash",
Version = "4.17.21",
Resolved = "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
Integrity = "sha512-abc123"
};
var package = BunPackage.FromPackageJson(
name: "lodash",
version: "4.17.21",
logicalPath: "node_modules/lodash",
realPath: null,
isPrivate: false,
lockEntry: lockEntry);
var evidence = package.CreateEvidence().ToList();
Assert.Equal(3, evidence.Count);
// File evidence
var fileEvidence = evidence.FirstOrDefault(e => e.Kind == LanguageEvidenceKind.File);
Assert.NotNull(fileEvidence);
Assert.Equal("node_modules", fileEvidence.Source);
Assert.Equal("node_modules/lodash/package.json", fileEvidence.Locator);
// Resolved evidence
var resolvedEvidence = evidence.FirstOrDefault(e => e.Source == "resolved");
Assert.NotNull(resolvedEvidence);
Assert.Equal(LanguageEvidenceKind.Metadata, resolvedEvidence.Kind);
Assert.Equal("bun.lock", resolvedEvidence.Locator);
Assert.Equal("https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", resolvedEvidence.Value);
// Integrity evidence
var integrityEvidence = evidence.FirstOrDefault(e => e.Source == "integrity");
Assert.NotNull(integrityEvidence);
Assert.Equal("sha512-abc123", integrityEvidence.Value);
}
[Fact]
public void CreateEvidence_NoLockEntry_ReturnsOnlyFileEvidence()
{
var package = BunPackage.FromPackageJson(
name: "lodash",
version: "4.17.21",
logicalPath: "node_modules/lodash",
realPath: null,
isPrivate: false,
lockEntry: null);
var evidence = package.CreateEvidence().ToList();
Assert.Single(evidence);
Assert.Equal(LanguageEvidenceKind.File, evidence[0].Kind);
}
#endregion
#region FromLockEntry Tests
[Fact]
public void FromLockEntry_CreatesPackageWithAllProperties()
{
var lockEntry = new BunLockEntry
{
Name = "ms",
Version = "2.1.3",
Resolved = "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
Integrity = "sha512-6FlzubTLZG3J2a",
IsDev = true,
IsOptional = false,
IsPeer = false,
SourceType = "npm",
Dependencies = new List<string> { "debug" }
};
var package = BunPackage.FromLockEntry(lockEntry, "bun.lock");
Assert.Equal("ms", package.Name);
Assert.Equal("2.1.3", package.Version);
Assert.Equal("pkg:npm/ms@2.1.3", package.Purl);
Assert.Equal("bun.lock", package.Source);
Assert.Equal("https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", package.Resolved);
Assert.Equal("sha512-6FlzubTLZG3J2a", package.Integrity);
Assert.True(package.IsDev);
Assert.False(package.IsOptional);
Assert.Equal("npm", package.SourceType);
Assert.Contains("debug", package.Dependencies);
}
#endregion
#region AddOccurrence Tests
[Fact]
public void AddOccurrence_AddsDuplicatePath_DoesNotDuplicate()
{
var package = BunPackage.FromPackageJson(
name: "lodash",
version: "4.17.21",
logicalPath: "node_modules/lodash",
realPath: null,
isPrivate: false,
lockEntry: null);
package.AddOccurrence("node_modules/lodash");
package.AddOccurrence("node_modules/lodash");
Assert.Single(package.OccurrencePaths);
}
[Fact]
public void AddOccurrence_AddsMultiplePaths_StoresAll()
{
var package = BunPackage.FromPackageJson(
name: "lodash",
version: "4.17.21",
logicalPath: "node_modules/lodash",
realPath: null,
isPrivate: false,
lockEntry: null);
package.AddOccurrence("node_modules/lodash");
package.AddOccurrence("packages/app/node_modules/lodash");
package.AddOccurrence("packages/lib/node_modules/lodash");
Assert.Equal(3, package.OccurrencePaths.Count);
}
#endregion
}

View File

@@ -0,0 +1,284 @@
using StellaOps.Scanner.Analyzers.Lang.Bun.Internal;
namespace StellaOps.Scanner.Analyzers.Lang.Bun.Tests.Parsers;
public sealed class BunWorkspaceHelperTests : IDisposable
{
private readonly string _tempDir;
public BunWorkspaceHelperTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"bun-workspace-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
#region ParseWorkspaceInfo Tests
[Fact]
public void ParseWorkspaceInfo_MissingPackageJson_ReturnsEmpty()
{
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
Assert.Empty(result.WorkspacePatterns);
Assert.Empty(result.WorkspacePaths);
Assert.Empty(result.DirectDependencies);
}
[Fact]
public void ParseWorkspaceInfo_NoWorkspaces_ReturnsEmptyPatterns()
{
var packageJson = """
{
"name": "my-project",
"dependencies": {
"lodash": "^4.17.21"
}
}
""";
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
Assert.Empty(result.WorkspacePatterns);
Assert.Single(result.DirectDependencies);
Assert.True(result.DirectDependencies.ContainsKey("lodash"));
}
[Fact]
public void ParseWorkspaceInfo_ArrayFormatWorkspaces_ReturnsPatterns()
{
var packageJson = """
{
"name": "my-project",
"workspaces": ["packages/*", "apps/*"]
}
""";
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
Assert.Equal(2, result.WorkspacePatterns.Count);
Assert.Contains("packages/*", result.WorkspacePatterns);
Assert.Contains("apps/*", result.WorkspacePatterns);
}
[Fact]
public void ParseWorkspaceInfo_ObjectFormatWorkspaces_ReturnsPatterns()
{
var packageJson = """
{
"name": "my-project",
"workspaces": {
"packages": ["packages/*", "apps/*"]
}
}
""";
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
Assert.Equal(2, result.WorkspacePatterns.Count);
}
[Fact]
public void ParseWorkspaceInfo_ResolvesWorkspacePaths()
{
var packageJson = """
{
"name": "my-project",
"workspaces": ["packages/*"]
}
""";
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
// Create workspace packages
var pkgADir = Path.Combine(_tempDir, "packages", "pkg-a");
Directory.CreateDirectory(pkgADir);
File.WriteAllText(Path.Combine(pkgADir, "package.json"), """{"name": "@my/pkg-a"}""");
var pkgBDir = Path.Combine(_tempDir, "packages", "pkg-b");
Directory.CreateDirectory(pkgBDir);
File.WriteAllText(Path.Combine(pkgBDir, "package.json"), """{"name": "@my/pkg-b"}""");
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
Assert.Equal(2, result.WorkspacePaths.Count);
}
[Fact]
public void ParseWorkspaceInfo_ParsesAllDependencyTypes()
{
var packageJson = """
{
"name": "my-project",
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"typescript": "^5.0.0"
},
"optionalDependencies": {
"fsevents": "^2.3.0"
},
"peerDependencies": {
"react": "^18.0.0"
}
}
""";
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
Assert.Equal(4, result.DirectDependencies.Count);
Assert.True(result.DirectDependencies.ContainsKey("lodash"));
Assert.Equal(BunWorkspaceHelper.DependencyType.Production, result.DirectDependencies["lodash"]);
Assert.True(result.DirectDependencies.ContainsKey("typescript"));
Assert.Equal(BunWorkspaceHelper.DependencyType.Dev, result.DirectDependencies["typescript"]);
Assert.True(result.DirectDependencies.ContainsKey("fsevents"));
Assert.Equal(BunWorkspaceHelper.DependencyType.Optional, result.DirectDependencies["fsevents"]);
Assert.True(result.DirectDependencies.ContainsKey("react"));
Assert.Equal(BunWorkspaceHelper.DependencyType.Peer, result.DirectDependencies["react"]);
}
[Fact]
public void ParseWorkspaceInfo_MergesDependencyFlags()
{
var packageJson = """
{
"name": "my-project",
"dependencies": {
"lodash": "^4.17.21"
},
"peerDependencies": {
"lodash": "^4.17.0"
}
}
""";
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
Assert.Single(result.DirectDependencies);
var depType = result.DirectDependencies["lodash"];
Assert.True((depType & BunWorkspaceHelper.DependencyType.Production) != 0);
Assert.True((depType & BunWorkspaceHelper.DependencyType.Peer) != 0);
}
[Fact]
public void ParseWorkspaceInfo_ParsesPatchedDependencies()
{
var packageJson = """
{
"name": "my-project",
"patchedDependencies": {
"lodash@4.17.21": "patches/lodash@4.17.21.patch"
}
}
""";
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
Assert.Single(result.PatchedDependencies);
Assert.True(result.PatchedDependencies.ContainsKey("lodash"));
Assert.Equal("patches/lodash@4.17.21.patch", result.PatchedDependencies["lodash"]);
}
[Fact]
public void ParseWorkspaceInfo_ScansPatchesDirectory()
{
var packageJson = """
{
"name": "my-project"
}
""";
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
// Create patches directory with patch files
var patchesDir = Path.Combine(_tempDir, "patches");
Directory.CreateDirectory(patchesDir);
File.WriteAllText(Path.Combine(patchesDir, "lodash@4.17.21.patch"), "diff content");
File.WriteAllText(Path.Combine(patchesDir, "@babel+core@7.24.0.patch"), "diff content");
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
Assert.Equal(2, result.PatchedDependencies.Count);
Assert.True(result.PatchedDependencies.ContainsKey("lodash"));
Assert.True(result.PatchedDependencies.ContainsKey("@babel+core"));
}
[Fact]
public void ParseWorkspaceInfo_ScansBunPatchesDirectory()
{
var packageJson = """
{
"name": "my-project"
}
""";
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
// Create .patches directory (Bun-specific)
var patchesDir = Path.Combine(_tempDir, ".patches");
Directory.CreateDirectory(patchesDir);
File.WriteAllText(Path.Combine(patchesDir, "ms@2.1.3.patch"), "diff content");
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
Assert.Single(result.PatchedDependencies);
Assert.True(result.PatchedDependencies.ContainsKey("ms"));
}
[Fact]
public void ParseWorkspaceInfo_MalformedJson_ReturnsEmpty()
{
File.WriteAllText(Path.Combine(_tempDir, "package.json"), "{ invalid json }");
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
Assert.Empty(result.DirectDependencies);
}
#endregion
#region IsDirect Tests
[Fact]
public void IsDirect_DirectDependency_ReturnsTrue()
{
var deps = new Dictionary<string, BunWorkspaceHelper.DependencyType>
{
["lodash"] = BunWorkspaceHelper.DependencyType.Production
};
var result = BunWorkspaceHelper.IsDirect("lodash", deps);
Assert.True(result);
}
[Fact]
public void IsDirect_TransitiveDependency_ReturnsFalse()
{
var deps = new Dictionary<string, BunWorkspaceHelper.DependencyType>
{
["lodash"] = BunWorkspaceHelper.DependencyType.Production
};
var result = BunWorkspaceHelper.IsDirect("ms", deps);
Assert.False(result);
}
#endregion
}