Skip to content

Commit

Permalink
Merge pull request #229 from DFE-Digital/chore/date-filtering
Browse files Browse the repository at this point in the history
Avoid using culture info for month abbreviations.
Log error if date format wrong in the data.
  • Loading branch information
RobertGHippo authored Jul 4, 2024
2 parents c1bf075 + eb02d0c commit a2d779f
Show file tree
Hide file tree
Showing 2 changed files with 240 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,24 @@ public class ContentfulContentFilterService(
: IContentFilterService
{
private const int Day = 28;
private static readonly DateTimeFormatInfo CurrentFormatInfo = CultureInfo.CurrentCulture.DateTimeFormat;

private readonly ReadOnlyDictionary<int, string>
_months = new(
new Dictionary<int, string>
{
{ 1, CurrentFormatInfo.AbbreviatedMonthNames[0] },
{ 2, CurrentFormatInfo.AbbreviatedMonthNames[1] },
{ 3, CurrentFormatInfo.AbbreviatedMonthNames[2] },
{ 4, CurrentFormatInfo.AbbreviatedMonthNames[3] },
{ 5, CurrentFormatInfo.AbbreviatedMonthNames[4] },
{ 6, CurrentFormatInfo.AbbreviatedMonthNames[5] },
{ 7, CurrentFormatInfo.AbbreviatedMonthNames[6] },
{ 8, CurrentFormatInfo.AbbreviatedMonthNames[7] },
{ 9, CurrentFormatInfo.AbbreviatedMonthNames[8] },
{ 10, CurrentFormatInfo.AbbreviatedMonthNames[9] },
{ 11, CurrentFormatInfo.AbbreviatedMonthNames[10] },
{ 12, CurrentFormatInfo.AbbreviatedMonthNames[11] }
});

private static readonly ReadOnlyDictionary<string, int>
Months = new(
new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase)
{
{ "Jan", 1 },
{ "Feb", 2 },
{ "Mar", 3 },
{ "Apr", 4 },
{ "May", 5 },
{ "Jun", 6 },
{ "Jul", 7 },
{ "Aug", 8 },
{ "Sep", 9 },
{ "Oct", 10 },
{ "Nov", 11 },
{ "Dec", 12 }
});

// Used by the unit tests to inject a mock builder that returns the query params
public QueryBuilder<Qualification> QueryBuilder { get; init; } = QueryBuilder<Qualification>.New;
Expand Down Expand Up @@ -94,6 +93,8 @@ private List<Qualification> FilterQualificationsByDate(int startDateMonth, int s
}
else if (qualificationStartDate is null
&& qualificationEndDate is not null
// ReSharper disable once MergeSequentialChecks
// ...reveals the intention more clearly this way
&& enteredStartDate <= qualificationEndDate)
{
// if qualification start date is null, check entered start date is <= ToWhichYear & add to results
Expand Down Expand Up @@ -124,12 +125,49 @@ private List<Qualification> FilterQualificationsByDate(int startDateMonth, int s

private DateOnly? ConvertToDateTime(string qualificationDate)
{
var splitQualificationDate = qualificationDate.Split('-');
if (splitQualificationDate.Length != 2) return null;
var (isValid, month, yearMod2000) = ValidateQualificationDate(qualificationDate);

if (!isValid)
{
return null;
}

var month = _months.FirstOrDefault(x => x.Value == splitQualificationDate[0]).Key;
var year = Convert.ToInt32(splitQualificationDate[1]) + 2000;
var year = yearMod2000 + 2000;

return new DateOnly(year, month, Day);
}

private (bool isValid, int month, int yearMod2000) ValidateQualificationDate(string qualificationDate)
{
var splitQualificationDate = qualificationDate.Split('-');
if (splitQualificationDate.Length != 2)
{
logger.LogError("Qualification date {QualificationDate} has unexpected format", qualificationDate);
return (false, 0, 0);
}

var abbreviatedMonth = splitQualificationDate[0];
var yearFilter = splitQualificationDate[1];

var yearIsValid = int.TryParse(yearFilter,
NumberStyles.Integer,
NumberFormatInfo.InvariantInfo,
out var yearPart);

if (!yearIsValid)
{
logger.LogError("Qualification date {QualificationDate} contains unexpected year value", qualificationDate);
return (false, 0, 0);
}

if (!Months.TryGetValue(abbreviatedMonth, out var month))
{
logger.LogError("Qualification date {QualificationDate} contains unexpected month value",
qualificationDate);

return (false, 0, 0);
}

return (true, month, yearPart);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Contentful.Core.Search;
using Dfe.EarlyYearsQualification.Content.Entities;
using Dfe.EarlyYearsQualification.Content.Services;
using Dfe.EarlyYearsQualification.UnitTests.Extensions;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
Expand Down Expand Up @@ -284,6 +285,73 @@ public async Task GetFilteredQualifications_FilterOnDates_ReturnsFilteredQualifi
filteredQualifications[2].QualificationId.Should().Be("EYQ-746");
}

[TestMethod]
public async Task GetFilteredQualifications_FilterOnDates_MonthIsCaseInsensitive_ReturnsFilteredQualifications()
{
var results = new ContentfulCollection<Qualification>
{
Items = new[]
{
new Qualification(
"EYQ-123",
"test",
"NCFE",
4,
"Apr-15",
"aug-19",
"abc/123/987",
"requirements"),
new Qualification(
"EYQ-741",
"test",
"Pearson",
4,
null,
"seP-19",
"def/456/951",
"requirements"),
new Qualification(
"EYQ-746",
"test",
"CACHE",
4,
"sEp-15",
null,
"ghi/456/951",
"requirements"),
new Qualification(
"EYQ-752",
"test",
"CACHE",
4,
"SEP-21",
null,
"ghi/456/951",
"requirements")
}
};
var mockContentfulClient = new Mock<IContentfulClient>();
mockContentfulClient.Setup(x => x.GetEntries(
It.IsAny<QueryBuilder<Qualification>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(results);

var mockQueryBuilder = new MockQueryBuilder();
var mockLogger = new Mock<ILogger<ContentfulContentFilterService>>();
var filterService = new ContentfulContentFilterService(mockContentfulClient.Object, mockLogger.Object)
{
QueryBuilder = mockQueryBuilder
};

var filteredQualifications = await filterService.GetFilteredQualifications(4, 5, 2016);

filteredQualifications.Should().NotBeNull();
filteredQualifications.Count.Should().Be(3);
filteredQualifications[0].QualificationId.Should().Be("EYQ-123");
filteredQualifications[1].QualificationId.Should().Be("EYQ-741");
filteredQualifications[2].QualificationId.Should().Be("EYQ-746");
}

[TestMethod]
public async Task GetFilteredQualifications_ContentfulClientThrowsException_ReturnsEmptyList()
{
Expand All @@ -301,6 +369,117 @@ public async Task GetFilteredQualifications_ContentfulClientThrowsException_Retu
filteredQualifications.Should().NotBeNull();
filteredQualifications.Should().BeEmpty();
}

[TestMethod]
public async Task GetFilteredQualifications_DataContainsInvalidDateFormat_LogsError()
{
var results = new ContentfulCollection<Qualification>
{
Items = new[]
{
new Qualification(
"EYQ-123",
"test",
"NCFE",
4,
"Sep15", // We expect Mmm-yy, e.g. "Sep-15"
"Aug-19",
"abc/123/987",
"requirements")
}
};

var mockContentfulClient = new Mock<IContentfulClient>();
mockContentfulClient.Setup(x => x.GetEntries(
It.IsAny<QueryBuilder<Qualification>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(results);

var mockQueryBuilder = new MockQueryBuilder();
var mockLogger = new Mock<ILogger<ContentfulContentFilterService>>();
var filterService = new ContentfulContentFilterService(mockContentfulClient.Object, mockLogger.Object)
{
QueryBuilder = mockQueryBuilder
};

await filterService.GetFilteredQualifications(4, 5, 2016);

mockLogger.VerifyError("Qualification date Sep15 has unexpected format");
}

[TestMethod]
public async Task GetFilteredQualifications_DataContainsInvalidMonth_LogsError()
{
var results = new ContentfulCollection<Qualification>
{
Items = new[]
{
new Qualification(
"EYQ-123",
"test",
"NCFE",
4,
"Sept-15", // "Sept" in the data: we expect "Sep"
"Aug-19",
"abc/123/987",
"requirements")
}
};

var mockContentfulClient = new Mock<IContentfulClient>();
mockContentfulClient.Setup(x => x.GetEntries(
It.IsAny<QueryBuilder<Qualification>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(results);

var mockQueryBuilder = new MockQueryBuilder();
var mockLogger = new Mock<ILogger<ContentfulContentFilterService>>();
var filterService = new ContentfulContentFilterService(mockContentfulClient.Object, mockLogger.Object)
{
QueryBuilder = mockQueryBuilder
};

await filterService.GetFilteredQualifications(4, 5, 2016);

mockLogger.VerifyError("Qualification date Sept-15 contains unexpected month value");
}

[TestMethod]
public async Task GetFilteredQualifications_DataContainsInvalidYear_LogsError()
{
var results = new ContentfulCollection<Qualification>
{
Items = new[]
{
new Qualification(
"EYQ-123",
"test",
"NCFE",
4,
"Sep-15",
"Aug-1a", // invalid year typo
"abc/123/987",
"requirements")
}
};

var mockContentfulClient = new Mock<IContentfulClient>();
mockContentfulClient.Setup(x => x.GetEntries(
It.IsAny<QueryBuilder<Qualification>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(results);

var mockQueryBuilder = new MockQueryBuilder();
var mockLogger = new Mock<ILogger<ContentfulContentFilterService>>();
var filterService = new ContentfulContentFilterService(mockContentfulClient.Object, mockLogger.Object)
{
QueryBuilder = mockQueryBuilder
};

await filterService.GetFilteredQualifications(4, 5, 2016);

mockLogger.VerifyError("Qualification date Aug-1a contains unexpected year value");
}
}

public class MockQueryBuilder : QueryBuilder<Qualification>
Expand Down

0 comments on commit a2d779f

Please sign in to comment.