diff --git a/src/ApplicationCore/Entities/BaseEntity.cs b/src/ApplicationCore/Entities/BaseEntity.cs index b021b65..78dc78f 100644 --- a/src/ApplicationCore/Entities/BaseEntity.cs +++ b/src/ApplicationCore/Entities/BaseEntity.cs @@ -1,5 +1,7 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities { + // This can easily be modified to be BaseEntity and public T Id to support different key types. + // Using non-generic integer types for simplicity and to ease caching logic public class BaseEntity { public int Id { get; set; } diff --git a/src/ApplicationCore/Entities/BasketItem.cs b/src/ApplicationCore/Entities/BasketItem.cs index c5f152c..5233ffd 100644 --- a/src/ApplicationCore/Entities/BasketItem.cs +++ b/src/ApplicationCore/Entities/BasketItem.cs @@ -5,6 +5,5 @@ public decimal UnitPrice { get; set; } public int Quantity { get; set; } public int CatalogItemId { get; set; } -// public CatalogItem Item { get; set; } } } diff --git a/src/ApplicationCore/Exceptions/CatalogImageMissingException.cs b/src/ApplicationCore/Exceptions/CatalogImageMissingException.cs deleted file mode 100644 index a08fbb5..0000000 --- a/src/ApplicationCore/Exceptions/CatalogImageMissingException.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace ApplicationCore.Exceptions -{ - /// - /// Note: No longer required. - /// - public class CatalogImageMissingException : Exception - { - public CatalogImageMissingException(string message, - Exception innerException = null) - : base(message, innerException: innerException) - { - } - public CatalogImageMissingException(Exception innerException) - : base("No catalog image found for the provided id.", - innerException: innerException) - { - } - - public CatalogImageMissingException() : base() - { - } - - public CatalogImageMissingException(string message) : base(message) - { - } - } -} diff --git a/src/ApplicationCore/Services/UriComposer.cs b/src/ApplicationCore/Services/UriComposer.cs index eb4bb3d..14f73f2 100644 --- a/src/ApplicationCore/Services/UriComposer.cs +++ b/src/ApplicationCore/Services/UriComposer.cs @@ -12,7 +12,6 @@ namespace ApplicationCore.Services public string ComposePicUri(string uriTemplate) { return uriTemplate.Replace("http://catalogbaseurltobereplaced", _catalogSettings.CatalogBaseUrl); - } } } diff --git a/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs b/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs new file mode 100644 index 0000000..a0663b9 --- /dev/null +++ b/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Identity; +using System.Threading.Tasks; + +namespace Infrastructure.Identity +{ + public class AppIdentityDbContextSeed + { + public static async Task SeedAsync(UserManager userManager) + { + var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" }; + await userManager.CreateAsync(defaultUser, "Pass@word1"); + } + } +} diff --git a/src/Web/Controllers/Api/BaseApiController.cs b/src/Web/Controllers/Api/BaseApiController.cs new file mode 100644 index 0000000..a268500 --- /dev/null +++ b/src/Web/Controllers/Api/BaseApiController.cs @@ -0,0 +1,10 @@ +using Microsoft.eShopWeb.Services; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Controllers.Api +{ + [Route("api/[controller]/[action]")] + public class BaseApiController : Controller + { } +} diff --git a/src/Web/Controllers/Api/CatalogController.cs b/src/Web/Controllers/Api/CatalogController.cs new file mode 100644 index 0000000..849e576 --- /dev/null +++ b/src/Web/Controllers/Api/CatalogController.cs @@ -0,0 +1,21 @@ +using Microsoft.eShopWeb.Services; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Controllers.Api +{ + public class CatalogController : BaseApiController + { + private readonly ICatalogService _catalogService; + + public CatalogController(ICatalogService catalogService) => _catalogService = catalogService; + + [HttpGet] + public async Task List(int? brandFilterApplied, int? typesFilterApplied, int? page) + { + var itemsPage = 10; + var catalogModel = await _catalogService.GetCatalogItems(page ?? 0, itemsPage, brandFilterApplied, typesFilterApplied); + return Ok(catalogModel); + } + } +} diff --git a/src/Web/Program.cs b/src/Web/Program.cs index 9810832..beabb52 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -18,25 +18,20 @@ namespace Microsoft.eShopWeb using (var scope = host.Services.CreateScope()) { var services = scope.ServiceProvider; - + var loggerFactory = services.GetRequiredService(); try { var catalogContext = services.GetRequiredService(); - var loggerFactory = services.GetRequiredService(); CatalogContextSeed.SeedAsync(catalogContext, loggerFactory) .Wait(); - // move to IdentitySeed method var userManager = services.GetRequiredService>(); - var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" }; - userManager.CreateAsync(defaultUser, "Pass@word1").Wait(); - + AppIdentityDbContextSeed.SeedAsync(userManager).Wait(); } catch (Exception ex) { - Console.WriteLine(ex.ToString()); - //var logger = services.GetRequiredService>(); - //logger.LogError(ex, "An error occurred seeding the DB."); + var logger = loggerFactory.CreateLogger(); + logger.LogError(ex, "An error occurred seeding the DB."); } } diff --git a/src/Web/Startup.cs b/src/Web/Startup.cs index 08363df..72fd549 100644 --- a/src/Web/Startup.cs +++ b/src/Web/Startup.cs @@ -30,29 +30,54 @@ namespace Microsoft.eShopWeb public IConfiguration Configuration { get; } - public void ConfigureServices(IServiceCollection services) + public void ConfigureDevelopmentServices(IServiceCollection services) { + // use in-memory database + ConfigureTestingServices(services); + + // use real database + // ConfigureProductionServices(services); + + } + public void ConfigureTestingServices(IServiceCollection services) + { + // use in-memory database + services.AddDbContext(c => + c.UseInMemoryDatabase("Catalog")); + + // Add Identity DbContext + services.AddDbContext(options => + options.UseInMemoryDatabase("Identity")); + + ConfigureServices(services); + } + + public void ConfigureProductionServices(IServiceCollection services) + { + // use real database services.AddDbContext(c => { try { - //c.UseInMemoryDatabase("Catalog"); - // Requires LocalDB which can be installed with SQL Server Express 2016 // https://www.microsoft.com/en-us/download/details.aspx?id=54284 c.UseSqlServer(Configuration.GetConnectionString("CatalogConnection")); } - catch (System.Exception ex ) + catch (System.Exception ex) { var message = ex.Message; - } + } }); // Add Identity DbContext services.AddDbContext(options => - //options.UseInMemoryDatabase("Identity")); options.UseSqlServer(Configuration.GetConnectionString("IdentityConnection"))); + ConfigureServices(services); + } + + public void ConfigureServices(IServiceCollection services) + { services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); @@ -129,22 +154,5 @@ namespace Microsoft.eShopWeb await context.Response.WriteAsync(sb.ToString()); })); } - - // moved to Program.cs - //public void ConfigureDevelopment(IApplicationBuilder app, - // IHostingEnvironment env, - // ILoggerFactory loggerFactory, - // UserManager userManager, - // CatalogContext catalogContext) - //{ - // Configure(app, env); - - // //Seed Data - // CatalogContextSeed.SeedAsync(app, catalogContext, loggerFactory) - // .Wait(); - - // var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" }; - // userManager.CreateAsync(defaultUser, "Pass@word1").Wait(); - //} } } diff --git a/tests/FunctionalTests/Web/Controllers/ApiCatalogControllerList.cs b/tests/FunctionalTests/Web/Controllers/ApiCatalogControllerList.cs new file mode 100644 index 0000000..7b25bee --- /dev/null +++ b/tests/FunctionalTests/Web/Controllers/ApiCatalogControllerList.cs @@ -0,0 +1,34 @@ +using System.IO; +using Xunit; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Microsoft.eShopWeb.ViewModels; +using System.Linq; + +namespace FunctionalTests.Web.Controllers +{ + public class ApiCatalogControllerList : BaseWebTest + { + [Fact] + public async Task ReturnsFirst10CatalogItems() + { + var response = await _client.GetAsync("/api/catalog/list"); + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + var model = JsonConvert.DeserializeObject(stringResponse); + + Assert.Equal(10, model.CatalogItems.Count()); + } + + [Fact] + public async Task ReturnsLast2CatalogItemsGivenPageIndex1() + { + var response = await _client.GetAsync("/api/catalog/list?page=1"); + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + var model = JsonConvert.DeserializeObject(stringResponse); + + Assert.Equal(2, model.CatalogItems.Count()); + } + } +} diff --git a/tests/FunctionalTests/Web/Controllers/BaseWebTest.cs b/tests/FunctionalTests/Web/Controllers/BaseWebTest.cs index adf7a05..8f96a07 100644 --- a/tests/FunctionalTests/Web/Controllers/BaseWebTest.cs +++ b/tests/FunctionalTests/Web/Controllers/BaseWebTest.cs @@ -6,6 +6,11 @@ using Microsoft.Extensions.PlatformAbstractions; using Microsoft.eShopWeb; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Logging; +using Infrastructure.Data; +using Infrastructure.Identity; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; namespace FunctionalTests.Web.Controllers { @@ -25,12 +30,25 @@ namespace FunctionalTests.Web.Controllers _contentRoot = GetProjectPath("src", startupAssembly); var builder = new WebHostBuilder() .UseContentRoot(_contentRoot) + .UseEnvironment("Testing") .UseStartup(); var server = new TestServer(builder); - var client = server.CreateClient(); - return client; + // seed data + using (var scope = server.Host.Services.CreateScope()) + { + var services = scope.ServiceProvider; + var loggerFactory = services.GetRequiredService(); + var catalogContext = services.GetRequiredService(); + CatalogContextSeed.SeedAsync(catalogContext, loggerFactory) + .Wait(); + + var userManager = services.GetRequiredService>(); + AppIdentityDbContextSeed.SeedAsync(userManager).Wait(); + } + + return server.CreateClient(); } /// diff --git a/tests/FunctionalTests/Web/Controllers/CatalogControllerGetImage.cs b/tests/FunctionalTests/Web/Controllers/CatalogControllerGetImage.cs deleted file mode 100644 index 2cd4d3a..0000000 --- a/tests/FunctionalTests/Web/Controllers/CatalogControllerGetImage.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.IO; -using Xunit; -using System.Threading.Tasks; - -namespace FunctionalTests.Web.Controllers -{ - public class CatalogControllerGetImage : BaseWebTest - { - //[Fact] - // GetImage replaced by static file middleware - public async Task ReturnsFileContentResultGivenValidId() - { - var testFilePath = Path.Combine(_contentRoot, "pics//1.png"); - var expectedFileBytes = File.ReadAllBytes(testFilePath); - - var response = await _client.GetAsync("/catalog/pic/1"); - response.EnsureSuccessStatusCode(); - var streamResponse = await response.Content.ReadAsStreamAsync(); - byte[] byteResult; - using (var ms = new MemoryStream()) - { - streamResponse.CopyTo(ms); - byteResult = ms.ToArray(); - } - - Assert.Equal(expectedFileBytes, byteResult); - } - } -} diff --git a/tests/IntegrationTests/IntegrationTests.csproj b/tests/IntegrationTests/IntegrationTests.csproj index f83abe6..b524a00 100644 --- a/tests/IntegrationTests/IntegrationTests.csproj +++ b/tests/IntegrationTests/IntegrationTests.csproj @@ -19,6 +19,7 @@ + diff --git a/tests/IntegrationTests/Repositories/OrderRepositoryTests/GetById.cs b/tests/IntegrationTests/Repositories/OrderRepositoryTests/GetById.cs new file mode 100644 index 0000000..3d6b659 --- /dev/null +++ b/tests/IntegrationTests/Repositories/OrderRepositoryTests/GetById.cs @@ -0,0 +1,46 @@ +using ApplicationCore.Entities.OrderAggregate; +using Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using UnitTests.Builders; +using Xunit; +using Xunit.Abstractions; + +namespace IntegrationTests.Repositories.OrderRepositoryTests +{ + public class GetById + { + private readonly CatalogContext _catalogContext; + private readonly OrderRepository _orderRepository; + private OrderBuilder OrderBuilder { get; } = new OrderBuilder(); + private readonly ITestOutputHelper _output; + public GetById(ITestOutputHelper output) + { + _output = output; + var dbOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "TestCatalog") + .Options; + _catalogContext = new CatalogContext(dbOptions); + _orderRepository = new OrderRepository(_catalogContext); + } + + [Fact] + public void GetsExistingOrder() + { + var existingOrder = OrderBuilder.WithDefaultValues(); + _catalogContext.Orders.Add(existingOrder); + _catalogContext.SaveChanges(); + int orderId = existingOrder.Id; + _output.WriteLine($"OrderId: {orderId}"); + + var orderFromRepo = _orderRepository.GetById(orderId); + Assert.Equal(OrderBuilder.TestBuyerId, orderFromRepo.BuyerId); + + // Note: Using InMemoryDatabase OrderItems is available. Will be null if using SQL DB. + var firstItem = orderFromRepo.OrderItems.FirstOrDefault(); + Assert.Equal(OrderBuilder.TestUnits, firstItem.Units); + } + } +} diff --git a/tests/UnitTests/ApplicationCore/Entities/BasketTests/AddItem.cs b/tests/UnitTests/ApplicationCore/Entities/BasketTests/AddItem.cs new file mode 100644 index 0000000..f11c23f --- /dev/null +++ b/tests/UnitTests/ApplicationCore/Entities/BasketTests/AddItem.cs @@ -0,0 +1,57 @@ +using Microsoft.eShopWeb.ApplicationCore.Entities; +using System.Linq; +using Xunit; + +namespace UnitTests.ApplicationCore.Entities.BasketTests +{ + public class AddItem + { + private int _testCatalogItemId = 123; + private decimal _testUnitPrice = 1.23m; + private int _testQuantity = 2; + + [Fact] + public void AddsBasketItemIfNotPresent() + { + var basket = new Basket(); + basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity); + + var firstItem = basket.Items.Single(); + Assert.Equal(_testCatalogItemId, firstItem.CatalogItemId); + Assert.Equal(_testUnitPrice, firstItem.UnitPrice); + Assert.Equal(_testQuantity, firstItem.Quantity); + } + + [Fact] + public void IncrementsQuantityOfItemIfPresent() + { + var basket = new Basket(); + basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity); + basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity); + + var firstItem = basket.Items.Single(); + Assert.Equal(_testQuantity * 2, firstItem.Quantity); + } + + [Fact] + public void KeepsOriginalUnitPriceIfMoreItemsAdded() + { + var basket = new Basket(); + basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity); + basket.AddItem(_testCatalogItemId, _testUnitPrice * 2, _testQuantity); + + var firstItem = basket.Items.Single(); + Assert.Equal(_testUnitPrice, firstItem.UnitPrice); + } + + [Fact] + public void DefaultsToQuantityOfOne() + { + var basket = new Basket(); + basket.AddItem(_testCatalogItemId, _testUnitPrice); + + var firstItem = basket.Items.Single(); + Assert.Equal(1, firstItem.Quantity); + } + } +} diff --git a/tests/UnitTests/Builders/AddressBuilder.cs b/tests/UnitTests/Builders/AddressBuilder.cs new file mode 100644 index 0000000..7720b4e --- /dev/null +++ b/tests/UnitTests/Builders/AddressBuilder.cs @@ -0,0 +1,28 @@ +using ApplicationCore.Entities.OrderAggregate; + +namespace UnitTests.Builders +{ + public class AddressBuilder + { + private Address _address; + public string TestStreet => "123 Main St."; + public string TestCity => "Kent"; + public string TestState => "OH"; + public string TestCountry => "USA"; + public string TestZipCode => "44240"; + + public AddressBuilder() + { + _address = WithDefaultValues(); + } + public Address Build() + { + return _address; + } + public Address WithDefaultValues() + { + _address = new Address(TestStreet, TestCity, TestState, TestCountry, TestZipCode); + return _address; + } + } +} diff --git a/tests/UnitTests/Builders/OrderBuilder.cs b/tests/UnitTests/Builders/OrderBuilder.cs new file mode 100644 index 0000000..05a9865 --- /dev/null +++ b/tests/UnitTests/Builders/OrderBuilder.cs @@ -0,0 +1,38 @@ +using ApplicationCore.Entities.OrderAggregate; +using System; +using System.Collections.Generic; +using System.Text; + +namespace UnitTests.Builders +{ + public class OrderBuilder + { + private Order _order; + public string TestBuyerId => "12345"; + public int TestCatalogItemId => 234; + public string TestProductName => "Test Product Name"; + public string TestPictureUri => "http://test.com/image.jpg"; + public decimal TestUnitPrice = 1.23m; + public int TestUnits = 3; + public CatalogItemOrdered TestCatalogItemOrdered { get; } + + public OrderBuilder() + { + TestCatalogItemOrdered = new CatalogItemOrdered(TestCatalogItemId, TestProductName, TestPictureUri); + _order = WithDefaultValues(); + } + + public Order Build() + { + return _order; + } + + public Order WithDefaultValues() + { + var orderItem = new OrderItem(TestCatalogItemOrdered, TestUnitPrice, TestUnits); + var itemList = new List() { orderItem }; + _order = new Order(TestBuyerId, new AddressBuilder().WithDefaultValues(), itemList); + return _order; + } + } +}