// ------------------------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. // ------------------------------------------------------------------------------ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Graph; using Test.OneDrive.Sdk.Mocks; namespace Test.OneDrive.Sdk { using Microsoft.OneDrive.Sdk; using Microsoft.OneDrive.Sdk.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; [TestClass] public class ChunkedUploadProviderTests { private Mock uploadSession; private Mock client; private Mock uploadStream; private int myChunkSize; private Mock mockChunkUploadProvider; [TestInitialize] public void TestInitialize() { this.uploadSession = new Mock(); this.uploadSession.Object.NextExpectedRanges = new[] {"0-"}; this.uploadSession.Object.UploadUrl = "http://www.example.com/api/v1.0"; this.client = new Mock(); this.uploadStream = new Mock(); this.StreamSetup(true); this.myChunkSize = 320 * 1024; this.mockChunkUploadProvider = new Mock(); } [TestMethod] public void ConstructorTest_Valid() { this.StreamSetup(true); var uploadProvider = new ChunkedUploadProvider( this.uploadSession.Object, this.client.Object, this.uploadStream.Object, 320*1024); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void ConstructorTest_InvalidStream() { this.StreamSetup(false); var uploadProvider = new ChunkedUploadProvider( this.uploadSession.Object, this.client.Object, this.uploadStream.Object, 320*1024); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void ConstructorTest_InvalidChunkSize() { this.StreamSetup(false); var uploadProvider = new ChunkedUploadProvider( this.uploadSession.Object, this.client.Object, this.uploadStream.Object, 12); } [TestMethod] public void GetUploadChunkRequests_OneRangeOneChunk() { var chunkSize = 320 * 1024; var totalSize = 100; var results = this.SetupGetUploadChunksTest(chunkSize, totalSize, new[] {"0-"}); var expectedRanges = new[] {new Tuple(0, 99, 100)}; this.AssertChunksAre(this.CreateUploadExpectedChunkRequests(expectedRanges), results); } [TestMethod] public void GetUploadChunkRequests_OneRangeMultiChunk() { var chunkSize = 320 * 1024; var totalSize = chunkSize*2 + 1; var results = this.SetupGetUploadChunksTest(chunkSize, totalSize, new[] { "0-" }); var expectedRanges = new[] { new Tuple(0, chunkSize-1, totalSize), new Tuple(chunkSize, 2*chunkSize-1, totalSize), new Tuple(2*chunkSize, 2*chunkSize, totalSize), }; this.AssertChunksAre(this.CreateUploadExpectedChunkRequests(expectedRanges), results); } [TestMethod] public void GetUploadChunkRequests_MultiRangeMultiChunk() { var chunkSize = 320 * 1024; var totalSize = chunkSize*5; var offset = 20; var results = this.SetupGetUploadChunksTest(chunkSize, totalSize, new[] { $"0-{chunkSize}", $"{chunkSize*3 - offset}-" }); var expectedRanges = new[] { // 0 - chunkSize new Tuple(0, chunkSize - 1, totalSize), new Tuple(chunkSize, chunkSize, totalSize), // (chunkSize*3-offset) - end new Tuple(3*chunkSize - offset, 4*chunkSize - offset - 1, totalSize), new Tuple(4*chunkSize - offset, 5*chunkSize - offset - 1, totalSize), new Tuple(5*chunkSize - offset, 5*chunkSize - 1, totalSize) }; this.AssertChunksAre(this.CreateUploadExpectedChunkRequests(expectedRanges), results); } [TestMethod] public void GetRangesRemaining_OneRangeOneChunk() { var chunkSize = 320*1024; var totalSize = 100; var results = this.SetupRangesRemainingTest(chunkSize, totalSize, new[] {"0-"}); var expected = new List> {new Tuple(0, 99)}; this.AssertRangesAre(expected, results); } [TestMethod] public void GetRangesRemaining_OneRangeMultiChunk() { var chunkSize = 320*1024; var totalSize = chunkSize*2 + 1; var results = this.SetupRangesRemainingTest(chunkSize, totalSize, new[] { "0-" }); var expected = new List> { new Tuple(0, chunkSize*2) }; this.AssertRangesAre(expected, results); } [TestMethod] public void GetRangesRemaining_MultiRangeMultiChunk() { var chunkSize = 320*1024; var totalSize = chunkSize*5; var results = this.SetupRangesRemainingTest(chunkSize, totalSize, new[] { $"0-{chunkSize - 1}", $"{chunkSize*2}-{chunkSize*3}", $"{chunkSize*4}-" }); var expected = new[] { new Tuple(0, chunkSize - 1), new Tuple(chunkSize*2, chunkSize*3), new Tuple(chunkSize*4, chunkSize*5-1) }; this.AssertRangesAre(expected, results); } [TestMethod] public void GetChunkRequestResponseTest_Success() { var result = this.SetupGetChunkResponseTest(verifyTrackedExceptions: false); Assert.IsNotNull(result.ItemResponse, "Expected Item in ItemResponse"); Assert.IsTrue(result.UploadSucceeded); } [TestMethod] public void GetChunkRequestResponseTest_SuccessAfterOneException() { var exception = new ServiceException(new Error {Code = "GeneralException"}); var result = this.SetupGetChunkResponseTest(exception); Assert.IsNotNull(result.ItemResponse, "Expected Item in ItemResponse"); Assert.IsTrue(result.UploadSucceeded); } [TestMethod] [ExpectedException(typeof(ServiceException))] public void GetChunkRequestResponseTest_Fail() { var exception = new ServiceException(new Error { Code = "Timeout" }); var result = this.SetupGetChunkResponseTest(exception, failsOnce: false); } [TestMethod] public void GetChunkRequestResponseTest_InvalidRange() { var exception = new ServiceException(new Error { Code = "InvalidRange" }); var result = this.SetupGetChunkResponseTest(exception, verifyTrackedExceptions: false); Assert.IsNull(result.ItemResponse, "Expected no Item in ItemResponse"); Assert.IsNull(result.UploadSession, "Expected no UploadSession in response"); } [TestMethod] public void UploadAsync_RetryUpToMax() { const string resultId = "awesome"; var provider = new Mock( this.uploadSession.Object, this.client.Object, this.uploadStream.Object, this.myChunkSize) { CallBase = true }; var mockRequest = new Mock( "http://www.example.com/api/v1.0", this.client.Object, null, 0, 1, 2); var emptyList = new List(); var singleRequest = new List {mockRequest.Object}; provider.SetupSequence(p => p.GetUploadChunkRequests(It.IsAny>())) .Returns(emptyList) .Returns(emptyList) .Returns(singleRequest); provider.Setup(p => p.GetChunkRequestResponseAsync( It.IsAny(), It.IsAny(), It.IsAny>())) .Returns(Task.FromResult(new UploadChunkResult {ItemResponse = new Item {Id = resultId} })); provider.Setup(p => p.UpdateSessionStatusAsync()).Returns(Task.FromResult(new UploadSession())); var task = provider.Object.UploadAsync(maxTries: 3); task.Wait(); Assert.AreEqual(task.Result?.Id, resultId, "Unexpected Item result"); } [TestMethod] [ExpectedException(typeof(TaskCanceledException))] public void UploadAsync_TooManyRetries() { var provider = new Mock( this.uploadSession.Object, this.client.Object, this.uploadStream.Object, this.myChunkSize) { CallBase = true }; var emptyList = new List(); provider.SetupSequence(p => p.GetUploadChunkRequests(It.IsAny>())) .Returns(emptyList) .Returns(emptyList) .Returns(emptyList); provider.Setup(p => p.UpdateSessionStatusAsync()).Returns(Task.FromResult(new UploadSession())); try { var task = provider.Object.UploadAsync(maxTries: 3); task.Wait(); } catch (AggregateException exception) { throw exception.InnerException; } } private List> SetupRangesRemainingTest( int chunkSize, long currentFileSize, IList nextExpectedRanges) { this.StreamSetup(true); var provider = new TestChunkedUploadProvider( this.uploadSession.Object, this.client.Object, this.uploadStream.Object, chunkSize); var url = "http://myurl"; this.uploadStream.Setup(s => s.Length).Returns(currentFileSize); var session = new UploadSession { UploadUrl = url, NextExpectedRanges = nextExpectedRanges }; return provider.GetRangesRemainingProxy(session); } private IEnumerable SetupGetUploadChunksTest(int chunkSize, long totalSize, IEnumerable ranges) { this.uploadSession.Object.NextExpectedRanges = ranges; this.uploadStream = new Mock(); this.uploadStream.Setup(s => s.Length).Returns(totalSize); this.StreamSetup(true); var provider = new ChunkedUploadProvider( this.uploadSession.Object, this.client.Object, this.uploadStream.Object, chunkSize); return provider.GetUploadChunkRequests(); } private UploadChunkResult SetupGetChunkResponseTest(ServiceException serviceException = null, bool failsOnce = true, bool verifyTrackedExceptions = true) { var chunkSize = 320 * 1024; var bytesToUpload = new byte[] { 4, 8, 15, 16 }; var trackedExceptions = new List(); this.uploadSession.Object.NextExpectedRanges = new[] { "0-" }; var stream = new MemoryStream(bytesToUpload.Length); stream.Write(bytesToUpload, 0, bytesToUpload.Length); stream.Seek(0, SeekOrigin.Begin); var provider = new ChunkedUploadProvider( this.uploadSession.Object, this.client.Object, stream, chunkSize); var mockRequest = new Mock( this.uploadSession.Object.UploadUrl, this.client.Object, null, 0, bytesToUpload.Length - 1, bytesToUpload.Length); if (serviceException != null && failsOnce) { mockRequest.SetupSequence(r => r.PutAsync( It.IsAny(), It.IsAny())) .Throws(serviceException) .Returns(Task.FromResult(new UploadChunkResult() { ItemResponse = new Item()})); } else if (serviceException != null) { mockRequest.Setup(r => r.PutAsync( It.IsAny(), It.IsAny())) .Throws(serviceException); } else { mockRequest.Setup(r => r.PutAsync( It.IsAny(), It.IsAny())) .Returns(Task.FromResult(new UploadChunkResult { ItemResponse = new Item()})); } var task = provider.GetChunkRequestResponseAsync(mockRequest.Object, bytesToUpload, trackedExceptions); try { task.Wait(); } catch (AggregateException exception) { throw exception.InnerException; } if (verifyTrackedExceptions) { Assert.IsTrue(trackedExceptions.Contains(serviceException), "Expected ServiceException in TrackedException list"); } return task.Result; } private void AssertRangesAre(IList> rangesExpected, IList> rangesReceived) { Assert.AreEqual(rangesExpected.Count, rangesReceived.Count, "Unexpected number of ranges remaining"); for (var index = 0; index < rangesExpected.Count; index++) { Assert.AreEqual( rangesExpected[index], rangesReceived[index], string.Format("Expected range {0}-{1}, received {2}-{3}", rangesExpected[index].Item1, rangesExpected[index].Item2, rangesReceived[index].Item1, rangesReceived[index].Item2)); } } private void AssertChunksAre(IEnumerable expectedChunks, IEnumerable receivedChunks) { Assert.AreEqual(expectedChunks.Count(), receivedChunks.Count(), "Incorrect number of chunks received"); var receivedSet = new HashSet>(); foreach (var chunk in receivedChunks) { Assert.IsTrue(receivedSet.Add(new Tuple(chunk.RangeBegin, chunk.RangeEnd, chunk.TotalSessionLength)), "Duplicate range added"); } foreach (var chunk in expectedChunks) { Assert.IsTrue(receivedSet.Remove(new Tuple(chunk.RangeBegin, chunk.RangeEnd, chunk.TotalSessionLength)), $"Expected chunk not found: {chunk.RangeBegin}-{chunk.RangeEnd}/{chunk.TotalSessionLength}"); } } private IEnumerable CreateUploadExpectedChunkRequests( IEnumerable> chunkSpecifiers) { return chunkSpecifiers.Select(chunk => new UploadChunkRequest( "http://www.example.com/api/v1.0", this.client.Object, null, chunk.Item1, chunk.Item2, chunk.Item3)); } private void StreamSetup(bool canReadAndSeek) { this.uploadStream.Setup(s => s.CanSeek).Returns(canReadAndSeek); this.uploadStream.Setup(s => s.CanRead).Returns(canReadAndSeek); } } }