474 lines
16 KiB
C#
474 lines
16 KiB
C#
using AyCode.Core.Extensions;
|
|
|
|
namespace AyCode.Core.Tests.Serialization;
|
|
|
|
/// <summary>
|
|
/// Tests for DateTime serialization in Binary format.
|
|
/// Covers edge cases like string-stored DateTime values in GenericAttribute-like scenarios.
|
|
/// </summary>
|
|
[TestClass]
|
|
public class AcBinaryDateTimeSerializationTests
|
|
{
|
|
#region DateTime Direct Serialization
|
|
|
|
[TestMethod]
|
|
public void DateTime_RoundTrip_PreservesValue()
|
|
{
|
|
var original = new DateTime(2025, 10, 24, 0, 27, 0, DateTimeKind.Utc);
|
|
|
|
var binary = AcBinarySerializer.Serialize(original);
|
|
var result = binary.BinaryTo<DateTime>();
|
|
|
|
Assert.AreEqual(original, result);
|
|
Assert.AreEqual(DateTimeKind.Utc, result.Kind);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void NullableDateTime_RoundTrip_PreservesValue()
|
|
{
|
|
DateTime? original = new DateTime(2025, 10, 24, 0, 27, 0, DateTimeKind.Utc);
|
|
|
|
var binary = AcBinarySerializer.Serialize(original);
|
|
var result = binary.BinaryTo<DateTime?>();
|
|
|
|
Assert.AreEqual(original, result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void NullableDateTime_Null_RoundTrip_PreservesNull()
|
|
{
|
|
DateTime? original = null;
|
|
|
|
var binary = AcBinarySerializer.Serialize(original);
|
|
var result = binary.BinaryTo<DateTime?>();
|
|
|
|
Assert.IsNull(result);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Object with DateTime Property
|
|
|
|
[TestMethod]
|
|
public void ObjectWithDateTime_RoundTrip_PreservesValue()
|
|
{
|
|
var original = new TestObjectWithDateTime
|
|
{
|
|
Id = 1,
|
|
CreatedAt = new DateTime(2025, 10, 24, 0, 27, 0, DateTimeKind.Utc),
|
|
UpdatedAt = new DateTime(2025, 12, 31, 23, 59, 59, DateTimeKind.Utc)
|
|
};
|
|
|
|
var binary = original.ToBinary();
|
|
var result = binary.BinaryTo<TestObjectWithDateTime>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(original.Id, result.Id);
|
|
Assert.AreEqual(original.CreatedAt, result.CreatedAt);
|
|
Assert.AreEqual(original.UpdatedAt, result.UpdatedAt);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ObjectWithNullableDateTime_Null_RoundTrip_PreservesNull()
|
|
{
|
|
var original = new TestObjectWithNullableDateTime
|
|
{
|
|
Id = 1,
|
|
DateOfReceipt = null
|
|
};
|
|
|
|
var binary = original.ToBinary();
|
|
var result = binary.BinaryTo<TestObjectWithNullableDateTime>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(original.Id, result.Id);
|
|
Assert.IsNull(result.DateOfReceipt);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ObjectWithNullableDateTime_HasValue_RoundTrip_PreservesValue()
|
|
{
|
|
var original = new TestObjectWithNullableDateTime
|
|
{
|
|
Id = 1,
|
|
DateOfReceipt = new DateTime(2025, 10, 24, 0, 27, 0, DateTimeKind.Utc)
|
|
};
|
|
|
|
var binary = original.ToBinary();
|
|
var result = binary.BinaryTo<TestObjectWithNullableDateTime>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(original.Id, result.Id);
|
|
Assert.AreEqual(original.DateOfReceipt, result.DateOfReceipt);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GenericAttribute-like Scenario (String stored DateTime)
|
|
|
|
[TestMethod]
|
|
public void GenericAttributeScenario_DateTimeAsString_PreservesValue()
|
|
{
|
|
var original = new TestGenericAttribute
|
|
{
|
|
Key = "DateOfReceipt",
|
|
Value = "10/24/2025 00:27:00"
|
|
};
|
|
|
|
var binary = original.ToBinary();
|
|
var result = binary.BinaryTo<TestGenericAttribute>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(original.Key, result.Key);
|
|
Assert.AreEqual(original.Value, result.Value);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void GenericAttributeScenario_ZeroValue_PreservesValue()
|
|
{
|
|
var original = new TestGenericAttribute
|
|
{
|
|
Key = "DateOfReceipt",
|
|
Value = "0"
|
|
};
|
|
|
|
var binary = original.ToBinary();
|
|
var result = binary.BinaryTo<TestGenericAttribute>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(original.Key, result.Key);
|
|
Assert.AreEqual("0", result.Value);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void GenericAttributeList_RoundTrip_PreservesAllValues()
|
|
{
|
|
var original = new List<TestGenericAttribute>
|
|
{
|
|
new() { Key = "DateOfReceipt", Value = "10/24/2025 00:27:00" },
|
|
new() { Key = "SomeNumber", Value = "42" },
|
|
new() { Key = "EmptyValue", Value = "" },
|
|
new() { Key = "ZeroValue", Value = "0" }
|
|
};
|
|
|
|
var binary = original.ToBinary();
|
|
var result = binary.BinaryTo<List<TestGenericAttribute>>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(4, result.Count);
|
|
|
|
Assert.AreEqual("DateOfReceipt", result[0].Key);
|
|
Assert.AreEqual("10/24/2025 00:27:00", result[0].Value);
|
|
|
|
Assert.AreEqual("SomeNumber", result[1].Key);
|
|
Assert.AreEqual("42", result[1].Value);
|
|
|
|
Assert.AreEqual("EmptyValue", result[2].Key);
|
|
Assert.AreEqual("", result[2].Value);
|
|
|
|
Assert.AreEqual("ZeroValue", result[3].Key);
|
|
Assert.AreEqual("0", result[3].Value);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ObjectWithGenericAttributes_RoundTrip_PreservesAllValues()
|
|
{
|
|
var original = new TestDtoWithGenericAttributes
|
|
{
|
|
Id = 123,
|
|
Name = "Test Order",
|
|
GenericAttributes =
|
|
[
|
|
new() { Key = "DateOfReceipt", Value = "10/24/2025 00:27:00" },
|
|
new() { Key = "Priority", Value = "1" }
|
|
]
|
|
};
|
|
|
|
var binary = original.ToBinary();
|
|
var result = binary.BinaryTo<TestDtoWithGenericAttributes>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(123, result.Id);
|
|
Assert.AreEqual("Test Order", result.Name);
|
|
Assert.AreEqual(2, result.GenericAttributes.Count);
|
|
|
|
var dateAttr = result.GenericAttributes.FirstOrDefault(x => x.Key == "DateOfReceipt");
|
|
Assert.IsNotNull(dateAttr);
|
|
Assert.AreEqual("10/24/2025 00:27:00", dateAttr.Value);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region JSON vs Binary Comparison
|
|
|
|
[TestMethod]
|
|
public void GenericAttribute_JsonAndBinary_ProduceSameResult()
|
|
{
|
|
var original = new TestGenericAttribute
|
|
{
|
|
Key = "DateOfReceipt",
|
|
Value = "10/24/2025 00:27:00"
|
|
};
|
|
|
|
var json = original.ToJson();
|
|
var jsonResult = json.JsonTo<TestGenericAttribute>();
|
|
|
|
var binary = original.ToBinary();
|
|
var binaryResult = binary.BinaryTo<TestGenericAttribute>();
|
|
|
|
Assert.IsNotNull(jsonResult);
|
|
Assert.IsNotNull(binaryResult);
|
|
|
|
Assert.AreEqual(jsonResult.Key, binaryResult.Key);
|
|
Assert.AreEqual(jsonResult.Value, binaryResult.Value);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void DtoWithGenericAttributes_JsonAndBinary_ProduceSameResult()
|
|
{
|
|
var original = new TestDtoWithGenericAttributes
|
|
{
|
|
Id = 123,
|
|
Name = "Test Order",
|
|
GenericAttributes =
|
|
[
|
|
new() { Key = "DateOfReceipt", Value = "10/24/2025 00:27:00" },
|
|
new() { Key = "ZeroValue", Value = "0" }
|
|
]
|
|
};
|
|
|
|
var json = original.ToJson();
|
|
var jsonResult = json.JsonTo<TestDtoWithGenericAttributes>();
|
|
|
|
var binary = original.ToBinary();
|
|
var binaryResult = binary.BinaryTo<TestDtoWithGenericAttributes>();
|
|
|
|
Assert.IsNotNull(jsonResult);
|
|
Assert.IsNotNull(binaryResult);
|
|
|
|
Assert.AreEqual(jsonResult.Id, binaryResult.Id);
|
|
Assert.AreEqual(jsonResult.Name, binaryResult.Name);
|
|
Assert.AreEqual(jsonResult.GenericAttributes.Count, binaryResult.GenericAttributes.Count);
|
|
|
|
for (int i = 0; i < jsonResult.GenericAttributes.Count; i++)
|
|
{
|
|
Assert.AreEqual(jsonResult.GenericAttributes[i].Key, binaryResult.GenericAttributes[i].Key);
|
|
Assert.AreEqual(jsonResult.GenericAttributes[i].Value, binaryResult.GenericAttributes[i].Value);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Test Models
|
|
|
|
public class TestObjectWithDateTime
|
|
{
|
|
public int Id { get; set; }
|
|
public DateTime CreatedAt { get; set; }
|
|
public DateTime UpdatedAt { get; set; }
|
|
}
|
|
|
|
public class TestObjectWithNullableDateTime
|
|
{
|
|
public int Id { get; set; }
|
|
public DateTime? DateOfReceipt { get; set; }
|
|
}
|
|
|
|
public class TestGenericAttribute
|
|
{
|
|
public string Key { get; set; } = "";
|
|
public string Value { get; set; } = "";
|
|
}
|
|
|
|
public class TestDtoWithGenericAttributes
|
|
{
|
|
public int Id { get; set; }
|
|
public string Name { get; set; } = "";
|
|
public List<TestGenericAttribute> GenericAttributes { get; set; } = [];
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Exact Production Scenario Test
|
|
|
|
/// <summary>
|
|
/// This test reproduces the exact production bug scenario:
|
|
/// 1. Server sends Binary serialized response with GenericAttributes
|
|
/// 2. Client deserializes the Binary response
|
|
/// 3. Client accesses DateOfReceipt property which reads from GenericAttributes
|
|
/// 4. CommonHelper2.To<DateTime> fails to parse the string value
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void ProductionScenario_GenericAttributeWithDateString_PreservesExactFormat()
|
|
{
|
|
// Arrange: Create DTO with GenericAttributes like in production
|
|
var original = new TestDtoWithGenericAttributes
|
|
{
|
|
Id = 123,
|
|
Name = "Test Order",
|
|
GenericAttributes =
|
|
[
|
|
new() { Key = "DateOfReceipt", Value = "10/24/2025 00:27:00" },
|
|
new() { Key = "Priority", Value = "1" },
|
|
new() { Key = "SomeFlag", Value = "true" }
|
|
]
|
|
};
|
|
|
|
// Act: Binary round-trip (simulates server->client communication)
|
|
var binary = original.ToBinary();
|
|
var result = binary.BinaryTo<TestDtoWithGenericAttributes>();
|
|
|
|
// Assert: The exact string value must be preserved
|
|
Assert.IsNotNull(result);
|
|
var dateAttr = result.GenericAttributes.FirstOrDefault(x => x.Key == "DateOfReceipt");
|
|
Assert.IsNotNull(dateAttr, "DateOfReceipt attribute should exist");
|
|
|
|
// This is the critical assertion - the EXACT string must be preserved
|
|
Assert.AreEqual("10/24/2025 00:27:00", dateAttr.Value,
|
|
$"Expected '10/24/2025 00:27:00' but got '{dateAttr.Value}'");
|
|
|
|
// Verify it can be parsed with US culture (which is how it was stored)
|
|
Assert.IsTrue(DateTime.TryParse(dateAttr.Value, new System.Globalization.CultureInfo("en-US"),
|
|
System.Globalization.DateTimeStyles.None, out var parsedDate),
|
|
$"Value '{dateAttr.Value}' should be parseable as US date format");
|
|
|
|
Assert.AreEqual(new DateTime(2025, 10, 24, 0, 27, 0), parsedDate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that verifies the exact bytes of the string are preserved.
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void ProductionScenario_StringWithSlashes_BytesArePreserved()
|
|
{
|
|
var original = "10/24/2025 00:27:00";
|
|
var originalBytes = System.Text.Encoding.UTF8.GetBytes(original);
|
|
|
|
// Serialize and deserialize
|
|
var binary = AcBinarySerializer.Serialize(original);
|
|
var result = binary.BinaryTo<string>();
|
|
|
|
Assert.IsNotNull(result);
|
|
var resultBytes = System.Text.Encoding.UTF8.GetBytes(result);
|
|
|
|
// Compare byte-by-byte
|
|
Assert.AreEqual(originalBytes.Length, resultBytes.Length, "String length changed after serialization");
|
|
for (int i = 0; i < originalBytes.Length; i++)
|
|
{
|
|
Assert.AreEqual(originalBytes[i], resultBytes[i],
|
|
$"Byte at position {i} differs: expected {originalBytes[i]:X2} ('{(char)originalBytes[i]}'), got {resultBytes[i]:X2} ('{(char)resultBytes[i]}')");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test with large list of GenericAttributes to catch any edge cases.
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void ProductionScenario_ManyGenericAttributes_AllPreserved()
|
|
{
|
|
var original = new TestDtoWithGenericAttributes
|
|
{
|
|
Id = 999,
|
|
Name = "Large Order",
|
|
GenericAttributes = Enumerable.Range(0, 50).Select(i => new TestGenericAttribute
|
|
{
|
|
Key = $"Attribute_{i}",
|
|
Value = i % 5 == 0 ? $"{i}/24/2025 00:00:00" : i.ToString()
|
|
}).ToList()
|
|
};
|
|
|
|
var binary = original.ToBinary();
|
|
var result = binary.BinaryTo<TestDtoWithGenericAttributes>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(50, result.GenericAttributes.Count);
|
|
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
var expectedValue = i % 5 == 0 ? $"{i}/24/2025 00:00:00" : i.ToString();
|
|
Assert.AreEqual($"Attribute_{i}", result.GenericAttributes[i].Key);
|
|
Assert.AreEqual(expectedValue, result.GenericAttributes[i].Value,
|
|
$"Attribute_{i} value mismatch: expected '{expectedValue}', got '{result.GenericAttributes[i].Value}'");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region CommonHelper2.To<DateTime> Simulation Tests
|
|
|
|
/// <summary>
|
|
/// This test simulates what CommonHelper2.To<DateTime> does with various string values.
|
|
/// It helps identify which values will cause the FormatException.
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void CommonHelperSimulation_ValidDateString_ParsesSuccessfully()
|
|
{
|
|
var dateString = "10/24/2025 00:27:00";
|
|
|
|
// This is what CommonHelper2.To<DateTime> does internally
|
|
var converter = System.ComponentModel.TypeDescriptor.GetConverter(typeof(DateTime));
|
|
Assert.IsTrue(converter.CanConvertFrom(typeof(string)));
|
|
|
|
// With InvariantCulture
|
|
var result = converter.ConvertFrom(null, System.Globalization.CultureInfo.InvariantCulture, dateString);
|
|
Assert.IsNotNull(result);
|
|
Assert.IsInstanceOfType(result, typeof(DateTime));
|
|
var dt = (DateTime)result;
|
|
// InvariantCulture interprets 10/24/2025 as October 24, 2025
|
|
Assert.AreEqual(2025, dt.Year);
|
|
Assert.AreEqual(10, dt.Month);
|
|
Assert.AreEqual(24, dt.Day);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This test shows that "0" cannot be parsed as DateTime - this is the actual bug!
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void CommonHelperSimulation_ZeroString_ThrowsFormatException()
|
|
{
|
|
var invalidValue = "0";
|
|
|
|
var converter = System.ComponentModel.TypeDescriptor.GetConverter(typeof(DateTime));
|
|
|
|
// This should throw FormatException - exactly what we see in production
|
|
var threw = false;
|
|
try
|
|
{
|
|
converter.ConvertFrom(null, System.Globalization.CultureInfo.InvariantCulture, invalidValue);
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
threw = true;
|
|
}
|
|
|
|
Assert.IsTrue(threw, "Converting '0' to DateTime should throw FormatException");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test various invalid DateTime strings that might be stored in GenericAttributes.
|
|
/// </summary>
|
|
[TestMethod]
|
|
[DataRow("0")]
|
|
[DataRow("null")]
|
|
[DataRow("undefined")]
|
|
[DataRow("N/A")]
|
|
public void CommonHelperSimulation_InvalidDateStrings_ThrowFormatException(string invalidValue)
|
|
{
|
|
var converter = System.ComponentModel.TypeDescriptor.GetConverter(typeof(DateTime));
|
|
|
|
var threw = false;
|
|
try
|
|
{
|
|
converter.ConvertFrom(null, System.Globalization.CultureInfo.InvariantCulture, invalidValue);
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
threw = true;
|
|
}
|
|
|
|
Assert.IsTrue(threw, $"Converting '{invalidValue}' to DateTime should throw FormatException");
|
|
}
|
|
|
|
#endregion
|
|
}
|