dependabot-nuget 0.294.0 → 0.295.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97e76c69b2fed1672ebe1e9b3b890e8f445139f26729895c72a196d49cd43d63
4
- data.tar.gz: 10ef8cb4d30c84e9e8f51fdc5bf4f529b451936ad8a34e905e69fbaa0b813cac
3
+ metadata.gz: 8ac3a95f5211adff79f8e7954308a4d3656b63c8be20b70a7b6f129f04402881
4
+ data.tar.gz: a79771c0cc6c0a08bfaa2a7cef8253902c7e8b92b868568c231064098ba77a1c
5
5
  SHA512:
6
- metadata.gz: 73cdefd4ef762621a7142a040a747011ee4a390635379ee311deccaf6664062834a8e809b6203816e81f8ebb24ed10f68d04975e9bcde40e5ffd2d3720779c68
7
- data.tar.gz: a8cde74fdb1b400ced101cac7132bb32c8dc6b3d36ff6c249908e33e9e8c719e63999c02982ba2bf7704b18007be0daac14dcdd6c39ba74c4a8c38ccad2ae11e
6
+ metadata.gz: 800097bc4856927e983da508d16b4e6d0fd75cf40b5ca5ed25901fbb467f0f99e8f8dc724341c0da6cc6126303c6437a5bd8c7521132c3363ed752181402ee1b
7
+ data.tar.gz: c0a1675e7cdf06ec0314d58092de09d633fa46892ae8f01c20ec74553d756265fda431434babb6eb140184b3380b78576f6309c1ec010f41e0e09e6ad6b28adb
@@ -262,7 +262,8 @@ public partial class DiscoveryWorker : IDiscoveryWorker
262
262
  }
263
263
  }
264
264
 
265
- return expandedProjects.ToImmutableArray();
265
+ var result = expandedProjects.OrderBy(p => p).ToImmutableArray();
266
+ return result;
266
267
  }
267
268
 
268
269
  private static IEnumerable<string> ExpandItemGroupFilesFromProject(string projectPath, params string[] itemTypes)
@@ -114,7 +114,7 @@ internal static class SdkProjectDiscovery
114
114
  var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(args, startingProjectDirectory, experimentsManager);
115
115
  return (exitCode, stdOut, stdErr);
116
116
  }, logger, retainMSBuildSdks: true);
117
- MSBuildHelper.ThrowOnUnauthenticatedFeed(stdOut);
117
+ MSBuildHelper.ThrowOnError(stdOut);
118
118
  if (stdOut.Contains("""error MSB4057: The target "GenerateBuildDependencyFile" does not exist in the project."""))
119
119
  {
120
120
  // this can happen if it's a non-SDK-style project; totally normal, not worth examining the binlog
@@ -4,6 +4,8 @@ using System.Text;
4
4
  using System.Text.Json;
5
5
  using System.Text.Json.Serialization;
6
6
 
7
+ using Microsoft.Extensions.FileSystemGlobbing;
8
+
7
9
  using NuGet.Versioning;
8
10
 
9
11
  using NuGetUpdater.Core.Analyze;
@@ -115,161 +117,145 @@ public class RunWorker
115
117
  await _apiHandler.UpdateDependencyList(discoveredUpdatedDependencies);
116
118
 
117
119
  // TODO: pull out relevant dependencies, then check each for updates and track the changes
118
- // TODO: for each top-level dependency, _or_ specific dependency (if security, use transitive)
119
120
  var originalDependencyFileContents = new Dictionary<string, string>();
120
121
  var actualUpdatedDependencies = new List<ReportedDependency>();
121
- if (job.AllowedUpdates.Any(a => a.UpdateType == UpdateType.All))
122
+ await _apiHandler.IncrementMetric(new()
122
123
  {
123
- await _apiHandler.IncrementMetric(new()
124
- {
125
- Metric = "updater.started",
126
- Tags = { ["operation"] = "group_update_all_versions" },
127
- });
124
+ Metric = "updater.started",
125
+ Tags = { ["operation"] = "group_update_all_versions" },
126
+ });
127
+
128
+ // track original contents for later handling
129
+ async Task TrackOriginalContentsAsync(string directory, string fileName)
130
+ {
131
+ var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath();
132
+ var localFullPath = Path.Join(repoContentsPath.FullName, repoFullPath);
133
+ var content = await File.ReadAllTextAsync(localFullPath);
134
+ originalDependencyFileContents[repoFullPath] = content;
135
+ }
128
136
 
129
- // track original contents for later handling
130
- async Task TrackOriginalContentsAsync(string directory, string fileName)
137
+ foreach (var project in discoveryResult.Projects)
138
+ {
139
+ var projectDirectory = Path.GetDirectoryName(project.FilePath);
140
+ await TrackOriginalContentsAsync(discoveryResult.Path, project.FilePath);
141
+ foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles))
131
142
  {
132
- var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath();
133
- var localFullPath = Path.Join(repoContentsPath.FullName, repoFullPath);
134
- var content = await File.ReadAllTextAsync(localFullPath);
135
- originalDependencyFileContents[repoFullPath] = content;
143
+ var extraFilePath = Path.Join(projectDirectory, extraFile);
144
+ await TrackOriginalContentsAsync(discoveryResult.Path, extraFilePath);
136
145
  }
146
+ // TODO: include global.json, etc.
147
+ }
137
148
 
138
- foreach (var project in discoveryResult.Projects)
149
+ // do update
150
+ _logger.Info($"Running update in directory {repoDirectory}");
151
+ foreach (var project in discoveryResult.Projects)
152
+ {
153
+ foreach (var dependency in project.Dependencies)
139
154
  {
140
- var projectDirectory = Path.GetDirectoryName(project.FilePath);
141
- await TrackOriginalContentsAsync(discoveryResult.Path, project.FilePath);
142
- foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles))
155
+ if (!IsUpdateAllowed(job, dependency))
143
156
  {
144
- var extraFilePath = Path.Join(projectDirectory, extraFile);
145
- await TrackOriginalContentsAsync(discoveryResult.Path, extraFilePath);
157
+ continue;
146
158
  }
147
- // TODO: include global.json, etc.
148
- }
149
159
 
