Files
RealCV/tests/RealCV.Tests/Services/TimelineAnalyserServiceTests.cs

582 lines
19 KiB
C#
Raw Normal View History

using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using RealCV.Application.Models;
using RealCV.Infrastructure.Services;
namespace RealCV.Tests.Services;
public class TimelineAnalyserServiceTests
{
private readonly TimelineAnalyserService _sut;
private readonly Mock<ILogger<TimelineAnalyserService>> _loggerMock;
public TimelineAnalyserServiceTests()
{
_loggerMock = new Mock<ILogger<TimelineAnalyserService>>();
_sut = new TimelineAnalyserService(_loggerMock.Object);
}
#region Empty and Single Entry Tests
[Fact]
public void Analyse_WithEmptyEmploymentList_ReturnsEmptyResults()
{
// Arrange
var employmentHistory = new List<EmploymentEntry>();
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.TotalGapMonths.Should().Be(0);
result.TotalOverlapMonths.Should().Be(0);
result.Gaps.Should().BeEmpty();
result.Overlaps.Should().BeEmpty();
}
[Fact]
public void Analyse_WithSingleEmploymentEntry_ReturnsNoGapsOrOverlaps()
{
// Arrange
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.TotalGapMonths.Should().Be(0);
result.TotalOverlapMonths.Should().Be(0);
result.Gaps.Should().BeEmpty();
result.Overlaps.Should().BeEmpty();
}
#endregion
#region Gap Detection Tests
[Fact]
public void Analyse_WithTwoConsecutiveJobs_ReturnsNoGaps()
{
// Arrange
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 12, 31)),
CreateEmployment("Company B", new DateOnly(2021, 1, 1), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.TotalGapMonths.Should().Be(0);
result.Gaps.Should().BeEmpty();
}
[Fact]
public void Analyse_WithThreeMonthGap_DetectsGapCorrectly()
{
// Arrange
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 6, 30)),
CreateEmployment("Company B", new DateOnly(2020, 10, 1), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Gaps.Should().HaveCount(1);
result.Gaps[0].StartDate.Should().Be(new DateOnly(2020, 6, 30));
result.Gaps[0].EndDate.Should().Be(new DateOnly(2020, 10, 1));
result.Gaps[0].Months.Should().BeGreaterThanOrEqualTo(3);
result.TotalGapMonths.Should().BeGreaterThanOrEqualTo(3);
}
[Fact]
public void Analyse_WithLargeGap_DetectsGapWithCorrectMonths()
{
// Arrange - 6 month gap
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 6, 15)),
CreateEmployment("Company B", new DateOnly(2021, 1, 15), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Gaps.Should().HaveCount(1);
result.TotalGapMonths.Should().BeGreaterThanOrEqualTo(6);
}
[Fact]
public void Analyse_WithSmallGapUnderThreeMonths_DoesNotFlagIt()
{
// Arrange - 2 month gap (should not be flagged)
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 6, 30)),
CreateEmployment("Company B", new DateOnly(2020, 8, 15), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Gaps.Should().BeEmpty();
result.TotalGapMonths.Should().Be(0);
}
[Fact]
public void Analyse_WithOneMonthGap_DoesNotFlagIt()
{
// Arrange - 1 month gap
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 6, 30)),
CreateEmployment("Company B", new DateOnly(2020, 8, 1), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Gaps.Should().BeEmpty();
result.TotalGapMonths.Should().Be(0);
}
#endregion
#region Overlap Detection Tests
[Fact]
public void Analyse_WithOverlappingJobsMoreThanTwoMonths_DetectsOverlap()
{
// Arrange - 4 month overlap
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 8, 31)),
CreateEmployment("Company B", new DateOnly(2020, 5, 1), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Overlaps.Should().HaveCount(1);
result.Overlaps[0].Company1.Should().Be("Company A");
result.Overlaps[0].Company2.Should().Be("Company B");
result.Overlaps[0].Months.Should().BeGreaterThan(2);
result.TotalOverlapMonths.Should().BeGreaterThan(2);
}
[Fact]
public void Analyse_WithOneMonthTransitionOverlap_DoesNotFlagIt()
{
// Arrange - 1 month overlap (transition period, should be allowed)
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 6, 15)),
CreateEmployment("Company B", new DateOnly(2020, 6, 1), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Overlaps.Should().BeEmpty();
result.TotalOverlapMonths.Should().Be(0);
}
[Fact]
public void Analyse_WithTwoMonthTransitionOverlap_DoesNotFlagIt()
{
// Arrange - 2 month overlap (transition period, should be allowed)
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 7, 31)),
CreateEmployment("Company B", new DateOnly(2020, 6, 1), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Overlaps.Should().BeEmpty();
result.TotalOverlapMonths.Should().Be(0);
}
[Fact]
public void Analyse_WithThreeMonthOverlap_DetectsOverlap()
{
// Arrange - 3 month overlap (exceeds 2 month allowed period)
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 8, 31)),
CreateEmployment("Company B", new DateOnly(2020, 6, 1), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Overlaps.Should().HaveCount(1);
result.Overlaps[0].Months.Should().BeGreaterThan(2);
}
#endregion
#region Mixed Scenarios Tests
[Fact]
public void Analyse_WithMultipleJobsMixedGapsAndOverlaps_DetectsAll()
{
// Arrange
var employmentHistory = new List<EmploymentEntry>
{
// Job 1: Jan 2019 - Dec 2019
CreateEmployment("Company A", new DateOnly(2019, 1, 1), new DateOnly(2019, 12, 31)),
// Job 2: June 2020 - Dec 2020 (6 month gap after Job 1 - should be detected)
CreateEmployment("Company B", new DateOnly(2020, 6, 1), new DateOnly(2020, 12, 31)),
// Job 3: Sep 2020 - June 2021 (4 month overlap with Job 2 - should be detected)
CreateEmployment("Company C", new DateOnly(2020, 9, 1), new DateOnly(2021, 6, 30))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Gaps.Should().HaveCount(1);
result.Gaps[0].StartDate.Should().Be(new DateOnly(2019, 12, 31));
result.Gaps[0].EndDate.Should().Be(new DateOnly(2020, 6, 1));
result.Overlaps.Should().HaveCount(1);
result.Overlaps[0].Company1.Should().Be("Company B");
result.Overlaps[0].Company2.Should().Be("Company C");
}
[Fact]
public void Analyse_WithMultipleGaps_SumsTotalGapMonths()
{
// Arrange
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2019, 1, 1), new DateOnly(2019, 6, 30)),
// 4 month gap
CreateEmployment("Company B", new DateOnly(2019, 11, 1), new DateOnly(2020, 4, 30)),
// 5 month gap
CreateEmployment("Company C", new DateOnly(2020, 10, 1), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Gaps.Should().HaveCount(2);
result.TotalGapMonths.Should().BeGreaterThanOrEqualTo(8);
}
[Fact]
public void Analyse_WithMultipleOverlaps_SumsTotalOverlapMonths()
{
// Arrange
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 9, 30)),
CreateEmployment("Company B", new DateOnly(2020, 5, 1), new DateOnly(2021, 3, 31)),
CreateEmployment("Company C", new DateOnly(2020, 12, 1), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Overlaps.Should().HaveCountGreaterThan(0);
result.TotalOverlapMonths.Should().BeGreaterThan(0);
}
#endregion
#region Null Date Handling Tests
[Fact]
public void Analyse_WithAllNullStartDates_ReturnsEmptyResults()
{
// Arrange
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", null, new DateOnly(2020, 12, 31)),
CreateEmployment("Company B", null, new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.TotalGapMonths.Should().Be(0);
result.TotalOverlapMonths.Should().Be(0);
result.Gaps.Should().BeEmpty();
result.Overlaps.Should().BeEmpty();
}
[Fact]
public void Analyse_WithSomeNullStartDates_FiltersThemOut()
{
// Arrange
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 6, 30)),
CreateEmployment("Company B", null, new DateOnly(2020, 12, 31)),
CreateEmployment("Company C", new DateOnly(2021, 1, 1), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
// Company B is filtered out, gap between A and C is 6 months
result.Gaps.Should().HaveCount(1);
result.Gaps[0].Months.Should().BeGreaterThanOrEqualTo(6);
}
[Fact]
public void Analyse_WithMixedNullDates_HandlesGracefully()
{
// Arrange
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", null, null),
CreateEmployment("Company B", new DateOnly(2020, 1, 1), new DateOnly(2020, 12, 31)),
CreateEmployment("Company C", null, new DateOnly(2021, 6, 30))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert - only Company B has valid start date, so no gaps/overlaps possible
result.Gaps.Should().BeEmpty();
result.Overlaps.Should().BeEmpty();
}
#endregion
#region Current Job (No End Date) Tests
[Fact]
public void Analyse_WithCurrentJobNoEndDate_UsesTodayAsEndDate()
{
// Arrange
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 6, 30)),
CreateCurrentEmployment("Company B", new DateOnly(2020, 7, 1))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Gaps.Should().BeEmpty();
result.TotalGapMonths.Should().Be(0);
}
[Fact]
public void Analyse_WithCurrentJobOverlappingPastJob_DetectsOverlap()
{
// Arrange - Current job started while still at previous job
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2024, 1, 1), new DateOnly(2024, 12, 31)),
CreateCurrentEmployment("Company B", new DateOnly(2024, 6, 1))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Overlaps.Should().HaveCount(1);
result.Overlaps[0].Company1.Should().Be("Company A");
result.Overlaps[0].Company2.Should().Be("Company B");
result.Overlaps[0].Months.Should().BeGreaterThan(2);
}
[Fact]
public void Analyse_WithTwoCurrentJobs_DetectsLargeOverlap()
{
// Arrange - Two concurrent current positions (unusual but valid scenario)
var employmentHistory = new List<EmploymentEntry>
{
CreateCurrentEmployment("Company A", new DateOnly(2023, 1, 1)),
CreateCurrentEmployment("Company B", new DateOnly(2023, 6, 1))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert - Should detect substantial overlap since both run to today
result.Overlaps.Should().HaveCount(1);
result.TotalOverlapMonths.Should().BeGreaterThan(2);
}
[Fact]
public void Analyse_WithGapBeforeCurrentJob_DetectsGap()
{
// Arrange
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2023, 1, 1), new DateOnly(2023, 6, 30)),
CreateCurrentEmployment("Company B", new DateOnly(2024, 1, 1))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Gaps.Should().HaveCount(1);
result.Gaps[0].Months.Should().BeGreaterThanOrEqualTo(6);
}
#endregion
#region Edge Cases and Ordering Tests
[Fact]
public void Analyse_WithUnsortedEmployment_SortsByStartDate()
{
// Arrange - Jobs provided out of order
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company C", new DateOnly(2022, 1, 1), new DateOnly(2022, 12, 31)),
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 6, 30)),
CreateEmployment("Company B", new DateOnly(2021, 1, 1), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert - Should detect gap between A (ends June 2020) and B (starts Jan 2021)
result.Gaps.Should().HaveCount(1);
result.Gaps[0].StartDate.Should().Be(new DateOnly(2020, 6, 30));
result.Gaps[0].EndDate.Should().Be(new DateOnly(2021, 1, 1));
}
[Fact]
public void Analyse_WithSameStartDate_HandlesCorrectly()
{
// Arrange - Two jobs starting on same date
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 12, 31)),
CreateEmployment("Company B", new DateOnly(2020, 1, 1), new DateOnly(2020, 8, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert - Should detect overlap
result.Overlaps.Should().HaveCount(1);
}
[Fact]
public void Analyse_WithNullList_ThrowsArgumentNullException()
{
// Arrange
List<EmploymentEntry>? employmentHistory = null;
// Act
var act = () => _sut.Analyse(employmentHistory!);
// Assert
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Analyse_WithExactlyThreeMonthGap_DetectsGap()
{
// Arrange - Exactly 3 month gap (boundary condition)
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 3, 31)),
CreateEmployment("Company B", new DateOnly(2020, 7, 1), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Gaps.Should().HaveCount(1);
result.Gaps[0].Months.Should().BeGreaterThanOrEqualTo(3);
}
[Fact]
public void Analyse_WithJobEndingSameDayNextStarts_NoGap()
{
// Arrange - Seamless transition
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 6, 30)),
CreateEmployment("Company B", new DateOnly(2020, 6, 30), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Gaps.Should().BeEmpty();
}
[Fact]
public void Analyse_WithJobEndingAfterNextStarts_DetectsOverlapIfSignificant()
{
// Arrange - Job ends after next starts by more than 2 months
var employmentHistory = new List<EmploymentEntry>
{
CreateEmployment("Company A", new DateOnly(2020, 1, 1), new DateOnly(2020, 10, 31)),
CreateEmployment("Company B", new DateOnly(2020, 6, 1), new DateOnly(2021, 12, 31))
};
// Act
var result = _sut.Analyse(employmentHistory);
// Assert
result.Overlaps.Should().HaveCount(1);
result.Overlaps[0].Months.Should().BeGreaterThan(2);
}
#endregion
#region Helper Methods
private static EmploymentEntry CreateEmployment(string companyName, DateOnly? startDate, DateOnly? endDate)
{
return new EmploymentEntry
{
CompanyName = companyName,
JobTitle = "Software Developer",
StartDate = startDate,
EndDate = endDate,
IsCurrent = false
};
}
private static EmploymentEntry CreateCurrentEmployment(string companyName, DateOnly startDate)
{
return new EmploymentEntry
{
CompanyName = companyName,
JobTitle = "Software Developer",
StartDate = startDate,
EndDate = null,
IsCurrent = true
};
}
#endregion
}