Adding additional functional tests for ModelBinding

This commit is contained in:
Pranav K 2014-12-11 10:09:42 -08:00
parent e078076408
commit ce8d840cc6
15 changed files with 589 additions and 8 deletions

View File

@ -7,11 +7,14 @@ using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using ModelBindingWebSite;
using ModelBindingWebSite.ViewModels;
using Newtonsoft.Json;
using Xunit;
@ -1083,5 +1086,244 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Should Update all included properties.
Assert.Equal("March", user.RegisterationMonth);
}
[Fact]
public async Task UpdateVehicle_WithJson_ProducesModelStateErrors()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var content = new
{
Year = 3012,
InspectedDates = new[]
{
new DateTime(4065, 10, 10)
},
Make = "Volttrax",
Model = "Epsum"
};
// Act
var response = await client.PutAsJsonAsync("http://localhost/api/vehicles/520", content);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var modelStateErrors = JsonConvert.DeserializeObject<IDictionary<string, IEnumerable<string>>>(body);
Assert.Equal(3, modelStateErrors.Count);
Assert.Equal(new[] {
"The field Year must be between 1980 and 2034.",
"Year is invalid"
}, modelStateErrors["model.Year"]);
var vinError = Assert.Single(modelStateErrors["model.Vin"]);
Assert.Equal("The Vin field is required.", vinError);
var trackingIdError = Assert.Single(modelStateErrors["X-TrackingId"]);
Assert.Equal("A value is required but was not present in the request.", trackingIdError);
}
[Fact]
public async Task UpdateVehicle_WithJson_DoesPropertyValidationPriorToValidationAtType()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var content = new
{
Year = 2007,
InspectedDates = new[]
{
new DateTime(4065, 10, 10)
},
Make = "Volttrax",
Model = "Epsum",
Vin = "Pqrs"
};
client.DefaultRequestHeaders.TryAddWithoutValidation("X-TrackingId", "trackingid");
// Act
var response = await client.PutAsJsonAsync("http://localhost/api/vehicles/520", content);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var modelStateErrors = JsonConvert.DeserializeObject<IDictionary<string, IEnumerable<string>>>(body);
var item = Assert.Single(modelStateErrors);
Assert.Equal("model.InspectedDates", item.Key);
var error = Assert.Single(item.Value);
Assert.Equal("Inspection date cannot be later than year of manufacture.", error);
}
[Fact]
public async Task UpdateVehicle_WithJson_BindsBodyAndServices()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var trackingId = Guid.NewGuid().ToString();
var postedContent = new
{
Year = 2010,
InspectedDates = new List<DateTime>
{
new DateTime(2008, 10, 01),
new DateTime(2009, 03, 01),
},
Make = "Volttrax",
Model = "Epsum",
Vin = "PQRS"
};
client.DefaultRequestHeaders.TryAddWithoutValidation("X-TrackingId", trackingId);
// Act
var response = await client.PutAsJsonAsync("http://localhost/api/vehicles/520", postedContent);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var actual = JsonConvert.DeserializeObject<VehicleViewModel>(body);
Assert.Equal(postedContent.Vin, actual.Vin);
Assert.Equal(postedContent.Make, actual.Make);
Assert.Equal(postedContent.InspectedDates, actual.InspectedDates.Select(d => d.DateTime));
Assert.Equal(trackingId, actual.LastUpdatedTrackingId);
}
[Fact]
public async Task UpdateVehicle_WithXml_BindsBodyServicesAndHeaders()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var trackingId = Guid.NewGuid().ToString();
var postedContent = new VehicleViewModel
{
Year = 2010,
InspectedDates = new DateTimeOffset[]
{
new DateTimeOffset(2008, 10, 01, 8, 3, 1, TimeSpan.Zero),
new DateTime(2009, 03, 01),
},
Make = "Volttrax",
Model = "Epsum",
Vin = "PQRS"
};
client.DefaultRequestHeaders.TryAddWithoutValidation("X-TrackingId", trackingId);
// Act
var response = await client.PutAsXmlAsync("http://localhost/api/vehicles/520", postedContent);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var actual = JsonConvert.DeserializeObject<VehicleViewModel>(body);
Assert.Equal(postedContent.Vin, actual.Vin);
Assert.Equal(postedContent.Make, actual.Make);
Assert.Equal(postedContent.InspectedDates, actual.InspectedDates);
Assert.Equal(trackingId, actual.LastUpdatedTrackingId);
}
// Simulates a browser based client that does a Ajax post for partial page updates.
[Fact]
public async Task UpdateDealerVehicle_PopulatesPropertyErrorsInViews()
{
// Arrange
var expectedContent = await GetType().GetTypeInfo().Assembly.ReadResourceAsStringAsync(
"compiler/resources/UpdateDealerVehicle_PopulatesPropertyErrorsInViews.txt");
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var postedContent = new
{
Year = 9001,
InspectedDates = new List<DateTime>
{
new DateTime(2008, 01, 01)
},
Make = "Acme",
Model = "Epsum",
Vin = "LongerThan8Chars",
};
var url = "http://localhost/dealers/32/update-vehicle?dealer.name=TestCarDealer&dealer.location=SE";
// Act
var response = await client.PostAsJsonAsync(url, postedContent);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Equal(expectedContent, body);
}
[Fact]
public async Task UpdateDealerVehicle_PopulatesValidationSummary()
{
// Arrange
var expectedContent = await GetType().GetTypeInfo().Assembly.ReadResourceAsStringAsync(
"compiler/resources/UpdateDealerVehicle_PopulatesValidationSummary.txt");
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var postedContent = new
{
Year = 2013,
InspectedDates = new List<DateTime>
{
new DateTime(2008, 01, 01)
},
Make = "Acme",
Model = "Epsum",
Vin = "8chars",
};
var url = "http://localhost/dealers/43/update-vehicle?dealer.name=TestCarDealer&dealer.location=SE";
// Act
var response = await client.PostAsJsonAsync(url, postedContent);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Equal(expectedContent, body);
}
[Fact]
public async Task UpdateDealerVehicle_UsesDefaultValuesForOptionalProperties()
{
// Arrange
var expectedContent = await GetType().GetTypeInfo().Assembly.ReadResourceAsStringAsync(
"compiler/resources/UpdateDealerVehicle_UpdateSuccessful.txt");
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var postedContent = new
{
Year = 2013,
InspectedDates = new DateTimeOffset[]
{
new DateTime(2008, 11, 01)
},
Make = "RealSlowCars",
Model = "Epsum",
Vin = "8chars",
};
var url = "http://localhost/dealers/43/update-vehicle?dealer.name=TestCarDealer&dealer.location=NE";
// Act
var response = await client.PostAsJsonAsync(url, postedContent);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Equal(expectedContent, body);
}
}
}