150
- // do update
151
- _logger.Info($"Running update in directory {repoDirectory}");
152
- foreach (var project in discoveryResult.Projects)
153
- {
154
- foreach (var dependency in project.Dependencies.Where(d => !d.IsTransitive))
160
+ var dependencyInfo = GetDependencyInfo(job, dependency);
161
+ var analysisResult = await _analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo);
162
+ // TODO: log analysisResult
163
+ if (analysisResult.CanUpdate)
155
164
  {
156
- if (dependency.Name == "Microsoft.NET.Sdk")
157
- {
158
- // this can't be updated
159
- // TODO: pull this out of discovery?
160
- continue;
161
- }
162
-
163
- if (dependency.Version is null)
164
- {
165
- // if we don't know the version, there's nothing we can do
166
- continue;
167
- }
165
+ var dependencyLocation = Path.Join(discoveryResult.Path, project.FilePath).FullyNormalizedRootedPath();
168
166
 
169
- var dependencyInfo = GetDependencyInfo(job, dependency);
170
- var analysisResult = await _analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo);
171
- // TODO: log analysisResult
172
- if (analysisResult.CanUpdate)
167
+ // TODO: this is inefficient, but not likely causing a bottleneck
168
+ var previousDependency = discoveredUpdatedDependencies.Dependencies
169
+ .Single(d => d.Name == dependency.Name && d.Requirements.Single().File == dependencyLocation);
170
+ var updatedDependency = new ReportedDependency()
173
171
  {
174
- var dependencyLocation = Path.Join(discoveryResult.Path, project.FilePath).FullyNormalizedRootedPath();
175
-
176
- // TODO: this is inefficient, but not likely causing a bottleneck
177
- var previousDependency = discoveredUpdatedDependencies.Dependencies
178
- .Single(d => d.Name == dependency.Name && d.Requirements.Single().File == dependencyLocation);
179
- var updatedDependency = new ReportedDependency()
180
- {
181
- Name = dependency.Name,
182
- Version = analysisResult.UpdatedVersion,
183
- Requirements =
184
- [
185
- new ReportedRequirement()
186
- {
187
- File = dependencyLocation,
188
- Requirement = analysisResult.UpdatedVersion,
189
- Groups = previousDependency.Requirements.Single().Groups,
190
- Source = new RequirementSource()
191
- {
192
- SourceUrl = analysisResult.UpdatedDependencies.FirstOrDefault(d => d.Name == dependency.Name)?.InfoUrl,
193
- },
194
- }
195
- ],
196
- PreviousVersion = dependency.Version,
197
- PreviousRequirements = previousDependency.Requirements,
198
- };
199
-
200
- var dependencyFilePath = Path.Join(discoveryResult.Path, project.FilePath).FullyNormalizedRootedPath();
201
- var updateResult = await _updaterWorker.RunAsync(repoContentsPath.FullName, dependencyFilePath, dependency.Name, dependency.Version!, analysisResult.UpdatedVersion, isTransitive: false);
202
- // TODO: need to report if anything was actually updated
203
- if (updateResult.Error is null)
204
- {
205
- if (dependencyLocation != dependencyFilePath)
172
+ Name = dependency.Name,
173
+ Version = analysisResult.UpdatedVersion,
174
+ Requirements =
175
+ [
176
+ new ReportedRequirement()
206
177
  {
207
- updatedDependency.Requirements.All(r => r.File == dependencyFilePath);
178
+ File = dependencyLocation,
179
+ Requirement = analysisResult.UpdatedVersion,
180
+ Groups = previousDependency.Requirements.Single().Groups,
181
+ Source = new RequirementSource()
182
+ {
183
+ SourceUrl = analysisResult.UpdatedDependencies.FirstOrDefault(d => d.Name == dependency.Name)?.InfoUrl,
184
+ },
208
185
  }
186
+ ],
187
+ PreviousVersion = dependency.Version,
188
+ PreviousRequirements = previousDependency.Requirements,
189
+ };
209
190
 
210
- actualUpdatedDependencies.Add(updatedDependency);
191
+ var dependencyFilePath = Path.Join(discoveryResult.Path, project.FilePath).FullyNormalizedRootedPath();
192
+ var updateResult = await _updaterWorker.RunAsync(repoContentsPath.FullName, dependencyFilePath, dependency.Name, dependency.Version!, analysisResult.UpdatedVersion, isTransitive: dependency.IsTransitive);
193
+ // TODO: need to report if anything was actually updated
194
+ if (updateResult.Error is null)
195
+ {
196
+ if (dependencyLocation != dependencyFilePath)
197
+ {
198
+ updatedDependency.Requirements.All(r => r.File == dependencyFilePath);
211
199
  }
200
+
201
+ actualUpdatedDependencies.Add(updatedDependency);
212
202
  }
213
203
  }
214
204
  }
205
+ }
215
206
 
216
- // create PR - we need to manually check file contents; we can't easily use `git status` in tests
217
- var updatedDependencyFiles = new Dictionary<string, DependencyFile>();
218
- async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName)
207
+ // create PR - we need to manually check file contents; we can't easily use `git status` in tests
208
+ var updatedDependencyFiles = new Dictionary<string, DependencyFile>();
209
+ async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName)
210
+ {
211
+ var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath();
212
+ var localFullPath = Path.GetFullPath(Path.Join(repoContentsPath.FullName, repoFullPath));
213
+ var originalContent = originalDependencyFileContents[repoFullPath];
214
+ var updatedContent = await File.ReadAllTextAsync(localFullPath);
215
+ if (updatedContent != originalContent)
219
216
  {
220
- var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath();
221
- var localFullPath = Path.GetFullPath(Path.Join(repoContentsPath.FullName, repoFullPath));
222
- var originalContent = originalDependencyFileContents[repoFullPath];
223
- var updatedContent = await File.ReadAllTextAsync(localFullPath);
224
- if (updatedContent != originalContent)
217
+ updatedDependencyFiles[localFullPath] = new DependencyFile()
225
218
  {
226
- updatedDependencyFiles[localFullPath] = new DependencyFile()
227
- {
228
- Name = Path.GetFileName(repoFullPath),
229
- Directory = Path.GetDirectoryName(repoFullPath)!.NormalizePathToUnix(),
230
- Content = updatedContent,
231
- };
232
- }
219
+ Name = Path.GetFileName(repoFullPath),
220
+ Directory = Path.GetDirectoryName(repoFullPath)!.NormalizePathToUnix(),
221
+ Content = updatedContent,
222
+ };
233
223
  }
