using System.Net;
using System.Net.Sockets;
using Sunfish.Config;
using Sunfish.Host;
namespace Sunfish.Tests;
public class ShareSettingsTests
{
[Fact]
public void SetsOptionsByIniKey()
{
var share = new ShareConfig { Name = "x" };
Assert.Null(ShareSettings.TrySet(share, "upload", "yes"));
Assert.Null(ShareSettings.TrySet(share, "Location", "fotos/"));
Assert.Null(ShareSettings.TrySet(share, "Users", "ana, pepe"));
Assert.True(share.Upload);
Assert.Equal("/fotos", share.Location);
Assert.Equal(["ana", "pepe"], share.Users);
Assert.NotNull(ShareSettings.TrySet(share, "Upload", "quizas"));
Assert.NotNull(ShareSettings.TrySet(share, "Inventada", "true"));
Assert.NotNull(ShareSettings.TrySet(share, "Location", "/$sunfish"));
}
[Fact]
public void SuggestsFreeNamesAndLocations()
{
var config = new SunfishConfig();
config.Shares.Add(new ShareConfig { Name = "fotos", Location = "/fotos", Path = "x" });
Assert.Equal("fotos-2", ShareSettings.SuggestName(config, "fotos"));
Assert.Equal("/fotos-2", ShareSettings.SuggestLocation(config, "fotos"));
Assert.Equal("/Mis fotos", ShareSettings.SuggestLocation(config, "Mis fotos"));
}
[Fact]
public void ValidatesBeforeSaving()
{
using var dir = new TempDir();
var config = new SunfishConfig();
config.Shares.Add(new ShareConfig { Name = "fotos", Location = "/fotos", Path = dir.Path });
Assert.Null(ShareSettings.Validate(config, new ShareConfig { Name = "otro", Location = "/otro", Path = dir.Path }, null));
Assert.NotNull(ShareSettings.Validate(config, new ShareConfig { Name = "FOTOS", Location = "/x", Path = dir.Path }, null));
Assert.NotNull(ShareSettings.Validate(config, new ShareConfig { Name = "otro", Location = "/Fotos", Path = dir.Path }, null));
Assert.NotNull(ShareSettings.Validate(config, new ShareConfig { Name = "otro", Location = "/y", Path = Path.Combine(dir.Path, "no") }, null));
Assert.NotNull(ShareSettings.Validate(config, new ShareConfig { Name = "con espacio", Location = "/y", Path = dir.Path }, null));
// Editarse a si mismo no choca con su propio nombre ni location.
Assert.Null(ShareSettings.Validate(config, new ShareConfig { Name = "fotos", Location = "/fotos", Path = dir.Path }, "fotos"));
// Un share desactivado puede compartir location.
Assert.Null(ShareSettings.Validate(config, new ShareConfig { Name = "otro", Location = "/fotos", Path = dir.Path, Enabled = false }, null));
}
[Fact]
public void CloneIsIndependent()
{
var config = SunfishConfig.Parse("# comentario\n[Share a]\nLocation = /a\nPath = C:\\a\n");
var copy = config.Clone();
copy.Shares[0].Upload = true;
Assert.False(config.Shares[0].Upload);
Assert.Contains("# comentario", copy.ToText());
}
}
public class HostTests
{
private static int FreePort()
{
var l = new TcpListener(IPAddress.Loopback, 0);
l.Start();
int port = ((IPEndPoint)l.LocalEndpoint).Port;
l.Stop();
return port;
}
[Fact]
public async Task ApplyRestartsOnPortChange()
{
var config = new SunfishConfig();
config.Server.Bind = [IPAddress.Loopback];
config.Server.HttpPort = FreePort();
config.Server.Ftp = false;
config.Server.Announce = false;
await using var host = new SunfishHost(config);
host.Start();
int first = host.HttpPort;
var copy = config.Clone();
copy.Server.HttpPort = FreePort();
Assert.True(await host.ApplyAsync(copy));
Assert.True(host.HttpRunning);
Assert.Equal(copy.Server.HttpPort, host.HttpPort);
Assert.NotEqual(first, host.HttpPort);
}
[Fact]
public async Task ApplyReportsBusyPorts()
{
var blocker = new TcpListener(IPAddress.Loopback, 0);
blocker.Start();
try
{
var config = new SunfishConfig();
config.Server.Bind = [IPAddress.Loopback];
config.Server.HttpPort = FreePort();
config.Server.Ftp = false;
config.Server.Announce = false;
await using var host = new SunfishHost(config);
host.Start();
var copy = config.Clone();
copy.Server.HttpPort = ((IPEndPoint)blocker.LocalEndpoint).Port;
Assert.False(await host.ApplyAsync(copy));
Assert.False(host.HttpRunning);
}
finally
{
blocker.Stop();
}
}
[Fact]
public async Task ReloadsTheFileWhenAnotherProgramChangesIt()
{
using var dir = new TempDir();
string path = Path.Combine(dir.Path, "sunfish.conf");
var config = new SunfishConfig();
config.Shares.Add(new ShareConfig { Name = "a", Location = "/a", Path = dir.Path });
config.Save(path);
await using var host = new SunfishHost(SunfishConfig.Load(path));
host.WatchConfigFile();
var changed = new TaskCompletionSource();
host.Changed += () => changed.TrySetResult();
File.AppendAllText(path, "\n[Share b]\nLocation = /b\nPath = " + dir.Path + "\n");
await changed.Task.WaitAsync(TimeSpan.FromSeconds(10));
Assert.Equal(["a", "b"], host.Config.Shares.Select(s => s.Name));
}
[Fact]
public async Task OverridesSurviveReloads()
{
using var dir = new TempDir();
string path = Path.Combine(dir.Path, "sunfish.conf");
new SunfishConfig().Save(path);
await using var host = new SunfishHost(SunfishConfig.Load(path)) { Overrides = c => c.Server.HttpPort = 9999 };
host.WatchConfigFile();
var changed = new TaskCompletionSource();
host.Changed += () => changed.TrySetResult();
File.AppendAllText(path, "\n[Server]\nName = Otro\n");
await changed.Task.WaitAsync(TimeSpan.FromSeconds(10));
Assert.Equal("Otro", host.Config.Server.Name);
Assert.Equal(9999, host.Config.Server.HttpPort);
}
[Fact]
public async Task OwnSavesAreNotReloadedTwice()
{
using var dir = new TempDir();
string path = Path.Combine(dir.Path, "sunfish.conf");
new SunfishConfig().Save(path);
await using var host = new SunfishHost(SunfishConfig.Load(path));
host.WatchConfigFile();
int changes = 0;
host.Changed += () => Interlocked.Increment(ref changes);
var copy = host.Config.Clone();
copy.Server.Name = "Nuevo";
await host.SaveAndApplyAsync(copy);
await Task.Delay(1200);
Assert.Equal(1, changes);
Assert.Equal("Nuevo", host.Config.Server.Name);
}
}