// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Moq; using Xunit; using Assert = Microsoft.TestCommon.AssertEx; namespace System.Web.Mvc.Async.Test { public class TaskAsyncActionDescriptorTest { private readonly MethodInfo _taskMethod = typeof(ExecuteController).GetMethod("SimpleTask"); [Fact] public void Constructor_SetsProperties() { // Arrange string actionName = "SomeAction"; ControllerDescriptor cd = new Mock().Object; // Act TaskAsyncActionDescriptor ad = new TaskAsyncActionDescriptor(_taskMethod, actionName, cd); // Assert Assert.Equal(_taskMethod, ad.TaskMethodInfo); Assert.Equal(actionName, ad.ActionName); Assert.Equal(cd, ad.ControllerDescriptor); } [Fact] public void Constructor_ThrowsIfActionNameIsEmpty() { // Arrange ControllerDescriptor cd = new Mock().Object; // Act & assert Assert.ThrowsArgumentNullOrEmpty( delegate { new TaskAsyncActionDescriptor(_taskMethod, "", cd); }, "actionName"); } [Fact] public void Constructor_ThrowsIfActionNameIsNull() { // Arrange ControllerDescriptor cd = new Mock().Object; // Act & assert Assert.ThrowsArgumentNullOrEmpty( delegate { new TaskAsyncActionDescriptor(_taskMethod, null, cd); }, "actionName"); } [Fact] public void Constructor_ThrowsIfTaskMethodInfoIsInvalid() { // Arrange ControllerDescriptor cd = new Mock().Object; MethodInfo getHashCodeMethod = typeof(object).GetMethod("GetHashCode"); // Act & assert Assert.Throws( delegate { new TaskAsyncActionDescriptor(getHashCodeMethod, "SomeAction", cd); }, @"Cannot create a descriptor for instance method 'Int32 GetHashCode()' on type 'System.Object' because the type does not derive from ControllerBase. Parameter name: taskMethodInfo"); } [Fact] public void Constructor_ThrowsIfTaskMethodInfoIsNull() { // Arrange ControllerDescriptor cd = new Mock().Object; // Act & assert Assert.ThrowsArgumentNull( delegate { new TaskAsyncActionDescriptor(null, "SomeAction", cd); }, "taskMethodInfo"); } [Fact] public void Constructor_ThrowsIfControllerDescriptorIsNull() { // Act & assert Assert.ThrowsArgumentNull( delegate { new TaskAsyncActionDescriptor(_taskMethod, "SomeAction", null); }, "controllerDescriptor"); } [Fact] public void ExecuteTask() { // Arrange TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("SimpleTask")); Dictionary parameters = new Dictionary() { { "doWork", true } }; ControllerContext controllerContext = GetControllerContext(); // Act object retVal = ExecuteHelper(actionDescriptor, parameters, controllerContext); // Assert Assert.Null(retVal); Assert.True((controllerContext.Controller as ExecuteController).WorkDone); } [Fact] public void ExecuteTaskGeneric() { // Arrange TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("GenericTask")); Dictionary parameters = new Dictionary() { { "taskId", "foo" } }; // Act object retVal = ExecuteHelper(actionDescriptor, parameters); // Assert Assert.Equal("foo", retVal); } [Fact] public void ExecuteTaskPreservesStackTraceOnException() { // Arrange TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("SimpleTaskException")); Dictionary parameters = new Dictionary() { { "doWork", true } }; // Act IAsyncResult result = actionDescriptor.BeginExecute(GetControllerContext(), parameters, null, null); // Assert InvalidOperationException ex = Assert.Throws( () => actionDescriptor.EndExecute(result), "Test exception from action" ); Assert.True(ex.StackTrace.Contains("System.Web.Mvc.Async.Test.TaskAsyncActionDescriptorTest.ExecuteController.")); } [Fact] public void ExecuteTaskGenericPreservesStackTraceOnException() { // Arrange TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("GenericTaskException")); Dictionary parameters = new Dictionary() { { "taskId", "foo" }, { "throwException", true } }; // Act IAsyncResult result = actionDescriptor.BeginExecute(GetControllerContext(), parameters, null, null); // Assert InvalidOperationException ex = Assert.Throws( () => actionDescriptor.EndExecute(result), "Test exception from action" ); Assert.True(ex.StackTrace.Contains("System.Web.Mvc.Async.Test.TaskAsyncActionDescriptorTest.ExecuteController.")); } [Fact] public void ExecuteTaskOfPrivateT() { // Arrange TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("TaskOfPrivateT")); ControllerContext controllerContext = GetControllerContext(); Dictionary parameters = new Dictionary(); // Act object retVal = ExecuteHelper(actionDescriptor, parameters, controllerContext); // Assert Assert.Null(retVal); Assert.True((controllerContext.Controller as ExecuteController).WorkDone); } [Fact] public void ExecuteTaskPreservesState() { // Arrange TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("SimpleTask")); Dictionary parameters = new Dictionary() { { "doWork", true } }; ControllerContext controllerContext = GetControllerContext(); // Act TaskWrapperAsyncResult result = (TaskWrapperAsyncResult)actionDescriptor.BeginExecute(GetControllerContext(), parameters, callback: null, state: "state"); // Assert Assert.Equal("state", result.AsyncState); } [Fact] public void ExecuteTaskWithNullParameterAndTimeout() { // Arrange TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("TaskTimeoutWithNullParam")); Dictionary token = new Dictionary() { { "nullParam", null }, { "cancellationToken", new CancellationToken() } }; // Act & assert Assert.Throws( () => actionDescriptor.EndExecute(actionDescriptor.BeginExecute(GetControllerContext(0), parameters: token, callback: null, state: null)), "The operation has timed out." ); } [Fact] public void ExecuteWithInfiniteTimeout() { // Arrange TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("TaskWithInfiniteTimeout")); ControllerContext controllerContext = GetControllerContext(Timeout.Infinite); Dictionary parameters = new Dictionary() { { "cancellationToken", new CancellationToken() } }; // Act object retVal = ExecuteHelper(actionDescriptor, parameters); // Assert Assert.Equal("Task Completed", retVal); } [Fact] public void ExecuteTaskWithImmediateTimeout() { // Arrange TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("TaskTimeout")); Dictionary token = new Dictionary() { { "cancellationToken", new CancellationToken() } }; // Act & assert Assert.Throws( () => actionDescriptor.EndExecute(actionDescriptor.BeginExecute(GetControllerContext(0), parameters: token, callback: null, state: null)), "The operation has timed out." ); } [Fact] public void ExecuteTaskWithTimeout() { // Arrange TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("TaskTimeout")); Dictionary token = new Dictionary() { { "cancellationToken", new CancellationToken() } }; // Act & assert Assert.Throws( () => actionDescriptor.EndExecute(actionDescriptor.BeginExecute(GetControllerContext(2000), parameters: token, callback: null, state: null)), "The operation has timed out." ); } [Fact] public void SynchronousExecuteThrows() { // Arrange TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("SimpleTask")); // Act & assert Assert.Throws( delegate { actionDescriptor.Execute(new ControllerContext(), new Dictionary()); }, "The asynchronous action method 'someName' returns a Task, which cannot be executed synchronously."); } [Fact] public void Execute_ThrowsIfControllerContextIsNull() { // Arrange TaskAsyncActionDescriptor ad = GetActionDescriptor(_taskMethod); // Act & assert Assert.ThrowsArgumentNull( delegate { ad.BeginExecute(null, new Dictionary(), null, null); }, "controllerContext"); } [Fact] public void Execute_ThrowsIfControllerIsNotAsyncManagerContainer() { // Arrange TaskAsyncActionDescriptor ad = GetActionDescriptor(_taskMethod); ControllerContext controllerContext = new ControllerContext() { Controller = new RegularSyncController() }; Dictionary parameters = new Dictionary() { { "doWork", true } }; // Act & assert Assert.Throws( delegate { ad.BeginExecute(controllerContext, parameters, null, null); }, @"The controller of type 'System.Web.Mvc.Async.Test.TaskAsyncActionDescriptorTest+RegularSyncController' must subclass AsyncController or implement the IAsyncManagerContainer interface."); } [Fact] public void Execute_ThrowsIfParametersIsNull() { // Arrange TaskAsyncActionDescriptor ad = GetActionDescriptor(_taskMethod); // Act & assert Assert.ThrowsArgumentNull( delegate { ad.BeginExecute(new ControllerContext(), null, null, null); }, "parameters"); } [Fact] public void GetCustomAttributesCallsMethodInfoGetCustomAttributes() { // Arrange object[] expected = new object[0]; Mock mockMethod = new Mock(); mockMethod.Setup(mi => mi.GetCustomAttributes(true)).Returns(expected); TaskAsyncActionDescriptor ad = new TaskAsyncActionDescriptor(mockMethod.Object, "someName", new Mock().Object, validateMethod: false) { DispatcherCache = new ActionMethodDispatcherCache() }; // Act object[] returned = ad.GetCustomAttributes(true); // Assert Assert.Same(expected, returned); } [Fact] public void GetCustomAttributesWithAttributeTypeCallsMethodInfoGetCustomAttributes() { // Arrange object[] expected = new object[0]; Mock mockMethod = new Mock(); mockMethod.Setup(mi => mi.GetCustomAttributes(typeof(ObsoleteAttribute), true)).Returns(expected); TaskAsyncActionDescriptor ad = new TaskAsyncActionDescriptor(mockMethod.Object, "someName", new Mock().Object, validateMethod: false) { DispatcherCache = new ActionMethodDispatcherCache() }; // Act object[] returned = ad.GetCustomAttributes(typeof(ObsoleteAttribute), true); // Assert Assert.Same(expected, returned); } [Fact] public void GetParameters() { // Arrange ParameterInfo pInfo = _taskMethod.GetParameters()[0]; TaskAsyncActionDescriptor ad = GetActionDescriptor(_taskMethod); // Act ParameterDescriptor[] pDescsFirstCall = ad.GetParameters(); ParameterDescriptor[] pDescsSecondCall = ad.GetParameters(); // Assert Assert.NotSame(pDescsFirstCall, pDescsSecondCall); // Should get a new array every time Assert.Equal(pDescsFirstCall, pDescsSecondCall); Assert.Single(pDescsFirstCall); ReflectedParameterDescriptor pDesc = pDescsFirstCall[0] as ReflectedParameterDescriptor; Assert.NotNull(pDesc); Assert.Same(ad, pDesc.ActionDescriptor); Assert.Same(pInfo, pDesc.ParameterInfo); } [Fact] public void GetSelectors() { // Arrange ControllerContext controllerContext = new Mock().Object; Mock mockMethod = new Mock(); Mock mockAttr = new Mock(); mockAttr.Setup(attr => attr.IsValidForRequest(controllerContext, mockMethod.Object)).Returns(true).Verifiable(); mockMethod.Setup(m => m.GetCustomAttributes(typeof(ActionMethodSelectorAttribute), true)).Returns(new ActionMethodSelectorAttribute[] { mockAttr.Object }); TaskAsyncActionDescriptor ad = new TaskAsyncActionDescriptor(mockMethod.Object, "someName", new Mock().Object, validateMethod: false) { DispatcherCache = new ActionMethodDispatcherCache() }; // Act ICollection selectors = ad.GetSelectors(); bool executedSuccessfully = selectors.All(s => s(controllerContext)); // Assert Assert.Single(selectors); Assert.True(executedSuccessfully); mockAttr.Verify(); } [Fact] public void IsDefined() { // Arrange TaskAsyncActionDescriptor ad = GetActionDescriptor(_taskMethod); // Act bool isDefined = ad.IsDefined(typeof(AuthorizeAttribute), inherit: true); // Assert Assert.True(isDefined); } public static object ExecuteHelper(TaskAsyncActionDescriptor actionDescriptor, Dictionary parameters, ControllerContext controllerContext = null) { SignalContainer resultContainer = new SignalContainer(); AsyncCallback callback = ar => { object o = actionDescriptor.EndExecute(ar); resultContainer.Signal(o); }; actionDescriptor.BeginExecute(controllerContext ?? GetControllerContext(), parameters, callback, state: null); return resultContainer.Wait(); } private static TaskAsyncActionDescriptor GetActionDescriptor(MethodInfo taskMethod) { return new TaskAsyncActionDescriptor(taskMethod, "someName", new Mock().Object) { DispatcherCache = new ActionMethodDispatcherCache() }; } private static ControllerContext GetControllerContext(int timeout = 45 * 1000) { Mock mockControllerContext = new Mock(); ExecuteController controller = new ExecuteController(); controller.AsyncManager.Timeout = timeout; mockControllerContext.Setup(c => c.Controller).Returns(controller); return mockControllerContext.Object; } private static MethodInfo GetExecuteControllerMethodInfo(string methodName) { return typeof(ExecuteController).GetMethod(methodName); } private class ExecuteController : AsyncController { public bool WorkDone { get; set; } public Task ReturnedTask { get; set; } public Task GenericTask(string taskId) { return Task.Factory.StartNew(() => taskId); } public Task GenericTaskException(string taskId, bool throwException) { return Task.Factory.StartNew(() => { if (throwException) { ThrowException(); } ; return taskId; }); } private void ThrowException() { throw new InvalidOperationException("Test exception from action"); } [Authorize] public Task SimpleTask(bool doWork) { return Task.Factory.StartNew(() => { WorkDone = doWork; }); } public Task SimpleTaskException(bool doWork) { return Task.Factory.StartNew(() => { ThrowException(); }); } public Task TaskTimeoutWithNullParam(Object nullParam, CancellationToken cancellationToken) { return TaskTimeout(cancellationToken); } public Task TaskWithInfiniteTimeout(CancellationToken cancellationToken) { return Task.Factory.StartNew(() => "Task Completed"); } public Task TaskTimeout(CancellationToken cancellationToken) { TaskCompletionSource completionSource = new TaskCompletionSource(); cancellationToken.Register(() => completionSource.TrySetCanceled()); ReturnedTask = completionSource.Task; return ReturnedTask; } public Task TaskOfPrivateT() { var completionSource = new TaskCompletionSource(); completionSource.SetResult(new PrivateObject()); WorkDone = true; return completionSource.Task; } private class PrivateObject { public override string ToString() { return "Private Object"; } } } // Controller is async, so derive from ControllerBase to get sync behavior. private class RegularSyncController : ControllerBase { protected override void ExecuteCore() { throw new NotImplementedException(); } } } }