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); }