Jo Shields a575963da9 Imported Upstream version 3.6.0
Former-commit-id: da6be194a6b1221998fc28233f2503bd61dd9d14
2014-08-13 10:39:27 +01:00

376 lines
17 KiB
C#

// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
using System.Web.Http.Filters;
using System.Web.Http.Routing;
using Microsoft.Web.Http.Data.Test.Models;
using Newtonsoft.Json;
using Xunit;
using Assert = Microsoft.TestCommon.AssertEx;
namespace Microsoft.Web.Http.Data.Test
{
public class DataControllerSubmitTests
{
// Verify that POSTs directly to CUD actions still go through the submit pipeline
[Fact]
public void Submit_Proxy_Insert()
{
Order order = new Order { OrderID = 1, OrderDate = DateTime.Now };
HttpResponseMessage response = this.ExecuteSelfHostRequest(TestConstants.CatalogUrl + "InsertOrder", "Catalog", order);
Order resultOrder = response.Content.ReadAsAsync<Order>().Result;
Assert.NotNull(resultOrder);
}
// Submit a changeset with multiple entries
[Fact]
public void Submit_Multiple_Success()
{
Order order = new Order { OrderID = 1, OrderDate = DateTime.Now };
Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
new ChangeSetEntry { Id = 1, Entity = order, Operation = ChangeOperation.Insert },
new ChangeSetEntry { Id = 2, Entity = product, Operation = ChangeOperation.Update }
};
ChangeSetEntry[] resultChangeSet = this.ExecuteSubmit(TestConstants.CatalogUrl + "Submit", "Catalog", changeSet);
Assert.Equal(2, resultChangeSet.Length);
Assert.True(resultChangeSet.All(p => !p.HasError));
}
// Submit a changeset with one parent object and multiple dependent children
[Fact]
public void Submit_Tree_Success()
{
Order order = new Order { OrderID = 1, OrderDate = DateTime.Now };
Order_Detail d1 = new Order_Detail { ProductID = 1 };
Order_Detail d2 = new Order_Detail { ProductID = 2 };
Dictionary<string, int[]> detailsAssociation = new Dictionary<string, int[]>();
detailsAssociation.Add("Order_Details", new int[] { 2, 3 });
ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
new ChangeSetEntry { Id = 1, Entity = order, Operation = ChangeOperation.Insert, Associations = detailsAssociation },
new ChangeSetEntry { Id = 2, Entity = d1, Operation = ChangeOperation.Insert },
new ChangeSetEntry { Id = 3, Entity = d2, Operation = ChangeOperation.Insert }
};
ChangeSetEntry[] resultChangeSet = this.ExecuteSubmit(TestConstants.CatalogUrl + "Submit", "Catalog", changeSet);
Assert.Equal(3, resultChangeSet.Length);
Assert.True(resultChangeSet.All(p => !p.HasError));
}
/// <summary>
/// End to end validation scenario showing changeset validation. DataAnnotations validation attributes are applied to
/// the model by DataController metadata providers (metadata coming all the way from the EF model, as well as "buddy
/// class" metadata), and these are validated during changeset validation. The validation results per entity/member are
/// returned via the changeset and verified.
/// </summary>
[Fact]
public void Submit_Validation_Failure()
{
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 };
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 };
ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
new ChangeSetEntry { Id = 1, Entity = newProduct, Operation = ChangeOperation.Insert },
new ChangeSetEntry { Id = 2, Entity = updateProduct, Operation = ChangeOperation.Update }
};
HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/NorthwindEFTest/Submit", "NorthwindEFTest", changeSet);
changeSet = response.Content.ReadAsAsync<ChangeSetEntry[]>().Result;
// errors for the new product
ValidationResultInfo[] errors = changeSet[0].ValidationErrors.ToArray();
Assert.Equal(2, errors.Length);
Assert.True(changeSet[0].HasError);
// validation rule inferred from EF model
Assert.Equal("ProductName", errors[0].SourceMemberNames.Single());
Assert.Equal("The ProductName field is required.", errors[0].Message);
// validation rule coming from buddy class
Assert.Equal("UnitPrice", errors[1].SourceMemberNames.Single());
Assert.Equal("The field UnitPrice must be between 0 and 1000000.", errors[1].Message);
// errors for the updated product
errors = changeSet[1].ValidationErrors.ToArray();
Assert.Equal(1, errors.Length);
Assert.True(changeSet[1].HasError);
// validation rule inferred from EF model
Assert.Equal("ProductName", errors[0].SourceMemberNames.Single());
Assert.Equal("The field ProductName must be a string with a maximum length of 40.", errors[0].Message);
}
[Fact]
public void Submit_Authorization_Success()
{
TestAuthAttribute.Reset();
Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
};
ChangeSetEntry[] resultChangeSet = this.ExecuteSubmit("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
Assert.Equal(1, resultChangeSet.Length);
Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global", "Class", "SubmitMethod", "UserMethod" }));
}
[Fact]
public void Submit_Authorization_Fail_UserMethod()
{
TestAuthAttribute.Reset();
Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
};
TestAuthAttribute.FailLevel = "UserMethod";
HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global", "Class", "SubmitMethod", "UserMethod" }));
Assert.Equal("Not Authorized", response.ReasonPhrase);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public void Submit_Authorization_Fail_SubmitMethod()
{
TestAuthAttribute.Reset();
Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
};
TestAuthAttribute.FailLevel = "SubmitMethod";
HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global", "Class", "SubmitMethod" }));
Assert.Equal("Not Authorized", response.ReasonPhrase);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public void Submit_Authorization_Fail_Class()
{
TestAuthAttribute.Reset();
Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
};
TestAuthAttribute.FailLevel = "Class";
HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global", "Class" }));
Assert.Equal("Not Authorized", response.ReasonPhrase);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public void Submit_Authorization_Fail_Global()
{
TestAuthAttribute.Reset();
Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
};
TestAuthAttribute.FailLevel = "Global";
HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global" }));
Assert.Equal("Not Authorized", response.ReasonPhrase);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
// Verify that a CUD operation that isn't supported for a given entity type
// results in a server error
[Fact]
public void Submit_ResolveActions_UnsupportedAction()
{
Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Delete }
};
HttpConfiguration configuration = new HttpConfiguration();
HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(configuration, "NorthwindEFTestController", typeof(NorthwindEFTestController));
DataControllerDescription description = DataControllerDescription.GetDescription(controllerDescriptor);
Assert.Throws<InvalidOperationException>(
() => DataController.ResolveActions(description, changeSet),
String.Format(Resource.DataController_InvalidAction, "Delete", "Product"));
}
/// <summary>
/// Execute a full roundtrip Submit request for the specified changeset, going through
/// the full serialization pipeline.
/// </summary>
private ChangeSetEntry[] ExecuteSubmit(string url, string controllerName, ChangeSetEntry[] changeSet)
{
HttpResponseMessage response = this.ExecuteSelfHostRequest(url, controllerName, changeSet);
ChangeSetEntry[] resultChangeSet = GetChangesetResponse(response);
return changeSet;
}
private HttpResponseMessage ExecuteSelfHostRequest(string url, string controller, object data)
{
return ExecuteSelfHostRequest(url, controller, data, "application/json");
}
private HttpResponseMessage ExecuteSelfHostRequest(string url, string controller, object data, string mediaType)
{
HttpConfiguration config = new HttpConfiguration();
IHttpRoute routeData;
if (!config.Routes.TryGetValue(controller, out routeData))
{
HttpRoute route = new HttpRoute("{controller}/{action}", new HttpRouteValueDictionary(controller));
config.Routes.Add(controller, route);
}
HttpControllerDispatcher dispatcher = new HttpControllerDispatcher(config);
HttpServer server = new HttpServer(config, dispatcher);
HttpMessageInvoker invoker = new HttpMessageInvoker(server);
string serializedChangeSet = String.Empty;
if (mediaType == "application/json")
{
JsonSerializer serializer = new JsonSerializer() { PreserveReferencesHandling = PreserveReferencesHandling.Objects, TypeNameHandling = TypeNameHandling.All };
MemoryStream ms = new MemoryStream();
JsonWriter writer = new JsonTextWriter(new StreamWriter(ms));
serializer.Serialize(writer, data);
writer.Flush();
ms.Seek(0, 0);
serializedChangeSet = Encoding.UTF8.GetString(ms.GetBuffer()).TrimEnd('\0');
}
else
{
DataContractSerializer ser = new DataContractSerializer(data.GetType(), GetTestKnownTypes());
MemoryStream ms = new MemoryStream();
ser.WriteObject(ms, data);
ms.Flush();
ms.Seek(0, 0);
serializedChangeSet = Encoding.UTF8.GetString(ms.GetBuffer()).TrimEnd('\0');
}
HttpRequestMessage request = TestHelpers.CreateTestMessage(url, HttpMethod.Post, config);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType));
request.Content = new StringContent(serializedChangeSet, Encoding.UTF8, mediaType);
return invoker.SendAsync(request, CancellationToken.None).Result;
}
/// <summary>
/// For the given Submit response, serialize and deserialize the content. This forces the
/// formatter pipeline to run so we can verify that registered serializers are being used
/// properly.
/// </summary>
private ChangeSetEntry[] GetChangesetResponse(HttpResponseMessage responseMessage)
{
// serialize the content to a stream
ObjectContent content = (ObjectContent)responseMessage.Content;
MemoryStream ms = new MemoryStream();
content.CopyToAsync(ms).Wait();
ms.Flush();
ms.Seek(0, 0);
// deserialize based on content type
ChangeSetEntry[] changeSet = null;
string mediaType = responseMessage.RequestMessage.Content.Headers.ContentType.MediaType;
if (mediaType == "application/json")
{
JsonSerializer ser = new JsonSerializer() { PreserveReferencesHandling = PreserveReferencesHandling.Objects, TypeNameHandling = TypeNameHandling.All };
changeSet = (ChangeSetEntry[])ser.Deserialize(new JsonTextReader(new StreamReader(ms)), content.ObjectType);
}
else
{
DataContractSerializer ser = new DataContractSerializer(content.ObjectType, GetTestKnownTypes());
changeSet = (ChangeSetEntry[])ser.ReadObject(ms);
}
return changeSet;
}
private IEnumerable<Type> GetTestKnownTypes()
{
List<Type> knownTypes = new List<Type>(new Type[] { typeof(Order), typeof(Product), typeof(Order_Detail) });
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) });
return knownTypes;
}
}
/// <summary>
/// Test controller used for multi-level authorization testing
/// </summary>
[TestAuth(Level = "Class")]
public class TestAuthController : DataController
{
[TestAuth(Level = "UserMethod")]
public void UpdateProduct(Product product)
{
}
[TestAuth(Level = "SubmitMethod")]
public override bool Submit(ChangeSet changeSet)
{
return base.Submit(changeSet);
}
protected override void Initialize(HttpControllerContext controllerContext)
{
controllerContext.Configuration.Filters.Add(new TestAuthAttribute() { Level = "Global" });
base.Initialize(controllerContext);
}
}
/// <summary>
/// Test authorization attribute used to verify authorization behavior.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class TestAuthAttribute : AuthorizationFilterAttribute
{
public string Level;
public static string FailLevel;
public static List<string> Log = new List<string>();
public override void OnAuthorization(HttpActionContext context)
{
TestAuthAttribute.Log.Add(Level);
if (FailLevel != null && FailLevel == Level)
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.ReasonPhrase = "Not Authorized";
context.Response = response;
}
base.OnAuthorization(context);
}
public static void Reset()
{
FailLevel = null;
Log.Clear();
}
}
}