Skip to content

Commit 0f2a7cf

Browse files
committed
C#: Make FeedManager unit-testable.
1 parent 29e1392 commit 0f2a7cf

3 files changed

Lines changed: 134 additions & 94 deletions

File tree

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs

Lines changed: 9 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Collections.Immutable;
4-
using System.IO;
54
using System.Linq;
6-
using System.Net;
7-
using System.Net.Http;
8-
using System.Security.Cryptography.X509Certificates;
9-
using System.Text;
105
using System.Text.RegularExpressions;
11-
using System.Threading;
12-
using System.Threading.Tasks;
136
using Semmle.Util;
147
using Semmle.Util.Logging;
158

@@ -22,9 +15,9 @@ internal sealed partial class FeedManager : IDisposable
2215
private readonly ILogger logger;
2316
private readonly IDotNet dotnet;
2417
private readonly IFileProvider fileProvider;
25-
private readonly IDependabotProxy? dependabotProxy;
2618
private readonly DependencyDirectory emptyPackageDirectory;
2719
private readonly ImmutableHashSet<string> privateRegistryFeeds;
20+
private readonly IFeedManagerIO feedManagerIo;
2821

2922
/// <summary>
3023
/// Gets whether there are private package registries configured for C#.
@@ -79,12 +72,12 @@ internal sealed partial class FeedManager : IDisposable
7972
/// </summary>
8073
public ImmutableHashSet<string> ReachableFallbackFeeds => lazyReachableFallbackFeeds.Value;
8174

82-
public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotProxy, IFileProvider fileProvider)
75+
public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotProxy, IFileProvider fileProvider, IFeedManagerIO feedManagerIo)
8376
{
8477
this.logger = logger;
8578
this.dotnet = dotnet;
86-
this.dependabotProxy = dependabotProxy;
8779
this.fileProvider = fileProvider;
80+
this.feedManagerIo = feedManagerIo;
8881
privateRegistryFeeds = dependabotProxy?.RegistryURLs.ToImmutableHashSet() ?? [];
8982
HasPrivateRegistryFeeds = privateRegistryFeeds.Count > 0;
9083
emptyPackageDirectory = new DependencyDirectory("empty", "empty package", logger);
@@ -105,17 +98,9 @@ public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotP
10598
});
10699
}
107100

108-
private string? GetDirectoryName(string path)
101+
public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotProxy, IFileProvider fileProvider)
102+
: this(logger, dotnet, dependabotProxy, fileProvider, new FeedManagerIO(logger, dependabotProxy))
109103
{
110-
try
111-
{
112-
return new FileInfo(path).Directory?.FullName;
113-
}
114-
catch (Exception exc)
115-
{
116-
logger.LogWarning($"Failed to get directory of '{path}': {exc}");
117-
}
118-
return null;
119104
}
120105

121106
private IEnumerable<string> GetFeeds(Func<IList<string>> getNugetFeeds)
@@ -193,7 +178,7 @@ private IEnumerable<string> FeedsToUseAux(HashSet<string> feedsToConsider)
193178
public IEnumerable<string> FeedsToUse(string path)
194179
{
195180
// Find the path specific feeds.
196-
var folder = GetDirectoryName(path);
181+
var folder = feedManagerIo.GetDirectoryName(path);
197182
var feedsToConsider = folder is not null ? GetFeedsFromFolder(folder).ToHashSet() : new HashSet<string>();
198183

199184
return FeedsToUseAux(feedsToConsider);
@@ -238,76 +223,6 @@ public List<string> MakeRestoreFeeds(string path)
238223
return (timeoutMilliSeconds, tryCount);
239224
}
240225

