namespace DodoSSH.Client.Ssh.Tests;
///
/// Remote paths, permission bits and byte counts — the three things the file browser renders.
///
///
/// Not in the SSH collection, so this class needs no container. Every case here is one where the obvious
/// implementation is wrong on Windows, at a filesystem root, or on a number that has just crossed a
/// threshold — which is to say, one that a listing of somebody's home directory would not reveal.
///
public sealed class RemotePathTests
{
[Theory]
[InlineData("/var/log", "syslog", "/var/log/syslog")]
[InlineData("/", "etc", "/etc")]
[InlineData("/home/dodo/", "notes", "/home/dodo/notes")]
public void Combine_JoinsWithExactlyOneSeparator(string directory, string name, string expected)
{
// Deliberately not Path.Combine, which on Windows would yield "/var/log\syslog" — a path the remote
// cannot resolve, failing as "no such file" somewhere the backslash is invisible.
SftpPath.Combine(directory, name).ShouldBe(expected);
}
[Theory]
[InlineData("/var/log/syslog", "/var/log")]
[InlineData("/var/log", "/var")]
[InlineData("/var", "/")]
[InlineData("/", "/")]
[InlineData("/var/log/", "/var")]
public void Parent_StopsAtTheRoot(string path, string expected)
{
// The root is its own parent rather than null, which is what lets the breadcrumb's "up" be a plain
// navigation with nothing above it to special-case.
SftpPath.Parent(path).ShouldBe(expected);
}
[Theory]
[InlineData("/var/log/syslog", "syslog")]
[InlineData("/var/log/", "log")]
[InlineData("/", "/")]
public void Name_IsTheLastSegment(string path, string expected)
{
SftpPath.Name(path).ShouldBe(expected);
}
[Fact]
public void Trail_NamesEverySegmentWithThePathThatReachesIt()
{
SftpPath.Trail("/var/log/nginx").ShouldBe(
[("var", "/var"), ("log", "/var/log"), ("nginx", "/var/log/nginx")]);
}
[Fact]
public void Trail_IsEmptyAtTheRoot()
{
// The root has no segment to name. The breadcrumb draws it as a leading separator, so a trail with a
// phantom empty crumb in it would render as a button with no label.
SftpPath.Trail("/").ShouldBeEmpty();
}
[Fact]
public void PosixMode_RendersTheKindCharacterAndThreeTriples()
{
PosixMode.Format(
SftpEntryKind.Directory,
ownerRead: true, ownerWrite: true, ownerExecute: true,
groupRead: true, groupWrite: false, groupExecute: true,
othersRead: true, othersWrite: false, othersExecute: true)
.ShouldBe("drwxr-xr-x");
PosixMode.Format(
SftpEntryKind.File,
ownerRead: true, ownerWrite: true, ownerExecute: false,
groupRead: true, groupWrite: false, groupExecute: false,
othersRead: false, othersWrite: false, othersExecute: false)
.ShouldBe("-rw-r-----");
PosixMode.Format(
SftpEntryKind.SymbolicLink,
ownerRead: true, ownerWrite: true, ownerExecute: true,
groupRead: true, groupWrite: true, groupExecute: true,
othersRead: true, othersWrite: true, othersExecute: true)
.ShouldBe("lrwxrwxrwx");
}
[Theory]
[InlineData("-rwxr-xr-x", true)]
[InlineData("-rw-r-xr--", true)]
[InlineData("-rw-r--r-x", true)]
[InlineData("-rw-r--r--", false)]
[InlineData("----------", false)]
public void PosixMode_ReadsAnExecuteBitInAnyTriple(string mode, bool expected)
{
// Any of the three, not the owner's: the account browsing is not necessarily the owner, and a file
// that only the group may run is still a file somebody runs.
PosixMode.HasAnyExecuteBit(mode).ShouldBe(expected);
}
[Theory]
[InlineData("-rw-rw-rw-", true)]
[InlineData("-rw-rw-r--", false)]
[InlineData("-rw-r--r--", false)]
[InlineData("--------w-", true)]
public void PosixMode_ReadsTheOthersWriteBit(string mode, bool expected)
{
// The others triple only. A group-writable file is writable by a named set of people, which is what
// groups are for; this asks about the column that names nobody.
PosixMode.IsWorldWritable(mode).ShouldBe(expected);
}
[Theory]
[InlineData("")]
[InlineData("rwxrwxrwx")]
[InlineData("?")]
public void PosixMode_AnswersFalseForAModeItDidNotWrite(string mode)
{
// These two decide a row's colour and nothing else. A listing is not worth failing over a string of
// the wrong length, and an index off the end of one is how that would happen.
PosixMode.HasAnyExecuteBit(mode).ShouldBeFalse();
PosixMode.IsWorldWritable(mode).ShouldBeFalse();
}
[Theory]
[InlineData(SftpEntryKind.File, "-rwxr-xr-x", true)]
[InlineData(SftpEntryKind.File, "-rw-r--r--", false)]
[InlineData(SftpEntryKind.Directory, "drwxr-xr-x", false)]
[InlineData(SftpEntryKind.SymbolicLink, "lrwxrwxrwx", false)]
public void AnEntry_IsExecutableOnlyWhenItIsAFile(SftpEntryKind kind, string mode, bool expected)
{
// The x on a directory means "may be searched", which is true of very nearly every directory a host
// has. A file browser that marked them all would be marking nothing.
Entry(kind, mode).IsExecutable.ShouldBe(expected);
}
[Theory]
[InlineData(SftpEntryKind.File, "-rw-rw-rw-", true)]
[InlineData(SftpEntryKind.File, "-rw-r--r--", false)]
[InlineData(SftpEntryKind.SymbolicLink, "lrwxrwxrwx", false)]
[InlineData(SftpEntryKind.Directory, "drwxrwxrwx", false)]
public void AnEntry_IsWorldWritableOnlyWhenItIsAFile(SftpEntryKind kind, string mode, bool expected)
{
// The two exclusions are the point. Every symbolic link is lrwxrwxrwx and its mode governs nothing —
// what may be written is the target, whose mode this listing never fetched. And a world-writable
// directory is /tmp, made safe by a sticky bit PosixMode does not render: warning there would be
// warning about the half of the mode that is on screen.
Entry(kind, mode).IsWorldWritable.ShouldBe(expected);
}
[Theory]
[InlineData(0, "0 B")]
[InlineData(1023, "1023 B")]
[InlineData(1024, "1.0 KB")]
[InlineData(10 * 1024, "10 KB")]
[InlineData(1536 * 1024, "1.5 MB")]
[InlineData(5L * 1024 * 1024 * 1024, "5.0 GB")]
public void ByteSize_KeepsOneDecimalOnlyWhileItMeansSomething(long bytes, string expected)
{
// The boundary at ten is the whole rule: "9.4 MB" and "512 MB" carry the same information, and
// "512.3 MB" claims a precision the figure does not have by the time it is that large.
ByteSize.Format(bytes).ShouldBe(expected);
}
/// An entry that is nothing but a kind and a mode, which is all these two questions read.
private static SftpEntry Entry(SftpEntryKind kind, string mode) =>
new("thing", "/tmp/thing", kind, 0, DateTimeOffset.UnixEpoch, mode);
}