Skip to content

Commit

Permalink
Reopen "Fix DueDate.StringDate conversion to and from DateTime." (#41)
Browse files Browse the repository at this point in the history
* Added FakeLocalTimeZone helper class.
The class is used to change the time zone of the tests upon initialization, and resets it back when disposed.

* Added more tests for DueDate model.
The added tests ensure consistency in StringDate property setter and getter.

* Changed date formatting in DueDate.StringDate property.
Date is now converted using the round-trip format without milliseconds.
Date is also no longer being converted to universal time before formatting.

* Made a simple change in floating due date test.
DateTimeKind is switched to Unspecified instead of Utc since the due date should be treated as floating.

* Fixed date convertion in `DueDate.StringDate` getter.
The fix reverts back to depending on `DueDate.Timezone` property to determine the conversion formatting.
However, it doesn't convert the `DueDate.Date` value to universal time unless the `DueDate` is intended to be fixed.

* Made a simple fix to the "fixed timezone" `DueDate` test.
Timezone info was missing for a fixed date.

* Fixed CreateNewItem_DueDateIsLocal_DueDateNotChanged test.
Since floating due dates are now converted to `Unspecified` date time, the method `ToLocalTime` is no longer used because it assumes the given date as `UTC`.

* Replaced DueDate public constructors with static methods.
Methods include detailed documentation comments.

* Removed redundant checks in DueDate.StringDate getter.
Static methods that are used to instantiate DueDates are now in charge of handling different kinds of dates.

* Updated tests with new DueDate static methods.

* Remove randomization in tests.
Fake time zone random selection is replaced by pre-determined custom offset.

* Remove unnecessary comments.

* Removed unnecessary SuppressFinalize call in DueDateTests.
  • Loading branch information
AhmedZaki99 authored Nov 1, 2023
1 parent 1f7f320 commit a90eb4f
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 55 deletions.
81 changes: 81 additions & 0 deletions src/Todoist.Net.Tests/Helpers/FakeLocalTimeZone.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Reflection;

namespace Todoist.Net.Tests.Helpers;

/// <summary>
/// A helper class that changes the local timezone to a fake timezone provided
/// at initialization, and resets the original local timezone when disposed.
/// </summary>
/// <remarks>
/// See this <see href="https://stackoverflow.com/questions/44413407/mock-the-country-timezone-you-are-running-unit-test-from">SO question</see> for more details.
/// </remarks>
public sealed class FakeLocalTimeZone : IDisposable
{

/// <summary>
/// The fake time zone info that has been set as local.
/// </summary>
public TimeZoneInfo FakeTimeZoneInfo { get; }


/// <summary>
/// Initializes a new instance of the <see cref="FakeLocalTimeZone"/> class.
/// </summary>
private FakeLocalTimeZone(TimeZoneInfo fakeTimeZoneInfo)
{
FakeTimeZoneInfo = fakeTimeZoneInfo;

var info = typeof(TimeZoneInfo).GetField("s_cachedData", BindingFlags.NonPublic | BindingFlags.Static);
var cachedData = info.GetValue(null);

var field = cachedData.GetType().GetField("_localTimeZone",
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Instance);

field.SetValue(cachedData, fakeTimeZoneInfo);
}

public void Dispose()
{
TimeZoneInfo.ClearCachedData();
}


/// <summary>
/// Changes the local time zone to the given <paramref name="fakeTimeZoneInfo"/>.
/// </summary>
/// <remarks>
/// Disposal of the returned object resets the local time zone to the original one.
/// </remarks>
/// <param name="fakeTimeZoneInfo">The time zone to set as local until disposal.</param>
/// <returns>
/// A <see cref="FakeLocalTimeZone"/> instance that represents the time zone change,
/// and used to reset it back to original at disposal.
/// </returns>
public static FakeLocalTimeZone ChangeLocalTimeZone(TimeZoneInfo fakeTimeZoneInfo)
{
return new FakeLocalTimeZone(fakeTimeZoneInfo);
}

/// <summary>
/// Changes the local time zone to a custom time zone with a <paramref name="baseUtcOffset"/>.
/// </summary>
/// <remarks>
/// Disposal of the returned object resets the local time zone to the original one.
/// </remarks>
/// <param name="baseUtcOffset">UTC offset of the custom time zone.</param>
/// <returns>
/// A <see cref="FakeLocalTimeZone"/> instance that represents the time zone change,
/// and used to reset it back to original at disposal.
/// </returns>
public static FakeLocalTimeZone ChangeLocalTimeZone(TimeSpan baseUtcOffset)
{
var fakeId = "Fake TimeZone";
var fakeDisplayName = $"(UTC+{baseUtcOffset:hh':'mm})";

var fakeTimeZoneInfo = TimeZoneInfo.CreateCustomTimeZone(fakeId, baseUtcOffset, fakeDisplayName, fakeDisplayName);

return new FakeLocalTimeZone(fakeTimeZoneInfo);
}

}
27 changes: 27 additions & 0 deletions src/Todoist.Net.Tests/Helpers/FakeLocalTimeZoneTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Linq;

using Todoist.Net.Tests.Extensions;

using Xunit;

namespace Todoist.Net.Tests.Helpers;

[Trait(Constants.TraitName, Constants.UnitTraitValue)]
public class FakeLocalTimeZoneTests
{

[Fact]
public void FakeLocalTimeZone_ShouldChangeLocalTimeZoneWithinScope_AndResetItBackOutsideScope()
{
var fakeTimeZoneOffset = TimeZoneInfo.Local.BaseUtcOffset + TimeSpan.FromHours(2);
var fakeLocalTimeZone = FakeLocalTimeZone.ChangeLocalTimeZone(fakeTimeZoneOffset);

using (fakeLocalTimeZone)
{
Assert.Equal(fakeLocalTimeZone.FakeTimeZoneInfo, TimeZoneInfo.Local);
}
Assert.NotEqual(fakeLocalTimeZone.FakeTimeZoneInfo, TimeZoneInfo.Local);
}

}
74 changes: 66 additions & 8 deletions src/Todoist.Net.Tests/Models/DueDateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,35 @@

using Todoist.Net.Models;
using Todoist.Net.Tests.Extensions;
using Todoist.Net.Tests.Helpers;

using Xunit;

namespace Todoist.Net.Tests.Models
{
[Trait(Constants.TraitName, Constants.UnitTraitValue)]
public class DueDateTests
public class DueDateTests : IDisposable
{

private readonly FakeLocalTimeZone _fakeLocalTimeZone;

public DueDateTests()
{
_fakeLocalTimeZone = FakeLocalTimeZone.ChangeLocalTimeZone(TimeSpan.FromHours(5));
}

public void Dispose()
{
_fakeLocalTimeZone?.Dispose();
}


[Fact]
public void DateTimeAssignment_FullDayEvent_Success()
{
var date = new DateTime(2018, 2, 5, 0, 0, 0, DateTimeKind.Utc);
var date = new DateTime(2018, 2, 5);

var dueDate = new DueDate(date, true);
var dueDate = DueDate.CreateFullDay(date);

Assert.Equal("2018-02-05", dueDate.StringDate);
Assert.True(dueDate.IsFullDay);
Expand All @@ -23,23 +39,65 @@ public void DateTimeAssignment_FullDayEvent_Success()
[Fact]
public void DateTimeAssignment_FloatingDueDateEvent_Success()
{
var date = new DateTime(2018, 2, 5, 0, 0, 0, DateTimeKind.Utc);
var date = new DateTime(2018, 2, 5);

var dueDate = new DueDate(date);
var dueDate = DueDate.CreateFloating(date);

Assert.Equal("2018-02-05T00:00:00", dueDate.StringDate);
Assert.False(dueDate.IsFullDay);
}

[Fact]
public void DateTimeAssignment_FloatingDueDateWithTimezoneEvent_Success()
public void DateTimeAssignment_FixedTimeZoneDueDateEvent_Success()
{
var date = new DateTime(2018, 2, 5, 0, 0, 0, DateTimeKind.Utc);
var date = new DateTime(2018, 2, 5);

var dueDate = new DueDate(date, false, "Asia/Jakarta");
var dueDate = DueDate.CreateFixedTimeZone(date, "Asia/Jakarta");

Assert.Equal("2018-02-05T00:00:00Z", dueDate.StringDate);
Assert.False(dueDate.IsFullDay);
}


[Fact]
public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFullDayDate()
{
var dueDate = new DueDate();
string initialValue = "2016-12-01";

dueDate.StringDate = initialValue;
string returnedValue = dueDate.StringDate;
dueDate.StringDate = returnedValue;

Assert.Equal(returnedValue, dueDate.StringDate);
}

[Fact]
public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFloatingDate()
{
var dueDate = new DueDate();
string initialValue = "2016-12-03T12:00:00";

dueDate.StringDate = initialValue;
string returnedValue = dueDate.StringDate;
dueDate.StringDate = returnedValue;

Assert.Equal(returnedValue, dueDate.StringDate);
}

[Fact]
public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFixedDate()
{
var dueDate = new DueDate();
string initialValue = "2016-12-06T13:00:00Z";

dueDate.StringDate = initialValue;
dueDate.Timezone = "Asia/Jakarta";

string returnedValue = dueDate.StringDate;
dueDate.StringDate = returnedValue;

Assert.Equal(returnedValue, dueDate.StringDate);
}
}
}
14 changes: 7 additions & 7 deletions src/Todoist.Net.Tests/Services/ItemsServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public void CreateItemClearDueDateAndDelete_Success()
{
var client = TodoistClientFactory.Create(_outputHelper);

var item = new Item("demo task") { DueDate = new DueDate("22 Dec 2021", language: Language.English) };
var item = new Item("demo task") { DueDate = DueDate.FromText("22 Dec 2021", Language.English) };
client.Items.AddAsync(item).Wait();

var itemInfo = client.Items.GetAsync(item.Id).Result;
Expand All @@ -113,7 +113,7 @@ public void CreateItem_InvalidPDueDate_ThrowsException()
{
var client = TodoistClientFactory.Create(_outputHelper);
var item = new Item("bad task");
item.DueDate = new DueDate("Invalid date string");
item.DueDate = DueDate.FromText("Invalid date string");

var aggregateException = Assert.ThrowsAsync<AggregateException>(
async () =>
Expand All @@ -133,7 +133,7 @@ public async Task MoveItemsToProject_Success()
var item = new Item("demo task");
client.Items.AddAsync(item).Wait();

item.DueDate = new DueDate("every fri");
item.DueDate = DueDate.FromText("every fri");
await client.Items.UpdateAsync(item);

var project = new Project(Guid.NewGuid().ToString());
Expand Down Expand Up @@ -162,7 +162,7 @@ public void QuickAddAsync_Success()

Assert.NotNull(item);

client.Items.CompleteRecurringAsync(new CompleteRecurringItemArgument(item.Id, new DueDate(DateTime.UtcNow.AddMonths(1)))).Wait();
client.Items.CompleteRecurringAsync(new CompleteRecurringItemArgument(item.Id, DueDate.CreateFloating(DateTime.UtcNow.AddMonths(1)))).Wait();
client.Items.CompleteRecurringAsync(item.Id).Wait();

client.Items.DeleteAsync(item.Id).Wait();
Expand All @@ -189,12 +189,12 @@ public void CreateNewItem_DueDateIsLocal_DueDateNotChanged()
{
var client = TodoistClientFactory.Create(_outputHelper);

var item = new Item("New task") { DueDate = new DueDate(DateTime.Now.AddYears(1).Date) };
var item = new Item("New task") { DueDate = DueDate.CreateFloating(DateTime.Now.AddYears(1).Date) };
var taskId = client.Items.AddAsync(item).Result;

var itemInfo = client.Items.GetAsync(taskId).Result;

Assert.Equal(item.DueDate.Date, itemInfo.Item.DueDate.Date?.ToLocalTime());
Assert.Equal(item.DueDate.Date, itemInfo.Item.DueDate.Date);

client.Items.DeleteAsync(item.Id).Wait();
}
Expand All @@ -208,7 +208,7 @@ public void CreateItemClearDurationAndDelete_Success()

var item = new Item("duration task")
{
DueDate = new DueDate("22 Dec 2021 at 9:15", language: Language.English),
DueDate = DueDate.FromText("22 Dec 2021 at 9:15", Language.English),
Duration = new Duration(45, DurationUnit.Minute)
};
client.Items.AddAsync(item).Wait();
Expand Down
4 changes: 2 additions & 2 deletions src/Todoist.Net.Tests/Services/ReminersServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public async Task CreateDelete_Success()

var itemId = await transaction.Items.AddAsync(new Item("Temp")).ConfigureAwait(false);
var reminderId =
await transaction.Reminders.AddAsync(new Reminder(itemId) { DueDate = new DueDate(DateTime.UtcNow.AddDays(1)) }).ConfigureAwait(false);
await transaction.Reminders.AddAsync(new Reminder(itemId) { DueDate = DueDate.CreateFloating(DateTime.UtcNow.AddDays(1)) }).ConfigureAwait(false);
await transaction.CommitAsync().ConfigureAwait(false);

var reminders = await client.Reminders.GetAsync().ConfigureAwait(false);
Expand All @@ -50,7 +50,7 @@ public async Task AddRelativeReminder_Success()

var item = new Item("Test")
{
DueDate = new DueDate(DateTime.UtcNow.AddDays(1))
DueDate = DueDate.CreateFloating(DateTime.UtcNow.AddDays(1))
};

var taskId = await client.Items.AddAsync(item).ConfigureAwait(false);
Expand Down
Loading

0 comments on commit a90eb4f

Please sign in to comment.