224
+ }
234
225
 
235
- foreach (var project in discoveryResult.Projects)
226
+ foreach (var project in discoveryResult.Projects)
227
+ {
228
+ await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, project.FilePath);
229
+ var projectDirectory = Path.GetDirectoryName(project.FilePath);
230
+ foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles))
236
231
  {
237
- await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, project.FilePath);
238
- var projectDirectory = Path.GetDirectoryName(project.FilePath);
239
- foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles))
240
- {
241
- var extraFilePath = Path.Join(projectDirectory, extraFile);
242
- await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, extraFilePath);
243
- }
244
- // TODO: handle global.json, etc.
232
+ var extraFilePath = Path.Join(projectDirectory, extraFile);
233
+ await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, extraFilePath);
245
234
  }
235
+ // TODO: handle global.json, etc.
236
+ }
246
237
 
247
- if (updatedDependencyFiles.Count > 0)
248
- {
249
- var updatedDependencyFileList = updatedDependencyFiles
250
- .OrderBy(kvp => kvp.Key)
251
- .Select(kvp => kvp.Value)
252
- .ToArray();
253
- var createPullRequest = new CreatePullRequest()
254
- {
255
- Dependencies = actualUpdatedDependencies.ToArray(),
256
- UpdatedDependencyFiles = updatedDependencyFileList,
257
- BaseCommitSha = baseCommitSha,
258
- CommitMessage = "TODO: message",
259
- PrTitle = "TODO: title",
260
- PrBody = "TODO: body",
261
- };
262
- await _apiHandler.CreatePullRequest(createPullRequest);
263
- // TODO: log updated dependencies to console
264
- }
265
- else
238
+ if (updatedDependencyFiles.Count > 0)
239
+ {
240
+ var updatedDependencyFileList = updatedDependencyFiles
241
+ .OrderBy(kvp => kvp.Key)
242
+ .Select(kvp => kvp.Value)
243
+ .ToArray();
244
+ var createPullRequest = new CreatePullRequest()
266
245
  {
267
- // TODO: log or throw if nothing was updated, but was expected to be
268
- }
246
+ Dependencies = actualUpdatedDependencies.ToArray(),
247
+ UpdatedDependencyFiles = updatedDependencyFileList,
248
+ BaseCommitSha = baseCommitSha,
249
+ CommitMessage = "TODO: message",
250
+ PrTitle = "TODO: title",
251
+ PrBody = "TODO: body",
252
+ };
253
+ await _apiHandler.CreatePullRequest(createPullRequest);
254
+ // TODO: log updated dependencies to console
269
255
  }
270
256
  else
271
257
  {
272
- // TODO: throw if no updates performed
258
+ // TODO: log or throw if nothing was updated, but was expected to be
273
259
  }
274
260
 
275
261
  var result = new RunResult()
@@ -289,6 +275,62 @@ public class RunWorker
289
275
  return result;
290
276
  }
291
277
 
278
+ internal static bool IsUpdateAllowed(Job job, Dependency dependency)
279
+ {
280
+ if (dependency.Name.Equals("Microsoft.NET.Sdk", StringComparison.OrdinalIgnoreCase))
281
+ {
282
+ // this can't be updated
283
+ // TODO: pull this out of discovery?
284
+ return false;
285
+ }
286
+
287
+ if (dependency.Version is null)
288
+ {
289
+ // if we don't know the version, there's nothing we can do
290
+ // TODO: pull this out of discovery?
291
+ return false;
292
+ }
293
+
294
+ var version = NuGetVersion.Parse(dependency.Version);
295
+ var dependencyInfo = GetDependencyInfo(job, dependency);
296
+ var isVulnerable = dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version));
297
+ var allowed = job.AllowedUpdates.Any(allowedUpdate =>
298
+ {
299
+ // check name restriction, if any
300
+ if (allowedUpdate.DependencyName is not null)
301
+ {
302
+ var matcher = new Matcher(StringComparison.OrdinalIgnoreCase)
303
+ .AddInclude(allowedUpdate.DependencyName);
304
+ var result = matcher.Match(dependency.Name);
305
+ if (!result.HasMatches)
306
+ {
307
+ return false;
308
+ }
309
+ }
310
+
311
+ var isSecurityUpdate = allowedUpdate.UpdateType == UpdateType.Security || job.SecurityUpdatesOnly;
312
+ if (isSecurityUpdate)
313
+ {
314
+ // only update if it's vulnerable
315
+ return isVulnerable;
316
+ }
317
+ else
318
+ {
319
+ // not a security update, so only update if...
320
+ // ...we've been explicitly asked to update this
321
+ if ((job.Dependencies ?? []).Any(d => d.Equals(dependency.Name, StringComparison.OrdinalIgnoreCase)))
322
+ {
323
+ return true;
324
+ }
325
+
326
+ // ...no specific update being performed, do it if it's not transitive
327
+ return !dependency.IsTransitive;
328
+ }
329
+ });
330
+
331
+ return allowed;
332
+ }
333
+
292
334
  internal static ImmutableArray<Requirement> GetIgnoredRequirementsForDependency(Job job, string dependencyName)
293
335
  {
294
336
  var ignoreConditions = job.IgnoreConditions
@@ -375,7 +417,7 @@ public class RunWorker
375
417
  new ReportedDependency()
376
418
  {
377
419
  Name = d.Name,
378
- Requirements = d.IsTransitive ? [] : [new ReportedRequirement()
420
+ Requirements = [new ReportedRequirement()
379
421
  {
380
422
  File = GetFullRepoPath(p.FilePath),
381
423
  Requirement = d.Version!,
@@ -347,7 +347,7 @@ internal static class PackageReferenceUpdater
347
347
  projectDirectory,
348
348
  experimentsManager
349
349
  );
350
- MSBuildHelper.ThrowOnUnauthenticatedFeed(stdout);
350
+ MSBuildHelper.ThrowOnError(stdout);
351
351
  if (exitCode != 0)
352
352
  {
353
353
  logger.Warn($" Transitive dependency [{dependencyName}/{newDependencyVersion}] was not added.\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}");
@@ -148,18 +148,15 @@ internal static partial class PackagesConfigUpdater
148
148
 
149
149
  if (exitCodeAgain != 0)
150
150
  {
151
- MSBuildHelper.ThrowOnMissingFile(fullOutput);
152
- MSBuildHelper.ThrowOnMissingFile(restoreOutput);
153
- MSBuildHelper.ThrowOnMissingPackages(restoreOutput);
151
+ MSBuildHelper.ThrowOnError(fullOutput);
152
+ MSBuildHelper.ThrowOnError(restoreOutput);
154
153
  throw new Exception($"Unable to restore.\nOutput:\n${restoreOutput}\n");
155
154
  }
156
155
 
157
156
  goto doRestore;
158
157
  }
159
158
 
160
- MSBuildHelper.ThrowOnUnauthenticatedFeed(fullOutput);
161
- MSBuildHelper.ThrowOnMissingFile(fullOutput);
162
- MSBuildHelper.ThrowOnMissingPackages(fullOutput);
159
+ MSBuildHelper.ThrowOnError(fullOutput);
163
160
  throw new Exception(fullOutput);
164
161
  }
165
162
  }
@@ -903,21 +903,6 @@ internal static partial class MSBuildHelper
903
903
  }
904
904
  }
