using System; using System.IO; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Renci.SshNet.Tests.Common { /// /// Base class for tests that replay a scripted server transcript (see ) /// into a download, with an isolated destination directory. /// /// /// The destination is <root>/a/b/dest below a unique temporary root, so that anything /// written above it, into a/b or a, is detected by . /// public abstract class ScpClientTranscriptTestBase { /// /// Gets the temporary root directory, which is deleted after each test. /// protected string Root { get; private set; } /// /// Gets the destination directory, <root>/a/b/dest. /// protected string Destination { get; private set; } /// /// Gets the parent of the destination directory, <root>/a/b. /// protected string DestinationParent { get { return Path.GetDirectoryName(Destination); } } [TestInitialize] public void CreateDestination() { Root = Path.Combine(Path.GetTempPath(), "sshnet-scp-" + Guid.NewGuid().ToString("N")); Destination = Directory.CreateDirectory(Path.Combine(Root, "a", "b", "dest")).FullName; } [TestCleanup] public void DeleteRoot() { if (Root != null && Directory.Exists(Root)) { Directory.Delete(LiteralPath(Root), recursive: true); } } /// /// Returns the same path with the \\?\ prefix on Windows, which turns off Win32 path /// normalization, so that entries with names such as file. or CON (which some /// tests create on purpose) can be read and deleted. /// protected static string LiteralPath(string path) { return Path.DirectorySeparatorChar == '\\' ? @"\\?\" + path : path; } /// /// Lists every file system entry below , as paths relative to it using /// '/' as separator, in no particular order. /// protected static string[] ListEntries(string directory) { return Directory.GetFileSystemEntries(directory, "*", SearchOption.AllDirectories) .Select(p => p.Substring(directory.Length + 1).Replace(Path.DirectorySeparatorChar, '/')) .ToArray(); } /// /// Runs a recursive download of into the destination directory, or /// into if specified (for example, the destination spelled /// differently). /// protected ScpTranscriptResult DownloadDirectory(ScpServerTranscript transcript, string destinationPath = null) { return transcript.Run( client => client.Download("/remote", new DirectoryInfo(destinationPath ?? Destination)), closeChannel: true); } /// /// Asserts that nothing exists below the root other than the directories leading to the /// destination, the contents of the destination, and (a path /// relative to the root, using '/' as separator), if specified. /// /// An entry outside of the destination that the test created itself. /// A description of the scenario, included in the failure message. protected void AssertNothingOutsideDestination(string expectedOutside = null, string context = null) { var outside = ListEntries(Root).Where(p => p is not "a" and not "a/b" and not "a/b/dest" && !p.StartsWith("a/b/dest/", StringComparison.Ordinal) && !string.Equals(p, expectedOutside, StringComparison.Ordinal)) .ToArray(); Assert.IsEmpty(outside, context + " Written outside of the destination: " + string.Join(", ", outside)); } /// /// Deletes everything in the destination directory. /// protected void ClearDestination() { foreach (var entry in Directory.GetFileSystemEntries(LiteralPath(Destination))) { if (Directory.Exists(entry)) { Directory.Delete(entry, recursive: true); } else { File.Delete(entry); } } } } }