/*
* Copyright (c) 2026 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.IO;
using System.Net.Http;
using System.Diagnostics;
using System.Threading.Tasks;
using FlaUI.Core.Tools;
using NUnit.Framework;
namespace ProtonVPN.UI.Tests.TestsHelper;
public class TorrentHelper
{
private const string TORRENT_PROCESS_NAME = "qbittorrent";
private const string PORT_CHECKER_API_BASE_URL = "https://portchecker.io/api";
private const string TORRENT_URL = "https://releases.ubuntu.com/24.04/ubuntu-24.04.4-desktop-amd64.iso.torrent";
private static readonly string _qbittorrentFolder = @"C:\qBittorrent";
private static readonly string _qbittorrentExePath = @"C:\Program Files\qBittorrent\qbittorrent.exe";
private static readonly string _torrentsPath = $@"{_qbittorrentFolder}\torrents";
private static readonly string _qbittorrentRuleName = "ProtonVPN UI Tests - Allow qbittorrent";
private static readonly string _qBittorrentFirewallScript = $@"
if (-not (Get-NetFirewallRule -DisplayName '{_qbittorrentRuleName} - TCP' -ErrorAction SilentlyContinue))
{{
New-NetFirewallRule -DisplayName '{_qbittorrentRuleName} - TCP' -Direction Inbound -Program '{_qbittorrentExePath}' -Action Allow -Profile Private,Public -Protocol TCP
}}
if (-not (Get-NetFirewallRule -DisplayName '{_qbittorrentRuleName} - UDP' -ErrorAction SilentlyContinue))
{{
New-NetFirewallRule -DisplayName '{_qbittorrentRuleName} - UDP' -Direction Inbound -Program '{_qbittorrentExePath}' -Action Allow -Profile Private,Public -Protocol UDP
}}
";
public static void AllowTorrentFirewall()
{
WindowsUtils.RunPowerShellScript(_qBittorrentFirewallScript);
}
public static void StartTorrentOnPort(int port)
{
RetryResult retry = Retry.WhileFalse(
() => StartTorrentOnPortAsync(port).GetAwaiter().GetResult(),
TestConstants.ThirtySecondsTimeout, TestConstants.ApiRetryInterval);
Assert.That(retry.Result, Is.True, $"Failed to start qBittorrent with port: {port}");
}
public static void IsPortOpen(string ip, int port)
{
RetryResult retry = Retry.WhileFalse(
() => IsPortOpenAsync(ip, port).GetAwaiter().GetResult(),
TestConstants.ThirtySecondsTimeout, TestConstants.ApiRetryInterval);
if (retry.Result)
{
TestContext.WriteLine($"SUCCESS: Port {port} is open on {ip}");
}
else
{
TestContext.WriteLine($"WARNING: Port {port} was not reported as open on {ip} within timeout");
}
}
public static void IsPortClosed(string ip, int port)
{
RetryResult retry = Retry.WhileTrue(
() => IsPortOpenAsync(ip, port).GetAwaiter().GetResult(),
TestConstants.ThirtySecondsTimeout, TestConstants.ApiRetryInterval);
if (retry.Result)
{
TestContext.WriteLine($"SUCCESS: Port {port} is not opened on {ip}");
}
else
{
TestContext.WriteLine($"WARNING: Port {port} is still reported as open on {ip} within timeout");
}
}
public static void StopAndCleanup()
{
foreach (Process process in Process.GetProcessesByName(TORRENT_PROCESS_NAME))
{
try
{
process.Kill();
process.WaitForExit(TestConstants.FiveSecondsTimeout);
}
catch
{
process.Kill(true);
process.WaitForExit(TestConstants.FiveSecondsTimeout);
}
finally
{
process.Dispose();
}
}
if (Directory.Exists(_torrentsPath))
{
Directory.Delete(_torrentsPath, recursive: true);
}
}
private static async Task StartTorrentOnPortAsync(int port)
{
Directory.CreateDirectory(_torrentsPath);
string torrentFile = Path.Combine(_qbittorrentFolder, "test.torrent");
if (!File.Exists(torrentFile))
{
using HttpClient client = new();
byte[] data = await client.GetByteArrayAsync(TORRENT_URL);
File.WriteAllBytes(torrentFile, data);
}
Process.Start(new ProcessStartInfo
{
FileName = _qbittorrentExePath,
Arguments = $"--confirm-legal-notice --skip-dialog=true --torrenting-port={port} --save-path={_torrentsPath} {torrentFile}",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
});
return true;
}
private static async Task IsPortOpenAsync(string ip, int port)
{
using HttpClient client = new();
string url = $"{PORT_CHECKER_API_BASE_URL}/{ip}/{port}";
try
{
HttpResponseMessage response = await client.GetAsync(url);
string result = await response.Content.ReadAsStringAsync();
TestContext.WriteLine($"DEBUG: {result}");
return result.Trim().Equals("true", StringComparison.OrdinalIgnoreCase);
}
catch (Exception ex)
{
TestContext.WriteLine($"WARNING: Port check request failed: {ex.Message}");
return false;
}
}
}