dependabot-nuget 0.265.0 → 0.266.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/UpdateCommand.cs +6 -6
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs +1 -1
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +73 -77
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs +21 -8
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs +24 -8
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs +33 -16
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +44 -25
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs +1 -1
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ErrorType.cs +8 -0
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs +2 -2
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NativeResult.cs +8 -0
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +18 -1
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +2 -1
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdateOperationResult.cs +5 -0
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +62 -22
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +19 -1
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs +6 -2
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs +448 -0
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs +23 -0
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +2 -0
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +148 -0
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs +1 -1
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs +1 -1
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TestHttpServer.cs +81 -0
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +27 -7
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs +32 -0
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +87 -2
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs +88 -0
  30. data/lib/dependabot/nuget/analysis/dependency_analysis.rb +3 -0
  31. data/lib/dependabot/nuget/file_fetcher.rb +1 -1
  32. data/lib/dependabot/nuget/metadata_finder.rb +160 -2
  33. data/lib/dependabot/nuget/native_discovery/native_workspace_discovery.rb +3 -0
  34. data/lib/dependabot/nuget/native_helpers.rb +34 -3
  35. data/lib/dependabot/nuget/native_update_checker/native_update_checker.rb +1 -0
  36. data/lib/dependabot/nuget/nuget_config_credential_helpers.rb +3 -0
  37. metadata +11 -7
@@ -1,3 +1,10 @@
1
+ using System.Net;
2
+ using System.Text.Json;
3
+ using System.Text.Json.Serialization;
4
+
5
+ using NuGetUpdater.Core.Analyze;
6
+ using NuGetUpdater.Core.Updater;
7
+
1
8
  namespace NuGetUpdater.Core;
2
9
 
3
10
  public class UpdaterWorker
@@ -5,48 +12,81 @@ public class UpdaterWorker
5
12
  private readonly Logger _logger;
6
13
  private readonly HashSet<string> _processedProjectPaths = new(StringComparer.OrdinalIgnoreCase);
7
14
 
15
+ internal static readonly JsonSerializerOptions SerializerOptions = new()
16
+ {
17
+ WriteIndented = true,
18
+ Converters = { new JsonStringEnumConverter() },
19
+ };
20
+
8
21
  public UpdaterWorker(Logger logger)
9
22
  {
10
23
  _logger = logger;
11
24
  }
12
25
 
13
- public async Task RunAsync(string repoRootPath, string workspacePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, bool isTransitive)
26
+ public async Task RunAsync(string repoRootPath, string workspacePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, bool isTransitive, string? resultOutputPath = null)
14
27
  {
15
28
  MSBuildHelper.RegisterMSBuild(Environment.CurrentDirectory, repoRootPath);
29
+ UpdateOperationResult result;
16
30
 
17
31
  if (!Path.IsPathRooted(workspacePath) || !File.Exists(workspacePath))
18
32
  {
19
33
  workspacePath = Path.GetFullPath(Path.Join(repoRootPath, workspacePath));
20
34
  }
21
35
 
22
- if (!isTransitive)
36
+ try
23
37
  {
24
- await DotNetToolsJsonUpdater.UpdateDependencyAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, _logger);
25
- await GlobalJsonUpdater.UpdateDependencyAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, _logger);
38
+ if (!isTransitive)
39
+ {
40
+ await DotNetToolsJsonUpdater.UpdateDependencyAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, _logger);
41
+ await GlobalJsonUpdater.UpdateDependencyAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, _logger);
42
+ }
43
+
44
+ var extension = Path.GetExtension(workspacePath).ToLowerInvariant();
45
+ switch (extension)
46
+ {
47
+ case ".sln":
48
+ await RunForSolutionAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
49
+ break;
50
+ case ".proj":
51
+ await RunForProjFileAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
52
+ break;
53
+ case ".csproj":
54
+ case ".fsproj":
55
+ case ".vbproj":
56
+ await RunForProjectAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
57
+ break;
58
+ default:
59
+ _logger.Log($"File extension [{extension}] is not supported.");
60
+ break;
61
+ }
62
+
63
+ result = new(); // all ok
64
+ _logger.Log("Update complete.");
65
+ }
66
+ catch (HttpRequestException ex)
67
+ when (ex.StatusCode == HttpStatusCode.Unauthorized || ex.StatusCode == HttpStatusCode.Forbidden)
68
+ {
69
+ // TODO: consolidate this error handling between AnalyzeWorker, DiscoveryWorker, and UpdateWorker
70
+ result = new()
71
+ {
72
+ ErrorType = ErrorType.AuthenticationFailure,
73
+ ErrorDetails = "(" + string.Join("|", NuGetContext.GetPackageSourceUrls(workspacePath)) + ")",
74
+ };
26
75
  }