View File

@ -0,0 +1,16 @@
<div>
<span class="bold">TestCarDealer</span>
<em>SE</em>
<input id="Dealer_Id" name="Dealer.Id" type="hidden" value="32" />
</div>
<div class="validation-summary-errors"><ul><li style="display:none"></li>
</ul></div>
<form action="/dealers/32/update-vehicle?dealer.name=TestCarDealer&amp;dealer.location=SE" method="post"> <fieldset>
<input class="input-validation-error" data-val="true" data-val-length="The field Vin must be a string with a maximum length of 8." data-val-length-max="8" data-val-required="The Vin field is required." id="Vehicle_Vin" name="Vehicle.Vin" type="text" value="LongerThan8Chars" />
<span class="field-validation-error" data-valmsg-for="Vehicle.Vin" data-valmsg-replace="true">The field Vin must be a string with a maximum length of 8.</span>
</fieldset>
<fieldset>
<input class="input-validation-error text-box single-line" data-val="true" data-val-range="The field Year must be between 1980 and 2034." data-val-range-max="2034" data-val-range-min="1980" id="Vehicle_Year" name="Vehicle.Year" type="number" value="9001" />
<span class="field-validation-error" data-valmsg-for="Vehicle.Year" data-valmsg-replace="true">The field Year must be between 1980 and 2034.</span>
</fieldset>
</form>

View File

