-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Reject DateTimes in deserialization that have no explicit offset (
#19)
- Loading branch information
Showing
6 changed files
with
160 additions
and
6 deletions.
There are no files selected for viewing
65 changes: 65 additions & 0 deletions
65
MaLoIdentModels/MaLoIdentModels/JsonSettings/DateTimeOffsetStringExtensionMethods.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
using System; | ||
using System.Globalization; | ||
using System.Text.Json; | ||
using System.Text.RegularExpressions; | ||
|
||
namespace MaLoIdentModels.JsonSettings; | ||
|
||
/// <summary> | ||
/// helper methods for handling strings that represent datetimes | ||
/// </summary> | ||
internal static class DateTimeOffsetStringExtensionMethods | ||
{ | ||
private const string FormatWithoutMilliseconds = "yyyy-MM-ddTHH:mm:ssK"; | ||
private const string FormatWithMilliseconds = "yyyy-MM-ddTHH:mm:ss.fffK"; | ||
private const string FormatWithMicroseconds = "yyyy-MM-ddTHH:mm:ss.ffffffK"; | ||
|
||
private static readonly string[] SupportedFormats = | ||
[ | ||
FormatWithoutMilliseconds, | ||
FormatWithMilliseconds, | ||
FormatWithMicroseconds, | ||
]; | ||
|
||
/// <summary> | ||
/// simple regex to check if a string ends with something that looks like a timezone or UTC offset. | ||
/// </summary> | ||
/// <remarks> | ||
/// Note that this isn't super strict, e.g. '+17:99' would also pass, but that's fine, | ||
/// because the actual parsing happens with regular DateTime parsing. | ||
/// </remarks> | ||
private static readonly Regex EndsWithOffsetPattern = new(@"^.+[\+\-]\d{1,2}:\d{2}$"); | ||
|
||
/// <summary> | ||
/// converts the given <paramref name="dtoString"/> to an datetimeoffset | ||
/// </summary> | ||
/// <param name="dtoString"></param> | ||
/// <returns></returns> | ||
/// <exception cref="JsonException">if no explicit offset is specified in <paramref name="dtoString"/></exception> | ||
internal static DateTimeOffset ToDateTimeOffset(this string dtoString) | ||
{ | ||
if (!(dtoString.EndsWith("Z") || EndsWithOffsetPattern.Match(dtoString).Success)) | ||
{ | ||
throw new JsonException( | ||
$"DateTimeOffset value must have an explicit offset, like 'Z' or '+/-HH:mm' but '{dtoString}' doesn't" | ||
); | ||
} | ||
|
||
if ( | ||
!DateTimeOffset.TryParseExact( | ||
dtoString, | ||
SupportedFormats, | ||
null, | ||
DateTimeStyles.None, | ||
out var result | ||
) | ||
) | ||
{ | ||
throw new JsonException( | ||
$"DateTimeOffset value must match any of the supported formats: {string.Join(", ", SupportedFormats)}, but '{dtoString}' doesn't" | ||
); | ||
} | ||
|
||
return new DateTimeOffset(result.UtcDateTime); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
MaLoIdentModels/MaLoIdentModelsTests/DateTimeOffsetDeserializationTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
using System.Text.Json; | ||
using FluentAssertions; | ||
using MaLoIdentModels; | ||
|
||
namespace MaLoIdentModelsTests; | ||
|
||
public class DateTimeOffsetDeserializationTests | ||
{ | ||
/// <summary> | ||
/// We want to avoid, that clients can send us serialized datetime(offsets), with no explicit offset given. | ||
/// So '2025-01-01T00:00:00Z' is fine but '2025-01-01T00:00:00' (implicit or no offset) is not. | ||
/// Also, we'll accept '2025-01-01T00:00:00+00:00' and '2025-01-01T01:00:00+01:00'. | ||
/// Both the latter are not "officially" allowed, but we think that providing any UTC offset is good enough, | ||
/// instead of the BDEW interpretation that "only UTC offset 0 indicated only by trailing Z" were ok. | ||
/// This is because we can convert any date+time+offset to UTC, even if the offset is != 0. | ||
/// </summary> | ||
[Theory] | ||
[InlineData("2023-08-02T22:00:00")] | ||
[InlineData("2023-08-02T22:00:00+17:99")] | ||
public void Deserializing_An_DatetimeOffset_Without_Explicit_Offset_Throws_JsonException( | ||
string invalidDateTimeOffsetString | ||
) | ||
{ | ||
var fileBody = File.ReadAllText("examples/request.json"); | ||
fileBody | ||
.Should() | ||
.Contain( | ||
"\"2023-08-02T22:00:00Z\"", | ||
because: "The original example data use trailing 'Z' to indicate UTC offset 0" | ||
); | ||
var requestJsonWithoutExplicitUtcOffset = fileBody.Replace( | ||
"\"2023-08-02T22:00:00Z\"", | ||
$"\"{invalidDateTimeOffsetString}\"" | ||
); | ||
var deserializing = () => | ||
JsonSerializer.Deserialize<IdentificationParameter>( | ||
requestJsonWithoutExplicitUtcOffset | ||
); | ||
deserializing.Should().Throw<JsonException>(); | ||
} | ||
|
||
/// <summary> | ||
/// We want to avoid, that clients can send us serialized datetime(offsets), with no explicit offset given. | ||
/// So '2025-01-01T00:00:00Z' is fine but '2025-01-01T00:00:00' (implicit or no offset) is not. | ||
/// Also, we'll accept '2025-01-01T00:00:00+00:00' and '2025-01-01T01:00:00+01:00'. | ||
/// Both the latter are not "officially" allowed, but we think that providing any UTC offset is good enough, | ||
/// instead of the BDEW interpretation that "only UTC offset 0 indicated only by trailing Z" were ok. | ||
/// This is because we can convert any date+time+offset to UTC, even if the offset is != 0. | ||
/// </summary> | ||
[Theory] | ||
[InlineData("2023-08-02T22:00:00Z")] | ||
[InlineData("2023-08-02T22:00:00+00:00")] | ||
[InlineData("2023-08-03T00:00:00+02:00")] | ||
[InlineData("2023-08-02T20:00:00-02:00")] | ||
[InlineData("2023-08-03T00:00:00.000+02:00")] | ||
[InlineData("2023-08-03T00:00:00.000000+02:00")] | ||
public void Deserializing_An_DatetimeOffset_With_Explicit_Offset_Works( | ||
string dateTimeOffsetString | ||
) | ||
{ | ||
var fileBody = File.ReadAllText("examples/request.json"); | ||
fileBody | ||
.Should() | ||
.Contain( | ||
"\"2023-08-02T22:00:00Z\"", | ||
because: "The original example data use trailing 'Z' to indicate UTC offset 0" | ||
); | ||
var requestJsonWithoutExplicitUtcOffset = fileBody.Replace( | ||
"\"2023-08-02T22:00:00Z\"", | ||
$"\"{dateTimeOffsetString}\"" | ||
); | ||
var deserializing = () => | ||
JsonSerializer.Deserialize<IdentificationParameter>( | ||
requestJsonWithoutExplicitUtcOffset | ||
); | ||
deserializing | ||
.Should() | ||
.NotThrow<JsonException>() | ||
.And.Subject.Invoke() | ||
.IdentificationDateTime.Should() | ||
.Be(new DateTimeOffset(2023, 8, 2, 22, 0, 0, 0, TimeSpan.Zero)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters