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:
@@ -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=="]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[install.scopes]
|
||||
"@company" = "https://npm.company.com/"
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "custom-registry-fixture",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@company/internal-pkg": "^1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -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=="]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "deep-tree-fixture",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"my-git-pkg@1.0.0": ["git+https://github.com/user/my-git-pkg.git#abc123def456", null]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "git-dependencies-fixture",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"my-git-pkg": "github:user/my-git-pkg#v1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -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=="]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
--- a/index.js
|
||||
+++ b/index.js
|
||||
@@ -1 +1 @@
|
||||
-module.exports = require('./lodash');
|
||||
+module.exports = require('./lodash-patched');
|
||||
@@ -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=="]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "scoped-packages-fixture",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
"@types/node": "^20.11.0"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user