Skip to content

Commit a90eb4f

Browse files
authored
Reopen "Fix DueDate.StringDate conversion to and from DateTime." (#41)
* 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.
1 parent 1f7f320 commit a90eb4f

File tree

6 files changed

+276
-55
lines changed

6 files changed

+276
-55
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System;
2+
using System.Reflection;
3+
4+
namespace Todoist.Net.Tests.Helpers;
5+
6+
/// <summary>
7+
/// A helper class that changes the local timezone to a fake timezone provided
8+
/// at initialization, and resets the original local timezone when disposed.
9+
/// </summary>
10+
/// <remarks>
11+
/// 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.
12+
/// </remarks>
13+
public sealed class FakeLocalTimeZone : IDisposable
14+
{
15+
16+
/// <summary>
17+
/// The fake time zone info that has been set as local.
18+
/// </summary>
19+
public TimeZoneInfo FakeTimeZoneInfo { get; }
20+
21+
22+
/// <summary>
23+
/// Initializes a new instance of the <see cref="FakeLocalTimeZone"/> class.
24+
/// </summary>
25+
private FakeLocalTimeZone(TimeZoneInfo fakeTimeZoneInfo)
26+
{
27+
FakeTimeZoneInfo = fakeTimeZoneInfo;
28+
29+
var info = typeof(TimeZoneInfo).GetField("s_cachedData", BindingFlags.NonPublic | BindingFlags.Static);
30+
var cachedData = info.GetValue(null);
31+
32+
var field = cachedData.GetType().GetField("_localTimeZone",
33+
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Instance);
34+
35+
field.SetValue(cachedData, fakeTimeZoneInfo);
36+
}
37+
38+
public void Dispose()
39+
{
40+
TimeZoneInfo.ClearCachedData();
41+
}
42+
43+
44+
/// <summary>
45+
/// Changes the local time zone to the given <paramref name="fakeTimeZoneInfo"/>.
46+
/// </summary>
47+
/// <remarks>
48+
/// Disposal of the returned object resets the local time zone to the original one.
49+
/// </remarks>
50+
/// <param name="fakeTimeZoneInfo">The time zone to set as local until disposal.</param>
51+
/// <returns>
52+
/// A <see cref="FakeLocalTimeZone"/> instance that represents the time zone change,
53+
/// and used to reset it back to original at disposal.
54+
/// </returns>
55+
public static FakeLocalTimeZone ChangeLocalTimeZone(TimeZoneInfo fakeTimeZoneInfo)
56+
{
57+
return new FakeLocalTimeZone(fakeTimeZoneInfo);
58+
}
59+
60+
/// <summary>
61+
/// Changes the local time zone to a custom time zone with a <paramref name="baseUtcOffset"/>.
62+
/// </summary>
63+
/// <remarks>
64+
/// Disposal of the returned object resets the local time zone to the original one.
65+
/// </remarks>
66+
/// <param name="baseUtcOffset">UTC offset of the custom time zone.</param>
67+
/// <returns>
68+
/// A <see cref="FakeLocalTimeZone"/> instance that represents the time zone change,
69+
/// and used to reset it back to original at disposal.
70+
/// </returns>
71+
public static FakeLocalTimeZone ChangeLocalTimeZone(TimeSpan baseUtcOffset)
72+
{
73+
var fakeId = "Fake TimeZone";
74+
var fakeDisplayName = $"(UTC+{baseUtcOffset:hh':'mm})";
75+
76+
var fakeTimeZoneInfo = TimeZoneInfo.CreateCustomTimeZone(fakeId, baseUtcOffset, fakeDisplayName, fakeDisplayName);
77+
78+
return new FakeLocalTimeZone(fakeTimeZoneInfo);
79+
}
80+
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Linq;
3+
4+
using Todoist.Net.Tests.Extensions;
5+
6+
using Xunit;
7+
8+
namespace Todoist.Net.Tests.Helpers;
9+
10+
[Trait(Constants.TraitName, Constants.UnitTraitValue)]
11+
public class FakeLocalTimeZoneTests
12+
{
13+
14+
[Fact]
15+
public void FakeLocalTimeZone_ShouldChangeLocalTimeZoneWithinScope_AndResetItBackOutsideScope()
16+
{
17+
var fakeTimeZoneOffset = TimeZoneInfo.Local.BaseUtcOffset + TimeSpan.FromHours(2);
18+
var fakeLocalTimeZone = FakeLocalTimeZone.ChangeLocalTimeZone(fakeTimeZoneOffset);
19+
20+
using (fakeLocalTimeZone)
21+
{
22+
Assert.Equal(fakeLocalTimeZone.FakeTimeZoneInfo, TimeZoneInfo.Local);
23+
}
24+
Assert.NotEqual(fakeLocalTimeZone.FakeTimeZoneInfo, TimeZoneInfo.Local);
25+
}
26+
27+
}

src/Todoist.Net.Tests/Models/DueDateTests.cs

+66-8
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,35 @@
22

33
using Todoist.Net.Models;
44
using Todoist.Net.Tests.Extensions;
5+
using Todoist.Net.Tests.Helpers;
6+
57
using Xunit;
68

79
namespace Todoist.Net.Tests.Models
810
{
911
[Trait(Constants.TraitName, Constants.UnitTraitValue)]
10-
public class DueDateTests
12+
public class DueDateTests : IDisposable
1113
{
14+
15+
private readonly FakeLocalTimeZone _fakeLocalTimeZone;
16+
17+
public DueDateTests()
18+
{
19+
_fakeLocalTimeZone = FakeLocalTimeZone.ChangeLocalTimeZone(TimeSpan.FromHours(5));
20+
}
21+
22+
public void Dispose()
23+
{
24+
_fakeLocalTimeZone?.Dispose();
25+
}
26+
27+
1228
[Fact]
1329
public void DateTimeAssignment_FullDayEvent_Success()
1430
{
15-
var date = new DateTime(2018, 2, 5, 0, 0, 0, DateTimeKind.Utc);
31+
var date = new DateTime(2018, 2, 5);
1632

17-
var dueDate = new DueDate(date, true);
33+
var dueDate = DueDate.CreateFullDay(date);
1834

1935
Assert.Equal("2018-02-05", dueDate.StringDate);
2036
Assert.True(dueDate.IsFullDay);
@@ -23,23 +39,65 @@ public void DateTimeAssignment_FullDayEvent_Success()
2339
[Fact]
2440
public void DateTimeAssignment_FloatingDueDateEvent_Success()
2541
{
26-
var date = new DateTime(2018, 2, 5, 0, 0, 0, DateTimeKind.Utc);
42+
var date = new DateTime(2018, 2, 5);
2743

28-
var dueDate = new DueDate(date);
44+
var dueDate = DueDate.CreateFloating(date);
2945

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

3450
[Fact]
35-
public void DateTimeAssignment_FloatingDueDateWithTimezoneEvent_Success()
51+
public void DateTimeAssignment_FixedTimeZoneDueDateEvent_Success()
3652
{
37-
var date = new DateTime(2018, 2, 5, 0, 0, 0, DateTimeKind.Utc);
53+
var date = new DateTime(2018, 2, 5);
3854

39-
var dueDate = new DueDate(date, false, "Asia/Jakarta");
55+
var dueDate = DueDate.CreateFixedTimeZone(date, "Asia/Jakarta");
4056

4157
Assert.Equal("2018-02-05T00:00:00Z", dueDate.StringDate);
4258
Assert.False(dueDate.IsFullDay);
4359
}
60+
61+
62+
[Fact]
63+
public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFullDayDate()
64+
{
65+
var dueDate = new DueDate();
66+
string initialValue = "2016-12-01";
67+
68+
dueDate.StringDate = initialValue;
69+
string returnedValue = dueDate.StringDate;
70+
dueDate.StringDate = returnedValue;
71+
72+
Assert.Equal(returnedValue, dueDate.StringDate);
73+
}
74+
75+
[Fact]
76+
public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFloatingDate()
77+
{
78+
var dueDate = new DueDate();
79+
string initialValue = "2016-12-03T12:00:00";
80+
81+
dueDate.StringDate = initialValue;
82+
string returnedValue = dueDate.StringDate;
83+
dueDate.StringDate = returnedValue;
84+
85+
Assert.Equal(returnedValue, dueDate.StringDate);
86+
}
87+
88+
[Fact]
89+
public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFixedDate()
90+
{
91+
var dueDate = new DueDate();
92+
string initialValue = "2016-12-06T13:00:00Z";
93+
94+
dueDate.StringDate = initialValue;
95+
dueDate.Timezone = "Asia/Jakarta";
96+
97+
string returnedValue = dueDate.StringDate;
98+
dueDate.StringDate = returnedValue;
99+
100+
Assert.Equal(returnedValue, dueDate.StringDate);
101+
}
44102
}
45103
}

src/Todoist.Net.Tests/Services/ItemsServiceTests.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public void CreateItemClearDueDateAndDelete_Success()
8989
{
9090
var client = TodoistClientFactory.Create(_outputHelper);
9191

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

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

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

136-
item.DueDate = new DueDate("every fri");
136+
item.DueDate = DueDate.FromText("every fri");
137137
await client.Items.UpdateAsync(item);
138138

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

163163
Assert.NotNull(item);
164164

165-
client.Items.CompleteRecurringAsync(new CompleteRecurringItemArgument(item.Id, new DueDate(DateTime.UtcNow.AddMonths(1)))).Wait();
165+
client.Items.CompleteRecurringAsync(new CompleteRecurringItemArgument(item.Id, DueDate.CreateFloating(DateTime.UtcNow.AddMonths(1)))).Wait();
166166
client.Items.CompleteRecurringAsync(item.Id).Wait();
167167

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

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

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

197-
Assert.Equal(item.DueDate.Date, itemInfo.Item.DueDate.Date?.ToLocalTime());
197+
Assert.Equal(item.DueDate.Date, itemInfo.Item.DueDate.Date);
198198

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

209209
var item = new Item("duration task")
210210
{
211-
DueDate = new DueDate("22 Dec 2021 at 9:15", language: Language.English),
211+
DueDate = DueDate.FromText("22 Dec 2021 at 9:15", Language.English),
212212
Duration = new Duration(45, DurationUnit.Minute)
213213
};
214214
client.Items.AddAsync(item).Wait();

src/Todoist.Net.Tests/Services/ReminersServiceTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public async Task CreateDelete_Success()
3030

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

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

5151
var item = new Item("Test")
5252
{
53-
DueDate = new DueDate(DateTime.UtcNow.AddDays(1))
53+
DueDate = DueDate.CreateFloating(DateTime.UtcNow.AddDays(1))
5454
};
5555

5656
var taskId = await client.Items.AddAsync(item).ConfigureAwait(false);

0 commit comments

Comments
 (0)