Skip to content

Commit

Permalink
Updated the DateTime scalar to enforce a specific format (#7329)
Browse files Browse the repository at this point in the history
  • Loading branch information
glen-84 authored Aug 2, 2024
1 parent 9aac996 commit a9ad558
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 9 deletions.
34 changes: 28 additions & 6 deletions src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.RegularExpressions;
using HotChocolate.Language;
using HotChocolate.Properties;

Expand All @@ -19,6 +20,10 @@ public class DateTimeType : ScalarType<DateTimeOffset, StringValueNode>
private const string _localFormat = "yyyy-MM-ddTHH\\:mm\\:ss.fffzzz";
private const string _specifiedBy = "https://www.graphql-scalars.com/date-time";

private static readonly Regex DateTimeScalarRegex = new(
@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,7})?(Z|[+-][0-9]{2}:[0-9]{2})$",
RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.Compiled);

/// <summary>
/// Initializes a new instance of the <see cref="DateTimeType"/> class.
/// </summary>
Expand Down Expand Up @@ -162,8 +167,28 @@ private static bool TryDeserializeFromString(
string? serialized,
[NotNullWhen(true)] out DateTimeOffset? value)
{
if (serialized is not null
&& serialized.EndsWith("Z")
if (serialized is null)
{
value = null;
return false;
}

// Check format.
if (!DateTimeScalarRegex.IsMatch(serialized))
{
value = null;
return false;
}

// No "Unknown Local Offset Convention".
// https://www.graphql-scalars.com/date-time/#no-unknown-local-offset-convention
if (serialized.EndsWith("-00:00"))
{
value = null;
return false;
}

if (serialized.EndsWith("Z")
&& DateTime.TryParse(
serialized,
CultureInfo.InvariantCulture,
Expand All @@ -176,10 +201,7 @@ private static bool TryDeserializeFromString(
return true;
}

if (serialized is not null
&& DateTimeOffset.TryParse(
serialized,
out var dt))
if (DateTimeOffset.TryParse(serialized, out var dt))
{
value = dt;
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ query foo($a: FooInput) {
new Dictionary<string, object>
{
{ "id", "934b987bc0d842bbabfd8a3b3f8b476e" },
{ "time", "2018-05-29T01:00Z" },
{ "time", "2018-05-29T01:00:00Z" },
{ "number", (byte)123 },
}
}
Expand All @@ -48,7 +48,8 @@ await ExpectValid(
query foo($time: DateTime) {
time(time: $time)
}",
request: r => r.SetVariableValues(new Dictionary<string, object> { { "time", "2018-05-29T01:00Z" }, }),
request: r => r.SetVariableValues(
new Dictionary<string, object> { { "time", "2018-05-29T01:00:00Z" }, }),
configure: c => c.AddQueryType<QueryType>())
.MatchSnapshotAsync();
}
Expand Down Expand Up @@ -89,7 +90,7 @@ query foo($a: FooInput) {
new Dictionary<string, object>
{
{ "id", "934b987bc0d842bbabfd8a3b3f8b476e" },
{ "time", "2018-05-29T01:00Z" },
{ "time", "2018-05-29T01:00:00Z" },
{ "number", (byte)123 },
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,41 @@ public void ParseLiteral_StringValueNode()
Assert.Equal(expectedDateTime, dateTime);
}

[Theory]
[MemberData(nameof(ValidDateTimeScalarStrings))]
public void ParseLiteral_StringValueNode_Valid(string dateTime, DateTimeOffset result)
{
// arrange
var dateTimeType = new DateTimeType();
var literal = new StringValueNode(dateTime);

// act
var dateTimeOffset = (DateTimeOffset?)dateTimeType.ParseLiteral(literal);

// assert
Assert.Equal(result, dateTimeOffset);
}

[Theory]
[MemberData(nameof(InvalidDateTimeScalarStrings))]
public void ParseLiteral_StringValueNode_Invalid(string dateTime)
{
// arrange
var dateTimeType = new DateTimeType();
var literal = new StringValueNode(dateTime);

// act
void Act()
{
dateTimeType.ParseLiteral(literal);
}

// assert
Assert.Equal(
"DateTime cannot parse the given literal of type `StringValueNode`.",
Assert.Throws<SerializationException>(Act).Message);
}

[InlineData("en-US")]
[InlineData("en-AU")]
[InlineData("en-GB")]
Expand Down Expand Up @@ -385,4 +420,66 @@ public class DefaultDateTime
{
public DateTime Test => default;
}

// https://www.graphql-scalars.com/date-time/#test-cases (valid strings)
public static TheoryData<string, DateTimeOffset> ValidDateTimeScalarStrings()
{
return new TheoryData<string, DateTimeOffset>
{
{
// A DateTime with UTC offset (+00:00).
"2011-08-30T13:22:53.108Z",
new(2011, 8, 30, 13, 22, 53, 108, new TimeSpan())
},
{
// A DateTime with +00:00 which is the same as UTC.
"2011-08-30T13:22:53.108+00:00",
new(2011, 8, 30, 13, 22, 53, 108, new TimeSpan())
},
{
// The z and t may be lower case.
"2011-08-30t13:22:53.108z",
new(2011, 8, 30, 13, 22, 53, 108, new TimeSpan())
},
{
// A DateTime with -3h offset.
"2011-08-30T13:22:53.108-03:00",
new(2011, 8, 30, 13, 22, 53, 108, new TimeSpan(-3, 0, 0))
},
{
// A DateTime with +3h 30min offset.
"2011-08-30T13:22:53.108+03:30",
new(2011, 8, 30, 13, 22, 53, 108, new TimeSpan(3, 30, 0))
}
};
}

// https://www.graphql-scalars.com/date-time/#test-cases (invalid strings)
public static TheoryData<string> InvalidDateTimeScalarStrings()
{
return new TheoryData<string>
{
// The minutes of the offset are missing.
"2011-08-30T13:22:53.108-03",
// Too many digits for fractions of a second. Exactly three expected.
// -> We diverge from the specification here, and allow 0-7 fractional digits.
// Fractions of a second are missing.
"2011-08-30T24:22:53Z",
// No offset provided.
"2011-08-30T13:22:53.108",
// No time provided.
"2011-08-30",
// Negative offset (-00:00) is not allowed.
"2011-08-30T13:22:53.108-00:00",
// Seconds are not allowed for the offset.
"2011-08-30T13:22:53.108+03:30:15",
// 24 is not allowed as hour of the time.
"2011-08-30T24:22:53.108Z",
// ReSharper disable once GrammarMistakeInComment
// 30th of February is not a valid date.
"2010-02-30T21:22:53.108Z",
// 25 is not a valid hour for offset.
"2010-02-11T21:22:53.108Z+25:11"
};
}
}

0 comments on commit a9ad558

Please sign in to comment.