Skip to content

Commit 6748194

Browse files
authored
Fix package json deserialization 'engines' property (#1629)
1 parent 8bb4237 commit 6748194

File tree

3 files changed

+266
-0
lines changed

3 files changed

+266
-0
lines changed

src/Microsoft.ComponentDetection.Detectors/npm/Contracts/PackageLockV2Package.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ internal sealed record PackageLockV2Package
8080
public string? License { get; init; }
8181

8282
[JsonPropertyName("engines")]
83+
[JsonConverter(typeof(PackageJsonEnginesConverter))]
8384
public IDictionary<string, string>? Engines { get; init; }
8485

8586
[JsonPropertyName("dependencies")]

src/Microsoft.ComponentDetection.Detectors/npm/Contracts/PackageLockV3Package.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ internal sealed record PackageLockV3Package
8080
public string? License { get; init; }
8181

8282
[JsonPropertyName("engines")]
83+
[JsonConverter(typeof(PackageJsonEnginesConverter))]
8384
public IDictionary<string, string>? Engines { get; init; }
8485

8586
[JsonPropertyName("dependencies")]
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Tests.Npm.Contracts;
2+
3+
using System.Collections.Generic;
4+
using System.Text.Json;
5+
using AwesomeAssertions;
6+
using Microsoft.ComponentDetection.Detectors.Npm.Contracts;
7+
using Microsoft.VisualStudio.TestTools.UnitTesting;
8+
9+
[TestClass]
10+
[TestCategory("Governance/All")]
11+
[TestCategory("Governance/ComponentDetection")]
12+
public class PackageLockPackageEnginesTests
13+
{
14+
private static readonly JsonSerializerOptions Options = new()
15+
{
16+
PropertyNameCaseInsensitive = true,
17+
};
18+
19+
[TestMethod]
20+
public void PackageLockV2Package_ParsesEnginesAsObject()
21+
{
22+
var json = """
23+
{
24+
"version": "1.0.0",
25+
"resolved": "https://registry.npmjs.org/test/-/test-1.0.0.tgz",
26+
"integrity": "sha512-abc123",
27+
"engines": { "node": ">=14.0.0", "npm": ">=6.0.0" }
28+
}
29+
""";
30+
31+
var result = JsonSerializer.Deserialize<PackageLockV2Package>(json, Options);
32+
33+
result.Should().NotBeNull();
34+
result!.Engines.Should().NotBeNull();
35+
result.Engines.Should().HaveCount(2);
36+
result.Engines!["node"].Should().Be(">=14.0.0");
37+
result.Engines["npm"].Should().Be(">=6.0.0");
38+
}
39+
40+
[TestMethod]
41+
public void PackageLockV2Package_ParsesEnginesAsArray()
42+
{
43+
// Legacy format: engines as array (e.g., concat-stream package)
44+
var json = """
45+
{
46+
"version": "1.0.0",
47+
"resolved": "https://registry.npmjs.org/test/-/test-1.0.0.tgz",
48+
"integrity": "sha512-abc123",
49+
"engines": ["node >= 0.8"]
50+
}
51+
""";
52+
53+
var result = JsonSerializer.Deserialize<PackageLockV2Package>(json, Options);
54+
55+
result.Should().NotBeNull();
56+
result!.Engines.Should().NotBeNull();
57+
58+
// Array format returns empty dictionary since we can't map to key-value pairs
59+
result.Engines.Should().BeEmpty();
60+
}
61+
62+
[TestMethod]
63+
public void PackageLockV2Package_ParsesEnginesArrayWithVscode()
64+
{
65+
var json = """
66+
{
67+
"version": "1.0.0",
68+
"resolved": "https://registry.npmjs.org/test/-/test-1.0.0.tgz",
69+
"integrity": "sha512-abc123",
70+
"engines": ["vscode ^1.60.0", "node >= 14"]
71+
}
72+
""";
73+
74+
var result = JsonSerializer.Deserialize<PackageLockV2Package>(json, Options);
75+
76+
result.Should().NotBeNull();
77+
result!.Engines.Should().NotBeNull();
78+
result.Engines.Should().ContainKey("vscode");
79+
result.Engines!["vscode"].Should().Be("vscode ^1.60.0");
80+
}
81+
82+
[TestMethod]
83+
public void PackageLockV2Package_ParsesNullEngines()
84+
{
85+
var json = """
86+
{
87+
"version": "1.0.0",
88+
"resolved": "https://registry.npmjs.org/test/-/test-1.0.0.tgz",
89+
"integrity": "sha512-abc123",
90+
"engines": null
91+
}
92+
""";
93+
94+
var result = JsonSerializer.Deserialize<PackageLockV2Package>(json, Options);
95+
96+
result.Should().NotBeNull();
97+
result!.Engines.Should().BeNull();
98+
}
99+
100+
[TestMethod]
101+
public void PackageLockV2Package_ParsesMissingEngines()
102+
{
103+
var json = """
104+
{
105+
"version": "1.0.0",
106+
"resolved": "https://registry.npmjs.org/test/-/test-1.0.0.tgz",
107+
"integrity": "sha512-abc123"
108+
}
109+
""";
110+
111+
var result = JsonSerializer.Deserialize<PackageLockV2Package>(json, Options);
112+
113+
result.Should().NotBeNull();
114+
result!.Engines.Should().BeNull();
115+
}
116+
117+
[TestMethod]
118+
public void PackageLockV3Package_ParsesEnginesAsObject()
119+
{
120+
var json = """
121+
{
122+
"version": "1.0.0",
123+
"resolved": "https://registry.npmjs.org/test/-/test-1.0.0.tgz",
124+
"integrity": "sha512-abc123",
125+
"engines": { "node": ">=16.0.0", "npm": ">=8.0.0" }
126+
}
127+
""";
128+
129+
var result = JsonSerializer.Deserialize<PackageLockV3Package>(json, Options);
130+
131+
result.Should().NotBeNull();
132+
result!.Engines.Should().NotBeNull();
133+
result.Engines.Should().HaveCount(2);
134+
result.Engines!["node"].Should().Be(">=16.0.0");
135+
result.Engines["npm"].Should().Be(">=8.0.0");
136+
}
137+
138+
[TestMethod]
139+
public void PackageLockV3Package_ParsesEnginesAsArray()
140+
{
141+
// Legacy format: engines as array (e.g., concat-stream package)
142+
var json = """
143+
{
144+
"version": "1.0.0",
145+
"resolved": "https://registry.npmjs.org/test/-/test-1.0.0.tgz",
146+
"integrity": "sha512-abc123",
147+
"engines": ["node >= 0.8"]
148+
}
149+
""";
150+
151+
var result = JsonSerializer.Deserialize<PackageLockV3Package>(json, Options);
152+
153+
result.Should().NotBeNull();
154+
result!.Engines.Should().NotBeNull();
155+
156+
// Array format returns empty dictionary since we can't map to key-value pairs
157+
result.Engines.Should().BeEmpty();
158+
}
159+
160+
[TestMethod]
161+
public void PackageLockV3Package_ParsesEnginesArrayWithVscode()
162+
{
163+
var json = """
164+
{
165+
"version": "1.0.0",
166+
"resolved": "https://registry.npmjs.org/test/-/test-1.0.0.tgz",
167+
"integrity": "sha512-abc123",
168+
"engines": ["vscode ^1.60.0", "node >= 14"]
169+
}
170+
""";
171+
172+
var result = JsonSerializer.Deserialize<PackageLockV3Package>(json, Options);
173+
174+
result.Should().NotBeNull();
175+
result!.Engines.Should().NotBeNull();
176+
result.Engines.Should().ContainKey("vscode");
177+
result.Engines!["vscode"].Should().Be("vscode ^1.60.0");
178+
}
179+
180+
[TestMethod]
181+
public void PackageLockV3Package_ParsesNullEngines()
182+
{
183+
var json = """
184+
{
185+
"version": "1.0.0",
186+
"resolved": "https://registry.npmjs.org/test/-/test-1.0.0.tgz",
187+
"integrity": "sha512-abc123",
188+
"engines": null
189+
}
190+
""";
191+
192+
var result = JsonSerializer.Deserialize<PackageLockV3Package>(json, Options);
193+
194+
result.Should().NotBeNull();
195+
result!.Engines.Should().BeNull();
196+
}
197+
198+
[TestMethod]
199+
public void PackageLockV3Package_ParsesMissingEngines()
200+
{
201+
var json = """
202+
{
203+
"version": "1.0.0",
204+
"resolved": "https://registry.npmjs.org/test/-/test-1.0.0.tgz",
205+
"integrity": "sha512-abc123"
206+
}
207+
""";
208+
209+
var result = JsonSerializer.Deserialize<PackageLockV3Package>(json, Options);
210+
211+
result.Should().NotBeNull();
212+
result!.Engines.Should().BeNull();
213+
}
214+
215+
[TestMethod]
216+
public void PackageLockV2Package_CanSerializeEngines()
217+
{
218+
var package = new PackageLockV2Package
219+
{
220+
Version = "1.0.0",
221+
Resolved = "https://registry.npmjs.org/test/-/test-1.0.0.tgz",
222+
Integrity = "sha512-abc123",
223+
Engines = new Dictionary<string, string>
224+
{
225+
["node"] = ">=14.0.0",
226+
["npm"] = ">=6.0.0",
227+
},
228+
};
229+
230+
var json = JsonSerializer.Serialize(package, Options);
231+
var deserialized = JsonSerializer.Deserialize<PackageLockV2Package>(json, Options);
232+
233+
deserialized.Should().NotBeNull();
234+
deserialized!.Engines.Should().NotBeNull();
235+
deserialized.Engines.Should().HaveCount(2);
236+
deserialized.Engines!["node"].Should().Be(">=14.0.0");
237+
deserialized.Engines["npm"].Should().Be(">=6.0.0");
238+
}
239+
240+
[TestMethod]
241+
public void PackageLockV3Package_CanSerializeEngines()
242+
{
243+
var package = new PackageLockV3Package
244+
{
245+
Version = "1.0.0",
246+
Resolved = "https://registry.npmjs.org/test/-/test-1.0.0.tgz",
247+
Integrity = "sha512-abc123",
248+
Engines = new Dictionary<string, string>
249+
{
250+
["node"] = ">=16.0.0",
251+
["npm"] = ">=8.0.0",
252+
},
253+
};
254+
255+
var json = JsonSerializer.Serialize(package, Options);
256+
var deserialized = JsonSerializer.Deserialize<PackageLockV3Package>(json, Options);
257+
258+
deserialized.Should().NotBeNull();
259+
deserialized!.Engines.Should().NotBeNull();
260+
deserialized.Engines.Should().HaveCount(2);
261+
deserialized.Engines!["node"].Should().Be(">=16.0.0");
262+
deserialized.Engines["npm"].Should().Be(">=8.0.0");
263+
}
264+
}

0 commit comments

Comments
 (0)