27
76
 
28
- var extension = Path.GetExtension(workspacePath).ToLowerInvariant();
29
- switch (extension)
77
+ _processedProjectPaths.Clear();
78
+ if (resultOutputPath is { })
30
79
  {
31
- case ".sln":
32
- await RunForSolutionAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
33
- break;
34
- case ".proj":
35
- await RunForProjFileAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
36
- break;
37
- case ".csproj":
38
- case ".fsproj":
39
- case ".vbproj":
40
- await RunForProjectAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
41
- break;
42
- default:
43
- _logger.Log($"File extension [{extension}] is not supported.");
44
- break;
80
+ await WriteResultFile(result, resultOutputPath, _logger);
45
81
  }
82
+ }
46
83
 
47
- _logger.Log("Update complete.");
84
+ internal static async Task WriteResultFile(UpdateOperationResult result, string resultOutputPath, Logger logger)
85
+ {
86
+ logger.Log($" Writing update result to [{resultOutputPath}].");
48
87
 
49
- _processedProjectPaths.Clear();
88
+ var resultJson = JsonSerializer.Serialize(result, SerializerOptions);
89
+ await File.WriteAllTextAsync(resultOutputPath, resultJson);
50
90
  }
51
91
 
52
92
  private async Task RunForSolutionAsync(
@@ -185,8 +185,9 @@ internal static partial class MSBuildHelper
185
185
  var versionSpecification = packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("Version", StringComparison.OrdinalIgnoreCase))?.Value
186
186
  ?? packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("VersionOverride", StringComparison.OrdinalIgnoreCase))?.Value
187
187
  ?? string.Empty;
188
- foreach (var attributeValue in new[] { packageItem.Include, packageItem.Update })
188
+ foreach (var rawAttributeValue in new[] { packageItem.Include, packageItem.Update })
189
189
  {
190
+ var attributeValue = rawAttributeValue?.Trim();
190
191
  if (!string.IsNullOrWhiteSpace(attributeValue))
191
192
  {
192
193
  if (packageInfo.TryGetValue(attributeValue, out var existingInfo))
@@ -312,6 +313,7 @@ internal static partial class MSBuildHelper
312
313
  {
313
314
  var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages);
314
315
  var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"restore \"{tempProjectPath}\"", workingDirectory: tempDirectory.FullName);
316
+ ThrowOnUnauthenticatedFeed(stdOut);
315
317
 
316
318
  // simple cases first
317
319
  // if restore failed, nothing we can do
@@ -506,6 +508,7 @@ internal static partial class MSBuildHelper
506
508
  <TargetFramework>{targetFramework}</TargetFramework>
507
509
  <GenerateDependencyFile>true</GenerateDependencyFile>
508
510
  <RunAnalyzers>false</RunAnalyzers>
511
+ <NuGetInteractive>false</NuGetInteractive>
509
512
  </PropertyGroup>
510
513
  <ItemGroup>
511
514
  {packageReferences}
@@ -565,6 +568,7 @@ internal static partial class MSBuildHelper
565
568
  var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages);
566
569
 
567
570
  var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", $"build \"{tempProjectPath}\" /t:_ReportDependencies", workingDirectory: tempDirectory.FullName);
571
+ ThrowOnUnauthenticatedFeed(stdout);
568
572
 
569
573
  if (exitCode == 0)
570
574
  {
@@ -602,6 +606,20 @@ internal static partial class MSBuildHelper
602
606
  }
603
607
  }
604
608
 
609
+ internal static void ThrowOnUnauthenticatedFeed(string stdout)
610
+ {
611
+ var unauthorizedMessageSnippets = new string[]
612
+ {
613
+ "The plugin credential provider could not acquire credentials",
614
+ "401 (Unauthorized)",
615
+ "error NU1301: Unable to load the service index for source",
616
+ };
617
+ if (unauthorizedMessageSnippets.Any(stdout.Contains))
618
+ {
619
+ throw new HttpRequestException(message: stdout, inner: null, statusCode: System.Net.HttpStatusCode.Unauthorized);
620
+ }
621
+ }
622
+
605
623
  internal static bool TryGetGlobalJsonPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? globalJsonPath)