905
905
 
906
- internal static void ThrowOnUnauthenticatedFeed(string stdout)
907
- {
908
- var unauthorizedMessageSnippets = new string[]
909
- {
910
- "The plugin credential provider could not acquire credentials",
911
- "401 (Unauthorized)",
912
- "error NU1301: Unable to load the service index for source",
913
- "Response status code does not indicate success: 403",
914
- };
915
- if (unauthorizedMessageSnippets.Any(stdout.Contains))
916
- {
917
- throw new HttpRequestException(message: stdout, inner: null, statusCode: System.Net.HttpStatusCode.Unauthorized);
918
- }
919
- }
920
-
921
906
  internal static string? GetMissingFile(string output)
922
907
  {
923
908
  var missingFilePatterns = new[]
@@ -934,7 +919,30 @@ internal static partial class MSBuildHelper
934
919
  return null;
935
920
  }
936
921
 
937
- internal static void ThrowOnMissingFile(string output)
922
+ internal static void ThrowOnError(string output)
923
+ {
924
+ ThrowOnUnauthenticatedFeed(output);
925
+ ThrowOnMissingFile(output);
926
+ ThrowOnMissingPackages(output);
927
+ ThrowOnUnresolvableDependencies(output);
928
+ }
929
+
930
+ private static void ThrowOnUnauthenticatedFeed(string stdout)
931
+ {
932
+ var unauthorizedMessageSnippets = new string[]
933
+ {
934
+ "The plugin credential provider could not acquire credentials",
935
+ "401 (Unauthorized)",
936
+ "error NU1301: Unable to load the service index for source",
937
+ "Response status code does not indicate success: 403",
938
+ };
939
+ if (unauthorizedMessageSnippets.Any(stdout.Contains))
940
+ {
941
+ throw new HttpRequestException(message: stdout, inner: null, statusCode: System.Net.HttpStatusCode.Unauthorized);
942
+ }
943
+ }
944
+
945
+ private static void ThrowOnMissingFile(string output)
938
946
  {
939
947
  var missingFile = GetMissingFile(output);
940
948
  if (missingFile is not null)
@@ -943,9 +951,9 @@ internal static partial class MSBuildHelper
943
951
  }
944
952
  }
945
953
 
946
- internal static void ThrowOnMissingPackages(string output)
954
+ private static void ThrowOnMissingPackages(string output)
947
955
  {
948
- var missingPackagesPattern = new Regex(@"Package '(?<PackageName>[^'].*)' is not found on source");
956
+ var missingPackagesPattern = new Regex(@"Package '(?<PackageName>[^']*)' is not found on source");
949
957
  var matchCollection = missingPackagesPattern.Matches(output);
950
958
  var missingPackages = matchCollection.Select(m => m.Groups["PackageName"].Value).Distinct().ToArray();
951
959
  if (missingPackages.Length > 0)
@@ -954,6 +962,16 @@ internal static partial class MSBuildHelper
954
962
  }
955
963
  }
956
964
 
965
+ private static void ThrowOnUnresolvableDependencies(string output)
966
+ {
967
+ var unresolvablePackagePattern = new Regex(@"Unable to resolve dependencies\. '(?<PackageName>[^ ]+) (?<PackageVersion>[^']+)'");
968
+ var match = unresolvablePackagePattern.Match(output);
969
+ if (match.Success)
970
+ {
971
+ throw new UpdateNotPossibleException([$"{match.Groups["PackageName"].Value}.{match.Groups["PackageVersion"].Value}"]);
972
+ }
973
+ }
974
+
957
975
  internal static bool TryGetGlobalJsonPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? globalJsonPath)
958
976
  {
959
977
  globalJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "global.json", caseSensitive: false);
@@ -1711,6 +1711,344 @@ public class RunWorkerTests
1711
1711
  );
1712
1712
  }
1713
1713
 