@ -0,0 +1,16 @@
<div>
<span class="bold">TestCarDealer</span>
<em>SE</em>
<input id="Dealer_Id" name="Dealer.Id" type="hidden" value="43" />
</div>
<div class="validation-summary-errors"><ul><li>Make is invalid for region.</li>
</ul></div>
<form action="/dealers/43/update-vehicle?dealer.name=TestCarDealer&amp;dealer.location=SE" method="post"> <fieldset>
<input data-val="true" data-val-length="The field Vin must be a string with a maximum length of 8." data-val-length-max="8" data-val-required="The Vin field is required." id="Vehicle_Vin" name="Vehicle.Vin" type="text" value="8chars" />
<span class="field-validation-valid" data-valmsg-for="Vehicle.Vin" data-valmsg-replace="true"></span>
</fieldset>
<fieldset>
<input class="text-box single-line" data-val="true" data-val-range="The field Year must be between 1980 and 2034." data-val-range-max="2034" data-val-range-min="1980" id="Vehicle_Year" name="Vehicle.Year" type="number" value="2013" />
<span class="field-validation-valid" data-valmsg-for="Vehicle.Year" data-valmsg-replace="true"></span>
</fieldset>
</form>

View File

@ -0,0 +1,24 @@
<div class="left">
<ul>
<li>
Vin: 8chars
</li>
<li>
Inspected Dates: 11/1/2008 12:00:00 AM -07:00
</li>
</ul>
</div>
<div>
<ul>
<li>
Dealer: 43
</li>
<li>
Phone: 999-99-0000
</li>
</ul>
</div>
<footer>
Tracked by
</footer>

View File

