first commit

This commit is contained in:
2026-09-10 16:43:09 +02:00
commit 79cf35f690
144 changed files with 8822 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
using App.Core.Entities.Maintenance;
using App.Core.Services.Maintenance;
namespace App.Core.Tests.Maintenance;
public class FrequencyCalculatorTests
{
private static MaintenancePlan CreatePlan(FrequencyType type, DateOnly startDate) => new()
{
MachineId = 1,
Name = "Test",
FrequencyType = type,
StartDate = startDate,
};
[Fact]
public void ComputeNextDueDate_Daily_ReturnsAnchorPlusOneDay()
{
var plan = CreatePlan(FrequencyType.Daily, new DateOnly(2026, 3, 10));
var next = FrequencyCalculator.ComputeNextDueDate(plan);
Assert.Equal(new DateOnly(2026, 3, 11), next);
}
[Fact]
public void ComputeNextDueDate_UsesLastExecutionDate_WhenPlanAlreadyRanOnce()
{
var plan = CreatePlan(FrequencyType.Daily, new DateOnly(2026, 1, 1));
plan.LastExecutionDateUtc = new DateOnly(2026, 3, 10);
var next = FrequencyCalculator.ComputeNextDueDate(plan);
Assert.Equal(new DateOnly(2026, 3, 11), next);
}
[Fact]
public void ComputeNextDueDate_Weekly_ReturnsNextConfiguredWeekday()
{
// 2026-03-10 est un mardi. Gamme hebdomadaire calée sur le lundi (1).
var plan = CreatePlan(FrequencyType.Weekly, new DateOnly(2026, 3, 10));
plan.DayOfWeek = (int)DayOfWeek.Monday;
var next = FrequencyCalculator.ComputeNextDueDate(plan);
Assert.Equal(new DateOnly(2026, 3, 16), next); // premier lundi après le mardi 10/03
Assert.Equal(DayOfWeek.Monday, next.DayOfWeek);
}
[Fact]
public void ComputeNextDueDate_Weekly_MissingDayOfWeek_Throws()
{
var plan = CreatePlan(FrequencyType.Weekly, new DateOnly(2026, 3, 10));
Assert.Throws<InvalidOperationException>(() => FrequencyCalculator.ComputeNextDueDate(plan));
}
[Fact]
public void ComputeNextDueDate_Monthly_ClampsToLastDayOfShorterMonth()
{
// Ancre en janvier, jour du mois = 31 : février n'a pas 31 jours -> clampé au 28/29.
var plan = CreatePlan(FrequencyType.Monthly, new DateOnly(2026, 1, 15));
plan.DayOfMonth = 31;
var next = FrequencyCalculator.ComputeNextDueDate(plan);
Assert.Equal(new DateOnly(2026, 2, 28), next); // 2026 n'est pas bissextile
}
[Fact]
public void ComputeNextDueDate_MonthlyRule_FirstMonday()
{
// Ancre en janvier -> cible février 2024. Le 1er lundi de février 2024 est le 05/02.
var plan = CreatePlan(FrequencyType.MonthlyRule, new DateOnly(2024, 1, 10));
plan.MonthlyRuleOccurrence = 1;
plan.MonthlyRuleWeekday = (int)DayOfWeek.Monday;
var next = FrequencyCalculator.ComputeNextDueDate(plan);
Assert.Equal(new DateOnly(2024, 2, 5), next);
}
[Fact]
public void ComputeNextDueDate_MonthlyRule_LastFriday()
{
// Cible février 2024 (bissextile). Le dernier vendredi de février 2024 est le 23/02.
var plan = CreatePlan(FrequencyType.MonthlyRule, new DateOnly(2024, 1, 10));
plan.MonthlyRuleOccurrence = -1;
plan.MonthlyRuleWeekday = (int)DayOfWeek.Friday;
var next = FrequencyCalculator.ComputeNextDueDate(plan);
Assert.Equal(new DateOnly(2024, 2, 23), next);
}
[Theory]
[InlineData(FrequencyType.Bimonthly, 2)]
[InlineData(FrequencyType.Quarterly, 3)]
[InlineData(FrequencyType.Semiannual, 6)]
[InlineData(FrequencyType.Annual, 12)]
public void ComputeNextDueDate_FixedMonthIntervals_AddsExpectedMonths(FrequencyType type, int months)
{
var anchor = new DateOnly(2026, 3, 10);
var plan = CreatePlan(type, anchor);
var next = FrequencyCalculator.ComputeNextDueDate(plan);
Assert.Equal(anchor.AddMonths(months), next);
}
[Fact]
public void ComputeNextDueDate_CustomDays_AddsConfiguredInterval()
{
var plan = CreatePlan(FrequencyType.CustomDays, new DateOnly(2026, 3, 10));
plan.CustomIntervalDays = 45;
var next = FrequencyCalculator.ComputeNextDueDate(plan);
Assert.Equal(new DateOnly(2026, 4, 24), next);
}
[Fact]
public void ComputeNextDueDate_CounterBased_ThrowsNotSupported()
{
var plan = CreatePlan(FrequencyType.CounterBased, new DateOnly(2026, 3, 10));
Assert.Throws<NotSupportedException>(() => FrequencyCalculator.ComputeNextDueDate(plan));
}
[Fact]
public void ComputeNextCounterThreshold_NoPriorExecution_StartsFromZero()
{
var plan = CreatePlan(FrequencyType.CounterBased, new DateOnly(2026, 3, 10));
plan.CounterThreshold = 500;
var threshold = FrequencyCalculator.ComputeNextCounterThreshold(plan);
Assert.Equal(500, threshold);
}
[Fact]
public void ComputeNextCounterThreshold_AddsToLastExecutionValue()
{
var plan = CreatePlan(FrequencyType.CounterBased, new DateOnly(2026, 3, 10));
plan.CounterThreshold = 500;
plan.LastExecutionCounterValue = 1200;
var threshold = FrequencyCalculator.ComputeNextCounterThreshold(plan);
Assert.Equal(1700, threshold);
}
[Fact]
public void IsCounterWithinLeadWindow_TrueOncePercentReached()
{
var plan = CreatePlan(FrequencyType.CounterBased, new DateOnly(2026, 3, 10));
plan.CounterThreshold = 100;
plan.LastExecutionCounterValue = 1000;
plan.LeadTimePercentForCounter = 90;
Assert.False(FrequencyCalculator.IsCounterWithinLeadWindow(plan, currentCounterValue: 1085, defaultLeadTimePercent: 80));
Assert.True(FrequencyCalculator.IsCounterWithinLeadWindow(plan, currentCounterValue: 1090, defaultLeadTimePercent: 80));
}
[Fact]
public void IsCounterWithinLeadWindow_UsesDefaultPercent_WhenPlanDoesNotOverride()
{
var plan = CreatePlan(FrequencyType.CounterBased, new DateOnly(2026, 3, 10));
plan.CounterThreshold = 100;
Assert.True(FrequencyCalculator.IsCounterWithinLeadWindow(plan, currentCounterValue: 81, defaultLeadTimePercent: 80));
Assert.False(FrequencyCalculator.IsCounterWithinLeadWindow(plan, currentCounterValue: 79, defaultLeadTimePercent: 80));
}
}