Skip to content

Commit

Permalink
New feature: Add support HttpClient timeout configuration (`sonar.htt…
Browse files Browse the repository at this point in the history
…p.timeout` parameter) (#1878)
  • Loading branch information
costin-zaharia-sonarsource authored Feb 16, 2024
1 parent 0fbf356 commit 53e50fc
Show file tree
Hide file tree
Showing 19 changed files with 176 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public class SonarPropertiesTests
SonarProperties.VsTestReportsPaths,
SonarProperties.WorkingDirectory,
SonarProperties.CacheBaseUrl,
SonarProperties.HttpTimeout
};

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,8 @@ public void PreArgProc_DynamicSettings()
AssertExpectedPropertyValue("key1", "value1", result);
AssertExpectedPropertyValue("key2", "value two with spaces", result);

result.GetAllProperties().Should().NotBeNull("GetAllProperties should not return null");
result.GetAllProperties().Should().HaveCount(3, "Unexpected number of properties");
result.AllProperties().Should().NotBeNull("GetAllProperties should not return null");
result.AllProperties().Should().HaveCount(3, "Unexpected number of properties");
}

[TestMethod]
Expand Down Expand Up @@ -445,6 +445,17 @@ public void PreArgProc_Organization()
args.Organization.Should().BeNull();
}

[TestMethod]
public void PreArgProc_Timeout()
{
CheckProcessingSucceeds("/key:k1", "/d:sonar.http.timeout=1").HttpTimeout.Should().Be(TimeSpan.FromSeconds(1));
CheckProcessingSucceeds("/key:k1", "/d:sonar.http.timeout=2").HttpTimeout.Should().Be(TimeSpan.FromSeconds(2));
CheckProcessingSucceeds("/key:k1").HttpTimeout.Should().Be(TimeSpan.FromSeconds(100));
CheckProcessingSucceeds("/key:k1", "/d:sonar.http.timeout=invalid").HttpTimeout.Should().Be(TimeoutProvider.DefaultHttpTimeout);
CheckProcessingSucceeds("/key:k1", "/d:sonar.http.timeout=-1").HttpTimeout.Should().Be(TimeoutProvider.DefaultHttpTimeout);
CheckProcessingSucceeds("/key:k1", "/d:sonar.http.timeout=0").HttpTimeout.Should().Be(TimeoutProvider.DefaultHttpTimeout);
}

#endregion Tests

#region Checks
Expand Down Expand Up @@ -505,7 +516,7 @@ private static void AssertExpectedPropertyValue(string key, string value, Proces
actualValue.Should().Be(value, "Dynamic setting does not have the expected value");

// Check the public list of properties
var found = Property.TryGetProperty(key, actual.GetAllProperties(), out Property match);
var found = Property.TryGetProperty(key, actual.AllProperties(), out Property match);
found.Should().BeTrue("Failed to find the expected property. Key: {0}", key);
match.Should().NotBeNull("Returned property should not be null. Key: {0}", key);
match.Value.Should().Be(value, "Property does not have the expected value");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class SonarCloudWebServerTest
private const string Token = "42";
private const string Organization = "org42";

private readonly TimeSpan httpTimeout = TimeSpan.FromSeconds(42);
private readonly TestDownloader downloader;
private readonly Version version;
private readonly TestLogger logger;
Expand All @@ -61,28 +62,29 @@ public SonarCloudWebServerTest()
}

[TestInitialize]
public void Init() => sut = new SonarCloudWebServer(downloader, version, logger, Organization);
public void Init() =>
sut = new SonarCloudWebServer(downloader, version, logger, Organization, httpTimeout);

[TestCleanup]
public void Cleanup() =>
sut?.Dispose();

[TestMethod]
public void Ctor_OrganizationNull_ShouldThrow() =>
((Func<SonarCloudWebServer>)(() => new SonarCloudWebServer(downloader, version, logger, null))).Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("organization");
((Func<SonarCloudWebServer>)(() => new SonarCloudWebServer(downloader, version, logger, null, httpTimeout))).Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("organization");

