using StellaOps.Scanner.Analyzers.Lang.Node.Internal; using Xunit; namespace StellaOps.Scanner.Analyzers.Lang.Node.Tests.Node; public sealed class NodeLockDataTests : IDisposable { private readonly string _tempDir; public NodeLockDataTests() { _tempDir = Path.Combine(Path.GetTempPath(), "node-lock-tests-" + Guid.NewGuid().ToString("N")[..8]); Directory.CreateDirectory(_tempDir); } public void Dispose() { if (Directory.Exists(_tempDir)) { try { Directory.Delete(_tempDir, recursive: true); } catch { // Ignore cleanup failures in tests } } } #region LoadAsync Orchestration Tests [Fact] public async Task LoadAsync_NoLockfiles_ReturnsEmpty() { // No lockfiles, no package.json var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Empty(result.DeclaredPackages); } [Fact] public async Task LoadAsync_EmptyRootPath_DoesNotThrow_WhenCurrentDirectoryHasPackageJson() { var originalDirectory = Environment.CurrentDirectory; var tempDirectory = Path.Combine(Path.GetTempPath(), "node-lock-tests-cwd-" + Guid.NewGuid().ToString("N")[..8]); Directory.CreateDirectory(tempDirectory); try { await File.WriteAllTextAsync(Path.Combine(tempDirectory, "package.json"), """ { "name": "fixture", "version": "0.0.0" } """); Environment.CurrentDirectory = tempDirectory; var result = await NodeLockData.LoadAsync(string.Empty, TestContext.Current.CancellationToken); Assert.Empty(result.DeclaredPackages); } finally { Environment.CurrentDirectory = originalDirectory; Directory.Delete(tempDirectory, recursive: true); } } [Fact] public async Task LoadAsync_OnlyPackageJson_CreatesDeclaredOnlyEntries() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), """ { "name": "test", "version": "1.0.0", "dependencies": { "lodash": "^4.17.21" } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); var entry = result.DeclaredPackages.First(); Assert.Equal("lodash", entry.Name); Assert.Equal("^4.17.21", entry.Version); Assert.Equal("package.json", entry.Source); } [Fact] public async Task LoadAsync_ResultsAreSortedDeterministically() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), """ { "dependencies": { "zeta": "^1.0.0", "alpha": "^2.0.0", "beta": "^1.0.0" } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); var names = result.DeclaredPackages.Select(x => x.Name).ToArray(); Assert.Equal(["alpha", "beta", "zeta"], names); } [Fact] public async Task LoadAsync_PackageLockTakesPrecedence_OverDeclaredOnly() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), """ { "dependencies": { "lodash": "^4.17.0" } } """); await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "name": "test", "lockfileVersion": 3, "packages": { "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" } } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); var entry = result.DeclaredPackages.First(); Assert.Equal("lodash", entry.Name); Assert.Equal("4.17.21", entry.Version); Assert.Equal("package-lock.json", entry.Source); } [Fact] public async Task LoadAsync_CancellationToken_IsRespected() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "node_modules/test": { "version": "1.0.0" } } } """); var cts = new CancellationTokenSource(); cts.Cancel(); await Assert.ThrowsAsync(async () => await NodeLockData.LoadAsync(_tempDir, cts.Token)); } #endregion #region package-lock.json v3+ Parsing Tests [Fact] public async Task LoadPackageLockJson_V3Format_ParsesPackages() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "node_modules/express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", "integrity": "sha512-abc123" } } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); var entry = result.DeclaredPackages.First(); Assert.Equal("express", entry.Name); Assert.Equal("4.18.2", entry.Version); Assert.Equal("https://registry.npmjs.org/express/-/express-4.18.2.tgz", entry.Resolved); Assert.Equal("sha512-abc123", entry.Integrity); } [Fact] public async Task LoadPackageLockJson_V3Format_ExtractsNameFromPath() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "node_modules/express": { "version": "4.18.2" } } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); Assert.Equal("express", result.DeclaredPackages.First().Name); } [Fact] public async Task LoadPackageLockJson_V3Format_ScopedPackages() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "node_modules/@angular/core": { "version": "17.0.0" }, "node_modules/@types/node": { "version": "20.10.0" } } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Equal(2, result.DeclaredPackages.Count); Assert.Contains(result.DeclaredPackages, e => e.Name == "@angular/core" && e.Version == "17.0.0"); Assert.Contains(result.DeclaredPackages, e => e.Name == "@types/node" && e.Version == "20.10.0"); } [Fact] public async Task LoadPackageLockJson_V3Format_SkipsEntriesWithNoVersionOrResolvedOrIntegrity() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "": { "name": "test-project", "license": "MIT" }, "node_modules/valid": { "version": "1.0.0" } } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); Assert.Equal("valid", result.DeclaredPackages.First().Name); } [Fact] public async Task LoadPackageLockJson_V3Format_NestedNodeModules() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "node_modules/parent": { "version": "1.0.0" }, "node_modules/parent/node_modules/child": { "version": "2.0.0" } } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Equal(2, result.DeclaredPackages.Count); Assert.Contains(result.DeclaredPackages, e => e.Name == "parent"); Assert.Contains(result.DeclaredPackages, e => e.Name == "child"); Assert.True(result.TryGet("node_modules/parent/node_modules/child", "child", "2.0.0", out var entry)); Assert.NotNull(entry); Assert.Equal("2.0.0", entry!.Version); } [Fact] public async Task LoadPackageLockJson_V3Format_NestedNodeModules_ScopedChild() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "node_modules/parent": { "version": "1.0.0" }, "node_modules/parent/node_modules/@types/node": { "version": "20.10.0" } } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Equal(2, result.DeclaredPackages.Count); Assert.Contains(result.DeclaredPackages, e => e.Name == "parent"); Assert.Contains(result.DeclaredPackages, e => e.Name == "@types/node"); Assert.True(result.TryGet("node_modules/parent/node_modules/@types/node", "@types/node", "20.10.0", out var entry)); Assert.NotNull(entry); Assert.Equal("20.10.0", entry!.Version); } [Fact] public async Task LoadPackageLockJson_V3Format_ExplicitNameOverridesPath() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "node_modules/aliased": { "name": "actual-package", "version": "1.0.0" } } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); Assert.Equal("actual-package", result.DeclaredPackages.First().Name); } #endregion #region package-lock.json Legacy Parsing Tests [Fact] public async Task LoadPackageLockJson_LegacyFormat_ParsesDependencies() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 1, "dependencies": { "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-xyz" } } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); var entry = result.DeclaredPackages.First(); Assert.Equal("lodash", entry.Name); Assert.Equal("4.17.21", entry.Version); } [Fact] public async Task LoadPackageLockJson_LegacyFormat_NestedDependencies() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 1, "dependencies": { "parent": { "version": "1.0.0", "dependencies": { "child": { "version": "2.0.0" } } } } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Equal(2, result.DeclaredPackages.Count); Assert.Contains(result.DeclaredPackages, e => e.Name == "parent"); Assert.Contains(result.DeclaredPackages, e => e.Name == "child"); } [Fact] public async Task LoadPackageLockJson_LegacyFormat_ScopedPackages() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 1, "dependencies": { "@babel/core": { "version": "7.23.0" } } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); Assert.Equal("@babel/core", result.DeclaredPackages.First().Name); } [Fact] public async Task LoadPackageLockJson_MalformedJson_ContinuesGracefully() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { this is not valid json } """); await File.WriteAllTextAsync(Path.Combine(_tempDir, "yarn.lock"), """ lodash@^4.17.21: version "4.17.21" """); // Should continue with yarn.lock parsing var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); Assert.Equal("lodash", result.DeclaredPackages.First().Name); } #endregion #region yarn.lock Parsing Tests [Fact] public async Task LoadYarnLock_ParsesBasicEntry() { // Parser expects quoted values using ExtractQuotedValue await File.WriteAllTextAsync(Path.Combine(_tempDir, "yarn.lock"), @"# yarn lockfile v1 lodash@^4.17.21: version ""4.17.21"" resolved ""https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz"" integrity ""sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ"" "); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); var entry = result.DeclaredPackages.First(); Assert.Equal("lodash", entry.Name); Assert.Equal("4.17.21", entry.Version); Assert.StartsWith("https://registry.yarnpkg.com", entry.Resolved); Assert.StartsWith("sha512-", entry.Integrity); } [Fact] public async Task LoadYarnLock_ScopedPackages() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "yarn.lock"), @"""@babel/core@^7.23.0"": version ""7.23.0"" ""@types/node@^20.0.0"": version ""20.10.0"" "); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Equal(2, result.DeclaredPackages.Count); Assert.Contains(result.DeclaredPackages, e => e.Name == "@babel/core"); Assert.Contains(result.DeclaredPackages, e => e.Name == "@types/node"); } [Fact] public async Task LoadYarnLock_MultipleVersionConstraints() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "yarn.lock"), @"""lodash@^4.0.0, lodash@^4.17.0, lodash@^4.17.21"": version ""4.17.21"" "); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); Assert.Equal("lodash", result.DeclaredPackages.First().Name); } [Fact] public async Task LoadYarnLock_QuotedPackageKey() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "yarn.lock"), @"""express@^4.18.0"": version ""4.18.2"" "); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); Assert.Equal("express", result.DeclaredPackages.First().Name); } [Fact] public async Task LoadYarnLock_FlushesAtEOF() { // No trailing newline - should still parse (integrity must be quoted) await File.WriteAllTextAsync(Path.Combine(_tempDir, "yarn.lock"), "lodash@^4.17.21:\n version \"4.17.21\"\n resolved \"https://example.com/lodash.tgz\"\n integrity \"sha512-abc\""); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); Assert.Equal("lodash", result.DeclaredPackages.First().Name); } [Fact] public async Task LoadYarnLock_MultiplePackages() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "yarn.lock"), @"express@^4.18.0: version ""4.18.2"" lodash@^4.17.21: version ""4.17.21"" axios@^1.6.0: version ""1.6.2"" "); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Equal(3, result.DeclaredPackages.Count); Assert.Contains(result.DeclaredPackages, e => e.Name == "express"); Assert.Contains(result.DeclaredPackages, e => e.Name == "lodash"); Assert.Contains(result.DeclaredPackages, e => e.Name == "axios"); } [Fact] public async Task LoadYarnLock_HandlesUnusualPackageKeys() { // Keys without @ separator are kept as-is as the package name await File.WriteAllTextAsync(Path.Combine(_tempDir, "yarn.lock"), @"""@scope/package@^1.0.0"": version ""1.0.0"" valid@^2.0.0: version ""2.0.0"" "); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Equal(2, result.DeclaredPackages.Count); Assert.Contains(result.DeclaredPackages, e => e.Name == "@scope/package"); Assert.Contains(result.DeclaredPackages, e => e.Name == "valid"); } [Fact] public async Task LoadYarnLock_BerryFormat_ParsesResolutionChecksum_AndSkipsMetadata() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "yarn.lock"), """ __metadata: version: 6 "lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: 10c0deadbeef """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); Assert.DoesNotContain(result.DeclaredPackages, e => e.Name == "__metadata"); var entry = result.DeclaredPackages.Single(); Assert.Equal("lodash", entry.Name); Assert.Equal("4.17.21", entry.Version); Assert.Equal("lodash@npm:4.17.21", entry.Resolved); Assert.Equal("checksum:10c0deadbeef", entry.Integrity); Assert.True(result.TryGet("", "lodash", "4.17.21", out var byVersion)); Assert.NotNull(byVersion); Assert.Equal("lodash@npm:^4.17.21", byVersion!.Locator); } #endregion #region pnpm-lock.yaml Parsing Tests [Fact] public async Task LoadPnpmLock_ParsesBasicEntry() { // pnpm-lock.yaml format: package keys start with " /" and use /package/version format // Version line is required for entry to be added to DeclaredPackages var content = "lockfileVersion: '6.0'\n" + "packages:\n" + " /lodash/4.17.21:\n" + " version: 4.17.21\n" + " resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ}\n"; await File.WriteAllTextAsync(Path.Combine(_tempDir, "pnpm-lock.yaml"), content); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); var entry = result.DeclaredPackages.First(); Assert.Equal("lodash", entry.Name); Assert.Equal("4.17.21", entry.Version); Assert.StartsWith("sha512-", entry.Integrity); } [Fact] public async Task LoadPnpmLock_ScopedPackages() { // Scoped packages use /@scope/package/version format var content = "lockfileVersion: '6.0'\n" + "packages:\n" + " /@angular/core/17.0.0:\n" + " version: 17.0.0\n" + " resolution: {integrity: sha512-abc123}\n" + " /@types/node/20.10.0:\n" + " version: 20.10.0\n" + " resolution: {integrity: sha512-def456}\n"; await File.WriteAllTextAsync(Path.Combine(_tempDir, "pnpm-lock.yaml"), content); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Equal(2, result.DeclaredPackages.Count); Assert.Contains(result.DeclaredPackages, e => e.Name == "@angular/core"); Assert.Contains(result.DeclaredPackages, e => e.Name == "@types/node"); } [Fact] public async Task LoadPnpmLock_ExtractsVersion() { var content = "lockfileVersion: '6.0'\n" + "packages:\n" + " /express/4.18.2:\n" + " version: 4.18.2\n" + " resolution: {integrity: sha512-xyz}\n"; await File.WriteAllTextAsync(Path.Combine(_tempDir, "pnpm-lock.yaml"), content); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); Assert.Equal("4.18.2", result.DeclaredPackages.First().Version); } [Fact] public async Task LoadPnpmLock_WhenVersionLineMissing_UsesVersionFromKey() { var content = "lockfileVersion: '6.0'\n" + "packages:\n" + " /express/4.18.2:\n" + " resolution: {integrity: sha512-xyz}\n"; await File.WriteAllTextAsync(Path.Combine(_tempDir, "pnpm-lock.yaml"), content); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); var entry = result.DeclaredPackages.Single(); Assert.Equal("express", entry.Name); Assert.Equal("4.18.2", entry.Version); Assert.Equal("sha512-xyz", entry.Integrity); } [Fact] public async Task LoadPnpmLock_ExtractsTarball() { var content = "lockfileVersion: '6.0'\n" + "packages:\n" + " /lodash/4.17.21:\n" + " version: 4.17.21\n" + " resolution: {tarball: https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz, integrity: sha512-abc}\n"; await File.WriteAllTextAsync(Path.Combine(_tempDir, "pnpm-lock.yaml"), content); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); Assert.Contains("lodash-4.17.21.tgz", result.DeclaredPackages.First().Resolved); } [Fact] public async Task LoadPnpmLock_IntegrityMissingReason_File() { var content = "lockfileVersion: '6.0'\n" + "packages:\n" + " /local-file/1.0.0:\n" + " resolution: {tarball: file:../local-file-1.0.0.tgz}\n"; await File.WriteAllTextAsync(Path.Combine(_tempDir, "pnpm-lock.yaml"), content); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); var entry = result.DeclaredPackages.Single(); Assert.Equal("local-file", entry.Name); Assert.True(entry.IntegrityMissing); Assert.Equal("file", entry.IntegrityMissingReason); Assert.StartsWith("file:", entry.Resolved, StringComparison.Ordinal); } [Fact] public async Task LoadPnpmLock_SnapshotsSection_IsParsed() { var content = "lockfileVersion: '9.0'\n" + "snapshots:\n" + " /snap-only/1.0.0:\n" + " resolution: {integrity: sha512-snap}\n"; await File.WriteAllTextAsync(Path.Combine(_tempDir, "pnpm-lock.yaml"), content); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); var entry = result.DeclaredPackages.Single(); Assert.Equal("snap-only", entry.Name); Assert.Equal("1.0.0", entry.Version); Assert.Equal("sha512-snap", entry.Integrity); } [Fact] public async Task LoadPnpmLock_SeparateIntegrityLine() { var content = "lockfileVersion: '6.0'\n" + "packages:\n" + " /express/4.18.2:\n" + " version: 4.18.2\n" + " integrity: sha512-separate-line-integrity\n"; await File.WriteAllTextAsync(Path.Combine(_tempDir, "pnpm-lock.yaml"), content); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); Assert.Equal("sha512-separate-line-integrity", result.DeclaredPackages.First().Integrity); } [Fact] public async Task LoadPnpmLock_PackagesWithoutIntegrity_AreKeptAndMarked() { var content = "lockfileVersion: '6.0'\n" + "packages:\n" + " /no-integrity/1.0.0:\n" + " version: 1.0.0\n" + " /has-integrity/2.0.0:\n" + " version: 2.0.0\n" + " resolution: {integrity: sha512-valid}\n"; await File.WriteAllTextAsync(Path.Combine(_tempDir, "pnpm-lock.yaml"), content); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Equal(2, result.DeclaredPackages.Count); var noIntegrity = result.DeclaredPackages.Single(e => e.Name == "no-integrity"); Assert.Equal("1.0.0", noIntegrity.Version); Assert.Null(noIntegrity.Integrity); Assert.True(noIntegrity.IntegrityMissing); Assert.Equal("missing", noIntegrity.IntegrityMissingReason); var hasIntegrity = result.DeclaredPackages.Single(e => e.Name == "has-integrity"); Assert.Equal("2.0.0", hasIntegrity.Version); Assert.Equal("sha512-valid", hasIntegrity.Integrity); Assert.False(hasIntegrity.IntegrityMissing); Assert.Null(hasIntegrity.IntegrityMissingReason); } [Fact] public async Task LoadPnpmLock_MultiplePackages() { var content = "lockfileVersion: '6.0'\n" + "packages:\n" + " /express/4.18.2:\n" + " version: 4.18.2\n" + " resolution: {integrity: sha512-express}\n" + " /lodash/4.17.21:\n" + " version: 4.17.21\n" + " resolution: {integrity: sha512-lodash}\n" + " /axios/1.6.2:\n" + " version: 1.6.2\n" + " resolution: {integrity: sha512-axios}\n"; await File.WriteAllTextAsync(Path.Combine(_tempDir, "pnpm-lock.yaml"), content); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Equal(3, result.DeclaredPackages.Count); } #endregion #region TryGet Tests [Fact] public async Task TryGet_ByPath_ReturnsEntry() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "node_modules/lodash": { "version": "4.17.21" } } } """); var lockData = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.True(lockData.TryGet("node_modules/lodash", "lodash", out var entry)); Assert.NotNull(entry); Assert.Equal("lodash", entry!.Name); } [Fact] public async Task TryGet_ByName_ReturnsEntry() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "yarn.lock"), """ lodash@^4.17.21: version "4.17.21" """); var lockData = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.True(lockData.TryGet("", "lodash", out var entry)); Assert.NotNull(entry); Assert.Equal("lodash", entry!.Name); } [Fact] public async Task TryGet_NotFound_ReturnsFalse() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "node_modules/lodash": { "version": "4.17.21" } } } """); var lockData = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.False(lockData.TryGet("node_modules/express", "express", out var entry)); Assert.Null(entry); } [Fact] public async Task TryGet_NormalizesBackslashes() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "node_modules/lodash": { "version": "4.17.21" } } } """); var lockData = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.True(lockData.TryGet("node_modules\\lodash", "lodash", out var entry)); Assert.NotNull(entry); } #endregion #region DependencyIndex Integration Tests [Fact] public async Task LoadAsync_SetsScope_FromPackageJson() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), """ { "dependencies": { "lodash": "^4.17.21" }, "devDependencies": { "jest": "^29.0.0" }, "optionalDependencies": { "fsevents": "^2.3.0" } } """); await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "node_modules/lodash": { "version": "4.17.21" }, "node_modules/jest": { "version": "29.7.0" }, "node_modules/fsevents": { "version": "2.3.3" } } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); var lodash = result.DeclaredPackages.First(e => e.Name == "lodash"); Assert.Equal(NodeDependencyScope.Production, lodash.Scope); Assert.False(lodash.IsOptional); var jest = result.DeclaredPackages.First(e => e.Name == "jest"); Assert.Equal(NodeDependencyScope.Development, jest.Scope); Assert.False(jest.IsOptional); var fsevents = result.DeclaredPackages.First(e => e.Name == "fsevents"); Assert.Equal(NodeDependencyScope.Optional, fsevents.Scope); Assert.True(fsevents.IsOptional); } [Fact] public async Task LoadAsync_DependencyIndex_IsAccessible() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), """ { "dependencies": { "express": "^4.18.0" } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.True(result.DependencyIndex.TryGetScope("express", out var scope)); Assert.Equal(NodeDependencyScope.Production, scope); } #endregion #region Edge Cases [Fact] public async Task LoadAsync_EmptyPackageLock_ReturnsEmpty() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": {} } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Empty(result.DeclaredPackages); } [Fact] public async Task LoadAsync_AllThreeLockfiles_MergesCorrectly() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "node_modules/from-npm": { "version": "1.0.0" } } } """); await File.WriteAllTextAsync(Path.Combine(_tempDir, "yarn.lock"), "from-yarn@^2.0.0:\n version \"2.0.0\"\n"); var pnpmContent = "lockfileVersion: '6.0'\n" + "packages:\n" + " /from-pnpm/3.0.0:\n" + " version: 3.0.0\n" + " resolution: {integrity: sha512-pnpm}\n"; await File.WriteAllTextAsync(Path.Combine(_tempDir, "pnpm-lock.yaml"), pnpmContent); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Equal(3, result.DeclaredPackages.Count); Assert.Contains(result.DeclaredPackages, e => e.Name == "from-npm"); Assert.Contains(result.DeclaredPackages, e => e.Name == "from-yarn"); Assert.Contains(result.DeclaredPackages, e => e.Name == "from-pnpm"); } [Fact] public async Task LoadAsync_PathWithLeadingDotSlash_Normalized() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "./node_modules/lodash": { "version": "4.17.21" } } } """); var lockData = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.True(lockData.TryGet("node_modules/lodash", "lodash", out var entry)); Assert.NotNull(entry); } [Fact] public async Task LoadAsync_DuplicatePackages_BothVersionsKeptSeparately() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "node_modules/lodash": { "version": "4.17.21" } } } """); await File.WriteAllTextAsync(Path.Combine(_tempDir, "yarn.lock"), """ lodash@^4.0.0: version "4.0.0" """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); // Both entries are kept in DeclaredPackages with different version keys Assert.Equal(2, result.DeclaredPackages.Count(e => e.Name == "lodash")); Assert.Contains(result.DeclaredPackages, e => e.Name == "lodash" && e.Version == "4.17.21"); Assert.Contains(result.DeclaredPackages, e => e.Name == "lodash" && e.Version == "4.0.0"); // For TryGet lookups by name, yarn.lock overwrites the byName dictionary (loaded second) Assert.True(result.TryGet("", "lodash", out var byNameEntry)); Assert.Equal("4.0.0", byNameEntry!.Version); Assert.True(result.TryGet("", "lodash", "4.17.21", out var byVersionEntry)); Assert.Equal("4.17.21", byVersionEntry!.Version); Assert.True(result.TryGet("", "lodash", "4.0.0", out var byVersionEntry2)); Assert.Equal("4.0.0", byVersionEntry2!.Version); // For TryGet lookups by path, package-lock.json entry is found Assert.True(result.TryGet("node_modules/lodash", "", out var byPathEntry)); Assert.Equal("4.17.21", byPathEntry!.Version); } [Fact] public async Task LoadAsync_UnicodePackageNames() { await File.WriteAllTextAsync(Path.Combine(_tempDir, "package-lock.json"), """ { "lockfileVersion": 3, "packages": { "node_modules/日本語": { "version": "1.0.0" } } } """); var result = await NodeLockData.LoadAsync(_tempDir, TestContext.Current.CancellationToken); Assert.Single(result.DeclaredPackages); Assert.Equal("日本語", result.DeclaredPackages.First().Name); } #endregion #region Helper Method Tests [Theory] [InlineData(" version \"1.0.0\"", "1.0.0")] [InlineData("version \"2.0.0\"", "2.0.0")] [InlineData("resolved \"https://example.com/pkg.tgz\"", "https://example.com/pkg.tgz")] [InlineData("no quotes here", null)] [InlineData("\"single quote\"", "single quote")] [InlineData("value \"\"", "")] public void ExtractQuotedValue_Scenarios(string input, string? expected) { // ExtractQuotedValue is private, but we can test it indirectly through yarn.lock parsing // For now, we'll just document the expected behavior in these theories Assert.True(true); // Placeholder - behavior tested through LoadYarnLock tests } [Theory] [InlineData("lodash@^4.17.21", "lodash")] [InlineData("\"lodash@^4.17.21\"", "lodash")] [InlineData("@babel/core@^7.23.0", "@babel/core")] [InlineData("lodash@^4.0.0, lodash@^4.17.0", "lodash")] public void ExtractPackageNameFromYarnKey_Scenarios(string key, string expectedName) { // Tested indirectly through LoadYarnLock tests Assert.True(true); // Placeholder } [Theory] [InlineData("/lodash@4.17.21", "lodash")] [InlineData("@angular/core@17.0.0", "@angular/core")] [InlineData("/@types/node@20.10.0", "@types/node")] [InlineData("express@4.18.2", "express")] public void ExtractNameFromPnpmKey_Scenarios(string key, string expectedName) { // Tested indirectly through LoadPnpmLock tests Assert.True(true); // Placeholder } [Theory] [InlineData("node_modules/lodash", "lodash")] [InlineData("node_modules/@angular/core", "@angular/core")] [InlineData("node_modules/parent/node_modules/child", "child")] [InlineData("", "")] [InlineData("./node_modules/express", "express")] public void ExtractNameFromPath_Scenarios(string path, string expectedName) { // Tested indirectly through LoadPackageLockJson tests Assert.True(true); // Placeholder } [Theory] [InlineData("node_modules/lodash", "node_modules/lodash")] [InlineData("node_modules\\lodash", "node_modules/lodash")] [InlineData("./node_modules/lodash", "node_modules/lodash")] [InlineData(".\\node_modules\\lodash", "node_modules/lodash")] [InlineData("", "")] public void NormalizeLockPath_Scenarios(string input, string expected) { // Tested indirectly through TryGet tests Assert.True(true); // Placeholder } #endregion }