241-
private static async Task<HttpResponseMessage> ExecuteGetRequest(string address, HttpClient httpClient, CancellationToken cancellationToken)
242-
{
243-
return await httpClient.GetAsync(address, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
244-
}
245-
246-
private bool IsFeedReachable(string feed, int timeoutMilliSeconds, int tryCount)
247-
{
248-
logger.LogInfo($"Checking if NuGet feed '{feed}' is reachable...");
249-
250-
// Configure the HttpClient to be aware of the Dependabot Proxy, if used.
251-
HttpClientHandler httpClientHandler = new();
252-
if (dependabotProxy != null)
253-
{
254-
httpClientHandler.Proxy = new WebProxy(dependabotProxy.Address);
255-
256-
if (dependabotProxy.Certificate != null)
257-
{
258-
httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, _) =>
259-
{
260-
if (chain is null || cert is null)
261-
{
262-
var msg = cert is null && chain is null
263-
? "certificate and chain"
264-
: chain is null
265-
? "chain"
266-
: "certificate";
267-
logger.LogWarning($"Dependabot proxy certificate validation failed due to missing {msg}");
268-
return false;
269-
}
270-
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
271-
chain.ChainPolicy.CustomTrustStore.Add(dependabotProxy.Certificate);
272-
return chain.Build(cert);
273-
};
274-
}
275-
}
276-
277-
using HttpClient client = new(httpClientHandler);
278-
279-
for (var i = 0; i < tryCount; i++)
280-
{
281-
using var cts = new CancellationTokenSource();
282-
cts.CancelAfter(timeoutMilliSeconds);
283-
try
284-
{
285-
logger.LogInfo($"Attempt {i + 1}/{tryCount} to reach NuGet feed '{feed}'.");
286-
using var response = ExecuteGetRequest(feed, client, cts.Token).GetAwaiter().GetResult();
287-
response.EnsureSuccessStatusCode();
288-
logger.LogInfo($"Querying NuGet feed '{feed}' succeeded.");
289-
return true;
290-
}
291-
catch (Exception exc)
292-
{
293-
if (exc is TaskCanceledException tce &&
294-
tce.CancellationToken == cts.Token &&
295-
cts.Token.IsCancellationRequested)
296-
{
297-
logger.LogInfo($"Didn't receive answer from NuGet feed '{feed}' in {timeoutMilliSeconds}ms.");
298-
timeoutMilliSeconds *= 2;
299-
continue;
300-
}
301-
302-
logger.LogInfo($"Querying NuGet feed '{feed}' failed. The reason for the failure: {exc.Message}");
303-
return false;
304-
}
305-
}
306-
307-
logger.LogWarning($"Didn't receive answer from NuGet feed '{feed}'. Tried it {tryCount} times.");
308-
return false;
309-
}
310-
311226
/// <summary>
312227
/// Retrieves a list of excluded NuGet feeds from the corresponding environment variable.
313228
/// </summary>
@@ -361,7 +276,7 @@ public bool IsDefaultFeedReachable()
361276
if (CheckNugetFeedResponsiveness)
362277
{
363278
var (initialTimeout, tryCount) = GetFeedRequestSettings(isFallback: false);
364-
return IsFeedReachable(PublicNugetOrgFeed, initialTimeout, tryCount);
279+
return feedManagerIo.IsFeedReachable(PublicNugetOrgFeed, initialTimeout, tryCount);
365280
}
366281

