diff --git a/samples/ConsoleAppWithDI/solution/Directory.Packages.props b/samples/ConsoleAppWithDI/solution/Directory.Packages.props index e7c688141..bd2506300 100644 --- a/samples/ConsoleAppWithDI/solution/Directory.Packages.props +++ b/samples/ConsoleAppWithDI/solution/Directory.Packages.props @@ -7,6 +7,7 @@ + diff --git a/samples/ConsoleAppWithDI/solution/src/Maris.ConsoleApp.Hosting/ConsoleAppHostedService.cs b/samples/ConsoleAppWithDI/solution/src/Maris.ConsoleApp.Hosting/ConsoleAppHostedService.cs index 83391d016..11b2aa1b9 100644 --- a/samples/ConsoleAppWithDI/solution/src/Maris.ConsoleApp.Hosting/ConsoleAppHostedService.cs +++ b/samples/ConsoleAppWithDI/solution/src/Maris.ConsoleApp.Hosting/ConsoleAppHostedService.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using Maris.ConsoleApp.Core; +using Maris.ConsoleApp.Core; using Maris.ConsoleApp.Hosting.Resources; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -15,7 +14,8 @@ internal class ConsoleAppHostedService : IHostedService private readonly ConsoleAppSettings settings; private readonly CommandExecutor executor; private readonly ILogger logger; - private readonly Stopwatch stopwatch = new(); + private readonly TimeProvider timeProvider; + private long startTime; /// /// クラスの新しいインスタンスを初期化します。 @@ -33,11 +33,34 @@ internal class ConsoleAppHostedService : IHostedService /// /// public ConsoleAppHostedService(IHostApplicationLifetime lifetime, ConsoleAppSettings settings, CommandExecutor executor, ILogger logger) + : this(lifetime, settings, executor, logger, TimeProvider.System) + { + } + + /// + /// クラスの新しいインスタンスを初期化します。 + /// + /// アプリケーションのライフタイムイベントを通知できるようにするためのオブジェクト + /// コンソールアプリケーションの設定項目を管理するオブジェクト。 + /// コマンドの実行を管理するオブジェクト。 + /// ロガー + /// 日時のプロバイダ。 + /// + /// + /// です。 + /// です。 + /// です。 + /// です。 + /// です。 + /// + /// + internal ConsoleAppHostedService(IHostApplicationLifetime lifetime, ConsoleAppSettings settings, CommandExecutor executor, ILogger logger, TimeProvider timeProvider) { this.lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime)); this.settings = settings ?? throw new ArgumentNullException(nameof(settings)); this.executor = executor ?? throw new ArgumentNullException(nameof(executor)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); } /// @@ -54,7 +77,7 @@ public ConsoleAppHostedService(IHostApplicationLifetime lifetime, ConsoleAppSett /// タスク。 public async Task StartAsync(CancellationToken cancellationToken) { - this.stopwatch.Start(); + this.startTime = this.timeProvider.GetTimestamp(); this.logger.LogInformation(Events.StartHostingService, Messages.StartHostingService, this.executor.CommandName); try { @@ -85,13 +108,12 @@ public async Task StartAsync(CancellationToken cancellationToken) /// タスク。 public Task StopAsync(CancellationToken cancellationToken) { - this.stopwatch.Stop(); this.logger.LogInformation( Events.StopHostingService, Messages.StopHostingService, this.executor.CommandName, Environment.ExitCode, - this.stopwatch.ElapsedMilliseconds); + this.timeProvider.GetElapsedTime(this.startTime).TotalMilliseconds); return Task.CompletedTask; } } diff --git a/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.IntegrationTests/Maris.ConsoleApp.IntegrationTests.csproj b/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.IntegrationTests/Maris.ConsoleApp.IntegrationTests.csproj index 033afafc3..340b22f4e 100644 --- a/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.IntegrationTests/Maris.ConsoleApp.IntegrationTests.csproj +++ b/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.IntegrationTests/Maris.ConsoleApp.IntegrationTests.csproj @@ -2,6 +2,7 @@ + all diff --git a/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.IntegrationTests/ScopeTests/ObjectState.cs b/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.IntegrationTests/ScopeTests/ObjectState.cs index c69ddb111..4472bcdc8 100644 --- a/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.IntegrationTests/ScopeTests/ObjectState.cs +++ b/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.IntegrationTests/ScopeTests/ObjectState.cs @@ -2,11 +2,16 @@ internal class ObjectState { - internal ObjectState(Guid objectId, Type objectType, Condition condition) + private readonly TimeProvider timeProvider; + private DateTime createDate; + + internal ObjectState(Guid objectId, Type objectType, Condition condition, TimeProvider timeProvider) { this.ObjectId = objectId; this.ObjectType = objectType; this.Condition = condition; + this.timeProvider = timeProvider; + this.createDate = this.timeProvider.GetLocalNow().UtcDateTime; } internal Guid ObjectId { get; private set; } @@ -15,5 +20,5 @@ internal ObjectState(Guid objectId, Type objectType, Condition condition) internal Condition Condition { get; private set; } - internal DateTime CreateDate { get; } = DateTime.Now; + internal DateTime CreateDate { get => this.createDate; } } diff --git a/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.IntegrationTests/ScopeTests/TestObjectBase.cs b/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.IntegrationTests/ScopeTests/TestObjectBase.cs index 4e490d2d6..0ca39a76c 100644 --- a/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.IntegrationTests/ScopeTests/TestObjectBase.cs +++ b/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.IntegrationTests/ScopeTests/TestObjectBase.cs @@ -1,15 +1,21 @@ -namespace Maris.ConsoleApp.IntegrationTests.ScopeTests; +using Microsoft.Extensions.Time.Testing; + +namespace Maris.ConsoleApp.IntegrationTests.ScopeTests; internal class TestObjectBase : IDisposable { private readonly Guid objectId = Guid.NewGuid(); private bool disposed; + private TimeProvider fakeTimeProvider = new FakeTimeProvider(); - public TestObjectBase() => + public TestObjectBase() + { ObjectStateHistory.Add(new( this.objectId, this.GetType(), - Condition.Creating)); + Condition.Creating, + this.fakeTimeProvider)); + } public void Dispose() { @@ -21,16 +27,17 @@ protected void LogHistory() => ObjectStateHistory.Add(new( this.objectId, this.GetType(), - this.disposed ? Condition.AlreadyDisposed : Condition.Alive)); + this.disposed ? Condition.AlreadyDisposed : Condition.Alive, + this.fakeTimeProvider)); protected virtual void Dispose(bool disposing) { - ObjectStateHistory.Add(new(this.objectId, this.GetType(), Condition.ObjectDisposing)); + ObjectStateHistory.Add(new(this.objectId, this.GetType(), Condition.ObjectDisposing, this.fakeTimeProvider)); if (!this.disposed) { this.disposed = true; - ObjectStateHistory.Add(new(this.objectId, this.GetType(), Condition.ObjectDisposed)); + ObjectStateHistory.Add(new(this.objectId, this.GetType(), Condition.ObjectDisposed, this.fakeTimeProvider)); } } } diff --git a/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.UnitTests/Hosting/ConsoleAppHostedServiceTest.cs b/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.UnitTests/Hosting/ConsoleAppHostedServiceTest.cs index a5c71e9cc..d8909370b 100644 --- a/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.UnitTests/Hosting/ConsoleAppHostedServiceTest.cs +++ b/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.UnitTests/Hosting/ConsoleAppHostedServiceTest.cs @@ -2,6 +2,7 @@ using Maris.ConsoleApp.Hosting; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Time.Testing; using Xunit.Abstractions; namespace Maris.ConsoleApp.UnitTests.Hosting; @@ -386,6 +387,37 @@ public async Task StopAsync_コマンドの実行完了後に呼び出す_コマ Assert.StartsWith($"sync-command コマンドのホストの処理が終了コード {exitCode} で完了しました。", record.Message); } + [Fact] + public async Task StopAsync_コマンドの実行時間がログに出力される() + { + // Arrange + var lifetime = Mock.Of(); + var settings = new ConsoleAppSettings(); + var commandAttribute = new CommandAttribute("sync-command", typeof(SyncCommandImpl)); + var parameter = new CommandParameter(); + var context = new ConsoleAppContext(commandAttribute, parameter); + var exitCode = 456; + var command = new SyncCommandImpl(exitCode); + command.Initialize(context); + var managerMock = CreateCommandManagerMock(command); + var manager = managerMock.Object; + var commandExecutorLogger = this.CreateTestLogger(); + var executor = new CommandExecutor(manager, commandExecutorLogger); + var logger = this.CreateTestLogger(); + var fakeTimeProvider = new FakeTimeProvider(); + var service = new ConsoleAppHostedService(lifetime, settings, executor, logger, fakeTimeProvider); + var cancellationToken = new CancellationToken(false); + await service.StartAsync(cancellationToken); + + // Act + fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(1000)); + await service.StopAsync(cancellationToken); + + // Assert + var record = this.LogCollector.LatestRecord; + Assert.Contains($"実行時間は 1000 ms でした。", record.Message); + } + private static Mock CreateCommandManagerMock(CommandBase? creatingCommand = null) { var command = creatingCommand ?? new TestCommand(); diff --git a/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.UnitTests/Maris.ConsoleApp.UnitTests.csproj b/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.UnitTests/Maris.ConsoleApp.UnitTests.csproj index 9aeff6e4e..66822e71d 100644 --- a/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.UnitTests/Maris.ConsoleApp.UnitTests.csproj +++ b/samples/ConsoleAppWithDI/solution/tests/Maris.ConsoleApp.UnitTests/Maris.ConsoleApp.UnitTests.csproj @@ -6,6 +6,7 @@ + diff --git a/samples/Dressca/dressca-backend/Directory.Packages.props b/samples/Dressca/dressca-backend/Directory.Packages.props index c8a807182..94c59eb2b 100644 --- a/samples/Dressca/dressca-backend/Directory.Packages.props +++ b/samples/Dressca/dressca-backend/Directory.Packages.props @@ -11,6 +11,7 @@ + diff --git a/samples/Dressca/dressca-backend/src/Dressca.ApplicationCore/Ordering/Order.cs b/samples/Dressca/dressca-backend/src/Dressca.ApplicationCore/Ordering/Order.cs index d758284d0..5c79d9292 100644 --- a/samples/Dressca/dressca-backend/src/Dressca.ApplicationCore/Ordering/Order.cs +++ b/samples/Dressca/dressca-backend/src/Dressca.ApplicationCore/Ordering/Order.cs @@ -10,6 +10,7 @@ public class Order { private readonly List orderItems = new(); private readonly Account? account; + private readonly TimeProvider timeProvider; private string? buyerId; private ShipTo? shipToAddress; @@ -31,6 +32,31 @@ public class Order /// /// public Order(string buyerId, ShipTo shipToAddress, List orderItems) + : this(buyerId, shipToAddress, orderItems, TimeProvider.System) + { + } + + /// + /// クラスの新しいインスタンスを初期化します。 + /// 単体テスト用に を受け取ることができます。 + /// + /// 購入者 Id 。 + /// 配送先住所。 + /// 注文アイテムのリスト。 + /// 日時のプロバイダ。通常はシステム日時。 + /// + /// + /// または空の文字列です。 + /// または空のリストです。 + /// + /// + /// + /// + /// です。 + /// です。 + /// + /// + internal Order(string buyerId, ShipTo shipToAddress, List orderItems, TimeProvider timeProvider) { if (orderItems is null || !orderItems.Any()) { @@ -46,11 +72,14 @@ public Order(string buyerId, ShipTo shipToAddress, List orderItems) this.DeliveryCharge = this.account.GetDeliveryCharge(); this.ConsumptionTax = this.account.GetConsumptionTax(); this.TotalPrice = this.account.GetTotalPrice(); + this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + this.OrderDate = this.timeProvider.GetLocalNow(); } private Order() { // Required by EF Core. + this.timeProvider = TimeProvider.System; } /// @@ -81,7 +110,7 @@ private set /// 注文日を取得します。 /// このクラスのインスタンスが生成されたシステム日時が自動的に設定されます. /// - public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now; + public DateTimeOffset OrderDate { get; private set; } /// /// お届け先を取得します。 diff --git a/samples/Dressca/dressca-backend/src/Dressca.Web/Baskets/BuyerIdFilterAttribute.cs b/samples/Dressca/dressca-backend/src/Dressca.Web/Baskets/BuyerIdFilterAttribute.cs index cd3c66de1..89b1f9fa6 100644 --- a/samples/Dressca/dressca-backend/src/Dressca.Web/Baskets/BuyerIdFilterAttribute.cs +++ b/samples/Dressca/dressca-backend/src/Dressca.Web/Baskets/BuyerIdFilterAttribute.cs @@ -19,16 +19,34 @@ public class BuyerIdFilterAttribute : ActionFilterAttribute { private const string DefaultBuyerIdCookieName = "Dressca-Bid"; private readonly string buyerIdCookieName; + private readonly TimeProvider timeProvider; /// /// クラスの新しいインスタンスを初期化します。 /// /// Cookie のキー名。未指定時は "Dressca-Bid" 。 + public BuyerIdFilterAttribute(string buyerIdCookieName = DefaultBuyerIdCookieName) + : this(buyerIdCookieName, TimeProvider.System) + { + } + + /// + /// クラスの新しいインタンスを初期化します。 + /// 単体テスト用に を受け取ることができます。 + /// + /// Cookie のキー名。 + /// 日時のプロバイダ。通常はシステム日時。 /// - /// です。 + /// + /// です。 + /// です。 + /// /// - public BuyerIdFilterAttribute(string buyerIdCookieName = DefaultBuyerIdCookieName) - => this.buyerIdCookieName = buyerIdCookieName ?? throw new ArgumentNullException(nameof(buyerIdCookieName)); + internal BuyerIdFilterAttribute(string buyerIdCookieName, TimeProvider timeProvider) + { + this.buyerIdCookieName = buyerIdCookieName ?? throw new ArgumentNullException(nameof(buyerIdCookieName)); + this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + } /// public override void OnActionExecuting(ActionExecutingContext context) @@ -61,7 +79,7 @@ public override void OnActionExecuted(ActionExecutedContext context) HttpOnly = true, SameSite = SameSiteMode.Strict, Secure = true, - Expires = DateTimeOffset.Now.AddDays(7), + Expires = this.timeProvider.GetLocalNow().AddDays(7), }); base.OnActionExecuted(context); diff --git a/samples/Dressca/dressca-backend/src/Dressca.Web/Dressca.Web.csproj b/samples/Dressca/dressca-backend/src/Dressca.Web/Dressca.Web.csproj index 1114bb191..2b29f8f44 100644 --- a/samples/Dressca/dressca-backend/src/Dressca.Web/Dressca.Web.csproj +++ b/samples/Dressca/dressca-backend/src/Dressca.Web/Dressca.Web.csproj @@ -59,6 +59,10 @@ + + + + diff --git a/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/ApplicationCore/Ordering/OrderTest.cs b/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/ApplicationCore/Ordering/OrderTest.cs index 378d808dc..08e2286cb 100644 --- a/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/ApplicationCore/Ordering/OrderTest.cs +++ b/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/ApplicationCore/Ordering/OrderTest.cs @@ -1,4 +1,5 @@ using Dressca.ApplicationCore.Ordering; +using Microsoft.Extensions.Time.Testing; namespace Dressca.UnitTests.ApplicationCore.Ordering; @@ -171,6 +172,24 @@ public void HasMatchingBuyerId_指定の購入者Idと一致しない_false() Assert.False(result); } + [Fact] + public void Constructor_OrderDateが注文時のシステム時刻と等しい() + { + // Arrange + var buyerId = Guid.NewGuid().ToString("D"); + var shipTo = CreateDefaultShipTo(); + var items = CreateDefaultOrderItems(); + var fakeTimeProvider = new FakeTimeProvider(); + var testOrderTime = new DateTimeOffset(2024, 4, 1, 00, 00, 00, new TimeSpan(9, 0, 0)); + fakeTimeProvider.SetUtcNow(testOrderTime); + + // Act + var order = new Order(buyerId, shipTo, items, fakeTimeProvider); + + // Assert + Assert.Equal(testOrderTime, order.OrderDate); + } + private static Address CreateDefaultAddress() { const string defaultPostalCode = "100-8924"; diff --git a/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/Dressca.UnitTests.csproj b/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/Dressca.UnitTests.csproj index 498fca40b..32472ae36 100644 --- a/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/Dressca.UnitTests.csproj +++ b/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/Dressca.UnitTests.csproj @@ -10,6 +10,7 @@ + diff --git a/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/Web/Baskets/BuyerIdFilterAttributeTest.cs b/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/Web/Baskets/BuyerIdFilterAttributeTest.cs new file mode 100644 index 000000000..8c5211c8f --- /dev/null +++ b/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/Web/Baskets/BuyerIdFilterAttributeTest.cs @@ -0,0 +1,36 @@ +using System.Globalization; +using Dressca.Web.Baskets; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Time.Testing; + +namespace Dressca.UnitTests.Web.Baskets; + +public class BuyerIdFilterAttributeTest +{ + [Fact] + public void Cookieの有効期限は7日間() + { + // Arrange + var buyerIdCookieName = "Dressca-Bid"; + var httpContext = new DefaultHttpContext(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var context = new ActionExecutedContext(actionContext, new List(), Mock.Of()); + var fakeTimeProvider = new FakeTimeProvider(); + var testCookieCreatedDateTime = new DateTimeOffset(2024, 4, 1, 00, 00, 00, TimeSpan.Zero); + fakeTimeProvider.SetUtcNow(testCookieCreatedDateTime); + var expectedDateTime = testCookieCreatedDateTime.AddDays(7); + var formattedExpectedDateTime = expectedDateTime.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture); + var filter = new BuyerIdFilterAttribute(buyerIdCookieName, fakeTimeProvider); + + // Act + filter.OnActionExecuted(context); + var setCookieString = httpContext.Response.Headers.SetCookie.ToString(); + + // Assert + Assert.Contains(formattedExpectedDateTime, setCookieString); + } +} diff --git a/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/Web/Baskets/HttpContextExtensionsTest.cs b/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/Web/Baskets/HttpContextExtensionsTest.cs index 0b29ecfcd..90f79406b 100644 --- a/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/Web/Baskets/HttpContextExtensionsTest.cs +++ b/samples/Dressca/dressca-backend/tests/Dressca.UnitTests/Web/Baskets/HttpContextExtensionsTest.cs @@ -1,5 +1,6 @@ using Dressca.Web.Baskets; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Time.Testing; namespace Dressca.UnitTests.Web.Baskets; @@ -24,9 +25,10 @@ public void GetBuyerId_購入者IdがHttpContextに存在しない_新たにGuid public void GetBuyerId_購入者Idが文字列型ではない_新たにGuid形式の購入者Idが発行される() { // Arrange + var fakeTimeProvider = new FakeTimeProvider(); var items = new Dictionary { - { "Dressca-BuyerId", DateTimeOffset.Now }, + { "Dressca-BuyerId", fakeTimeProvider.GetLocalNow() }, }; var httpContextMock = new Mock(); httpContextMock.SetupProperty(httpContext => httpContext.Items, items);