606
624
  {
607
625
  globalJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "global.json", caseSensitive: false);
@@ -18,7 +18,8 @@ public class AnalyzeWorkerTestBase
18
18
  WorkspaceDiscoveryResult discovery,
19
19
  DependencyInfo dependencyInfo,
20
20
  ExpectedAnalysisResult expectedResult,
21
- MockNuGetPackage[]? packages = null)
21
+ MockNuGetPackage[]? packages = null,
22
+ TestFile[]? extraFiles = null)
22
23
  {
23
24
  var relativeDependencyPath = $"./dependabot/dependency/{dependencyInfo.Name}.json";
24
25
 
@@ -27,7 +28,8 @@ public class AnalyzeWorkerTestBase
27
28
  (relativeDependencyPath, JsonSerializer.Serialize(dependencyInfo, AnalyzeWorker.SerializerOptions)),
28
29
  ];
29
30
 
30
- var actualResult = await RunAnalyzerAsync(dependencyInfo.Name, files, async directoryPath =>
31
+ var allFiles = files.Concat(extraFiles ?? []).ToArray();
32
+ var actualResult = await RunAnalyzerAsync(dependencyInfo.Name, allFiles, async directoryPath =>
31
33
  {
32
34
  await UpdateWorkerTestBase.MockNuGetPackagesInDirectory(packages, directoryPath);
33
35
 
@@ -50,6 +52,8 @@ public class AnalyzeWorkerTestBase
50
52
  Assert.Equal(expectedResult.VersionComesFromMultiDependencyProperty, actualResult.VersionComesFromMultiDependencyProperty);
51
53
  ValidateDependencies(expectedResult.UpdatedDependencies, actualResult.UpdatedDependencies);
52
54
  Assert.Equal(expectedResult.ExpectedUpdatedDependenciesCount ?? expectedResult.UpdatedDependencies.Length, actualResult.UpdatedDependencies.Length);
55
+ Assert.Equal(expectedResult.ErrorType, actualResult.ErrorType);
56
+ Assert.Equal(expectedResult.ErrorDetails, actualResult.ErrorDetails);
53
57
 
54
58
  return;
55
59
 
@@ -1,3 +1,8 @@
1
+ using System.Text;
2
+ using System.Text.Json;
3
+
4
+ using NuGet;
5
+
1
6
  using NuGetUpdater.Core.Analyze;
2
7
 
3
8
  using Xunit;
@@ -301,4 +306,447 @@ public partial class AnalyzeWorkerTests : AnalyzeWorkerTestBase
301
306
  }
302
307
  );
303
308
  }
