diff --git a/src/MusicStore/Components/AnnouncementComponent.cs b/src/MusicStore/Components/AnnouncementComponent.cs index 14497b5bc3..3f71aad489 100644 --- a/src/MusicStore/Components/AnnouncementComponent.cs +++ b/src/MusicStore/Components/AnnouncementComponent.cs @@ -25,6 +25,13 @@ namespace MusicStore.Components set; } + [Activate] + public ISystemClock Clock + { + get; + set; + } + public async Task InvokeAsync() { var latestAlbum = await Cache.GetOrSet("latestAlbum", async context => @@ -36,11 +43,11 @@ namespace MusicStore.Components return View(latestAlbum); } - private Task GetLatestAlbum() + private async Task GetLatestAlbum() { - var latestAlbum = DbContext.Albums + var latestAlbum = await DbContext.Albums .OrderByDescending(a => a.Created) - .Where(a => (a.Created - DateTime.UtcNow).TotalDays <= 2) + .Where(a => (a.Created - Clock.UtcNow).TotalDays <= 2) .FirstOrDefaultAsync(); return latestAlbum; diff --git a/src/MusicStore/Components/ISystemClock.cs b/src/MusicStore/Components/ISystemClock.cs new file mode 100644 index 0000000000..9673d009bd --- /dev/null +++ b/src/MusicStore/Components/ISystemClock.cs @@ -0,0 +1,16 @@ +using System; + +namespace MusicStore.Components +{ + /// + /// Abstracts the system clock to facilitate testing. + /// + public interface ISystemClock + { + /// + /// Gets a DateTime object that is set to the current date and time on this computer, + /// expressed as the Coordinated Universal Time(UTC) + /// + DateTime UtcNow { get; } + } +} \ No newline at end of file diff --git a/src/MusicStore/Components/SystemClock.cs b/src/MusicStore/Components/SystemClock.cs new file mode 100644 index 0000000000..68fa7b7ae8 --- /dev/null +++ b/src/MusicStore/Components/SystemClock.cs @@ -0,0 +1,22 @@ +using System; + +namespace MusicStore.Components +{ + /// + /// Provides access to the normal system clock. + /// + public class SystemClock : ISystemClock + { + /// + public DateTime UtcNow + { + get + { + // The clock measures whole seconds only, and truncates the milliseconds, + // because millisecond resolution is inconsistent among various underlying systems. + DateTime utcNow = DateTime.UtcNow; + return utcNow.AddMilliseconds(-utcNow.Millisecond); + } + } + } +} diff --git a/src/MusicStore/Models/ShoppingCart.cs b/src/MusicStore/Models/ShoppingCart.cs index a8c8a1422f..abcec5b572 100644 --- a/src/MusicStore/Models/ShoppingCart.cs +++ b/src/MusicStore/Models/ShoppingCart.cs @@ -147,7 +147,7 @@ namespace MusicStore.Models return order.OrderId; } - // We're using HttpContextBase to allow access to cookies. + // We're using HttpContextBase to allow access to sessions. private string GetCartId(HttpContext context) { var cartId = context.Session.GetString("Session"); diff --git a/src/MusicStore/Startup.cs b/src/MusicStore/Startup.cs index 4cd38a8bd1..c32edbd08e 100644 --- a/src/MusicStore/Startup.cs +++ b/src/MusicStore/Startup.cs @@ -10,6 +10,7 @@ using Microsoft.Framework.ConfigurationModel; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.Logging; using Microsoft.Framework.Runtime; +using MusicStore.Components; using MusicStore.Models; namespace MusicStore @@ -104,6 +105,9 @@ namespace MusicStore services.AddCaching(); services.AddSession(); + // Add the system clock service + services.AddSingleton(); + // Configure Auth services.Configure(options => { diff --git a/test/MusicStore.Test/AnnouncementComponentTest.cs b/test/MusicStore.Test/AnnouncementComponentTest.cs new file mode 100644 index 0000000000..fbcc99a1ea --- /dev/null +++ b/test/MusicStore.Test/AnnouncementComponentTest.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; +using Microsoft.Data.Entity; +using Microsoft.Framework.Caching.Memory; +using Microsoft.Framework.DependencyInjection; +using MusicStore.Models; +using Xunit; + +namespace MusicStore.Components +{ + public class AnnouncementComponentTest + { + private readonly IServiceProvider _serviceProvider; + + public AnnouncementComponentTest() + { + var services = new ServiceCollection(); + + services.AddEntityFramework() + .AddInMemoryStore() + .AddDbContext(); + + _serviceProvider = services.BuildServiceProvider(); + } + + [Fact] + public async Task AnnouncementComponent_Returns_LatestAlbum() + { + // Arrange + var today = new DateTime(year: 2002, month: 10, day: 30); + + var announcementComponent = new AnnouncementComponent() + { + DbContext = _serviceProvider.GetRequiredService(), + Cache = _serviceProvider.GetRequiredService(), + Clock = new TestSystemClock() { UtcNow = today }, + }; + + PopulateData(announcementComponent.DbContext, latestAlbumDate: today); + + // Action + var result = await announcementComponent.InvokeAsync(); + + // Assert + Assert.NotNull(result); + var viewResult = Assert.IsType(result); + Assert.Null(viewResult.ViewName); + var albumResult = Assert.IsType(viewResult.ViewData.Model); + Assert.Equal(today, albumResult.Created.Date); + } + + private static void PopulateData(DbContext context, DateTime latestAlbumDate) + { + var albums = TestAlbumDataProvider.GetAlbums(latestAlbumDate); + + foreach (var album in albums) + { + context.Add(album); + } + + context.SaveChanges(); + } + + private class TestAlbumDataProvider + { + public static Album[] GetAlbums(DateTime latestAlbumDate) + { + var generes = Enumerable.Range(1, 10).Select(n => + new Genre() + { + GenreId = n, + Name = "Genre Name " + n, + }).ToArray(); + + var artists = Enumerable.Range(1, 10).Select(n => + new Artist() + { + ArtistId = n + 1, + Name = "Artist Name " + n, + }).ToArray(); + + var albums = Enumerable.Range(1, 10).Select(n => + new Album() + { + Artist = artists[n - 1], + ArtistId = n, + Genre = generes[n - 1], + GenreId = n, + Created = latestAlbumDate.AddDays(1 - n), + }).ToArray(); + + return albums; + } + } + + private class TestSystemClock : ISystemClock + { + public DateTime UtcNow { get; set; } + } + } +} diff --git a/test/MusicStore.Test/CartSummaryComponentTest.cs b/test/MusicStore.Test/CartSummaryComponentTest.cs new file mode 100644 index 0000000000..3fcfc65e6f --- /dev/null +++ b/test/MusicStore.Test/CartSummaryComponentTest.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Session; +using Microsoft.Framework.Caching.Distributed; +using Microsoft.Framework.Caching.Memory; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging.Testing; +using MusicStore.Models; +using Xunit; + +namespace MusicStore.Components +{ + public class CartSummaryComponentTest + { + private readonly IServiceProvider _serviceProvider; + + public CartSummaryComponentTest() + { + var services = new ServiceCollection(); + + services.AddEntityFramework() + .AddInMemoryStore() + .AddDbContext(); + + _serviceProvider = services.BuildServiceProvider(); + } + + [Fact] + public async Task CartSummaryComponent_Returns_CartedItems() + { + // Arrange + var viewContext = new ViewContext() + { + HttpContext = new DefaultHttpContext() + }; + + // Session initialization + var cartId = "CartId_A"; + var sessionFeature = new SessionFeature() + { + Session = CreateTestSession(), + }; + viewContext.HttpContext.SetFeature(sessionFeature); + viewContext.HttpContext.Session.SetString("Session", cartId); + + // DbContext initialization + var dbContext = _serviceProvider.GetRequiredService(); + PopulateData(dbContext, cartId, albumTitle: "AlbumA", itemCount: 10); + + // CartSummaryComponent initialization + var cartSummaryComponent = new CartSummaryComponent() + { + DbContext = dbContext, + ViewContext = viewContext, + }; + + // Act + var result = await cartSummaryComponent.InvokeAsync(); + + // Assert + Assert.NotNull(result); + var viewResult = Assert.IsType(result); + Assert.Null(viewResult.ViewName); + Assert.Null(viewResult.ViewData.Model); + Assert.Equal(10, cartSummaryComponent.ViewBag.CartCount); + Assert.Equal("AlbumA", cartSummaryComponent.ViewBag.CartSummary); + } + + private static ISession CreateTestSession() + { + return new DistributedSession( + new LocalCache(new MemoryCache(new MemoryCacheOptions())), + "sessionId_A", + idleTimeout: TimeSpan.MaxValue, + tryEstablishSession: () => true, + loggerFactory: new NullLoggerFactory(), + isNewSessionKey: true); + } + + private static void PopulateData(MusicStoreContext context, string cartId, string albumTitle, int itemCount) + { + var album = new Album() + { + AlbumId = 1, + Title = albumTitle, + }; + + var cartItems = Enumerable.Range(1, itemCount).Select(n => + new CartItem() + { + AlbumId = 1, + Album = album, + Count = 1, + CartId = cartId, + }).ToArray(); + + context.AddRange(cartItems); + context.SaveChanges(); + } + } +} diff --git a/test/MusicStore.Test/GenreMenuComponentTest.cs b/test/MusicStore.Test/GenreMenuComponentTest.cs new file mode 100644 index 0000000000..76da71bb1d --- /dev/null +++ b/test/MusicStore.Test/GenreMenuComponentTest.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; +using Microsoft.Framework.DependencyInjection; +using MusicStore.Models; +using Xunit; + +namespace MusicStore.Components +{ + public class GenreMenuComponentTest + { + private readonly IServiceProvider _serviceProvider; + + public GenreMenuComponentTest() + { + var services = new ServiceCollection(); + services.AddEntityFramework() + .AddInMemoryStore() + .AddDbContext(); + + _serviceProvider = services.BuildServiceProvider(); + } + + [Fact] + public async Task GenreMenuComponent_Returns_NineGenres() + { + // Arrange + var genreMenuComponent = new GenreMenuComponent() + { + DbContext = _serviceProvider.GetRequiredService(), + }; + + PopulateData(genreMenuComponent.DbContext); + + // Act + var result = await genreMenuComponent.InvokeAsync(); + + // Assert + Assert.NotNull(result); + var viewResult = Assert.IsType(result); + Assert.Null(viewResult.ViewName); + var genreResult = Assert.IsType>(viewResult.ViewData.Model); + Assert.Equal(9, genreResult.Count); + } + + private static void PopulateData(MusicStoreContext context) + { + var genres = Enumerable.Range(1, 10).Select(n => new Genre()); + + context.AddRange(genres); + context.SaveChanges(); + } + } +}