using Sunfish.Config;
using Sunfish.Shares;
namespace Sunfish.Tests;
public class PathRulesTests
{
[Theory]
[InlineData("fotos", true)]
[InlineData("Fotos del viaje.jpeg", true)]
[InlineData("año", true)]
[InlineData("..", false)]
[InlineData(".", false)]
[InlineData("", false)]
[InlineData("a\\b", false)]
[InlineData("a:b", false)]
[InlineData("a\u0001", false)]
public void SegmentsOnAnySystem(string segment, bool valid) =>
Assert.Equal(valid, PathRules.IsValidSegment(segment, windows: false));
[Theory]
[InlineData("CON", false)]
[InlineData("con.txt", false)]
[InlineData("LPT1", false)]
[InlineData("nombre.", false)]
[InlineData("nombre ", false)]
[InlineData("a*b", false)]
[InlineData("CONSOLA", true)]
[InlineData("COM10", true)]
public void SegmentsOnWindows(string segment, bool valid) =>
Assert.Equal(valid, PathRules.IsValidSegment(segment, windows: true));
[Fact]
public void SplitsAndDecodesUrlPaths()
{
Assert.True(PathRules.TrySplitUrlPath("/fotos//2026/a%20b.jpg", out var segments, out bool slash));
Assert.Equal(["fotos", "2026", "a b.jpg"], segments);
Assert.False(slash);
Assert.True(PathRules.TrySplitUrlPath("/", out segments, out slash));
Assert.Empty(segments);
Assert.True(slash);
}
[Theory]
[InlineData("/../secreto")]
[InlineData("/a/%2e%2e/b")]
[InlineData("/a%2fb")]
[InlineData("/a%5cb")]
[InlineData("/c%3a")]
public void RejectsTraversalEvenWhenEncoded(string path) =>
Assert.False(PathRules.TrySplitUrlPath(path, out _, out _));
[Fact]
public void DecodesUtf8AndFallsBackToLatin1()
{
Assert.Equal("año", PathRules.PercentDecode("a%C3%B1o"));
Assert.Equal("año", PathRules.PercentDecode("a%F1o")); // navegador antiguo que envia Latin-1
}
[Theory]
[InlineData("/", "/")]
[InlineData("fotos", "/fotos")]
[InlineData("/fotos/", "/fotos")]
[InlineData("//a//b", "/a/b")]
public void NormalizesLocations(string input, string expected)
{
Assert.True(LocationPath.TryNormalize(input, out var normalized));
Assert.Equal(expected, normalized);
}
}
public class ShareTreeTests
{
private static ShareConfig S(string name, string location) => new() { Name = name, Location = location, Path = "x" };
private static readonly ShareTree Tree = new([
S("web", "/"),
S("fotos", "/fotos"),
S("viaje", "/fotos/2026/viaje"),
S("musica", "/Musica/clasica"),
]);
[Theory]
[InlineData("index.html", "web", "index.html")]
[InlineData("fotos/a.jpg", "fotos", "a.jpg")]
[InlineData("fotos/2026/b.jpg", "fotos", "2026/b.jpg")]
[InlineData("fotos/2026/viaje/c.jpg", "viaje", "c.jpg")]
[InlineData("FOTOS/x", "fotos", "x")]
[InlineData("fotosviejas/x", "web", "fotosviejas/x")]
public void LongestPrefixWins(string path, string share, string relative)
{
var r = Tree.Resolve(path.Split('/'));
Assert.Equal(share, r.Share?.Name);
Assert.Equal(relative, string.Join('/', r.Relative));
}
[Fact]
public void ComparesWholeSegments()
{
var tree = new ShareTree([S("fotos", "/fotos")]);
Assert.Null(tree.Resolve(["foto"]).Share);
Assert.Null(tree.Resolve(["fotosx"]).Share);
}
[Fact]
public void ListsVirtualChildren()
{
Assert.Equal(["fotos", "Musica"], Tree.Resolve([]).VirtualChildren);
Assert.Equal(["2026"], Tree.Resolve(["fotos"]).VirtualChildren);
Assert.Equal(["viaje"], Tree.Resolve(["fotos", "2026"]).VirtualChildren);
Assert.Equal(["clasica"], Tree.Resolve(["musica"]).VirtualChildren);
}
[Fact]
public void IgnoresDisabledShares()
{
var off = S("off", "/off");
off.Enabled = false;
var tree = new ShareTree([off]);
Assert.Null(tree.Resolve(["off"]).Share);
Assert.Empty(tree.Resolve([]).VirtualChildren);
}
}
public class ShareFileSystemTests
{
[Fact]
public void MapsInsideTheShare()
{
using var dir = new TempDir();
dir.File("a/b.txt", "x");
var share = new ShareConfig { Path = dir.Path };
string? physical = ShareFileSystem.TryGetPhysicalPath(share, ["a", "b.txt"]);
Assert.Equal(Path.Combine(dir.Path, "a", "b.txt"), physical);
}
[Fact]
public void RejectsEscapes()
{
using var dir = new TempDir();
var share = new ShareConfig { Path = dir.Path };
Assert.Null(ShareFileSystem.TryGetPhysicalPath(share, [".."]));
Assert.Null(ShareFileSystem.TryGetPhysicalPath(share, ["a", "..", ".."]));
Assert.Null(ShareFileSystem.TryGetPhysicalPath(share, ["C:"]));
}
[Fact]
public void DoesNotFollowLinksOutOfTheShare()
{
using var outside = new TempDir();
using var dir = new TempDir();
string link = Path.Combine(dir.Path, "salida");
try
{
Directory.CreateSymbolicLink(link, outside.Path);
}
catch (Exception e) when (e is UnauthorizedAccessException or IOException)
{
return; // Windows sin modo desarrollador no deja crear enlaces simbolicos
}
outside.File("secreto.txt", "x");
var share = new ShareConfig { Path = dir.Path };
Assert.Null(ShareFileSystem.TryGetPhysicalPath(share, ["salida"]));
Assert.Null(ShareFileSystem.TryGetPhysicalPath(share, ["salida", "secreto.txt"]));
}
[Fact]
public void ListingSkipsHiddenEntries()
{
using var dir = new TempDir();
dir.File("visible.txt");
dir.File(".oculto");
dir.Folder("carpeta");
var names = ShareFileSystem.List(dir.Path).Select(e => e.Name).Order().ToList();
Assert.Equal(["carpeta", "visible.txt"], names);
}
}