From 3b1339787f63eec25e00985cfce76b1a87c5eccb Mon Sep 17 00:00:00 2001 From: yigith Date: Thu, 25 Jun 2020 03:48:23 +0300 Subject: [PATCH] 401 fix (#408) * transfer basket on login * review page * unit tests for TransferBasketAsync --- src/ApplicationCore/Services/BasketService.cs | 21 +++-- .../Identity/Pages/Account/Login.cshtml.cs | 16 +++- src/Web/Pages/Basket/Checkout.cshtml | 80 ++++++++++++++++-- src/Web/Pages/Basket/Checkout.cshtml.cs | 14 ++-- src/Web/Pages/Basket/Index.cshtml | 45 ++++------ src/Web/Pages/Basket/Success.cshtml | 17 ++++ src/Web/Pages/Basket/Success.cshtml.cs | 19 +++++ src/Web/Web.csproj | 2 +- .../wwwroot/css/basket/basket.component.css | 2 + .../css/basket/basket.component.min.css | 2 +- .../wwwroot/css/basket/basket.component.scss | 4 + .../BasketServiceTests/TransferBasket.cs | 83 ++++++++++++++++++- 12 files changed, 254 insertions(+), 51 deletions(-) create mode 100644 src/Web/Pages/Basket/Success.cshtml create mode 100644 src/Web/Pages/Basket/Success.cshtml.cs diff --git a/src/ApplicationCore/Services/BasketService.cs b/src/ApplicationCore/Services/BasketService.cs index a4dcc51..84bc01e 100644 --- a/src/ApplicationCore/Services/BasketService.cs +++ b/src/ApplicationCore/Services/BasketService.cs @@ -59,11 +59,22 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services { Guard.Against.NullOrEmpty(anonymousId, nameof(anonymousId)); Guard.Against.NullOrEmpty(userName, nameof(userName)); - var basketSpec = new BasketWithItemsSpecification(anonymousId); - var basket = (await _basketRepository.FirstOrDefaultAsync(basketSpec)); - if (basket == null) return; - basket.SetNewBuyerId(userName); - await _basketRepository.UpdateAsync(basket); + var anonymousBasketSpec = new BasketWithItemsSpecification(anonymousId); + var anonymousBasket = await _basketRepository.FirstOrDefaultAsync(anonymousBasketSpec); + if (anonymousBasket == null) return; + var userBasketSpec = new BasketWithItemsSpecification(userName); + var userBasket = await _basketRepository.FirstOrDefaultAsync(userBasketSpec); + if (userBasket == null) + { + userBasket = new Basket(userName); + await _basketRepository.AddAsync(userBasket); + } + foreach (var item in anonymousBasket.Items) + { + userBasket.AddItem(item.CatalogItemId, item.UnitPrice, item.Quantity); + } + await _basketRepository.UpdateAsync(userBasket); + await _basketRepository.DeleteAsync(anonymousBasket); } } } diff --git a/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs b/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs index 72ecf94..33cf482 100644 --- a/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs +++ b/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.Extensions.Logging; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account { @@ -18,11 +19,13 @@ namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account { private readonly SignInManager _signInManager; private readonly ILogger _logger; + private readonly IBasketService _basketService; - public LoginModel(SignInManager signInManager, ILogger logger) + public LoginModel(SignInManager signInManager, ILogger logger, IBasketService basketService) { _signInManager = signInManager; _logger = logger; + _basketService = basketService; } [BindProperty] @@ -78,6 +81,7 @@ namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account if (result.Succeeded) { _logger.LogInformation("User logged in."); + await TransferAnonymousBasketToUserAsync(Input.Email); return LocalRedirect(returnUrl); } if (result.RequiresTwoFactor) @@ -99,5 +103,15 @@ namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account // If we got this far, something failed, redisplay form return Page(); } + + private async Task TransferAnonymousBasketToUserAsync(string userName) + { + if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME)) + { + var anonymousId = Request.Cookies[Constants.BASKET_COOKIENAME]; + await _basketService.TransferBasketAsync(anonymousId, userName); + Response.Cookies.Delete(Constants.BASKET_COOKIENAME); + } + } } } diff --git a/src/Web/Pages/Basket/Checkout.cshtml b/src/Web/Pages/Basket/Checkout.cshtml index 01e0a78..aa483ab 100644 --- a/src/Web/Pages/Basket/Checkout.cshtml +++ b/src/Web/Pages/Basket/Checkout.cshtml @@ -1,7 +1,7 @@ @page - @model CheckoutModel +@model CheckoutModel @{ - ViewData["Title"] = "Checkout Complete"; + ViewData["Title"] = "Checkout"; }
@@ -10,7 +10,77 @@
-

Thanks for your Order!

+

Review

+ @if (Model.BasketModel.Items.Any()) + { +
+
+
+
Product
+
+
Price
+
Quantity
+
Cost
+
+
+
+ @for (int i = 0; i < Model.BasketModel.Items.Count; i++) + { + var item = Model.BasketModel.Items[i]; +
+
+
+ +
+
@item.ProductName
+
$ @item.UnitPrice.ToString("N2")
+
+ + + @item.Quantity +
+
$ @Math.Round(item.Quantity * item.UnitPrice, 2).ToString("N2")
+
+ +
+ + } + +
+
+
+
Total
+
+ +
+
+
$ @Model.BasketModel.Total().ToString("N2")
+
+ +
+
+
+
+
+
+ [ Back ] +
+
+ +
+
+
+ + } + else + { +

+ Basket is empty. +

+ +
+ [ Continue Shopping..] +
+ } + \ No newline at end of file diff --git a/src/Web/Pages/Basket/Checkout.cshtml.cs b/src/Web/Pages/Basket/Checkout.cshtml.cs index 6b973ed..19bab7f 100644 --- a/src/Web/Pages/Basket/Checkout.cshtml.cs +++ b/src/Web/Pages/Basket/Checkout.cshtml.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; @@ -14,6 +15,7 @@ using System.Threading.Tasks; namespace Microsoft.eShopWeb.Web.Pages.Basket { + [Authorize] public class CheckoutModel : PageModel { private readonly IBasketService _basketService; @@ -40,13 +42,7 @@ namespace Microsoft.eShopWeb.Web.Pages.Basket public async Task OnGet() { - if (HttpContext.Request.Query.ContainsKey(Constants.BASKET_ID)) - { - var basketId = int.Parse(HttpContext.Request.Query[Constants.BASKET_ID]); - await _basketService.TransferBasketAsync(Request.Cookies[Constants.BASKET_COOKIENAME], User.Identity.Name); - await _orderService.CreateOrderAsync(basketId, new Address("123 Main St.", "Kent", "OH", "United States", "44240")); - await _basketService.DeleteBasketAsync(basketId); - } + await SetBasketModelAsync(); } public async Task OnPost(IEnumerable items) @@ -72,7 +68,7 @@ namespace Microsoft.eShopWeb.Web.Pages.Basket return RedirectToPage("/Basket/Index"); } - return RedirectToPage(); + return RedirectToPage("Success"); } private async Task SetBasketModelAsync() diff --git a/src/Web/Pages/Basket/Index.cshtml b/src/Web/Pages/Basket/Index.cshtml index 2af8179..cf05540 100644 --- a/src/Web/Pages/Basket/Index.cshtml +++ b/src/Web/Pages/Basket/Index.cshtml @@ -27,23 +27,23 @@ @for (int i = 0; i < Model.BasketModel.Items.Count; i++) { var item = Model.BasketModel.Items[i]; -
-
-
- -
-
@item.ProductName
-
$ @item.UnitPrice.ToString("N2")
-
- - -
-
$ @Math.Round(item.Quantity * item.UnitPrice, 2).ToString("N2")
-
-
+
+
+
+ +
+
@item.ProductName
+
$ @item.UnitPrice.ToString("N2")
+
+ + +
+
$ @Math.Round(item.Quantity * item.UnitPrice, 2).ToString("N2")
+
+
-
-
+
+
}
@@ -71,16 +71,7 @@ asp-page-handler="Update"> [ Update ] - @{ - var data = new Dictionary - { - { Constants.BASKET_ID, Model.BasketModel.Id.ToString() }, - }; - } - + [ Checkout ]
@@ -91,7 +82,7 @@

Basket is empty.

- +
[ Continue Shopping..]
diff --git a/src/Web/Pages/Basket/Success.cshtml b/src/Web/Pages/Basket/Success.cshtml new file mode 100644 index 0000000..7f24dca --- /dev/null +++ b/src/Web/Pages/Basket/Success.cshtml @@ -0,0 +1,17 @@ +@page +@model SuccessModel +@{ + ViewData["Title"] = "Checkout Complete"; +} + +
+
+ +
+
+ +
+

Thanks for your Order!

+ + Continue Shopping... +
\ No newline at end of file diff --git a/src/Web/Pages/Basket/Success.cshtml.cs b/src/Web/Pages/Basket/Success.cshtml.cs new file mode 100644 index 0000000..3f1882f --- /dev/null +++ b/src/Web/Pages/Basket/Success.cshtml.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Microsoft.eShopWeb.Web.Pages.Basket +{ + [Authorize] + public class SuccessModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index 42fe238..f3ca000 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/Web/wwwroot/css/basket/basket.component.css b/src/Web/wwwroot/css/basket/basket.component.css index 6007b93..0287db7 100644 --- a/src/Web/wwwroot/css/basket/basket.component.css +++ b/src/Web/wwwroot/css/basket/basket.component.css @@ -46,3 +46,5 @@ .esh-basket-checkout:hover { background-color: #4a760f; transition: all 0.35s; } + .esh-basket-checkout:visited { + color: #FFFFFF; } diff --git a/src/Web/wwwroot/css/basket/basket.component.min.css b/src/Web/wwwroot/css/basket/basket.component.min.css index ddc57ea..5c5eea6 100644 --- a/src/Web/wwwroot/css/basket/basket.component.min.css +++ b/src/Web/wwwroot/css/basket/basket.component.min.css @@ -1 +1 @@ -.esh-basket{min-height:80vh;}.esh-basket-titles{padding-bottom:1rem;padding-top:2rem;}.esh-basket-titles--clean{padding-bottom:0;padding-top:0;}.esh-basket-title{text-transform:uppercase;}.esh-basket-items--border{border-bottom:1px solid #eee;padding:.5rem 0;}.esh-basket-items--border:last-of-type{border-color:transparent;}.esh-basket-items-margin-left1{margin-left:1px;}.esh-basket-item{font-size:1rem;font-weight:300;}.esh-basket-item--middle{line-height:8rem;}@media screen and (max-width:1024px){.esh-basket-item--middle{line-height:1rem;}}.esh-basket-item--mark{color:#00a69c;}.esh-basket-image{height:8rem;}.esh-basket-input{line-height:1rem;width:100%;}.esh-basket-checkout{background-color:#83d01b;border:0;border-radius:0;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s;}.esh-basket-checkout:hover{background-color:#4a760f;transition:all .35s;} \ No newline at end of file +.esh-basket{min-height:80vh;}.esh-basket-titles{padding-bottom:1rem;padding-top:2rem;}.esh-basket-titles--clean{padding-bottom:0;padding-top:0;}.esh-basket-title{text-transform:uppercase;}.esh-basket-items--border{border-bottom:1px solid #eee;padding:.5rem 0;}.esh-basket-items--border:last-of-type{border-color:transparent;}.esh-basket-items-margin-left1{margin-left:1px;}.esh-basket-item{font-size:1rem;font-weight:300;}.esh-basket-item--middle{line-height:8rem;}@media screen and (max-width:1024px){.esh-basket-item--middle{line-height:1rem;}}.esh-basket-item--mark{color:#00a69c;}.esh-basket-image{height:8rem;}.esh-basket-input{line-height:1rem;width:100%;}.esh-basket-checkout{background-color:#83d01b;border:0;border-radius:0;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s;}.esh-basket-checkout:hover{background-color:#4a760f;transition:all .35s;}.esh-basket-checkout:visited{color:#fff;} \ No newline at end of file diff --git a/src/Web/wwwroot/css/basket/basket.component.scss b/src/Web/wwwroot/css/basket/basket.component.scss index f726620..e03828e 100644 --- a/src/Web/wwwroot/css/basket/basket.component.scss +++ b/src/Web/wwwroot/css/basket/basket.component.scss @@ -82,6 +82,10 @@ background-color: $color-secondary-darker; transition: all $animation-speed-default; } + + &:visited { + color: $color-foreground-brighter; + } } } diff --git a/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/TransferBasket.cs b/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/TransferBasket.cs index 6931474..deee7c0 100644 --- a/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/TransferBasket.cs +++ b/tests/UnitTests/ApplicationCore/Services/BasketServiceTests/TransferBasket.cs @@ -1,5 +1,12 @@ -using Microsoft.eShopWeb.ApplicationCore.Services; +using Ardalis.Specification; +using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.ApplicationCore.Services; +using Microsoft.eShopWeb.ApplicationCore.Specifications; +using Moq; using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Xunit; @@ -7,6 +14,17 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTes { public class TransferBasket { + private readonly string _nonexistentAnonymousBasketBuyerId = "nonexistent-anonymous-basket-buyer-id"; + private readonly string _existentAnonymousBasketBuyerId = "existent-anonymous-basket-buyer-id"; + private readonly string _nonexistentUserBasketBuyerId = "newuser@microsoft.com"; + private readonly string _existentUserBasketBuyerId = "testuser@microsoft.com"; + private readonly Mock> _mockBasketRepo; + + public TransferBasket() + { + _mockBasketRepo = new Mock>(); + } + [Fact] public async Task ThrowsGivenNullAnonymousId() { @@ -21,6 +39,67 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTes var basketService = new BasketService(null, null); await Assert.ThrowsAsync(async () => await basketService.TransferBasketAsync("abcdefg", null)); - } + } + + [Fact] + public async Task InvokesBasketRepositoryFirstOrDefaultAsyncOnceIfAnonymousBasketNotExists() + { + var anonymousBasket = null as Basket; + var userBasket = new Basket(_existentUserBasketBuyerId); + _mockBasketRepo.SetupSequence(x => x.FirstOrDefaultAsync(It.IsAny())) + .ReturnsAsync(anonymousBasket) + .ReturnsAsync(userBasket); + var basketService = new BasketService(_mockBasketRepo.Object, null); + await basketService.TransferBasketAsync(_nonexistentAnonymousBasketBuyerId, _existentUserBasketBuyerId); + _mockBasketRepo.Verify(x => x.FirstOrDefaultAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task TransferAnonymousBasketItemsWhilePreservingExistingUserBasketItems() + { + var anonymousBasket = new Basket(_existentAnonymousBasketBuyerId); + anonymousBasket.AddItem(1, 10, 1); + anonymousBasket.AddItem(3, 55, 7); + var userBasket = new Basket(_existentUserBasketBuyerId); + userBasket.AddItem(1, 10, 4); + userBasket.AddItem(2, 99, 3); + _mockBasketRepo.SetupSequence(x => x.FirstOrDefaultAsync(It.IsAny())) + .ReturnsAsync(anonymousBasket) + .ReturnsAsync(userBasket); + var basketService = new BasketService(_mockBasketRepo.Object, null); + await basketService.TransferBasketAsync(_nonexistentAnonymousBasketBuyerId, _existentUserBasketBuyerId); + _mockBasketRepo.Verify(x => x.UpdateAsync(userBasket), Times.Once); + Assert.Equal(3, userBasket.Items.Count); + Assert.Contains(userBasket.Items, x => x.CatalogItemId == 1 && x.UnitPrice == 10 && x.Quantity == 5); + Assert.Contains(userBasket.Items, x => x.CatalogItemId == 2 && x.UnitPrice == 99 && x.Quantity == 3); + Assert.Contains(userBasket.Items, x => x.CatalogItemId == 3 && x.UnitPrice == 55 && x.Quantity == 7); + } + + [Fact] + public async Task RemovesAnonymousBasketAfterUpdatingUserBasket() + { + var anonymousBasket = new Basket(_existentAnonymousBasketBuyerId); + var userBasket = new Basket(_existentUserBasketBuyerId); + _mockBasketRepo.SetupSequence(x => x.FirstOrDefaultAsync(It.IsAny())) + .ReturnsAsync(anonymousBasket) + .ReturnsAsync(userBasket); + var basketService = new BasketService(_mockBasketRepo.Object, null); + await basketService.TransferBasketAsync(_nonexistentAnonymousBasketBuyerId, _existentUserBasketBuyerId); + _mockBasketRepo.Verify(x => x.UpdateAsync(userBasket), Times.Once); + _mockBasketRepo.Verify(x => x.DeleteAsync(anonymousBasket), Times.Once); + } + + [Fact] + public async Task CreatesNewUserBasketIfNotExists() + { + var anonymousBasket = new Basket(_existentAnonymousBasketBuyerId); + var userBasket = null as Basket; + _mockBasketRepo.SetupSequence(x => x.FirstOrDefaultAsync(It.IsAny())) + .ReturnsAsync(anonymousBasket) + .ReturnsAsync(userBasket); + var basketService = new BasketService(_mockBasketRepo.Object, null); + await basketService.TransferBasketAsync(_existentAnonymousBasketBuyerId, _nonexistentUserBasketBuyerId); + _mockBasketRepo.Verify(x => x.AddAsync(It.Is(x => x.BuyerId == _nonexistentUserBasketBuyerId)), Times.Once); + } } }