using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Moq; using TeleBot.Http; using Xunit; namespace TeleBot.Tests.Security { /// /// Integration tests that verify actual TOR connectivity. /// These tests require a running TOR service on localhost:9050 /// Skip these tests if TOR is not available (CI/CD environments). /// public class TorConnectivityTests : IDisposable { private readonly Mock _mockLogger; private bool _torAvailable; public TorConnectivityTests() { _mockLogger = new Mock(); _torAvailable = CheckTorAvailability(); } /// /// Checks if TOR is available on localhost:9050 /// private bool CheckTorAvailability() { try { using var client = new TcpClient(); var result = client.BeginConnect("127.0.0.1", 9050, null, null); var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(1)); if (success) { client.EndConnect(result); return true; } return false; } catch { return false; } } [Fact] public async Task TorConnection_WhenAvailable_CanConnect() { // Skip if TOR not available if (!_torAvailable) { // Log skip reason return; } // Arrange var config = CreateTorConfiguration(); var handler = Socks5HttpHandler.Create(config, _mockLogger.Object); using var client = new HttpClient(handler); client.Timeout = TimeSpan.FromSeconds(30); // Act & Assert try { // Try to connect to TOR check service var response = await client.GetAsync("https://check.torproject.org/api/ip"); Assert.True(response.IsSuccessStatusCode, "Should successfully connect through TOR proxy"); var content = await response.Content.ReadAsStringAsync(); // The TOR check API returns JSON with "IsTor" field Assert.Contains("IsTor", content, "Response should indicate TOR connection status"); } catch (HttpRequestException ex) when (ex.InnerException is SocketException) { // TOR might not be running - skip test return; } } [Fact] public async Task TorConnection_ChecksRealIP_IsDifferent() { // Skip if TOR not available if (!_torAvailable) { return; } // Arrange var config = CreateTorConfiguration(); var torHandler = Socks5HttpHandler.Create(config, _mockLogger.Object); var directHandler = Socks5HttpHandler.CreateDirect(_mockLogger.Object); string? torIp = null; string? directIp = null; try { // Get IP through TOR using (var torClient = new HttpClient(torHandler)) { torClient.Timeout = TimeSpan.FromSeconds(30); var response = await torClient.GetAsync("https://api.ipify.org"); if (response.IsSuccessStatusCode) { torIp = await response.Content.ReadAsStringAsync(); } } // Get IP directly using (var directClient = new HttpClient(directHandler)) { directClient.Timeout = TimeSpan.FromSeconds(10); var response = await directClient.GetAsync("https://api.ipify.org"); if (response.IsSuccessStatusCode) { directIp = await response.Content.ReadAsStringAsync(); } } // Assert - IPs should be different (TOR exit node vs real IP) if (!string.IsNullOrEmpty(torIp) && !string.IsNullOrEmpty(directIp)) { Assert.NotEqual(torIp, directIp, $"TOR IP ({torIp}) should be different from direct IP ({directIp})"); } } catch (HttpRequestException) { // Network issue - skip test return; } } [Fact] public async Task TorConnection_Timeout_IsReasonable() { // Skip if TOR not available if (!_torAvailable) { return; } // Arrange var config = CreateTorConfiguration(); var handler = Socks5HttpHandler.Create(config, _mockLogger.Object); using var client = new HttpClient(handler); client.Timeout = TimeSpan.FromSeconds(30); // Act var startTime = DateTime.UtcNow; try { var response = await client.GetAsync("https://check.torproject.org"); var elapsed = DateTime.UtcNow - startTime; // Assert - TOR adds latency but should still be reasonable Assert.True(elapsed < TimeSpan.FromSeconds(30), $"TOR connection took {elapsed.TotalSeconds}s - should be under 30s"); } catch (HttpRequestException) { // Connection failed - could be TOR issue, skip return; } } [Fact] public void TorProxy_Address_IsLocalhost() { // Arrange var config = CreateTorConfiguration(); // Act var handler = Socks5HttpHandler.Create(config, _mockLogger.Object); // Assert - Security check var proxy = handler.Proxy as WebProxy; Assert.NotNull(proxy); Assert.Contains("127.0.0.1", proxy.Address?.ToString() ?? ""); Assert.DoesNotContain("0.0.0.0", proxy.Address?.ToString() ?? ""); } [Fact] public void TorProxy_Protocol_IsSocks5() { // Arrange var config = CreateTorConfiguration(); // Act var handler = Socks5HttpHandler.Create(config, _mockLogger.Object); // Assert - Verify SOCKS5 protocol var proxy = handler.Proxy as WebProxy; Assert.NotNull(proxy); Assert.Contains("socks5://", proxy.Address?.ToString() ?? ""); } // Helper to create TOR configuration private IConfiguration CreateTorConfiguration() { var configData = new Dictionary { ["Privacy:EnableTor"] = "true", ["Privacy:TorSocksPort"] = "9050" }; return new ConfigurationBuilder() .AddInMemoryCollection(configData!) .Build(); } public void Dispose() { // Cleanup if needed } } // Helper class for TCP client check class TcpClient : IDisposable { private readonly System.Net.Sockets.TcpClient _client; public TcpClient() { _client = new System.Net.Sockets.TcpClient(); } public IAsyncResult BeginConnect(string host, int port, AsyncCallback? callback, object? state) { return _client.BeginConnect(host, port, callback, state); } public void EndConnect(IAsyncResult asyncResult) { _client.EndConnect(asyncResult); } public void Dispose() { _client?.Dispose(); } } }