367282
return true;
@@ -380,7 +295,7 @@ private List<string> GetReachableNuGetFeeds(HashSet<string> feedsToCheck, bool i
380295

381296
var (initialTimeout, tryCount) = GetFeedRequestSettings(isFallback);
382297
var reachableFeeds = feedsToCheck
383-
.Where(feed => IsFeedReachable(feed, initialTimeout, tryCount))
298+
.Where(feed => feedManagerIo.IsFeedReachable(feed, initialTimeout, tryCount))
384299
.ToList();
385300

386301
if (reachableFeeds.Count == 0)
@@ -464,7 +379,7 @@ private ImmutableHashSet<string> GetAllFeeds()
464379
if (nugetConfigs.Count > 0)
465380
{
466381
var nugetConfigFeeds = nugetConfigs
467-
.Select(GetDirectoryName)
382+
.Select(feedManagerIo.GetDirectoryName)
468383
.Where(folder => folder != null)
469384
.SelectMany(folder => GetFeedsFromFolder(folder!))
470385
.ToHashSet();
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
2+
using System;
3+
using System.IO;
4+
using Semmle.Util.Logging;
5+
using System.Net.Http;
6+
using System.Net;
7+
using System.Security.Cryptography.X509Certificates;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
namespace Semmle.Extraction.CSharp.DependencyFetching
12+
{
13+
public class FeedManagerIO : IFeedManagerIO
14+
{
15+
private readonly ILogger logger;
16+
private readonly IDependabotProxy? dependabotProxy;
17+
18+
public FeedManagerIO(ILogger logger, IDependabotProxy? dependabotProxy)
19+
{
20+
this.logger = logger;
21+
this.dependabotProxy = dependabotProxy;
22+
}
23+
24+
public string? GetDirectoryName(string path)
25+
{
26+
try
27+
{
28+
return new FileInfo(path).Directory?.FullName;
29+
}
30+
catch (Exception exc)
31+
{
32+
logger.LogWarning($"Failed to get directory of '{path}': {exc}");
33+
}
34+
return null;
35+
}
36+
37+
private static async Task<HttpResponseMessage> ExecuteGetRequest(string address, HttpClient httpClient, CancellationToken cancellationToken)
38+
{
39+
return await httpClient.GetAsync(address, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
40+
}
41+
42+
public bool IsFeedReachable(string feed, int timeoutMilliSeconds, int tryCount)
43+
{
44+
logger.LogInfo($"Checking if NuGet feed '{feed}' is reachable...");
45+
46+
// Configure the HttpClient to be aware of the Dependabot Proxy, if used.
47+
HttpClientHandler httpClientHandler = new();
48+
if (dependabotProxy != null)
49+
{
50+
httpClientHandler.Proxy = new WebProxy(dependabotProxy.Address);
51+
52+
if (dependabotProxy.Certificate != null)
53+
{
54+
httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, _) =>
55+
{
56+
if (chain is null || cert is null)
57+
{
58+
var msg = cert is null && chain is null
59+
? "certificate and chain"
60+
: chain is null
61+
? "chain"
62+
: "certificate";
63+
logger.LogWarning($"Dependabot proxy certificate validation failed due to missing {msg}");
64+
return false;
65+
}
66+
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
67+
chain.ChainPolicy.CustomTrustStore.Add(dependabotProxy.Certificate);
68+
return chain.Build(cert);
69+
};
70+
}
71+
}
72+
73+
using HttpClient client = new(httpClientHandler);
74+
75+
for (var i = 0; i < tryCount; i++)
76+
{
77+
using var cts = new CancellationTokenSource();
78+
cts.CancelAfter(timeoutMilliSeconds);
79+
try
80+
{
81+
logger.LogInfo($"Attempt {i + 1}/{tryCount} to reach NuGet feed '{feed}'.");
82+
using var response = ExecuteGetRequest(feed, client, cts.Token).GetAwaiter().GetResult();
83+
response.EnsureSuccessStatusCode();
84+
logger.LogInfo($"Querying NuGet feed '{feed}' succeeded.");
85+
return true;
86+
}
87+
catch (Exception exc)
88+
{
89+
if (exc is TaskCanceledException tce &&
90+
tce.CancellationToken == cts.Token &&
91+
cts.Token.IsCancellationRequested)
92+
{
93+
logger.LogInfo($"Didn't receive answer from NuGet feed '{feed}' in {timeoutMilliSeconds}ms.");
94+
timeoutMilliSeconds *= 2;
95+
continue;
96+
}
97+
98+
logger.LogInfo($"Querying NuGet feed '{feed}' failed. The reason for the failure: {exc.Message}");
99+
return false;
100+
}
101+
}
102+
103+
logger.LogWarning($"Didn't receive answer from NuGet feed '{feed}'. Tried it {tryCount} times.");
104+
return false;
105+
}
106+
107+
108+
}
109+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
namespace Semmle.Extraction.CSharp.DependencyFetching
3+
{
4+
public interface IFeedManagerIO
5+
{
6+
/// <summary>
7+
/// Gets the directory name of the specified path.
8+
/// </summary>
9+
string? GetDirectoryName(string path);
10+
11+
/// <summary>
12+
/// Returns true if the feed is reachable within the specified timeout and try count.
13+
/// </summary>
14+
bool IsFeedReachable(string feed, int timeoutMilliSeconds, int tryCount);
15+
}
16+
}

0 commit comments

Comments
 (0)