From 4836c4b863b43a767f686add471387b5b9b32c61 Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sat, 12 Apr 2025 23:52:00 +0530 Subject: [PATCH 1/4] fix: enhance DateCustomSerializer and add tests * use ISO 8601 date and time format * fix breaking test --- .../PropertyValue/DateCustomConverter.cs | 9 +- Test/Notion.UnitTests/DatabasesClientTests.cs | 2 +- .../DateCustomConverterTests.cs | 219 ++++++++++++++++++ 3 files changed, 226 insertions(+), 4 deletions(-) create mode 100644 Test/Notion.UnitTests/DateCustomConverterTests.cs diff --git a/Src/Notion.Client/Models/PropertyValue/DateCustomConverter.cs b/Src/Notion.Client/Models/PropertyValue/DateCustomConverter.cs index 6823c30..d4af88f 100644 --- a/Src/Notion.Client/Models/PropertyValue/DateCustomConverter.cs +++ b/Src/Notion.Client/Models/PropertyValue/DateCustomConverter.cs @@ -5,6 +5,9 @@ namespace Notion.Client { public class DateCustomConverter : JsonConverter { + private const string DateFormat = "yyyy-MM-dd"; + private const string DateTimeFormat = "yyyy-MM-ddTHH:mm:ssZ"; + public override Date ReadJson(JsonReader reader, Type objectType, Date existingValue, bool hasExistingValue, JsonSerializer serializer) { @@ -39,14 +42,14 @@ public override void WriteJson(JsonWriter writer, Date value, JsonSerializer ser if (value.Start.HasValue) { - string startFormat = value.IncludeTime ? "yyyy-MM-ddTHH:mm:ss" : "yyyy-MM-dd"; + string startFormat = value.IncludeTime ? DateTimeFormat : DateFormat; writer.WritePropertyName("start"); writer.WriteValue(value.Start.Value.ToString(startFormat)); } if (value.End.HasValue) { - string endFormat = value.IncludeTime ? "yyyy-MM-ddTHH:mm:ss" : "yyyy-MM-dd"; + string endFormat = value.IncludeTime ? DateTimeFormat : DateFormat; writer.WritePropertyName("end"); writer.WriteValue(value.End.Value.ToString(endFormat)); } @@ -71,7 +74,7 @@ public override void WriteJson(JsonWriter writer, Date value, JsonSerializer ser includeTime = dateTimeString.Contains("T") || dateTimeString.Contains(" "); - return DateTimeOffset.Parse(dateTimeString).UtcDateTime; + return DateTimeOffset.Parse(dateTimeString, null, System.Globalization.DateTimeStyles.AssumeUniversal).UtcDateTime; } } } diff --git a/Test/Notion.UnitTests/DatabasesClientTests.cs b/Test/Notion.UnitTests/DatabasesClientTests.cs index 1edbc2e..30deae7 100644 --- a/Test/Notion.UnitTests/DatabasesClientTests.cs +++ b/Test/Notion.UnitTests/DatabasesClientTests.cs @@ -500,7 +500,7 @@ var jsonData }); //var formulaPropertyValue = (FormulaPropertyValue)page.Properties["FormulaProp"]; - formulaPropertyValue.Formula.Date.Start.Should().Be(DateTime.Parse("2021-06-28")); + formulaPropertyValue.Formula.Date.Start.Should().Be(DateTimeOffset.Parse("2021-06-28", null, System.Globalization.DateTimeStyles.AssumeUniversal).UtcDateTime); formulaPropertyValue.Formula.Date.End.Should().BeNull(); } } diff --git a/Test/Notion.UnitTests/DateCustomConverterTests.cs b/Test/Notion.UnitTests/DateCustomConverterTests.cs new file mode 100644 index 0000000..81c33f5 --- /dev/null +++ b/Test/Notion.UnitTests/DateCustomConverterTests.cs @@ -0,0 +1,219 @@ +using System; +using System.IO; +using Newtonsoft.Json; +using Notion.Client; +using Xunit; + +namespace NotionUnitTests.PropertyValue; + +public class DateCustomConverterTests +{ + private readonly DateCustomConverter _converter = new(); + private readonly JsonSerializer _serializer = new(); + + [Fact] + public void Serialize_null_writes_null() + { + // Arrange + Date date = null; + var stringWriter = new StringWriter(); + var jsonWriter = new JsonTextWriter(stringWriter); + + // Act + _converter.WriteJson(jsonWriter, date, _serializer); + jsonWriter.Flush(); + + // Assert + Assert.Equal("null", stringWriter.ToString()); + } + + [Fact] + public void Serialize_start_date_only_produces_correct_json() + { + // Arrange + var date = new Date + { + Start = new DateTimeOffset(2023, 5, 15, 0, 0, 0, TimeSpan.Zero), + IncludeTime = false + }; + + // Act + var json = JsonConvert.SerializeObject(date); + + // Assert + Assert.Contains("\"start\":\"2023-05-15\"", json); + Assert.DoesNotContain("\"end\":", json); + Assert.DoesNotContain("\"time_zone\":", json); + } + + [Fact] + public void Serialize_start_and_end_dates_produces_correct_json() + { + // Arrange + var date = new Date + { + Start = new DateTimeOffset(2023, 5, 15, 0, 0, 0, TimeSpan.Zero), + End = new DateTimeOffset(2023, 5, 20, 0, 0, 0, TimeSpan.Zero), + IncludeTime = false + }; + + // Act + var json = JsonConvert.SerializeObject(date); + + // Assert + Assert.Contains("\"start\":\"2023-05-15\"", json); + Assert.Contains("\"end\":\"2023-05-20\"", json); + Assert.DoesNotContain("\"time_zone\":", json); + } + + [Fact] + public void Serialize_with_time_included_formats_time_correctly() + { + // Arrange + var date = new Date + { + Start = new DateTimeOffset(2023, 5, 15, 14, 30, 45, TimeSpan.Zero), + IncludeTime = true + }; + + // Act + var json = JsonConvert.SerializeObject(date); + + // Assert + Assert.Contains("\"start\":\"2023-05-15T14:30:45Z\"", json); + } + + [Fact] + public void Serialize_with_time_zone_includes_time_zone() + { + // Arrange + var date = new Date + { + Start = new DateTimeOffset(2023, 5, 15, 14, 30, 45, TimeSpan.Zero), + TimeZone = "Europe/London", + IncludeTime = true + }; + + // Act + var json = JsonConvert.SerializeObject(date); + + // Assert + Assert.Contains("\"start\":\"2023-05-15T14:30:45Z\"", json); + Assert.Contains("\"time_zone\":\"Europe/London\"", json); + } + + [Fact] + public void Deserialize_null_returns_null() + { + // Arrange + const string Json = "null"; + + // Act + var result = JsonConvert.DeserializeObject(Json); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Deserialize_start_date_only_returns_correct_date() + { + // Arrange + const string Json = "{\"start\":\"2023-05-15\"}"; + + // Act + var result = JsonConvert.DeserializeObject(Json); + + // Assert + Assert.NotNull(result); + Assert.Equal(new DateTimeOffset(2023, 5, 15, 0, 0, 0, TimeSpan.Zero), result.Start); + Assert.Null(result.End); + Assert.Null(result.TimeZone); + Assert.False(result.IncludeTime); + } + + [Fact] + public void Deserialize_with_time_sets_include_time_flag() + { + // Arrange + const string Json = "{\"start\":\"2023-05-15T14:30:45\"}"; + + // Act + var result = JsonConvert.DeserializeObject(Json); + + // Assert + Assert.NotNull(result); + Assert.Equal(new DateTimeOffset(2023, 5, 15, 14, 30, 45, TimeSpan.Zero), result.Start); + Assert.True(result.IncludeTime); + } + + [Fact] + public void Deserialize_with_start_end_and_time_zone_returns_complete_date() + { + // Arrange + const string Json = "{\"start\":\"2023-05-15T14:30:45\",\"end\":\"2023-05-20T16:45:00\",\"time_zone\":\"America/New_York\"}"; + + // Act + var result = JsonConvert.DeserializeObject(Json); + + // Assert + Assert.NotNull(result); + Assert.Equal(new DateTimeOffset(2023, 5, 15, 14, 30, 45, TimeSpan.Zero), result.Start); + Assert.Equal(new DateTimeOffset(2023, 5, 20, 16, 45, 0, TimeSpan.Zero), result.End); + Assert.Equal("America/New_York", result.TimeZone); + Assert.True(result.IncludeTime); + } + + [Fact] + public void Date_property_value_serialize_deserialize_maintains_data() + { + // Arrange + var datePropertyValue = new DatePropertyValue + { + Date = new Date + { + Start = new DateTimeOffset(2023, 5, 15, 14, 30, 45, TimeSpan.Zero), + End = new DateTimeOffset(2023, 5, 20, 16, 45, 0, TimeSpan.Zero), + TimeZone = "Europe/Berlin", + IncludeTime = true + } + }; + + // Act + var json = JsonConvert.SerializeObject(datePropertyValue); + var result = JsonConvert.DeserializeObject(json); + + // Assert + Assert.NotNull(result); + Assert.Equal(PropertyValueType.Date, result.Type); + Assert.NotNull(result.Date); + Assert.Equal(datePropertyValue.Date.Start, result.Date.Start); + Assert.Equal(datePropertyValue.Date.End, result.Date.End); + Assert.Equal(datePropertyValue.Date.TimeZone, result.Date.TimeZone); + Assert.Equal(datePropertyValue.Date.IncludeTime, result.Date.IncludeTime); + } + + [Fact] + public void Round_trip_preserves_data() + { + // Arrange + var originalDate = new Date + { + Start = new DateTimeOffset(2023, 5, 15, 14, 30, 45, TimeSpan.Zero), + End = new DateTimeOffset(2023, 5, 20, 16, 45, 0, TimeSpan.Zero), + TimeZone = "Europe/Berlin", + IncludeTime = true + }; + + // Act + var json = JsonConvert.SerializeObject(originalDate); + var deserializedDate = JsonConvert.DeserializeObject(json); + + // Assert + Assert.NotNull(deserializedDate); + Assert.Equal(originalDate.Start, deserializedDate.Start); + Assert.Equal(originalDate.End, deserializedDate.End); + Assert.Equal(originalDate.TimeZone, deserializedDate.TimeZone); + Assert.True(deserializedDate.IncludeTime); + } +} From 42d7e798f663cf8189f09c6b6444c4771d89b6c0 Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sat, 12 Apr 2025 23:52:36 +0530 Subject: [PATCH 2/4] add explicit JsonIgnore attribute to IncludeTime property in Date class --- Src/Notion.Client/Models/PropertyValue/DatePropertyValue.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Src/Notion.Client/Models/PropertyValue/DatePropertyValue.cs b/Src/Notion.Client/Models/PropertyValue/DatePropertyValue.cs index b79707d..66f9c6b 100644 --- a/Src/Notion.Client/Models/PropertyValue/DatePropertyValue.cs +++ b/Src/Notion.Client/Models/PropertyValue/DatePropertyValue.cs @@ -48,6 +48,7 @@ public class Date /// /// Whether to include time /// + [JsonIgnore] public bool IncludeTime { get; set; } = true; } } From 48ee22a8f1afecc05a8ae24f4b0d932b06b4baa9 Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sun, 13 Apr 2025 00:49:23 +0530 Subject: [PATCH 3/4] ensure culture-invariant date formatting in DateCustomConverter --- .../Models/PropertyValue/DateCustomConverter.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Src/Notion.Client/Models/PropertyValue/DateCustomConverter.cs b/Src/Notion.Client/Models/PropertyValue/DateCustomConverter.cs index d4af88f..78f14cd 100644 --- a/Src/Notion.Client/Models/PropertyValue/DateCustomConverter.cs +++ b/Src/Notion.Client/Models/PropertyValue/DateCustomConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using Newtonsoft.Json; namespace Notion.Client @@ -44,14 +45,14 @@ public override void WriteJson(JsonWriter writer, Date value, JsonSerializer ser { string startFormat = value.IncludeTime ? DateTimeFormat : DateFormat; writer.WritePropertyName("start"); - writer.WriteValue(value.Start.Value.ToString(startFormat)); + writer.WriteValue(value.Start.Value.ToString(startFormat, CultureInfo.InvariantCulture)); } if (value.End.HasValue) { string endFormat = value.IncludeTime ? DateTimeFormat : DateFormat; writer.WritePropertyName("end"); - writer.WriteValue(value.End.Value.ToString(endFormat)); + writer.WriteValue(value.End.Value.ToString(endFormat, CultureInfo.InvariantCulture)); } if (!string.IsNullOrEmpty(value.TimeZone)) @@ -74,7 +75,7 @@ public override void WriteJson(JsonWriter writer, Date value, JsonSerializer ser includeTime = dateTimeString.Contains("T") || dateTimeString.Contains(" "); - return DateTimeOffset.Parse(dateTimeString, null, System.Globalization.DateTimeStyles.AssumeUniversal).UtcDateTime; + return DateTimeOffset.Parse(dateTimeString, null, DateTimeStyles.AssumeUniversal).UtcDateTime; } } } From 4e9c6bebae87b314724e72942fe6243125debb9d Mon Sep 17 00:00:00 2001 From: Vedant Koditkar <18693839+KoditkarVedant@users.noreply.github.com> Date: Sun, 13 Apr 2025 01:09:13 +0530 Subject: [PATCH 4/4] Use CultureInfo.InvariantCulture in DateTimeOffset.Parse to ensure consistent parsing of ISO 8601 dates across different environments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Src/Notion.Client/Models/PropertyValue/DateCustomConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/Notion.Client/Models/PropertyValue/DateCustomConverter.cs b/Src/Notion.Client/Models/PropertyValue/DateCustomConverter.cs index 78f14cd..37b84bc 100644 --- a/Src/Notion.Client/Models/PropertyValue/DateCustomConverter.cs +++ b/Src/Notion.Client/Models/PropertyValue/DateCustomConverter.cs @@ -75,7 +75,7 @@ public override void WriteJson(JsonWriter writer, Date value, JsonSerializer ser includeTime = dateTimeString.Contains("T") || dateTimeString.Contains(" "); - return DateTimeOffset.Parse(dateTimeString, null, DateTimeStyles.AssumeUniversal).UtcDateTime; + return DateTimeOffset.Parse(dateTimeString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).UtcDateTime; } } }