diff --git a/Src/AutoFixture/DataAnnotations/DateTimeRangedRequestRelay.cs b/Src/AutoFixture/DataAnnotations/DateTimeRangedRequestRelay.cs new file mode 100644 index 000000000..b5c7b7298 --- /dev/null +++ b/Src/AutoFixture/DataAnnotations/DateTimeRangedRequestRelay.cs @@ -0,0 +1,65 @@ +using System; +using System.Globalization; +using AutoFixture.Kernel; + +namespace AutoFixture.DataAnnotations +{ + /// + /// Handles of DateTime type by forwarding requests + /// to the with min and max DateTime as ticks values. + /// + public class DateTimeRangedRequestRelay : ISpecimenBuilder + { + /// + public object Create(object request, ISpecimenContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + + var rangedRequest = request as RangedRequest; + + if (rangedRequest == null) + return new NoSpecimen(); + + if (rangedRequest.MemberType != typeof(DateTime)) + return new NoSpecimen(); + + return CreateDateTimeSpecimen(rangedRequest, context); + } + + private static object CreateDateTimeSpecimen(RangedRequest rangedRequest, ISpecimenContext context) + { + if (!(rangedRequest.Minimum is string) || !(rangedRequest.Maximum is string)) + return new NoSpecimen(); + + var range = ParseTimeSpanRange(rangedRequest); + return RandomizeDateTimeInRange(range, context); + } + + private static DateTimeRange ParseTimeSpanRange(RangedRequest rangedRequest) + { + return new DateTimeRange + { + Min = DateTime.Parse((string)rangedRequest.Minimum, CultureInfo.CurrentCulture), + Max = DateTime.Parse((string)rangedRequest.Maximum, CultureInfo.CurrentCulture) + }; + } + + private static object RandomizeDateTimeInRange(DateTimeRange range, ISpecimenContext context) + { + var ticksInRange = context.Resolve( + new RangedNumberRequest(typeof(long), range.Min.Ticks, range.Max.Ticks)); + + if (ticksInRange is NoSpecimen) + return new NoSpecimen(); + + return new DateTime((long)ticksInRange); + } + + private struct DateTimeRange + { + public DateTime Min { get; set; } + + public DateTime Max { get; set; } + } + } +} diff --git a/Src/AutoFixture/Fixture.cs b/Src/AutoFixture/Fixture.cs index 2126d4f02..ae13b10f5 100644 --- a/Src/AutoFixture/Fixture.cs +++ b/Src/AutoFixture/Fixture.cs @@ -124,6 +124,7 @@ public Fixture(ISpecimenBuilder engine, MultipleRelay multiple) new RangeAttributeRelay(), new NumericRangedRequestRelay(), new EnumRangedRequestRelay(), + new DateTimeRangedRequestRelay(), new TimeSpanRangedRequestRelay(), new StringLengthAttributeRelay(), new MinAndMaxLengthAttributeRelay(), diff --git a/Src/AutoFixtureUnitTest/DataAnnotations/DateTimeRangedRequestRelayTest.cs b/Src/AutoFixtureUnitTest/DataAnnotations/DateTimeRangedRequestRelayTest.cs new file mode 100644 index 000000000..23264ca23 --- /dev/null +++ b/Src/AutoFixtureUnitTest/DataAnnotations/DateTimeRangedRequestRelayTest.cs @@ -0,0 +1,118 @@ +using System; +using AutoFixture.DataAnnotations; +using AutoFixture.Kernel; +using AutoFixtureUnitTest.Kernel; +using Xunit; + +namespace AutoFixtureUnitTest.DataAnnotations +{ + public class DateTimeRangedRequestRelayTest + { + [Fact] + public void SutShouldBeASpecimenBuilder() + { + // Arrange + var sut = new DateTimeRangedRequestRelay(); + + // Act & Assert + Assert.IsAssignableFrom(sut); + } + + [Fact] + public void ShouldFailForNullContext() + { + // Arrange + var sut = new DateTimeRangedRequestRelay(); + var request = new object(); + + // Act & Assert + Assert.ThrowsAny(() => + sut.Create(request, null)); + } + + [Fact] + public void ShouldNotHandleRequestIfMemberTypeIsNotDateTime() + { + // Arrange + var sut = new DateTimeRangedRequestRelay(); + var request = + new RangedRequest(memberType: typeof(object), operandType: typeof(object), minimum: 0, maximum: 1); + var context = new DelegatingSpecimenContext(); + + // Act + var result = sut.Create(request, context); + + // Assert + Assert.Equal(new NoSpecimen(), result); + } + + [Fact] + public void ShouldHandleRequestOfDateTimeType() + { + // Arrange + var sut = new DateTimeRangedRequestRelay(); + var request = + new RangedRequest(memberType: typeof(DateTime), operandType: typeof(DateTime), minimum: "2020-01-01 00:00:00", + maximum: "2020-12-31 23:59:59"); + + var context = new DelegatingSpecimenContext + { + OnResolve = _ => new DateTime(2020, 06, 15, 12, 30, 30).Ticks + }; + + // Act + var actualResult = sut.Create(request, context); + + // Assert + Assert.Equal(new DateTime(2020, 06, 15, 12, 30, 30), actualResult); + } + + [Fact] + public void ShouldReturnNoSpecimenIfUnableToSatisfyRangedRequest() + { + // Arrange + var sut = new DateTimeRangedRequestRelay(); + var request = + new RangedRequest(memberType: typeof(DateTime), operandType: typeof(DateTime), minimum: "2020-01-01 00:00:00", + maximum: "2020-12-31 23:59:59"); + + var context = new DelegatingSpecimenContext + { + OnResolve = _ => new NoSpecimen() + }; + + // Act + var actualResult = sut.Create(request, context); + + // Assert + Assert.Equal(new NoSpecimen(), actualResult); + } + + [Fact] + public void ShouldCorrectPassMinimumAndMaximumAsTicks() + { + // Arrange + var sut = new DateTimeRangedRequestRelay(); + + var request = new RangedRequest(typeof(DateTime), typeof(DateTime), "2020-01-01 00:00:00", "2020-12-31 23:59:59"); + RangedNumberRequest capturedNumericRequest = null; + + var context = new DelegatingSpecimenContext + { + OnResolve = r => + { + capturedNumericRequest = (RangedNumberRequest)r; + return new NoSpecimen(); + } + }; + + // Act + sut.Create(request, context); + + // Assert + Assert.NotNull(capturedNumericRequest); + Assert.Equal(new DateTime(2020, 1, 1, 0, 0, 0).Ticks, capturedNumericRequest.Minimum); + Assert.Equal(new DateTime(2020, 12, 31, 23, 59, 59).Ticks, capturedNumericRequest.Maximum); + } + } +} diff --git a/Src/AutoFixtureUnitTest/FixtureTest.cs b/Src/AutoFixtureUnitTest/FixtureTest.cs index c8e972f5d..ee6384b7b 100644 --- a/Src/AutoFixtureUnitTest/FixtureTest.cs +++ b/Src/AutoFixtureUnitTest/FixtureTest.cs @@ -6147,6 +6147,25 @@ IPostprocessComposer ConfigurePropertyField(IPostprocessComposer compos } } + private class TypeWithRangedDateTimeProperty + { + [Range(typeof(DateTime), "01/01/1900", "12/31/2020")] + public DateTime StringRangedDateTimeProperty { get; set; } + } + + [Fact] + public void ShouldCorrectlyResolveDateTimePropertiesDecoratedWithRange() + { + // Arrange + var sut = new Fixture(); + + // Act + var result = sut.Create(); + + // Assert + Assert.InRange(result.StringRangedDateTimeProperty, new DateTime(1900, 1, 1), new DateTime(2020, 12, 31)); + } + private class TypeWithRangedTimeSpanProperty { [Range(typeof(TimeSpan), "02:00:00", "12:00:00")] @@ -6386,4 +6405,4 @@ public void ShouldResolveFixedNumberOfItemsForRangedSequenceRequest() Assert.Equal(expectedLength, resultSeq.Count()); } } -} \ No newline at end of file +}