[TestMethod]
public void IsServerVersionSupported_IsSonarCloud_ShouldReturnTrue()
{
sut = new SonarCloudWebServer(downloader, version, logger, Organization);
sut = new SonarCloudWebServer(downloader, version, logger, Organization, httpTimeout);

sut.IsServerVersionSupported().Should().BeTrue();
}

[TestMethod]
public async Task IsLicenseValid_IsSonarCloud_ShouldReturnTrue()
{
sut = new SonarCloudWebServer(downloader, version, logger, Organization);
sut = new SonarCloudWebServer(downloader, version, logger, Organization, httpTimeout);

(await sut.IsServerLicenseValid()).Should().BeTrue();
}
Expand All @@ -98,7 +100,7 @@ public async Task IsLicenseValid_AlwaysValid()
[TestMethod]
public void WarnIfDeprecated_ShouldNotWarn()
{
sut = new SonarCloudWebServer(downloader, new Version("0.0.1"), logger, Organization);
sut = new SonarCloudWebServer(downloader, new Version("0.0.1"), logger, Organization, httpTimeout);

logger.Warnings.Should().BeEmpty();
}
Expand Down Expand Up @@ -138,7 +140,7 @@ public void DownloadProperties_Success()
]
}
]}"));
sut = new SonarCloudWebServer(downloaderMock.Object, version, logger, Organization);
sut = new SonarCloudWebServer(downloaderMock.Object, version, logger, Organization, httpTimeout);

var result = sut.DownloadProperties("comp", null).Result;