1714
+ [Fact]
1715
+ public async Task UpdatePackageWithDifferentVersionsInDifferentDirectories()
1716
+ {
1717
+ // this test passes `null` for discovery, analyze, and update workers to fully test the desired behavior
1718
+
1719
+ // the same dependency Some.Package is reported for 3 cases:
1720
+ // library1.csproj - top level dependency, already up to date
1721
+ // library2.csproj - top level dependency, needs direct update
1722
+ // library3.csproj - transitive dependency, needs pin
1723
+ await RunAsync(
1724
+ experimentsManager: new ExperimentsManager() { UseDirectDiscovery = true },
1725
+ packages: [
1726
+ MockNuGetPackage.CreateSimplePackage("Some.Package", "1.0.0", "net8.0"),
1727
+ MockNuGetPackage.CreateSimplePackage("Some.Package", "2.0.0", "net8.0"),
1728
+ MockNuGetPackage.CreateSimplePackage("Package.With.Transitive.Dependency", "0.1.0", "net8.0", [(null, [("Some.Package", "1.0.0")])]),
1729
+ ],
1730
+ job: new Job()
1731
+ {
1732
+ AllowedUpdates = [new() { UpdateType = UpdateType.Security }],
1733
+ SecurityAdvisories =
1734
+ [
1735
+ new()
1736
+ {
1737
+ DependencyName = "Some.Package",
1738
+ AffectedVersions = [Requirement.Parse("= 1.0.0")]
1739
+ }
1740
+ ],
1741
+ Source = new()
1742
+ {
1743
+ Provider = "github",
1744
+ Repo = "test/repo",
1745
+ Directory = "/"
1746
+ }
1747
+ },
1748
+ files: [
1749
+ ("dirs.proj", """
1750
+ <Project>
1751
+ <ItemGroup>
1752
+ <ProjectFile Include="library1\library1.csproj" />
1753
+ <ProjectFile Include="library2\library2.csproj" />
1754
+ <ProjectFile Include="library3\library3.csproj" />
1755
+ </ItemGroup>
1756
+ </Project>
1757
+ """),
1758
+ ("Directory.Build.props", "<Project />"),
1759
+ ("Directory.Build.targets", "<Project />"),
1760
+ ("Directory.Packages.props", """
1761
+ <Project>
1762
+ <PropertyGroup>
1763
+ <ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
1764
+ </PropertyGroup>
1765
+ </Project>
1766
+ """),
1767
+ ("library1/library1.csproj", """
1768
+ <Project Sdk="Microsoft.NET.Sdk">
1769
+ <PropertyGroup>
1770
+ <TargetFramework>net8.0</TargetFramework>
1771
+ </PropertyGroup>
1772
+ <ItemGroup>
1773
+ <PackageReference Include="Some.Package" Version="2.0.0" />
1774
+ </ItemGroup>
1775
+ </Project>
1776
+ """),
1777
+ ("library2/library2.csproj", """
1778
+ <Project Sdk="Microsoft.NET.Sdk">
1779
+ <PropertyGroup>
1780
+ <TargetFramework>net8.0</TargetFramework>
1781
+ </PropertyGroup>
1782
+ <ItemGroup>
1783
+ <PackageReference Include="Some.Package" Version="1.0.0" />
1784
+ </ItemGroup>
1785
+ </Project>
1786
+ """),
1787
+ ("library3/library3.csproj", """
1788
+ <Project Sdk="Microsoft.NET.Sdk">
1789
+ <PropertyGroup>
1790
+ <TargetFramework>net8.0</TargetFramework>
1791
+ </PropertyGroup>
1792
+ <ItemGroup>
1793
+ <PackageReference Include="Package.With.Transitive.Dependency" Version="0.1.0" />
1794
+ </ItemGroup>
1795
+ </Project>
1796
+ """),
1797
+ ],
1798
+ discoveryWorker: null,
1799
+ analyzeWorker: null,
1800
+ updaterWorker: null,
1801
+ expectedResult: new RunResult()
1802
+ {
1803
+ Base64DependencyFiles =
1804
+ [
1805
+ new DependencyFile()
1806
+ {
1807
+ Directory = "/",
1808
+ Name = "Directory.Build.props",
1809
+ Content = Convert.ToBase64String(Encoding.UTF8.GetBytes("<Project />"))
1810
+ },
1811
+ new DependencyFile()
1812
+ {
1813
+ Directory = "/",
1814
+ Name = "Directory.Build.targets",
1815
+ Content = Convert.ToBase64String(Encoding.UTF8.GetBytes("<Project />"))
1816
+ },
1817
+ new DependencyFile()
1818
+ {
1819
+ Directory = "/",
1820
+ Name = "Directory.Packages.props",
1821
+ Content = Convert.ToBase64String(Encoding.UTF8.GetBytes("""
1822
+ <Project>
1823
+ <PropertyGroup>
1824
+ <ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
1825
+ </PropertyGroup>
1826
+ </Project>
1827
+ """))
1828
+ },
1829
+ new DependencyFile()
1830
+ {
1831
+ Directory = "/library1",
1832
+ Name = "library1.csproj",
1833
+ Content = Convert.ToBase64String(Encoding.UTF8.GetBytes("""
1834
+ <Project Sdk="Microsoft.NET.Sdk">
1835
+ <PropertyGroup>
1836
+ <TargetFramework>net8.0</TargetFramework>
1837
+ </PropertyGroup>
1838
+ <ItemGroup>
1839
+ <PackageReference Include="Some.Package" Version="2.0.0" />
1840
+ </ItemGroup>
1841
+ </Project>
1842
+ """))
1843
+ },
1844
+ new DependencyFile()
1845
+ {
1846
+ Directory = "/library2",
1847
+ Name = "library2.csproj",
1848
+ Content = Convert.ToBase64String(Encoding.UTF8.GetBytes("""
1849
+ <Project Sdk="Microsoft.NET.Sdk">
1850
+ <PropertyGroup>
1851
+ <TargetFramework>net8.0</TargetFramework>
1852
+ </PropertyGroup>
1853
+ <ItemGroup>
1854
+ <PackageReference Include="Some.Package" Version="1.0.0" />
1855
+ </ItemGroup>
1856
+ </Project>
1857
+ """))
1858
+ },
1859
+ new DependencyFile()
1860
+ {
1861
+ Directory = "/library3",
1862
+ Name = "library3.csproj",
1863
+ Content = Convert.ToBase64String(Encoding.UTF8.GetBytes("""
1864
+ <Project Sdk="Microsoft.NET.Sdk">
1865
+ <PropertyGroup>
1866
+ <TargetFramework>net8.0</TargetFramework>
1867
+ </PropertyGroup>
1868
+ <ItemGroup>
1869
+ <PackageReference Include="Package.With.Transitive.Dependency" Version="0.1.0" />
1870
+ </ItemGroup>
1871
+ </Project>
1872
+ """))
1873
+ }
1874
+ ],
1875
+ BaseCommitSha = "TEST-COMMIT-SHA",
1876
+ },
1877
+ expectedApiMessages: [
1878
+ new UpdatedDependencyList()
1879
+ {
1880
+ Dependencies = [
1881
+ new()
1882
+ {
1883
+ Name = "Some.Package",
1884
+ Version = "2.0.0",
1885
+ Requirements = [
1886
+ new()
1887
+ {
1888
+ Requirement = "2.0.0",
1889
+ File = "/library1/library1.csproj",
1890
+ Groups = ["dependencies"],
1891
+ }
1892
+ ]
1893
+ },
1894
+ new()
1895
+ {
1896
+ Name = "Some.Package",
1897
+ Version = "1.0.0",
1898
+ Requirements = [
1899
+ new()
1900
+ {
1901
+ Requirement = "1.0.0",
1902
+ File = "/library2/library2.csproj",
1903
+ Groups = ["dependencies"],
1904
+ }
1905
+ ]
1906
+ },
1907
+ new()
1908
+ {
1909
+ Name = "Package.With.Transitive.Dependency",
1910
+ Version = "0.1.0",
1911
+ Requirements = [
1912
+ new()
1913
+ {
1914
+ Requirement = "0.1.0",
1915
+ File = "/library3/library3.csproj",
1916
+ Groups = ["dependencies"],
1917
+ }
1918
+ ]
1919
+ },
1920
+ new()
1921
+ {
1922
+ Name = "Some.Package",
1923
+ Version = "1.0.0",
1924
+ Requirements = [
1925
+ new()
1926
+ {
1927
+ Requirement = "1.0.0",
1928
+ File = "/library3/library3.csproj",
1929
+ Groups = ["dependencies"],
1930
+ }
1931
+ ]
1932
+ },
1933
+ ],
1934
+ DependencyFiles = [
1935
+ "/Directory.Build.props",
1936
+ "/Directory.Build.targets",
1937
+ "/Directory.Packages.props",
1938
+ "/library1/library1.csproj",
1939
+ "/library2/library2.csproj",
1940
+ "/library3/library3.csproj",
1941
+ ],
1942
+ },
1943
+ new IncrementMetric()
1944
+ {
1945
+ Metric = "updater.started",
1946
+ Tags = new()
1947
+ {
1948
+ ["operation"] = "group_update_all_versions"
1949
+ }
1950
+ },
1951
+ new CreatePullRequest()
1952
+ {
1953
+ Dependencies = [
1954
+ new()
1955
+ {
1956
+ Name = "Some.Package",
1957
+ Version = "2.0.0",
1958
+ Requirements = [
1959
+ new()
1960
+ {
1961
+ Requirement = "2.0.0",
1962
+ File = "/library2/library2.csproj",
1963
+ Groups = ["dependencies"],
1964
+ Source = new()
1965
+ {
1966
+ SourceUrl = null,
1967
+ Type = "nuget_repo",
1968
+ }
1969
+ }
1970
+ ],
1971
+ PreviousVersion = "1.0.0",
1972
+ PreviousRequirements = [
1973
+ new()
1974
+ {
1975
+ Requirement = "1.0.0",
1976
+ File = "/library2/library2.csproj",
1977
+ Groups = ["dependencies"],
1978
+ }
1979
+ ],
1980
+ },
1981
+ new()
1982
+ {
1983
+ Name = "Some.Package",
1984
+ Version = "2.0.0",
1985
+ Requirements = [
1986
+ new()
1987
+ {
1988
+ Requirement = "2.0.0",
1989
+ File = "/library3/library3.csproj",
1990
+ Groups = ["dependencies"],
1991
+ Source = new()
1992
+ {
1993
+ SourceUrl = null,
1994
+ Type = "nuget_repo",
1995
+ }
1996
+ }
1997
+ ],
1998
+ PreviousVersion = "1.0.0",
1999
+ PreviousRequirements = [
2000
+ new()
2001
+ {
2002
+ Requirement = "1.0.0",
2003
+ File = "/library3/library3.csproj",
2004
+ Groups = ["dependencies"],
2005
+ }
2006
+ ],
2007
+ },
2008
+ ],
2009
+ UpdatedDependencyFiles = [
2010
+ new()
2011
+ {
2012
+ Directory = "/library2",
2013
+ Name = "library2.csproj",
2014
+ Content = """
2015
+ <Project Sdk="Microsoft.NET.Sdk">
2016
+ <PropertyGroup>
2017
+ <TargetFramework>net8.0</TargetFramework>
2018
+ </PropertyGroup>
2019
+ <ItemGroup>
2020
+ <PackageReference Include="Some.Package" Version="2.0.0" />
2021
+ </ItemGroup>
2022
+ </Project>
2023
+ """
2024
+ },
2025
+ new()
2026
+ {
2027
+ Directory = "/library3",
2028
+ Name = "library3.csproj",
2029
+ Content = """
2030
+ <Project Sdk="Microsoft.NET.Sdk">
2031
+ <PropertyGroup>
2032
+ <TargetFramework>net8.0</TargetFramework>
2033
+ </PropertyGroup>
2034
+ <ItemGroup>
2035
+ <PackageReference Include="Package.With.Transitive.Dependency" Version="0.1.0" />
2036
+ <PackageReference Include="Some.Package" Version="2.0.0" />
2037
+ </ItemGroup>
2038
+ </Project>
2039
+ """
2040
+ }
2041
+ ],
2042
+ BaseCommitSha = "TEST-COMMIT-SHA",
2043
+ CommitMessage = "TODO: message",
2044
+ PrTitle = "TODO: title",
2045
+ PrBody = "TODO: body"
2046
+ },
2047
+ new MarkAsProcessed("TEST-COMMIT-SHA")
2048
+ ]
2049
+ );
2050
+ }
2051
+
1714
2052
  private static async Task RunAsync(Job job, TestFile[] files, IDiscoveryWorker? discoveryWorker, IAnalyzeWorker? analyzeWorker, IUpdaterWorker? updaterWorker, RunResult expectedResult, object[] expectedApiMessages, MockNuGetPackage[]? packages = null, ExperimentsManager? experimentsManager = null, string? repoContentsPath = null)
