SftpFileStreamTest.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. using System;
  2. using System.IO;
  3. using System.Linq;
  4. using System.Threading;
  5. using System.Threading.Tasks;
  6. using Microsoft.Extensions.Logging.Abstractions;
  7. using Microsoft.VisualStudio.TestTools.UnitTesting;
  8. using Moq;
  9. using Renci.SshNet.Common;
  10. using Renci.SshNet.Sftp;
  11. using Renci.SshNet.Sftp.Responses;
  12. namespace Renci.SshNet.Tests.Classes.Sftp
  13. {
  14. [TestClass]
  15. public class SftpFileStreamTest
  16. {
  17. [TestMethod]
  18. [DataRow(false)]
  19. [DataRow(true)]
  20. public async Task BadFileMode_ThrowsArgumentOutOfRangeException(bool isAsync)
  21. {
  22. ArgumentOutOfRangeException ex;
  23. if (isAsync)
  24. {
  25. ex = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
  26. SftpFileStream.OpenAsync(new Mock<ISftpSession>().Object, "file.txt", mode: 0, FileAccess.Read, bufferSize: 1024, CancellationToken.None));
  27. }
  28. else
  29. {
  30. ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
  31. SftpFileStream.Open(new Mock<ISftpSession>().Object, "file.txt", mode: 0, FileAccess.Read, bufferSize: 1024));
  32. }
  33. Assert.AreEqual("mode", ex.ParamName);
  34. }
  35. [TestMethod]
  36. [DataRow(false)]
  37. [DataRow(true)]
  38. public async Task BadFileAccess_ThrowsArgumentOutOfRangeException(bool isAsync)
  39. {
  40. ArgumentOutOfRangeException ex;
  41. if (isAsync)
  42. {
  43. ex = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
  44. SftpFileStream.OpenAsync(new Mock<ISftpSession>().Object, "file.txt", FileMode.Open, access: 0, bufferSize: 1024, CancellationToken.None));
  45. }
  46. else
  47. {
  48. ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
  49. SftpFileStream.Open(new Mock<ISftpSession>().Object, "file.txt", FileMode.Open, access: 0, bufferSize: 1024));
  50. }
  51. Assert.AreEqual("access", ex.ParamName);
  52. }
  53. [TestMethod]
  54. [DataRow(FileMode.Append, FileAccess.Read, false)]
  55. [DataRow(FileMode.Append, FileAccess.Read, true)]
  56. [DataRow(FileMode.Append, FileAccess.ReadWrite, false)]
  57. [DataRow(FileMode.Append, FileAccess.ReadWrite, true)]
  58. [DataRow(FileMode.Create, FileAccess.Read, false)]
  59. [DataRow(FileMode.Create, FileAccess.Read, true)]
  60. [DataRow(FileMode.CreateNew, FileAccess.Read, false)]
  61. [DataRow(FileMode.CreateNew, FileAccess.Read, true)]
  62. [DataRow(FileMode.Truncate, FileAccess.Read, false)]
  63. [DataRow(FileMode.Truncate, FileAccess.Read, true)]
  64. public async Task InvalidModeAccessCombination_ThrowsArgumentException(FileMode mode, FileAccess access, bool isAsync)
  65. {
  66. ArgumentException ex;
  67. if (isAsync)
  68. {
  69. ex = await Assert.ThrowsAsync<ArgumentException>(() =>
  70. SftpFileStream.OpenAsync(new Mock<ISftpSession>().Object, "file.txt", mode, access, bufferSize: 1024, CancellationToken.None));
  71. }
  72. else
  73. {
  74. ex = Assert.Throws<ArgumentException>(() =>
  75. SftpFileStream.Open(new Mock<ISftpSession>().Object, "file.txt", mode, access, bufferSize: 1024));
  76. }
  77. Assert.AreEqual("access", ex.ParamName);
  78. }
  79. [TestMethod]
  80. public void ReadWithWriteAccess_ThrowsNotSupportedException()
  81. {
  82. var sessionMock = new Mock<ISftpSession>();
  83. sessionMock.Setup(s => s.CalculateOptimalWriteLength(It.IsAny<uint>(), It.IsAny<byte[]>())).Returns<uint, byte[]>((x, _) => x);
  84. sessionMock.Setup(s => s.IsOpen).Returns(true);
  85. SetupRemoteSize(sessionMock, 128);
  86. var s = SftpFileStream.Open(sessionMock.Object, "file.txt", FileMode.Create, FileAccess.Write, bufferSize: 1024);
  87. Assert.IsFalse(s.CanRead);
  88. Assert.Throws<NotSupportedException>(() => _ = s.Read(new byte[4], 0, 4));
  89. Assert.Throws<NotSupportedException>(() => _ = s.ReadByte());
  90. Assert.Throws<NotSupportedException>(() => _ = s.ReadAsync(new byte[4], 0, 4).GetAwaiter().GetResult());
  91. Assert.Throws<NotSupportedException>(() => _ = s.EndRead(s.BeginRead(new byte[4], 0, 4, null, null)));
  92. #if NET
  93. Assert.Throws<NotSupportedException>(() => _ = s.Read(new byte[4]));
  94. Assert.Throws<NotSupportedException>(() => _ = s.ReadAsync(new byte[4]).AsTask().GetAwaiter().GetResult());
  95. #endif
  96. Assert.Throws<NotSupportedException>(() => s.CopyTo(Stream.Null));
  97. Assert.Throws<NotSupportedException>(() => s.CopyToAsync(Stream.Null).GetAwaiter().GetResult());
  98. }
  99. [TestMethod]
  100. public void WriteWithReadAccess_ThrowsNotSupportedException()
  101. {
  102. var sessionMock = new Mock<ISftpSession>();
  103. sessionMock.Setup(s => s.CalculateOptimalWriteLength(It.IsAny<uint>(), It.IsAny<byte[]>())).Returns<uint, byte[]>((x, _) => x);
  104. sessionMock.Setup(s => s.IsOpen).Returns(true);
  105. var s = SftpFileStream.Open(sessionMock.Object, "file.txt", FileMode.Open, FileAccess.Read, bufferSize: 1024);
  106. Assert.IsFalse(s.CanWrite);
  107. Assert.Throws<NotSupportedException>(() => s.Write(new byte[4], 0, 4));
  108. Assert.Throws<NotSupportedException>(() => s.WriteByte(0xf));
  109. Assert.Throws<NotSupportedException>(() => s.WriteAsync(new byte[4], 0, 4).GetAwaiter().GetResult());
  110. Assert.Throws<NotSupportedException>(() => s.EndWrite(s.BeginWrite(new byte[4], 0, 4, null, null)));
  111. #if NET
  112. Assert.Throws<NotSupportedException>(() => s.Write(new byte[4]));
  113. Assert.Throws<NotSupportedException>(() => s.WriteAsync(new byte[4]).AsTask().GetAwaiter().GetResult());
  114. #endif
  115. Assert.Throws<NotSupportedException>(() => s.SetLength(1024));
  116. }
  117. [TestMethod]
  118. [DataRow(-1, SeekOrigin.Begin)]
  119. [DataRow(-1, SeekOrigin.Current)]
  120. [DataRow(-1000, SeekOrigin.End)]
  121. public void SeekBeforeBeginning_ThrowsIOException(long offset, SeekOrigin origin)
  122. {
  123. var sessionMock = new Mock<ISftpSession>();
  124. sessionMock.Setup(s => s.CalculateOptimalReadLength(It.IsAny<uint>())).Returns<uint>(x => x);
  125. sessionMock.Setup(s => s.CalculateOptimalWriteLength(It.IsAny<uint>(), It.IsAny<byte[]>())).Returns<uint, byte[]>((x, _) => x);
  126. sessionMock.Setup(s => s.IsOpen).Returns(true);
  127. SetupRemoteSize(sessionMock, 128);
  128. var s = SftpFileStream.Open(sessionMock.Object, "file.txt", FileMode.Open, FileAccess.Read, bufferSize: 1024);
  129. Assert.Throws<IOException>(() => s.Seek(offset, origin));
  130. }
  131. private static void SetupRemoteSize(Mock<ISftpSession> sessionMock, long size)
  132. {
  133. sessionMock.Setup(s => s.RequestFStat(It.IsAny<byte[]>())).Returns(new SftpFileAttributes(
  134. default, default, size: size, default, default, default, default
  135. ));
  136. }
  137. // Operations which should cause writes to be flushed because they depend on
  138. // the remote file being up to date.
  139. // Most of these are already implicitly covered by integration tests and may
  140. // not be so valuable here.
  141. [TestMethod]
  142. public void Flush_SendsBufferedWrites()
  143. {
  144. TestSendsBufferedWrites(s => s.Flush());
  145. }
  146. [TestMethod]
  147. public void Read_SendsBufferedWrites()
  148. {
  149. TestSendsBufferedWrites(s => _ = s.Read(new byte[16], 0, 16));
  150. }
  151. [TestMethod]
  152. public void Seek_SendsBufferedWrites()
  153. {
  154. TestSendsBufferedWrites(s => _ = s.Seek(-1, SeekOrigin.Current));
  155. }
  156. [TestMethod]
  157. public void SetPosition_SendsBufferedWrites()
  158. {
  159. TestSendsBufferedWrites(s => s.Position++);
  160. }
  161. [TestMethod]
  162. public void SetLength_SendsBufferedWrites()
  163. {
  164. TestSendsBufferedWrites(s => s.SetLength(256));
  165. }
  166. [TestMethod]
  167. public void GetLength_SendsBufferedWrites()
  168. {
  169. TestSendsBufferedWrites(s => _ = s.Length);
  170. }
  171. [TestMethod]
  172. public void Dispose_SendsBufferedWrites()
  173. {
  174. TestSendsBufferedWrites(s => s.Dispose());
  175. }
  176. private void TestSendsBufferedWrites(Action<SftpFileStream> flushAction)
  177. {
  178. var sessionMock = new Mock<ISftpSession>();
  179. sessionMock.Setup(s => s.CalculateOptimalReadLength(It.IsAny<uint>())).Returns<uint>(x => x);
  180. sessionMock.Setup(s => s.CalculateOptimalWriteLength(It.IsAny<uint>(), It.IsAny<byte[]>())).Returns<uint, byte[]>((x, _) => x);
  181. sessionMock.Setup(s => s.IsOpen).Returns(true);
  182. SetupRemoteSize(sessionMock, 0);
  183. var s = SftpFileStream.Open(sessionMock.Object, "file.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, bufferSize: 1024);
  184. // Buffer some data
  185. byte[] newData = "Some new bytes"u8.ToArray();
  186. s.Write(newData, 0, newData.Length);
  187. byte[] newData2 = "Some more bytes"u8.ToArray();
  188. s.Write(newData2, 0, newData2.Length);
  189. // The written data does not exceed bufferSize so we do not expect
  190. // it to have been sent.
  191. sessionMock.Verify(s => s.RequestWrite(
  192. It.IsAny<byte[]>(),
  193. It.IsAny<ulong>(),
  194. It.IsAny<byte[]>(),
  195. It.IsAny<int>(),
  196. It.IsAny<int>(),
  197. It.IsAny<AutoResetEvent>(),
  198. It.IsAny<Action<SftpStatusResponse>>()),
  199. Times.Never);
  200. // Whatever is called here should trigger the bytes to be sent
  201. flushAction(s);
  202. VerifyRequestWrite(sessionMock, newData.Concat(newData2), serverOffset: 0);
  203. }
  204. [TestMethod]
  205. public void Dispose()
  206. {
  207. var sessionMock = new Mock<ISftpSession>();
  208. sessionMock.Setup(s => s.CalculateOptimalWriteLength(It.IsAny<uint>(), It.IsAny<byte[]>())).Returns<uint, byte[]>((x, _) => x);
  209. sessionMock.Setup(s => s.IsOpen).Returns(true);
  210. var s = SftpFileStream.Open(sessionMock.Object, "file.txt", FileMode.Create, FileAccess.ReadWrite, bufferSize: 1024);
  211. Assert.IsTrue(s.CanRead);
  212. Assert.IsTrue(s.CanWrite);
  213. s.Dispose();
  214. sessionMock.Verify(p => p.RequestClose(It.IsAny<byte[]>()), Times.Once);
  215. Assert.IsFalse(s.CanRead);
  216. Assert.IsFalse(s.CanSeek);
  217. Assert.IsFalse(s.CanWrite);
  218. Assert.Throws<ObjectDisposedException>(() => s.Read(new byte[16], 0, 16));
  219. Assert.Throws<ObjectDisposedException>(() => s.ReadByte());
  220. Assert.Throws<ObjectDisposedException>(() => s.Write(new byte[16], 0, 16));
  221. Assert.Throws<ObjectDisposedException>(() => s.WriteByte(0xf));
  222. Assert.Throws<ObjectDisposedException>(() => s.CopyTo(Stream.Null));
  223. Assert.Throws<ObjectDisposedException>(s.Flush);
  224. Assert.Throws<ObjectDisposedException>(() => s.Seek(0, SeekOrigin.Begin));
  225. Assert.Throws<ObjectDisposedException>(() => s.SetLength(128));
  226. Assert.Throws<ObjectDisposedException>(() => _ = s.Length);
  227. // Test no-op second dispose
  228. s.Dispose();
  229. sessionMock.Verify(p => p.RequestClose(It.IsAny<byte[]>()), Times.Once);
  230. }
  231. [TestMethod]
  232. public void FstatFailure_DisablesSeek()
  233. {
  234. TestFstatFailure(fstat => fstat.Throws<SftpPermissionDeniedException>());
  235. }
  236. [TestMethod]
  237. public void FstatSizeNotReturned_DisablesSeek()
  238. {
  239. TestFstatFailure(fstat => fstat.Returns(SftpFileAttributes.FromBytes([0, 0, 0, 0])));
  240. }
  241. private void TestFstatFailure(Action<Moq.Language.Flow.ISetup<ISftpSession, SftpFileAttributes>> fstatSetup)
  242. {
  243. var sessionMock = new Mock<ISftpSession>();
  244. sessionMock.Setup(s => s.CalculateOptimalReadLength(It.IsAny<uint>())).Returns<uint>(x => x);
  245. sessionMock.Setup(s => s.CalculateOptimalWriteLength(It.IsAny<uint>(), It.IsAny<byte[]>())).Returns<uint, byte[]>((x, _) => x);
  246. sessionMock.Setup(p => p.SessionLoggerFactory).Returns(NullLoggerFactory.Instance);
  247. sessionMock.Setup(s => s.IsOpen).Returns(true);
  248. fstatSetup(sessionMock.Setup(s => s.RequestFStat(It.IsAny<byte[]>())));
  249. var s = SftpFileStream.Open(sessionMock.Object, "file.txt", FileMode.Open, FileAccess.ReadWrite, bufferSize: 1024);
  250. Assert.IsFalse(s.CanSeek);
  251. Assert.IsTrue(s.CanRead);
  252. Assert.IsTrue(s.CanWrite);
  253. Assert.Throws<NotSupportedException>(() => s.Position);
  254. Assert.Throws<NotSupportedException>(() => s.Length);
  255. Assert.Throws<NotSupportedException>(() => s.Seek(0, SeekOrigin.Begin));
  256. Assert.Throws<NotSupportedException>(() => s.SetLength(1024));
  257. // Reads and writes still succeed.
  258. _ = s.Read(new byte[16], 0, 16);
  259. s.Write(new byte[16], 0, 16);
  260. s.Flush();
  261. }
  262. private static void VerifyRequestWrite(Mock<ISftpSession> sessionMock, ReadOnlyMemory<byte> newData, int serverOffset)
  263. {
  264. sessionMock.Verify(s => s.RequestWrite(
  265. /* handle: */ It.IsAny<byte[]>(),
  266. /* serverOffset: */ (ulong)serverOffset,
  267. /* data: */ It.Is<byte[]>(x => IndexOf(x, newData) >= 0),
  268. /* offset: */ It.IsAny<int>(),
  269. /* length: */ newData.Length,
  270. /* wait: */ It.IsAny<AutoResetEvent>(),
  271. /* writeCompleted: */ It.IsAny<Action<SftpStatusResponse>>()),
  272. Times.Once);
  273. }
  274. private static int IndexOf(byte[] searchSpace, ReadOnlyMemory<byte> searchValue)
  275. {
  276. // Needed in a (non-local) function because expression lambdas can't contain spans
  277. return searchSpace.AsSpan().IndexOf(searchValue.Span);
  278. }
  279. }
  280. }