diff --git a/Docs/pages/docs/time-system/configuring-time.mdx b/Docs/pages/docs/time-system/configuring-time.mdx index 1738ebaf7..d856f366f 100644 --- a/Docs/pages/docs/time-system/configuring-time.mdx +++ b/Docs/pages/docs/time-system/configuring-time.mdx @@ -22,7 +22,7 @@ new MockTimeSystem(TimeProviderFactory.Now()); ## Local time zone -`MockTimeSystem` exposes a controllable local time zone through `timeSystem.TimeZoneInfo` (an `ITimeZoneInfo`), and uses it to convert the simulated instant for `DateTime.Now` and `DateTime.Today`. This makes time-zone-dependent code deterministic instead of depending on the machine the test runs on. +`MockTimeSystem` exposes a controllable local time zone through `timeSystem.TimeZoneInfo` (an `ITimeZoneInfo`), and uses it to convert the simulated instant for `DateTime.Now`, `DateTime.Today` and `DateTimeOffset.Now`. This makes time-zone-dependent code deterministic instead of depending on the machine the test runs on. The factory picks the initial local time zone: @@ -51,8 +51,9 @@ TimeZoneInfo tokyo = TimeZoneInfo.CreateCustomTimeZone( "Test/Tokyo", TimeSpan.FromHours(9), "Test JST", "Test JST"); timeSystem.TimeProvider.LocalTimeZone = tokyo; -DateTime now = timeSystem.DateTime.Now; // 2026-01-15 09:00 (JST) -TimeZoneInfo local = timeSystem.TimeZoneInfo.Local; // Test/Tokyo +DateTime now = timeSystem.DateTime.Now; // 2026-01-15 09:00 (JST) +DateTimeOffset offsetNow = timeSystem.DateTimeOffset.Now; // offset +09:00 +TimeZoneInfo local = timeSystem.TimeZoneInfo.Local; // Test/Tokyo // RegisterTimeZone adds zones resolvable via FindSystemTimeZoneById / GetSystemTimeZones timeSystem.TimeProvider.RegisterTimeZone(tokyo); diff --git a/Docs/pages/docs/time-system/index.mdx b/Docs/pages/docs/time-system/index.mdx index eabd50ae4..22da76156 100644 --- a/Docs/pages/docs/time-system/index.mdx +++ b/Docs/pages/docs/time-system/index.mdx @@ -11,6 +11,7 @@ title: Time system | Sub-property | Wraps | |-------------------|-------------------------------------------------------------------------------------------------------------------------------| | `DateTime` | `DateTime.Now`, `DateTime.UtcNow`, `DateTime.Today` (plus `MinValue`, `MaxValue`, `UnixEpoch`) | +| `DateTimeOffset` | `DateTimeOffset.Now`, `DateTimeOffset.UtcNow` (plus `MinValue`, `MaxValue`, `UnixEpoch`) | | `Stopwatch` | [`System.Diagnostics.Stopwatch`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch) | | `Task` | [`Task.Delay`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.delay) | | `Thread` | [`Thread.Sleep`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread.sleep) | diff --git a/Source/Testably.Abstractions.Testing/MockTimeSystem.cs b/Source/Testably.Abstractions.Testing/MockTimeSystem.cs index e8a1e9e63..02c1a177e 100644 --- a/Source/Testably.Abstractions.Testing/MockTimeSystem.cs +++ b/Source/Testably.Abstractions.Testing/MockTimeSystem.cs @@ -29,6 +29,7 @@ public INotificationHandler On private readonly NotificationHandler _callbackHandler; private readonly DateTimeMock _dateTimeMock; + private readonly DateTimeOffsetMock _dateTimeOffsetMock; private readonly TimeZoneInfoMock _timeZoneInfoMock; private readonly StopwatchFactoryMock _stopwatchFactoryMock; private readonly TaskMock _taskMock; @@ -93,6 +94,7 @@ public MockTimeSystem(ITimeProviderFactory timeProvider, Func _dateTimeMock; /// - /// - /// The mock implementation for is added in a follow-up change. - /// public IDateTimeOffset DateTimeOffset - => null!; + => _dateTimeOffsetMock; #if FEATURE_PERIODIC_TIMER /// diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/DateTimeOffsetMock.cs b/Source/Testably.Abstractions.Testing/TimeSystem/DateTimeOffsetMock.cs new file mode 100644 index 000000000..a8a4221ab --- /dev/null +++ b/Source/Testably.Abstractions.Testing/TimeSystem/DateTimeOffsetMock.cs @@ -0,0 +1,61 @@ +using System; +using Testably.Abstractions.TimeSystem; + +namespace Testably.Abstractions.Testing.TimeSystem; + +internal sealed class DateTimeOffsetMock : IDateTimeOffset +{ + private readonly NotificationHandler _callbackHandler; + private readonly MockTimeSystem _mockTimeSystem; + + internal DateTimeOffsetMock(MockTimeSystem timeSystem, + NotificationHandler callbackHandler) + { + _mockTimeSystem = timeSystem; + _callbackHandler = callbackHandler; + } + + #region IDateTimeOffset Members + + /// + public DateTimeOffset MaxValue + => new(_mockTimeSystem.TimeProvider.MaxValue.Ticks, TimeSpan.Zero); + + /// + public DateTimeOffset MinValue + => new(_mockTimeSystem.TimeProvider.MinValue.Ticks, TimeSpan.Zero); + + /// + public DateTimeOffset Now + { + get + { + DateTimeOffset value = TimeZoneInfo.ConvertTime( + new DateTimeOffset(_mockTimeSystem.TimeProvider.Read().ToUniversalTime()), + _mockTimeSystem.TimeProvider.LocalTimeZone); + _callbackHandler.InvokeDateTimeOffsetReadCallbacks(value); + return value; + } + } + + /// + public ITimeSystem TimeSystem + => _mockTimeSystem; + + /// + public DateTimeOffset UnixEpoch + => new(_mockTimeSystem.TimeProvider.UnixEpoch.Ticks, TimeSpan.Zero); + + /// + public DateTimeOffset UtcNow + { + get + { + DateTimeOffset value = new(_mockTimeSystem.TimeProvider.Read().ToUniversalTime()); + _callbackHandler.InvokeDateTimeOffsetReadCallbacks(value); + return value; + } + } + + #endregion +} diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/INotificationHandler.cs b/Source/Testably.Abstractions.Testing/TimeSystem/INotificationHandler.cs index 92cdbe849..0517a9e6d 100644 --- a/Source/Testably.Abstractions.Testing/TimeSystem/INotificationHandler.cs +++ b/Source/Testably.Abstractions.Testing/TimeSystem/INotificationHandler.cs @@ -34,6 +34,23 @@ IAwaitableCallback DateTimeRead( Action? callback = null, Func? predicate = null); + /// + /// Callback executed when any of the following DateTimeOffset read methods is called:
+ /// -
+ /// - + ///
+ /// + /// (optional) The callback to execute after DateTimeOffset was read. + /// + /// + /// (optional) A predicate used to filter which callbacks should be notified.
+ /// If set to (default value) all callbacks are notified. + /// + /// A to un-register the callback on dispose. + IAwaitableCallback DateTimeOffsetRead( + Action? callback = null, + Func? predicate = null); + /// /// Callback executed when any of the following Task.Delay overloads is called:
/// -
diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/NotificationHandler.cs b/Source/Testably.Abstractions.Testing/TimeSystem/NotificationHandler.cs index 16e788bff..9e9295718 100644 --- a/Source/Testably.Abstractions.Testing/TimeSystem/NotificationHandler.cs +++ b/Source/Testably.Abstractions.Testing/TimeSystem/NotificationHandler.cs @@ -15,6 +15,9 @@ internal sealed class NotificationHandler(MockTimeSystem mockTimeSystem) private readonly Notification.INotificationFactory _dateTimeReadCallbacks = Notification.CreateFactory(); + private readonly Notification.INotificationFactory + _dateTimeOffsetReadCallbacks = Notification.CreateFactory(); + #if FEATURE_PERIODIC_TIMER private readonly Notification.INotificationFactory _periodicTimerWaitingForNextTickCallbacks = Notification.CreateFactory(); @@ -42,6 +45,12 @@ public IAwaitableCallback DateTimeRead( Func? predicate = null) => _dateTimeReadCallbacks.RegisterCallback(callback, predicate); + /// + public IAwaitableCallback DateTimeOffsetRead( + Action? callback = null, + Func? predicate = null) + => _dateTimeOffsetReadCallbacks.RegisterCallback(callback, predicate); + /// public IAwaitableCallback TaskDelay( Action? callback = null, @@ -79,6 +88,9 @@ public IAwaitableCallback WaitingForNextTick( public void InvokeDateTimeReadCallbacks(DateTime now) => _dateTimeReadCallbacks.InvokeCallbacks(now); + public void InvokeDateTimeOffsetReadCallbacks(DateTimeOffset now) + => _dateTimeOffsetReadCallbacks.InvokeCallbacks(now); + #if FEATURE_PERIODIC_TIMER public void InvokePeriodicTimerWaitingForNextTick(IPeriodicTimer timer) => _periodicTimerWaitingForNextTickCallbacks.InvokeCallbacks(timer); diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net10.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net10.0.txt index 2900b198d..1bbc25753 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net10.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net10.0.txt @@ -461,6 +461,7 @@ namespace Testably.Abstractions.Testing.TimeSystem public interface INotificationHandler { Testably.Abstractions.Testing.TimeSystem.IPeriodicTimerNotificationHandler PeriodicTimer { get; } + Testably.Abstractions.Testing.IAwaitableCallback DateTimeOffsetRead(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback DateTimeRead(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback TaskDelay(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback ThreadSleep(System.Action? callback = null, System.Func? predicate = null); diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt index fcd0fd335..0f17049f4 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt @@ -445,6 +445,7 @@ namespace Testably.Abstractions.Testing.TimeSystem { public interface INotificationHandler { + Testably.Abstractions.Testing.IAwaitableCallback DateTimeOffsetRead(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback DateTimeRead(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback TaskDelay(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback ThreadSleep(System.Action? callback = null, System.Func? predicate = null); diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt index 3e472157f..4169d994f 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt @@ -461,6 +461,7 @@ namespace Testably.Abstractions.Testing.TimeSystem public interface INotificationHandler { Testably.Abstractions.Testing.TimeSystem.IPeriodicTimerNotificationHandler PeriodicTimer { get; } + Testably.Abstractions.Testing.IAwaitableCallback DateTimeOffsetRead(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback DateTimeRead(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback TaskDelay(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback ThreadSleep(System.Action? callback = null, System.Func? predicate = null); diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net9.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net9.0.txt index a810c47d0..1a08fac7b 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net9.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net9.0.txt @@ -461,6 +461,7 @@ namespace Testably.Abstractions.Testing.TimeSystem public interface INotificationHandler { Testably.Abstractions.Testing.TimeSystem.IPeriodicTimerNotificationHandler PeriodicTimer { get; } + Testably.Abstractions.Testing.IAwaitableCallback DateTimeOffsetRead(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback DateTimeRead(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback TaskDelay(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback ThreadSleep(System.Action? callback = null, System.Func? predicate = null); diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt index c1be40eb0..77709ea3f 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt @@ -425,6 +425,7 @@ namespace Testably.Abstractions.Testing.TimeSystem { public interface INotificationHandler { + Testably.Abstractions.Testing.IAwaitableCallback DateTimeOffsetRead(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback DateTimeRead(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback TaskDelay(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback ThreadSleep(System.Action? callback = null, System.Func? predicate = null); diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt index f88cf30e2..f5aca6e07 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt @@ -438,6 +438,7 @@ namespace Testably.Abstractions.Testing.TimeSystem { public interface INotificationHandler { + Testably.Abstractions.Testing.IAwaitableCallback DateTimeOffsetRead(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback DateTimeRead(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback TaskDelay(System.Action? callback = null, System.Func? predicate = null); Testably.Abstractions.Testing.IAwaitableCallback ThreadSleep(System.Action? callback = null, System.Func? predicate = null); diff --git a/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/DateTimeOffsetMockTests.cs b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/DateTimeOffsetMockTests.cs new file mode 100644 index 000000000..bec75e046 --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/DateTimeOffsetMockTests.cs @@ -0,0 +1,33 @@ +namespace Testably.Abstractions.Testing.Tests.TimeSystem; + +public class DateTimeOffsetMockTests +{ + [Test] + public async Task Now_ShouldUseConfiguredLocalTimeZoneOffset() + { + DateTime utcNow = new(2024, 1, 15, 12, 0, 0, DateTimeKind.Utc); + TimeZoneInfo timeZone = TimeZoneInfo.CreateCustomTimeZone( + "Custom/Plus05", TimeSpan.FromHours(5), "Custom +05", "Custom +05"); + MockTimeSystem timeSystem = new(TimeProviderFactory.Use(utcNow, timeZone)); + + DateTimeOffset now = timeSystem.DateTimeOffset.Now; + + await That(now.Offset).IsEqualTo(TimeSpan.FromHours(5)); + await That(now) + .IsEqualTo(new DateTimeOffset(2024, 1, 15, 17, 0, 0, TimeSpan.FromHours(5))); + } + + [Test] + public async Task UtcNow_ShouldUseZeroOffset() + { + DateTime utcNow = new(2024, 1, 15, 12, 0, 0, DateTimeKind.Utc); + TimeZoneInfo timeZone = TimeZoneInfo.CreateCustomTimeZone( + "Custom/Plus05", TimeSpan.FromHours(5), "Custom +05", "Custom +05"); + MockTimeSystem timeSystem = new(TimeProviderFactory.Use(utcNow, timeZone)); + + DateTimeOffset result = timeSystem.DateTimeOffset.UtcNow; + + await That(result.Offset).IsEqualTo(TimeSpan.Zero); + await That(result).IsEqualTo(new DateTimeOffset(utcNow)); + } +} diff --git a/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/NotificationHandlerTests.cs b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/NotificationHandlerTests.cs index 80ab16421..9bf1a40aa 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/NotificationHandlerTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/NotificationHandlerTests.cs @@ -88,6 +88,73 @@ public async Task OnDateTimeRead_UtcNow_ShouldExecuteCallbackWithCorrectParamete await That(receivedTime).IsEqualTo(expectedTime); } + [Test] + public async Task OnDateTimeOffsetRead_DisposedCallback_ShouldNotBeCalled() + { + DateTime expectedTime = TimeTestHelper.GetRandomTime(DateTimeKind.Local); + MockTimeSystem timeSystem = new(expectedTime); + DateTimeOffset? receivedTime = null; + IDisposable disposable = timeSystem.On.DateTimeOffsetRead(d => receivedTime = d); + + disposable.Dispose(); + _ = timeSystem.DateTimeOffset.Now; + + await That(receivedTime).IsNull(); + } + + [Test] + public async Task OnDateTimeOffsetRead_MultipleCallbacks_DisposeOne_ShouldCallOtherCallbacks() + { + DateTime expectedTime = TimeTestHelper.GetRandomTime(DateTimeKind.Local); + MockTimeSystem timeSystem = new(expectedTime); + DateTimeOffset? receivedTime1 = null; + DateTimeOffset? receivedTime2 = null; + + using (timeSystem.On.DateTimeOffsetRead(d => receivedTime1 = d)) + { + timeSystem.On.DateTimeOffsetRead(d => receivedTime2 = d).Dispose(); + _ = timeSystem.DateTimeOffset.Now; + } + + await That(receivedTime1).IsEqualTo(new DateTimeOffset(expectedTime)); + await That(receivedTime2).IsNull(); + } + + [Test] + public async Task OnDateTimeOffsetRead_MultipleCallbacks_ShouldAllBeCalled() + { + DateTime expectedTime = TimeTestHelper.GetRandomTime(DateTimeKind.Local); + MockTimeSystem timeSystem = new(expectedTime); + DateTimeOffset? receivedTime1 = null; + DateTimeOffset? receivedTime2 = null; + + using (timeSystem.On.DateTimeOffsetRead(d => receivedTime1 = d)) + { + using (timeSystem.On.DateTimeOffsetRead(d => receivedTime2 = d)) + { + _ = timeSystem.DateTimeOffset.Now; + } + } + + await That(receivedTime1).IsEqualTo(new DateTimeOffset(expectedTime)); + await That(receivedTime2).IsEqualTo(new DateTimeOffset(expectedTime)); + } + + [Test] + public async Task OnDateTimeOffsetRead_UtcNow_ShouldExecuteCallbackWithCorrectParameter() + { + DateTime expectedTime = TimeTestHelper.GetRandomTime(DateTimeKind.Utc); + MockTimeSystem timeSystem = new(expectedTime); + DateTimeOffset? receivedTime = null; + + using (timeSystem.On.DateTimeOffsetRead(d => receivedTime = d)) + { + _ = timeSystem.DateTimeOffset.UtcNow; + } + + await That(receivedTime).IsEqualTo(new DateTimeOffset(expectedTime, TimeSpan.Zero)); + } + #if FEATURE_PERIODIC_TIMER [Test] public async Task OnPeriodicTimerWaitingForNextTick_DisposedCallback_ShouldNotBeCalled() diff --git a/Tests/Testably.Abstractions.Tests/TimeSystem/DateTimeOffsetTests.cs b/Tests/Testably.Abstractions.Tests/TimeSystem/DateTimeOffsetTests.cs new file mode 100644 index 000000000..f6d9930dd --- /dev/null +++ b/Tests/Testably.Abstractions.Tests/TimeSystem/DateTimeOffsetTests.cs @@ -0,0 +1,80 @@ +using aweXpect.Chronology; + +namespace Testably.Abstractions.Tests.TimeSystem; + +[TimeSystemTests] +public class DateTimeOffsetTests(TimeSystemTestData testData) : TimeSystemTestBase(testData) +{ + [Test] + public async Task MaxValue_ShouldReturnDefaultValue() + { + DateTimeOffset expectedResult = DateTimeOffset.MaxValue; + + DateTimeOffset result = TimeSystem.DateTimeOffset.MaxValue; + + await That(result).IsEqualTo(expectedResult); + } + + [Test] + public async Task MinValue_ShouldReturnDefaultValue() + { + DateTimeOffset expectedResult = DateTimeOffset.MinValue; + + DateTimeOffset result = TimeSystem.DateTimeOffset.MinValue; + + await That(result).IsEqualTo(expectedResult); + } + + [Test] + public async Task Now_ShouldBeSetToNow() + { + // Tests are brittle on the build system + TimeSpan tolerance = TimeSpan.FromMilliseconds(250); + + DateTimeOffset result = TimeSystem.DateTimeOffset.Now; + DateTimeOffset after = DateTimeOffset.Now; + + await That(result.Offset).IsEqualTo(TimeZoneInfo.Local.GetUtcOffset(result)); + await That(result).IsBetween(new DateTimeOffset(BeforeTime.ToLocalTime())).And(after) + .Within(tolerance); + } + + [Test] + public async Task NowAndUtcNow_ShouldRepresentTheSameInstant() + { + DateTimeOffset utcNowBefore = TimeSystem.DateTimeOffset.UtcNow; + DateTimeOffset now = TimeSystem.DateTimeOffset.Now; + DateTimeOffset utcNowAfter = TimeSystem.DateTimeOffset.UtcNow; + + await That(now.Offset).IsEqualTo(TimeZoneInfo.Local.GetUtcOffset(now)); + await That(now.UtcDateTime) + .IsBetween(utcNowBefore.UtcDateTime).And(utcNowAfter.UtcDateTime) + .Within(50.Milliseconds()); + } + + [Test] + public async Task UnixEpoch_ShouldReturnDefaultValue() + { + #pragma warning disable MA0114 // Use DateTimeOffset.UnixEpoch + DateTimeOffset expectedResult = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); + #pragma warning restore MA0114 + + DateTimeOffset result = TimeSystem.DateTimeOffset.UnixEpoch; + + await That(result).IsEqualTo(expectedResult); + } + + [Test] + public async Task UtcNow_ShouldBeSetToUtcNow() + { + // Tests are brittle on the build system + TimeSpan tolerance = TimeSpan.FromMilliseconds(250); + + DateTimeOffset result = TimeSystem.DateTimeOffset.UtcNow; + DateTimeOffset after = DateTimeOffset.UtcNow; + + await That(result.Offset).IsEqualTo(TimeSpan.Zero); + await That(result).IsBetween(new DateTimeOffset(BeforeTime, TimeSpan.Zero)).And(after) + .Within(tolerance); + } +}