1715
2053
  {
1716
2054
  // arrange
@@ -0,0 +1,286 @@
1
+ using System.Collections.Immutable;
2
+
3
+ using NuGetUpdater.Core.Analyze;
4
+ using NuGetUpdater.Core.Run;
5
+ using NuGetUpdater.Core.Run.ApiModel;
6
+
7
+ using Xunit;
8
+
9
+ using DepType = NuGetUpdater.Core.Run.ApiModel.DependencyType;
10
+
11
+ namespace NuGetUpdater.Core.Test.Run;
12
+
13
+ public class UpdateAllowedTests
14
+ {
15
+ [Theory]
16
+ [MemberData(nameof(IsUpdateAllowedTestData))]
17
+ public void IsUpdateAllowed(Job job, Dependency dependency, bool expectedResult)
18
+ {
19
+ var actualResult = RunWorker.IsUpdateAllowed(job, dependency);
20
+ Assert.Equal(expectedResult, actualResult);
21
+ }
22
+
23
+ public static IEnumerable<object[]> IsUpdateAllowedTestData()
24
+ {
25
+ // with default allowed updates on a transitive dependency
26
+ yield return
27
+ [
28
+ CreateJob(
29
+ allowedUpdates: [
30
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All }
31
+ ],
32
+ securityAdvisories: [
33
+ new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] }
34
+ ],
35
+ securityUpdatesOnly: false),
36
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: true),
37
+ // expectedResult
38
+ false,
39
+ ];
40
+
41
+ // when dealing with a security update
42
+ yield return
43
+ [
44
+ CreateJob(
45
+ allowedUpdates: [
46
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All }
47
+ ],
48
+ securityAdvisories: [
49
+ new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] }
50
+ ],
51
+ securityUpdatesOnly: true),
52
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: true),
53
+ // expectedResult
54
+ true,
55
+ ];
56
+
57
+ // with a top-level dependency
58
+ yield return
59
+ [
60
+ CreateJob(
61
+ allowedUpdates: [
62
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All },
63
+ new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security }
64
+ ],
65
+ securityAdvisories: [],
66
+ securityUpdatesOnly: false),
67
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
68
+ // expectedResult
69
+ true,
70
+ ];
71
+
72
+ // with a sub-dependency
73
+ yield return
74
+ [
75
+ CreateJob(
76
+ allowedUpdates: [
77
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All },
78
+ new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security }
79
+ ],
80
+ securityAdvisories: [],
81
+ securityUpdatesOnly: false),
82
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: true),
83
+ // expectedResult
84
+ false,
85
+ ];
86
+
87
+ // when insecure
88
+ yield return
89
+ [
90
+ CreateJob(
91
+ allowedUpdates: [
92
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All },
93
+ new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security }
94
+ ],
95
+ securityAdvisories: [
96
+ new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] }
97
+ ],
98
+ securityUpdatesOnly: false),
99
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: true),
100
+ // expectedResult
101
+ true,
102
+ ];
103
+
104
+ // when only security fixes are allowed
105
+ yield return
106
+ [
107
+ CreateJob(
108
+ allowedUpdates: [
109
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All },
110
+ new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security }
111
+ ],
112
+ securityAdvisories: [],
113
+ securityUpdatesOnly: true),
114
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
115
+ // expectedResult
116
+ false,
117
+ ];
118
+
119
+ // when dealing with a security fix
120
+ yield return
121
+ [
122
+ CreateJob(
123
+ allowedUpdates: [
124
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All },
125
+ new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security }
126
+ ],
127
+ securityAdvisories: [
128
+ new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] }
129
+ ],
130
+ securityUpdatesOnly: true),
131
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
132
+ // expectedResult
133
+ true,
134
+ ];
135
+
136
+ // when dealing with a security fix that doesn't apply
137
+ yield return
138
+ [
139
+ CreateJob(
140
+ allowedUpdates: [
141
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All },
142
+ new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security }
143
+ ],
144
+ securityAdvisories: [
145
+ new Advisory() { DependencyName = "Some.Package", AffectedVersions = [Requirement.Parse("> 1.8.0")], PatchedVersions = [], UnaffectedVersions = [] }
146
+ ],
147
+ securityUpdatesOnly: true),
148
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
149
+ // expectedResult
150
+ false,
151
+ ];
152
+
153
+ // when dealing with a security fix that doesn't apply to some versions
154
+ yield return
155
+ [
156
+ CreateJob(
157
+ allowedUpdates: [
158
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All },
159
+ new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security }
160
+ ],
161
+ securityAdvisories: [
162
+ new Advisory() { DependencyName = "Some.Package", AffectedVersions = [Requirement.Parse("< 1.8.0"), Requirement.Parse("> 1.8.0")], PatchedVersions = [], UnaffectedVersions = [] }
163
+ ],
164
+ securityUpdatesOnly: true),
165
+ new Dependency("Some.Package", "1.8.1", DependencyType.PackageReference, IsTransitive: false),
166
+ // expectedResult
167
+ true,
168
+ ];
169
+
170
+ // when a dependency allow list that includes the dependency
171
+ yield return
172
+ [
173
+ CreateJob(
174
+ allowedUpdates: [
175
+ new AllowedUpdate() { DependencyName = "Some.Package" }
176
+ ],
177
+ securityAdvisories: [],
178
+ securityUpdatesOnly: false),
179
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
180
+ // expectedResult
181
+ true,
182
+ ];
183
+
184
+ // with a dependency allow list that uses a wildcard
185
+ yield return
186
+ [
187
+ CreateJob(
188
+ allowedUpdates: [
189
+ new AllowedUpdate() { DependencyName = "Some.*" }
190
+ ],
191
+ securityAdvisories: [],
192
+ securityUpdatesOnly: false),
193
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
194
+ // expectedResult
195
+ true,
196
+ ];
197
+
198
+ // when dependency allow list that excludes the dependency
199
+ yield return
200
+ [
201
+ CreateJob(
202
+ allowedUpdates: [
203
+ new AllowedUpdate() { DependencyName = "Unrelated.Package" }
204
+ ],
205
+ securityAdvisories: [],
206
+ securityUpdatesOnly: false),
207
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
208
+ // expectedResult
209
+ false,
210
+ ];
211
+
212
+ // when matching with an incomplete dependency name
213
+ yield return
214
+ [
215
+ CreateJob(
216
+ allowedUpdates: [
217
+ new AllowedUpdate() { DependencyName = "Some" }
218
+ ],
219
+ securityAdvisories: [],
220
+ securityUpdatesOnly: false),
221
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
222
+ // expectedResult
223
+ false,
224
+ ];
225
+
226
+ // with a dependency allow list that uses a wildcard
227
+ yield return
228
+ [
229
+ CreateJob(
230
+ allowedUpdates: [
231
+ new AllowedUpdate() { DependencyName = "Unrelated.*" }
232
+ ],
233
+ securityAdvisories: [],
234
+ securityUpdatesOnly: false),
235
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
236
+ // expectedResult
237
+ false,
238
+ ];
239
+
240
+ // when security fixes are also allowed
241
+ yield return
242
+ [
243
+ CreateJob(
244
+ allowedUpdates: [
245
+ new AllowedUpdate() { DependencyName = "Unrelated.Package" },
246
+ new AllowedUpdate() { UpdateType = UpdateType.Security }
247
+ ],
248
+ securityAdvisories: [],
249
+ securityUpdatesOnly: false),
250
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
251
+ // expectedResult
252
+ false,
253
+ ];
254
+
255
+ // when dealing with a security fix
256
+ yield return
257
+ [
258
+ CreateJob(
259
+ allowedUpdates: [
260
+ new AllowedUpdate() { DependencyName = "Unrelated.Package"}, new AllowedUpdate(){ UpdateType = UpdateType.Security }
261
+ ],
262
+ securityAdvisories: [
263
+ new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] }
264
+ ],
265
+ securityUpdatesOnly: false),
266
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
267
+ // expectedResult
268
+ true,
269
+ ];
270
+ }
271
+
272
+ private static Job CreateJob(AllowedUpdate[] allowedUpdates, Advisory[] securityAdvisories, bool securityUpdatesOnly)
273
+ {
274
+ return new Job()
275
+ {
276
+ AllowedUpdates = allowedUpdates.ToImmutableArray(),
277
+ SecurityAdvisories = securityAdvisories.ToImmutableArray(),
278
+ SecurityUpdatesOnly = securityUpdatesOnly,
279
+ Source = new()
280
+ {
281
+ Provider = "nuget",
282
+ Repo = "test/repo",
283
+ }
284
+ };
285
+ }
286
+ }
@@ -94,7 +94,15 @@ public class UpdatedDependencyListTests
94
94
  {
95
95
  Name = "System.Text.Json",
96
96
  Version = "6.0.0",
97
- Requirements = [],
97
+ Requirements =
98
+ [
99
+ new ReportedRequirement()
100
+ {
101
+ Requirement = "6.0.0",
102
+ File = "/src/c/project.csproj",
103
+ Groups = ["dependencies"],
104
+ }
105
+ ],
98
106
  },
