/*
* Copyright (c) 2023 Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonVPN. If not, see .
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using ARSoft.Tools.Net;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NSubstitute;
using ProtonVPN.Common.Core.Networking;
using ProtonVPN.Configurations.Contracts;
using ProtonVPN.Dns.Contracts;
using ProtonVPN.Dns.Resolvers;
using ProtonVPN.Dns.Tests.Mocks;
using RichardSzalay.MockHttp;
namespace ProtonVPN.Dns.Tests.Resolvers;
[TestClass]
public abstract class DnsOverHttpsResolverTestBase
where T : DnsOverHttpsResolverBase
{
private const string TEST_TOO_QUICK_MESSAGE =
"The test completed too quickly, meaning the request did not took several seconds before timing out.";
private const string TEST_CANCEL_TOOK_TOO_LONG_MESSAGE =
"The test took too long to complete, meaning the cancellation is not working.";
private readonly string _host;
private readonly IList _dohProviders = new List()
{
"https://dns11.quad9.net/dns-query",
"https://dns.google/dns-query",
};
private readonly IList _ipAddresses = new List()
{
new IpAddress(IPAddress.Parse("192.168.1.1")),
new IpAddress(IPAddress.Parse("192.168.2.2")),
new IpAddress(IPAddress.Parse("192.168.3.3")),
};
private MockOfLogger _logger;
private CancellationTokenSource _cancellationTokenSource;
private Stopwatch _stopwatch;
private MockHttpMessageHandler _mockHttpMessageHandler;
private MockOfHttpClientFactory _httpClientFactory;
private IDnsOverHttpsProvidersManager _mockOfDnsOverHttpsProvidersManager;
private IConfiguration _configuration;
private T _resolver;
protected DnsOverHttpsResolverTestBase(string host)
{
_host = host;
}
[TestInitialize]
public void TestInitialize()
{
_logger = new MockOfLogger();
_cancellationTokenSource = new CancellationTokenSource();
_stopwatch = new Stopwatch();
_mockHttpMessageHandler = new MockHttpMessageHandler();
_httpClientFactory = new MockOfHttpClientFactory(_mockHttpMessageHandler);
_mockOfDnsOverHttpsProvidersManager = Substitute.For();
_configuration = Substitute.For();
_configuration.DohClientTimeout.Returns(TimeSpan.FromSeconds(10));
_configuration.DnsOverHttpsPerProviderTimeout.Returns(TimeSpan.FromSeconds(20));
_configuration.DnsResolveTimeout.Returns(TimeSpan.FromSeconds(30));
_configuration.DefaultDnsTimeToLive.Returns(TimeSpan.FromMinutes(20));
}
[TestCleanup]
public void TestCleanup()
{
_logger = null;
_cancellationTokenSource = null;
_stopwatch = null;
_mockHttpMessageHandler = null;
_httpClientFactory = null;
_mockOfDnsOverHttpsProvidersManager = null;
_configuration = null;
_resolver = null;
}
[TestMethod]
public async Task TestResolveAsync()
{
InitializeResolverCorrectly();
InitializeSuccessfulRequests();
DnsResponse response = await ExecuteAsync(_host);
AssertCorrectResponse(response);
Assert.IsTrue(response.TimeToLive > TimeSpan.Zero);
Assert.IsTrue(response.ExpirationDateTimeUtc > DateTime.UtcNow);
Assert.IsTrue(response.ResponseDateTimeUtc <= DateTime.UtcNow);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(9));
}
private void InitializeSuccessfulRequests()
{
foreach (IpAddress ipAddress in _ipAddresses)
{
_mockHttpMessageHandler
.When($"https://{ipAddress}/dns-query?dns=AAABAAABAAAAAAAAA2FwaQlwcm90b252cG4CY2gAAAEAAQ")
.Respond(_ => new(HttpStatusCode.OK)
{
Content = new ByteArrayContent(
"AACBgAABAAEAAAAAA2FwaQlwcm90b252cG4CY2gAAAEAAcAMAAEAAQAAAfAABLmfn6o=".FromBase64String())
});
_mockHttpMessageHandler
.When($"https://{ipAddress}/dns-query?dns=AAABAAABAAAAAAAAG2RNRllHU0xUUU9KWFhJMzNPT1pZRzRMVEROQQlwcm90b25wcm8DeHl6AAAQAAE")
.Respond(_ => new(HttpStatusCode.OK)
{
Content = new ByteArrayContent(
"AACBgAABAAMAAAAAG2RNRllHU0xUUU9KWFhJMzNPT1pZRzRMVEROQQlwcm90b25wcm8DeHl6AAAQAAHADAAQAAEAAAB4ADMyZWMyLTMtNjUtMjYtMTQ4LmV1LWNlbnRyYWwtMS5jb21wdXRlLmFtYXpvbmF3cy5jb23ADAAQAAEAAAB4ADY1ZWMyLTE4LTE5Mi0xMTUtMTg2LmV1LWNlbnRyYWwtMS5jb21wdXRlLmFtYXpvbmF3cy5jb23ADAAQAAEAAAB4ADQzZWMyLTMtNjgtMjMyLTIwMy5ldS1jZW50cmFsLTEuY29tcHV0ZS5hbWF6b25hd3MuY29t".FromBase64String())
});
}
}
protected abstract void AssertCorrectResponse(DnsResponse response);
private async Task ExecuteAsync(string host)
{
return await ExecuteWithStopwatchAsync(() => ResolveAsync(host));
}
private async Task ResolveAsync(string host)
{
return await _resolver.ResolveAsync(host, _cancellationTokenSource.Token);
}
private async Task ExecuteWithStopwatchAsync(Func> task)
{
_stopwatch.Start();
DnsResponse response = await task();
_stopwatch.Stop();
return response;
}
private void InitializeResolverCorrectly()
{
_configuration.DohProviders.Returns(_dohProviders.ToList());
_resolver = CreateResolver(_configuration, _logger, _httpClientFactory, _mockOfDnsOverHttpsProvidersManager);
InitializeDnsOverHttpsProvidersManagerReturningSuccessfully();
}
private void InitializeDnsOverHttpsProvidersManagerReturningSuccessfully()
{
foreach (string dohProvider in _dohProviders)
{
_mockOfDnsOverHttpsProvidersManager
.GetAsync(dohProvider, Arg.Any())
.ReturnsForAnyArgs(_ipAddresses.ToList());
}
}
protected abstract T CreateResolver(IConfiguration configuration, MockOfLogger logger,
MockOfHttpClientFactory httpClientFactory, IDnsOverHttpsProvidersManager dnsOverHttpsProvidersManager);
[TestMethod]
public async Task TestResolveAsync_WithNonExistentHost()
{
string host = "g5f16gfds1gdsf5g16dsfg15fs5gfds651d61s651g6516gf1s6fdgfs.vhbverhu";
InitializeResolverCorrectly();
DnsResponse response = await ExecuteAsync(host);
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(15));
}
[TestMethod]
public async Task TestResolveAsync_WithEmptyHost()
{
string host = string.Empty;
InitializeResolverCorrectly();
DnsResponse response = await ExecuteAsync(host);
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(1));
}
[TestMethod]
public async Task TestResolveAsync_WithNullHost()
{
string host = null;
InitializeResolverCorrectly();
DnsResponse response = await ExecuteAsync(host);
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(1));
}
[TestMethod]
public async Task TestResolveAsync_WithCancelledToken()
{
string host = null;
InitializeResolverCorrectly();
_cancellationTokenSource.Cancel();
DnsResponse response = await ExecuteAsync(host);
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(1));
}
[TestMethod]
public async Task TestResolveAsync_WithWrongProviderUrls()
{
InitializeResolverWithWrongProviderUrls();
DnsResponse response = await ExecuteAsync(_host);
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(15));
}
private void InitializeResolverWithWrongProviderUrls()
{
_configuration.DohProviders.Returns(new List()
{
"https://g5f16gfds1gdsf5g16dsfg15fs5gfds651d61s651g6516gf1s6fdgfs.vhbverhu/dns-query",
"https://g5f16gfds1gdsf5g16dsfg15fs5gfds651d61s651g6516gf1s6fdgfs.vhbverhu/dns-query/",
"https://g5f16gfds1gdsf5g16dsfg15fs5gfds651d61s651g6516gf1s6fdgfs.vhbverhu/",
"https://g5f16gfds1gdsf5g16dsfg15fs5gfds651d61s651g6516gf1s6fdgfs.vhbverhu",
});
_resolver = CreateResolver(_configuration, _logger, _httpClientFactory, _mockOfDnsOverHttpsProvidersManager);
}
[TestMethod]
public async Task TestResolveAsync_WithWrongProviderIpAddresses()
{
InitializeResolverWithWrongProviderIpAddresses();
DnsResponse response = await ExecuteAsync(_host);
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(15));
}
private void InitializeResolverWithWrongProviderIpAddresses()
{
_configuration.DohProviders.Returns(new List()
{
"https://192.168.153.153",
"https://192.168.154.154",
"https://192.168.155.155",
});
_resolver = CreateResolver(_configuration, _logger, _httpClientFactory, _mockOfDnsOverHttpsProvidersManager);
InitializeDnsOverHttpsProvidersManagerTimingOut();
}
private void InitializeDnsOverHttpsProvidersManagerTimingOut()
{
foreach (string dohProvider in _dohProviders)
{
_mockOfDnsOverHttpsProvidersManager
.GetAsync(dohProvider, Arg.Any())
.ReturnsForAnyArgs(async _ => await TimeoutHttpRequestAsync());
}
}
private async Task> TimeoutHttpRequestAsync()
{
await Task.Delay(TimeSpan.FromSeconds(10), _cancellationTokenSource.Token);
throw new TimeoutException("Unit test HTTP request timeout");
}
[TestMethod]
public async Task TestResolveAsync_WhenCancelled()
{
InitializeResolverWithWrongProviderIpAddresses();
_stopwatch.Start();
Task task = Task.Run(() => _resolver.ResolveAsync(_host, _cancellationTokenSource.Token));
Task.Delay(TimeSpan.FromSeconds(3)).ContinueWith(_ => _cancellationTokenSource.Cancel());
DnsResponse response = await task;
_stopwatch.Stop();
Assert.IsNull(response);
Assert.IsTrue(_stopwatch.Elapsed > TimeSpan.FromSeconds(2), TEST_TOO_QUICK_MESSAGE);
Assert.IsTrue(_stopwatch.Elapsed < TimeSpan.FromSeconds(5), TEST_CANCEL_TOOK_TOO_LONG_MESSAGE);
}
}