PageRenderTime 47ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/test/Microsoft.Web.Http.Data.Test/DataControllerSubmitTests.cs

https://bitbucket.org/mdavid/aspnetwebstack
C# | 373 lines | 284 code | 56 blank | 33 comment | 9 complexity | f469d28ea5e3684e96f50bc553709289 MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Net;
  6. using System.Net.Http;
  7. using System.Net.Http.Headers;
  8. using System.Runtime.Serialization;
  9. using System.Text;
  10. using System.Threading;
  11. using System.Web.Http;
  12. using System.Web.Http.Controllers;
  13. using System.Web.Http.Dispatcher;
  14. using System.Web.Http.Filters;
  15. using System.Web.Http.Routing;
  16. using Microsoft.Web.Http.Data.Test.Models;
  17. using Newtonsoft.Json;
  18. using Xunit;
  19. using Assert = Microsoft.TestCommon.AssertEx;
  20. namespace Microsoft.Web.Http.Data.Test
  21. {
  22. public class DataControllerSubmitTests
  23. {
  24. // Verify that POSTs directly to CUD actions still go through the submit pipeline
  25. [Fact]
  26. public void Submit_Proxy_Insert()
  27. {
  28. Order order = new Order { OrderID = 1, OrderDate = DateTime.Now };
  29. HttpResponseMessage response = this.ExecuteSelfHostRequest(TestConstants.CatalogUrl + "InsertOrder", "Catalog", order);
  30. Order resultOrder = response.Content.ReadAsAsync<Order>().Result;
  31. Assert.NotNull(resultOrder);
  32. }
  33. // Submit a changeset with multiple entries
  34. [Fact]
  35. public void Submit_Multiple_Success()
  36. {
  37. Order order = new Order { OrderID = 1, OrderDate = DateTime.Now };
  38. Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
  39. ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
  40. new ChangeSetEntry { Id = 1, Entity = order, Operation = ChangeOperation.Insert },
  41. new ChangeSetEntry { Id = 2, Entity = product, Operation = ChangeOperation.Update }
  42. };
  43. ChangeSetEntry[] resultChangeSet = this.ExecuteSubmit(TestConstants.CatalogUrl + "Submit", "Catalog", changeSet);
  44. Assert.Equal(2, resultChangeSet.Length);
  45. Assert.True(resultChangeSet.All(p => !p.HasError));
  46. }
  47. // Submit a changeset with one parent object and multiple dependent children
  48. [Fact]
  49. public void Submit_Tree_Success()
  50. {
  51. Order order = new Order { OrderID = 1, OrderDate = DateTime.Now };
  52. Order_Detail d1 = new Order_Detail { ProductID = 1 };
  53. Order_Detail d2 = new Order_Detail { ProductID = 2 };
  54. Dictionary<string, int[]> detailsAssociation = new Dictionary<string, int[]>();
  55. detailsAssociation.Add("Order_Details", new int[] { 2, 3 });
  56. ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
  57. new ChangeSetEntry { Id = 1, Entity = order, Operation = ChangeOperation.Insert, Associations = detailsAssociation },
  58. new ChangeSetEntry { Id = 2, Entity = d1, Operation = ChangeOperation.Insert },
  59. new ChangeSetEntry { Id = 3, Entity = d2, Operation = ChangeOperation.Insert }
  60. };
  61. ChangeSetEntry[] resultChangeSet = this.ExecuteSubmit(TestConstants.CatalogUrl + "Submit", "Catalog", changeSet);
  62. Assert.Equal(3, resultChangeSet.Length);
  63. Assert.True(resultChangeSet.All(p => !p.HasError));
  64. }
  65. /// <summary>
  66. /// End to end validation scenario showing changeset validation. DataAnnotations validation attributes are applied to
  67. /// the model by DataController metadata providers (metadata coming all the way from the EF model, as well as "buddy
  68. /// class" metadata), and these are validated during changeset validation. The validation results per entity/member are
  69. /// returned via the changeset and verified.
  70. /// </summary>
  71. [Fact]
  72. public void Submit_Validation_Failure()
  73. {
  74. Microsoft.Web.Http.Data.Test.Models.EF.Product newProduct = new Microsoft.Web.Http.Data.Test.Models.EF.Product { ProductID = 1, ProductName = String.Empty, UnitPrice = -1 };
  75. Microsoft.Web.Http.Data.Test.Models.EF.Product updateProduct = new Microsoft.Web.Http.Data.Test.Models.EF.Product { ProductID = 1, ProductName = new string('x', 50), UnitPrice = 55.77M };
  76. ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
  77. new ChangeSetEntry { Id = 1, Entity = newProduct, Operation = ChangeOperation.Insert },
  78. new ChangeSetEntry { Id = 2, Entity = updateProduct, Operation = ChangeOperation.Update }
  79. };
  80. HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/NorthwindEFTest/Submit", "NorthwindEFTest", changeSet);
  81. changeSet = response.Content.ReadAsAsync<ChangeSetEntry[]>().Result;
  82. // errors for the new product
  83. ValidationResultInfo[] errors = changeSet[0].ValidationErrors.ToArray();
  84. Assert.Equal(2, errors.Length);
  85. Assert.True(changeSet[0].HasError);
  86. // validation rule inferred from EF model
  87. Assert.Equal("ProductName", errors[0].SourceMemberNames.Single());
  88. Assert.Equal("The ProductName field is required.", errors[0].Message);
  89. // validation rule coming from buddy class
  90. Assert.Equal("UnitPrice", errors[1].SourceMemberNames.Single());
  91. Assert.Equal("The field UnitPrice must be between 0 and 1000000.", errors[1].Message);
  92. // errors for the updated product
  93. errors = changeSet[1].ValidationErrors.ToArray();
  94. Assert.Equal(1, errors.Length);
  95. Assert.True(changeSet[1].HasError);
  96. // validation rule inferred from EF model
  97. Assert.Equal("ProductName", errors[0].SourceMemberNames.Single());
  98. Assert.Equal("The field ProductName must be a string with a maximum length of 40.", errors[0].Message);
  99. }
  100. [Fact]
  101. public void Submit_Authorization_Success()
  102. {
  103. TestAuthAttribute.Reset();
  104. Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
  105. ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
  106. new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
  107. };
  108. ChangeSetEntry[] resultChangeSet = this.ExecuteSubmit("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
  109. Assert.Equal(1, resultChangeSet.Length);
  110. Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global", "Class", "SubmitMethod", "UserMethod" }));
  111. }
  112. [Fact]
  113. public void Submit_Authorization_Fail_UserMethod()
  114. {
  115. TestAuthAttribute.Reset();
  116. Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
  117. ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
  118. new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
  119. };
  120. TestAuthAttribute.FailLevel = "UserMethod";
  121. HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
  122. Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global", "Class", "SubmitMethod", "UserMethod" }));
  123. Assert.Equal("Not Authorized", response.ReasonPhrase);
  124. Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
  125. }
  126. [Fact]
  127. public void Submit_Authorization_Fail_SubmitMethod()
  128. {
  129. TestAuthAttribute.Reset();
  130. Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
  131. ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
  132. new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
  133. };
  134. TestAuthAttribute.FailLevel = "SubmitMethod";
  135. HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
  136. Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global", "Class", "SubmitMethod" }));
  137. Assert.Equal("Not Authorized", response.ReasonPhrase);
  138. Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
  139. }
  140. [Fact]
  141. public void Submit_Authorization_Fail_Class()
  142. {
  143. TestAuthAttribute.Reset();
  144. Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
  145. ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
  146. new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
  147. };
  148. TestAuthAttribute.FailLevel = "Class";
  149. HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
  150. Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global", "Class" }));
  151. Assert.Equal("Not Authorized", response.ReasonPhrase);
  152. Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
  153. }
  154. [Fact]
  155. public void Submit_Authorization_Fail_Global()
  156. {
  157. TestAuthAttribute.Reset();
  158. Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
  159. ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
  160. new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
  161. };
  162. TestAuthAttribute.FailLevel = "Global";
  163. HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
  164. Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global" }));
  165. Assert.Equal("Not Authorized", response.ReasonPhrase);
  166. Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
  167. }
  168. // Verify that a CUD operation that isn't supported for a given entity type
  169. // results in a server error
  170. [Fact]
  171. public void Submit_ResolveActions_UnsupportedAction()
  172. {
  173. Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
  174. ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
  175. new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Delete }
  176. };
  177. HttpConfiguration configuration = new HttpConfiguration();
  178. HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(configuration, "NorthwindEFTestController", typeof(NorthwindEFTestController));
  179. DataControllerDescription description = DataControllerDescription.GetDescription(controllerDescriptor);
  180. Assert.Throws<InvalidOperationException>(
  181. () => DataController.ResolveActions(description, changeSet),
  182. String.Format(Resource.DataController_InvalidAction, "Delete", "Product"));
  183. }
  184. /// <summary>
  185. /// Execute a full roundtrip Submit request for the specified changeset, going through
  186. /// the full serialization pipeline.
  187. /// </summary>
  188. private ChangeSetEntry[] ExecuteSubmit(string url, string controllerName, ChangeSetEntry[] changeSet)
  189. {
  190. HttpResponseMessage response = this.ExecuteSelfHostRequest(url, controllerName, changeSet);
  191. ChangeSetEntry[] resultChangeSet = GetChangesetResponse(response);
  192. return changeSet;
  193. }
  194. private HttpResponseMessage ExecuteSelfHostRequest(string url, string controller, object data)
  195. {
  196. return ExecuteSelfHostRequest(url, controller, data, "application/json");
  197. }
  198. private HttpResponseMessage ExecuteSelfHostRequest(string url, string controller, object data, string mediaType)
  199. {
  200. HttpConfiguration config = new HttpConfiguration();
  201. IHttpRoute routeData;
  202. if (!config.Routes.TryGetValue(controller, out routeData))
  203. {
  204. HttpRoute route = new HttpRoute("{controller}/{action}", new HttpRouteValueDictionary(controller));
  205. config.Routes.Add(controller, route);
  206. }
  207. HttpControllerDispatcher dispatcher = new HttpControllerDispatcher(config);
  208. HttpServer server = new HttpServer(config, dispatcher);
  209. HttpMessageInvoker invoker = new HttpMessageInvoker(server);
  210. string serializedChangeSet = String.Empty;
  211. if (mediaType == "application/json")
  212. {
  213. JsonSerializer serializer = new JsonSerializer() { PreserveReferencesHandling = PreserveReferencesHandling.Objects, TypeNameHandling = TypeNameHandling.All };
  214. MemoryStream ms = new MemoryStream();
  215. JsonWriter writer = new JsonTextWriter(new StreamWriter(ms));
  216. serializer.Serialize(writer, data);
  217. writer.Flush();
  218. ms.Seek(0, 0);
  219. serializedChangeSet = Encoding.UTF8.GetString(ms.GetBuffer()).TrimEnd('\0');
  220. }
  221. else
  222. {
  223. DataContractSerializer ser = new DataContractSerializer(data.GetType(), GetTestKnownTypes());
  224. MemoryStream ms = new MemoryStream();
  225. ser.WriteObject(ms, data);
  226. ms.Flush();
  227. ms.Seek(0, 0);
  228. serializedChangeSet = Encoding.UTF8.GetString(ms.GetBuffer()).TrimEnd('\0');
  229. }
  230. HttpRequestMessage request = TestHelpers.CreateTestMessage(url, HttpMethod.Post, config);
  231. request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType));
  232. request.Content = new StringContent(serializedChangeSet, Encoding.UTF8, mediaType);
  233. return invoker.SendAsync(request, CancellationToken.None).Result;
  234. }
  235. /// <summary>
  236. /// For the given Submit response, serialize and deserialize the content. This forces the
  237. /// formatter pipeline to run so we can verify that registered serializers are being used
  238. /// properly.
  239. /// </summary>
  240. private ChangeSetEntry[] GetChangesetResponse(HttpResponseMessage responseMessage)
  241. {
  242. // serialize the content to a stream
  243. ObjectContent content = (ObjectContent)responseMessage.Content;
  244. MemoryStream ms = new MemoryStream();
  245. content.CopyToAsync(ms).Wait();
  246. ms.Flush();
  247. ms.Seek(0, 0);
  248. // deserialize based on content type
  249. ChangeSetEntry[] changeSet = null;
  250. string mediaType = responseMessage.RequestMessage.Content.Headers.ContentType.MediaType;
  251. if (mediaType == "application/json")
  252. {
  253. JsonSerializer ser = new JsonSerializer() { PreserveReferencesHandling = PreserveReferencesHandling.Objects, TypeNameHandling = TypeNameHandling.All };
  254. changeSet = (ChangeSetEntry[])ser.Deserialize(new JsonTextReader(new StreamReader(ms)), content.ObjectType);
  255. }
  256. else
  257. {
  258. DataContractSerializer ser = new DataContractSerializer(content.ObjectType, GetTestKnownTypes());
  259. changeSet = (ChangeSetEntry[])ser.ReadObject(ms);
  260. }
  261. return changeSet;
  262. }
  263. private IEnumerable<Type> GetTestKnownTypes()
  264. {
  265. List<Type> knownTypes = new List<Type>(new Type[] { typeof(Order), typeof(Product), typeof(Order_Detail) });
  266. knownTypes.AddRange(new Type[] { typeof(Microsoft.Web.Http.Data.Test.Models.EF.Order), typeof(Microsoft.Web.Http.Data.Test.Models.EF.Product), typeof(Microsoft.Web.Http.Data.Test.Models.EF.Order_Detail) });
  267. return knownTypes;
  268. }
  269. }
  270. /// <summary>
  271. /// Test controller used for multi-level authorization testing
  272. /// </summary>
  273. [TestAuth(Level = "Class")]
  274. public class TestAuthController : DataController
  275. {
  276. [TestAuth(Level = "UserMethod")]
  277. public void UpdateProduct(Product product)
  278. {
  279. }
  280. [TestAuth(Level = "SubmitMethod")]
  281. public override bool Submit(ChangeSet changeSet)
  282. {
  283. return base.Submit(changeSet);
  284. }
  285. protected override void Initialize(HttpControllerContext controllerContext)
  286. {
  287. controllerContext.Configuration.Filters.Add(new TestAuthAttribute() { Level = "Global" });
  288. base.Initialize(controllerContext);
  289. }
  290. }
  291. /// <summary>
  292. /// Test authorization attribute used to verify authorization behavior.
  293. /// </summary>
  294. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
  295. public class TestAuthAttribute : AuthorizationFilterAttribute
  296. {
  297. public string Level;
  298. public static string FailLevel;
  299. public static List<string> Log = new List<string>();
  300. public override void OnAuthorization(HttpActionContext context)
  301. {
  302. TestAuthAttribute.Log.Add(Level);
  303. if (FailLevel != null && FailLevel == Level)
  304. {
  305. HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
  306. response.ReasonPhrase = "Not Authorized";
  307. context.Response = response;
  308. }
  309. base.OnAuthorization(context);
  310. }
  311. public static void Reset()
  312. {
  313. FailLevel = null;
  314. Log.Clear();
  315. }
  316. }
  317. }