using System.Net;
using System.Net.Sockets;
using System.Text;
using Sunfish.Config;
using Sunfish.Host;
namespace Sunfish.Tests;
/// <summary>
/// Las variables de entorno son del proceso entero: las pruebas que las tocan
/// comparten esta coleccion para que xUnit no las ejecute en paralelo.
/// </summary>
[CollectionDefinition(Name, DisableParallelization = true)]
public sealed class EnvironmentCollection
{
public const string Name = "Environment";
}
/// <summary>Fija variables de entorno y las restaura al terminar.</summary>
internal sealed class EnvScope : IDisposable
{
private readonly Dictionary<string, string?> _saved = new();
public EnvScope Set(string name, string? value)
{
if (!_saved.ContainsKey(name))
_saved[name] = Environment.GetEnvironmentVariable(name);
Environment.SetEnvironmentVariable(name, value);
return this;
}
public void Dispose()
{
foreach (var (name, value) in _saved)
Environment.SetEnvironmentVariable(name, value);
}
}
/// <summary>Carpeta temporal que se borra al terminar.</summary>
internal sealed class TempDir : IDisposable
{
public string Path { get; } =
System.IO.Path.Combine(System.IO.Path.GetTempPath(), "sunfish-tests-" + Guid.NewGuid().ToString("N"));
public TempDir() => Directory.CreateDirectory(Path);
public string File(string name, string content = "")
{
string full = System.IO.Path.Combine(Path, name);
Directory.CreateDirectory(System.IO.Path.GetDirectoryName(full)!);
System.IO.File.WriteAllText(full, content);
return full;
}
public string Folder(string name)
{
string full = System.IO.Path.Combine(Path, name);
Directory.CreateDirectory(full);
return full;
}
public void Dispose()
{
try { Directory.Delete(Path, recursive: true); }
catch (IOException) { }
catch (UnauthorizedAccessException) { }
}
}
/// <summary>Servidor real en 127.0.0.1 con un puerto libre, y un cliente HTTP en bruto.</summary>
internal sealed class TestServer : IAsyncDisposable
{
private readonly SunfishHost _host;
public TestServer(SunfishConfig config)
{
config.Server.Bind = [IPAddress.Loopback];
config.Server.HttpPort = 0;
config.Server.FtpPort = 0;
config.Server.Announce = false;
_host = new SunfishHost(config);
_host.Start();
}
public SunfishHost Host => _host;
public int Port => _host.HttpPort;
public int FtpPort => _host.FtpPort;
/// <summary>Envia la peticion tal cual y devuelve todo lo recibido hasta el cierre.</summary>
public async Task<RawResponse> SendAsync(string request)
{
using var client = new TcpClient();
await client.ConnectAsync(IPAddress.Loopback, Port);
var stream = client.GetStream();
await stream.WriteAsync(Encoding.Latin1.GetBytes(request));
var ms = new MemoryStream();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
await stream.CopyToAsync(ms, cts.Token);
return new RawResponse(ms.ToArray());
}
/// <summary>GET con HTTP/1.0 y cierre de conexion.</summary>
public Task<RawResponse> GetAsync(string path, string? userAgent = null, string extraHeaders = "") =>
SendAsync($"GET {path} HTTP/1.0\r\n" +
(userAgent is null ? "" : $"User-Agent: {userAgent}\r\n") +
extraHeaders + "\r\n");
public async ValueTask DisposeAsync() => await _host.DisposeAsync();
}
internal sealed class RawResponse
{
public RawResponse(byte[] data)
{
Data = data;
string text = Encoding.Latin1.GetString(data);
int end = text.IndexOf("\r\n\r\n", StringComparison.Ordinal);
if (text.StartsWith("HTTP/", StringComparison.Ordinal) && end >= 0)
{
var lines = text[..end].Split("\r\n");
Status = int.Parse(lines[0].Split(' ')[1]);
foreach (var line in lines.Skip(1))
{
int colon = line.IndexOf(':');
Headers[line[..colon].Trim()] = line[(colon + 1)..].Trim();
}
Body = data[(end + 4)..];
}
else
{
Body = data; // HTTP/0.9: sin cabeceras
}
}
public byte[] Data { get; }
public int Status { get; }
public Dictionary<string, string> Headers { get; } = new(StringComparer.OrdinalIgnoreCase);
public byte[] Body { get; }
public string BodyText => Encoding.Latin1.GetString(Body);
}