@ -0,0 +1,54 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Net;
using Microsoft.AspNet.Mvc;
using ModelBindingWebSite.Services;
using ModelBindingWebSite.ViewModels;
namespace ModelBindingWebSite
{
public class VehicleController : Controller
{
[HttpPut("/api/vehicles/{id}")]
[Produces("application/json")]
public object UpdateVehicleApi(
[Range(1, 500)] int id,
[FromBody] VehicleViewModel model,
[FromServices] IVehicleService service,
[FromHeader(Name = "X-TrackingId")] string trackingId)
{
if (!ModelState.IsValid)
{
return SerializeModelState();
}
service.Update(id, model, trackingId);
return model;
}
[HttpPost("/dealers/{dealer.id:int}/update-vehicle")]
public IActionResult UpdateDealerVehicle(VehicleWithDealerViewModel model)
{
if (!ModelState.IsValid)
{
return PartialView("UpdateVehicle", model);
}
model.Update();
return PartialView("UpdateSuccessful", model);
}
public IDictionary<string, IEnumerable<string>> SerializeModelState()
{
Response.StatusCode = (int)HttpStatusCode.BadRequest;
return ModelState.Where(item => item.Value.Errors.Count > 0)
.ToDictionary(item => item.Key, item => item.Value.Errors.Select(e => e.ErrorMessage));
}
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using ModelBindingWebSite.ViewModels;
namespace ModelBindingWebSite.Services
{
public interface ILocationService
{
bool IsValidMakeForRegion(string make, string region);
bool Update(VehicleWithDealerViewModel model);
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using ModelBindingWebSite.ViewModels;
namespace ModelBindingWebSite.Services
{
public interface IVehicleService
{
void Update(int id, VehicleViewModel vehicle, string trackingId);
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using ModelBindingWebSite.ViewModels;
namespace ModelBindingWebSite.Services
{
public class LocationService : ILocationService
{
public bool Update(VehicleWithDealerViewModel viewModel)
{
return true;
}
public bool IsValidMakeForRegion(string make, string region)
{
switch (make)
{
case "Acme":
return region == "NW" || "region" == "South Central";
case "FastCars":
return region != "Central";
}
return true;
}
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using ModelBindingWebSite.ViewModels;
namespace ModelBindingWebSite.Services
{
public class VehicleService : IVehicleService
{
public void Update(int id, VehicleViewModel vehicle, string trackingId)
{
if (trackingId == null)
{
throw new ArgumentException(nameof(trackingId));
}
vehicle.LastUpdatedTrackingId = trackingId;
}
}
}

View File

@ -3,9 +3,8 @@
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;
using ModelBindingWebSite.Services;
namespace ModelBindingWebSite
{
@ -28,17 +27,15 @@ namespace ModelBindingWebSite
services.AddSingleton<ICalculator, DefaultCalculator>();
services.AddSingleton<ITestService, TestService>();
services.AddTransient<IVehicleService, VehicleService>();
services.AddTransient<ILocationService, LocationService>();
});
app.UseErrorReporter();
// Add MVC to the request pipeline
app.UseMvc(routes =>
{
routes.MapRoute("ActionAsMethod", "{controller}/{action}",
defaults: new { controller = "Home", action = "Index" });
});
app.UseMvc();
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.ComponentModel.DataAnnotations;
namespace ModelBindingWebSite.ViewModels
{
public class DealerViewModel
{
private const string DefaultCustomerServiceNumber = "999-99-0000";
public int Id { get; set; }
public string Name { get; set; }
[Required]
public string Location { get; set; }
[DataType(DataType.PhoneNumber)]
public string Phone { get; set; } = DefaultCustomerServiceNumber;
}
}

View File

@ -0,0 +1,50 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace ModelBindingWebSite.ViewModels
{
public class VehicleViewModel : IValidatableObject
{
[Required]
[StringLength(8)]
public string Vin { get; set; }
public string Make { get; set; }
public string Model { get; set; }
[Range(1980, 2034)]
[CustomValidation(typeof(VehicleViewModel), nameof(ValidateYear))]
public int Year { get; set; }
[Required]
[MaxLength(10)]
public DateTimeOffset[] InspectedDates { get; set; }
public string LastUpdatedTrackingId { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (InspectedDates.Any(d => d.Year > Year))
{
yield return new ValidationResult("Inspection date cannot be later than year of manufacture.",
new[] { nameof(InspectedDates) });
}
}
public static ValidationResult ValidateYear(int year)
{
if (year > DateTime.UtcNow.Year)
{
return new ValidationResult("Year is invalid");
}
return ValidationResult.Success;
}
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc;
using ModelBindingWebSite.Services;
namespace ModelBindingWebSite.ViewModels
{
public class VehicleWithDealerViewModel : IValidatableObject
{
[Required]
public DealerViewModel Dealer { get; set; }
[Required]
[FromBody]
public VehicleViewModel Vehicle { get; set; }
[FromServices]
public ILocationService LocationService { get; set; }
[FromHeader(Name = "X-TrackingId")]
public string TrackingId { get; set; } = "default-tracking-id";
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!LocationService.IsValidMakeForRegion(Vehicle.Make, Dealer.Location))
{
yield return new ValidationResult("Make is invalid for region.");
}
}
public void Update()
{
LocationService.Update(this);
}
}
}

View File

@ -0,0 +1,26 @@
@model ModelBindingWebSite.ViewModels.VehicleWithDealerViewModel
<div class="left">
<ul>
<li>
Vin: @Html.DisplayFor(m => m.Vehicle.Vin)
</li>
<li>
Inspected Dates: @Html.DisplayFor(m => m.Vehicle.InspectedDates)
</li>
</ul>
</div>
<div>
<ul>
<li>
Dealer: @Html.DisplayFor(m => m.Dealer.Id)
</li>
<li>
Phone: @Html.DisplayFor(m => m.Dealer.Phone)
</li>
</ul>
</div>
<footer>
Tracked by @Model.TrackingId
</footer>

View File

@ -0,0 +1,20 @@
@model ModelBindingWebSite.ViewModels.VehicleWithDealerViewModel
<div>
<span class="bold">@Model.Dealer.Name</span>
<em>@Model.Dealer.Location</em>
@Html.HiddenFor(m => m.Dealer.Id)
</div>
@Html.ValidationSummary(excludePropertyErrors: true)
@using (Html.BeginForm())
{
<fieldset>
@Html.TextBoxFor(m => m.Vehicle.Vin)
@Html.ValidationMessageFor(m => m.Vehicle.Vin)
</fieldset>
<fieldset>
@Html.EditorFor(m => m.Vehicle.Year)
@Html.ValidationMessageFor(m => m.Vehicle.Year)
</fieldset>
}