Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
f5fb773395 | |||
1e70fef3a7 | |||
9afd2e2cfd | |||
c0b4f638f9 | |||
57a8f62c14 | |||
610a360fb2 | |||
85f354b5ba | |||
2047c92b96 | |||
911ab9f5c8 | |||
436f78cf71 | |||
f1cdbf375b | |||
35c520e1ed | |||
d33320ce73 | |||
8c22c6e26a | |||
6801b717df | |||
8318883c0a | |||
de9d07f75f | |||
6e6ae67b21 |
@ -10,5 +10,6 @@ namespace SportsStore.Domain.Abstract
|
||||
public interface IProductRepository
|
||||
{
|
||||
IEnumerable<Product> Products { get; }
|
||||
void SaveProduct(Product product);
|
||||
}
|
||||
}
|
||||
|
@ -12,5 +12,25 @@ namespace SportsStore.Domain.Concrete
|
||||
{
|
||||
get { return context.Products; }
|
||||
}
|
||||
|
||||
public void SaveProduct(Product product)
|
||||
{
|
||||
if (product.ProductID == 0)
|
||||
{
|
||||
context.Products.Add(product);
|
||||
}
|
||||
else
|
||||
{
|
||||
Product dbEntry = context.Products.Find(product.ProductID);
|
||||
if (dbEntry != null)
|
||||
{
|
||||
dbEntry.Name = product.Name;
|
||||
dbEntry.Description = product.Description;
|
||||
dbEntry.Price = product.Price;
|
||||
dbEntry.Category = product.Category;
|
||||
}
|
||||
}
|
||||
context.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web.Mvc;
|
||||
|
||||
namespace SportsStore.Domain.Entities
|
||||
{
|
||||
public class Product
|
||||
{
|
||||
[HiddenInput(DisplayValue = false)]
|
||||
public int ProductID { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Please enter a product name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[DataType(DataType.MultilineText)]
|
||||
[Required(ErrorMessage = "Please enter a description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[Required]
|
||||
[Range(0.01, double.MaxValue, ErrorMessage = "Please enter a positive price")]
|
||||
public decimal Price { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Please specify a category")]
|
||||
public string Category { get; set; }
|
||||
}
|
||||
}
|
||||
|
130
SportsStore/SportsStore.UnitTests/AdminTests.cs
Normal file
130
SportsStore/SportsStore.UnitTests/AdminTests.cs
Normal file
@ -0,0 +1,130 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
using SportsStore.Domain.Abstract;
|
||||
using SportsStore.Domain.Entities;
|
||||
using SportsStore.WebUI.Controllers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Web.Mvc;
|
||||
|
||||
namespace SportsStore.UnitTests
|
||||
{
|
||||
[TestClass]
|
||||
public class AdminTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Index_Contains_All_Products()
|
||||
{
|
||||
// Arrange - create the mock repository
|
||||
Mock<IProductRepository> mock = new Mock<IProductRepository>();
|
||||
mock.Setup(m => m.Products).Returns(new Product[]
|
||||
{
|
||||
new Product {ProductID = 1, Name = "P1"},
|
||||
new Product {ProductID = 2, Name = "P2"},
|
||||
new Product {ProductID = 3, Name = "P3"},
|
||||
});
|
||||
|
||||
// Arrange - create a controller
|
||||
AdminController target = new AdminController(mock.Object);
|
||||
|
||||
// Action
|
||||
Product[] result = ((IEnumerable<Product>)target.Index().ViewData.Model).ToArray();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(result.Length, 3);
|
||||
Assert.AreEqual("P1", result[0].Name);
|
||||
Assert.AreEqual("P2", result[1].Name);
|
||||
Assert.AreEqual("P3", result[2].Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Can_Edit_Product()
|
||||
{
|
||||
// Arrange - create the mock repository
|
||||
Mock<IProductRepository> mock = new Mock<IProductRepository>();
|
||||
mock.Setup(m => m.Products).Returns(new Product[]
|
||||
{
|
||||
new Product {ProductID = 1, Name = "P1"},
|
||||
new Product {ProductID = 2, Name = "P2"},
|
||||
new Product {ProductID = 3, Name = "P3"},
|
||||
});
|
||||
|
||||
// Arrange - create the controller
|
||||
AdminController target = new AdminController(mock.Object);
|
||||
|
||||
// Act
|
||||
Product p1 = target.Edit(1).ViewData.Model as Product;
|
||||
Product p2 = target.Edit(2).ViewData.Model as Product;
|
||||
Product p3 = target.Edit(3).ViewData.Model as Product;
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, p1.ProductID);
|
||||
Assert.AreEqual(2, p2.ProductID);
|
||||
Assert.AreEqual(3, p3.ProductID);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Cannot_Edit_Nonexistent_Product()
|
||||
{
|
||||
// Arrange - create the mock repository
|
||||
Mock<IProductRepository> mock = new Mock<IProductRepository>();
|
||||
mock.Setup(m => m.Products).Returns(new Product[] {
|
||||
new Product {ProductID = 1, Name = "P1"},
|
||||
new Product {ProductID = 2, Name = "P2"},
|
||||
new Product {ProductID = 3, Name = "P3"},
|
||||
});
|
||||
|
||||
// Arrange - create the controller
|
||||
AdminController target = new AdminController(mock.Object);
|
||||
|
||||
// Act
|
||||
Product result = (Product)target.Edit(4).ViewData.Model;
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Can_Save_Valid_Changes()
|
||||
{
|
||||
// Arrange - create mock repository
|
||||
Mock<IProductRepository> mock = new Mock<IProductRepository>();
|
||||
// Arrange - create the controller
|
||||
AdminController target = new AdminController(mock.Object);
|
||||
// Arrange - create a product
|
||||
Product product = new Product { Name = "Test" };
|
||||
|
||||
// Act - try to save the product
|
||||
ActionResult result = target.Edit(product);
|
||||
|
||||
// Assert - check that the repository was called
|
||||
mock.Verify(m => m.SaveProduct(product));
|
||||
|
||||
// Assert - check the method result type
|
||||
Assert.IsNotInstanceOfType(result, typeof(ViewResult));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Cannot_Save_Invalid_Changes()
|
||||
{
|
||||
// Arrange - create mock repository
|
||||
Mock<IProductRepository> mock = new Mock<IProductRepository>();
|
||||
// Arrange - create the controller
|
||||
AdminController target = new AdminController(mock.Object);
|
||||
// Arrange - create a product
|
||||
Product product = new Product { Name = "Test" };
|
||||
// Arrange - add an error to the model state
|
||||
target.ModelState.AddModelError("error", "error");
|
||||
|
||||
// Act - try to save the product
|
||||
ActionResult result = target.Edit(product);
|
||||
|
||||
// Assert - check that the repository was not called
|
||||
mock.Verify(m => m.SaveProduct(It.IsAny<Product>()), Times.Never());
|
||||
// Assert - check the method result type
|
||||
Assert.IsInstanceOfType(result, typeof(ViewResult));
|
||||
}
|
||||
}
|
||||
}
|
@ -87,6 +87,7 @@
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AdminTests.cs" />
|
||||
<Compile Include="App_Start\NinjectWebCommon.cs" />
|
||||
<Compile Include="UnitTest1.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
|
49
SportsStore/SportsStore.WebUI/Controllers/AdminController.cs
Normal file
49
SportsStore/SportsStore.WebUI/Controllers/AdminController.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using SportsStore.Domain.Abstract;
|
||||
using SportsStore.Domain.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
using System.Web.Mvc;
|
||||
|
||||
namespace SportsStore.WebUI.Controllers
|
||||
{
|
||||
public class AdminController : Controller
|
||||
{
|
||||
private IProductRepository repository;
|
||||
|
||||
public AdminController(IProductRepository repo)
|
||||
{
|
||||
repository = repo;
|
||||
}
|
||||
|
||||
// GET: Admin
|
||||
public ViewResult Index()
|
||||
{
|
||||
return View(repository.Products);
|
||||
}
|
||||
|
||||
public ViewResult Edit(int productId)
|
||||
{
|
||||
Product product = repository.Products
|
||||
.FirstOrDefault(p => p.ProductID == productId);
|
||||
return View(product);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult Edit(Product product)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
repository.SaveProduct(product);
|
||||
TempData["message"] = string.Format("{0} has been saved", product.Name);
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
else
|
||||
{
|
||||
// there is something wrong with the data values
|
||||
return View(product);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -131,6 +131,7 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="App_Start\NinjectWebCommon.cs" />
|
||||
<Compile Include="App_Start\RouteConfig.cs" />
|
||||
<Compile Include="Controllers\AdminController.cs" />
|
||||
<Compile Include="Controllers\CartController.cs" />
|
||||
<Compile Include="Controllers\NavController.cs" />
|
||||
<Compile Include="Controllers\ProductController.cs" />
|
||||
@ -158,6 +159,9 @@
|
||||
<Content Include="Views\Cart\Summary.cshtml" />
|
||||
<Content Include="Views\Cart\Checkout.cshtml" />
|
||||
<Content Include="Views\Cart\Completed.cshtml" />
|
||||
<Content Include="Views\Shared\_AdminLayout.cshtml" />
|
||||
<Content Include="Views\Admin\Index.cshtml" />
|
||||
<Content Include="Views\Admin\Edit.cshtml" />
|
||||
<None Include="Web.Debug.config">
|
||||
<DependentUpon>Web.config</DependentUpon>
|
||||
</None>
|
||||
|
46
SportsStore/SportsStore.WebUI/Views/Admin/Edit.cshtml
Normal file
46
SportsStore/SportsStore.WebUI/Views/Admin/Edit.cshtml
Normal file
@ -0,0 +1,46 @@
|
||||
@model SportsStore.Domain.Entities.Product
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Admin: Edit" + Model.Name;
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
}
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-heading">
|
||||
<h3>Edit @Model.Name</h3>
|
||||
</div>
|
||||
|
||||
@using (Html.BeginForm())
|
||||
{
|
||||
<div class="panel-body">
|
||||
@Html.HiddenFor(m => m.ProductID)
|
||||
@foreach (var property in ViewData.ModelMetadata.Properties)
|
||||
{
|
||||
if (property.PropertyName != "ProductID")
|
||||
{
|
||||
<div class="form-group">
|
||||
<label>@(property.DisplayName ?? property.PropertyName)</label>
|
||||
@if (property.PropertyName == "Description")
|
||||
{
|
||||
@Html.TextArea(property.PropertyName, null,
|
||||
new { @class = "form-control", rows = 5 })
|
||||
}
|
||||
else
|
||||
{
|
||||
@Html.TextBox(property.PropertyName, null,
|
||||
new { @class = "form-control" })
|
||||
}
|
||||
@Html.ValidationMessage(property.PropertyName)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="panel-footer">
|
||||
<input type="submit" value="Save" class="btn btn-primary" />
|
||||
@Html.ActionLink("Cancel and return to List", "Index", null,
|
||||
new { @class = "btn btn-default" })
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
47
SportsStore/SportsStore.WebUI/Views/Admin/Index.cshtml
Normal file
47
SportsStore/SportsStore.WebUI/Views/Admin/Index.cshtml
Normal file
@ -0,0 +1,47 @@
|
||||
@model IEnumerable<SportsStore.Domain.Entities.Product>
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Index";
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
}
|
||||
|
||||
<h2>Index</h2>
|
||||
<p>
|
||||
@Html.ActionLink("Create New", "Create")
|
||||
</p>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3>All Products</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-striped table-condensed table-bordered">
|
||||
<tr>
|
||||
<th class="text-right">ID</th>
|
||||
<th>Name</th>
|
||||
<th class="text-right">Price</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-right">@item.ProductID</td>
|
||||
<td>@Html.ActionLink(item.Name, "Edit", new { item.ProductID })</td>
|
||||
<td class="text-right">@item.Price.ToString("c")</td>
|
||||
<td class="text-center">
|
||||
@using (Html.BeginForm("Delete", "Admin"))
|
||||
{
|
||||
@Html.Hidden("ProductID", item.ProductID)
|
||||
<input type="submit"
|
||||
class="btn btn-default btn-xs"
|
||||
value="Delete" />
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
@Html.ActionLink("Add a new product", "Create", null, new { @class = "btn btn-default" })
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,24 @@
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width"/>
|
||||
<link href="~/Content/bootstrap.css" rel="stylesheet" />
|
||||
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
|
||||
<link href="~/Content/ErrorStyles.css" rel="stylesheet" />
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
@if (TempData["message"] != null)
|
||||
{
|
||||
<div class="alert alert-success">@TempData["message"]</div>
|
||||
}
|
||||
@RenderBody()
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user