99
107
  new ReportedDependency()
100
108
  {
@@ -1,7 +1,8 @@
1
1
  using System.Collections.Immutable;
2
+ using System.Text.Json;
2
3
 
3
- using NuGet.Build.Tasks;
4
-
4
+ using NuGetUpdater.Core.Run;
5
+ using NuGetUpdater.Core.Run.ApiModel;
5
6
  using NuGetUpdater.Core.Test.Update;
6
7
 
7
8
  using Xunit;
@@ -1366,6 +1367,91 @@ public class MSBuildHelperTests : TestBase
1366
1367
  }
1367
1368
  #endregion
1368
1369
 
1370
+ [Theory]
1371
+ [MemberData(nameof(GenerateErrorFromToolOutputTestData))]
1372
+ public async Task GenerateErrorFromToolOutput(string output, JobErrorBase? expectedError)
1373
+ {
1374
+ Exception? exception = null;
1375
+ try
1376
+ {
1377
+ MSBuildHelper.ThrowOnError(output);
1378
+ }
1379
+ catch (Exception ex)
1380
+ {
1381
+ exception = ex;
1382
+ }
1383
+
1384
+ if (expectedError is null)
1385
+ {
1386
+ Assert.Null(exception);
1387
+ }
1388
+ else
1389
+ {
1390
+ Assert.NotNull(exception);
1391
+ using var tempDir = await TemporaryDirectory.CreateWithContentsAsync([("NuGet.Config", """
1392
+ <configuration>
1393
+ <packageSources>
1394
+ <clear />
1395
+ <add key="test-feed" value="http://localhost/test-feed" />
1396
+ </packageSources>
1397
+ </configuration>
1398
+ """)]);
1399
+ var actualError = JobErrorBase.ErrorFromException(exception, "TEST-JOB-ID", tempDir.DirectoryPath);
1400
+ if (actualError is DependencyFileNotFound notFound)
1401
+ {
1402
+ // normalize default message for the test
1403
+ actualError = new DependencyFileNotFound(notFound.Details["file-path"].ToString()!, "test message");
1404
+ }
1405
+
1406
+ var actualErrorJson = JsonSerializer.Serialize(actualError, RunWorker.SerializerOptions);
1407
+ var expectedErrorJson = JsonSerializer.Serialize(expectedError, RunWorker.SerializerOptions);
1408
+ Assert.Equal(expectedErrorJson, actualErrorJson);
1409
+ }
1410
+ }
1411
+
1412
+ public static IEnumerable<object?[]> GenerateErrorFromToolOutputTestData()
1413
+ {
1414
+ yield return
1415
+ [
1416
+ // output
1417
+ "Everything was good.",
1418
+ // expectedError
1419
+ null,
1420
+ ];
1421
+
1422
+ yield return
1423
+ [
1424
+ // output
1425
+ "Response status code does not indicate success: 403",
1426
+ // expectedError
1427
+ new PrivateSourceAuthenticationFailure(["http://localhost/test-feed"]),
1428
+ ];
1429
+
1430
+ yield return
1431
+ [
1432
+ // output
1433
+ "The imported file \"some.file\" does not exist",
1434
+ // expectedError
1435
+ new DependencyFileNotFound("some.file", "test message"),
1436
+ ];
1437
+
1438
+ yield return
1439
+ [
1440
+ // output
1441
+ "Package 'Some.Package' is not found on source",
1442
+ // expectedError
1443
+ new UpdateNotPossible(["Some.Package"]),
1444
+ ];
1445
+
1446
+ yield return
1447
+ [
1448
+ // output
1449
+ "Unable to resolve dependencies. 'Some.Package 1.2.3' is not compatible with",
1450
+ // expectedError
1451
+ new UpdateNotPossible(["Some.Package.1.2.3"]),
1452
+ ];
1453
+ }
1454
+
1369
1455
  public static IEnumerable<object[]> GetTopLevelPackageDependencyInfosTestData()
1370
1456
  {
1371
1457
  // simple case
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-nuget
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.294.0
4
+ version: 0.295.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-23 00:00:00.000000000 Z
11
+ date: 2025-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dependabot-common
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.294.0
19
+ version: 0.295.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.294.0
26
+ version: 0.295.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rubyzip
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -351,6 +351,7 @@ files:
351
351
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs
352
352
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs
353
353
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/TestApiHandler.cs
354
+ - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateAllowedTests.cs
354
355
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdatedDependencyListTests.cs
355
356
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryDirectory.cs
356
357
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryEnvironment.cs
@@ -528,7 +529,7 @@ licenses:
528
529
  - MIT
529
530
  metadata:
530
531
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
531
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.294.0
532
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.295.0
532
533
  post_install_message:
533
534
  rdoc_options: []
534
535
  require_paths: