From 40c93eeb9cb5e0b129e92191611cc9947e17fd4b Mon Sep 17 00:00:00 2001 From: Ahmed Zaki Date: Thu, 5 Oct 2023 19:27:21 +0300 Subject: [PATCH 01/13] Added FakeLocalTimeZone helper class. The class is used to change the time zone of the tests upon initialization, and resets it back when disposed. --- .../Helpers/FakeLocalTimeZone.cs | 37 +++++++++++++++++++ .../Helpers/FakeLocalTimeZoneTests.cs | 37 +++++++++++++++++++ src/Todoist.Net.Tests/Models/DueDateTests.cs | 24 +++++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/Todoist.Net.Tests/Helpers/FakeLocalTimeZone.cs create mode 100644 src/Todoist.Net.Tests/Helpers/FakeLocalTimeZoneTests.cs diff --git a/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZone.cs b/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZone.cs new file mode 100644 index 0000000..6f662e2 --- /dev/null +++ b/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZone.cs @@ -0,0 +1,37 @@ +using System; +using System.Reflection; + +namespace Todoist.Net.Tests.Helpers; + +/// +/// A helper class that changes the local timezone to a fake timezone provided +/// at initialization, and resets the original local timezone when disposed. +/// +/// +/// See this SO question for more details. +/// +public sealed class FakeLocalTimeZone : IDisposable +{ + + /// + /// Initializes a new instance of the class + /// and changes the local time zone to the given + /// until it's disposed. + /// + /// The time zone to set as local until disposal. + public FakeLocalTimeZone(TimeZoneInfo 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(); + } +} diff --git a/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZoneTests.cs b/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZoneTests.cs new file mode 100644 index 0000000..aba5278 --- /dev/null +++ b/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZoneTests.cs @@ -0,0 +1,37 @@ +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 actualTimeZoneInfo = TimeZoneInfo.Local; + + var timeZoneCollection = TimeZoneInfo + .GetSystemTimeZones() + .Where(t => !actualTimeZoneInfo.Equals(t)) + .ToArray(); + + var randomIndex = new Random().Next(timeZoneCollection.Length); + var fakeTimeZoneInfo = timeZoneCollection.ElementAt(randomIndex); + + + Assert.NotEqual(fakeTimeZoneInfo, actualTimeZoneInfo); + + using (var fakeLocalTimeZone = new FakeLocalTimeZone(fakeTimeZoneInfo)) + { + Assert.Equal(fakeTimeZoneInfo, TimeZoneInfo.Local); + } + Assert.Equal(actualTimeZoneInfo, TimeZoneInfo.Local); + } + +} diff --git a/src/Todoist.Net.Tests/Models/DueDateTests.cs b/src/Todoist.Net.Tests/Models/DueDateTests.cs index 359ce82..27fa6a1 100644 --- a/src/Todoist.Net.Tests/Models/DueDateTests.cs +++ b/src/Todoist.Net.Tests/Models/DueDateTests.cs @@ -2,13 +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() + { + var timeZoneCollection = System.TimeZoneInfo.GetSystemTimeZones(); + + var randomIndex = new Random().Next(timeZoneCollection.Count); + var fakeTimeZoneInfo = timeZoneCollection[randomIndex]; + + _fakeLocalTimeZone = new FakeLocalTimeZone(fakeTimeZoneInfo); + } + + public void Dispose() + { + _fakeLocalTimeZone.Dispose(); + GC.SuppressFinalize(this); + } + + [Fact] public void DateTimeAssignment_FullDayEvent_Success() { From 3764d047bff1c80c502cbb40d9506bc885a599dc Mon Sep 17 00:00:00 2001 From: Ahmed Zaki Date: Thu, 5 Oct 2023 19:32:09 +0300 Subject: [PATCH 02/13] Added more tests for DueDate model. The added tests ensure consistency in StringDate property setter and getter. --- src/Todoist.Net.Tests/Models/DueDateTests.cs | 49 ++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/Todoist.Net.Tests/Models/DueDateTests.cs b/src/Todoist.Net.Tests/Models/DueDateTests.cs index 27fa6a1..b9fb739 100644 --- a/src/Todoist.Net.Tests/Models/DueDateTests.cs +++ b/src/Todoist.Net.Tests/Models/DueDateTests.cs @@ -63,5 +63,54 @@ public void DateTimeAssignment_FloatingDueDateWithTimezoneEvent_Success() Assert.Equal("2018-02-05T00:00:00Z", dueDate.StringDate); Assert.False(dueDate.IsFullDay); } + + + [Fact] + public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFullDayDate() + { + // Arrange + var dueDate = new DueDate(); + string initialValue = "2016-12-01"; + + // Act + dueDate.StringDate = initialValue; // Set initial value. + string returnedValue = dueDate.StringDate; // Get. + dueDate.StringDate = returnedValue; // Set returned value. + + // Assert + Assert.Equal(returnedValue, dueDate.StringDate); + } + + [Fact] + public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFloatingDate() + { + // Arrange + var dueDate = new DueDate(); + string initialValue = "2016-12-03T12:00:00"; + + // Act + dueDate.StringDate = initialValue; // Set initial value. + string returnedValue = dueDate.StringDate; // Get. + dueDate.StringDate = returnedValue; // Set returned value. + + // Assert + Assert.Equal(returnedValue, dueDate.StringDate); + } + + [Fact] + public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFixedDate() + { + // Arrange + var dueDate = new DueDate(); + string initialValue = "2016-12-06T13:00:00Z"; + + // Act + dueDate.StringDate = initialValue; // Set initial value. + string returnedValue = dueDate.StringDate; // Get. + dueDate.StringDate = returnedValue; // Set returned value. + + // Assert + Assert.Equal(returnedValue, dueDate.StringDate); + } } } From 83dbd3b3450f30d4358e9e211b2a198972ee2f0c Mon Sep 17 00:00:00 2001 From: Ahmed Zaki Date: Fri, 6 Oct 2023 21:32:50 +0300 Subject: [PATCH 03/13] 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. --- src/Todoist.Net/Models/DueDate.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Todoist.Net/Models/DueDate.cs b/src/Todoist.Net/Models/DueDate.cs index ea43054..8b044fa 100644 --- a/src/Todoist.Net/Models/DueDate.cs +++ b/src/Todoist.Net/Models/DueDate.cs @@ -10,6 +10,7 @@ namespace Todoist.Net.Models /// public class DueDate { + private const string DefaultEventDateFormat = "yyyy-MM-ddTHH:mm:ssK"; // Roundtrip without milliseconds. private const string FullDayEventDateFormat = "yyyy-MM-dd"; /// @@ -124,13 +125,7 @@ internal string StringDate return Date.Value.ToString(FullDayEventDateFormat); } - var date = Date.Value.ToUniversalTime(); - if (string.IsNullOrEmpty(Timezone)) - { - return date.ToString("s"); - } - - return date.ToString("s") + "Z"; + return Date.Value.ToString(DefaultEventDateFormat); } set @@ -143,7 +138,7 @@ internal string StringDate return; } - Date = DateTime.Parse(value, CultureInfo.InvariantCulture); + Date = DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); } } } From 8cbdaa849c8099f829a5f361b234df8661eafe59 Mon Sep 17 00:00:00 2001 From: Ahmed Zaki Date: Fri, 6 Oct 2023 21:34:40 +0300 Subject: [PATCH 04/13] 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. --- src/Todoist.Net.Tests/Models/DueDateTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Todoist.Net.Tests/Models/DueDateTests.cs b/src/Todoist.Net.Tests/Models/DueDateTests.cs index b9fb739..1a6b958 100644 --- a/src/Todoist.Net.Tests/Models/DueDateTests.cs +++ b/src/Todoist.Net.Tests/Models/DueDateTests.cs @@ -45,7 +45,7 @@ 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, 0, 0, 0, DateTimeKind.Unspecified); var dueDate = new DueDate(date); From 075b3a827ed28b2ef4843c2f5d2cee8921ab5cf5 Mon Sep 17 00:00:00 2001 From: Ahmed Zaki Date: Fri, 27 Oct 2023 18:47:21 +0200 Subject: [PATCH 05/13] 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. --- src/Todoist.Net/Models/DueDate.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Todoist.Net/Models/DueDate.cs b/src/Todoist.Net/Models/DueDate.cs index 8b044fa..d9b9b35 100644 --- a/src/Todoist.Net/Models/DueDate.cs +++ b/src/Todoist.Net/Models/DueDate.cs @@ -10,8 +10,9 @@ namespace Todoist.Net.Models /// public class DueDate { - private const string DefaultEventDateFormat = "yyyy-MM-ddTHH:mm:ssK"; // Roundtrip without milliseconds. private const string FullDayEventDateFormat = "yyyy-MM-dd"; + private const string FloatingEventDateFormat = "yyyy-MM-ddTHH:mm:ss"; + private const string FixedEventDateFormat = "yyyy-MM-ddTHH:mm:ssZ"; /// /// Initializes a new instance of the class. @@ -125,7 +126,17 @@ internal string StringDate return Date.Value.ToString(FullDayEventDateFormat); } - return Date.Value.ToString(DefaultEventDateFormat); + if (string.IsNullOrEmpty(Timezone)) + { + return Date.Value.ToString(FloatingEventDateFormat); + } + + if (Date.Value.Kind == DateTimeKind.Local) + { + return Date.Value.ToUniversalTime().ToString(FixedEventDateFormat); + } + + return Date.Value.ToString(FixedEventDateFormat); // Unspecified dates are assuemd UTC. } set From 45156623bda571cb6dc9e209366868f560d29133 Mon Sep 17 00:00:00 2001 From: Ahmed Zaki Date: Fri, 27 Oct 2023 19:57:14 +0200 Subject: [PATCH 06/13] Made a simple fix to the "fixed timezone" `DueDate` test. Timezone info was missing for a fixed date. --- src/Todoist.Net.Tests/Models/DueDateTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Todoist.Net.Tests/Models/DueDateTests.cs b/src/Todoist.Net.Tests/Models/DueDateTests.cs index 1a6b958..5eca17a 100644 --- a/src/Todoist.Net.Tests/Models/DueDateTests.cs +++ b/src/Todoist.Net.Tests/Models/DueDateTests.cs @@ -106,6 +106,8 @@ public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFixedDa // Act dueDate.StringDate = initialValue; // Set initial value. + dueDate.Timezone = "Asia/Jakarta"; // Set fixed timezone. + string returnedValue = dueDate.StringDate; // Get. dueDate.StringDate = returnedValue; // Set returned value. From 6577617a02e60aafd898884c9cfccee14e66f5fd Mon Sep 17 00:00:00 2001 From: Ahmed Zaki Date: Fri, 27 Oct 2023 20:00:40 +0200 Subject: [PATCH 07/13] 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`. --- src/Todoist.Net.Tests/Services/ItemsServiceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Todoist.Net.Tests/Services/ItemsServiceTests.cs b/src/Todoist.Net.Tests/Services/ItemsServiceTests.cs index 68eba51..4e86513 100644 --- a/src/Todoist.Net.Tests/Services/ItemsServiceTests.cs +++ b/src/Todoist.Net.Tests/Services/ItemsServiceTests.cs @@ -193,7 +193,7 @@ public void CreateNewItem_DueDateIsLocal_DueDateNotChanged() 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(); } From d0e6118e8ebe018d8c3f47d587ce7dd0095ba6cf Mon Sep 17 00:00:00 2001 From: Ahmed Zaki Date: Fri, 27 Oct 2023 23:14:33 +0200 Subject: [PATCH 08/13] Replaced DueDate public constructors with static methods. Methods include detailed documentation comments. --- src/Todoist.Net/Models/DueDate.cs | 120 ++++++++++++++++++++++-------- 1 file changed, 90 insertions(+), 30 deletions(-) diff --git a/src/Todoist.Net/Models/DueDate.cs b/src/Todoist.Net/Models/DueDate.cs index d9b9b35..84414d5 100644 --- a/src/Todoist.Net/Models/DueDate.cs +++ b/src/Todoist.Net/Models/DueDate.cs @@ -14,36 +14,6 @@ public class DueDate private const string FloatingEventDateFormat = "yyyy-MM-ddTHH:mm:ss"; private const string FixedEventDateFormat = "yyyy-MM-ddTHH:mm:ssZ"; - /// - /// Initializes a new instance of the class. - /// - /// The date time. - /// if set to true then it's a full day event. - /// The timezone. - public DueDate(DateTime date, bool isFullDay = false, string timezone = null) - { - Date = date; - IsFullDay = isFullDay; - Timezone = timezone; - } - - /// - /// Initializes a new instance of the class. - /// - /// The text (every day; each monday) - /// The date time. this can be used with 'text' parameter to create a recurring from specific date - /// if set to true then it's a full day event. - /// The timezone. - /// The language. - public DueDate(string text, DateTime? date = null, bool isFullDay = false, string timezone = null, Language language = null) - { - Text = text; - Date = date; - IsFullDay = isFullDay; - Timezone = timezone; - Language = language; - } - internal DueDate() { } @@ -152,5 +122,95 @@ internal string StringDate Date = DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); } } + + + /// + /// Creates a new instance of from text (every day; each Monday). + /// + /// + /// See Todoist documentation for more information. + /// + /// The human-readable representation of due date (every day; each Monday) + /// The language of the text. + /// The created instance. + public static DueDate FromText(string text, Language language = null) => new DueDate + { + Text = text, + Language = language + }; + + /// + /// Creates a full-day in the format of YYYY-MM-DD (RFC 3339). + /// + /// The full-day date. + /// + /// + /// Only the date component of the given is used, and any time data is truncated. + /// + /// + /// See Todoist documentation for more information. + /// + /// + /// The created instance. + public static DueDate CreateFullDay(DateTime date) => new DueDate + { + Date = date.Date, + IsFullDay = true + }; + + /// + /// Creates a floating in the format of YYYY-MM-DDTHH:MM:SS. + /// + /// The floating date. + /// + /// + /// The given is treated as , and any timezone data is truncated. + /// + /// + /// Floating due dates always represent an event in the current user's timezone. + ///
+ /// Note that it's not quite compatible with RFC 3339, + /// because the concept of timezone is not applicable to this object. + ///
+ /// + /// See Todoist documentation for more information. + /// + ///
+ /// The created instance. + public static DueDate CreateFloating(DateTime dateTime) => new DueDate + { + Date = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified), // Floating dates are unspecified. + IsFullDay = false + }; + + /// + /// Creates a fixed timezone in the format of YYYY-MM-DDTHH:MM:SSZ (RFC 3339). + /// + /// The fixed timezone date. + /// The timezone of the due instance. + /// + /// + /// Fixed due date is stored in UTC, hence, of kind is assumed UTC, + ///
and of kind is converted to UTC based on the system timezone. + ///
+ /// + /// See Todoist documentation for more information. + /// + ///
+ /// The created instance. + public static DueDate CreateFixedTimeZone(DateTime dateTime, string timezone) + { + if (dateTime.Kind == DateTimeKind.Unspecified) + { + dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); // Unspecified dates are assumed UTC. + } + return new DueDate + { + Date = dateTime.ToUniversalTime(), // Local dates are converted to UTC. + IsFullDay = false, + Timezone = timezone + }; + } + } } From b4fb225c384ec7f49957239339a7ffff902893cf Mon Sep 17 00:00:00 2001 From: Ahmed Zaki Date: Fri, 27 Oct 2023 23:17:51 +0200 Subject: [PATCH 09/13] 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. --- src/Todoist.Net/Models/DueDate.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/Todoist.Net/Models/DueDate.cs b/src/Todoist.Net/Models/DueDate.cs index 84414d5..dc57918 100644 --- a/src/Todoist.Net/Models/DueDate.cs +++ b/src/Todoist.Net/Models/DueDate.cs @@ -11,8 +11,7 @@ namespace Todoist.Net.Models public class DueDate { private const string FullDayEventDateFormat = "yyyy-MM-dd"; - private const string FloatingEventDateFormat = "yyyy-MM-ddTHH:mm:ss"; - private const string FixedEventDateFormat = "yyyy-MM-ddTHH:mm:ssZ"; + private const string DefaultEventDateFormat = "yyyy-MM-ddTHH:mm:ssK"; internal DueDate() { @@ -96,17 +95,7 @@ internal string StringDate return Date.Value.ToString(FullDayEventDateFormat); } - if (string.IsNullOrEmpty(Timezone)) - { - return Date.Value.ToString(FloatingEventDateFormat); - } - - if (Date.Value.Kind == DateTimeKind.Local) - { - return Date.Value.ToUniversalTime().ToString(FixedEventDateFormat); - } - - return Date.Value.ToString(FixedEventDateFormat); // Unspecified dates are assuemd UTC. + return Date.Value.ToString(DefaultEventDateFormat); } set From faffb7c4a0a41fb1239c7b6f806be4cc01c37c11 Mon Sep 17 00:00:00 2001 From: Ahmed Zaki Date: Fri, 27 Oct 2023 23:19:11 +0200 Subject: [PATCH 10/13] Updated tests with new DueDate static methods. --- src/Todoist.Net.Tests/Models/DueDateTests.cs | 14 +++++++------- .../Services/ItemsServiceTests.cs | 12 ++++++------ .../Services/ReminersServiceTests.cs | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Todoist.Net.Tests/Models/DueDateTests.cs b/src/Todoist.Net.Tests/Models/DueDateTests.cs index 5eca17a..3755a74 100644 --- a/src/Todoist.Net.Tests/Models/DueDateTests.cs +++ b/src/Todoist.Net.Tests/Models/DueDateTests.cs @@ -34,9 +34,9 @@ public void 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); @@ -45,20 +45,20 @@ public void DateTimeAssignment_FullDayEvent_Success() [Fact] public void DateTimeAssignment_FloatingDueDateEvent_Success() { - var date = new DateTime(2018, 2, 5, 0, 0, 0, DateTimeKind.Unspecified); + 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); diff --git a/src/Todoist.Net.Tests/Services/ItemsServiceTests.cs b/src/Todoist.Net.Tests/Services/ItemsServiceTests.cs index 4e86513..61f2841 100644 --- a/src/Todoist.Net.Tests/Services/ItemsServiceTests.cs +++ b/src/Todoist.Net.Tests/Services/ItemsServiceTests.cs @@ -88,7 +88,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; @@ -112,7 +112,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( async () => @@ -132,7 +132,7 @@ public void 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"); client.Items.UpdateAsync(item).Wait(); var project = new Project(Guid.NewGuid().ToString()); @@ -161,7 +161,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(); @@ -188,7 +188,7 @@ 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; @@ -207,7 +207,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(); diff --git a/src/Todoist.Net.Tests/Services/ReminersServiceTests.cs b/src/Todoist.Net.Tests/Services/ReminersServiceTests.cs index bf3d8e6..a5e1414 100644 --- a/src/Todoist.Net.Tests/Services/ReminersServiceTests.cs +++ b/src/Todoist.Net.Tests/Services/ReminersServiceTests.cs @@ -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); @@ -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); From 7325cb5c1aad9cd1494cadf5f8a3d48a82a8e4ae Mon Sep 17 00:00:00 2001 From: Ahmed Zaki Date: Tue, 31 Oct 2023 12:32:14 +0200 Subject: [PATCH 11/13] Remove randomization in tests. Fake time zone random selection is replaced by pre-determined custom offset. --- .../Helpers/FakeLocalTimeZone.cs | 54 +++++++++++++++++-- .../Helpers/FakeLocalTimeZoneTests.cs | 20 ++----- src/Todoist.Net.Tests/Models/DueDateTests.cs | 7 +-- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZone.cs b/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZone.cs index 6f662e2..82117dc 100644 --- a/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZone.cs +++ b/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZone.cs @@ -14,13 +14,18 @@ public sealed class FakeLocalTimeZone : IDisposable { /// - /// Initializes a new instance of the class - /// and changes the local time zone to the given - /// until it's disposed. + /// The fake time zone info that has been set as local. /// - /// The time zone to set as local until disposal. - public FakeLocalTimeZone(TimeZoneInfo fakeTimeZoneInfo) + public TimeZoneInfo FakeTimeZoneInfo { get; } + + + /// + /// Initializes a new instance of the class. + /// + private FakeLocalTimeZone(TimeZoneInfo fakeTimeZoneInfo) { + FakeTimeZoneInfo = fakeTimeZoneInfo; + var info = typeof(TimeZoneInfo).GetField("s_cachedData", BindingFlags.NonPublic | BindingFlags.Static); var cachedData = info.GetValue(null); @@ -34,4 +39,43 @@ public void Dispose() { TimeZoneInfo.ClearCachedData(); } + + + /// + /// Changes the local time zone to the given . + /// + /// + /// Disposal of the returned object resets the local time zone to the original one. + /// + /// The time zone to set as local until disposal. + /// + /// A instance that represents the time zone change, + /// and used to reset it back to original at disposal. + /// + public static FakeLocalTimeZone ChangeLocalTimeZone(TimeZoneInfo fakeTimeZoneInfo) + { + return new FakeLocalTimeZone(fakeTimeZoneInfo); + } + + /// + /// Changes the local time zone to a custom time zone with a . + /// + /// + /// Disposal of the returned object resets the local time zone to the original one. + /// + /// UTC offset of the custom time zone. + /// + /// A instance that represents the time zone change, + /// and used to reset it back to original at disposal. + /// + 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); + } + } diff --git a/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZoneTests.cs b/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZoneTests.cs index aba5278..ca5b3c9 100644 --- a/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZoneTests.cs +++ b/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZoneTests.cs @@ -14,24 +14,14 @@ public class FakeLocalTimeZoneTests [Fact] public void FakeLocalTimeZone_ShouldChangeLocalTimeZoneWithinScope_AndResetItBackOutsideScope() { - var actualTimeZoneInfo = TimeZoneInfo.Local; + var fakeTimeZoneOffset = TimeZoneInfo.Local.BaseUtcOffset + TimeSpan.FromHours(2); + var fakeLocalTimeZone = FakeLocalTimeZone.ChangeLocalTimeZone(fakeTimeZoneOffset); - var timeZoneCollection = TimeZoneInfo - .GetSystemTimeZones() - .Where(t => !actualTimeZoneInfo.Equals(t)) - .ToArray(); - - var randomIndex = new Random().Next(timeZoneCollection.Length); - var fakeTimeZoneInfo = timeZoneCollection.ElementAt(randomIndex); - - - Assert.NotEqual(fakeTimeZoneInfo, actualTimeZoneInfo); - - using (var fakeLocalTimeZone = new FakeLocalTimeZone(fakeTimeZoneInfo)) + using (fakeLocalTimeZone) { - Assert.Equal(fakeTimeZoneInfo, TimeZoneInfo.Local); + Assert.Equal(fakeLocalTimeZone.FakeTimeZoneInfo, TimeZoneInfo.Local); } - Assert.Equal(actualTimeZoneInfo, TimeZoneInfo.Local); + Assert.NotEqual(fakeLocalTimeZone.FakeTimeZoneInfo, TimeZoneInfo.Local); } } diff --git a/src/Todoist.Net.Tests/Models/DueDateTests.cs b/src/Todoist.Net.Tests/Models/DueDateTests.cs index 3755a74..2b7c3d7 100644 --- a/src/Todoist.Net.Tests/Models/DueDateTests.cs +++ b/src/Todoist.Net.Tests/Models/DueDateTests.cs @@ -16,12 +16,7 @@ public class DueDateTests : IDisposable public DueDateTests() { - var timeZoneCollection = System.TimeZoneInfo.GetSystemTimeZones(); - - var randomIndex = new Random().Next(timeZoneCollection.Count); - var fakeTimeZoneInfo = timeZoneCollection[randomIndex]; - - _fakeLocalTimeZone = new FakeLocalTimeZone(fakeTimeZoneInfo); + _fakeLocalTimeZone = FakeLocalTimeZone.ChangeLocalTimeZone(TimeSpan.FromHours(5)); } public void Dispose() From 9a1547734f95a217d80710b4524525a2492c6d07 Mon Sep 17 00:00:00 2001 From: Ahmed Zaki Date: Tue, 31 Oct 2023 12:49:15 +0200 Subject: [PATCH 12/13] Remove unnecessary comments. --- src/Todoist.Net.Tests/Models/DueDateTests.cs | 29 +++++++------------- src/Todoist.Net/Models/DueDate.cs | 6 ++-- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/Todoist.Net.Tests/Models/DueDateTests.cs b/src/Todoist.Net.Tests/Models/DueDateTests.cs index 2b7c3d7..3145cb2 100644 --- a/src/Todoist.Net.Tests/Models/DueDateTests.cs +++ b/src/Todoist.Net.Tests/Models/DueDateTests.cs @@ -63,50 +63,41 @@ public void DateTimeAssignment_FixedTimeZoneDueDateEvent_Success() [Fact] public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFullDayDate() { - // Arrange var dueDate = new DueDate(); string initialValue = "2016-12-01"; - // Act - dueDate.StringDate = initialValue; // Set initial value. - string returnedValue = dueDate.StringDate; // Get. - dueDate.StringDate = returnedValue; // Set returned value. + dueDate.StringDate = initialValue; + string returnedValue = dueDate.StringDate; + dueDate.StringDate = returnedValue; - // Assert Assert.Equal(returnedValue, dueDate.StringDate); } [Fact] public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFloatingDate() { - // Arrange var dueDate = new DueDate(); string initialValue = "2016-12-03T12:00:00"; - // Act - dueDate.StringDate = initialValue; // Set initial value. - string returnedValue = dueDate.StringDate; // Get. - dueDate.StringDate = returnedValue; // Set returned value. + dueDate.StringDate = initialValue; + string returnedValue = dueDate.StringDate; + dueDate.StringDate = returnedValue; - // Assert Assert.Equal(returnedValue, dueDate.StringDate); } [Fact] public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFixedDate() { - // Arrange var dueDate = new DueDate(); string initialValue = "2016-12-06T13:00:00Z"; - // Act - dueDate.StringDate = initialValue; // Set initial value. - dueDate.Timezone = "Asia/Jakarta"; // Set fixed timezone. + dueDate.StringDate = initialValue; + dueDate.Timezone = "Asia/Jakarta"; - string returnedValue = dueDate.StringDate; // Get. - dueDate.StringDate = returnedValue; // Set returned value. + string returnedValue = dueDate.StringDate; + dueDate.StringDate = returnedValue; - // Assert Assert.Equal(returnedValue, dueDate.StringDate); } } diff --git a/src/Todoist.Net/Models/DueDate.cs b/src/Todoist.Net/Models/DueDate.cs index dc57918..e0f437a 100644 --- a/src/Todoist.Net/Models/DueDate.cs +++ b/src/Todoist.Net/Models/DueDate.cs @@ -129,7 +129,7 @@ internal string StringDate }; /// - /// Creates a full-day in the format of YYYY-MM-DD (RFC 3339). + /// Creates a full-day . /// /// The full-day date. /// @@ -148,7 +148,7 @@ internal string StringDate }; /// - /// Creates a floating in the format of YYYY-MM-DDTHH:MM:SS. + /// Creates a floating . /// /// The floating date. /// @@ -173,7 +173,7 @@ internal string StringDate }; /// - /// Creates a fixed timezone in the format of YYYY-MM-DDTHH:MM:SSZ (RFC 3339). + /// Creates a fixed timezone . /// /// The fixed timezone date. /// The timezone of the due instance. From 9fb688b09e779aefbf906a62d05b9114c995fd75 Mon Sep 17 00:00:00 2001 From: Ahmed Zaki Date: Tue, 31 Oct 2023 13:04:54 +0200 Subject: [PATCH 13/13] Removed unnecessary SuppressFinalize call in DueDateTests. --- src/Todoist.Net.Tests/Models/DueDateTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Todoist.Net.Tests/Models/DueDateTests.cs b/src/Todoist.Net.Tests/Models/DueDateTests.cs index 3145cb2..e3d7dcd 100644 --- a/src/Todoist.Net.Tests/Models/DueDateTests.cs +++ b/src/Todoist.Net.Tests/Models/DueDateTests.cs @@ -21,8 +21,7 @@ public DueDateTests() public void Dispose() { - _fakeLocalTimeZone.Dispose(); - GC.SuppressFinalize(this); + _fakeLocalTimeZone?.Dispose(); }