309
+
310
+ [Fact]
311
+ public async Task VersionFinderCanHandle404FromPackageSource_V2()
312
+ {
313
+ static (int, byte[]) TestHttpHandler1(string uriString)
314
+ {
315
+ // this is a valid nuget package source, but doesn't contain anything
316
+ var uri = new Uri(uriString, UriKind.Absolute);
317
+ var baseUrl = $"{uri.Scheme}://{uri.Host}:{uri.Port}";
318
+ return uri.PathAndQuery switch
319
+ {
320
+ "/api/v2/" => (200, Encoding.UTF8.GetBytes($"""
321
+ <service xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom" xml:base="{baseUrl}/api/v2">
322
+ <workspace>
323
+ <atom:title type="text">Default</atom:title>
324
+ <collection href="Packages">
325
+ <atom:title type="text">Packages</atom:title>
326
+ </collection>
327
+ </workspace>
328
+ </service>
329
+ """)),
330
+ _ => (404, Encoding.UTF8.GetBytes("{}")), // nothing else is found
331
+ };
332
+ }
333
+ var desktopAppRefPackage = MockNuGetPackage.WellKnownReferencePackage("Microsoft.WindowsDesktop.App", "net8.0");
334
+ (int, byte[]) TestHttpHandler2(string uriString)
335
+ {
336
+ // this contains the actual package
337
+ var uri = new Uri(uriString, UriKind.Absolute);
338
+ var baseUrl = $"{uri.Scheme}://{uri.Host}:{uri.Port}";
339
+ switch (uri.PathAndQuery)
340
+ {
341
+ case "/api/v2/":
342
+ return (200, Encoding.UTF8.GetBytes($"""
343
+ <service xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom" xml:base="{baseUrl}/api/v2">
344
+ <workspace>
345
+ <atom:title type="text">Default</atom:title>
346
+ <collection href="Packages">
347
+ <atom:title type="text">Packages</atom:title>
348
+ </collection>
349
+ </workspace>
350
+ </service>
351
+ """));
352
+ case "/api/v2/FindPackagesById()?id='Some.Package'&semVerLevel=2.0.0":
353
+ return (200, Encoding.UTF8.GetBytes($"""
354
+ <feed xml:base="{baseUrl}/api/v2" xmlns="http://www.w3.org/2005/Atom"
355
+ xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
356
+ xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
357
+ xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml">
358
+ <m:count>2</m:count>
359
+ <id>http://schemas.datacontract.org/2004/07/</id>
360
+ <title />
361
+ <updated>{DateTime.UtcNow:O}</updated>
362
+ <link rel="self" href="{baseUrl}/api/v2/Packages" />
363
+ <entry>
364
+ <id>{baseUrl}/api/v2/Packages(Id='Some.Package',Version='1.0.0')</id>
365
+ <content type="application/zip" src="{baseUrl}/api/v2/package/Some.Package/1.0.0" />
366
+ <m:properties>
367
+ <d:Version>1.0.0</d:Version>
368
+ </m:properties>
369
+ </entry>
370
+ <entry>
371
+ <id>{baseUrl}/api/v2/Packages(Id='Some.Package',Version='1.2.3')</id>
372
+ <content type="application/zip" src="{baseUrl}/api/v2/package/Some.Package/1.2.3" />
373
+ <m:properties>
374
+ <d:Version>1.2.3</d:Version>
375
+ </m:properties>
376
+ </entry>
377
+ </feed>
378
+ """));
379
+ case "/api/v2/Packages(Id='Some.Package',Version='1.2.3')":
380
+ return (200, Encoding.UTF8.GetBytes($"""
381
+ <entry xml:base="{baseUrl}/api/v2" xmlns="http://www.w3.org/2005/Atom"
382
+ xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
383
+ xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
384
+ xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml">
385
+ <id>{baseUrl}/api/v2/Packages(Id='Some.Package',Version='1.2.3')</id>
386
+ <updated>{DateTime.UtcNow:O}</updated>
387
+ <content type="application/zip" src="{baseUrl}/api/v2/package/Some.Package/1.2.3" />
388
+ <m:properties>
389
+ <d:Version>1.2.3</d:Version>
390
+ </m:properties>
391
+ </entry>
392
+ """));
393
+ case "/api/v2/package/Some.Package/1.2.3":
394
+ return (200, MockNuGetPackage.CreateSimplePackage("Some.Package", "1.2.3", "net8.0").GetZipStream().ReadAllBytes());
395
+ case "/api/v2/FindPackagesById()?id='Microsoft.WindowsDesktop.App.Ref'&semVerLevel=2.0.0":
396
+ return (200, Encoding.UTF8.GetBytes($"""
397
+ <feed xml:base="{baseUrl}/api/v2" xmlns="http://www.w3.org/2005/Atom"
398
+ xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
399
+ xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
400
+ xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml">
401
+ <m:count>1</m:count>
402
+ <id>http://schemas.datacontract.org/2004/07/</id>
403
+ <title />
404
+ <updated>{DateTime.UtcNow:O}</updated>
405
+ <link rel="self" href="{baseUrl}/api/v2/Packages" />
406
+ <entry>
407
+ <id>{baseUrl}/api/v2/Packages(Id='Microsoft.WindowsDesktop.App.Ref',Version='{desktopAppRefPackage.Version}')</id>
408
+ <content type="application/zip" src="{baseUrl}/api/v2/package/Microsoft.WindowsDesktop.App.Ref/{desktopAppRefPackage.Version}" />
409
+ <m:properties>
410
+ <d:Version>{desktopAppRefPackage.Version}</d:Version>
411
+ </m:properties>
412
+ </entry>
413
+ </feed>
414
+ """));
415
+ default:
416
+ if (uri.PathAndQuery == $"/api/v2/package/Microsoft.WindowsDesktop.App.Ref/{desktopAppRefPackage.Version}")
417
+ {
418
+ return (200, desktopAppRefPackage.GetZipStream().ReadAllBytes());
419
+ }
420
+
421
+ // nothing else is found
422
+ return (404, Encoding.UTF8.GetBytes("{}"));
423
+ };
424
+ }
425
+ using var http1 = TestHttpServer.CreateTestServer(TestHttpHandler1);
426
+ using var http2 = TestHttpServer.CreateTestServer(TestHttpHandler2);
427
+ await TestAnalyzeAsync(
428
+ extraFiles:
429
+ [
430
+ ("NuGet.Config", $"""
431
+ <configuration>
432
+ <packageSources>
433
+ <clear />
434
+ <add key="package_feed_1" value="{http1.BaseUrl.TrimEnd('/')}/api/v2/" allowInsecureConnections="true" />
435
+ <add key="package_feed_2" value="{http2.BaseUrl.TrimEnd('/')}/api/v2/" allowInsecureConnections="true" />
436
+ </packageSources>
437
+ </configuration>
438
+ """)
439
+ ],
440
+ discovery: new()
441
+ {
442
+ Path = "/",
443
+ Projects =
444
+ [
445
+ new()
446
+ {
447
+ FilePath = "./project.csproj",
448
+ TargetFrameworks = ["net8.0"],
449
+ Dependencies =
450
+ [
451
+ new("Some.Package", "1.0.0", DependencyType.PackageReference),
452
+ ]
453
+ }
454
+ ]
455
+ },
456
+ dependencyInfo: new()
457
+ {
458
+ Name = "Some.Package",
459
+ Version = "1.0.0",
460
+ IgnoredVersions = [],
461
+ IsVulnerable = false,
462
+ Vulnerabilities = [],
463
+ },
464
+ expectedResult: new()
465
+ {
466
+ UpdatedVersion = "1.2.3",
467
+ CanUpdate = true,
468
+ VersionComesFromMultiDependencyProperty = false,
469
+ UpdatedDependencies =
470
+ [
471
+ new("Some.Package", "1.2.3", DependencyType.Unknown, TargetFrameworks: ["net8.0"]),
472
+ ],
473
+ }
474
+ );
475
+ }
476
+
477
+ [Fact]
478
+ public async Task VersionFinderCanHandle404FromPackageSource_V3()
479
+ {
480
+ static (int, byte[]) TestHttpHandler1(string uriString)
481
+ {
482
+ // this is a valid nuget package source, but doesn't contain anything
483
+ var uri = new Uri(uriString, UriKind.Absolute);
484
+ var baseUrl = $"{uri.Scheme}://{uri.Host}:{uri.Port}";
485
+ return uri.PathAndQuery switch
486
+ {
487
+ "/index.json" => (200, Encoding.UTF8.GetBytes($$"""
488
+ {
489
+ "version": "3.0.0",
490
+ "resources": [
491
+ {
492
+ "@id": "{{baseUrl}}/download",
493
+ "@type": "PackageBaseAddress/3.0.0"
494
+ },
495
+ {
496
+ "@id": "{{baseUrl}}/query",
497
+ "@type": "SearchQueryService"
498
+ },
499
+ {
500
+ "@id": "{{baseUrl}}/registrations",
501
+ "@type": "RegistrationsBaseUrl"
502
+ }
503
+ ]
504
+ }
505
+ """)),
506
+ _ => (404, Encoding.UTF8.GetBytes("{}")), // nothing else is found
507
+ };
508
+ }
509
+ var desktopAppRefPackage = MockNuGetPackage.WellKnownReferencePackage("Microsoft.WindowsDesktop.App", "net8.0");
510
+ (int, byte[]) TestHttpHandler2(string uriString)
511
+ {
512
+ // this contains the actual package
513
+ var uri = new Uri(uriString, UriKind.Absolute);
514
+ var baseUrl = $"{uri.Scheme}://{uri.Host}:{uri.Port}";
515
+ switch (uri.PathAndQuery)
516
+ {
517
+ case "/index.json":
518
+ return (200, Encoding.UTF8.GetBytes($$"""
519
+ {
520
+ "version": "3.0.0",
521
+ "resources": [
522
+ {
523
+ "@id": "{{baseUrl}}/download",
524
+ "@type": "PackageBaseAddress/3.0.0"
525
+ },
526
+ {
527
+ "@id": "{{baseUrl}}/query",
528
+ "@type": "SearchQueryService"
529
+ },
530
+ {
531
+ "@id": "{{baseUrl}}/registrations",
532
+ "@type": "RegistrationsBaseUrl"
533
+ }
534
+ ]
535
+ }
536
+ """));
537
+ case "/registrations/some.package/index.json":
538
+ return (200, Encoding.UTF8.GetBytes("""
539
+ {
540
+ "count": 1,
541
+ "items": [
542
+ {
543
+ "lower": "1.0.0",
544
+ "upper": "1.2.3",
545
+ "items": [
546
+ {
547
+ "catalogEntry": {
548
+ "listed": true,
549
+ "version": "1.0.0"
550
+ }
551
+ },
552
+ {
553
+ "catalogEntry": {
554
+ "listed": true,
555
+ "version": "1.2.3"
556
+ }
557
+ }
558
+ ]
559
+ }
560
+ ]
561
+ }
562
+ """));
563
+ case "/download/some.package/index.json":
564
+ return (200, Encoding.UTF8.GetBytes("""
565
+ {
566
+ "versions": [
567
+ "1.0.0",
568
+ "1.2.3"
569
+ ]
570
+ }
571
+ """));
572
+ case "/download/microsoft.windowsdesktop.app.ref/index.json":
573
+ return (200, Encoding.UTF8.GetBytes($$"""
574
+ {
575
+ "versions": [
576
+ "{{desktopAppRefPackage.Version}}"
577
+ ]
578
+ }
579
+ """));
580
+ case "/download/some.package/1.0.0/some.package.1.0.0.nupkg":
581
+ return (200, MockNuGetPackage.CreateSimplePackage("Some.Package", "1.0.0", "net8.0").GetZipStream().ReadAllBytes());
582
+ case "/download/some.package/1.2.3/some.package.1.2.3.nupkg":
583
+ return (200, MockNuGetPackage.CreateSimplePackage("Some.Package", "1.2.3", "net8.0").GetZipStream().ReadAllBytes());
584
+ default:
585
+ if (uri.PathAndQuery == $"/download/microsoft.windowsdesktop.app.ref/{desktopAppRefPackage.Version}/microsoft.windowsdesktop.app.ref.{desktopAppRefPackage.Version}.nupkg")
586
+ {
587
+ return (200, desktopAppRefPackage.GetZipStream().ReadAllBytes());
588
+ }
589
+
590
+ // nothing else is found
591
+ return (404, Encoding.UTF8.GetBytes("{}"));
592
+ };
593
+ }
594
+ using var http1 = TestHttpServer.CreateTestServer(TestHttpHandler1);
595
+ using var http2 = TestHttpServer.CreateTestServer(TestHttpHandler2);
596
+ await TestAnalyzeAsync(
597
+ extraFiles:
598
+ [
599
+ ("NuGet.Config", $"""
600
+ <configuration>
601
+ <packageSources>
602
+ <clear />
603
+ <add key="package_feed_1" value="{http1.BaseUrl.TrimEnd('/')}/index.json" allowInsecureConnections="true" />
604
+ <add key="package_feed_2" value="{http2.BaseUrl.TrimEnd('/')}/index.json" allowInsecureConnections="true" />
605
+ </packageSources>
606
+ </configuration>
607
+ """)
608
+ ],
609
+ discovery: new()
610
+ {
611
+ Path = "/",
612
+ Projects =
613
+ [
614
+ new()
615
+ {
616
+ FilePath = "./project.csproj",
617
+ TargetFrameworks = ["net8.0"],
618
+ Dependencies =
619
+ [
620
+ new("Some.Package", "1.0.0", DependencyType.PackageReference),
621
+ ]
622
+ }
623
+ ]
624
+ },
625
+ dependencyInfo: new()
626
+ {
627
+ Name = "Some.Package",
628
+ Version = "1.0.0",
629
+ IgnoredVersions = [],
630
+ IsVulnerable = false,
631
+ Vulnerabilities = [],
632
+ },
633
+ expectedResult: new()
634
+ {
635
+ UpdatedVersion = "1.2.3",
636
+ CanUpdate = true,
637
+ VersionComesFromMultiDependencyProperty = false,
638
+ UpdatedDependencies =
639
+ [
640
+ new("Some.Package", "1.2.3", DependencyType.Unknown, TargetFrameworks: ["net8.0"]),
641
+ ],
642
+ }
643
+ );
644
+ }
645
+
646
+ [Fact]
647
+ public async Task ResultFileHasCorrectShapeForAuthenticationFailure()
648
+ {
649
+ using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync([]);
650
+ await AnalyzeWorker.WriteResultsAsync(temporaryDirectory.DirectoryPath, "Some.Dependency", new()
651
+ {
652
+ ErrorType = ErrorType.AuthenticationFailure,
653
+ ErrorDetails = "<some package feed>",
654
+ UpdatedVersion = "",
655
+ UpdatedDependencies = [],
656
+ }, new Logger(false));
657
+ var discoveryContents = await File.ReadAllTextAsync(Path.Combine(temporaryDirectory.DirectoryPath, "Some.Dependency.json"));
658
+
659
+ // raw result file should look like this:
660
+ // {
661
+ // ...
662
+ // "ErrorType": "AuthenticationFailure",
663
+ // "ErrorDetails": "<some package feed>",
664
+ // ...
665
+ // }
666
+ var jsonDocument = JsonDocument.Parse(discoveryContents);
667
+ var errorType = jsonDocument.RootElement.GetProperty("ErrorType");
668
+ var errorDetails = jsonDocument.RootElement.GetProperty("ErrorDetails");
669
+
670
+ Assert.Equal("AuthenticationFailure", errorType.GetString());
671
+ Assert.Equal("<some package feed>", errorDetails.GetString());
672
+ }
673
+
674
+ [Fact]
675
+ public async Task ReportsPrivateSourceAuthenticationFailure()
676
+ {
677
+ static (int, string) TestHttpHandler(string uriString)
678
+ {
679
+ var uri = new Uri(uriString, UriKind.Absolute);
680
+ var baseUrl = $"{uri.Scheme}://{uri.Host}:{uri.Port}";
681
+ return uri.PathAndQuery switch
682
+ {
683
+ // initial request is good
684
+ "/index.json" => (200, $$"""
685
+ {
686
+ "version": "3.0.0",
687
+ "resources": [
688
+ {
689
+ "@id": "{{baseUrl}}/download",
690
+ "@type": "PackageBaseAddress/3.0.0"
691
+ },
692
+ {
693
+ "@id": "{{baseUrl}}/query",
694
+ "@type": "SearchQueryService"
695
+ },
696
+ {
697
+ "@id": "{{baseUrl}}/registrations",
698
+ "@type": "RegistrationsBaseUrl"
699
+ }
700
+ ]
701
+ }
702
+ """),
703
+ // all other requests are unauthorized
704
+ _ => (401, "{}"),
705
+ };
706
+ }
707
+ using var http = TestHttpServer.CreateTestStringServer(TestHttpHandler);
708
+ await TestAnalyzeAsync(
709
+ extraFiles:
710
+ [
711
+ ("NuGet.Config", $"""
712
+ <configuration>
713
+ <packageSources>
714
+ <clear />
715
+ <add key="private_feed" value="{http.BaseUrl.TrimEnd('/')}/index.json" allowInsecureConnections="true" />
716
+ </packageSources>
717
+ </configuration>
718
+ """)
719
+ ],
720
+ discovery: new()
721
+ {
722
+ Path = "/",
723
+ Projects = [
724
+ new()
725
+ {
726
+ FilePath = "./project.csproj",
727
+ TargetFrameworks = ["net8.0"],
728
+ Dependencies = [
729
+ new("Some.Package", "1.2.3", DependencyType.PackageReference),
730
+ ],
731
+ }
732
+ ]
733
+ },
734
+ dependencyInfo: new()
735
+ {
736
+ Name = "Some.Package",
737
+ Version = "1.2.3",
738
+ IgnoredVersions = [],
739
+ IsVulnerable = false,
740
+ Vulnerabilities = [],
741
+ },
742
+ expectedResult: new()
743
+ {
744
+ ErrorType = ErrorType.AuthenticationFailure,
745
+ ErrorDetails = $"({http.BaseUrl.TrimEnd('/')}/index.json)",
746
+ UpdatedVersion = string.Empty,
747
+ CanUpdate = false,
748
+ UpdatedDependencies = [],
749
+ }
750
+ );
751
+ }
304
752
  }