Expand All @@ -154,7 +156,7 @@ public void DownloadProperties_Success()
[TestMethod]
public void DownloadProperties_NullProjectKey_Throws()
{
sut = new SonarCloudWebServer(downloader, version, logger, Organization);
sut = new SonarCloudWebServer(downloader, version, logger, Organization, httpTimeout);

Action act = () => _ = sut.DownloadProperties(null, null).Result;

Expand Down Expand Up @@ -183,7 +185,7 @@ public async Task DownloadCache_NullArgument()
[DataRow("project", "branch", "token", "Incremental PR analysis: CacheBaseUrl was not successfully retrieved.")]
public async Task DownloadCache_InvalidArguments(string projectKey, string branch, string token, string infoMessage)
{
sut = new SonarCloudWebServer(MockIDownloader(), version, logger, Organization);
sut = new SonarCloudWebServer(MockIDownloader(), version, logger, Organization, httpTimeout);
var localSettings = CreateLocalSettings(projectKey, branch, Organization, token);

var res = await sut.DownloadCache(localSettings);
Expand All @@ -205,7 +207,7 @@ public async Task DownloadCache_AutomaticallyDeduceBaseBranch(string provider, s
const string organization = "org42";
using var stream = new MemoryStream();
var handler = MockHttpHandler("http://myhost:222/v1/sensor_cache/prepare_read?organization=org42&project=project-key&branch=branch-42", "https://www.ephemeralUrl.com", stream);
sut = new SonarCloudWebServer(MockIDownloader("http://myhost:222"), version, logger, organization, handler.Object);
sut = new SonarCloudWebServer(MockIDownloader("http://myhost:222"), version, logger, organization, httpTimeout, handler.Object);
var localSettings = CreateLocalSettings(ProjectKey, null, organization, Token);

await sut.DownloadCache(localSettings);
Expand All @@ -227,7 +229,7 @@ public async Task DownloadCache_UserInputSupersedesAutomaticDetection(string var
const string organization = "org42";
using var stream = new MemoryStream();
var handler = MockHttpHandler("http://myhost:222/v1/sensor_cache/prepare_read?organization=org42&project=project-key&branch=project-branch", "https://www.ephemeralUrl.com", stream);
sut = new SonarCloudWebServer(MockIDownloader("http://myhost:222"), version, logger, organization, handler.Object);
sut = new SonarCloudWebServer(MockIDownloader("http://myhost:222"), version, logger, organization, httpTimeout, handler.Object);
var localSettings = CreateLocalSettings(ProjectKey, ProjectBranch, organization, Token);

await sut.DownloadCache(localSettings);
Expand All @@ -246,7 +248,7 @@ public async Task DownloadCache_RequestUrl(string cacheBaseUrl, string cacheFull
using var stream = new MemoryStream();
var handler = MockHttpHandler(cacheFullUrl, "https://www.ephemeralUrl.com", stream);

sut = new SonarCloudWebServer(MockIDownloader(cacheBaseUrl), version, logger, organization, handler.Object);
sut = new SonarCloudWebServer(MockIDownloader(cacheBaseUrl), version, logger, organization, httpTimeout, handler.Object);
var localSettings = CreateLocalSettings(ProjectKey, ProjectBranch, organization, Token);

var result = await sut.DownloadCache(localSettings);
Expand All @@ -265,7 +267,7 @@ public async Task DownloadCache_CacheHit(string tokenKey)
var cacheFullUrl = $"https://www.cacheBaseUrl.com/v1/sensor_cache/prepare_read?organization={Organization}&project=project-key&branch=project-branch";
using var stream = CreateCacheStream(new SensorCacheEntry { Key = "key", Data = ByteString.CopyFromUtf8("value") });
var handler = MockHttpHandler(cacheFullUrl, "https://www.ephemeralUrl.com", stream);
sut = new SonarCloudWebServer(MockIDownloader(cacheBaseUrl), version, logger, Organization, handler.Object);
sut = new SonarCloudWebServer(MockIDownloader(cacheBaseUrl), version, logger, Organization, httpTimeout, handler.Object);
var localSettings = CreateLocalSettings(ProjectKey, ProjectBranch, Organization, Token, tokenKey);

var result = await sut.DownloadCache(localSettings);
Expand All @@ -282,7 +284,7 @@ public async Task DownloadCache_PrepareRead_UnsuccessfulResponse()
const string cacheBaseUrl = "https://www.cacheBaseUrl.com";
var cacheFullUrl = $"https://www.cacheBaseUrl.com/v1/sensor_cache/prepare_read?organization={Organization}&project=project-key&branch=project-branch";
var handler = MockHttpHandler(cacheFullUrl, "irrelevant", HttpStatusCode.Forbidden);
sut = new SonarCloudWebServer(MockIDownloader(cacheBaseUrl), version, logger, Organization, handler.Object);
sut = new SonarCloudWebServer(MockIDownloader(cacheBaseUrl), version, logger, Organization, httpTimeout, handler.Object);
var localSettings = CreateLocalSettings(ProjectKey, ProjectBranch, Organization, Token);

var result = await sut.DownloadCache(localSettings);
Expand All @@ -298,7 +300,7 @@ public async Task DownloadCache_PrepareRead_EmptyResponse()
const string cacheBaseUrl = "https://www.cacheBaseUrl.com";
var cacheFullUrl = $"https://www.cacheBaseUrl.com/v1/sensor_cache/prepare_read?organization={Organization}&project=project-key&branch=project-branch";
var handler = MockHttpHandler(cacheFullUrl, string.Empty);
sut = new SonarCloudWebServer(MockIDownloader(cacheBaseUrl), version, logger, Organization, handler.Object);
sut = new SonarCloudWebServer(MockIDownloader(cacheBaseUrl), version, logger, Organization, httpTimeout, handler.Object);
var localSettings = CreateLocalSettings(ProjectKey, ProjectBranch, Organization, Token);

var result = await sut.DownloadCache(localSettings);
Expand All @@ -314,7 +316,7 @@ public async Task DownloadCache_PrepareRead_CacheDisabled()
const string cacheBaseUrl = "https://www.cacheBaseUrl.com";
var cacheFullUrl = $"https://www.cacheBaseUrl.com/v1/sensor_cache/prepare_read?organization={Organization}&project=project-key&branch=project-branch";
var handler = MockHttpHandler(cacheFullUrl, $@"{{ ""enabled"": ""false"", ""url"":""https://www.sonarsource.com"" }}");
sut = new SonarCloudWebServer(MockIDownloader(cacheBaseUrl), version, logger, Organization, handler.Object);
sut = new SonarCloudWebServer(MockIDownloader(cacheBaseUrl), version, logger, Organization, httpTimeout, handler.Object);
var localSettings = CreateLocalSettings(ProjectKey, ProjectBranch, Organization, Token);

var result = await sut.DownloadCache(localSettings);
Expand All @@ -330,7 +332,7 @@ public async Task DownloadCache_PrepareRead_CacheEnabledButUrlMissing()
const string cacheBaseUrl = "https://www.cacheBaseUrl.com";
var cacheFullUrl = $"https://www.cacheBaseUrl.com/v1/sensor_cache/prepare_read?organization={Organization}&project=project-key&branch=project-branch";
var handler = MockHttpHandler(cacheFullUrl, $@"{{ ""enabled"": ""true"" }}");
sut = new SonarCloudWebServer(MockIDownloader(cacheBaseUrl), version, logger, Organization, handler.Object);
sut = new SonarCloudWebServer(MockIDownloader(cacheBaseUrl), version, logger, Organization, httpTimeout, handler.Object);
var localSettings = CreateLocalSettings(ProjectKey, ProjectBranch, Organization, Token);

var result = await sut.DownloadCache(localSettings);
Expand All @@ -348,7 +350,7 @@ public async Task DownloadCache_ThrowException()

using var stream = new MemoryStream(new byte[] { 42, 42 }); // this is a random byte array that fails deserialization
var handler = MockHttpHandler(cacheFullUrl, "https://www.ephemeralUrl.com", stream);
sut = new SonarCloudWebServer(MockIDownloader(cacheBaseUrl), version, logger, Organization, handler.Object);
sut = new SonarCloudWebServer(MockIDownloader(cacheBaseUrl), version, logger, Organization, httpTimeout, handler.Object);
var localSettings = CreateLocalSettings(ProjectKey, ProjectBranch, Organization, Token);

var result = await sut.DownloadCache(localSettings);
Expand All @@ -374,7 +376,7 @@ public async Task DownloadRules_SonarCloud()
""type"": ""BUG""
}
]}";
sut = new SonarCloudWebServer(testDownloader, version, logger, Organization);
sut = new SonarCloudWebServer(testDownloader, version, logger, Organization, httpTimeout);

var rules = await sut.DownloadRules("qp");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class WebClientDownloaderBuilderTest
private const string CertificatePath = "certtestsonar.pem";
private const string CertificatePassword = "dummypw";
private const string BaseAddress = "https://sonarsource.com/";
private readonly TimeSpan httpTimeout = TimeSpan.FromSeconds(42);
private TestLogger logger;

[TestInitialize]
Expand All @@ -48,7 +49,7 @@ public void TestInitialize() =>
[DataRow("admin", "password", "Basic YWRtaW46cGFzc3dvcmQ=")]
public void Build_WithAuthorization_ShouldHaveAuthorizationHeader(string username, string password, string expected)
{
var sut = new WebClientDownloaderBuilder(BaseAddress, logger);
var sut = new WebClientDownloaderBuilder(BaseAddress, httpTimeout, logger);

var result = sut.AddAuthorization(username, password).Build();

Expand All @@ -59,7 +60,7 @@ public void Build_WithAuthorization_ShouldHaveAuthorizationHeader(string usernam
public void Build_BasicBuild_UserAgentShouldBeSet()
{
var scannerVersion = typeof(WebClientDownloaderTest).Assembly.GetName().Version.ToDisplayString();
var sut = new WebClientDownloaderBuilder(BaseAddress, logger);
var sut = new WebClientDownloaderBuilder(BaseAddress, httpTimeout, logger);

var result = sut.Build();

Expand All @@ -69,7 +70,7 @@ public void Build_BasicBuild_UserAgentShouldBeSet()
[TestMethod]
public void Build_FullBuild_ShouldSucceed()
{
var sut = new WebClientDownloaderBuilder(BaseAddress, logger);
var sut = new WebClientDownloaderBuilder(BaseAddress, httpTimeout, logger);

var result = sut.AddCertificate(CertificatePath, CertificatePassword).AddAuthorization("admin", "password").Build();

Expand All @@ -79,7 +80,7 @@ public void Build_FullBuild_ShouldSucceed()
[TestMethod]
public void AddAuthorization_UserNameWithSemiColon_ShouldThrow()
{
Action act = () => new WebClientDownloaderBuilder(BaseAddress, logger).AddAuthorization("admin:name", string.Empty);
Action act = () => new WebClientDownloaderBuilder(BaseAddress, httpTimeout, logger).AddAuthorization("admin:name", string.Empty);

act.Should().ThrowExactly<ArgumentException>().WithMessage("username cannot contain the ':' character due to basic authentication limitations");
}
Expand All @@ -94,7 +95,7 @@ public void AddAuthorization_UserNameWithSemiColon_ShouldThrow()
[DataRow("höhö")]
public void AddAuthorization_NonAsciiUserName_ShouldThrow(string userName)
{
Action act = () => new WebClientDownloaderBuilder(BaseAddress, logger).AddAuthorization(userName, "password");
Action act = () => new WebClientDownloaderBuilder(BaseAddress, httpTimeout, logger).AddAuthorization(userName, "password");

act.Should().ThrowExactly<ArgumentException>().WithMessage("username and password should contain only ASCII characters due to basic authentication limitations");
}
Expand All @@ -109,25 +110,25 @@ public void AddAuthorization_NonAsciiUserName_ShouldThrow(string userName)
[DataRow("höhö")]
public void AddAuthorization_NonAsciiPassword_ShouldThrow(string password)
{
Action act = () => new WebClientDownloaderBuilder(BaseAddress, logger).AddAuthorization("userName", password);
Action act = () => new WebClientDownloaderBuilder(BaseAddress, httpTimeout, logger).AddAuthorization("userName", password);

act.Should().ThrowExactly<ArgumentException>().WithMessage("username and password should contain only ASCII characters due to basic authentication limitations");
}

[TestMethod]
public void AddCertificate_ExistingCertificateWithValidPassword_ShouldNotThrow() =>
FluentActions.Invoking(() => new WebClientDownloaderBuilder(BaseAddress, logger).AddCertificate(CertificatePath, CertificatePassword)).Should().NotThrow();
FluentActions.Invoking(() => new WebClientDownloaderBuilder(BaseAddress, httpTimeout, logger).AddCertificate(CertificatePath, CertificatePassword)).Should().NotThrow();

[TestMethod]
[DataRow(null, "something")]
[DataRow("something", null)]
[DataRow(null, null)]
public void AddCertificate_NullParameter_ShouldNotThrow(string clientCertPath, string clientCertPassword) =>
FluentActions.Invoking(() => new WebClientDownloaderBuilder(BaseAddress, logger).AddCertificate(clientCertPath, clientCertPassword)).Should().NotThrow();
FluentActions.Invoking(() => new WebClientDownloaderBuilder(BaseAddress, httpTimeout, logger).AddCertificate(clientCertPath, clientCertPassword)).Should().NotThrow();

[TestMethod]
public void AddCertificate_CertificateDoesNotExist_ShouldThrow() =>
FluentActions.Invoking(() => new WebClientDownloaderBuilder(BaseAddress, logger).AddCertificate("missingcert.pem", "dummypw")).Should().Throw<CryptographicException>();
FluentActions.Invoking(() => new WebClientDownloaderBuilder(BaseAddress, httpTimeout, logger).AddCertificate("missingcert.pem", "dummypw")).Should().Throw<CryptographicException>();

private static string GetHeader(WebClientDownloader downloader, string header)
{
Expand Down
Loading

0 comments on commit 53e50fc

Please sign in to comment.