dependabot-nuget 0.265.0 → 0.266.0

Sign up to get free protection for your applications and to get access to all the features.
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
  }