промежуточный коммит
This commit is contained in:
+214
@@ -9,6 +9,7 @@ using Tnsu.ApprovalRoute.Application.Reminders;
|
||||
using Tnsu.ApprovalRoute.Application.RequestTypes;
|
||||
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
||||
using Tnsu.ApprovalRoute.Infrastructure.SharePoint;
|
||||
using Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
|
||||
using Tnsu.ApprovalRoute.IntegrationTests.Infrastructure;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.IntegrationTests.ApplicationServices;
|
||||
@@ -53,6 +54,23 @@ public sealed class ReminderServiceTests : IClassFixture<PostgresDatabaseFixture
|
||||
return listUrl;
|
||||
}
|
||||
|
||||
// pageUrl отдельным параметром (а не /{name}, как в CreateTypeWithSlaAsync) —
|
||||
// GetProbationEndingAsync ищет тип НЕ по SLA, а именно по PageUrl (см.
|
||||
// RemindersOptions.RecruitingHirePageUrl), и у каждого теста здесь свой
|
||||
// уникальный PageUrl, передаваемый в опции сервиса той же строкой: так тесты
|
||||
// не видят чужие «recruiting-hire»-записи, случайно оставшиеся от других
|
||||
// тестов в общей БД фикстуры.
|
||||
private async Task<(string ListUrl, string PageUrl)> CreateHireTypeAsync(string name)
|
||||
{
|
||||
var listUrl = $"/sites/requests/Lists/{name.Replace(" ", string.Empty)}";
|
||||
var pageUrl = $"/{name.Replace(" ", string.Empty)}";
|
||||
await _requestTypeService.CreateAsync(
|
||||
new CreateRequestTypeRequest(name, pageUrl, listUrl, SiteUrl, "General"),
|
||||
CancellationToken.None);
|
||||
|
||||
return (listUrl, pageUrl);
|
||||
}
|
||||
|
||||
private static JsonElement Json(object value) => JsonSerializer.SerializeToElement(value);
|
||||
|
||||
private static object Row(
|
||||
@@ -104,6 +122,7 @@ public sealed class ReminderServiceTests : IClassFixture<PostgresDatabaseFixture
|
||||
_services.GetRequiredService<ApprovalRouteDbContext>(),
|
||||
sharePoint.Object,
|
||||
_services.GetRequiredService<ListPartitionRegistry>(),
|
||||
Mock.Of<ITnsuPortalClient>(),
|
||||
Options.Create(new RemindersOptions()),
|
||||
NullLogger<ReminderService>.Instance);
|
||||
}
|
||||
@@ -123,6 +142,7 @@ public sealed class ReminderServiceTests : IClassFixture<PostgresDatabaseFixture
|
||||
reminder.Email.ShouldBe("approver@tnsu.kz");
|
||||
reminder.HoursOverdue.ShouldBe(24);
|
||||
reminder.RequestId.ShouldBe(1);
|
||||
reminder.Category.ShouldBe(ReminderCategory.ApprovalOverdue);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Срок ещё не вышел — напоминания нет")]
|
||||
@@ -258,4 +278,198 @@ public sealed class ReminderServiceTests : IClassFixture<PostgresDatabaseFixture
|
||||
|
||||
result.Value.ShouldHaveSingleItem().Email.ShouldBe("approver@tnsu.kz");
|
||||
}
|
||||
|
||||
private static object HireDataRow(
|
||||
int id, string status, string? candidateName, string? hireDate, string? probationPeriod, string? code = null) =>
|
||||
new
|
||||
{
|
||||
Id = id,
|
||||
RequestStatus = status,
|
||||
CandidateName = candidateName,
|
||||
HireDate = hireDate,
|
||||
ProbationPeriod = probationPeriod,
|
||||
RequestUniqueCode = code
|
||||
};
|
||||
|
||||
private ReminderService BuildProbationService(
|
||||
string pageUrl, string listUrl, IReadOnlyList<object> rows, Mock<ITnsuPortalClient> portalClient,
|
||||
int daysBeforeEnd = 3)
|
||||
{
|
||||
const string hireDataListTitle = "Прием на работу тест — данные";
|
||||
|
||||
var sharePoint = new Mock<ISharePointClient>();
|
||||
sharePoint
|
||||
.Setup(x => x.GetListTitleAsync(SiteUrl, listUrl, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(hireDataListTitle);
|
||||
|
||||
// GetProbationEndingAsync читает только «Согласовано»/«Закрыта» — SharePoint
|
||||
// фильтрует это сам ($filter в запросе), мок повторяет ту же фильтрацию.
|
||||
var closedRows = rows
|
||||
.Where(r => (string)r.GetType().GetProperty("RequestStatus")!.GetValue(r)! is "Согласовано" or "Закрыта")
|
||||
.ToList();
|
||||
sharePoint
|
||||
.Setup(x => x.GetItemsByListTitleAsync(
|
||||
SiteUrl, hireDataListTitle, It.Is<string>(q => q.Contains("RequestStatus eq")), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(closedRows.Select(Json).ToList());
|
||||
|
||||
return new ReminderService(
|
||||
_services.GetRequiredService<ApprovalRouteDbContext>(),
|
||||
sharePoint.Object,
|
||||
_services.GetRequiredService<ListPartitionRegistry>(),
|
||||
portalClient.Object,
|
||||
Options.Create(new RemindersOptions { RecruitingHirePageUrl = pageUrl, ProbationReminderDaysBeforeEnd = daysBeforeEnd }),
|
||||
NullLogger<ReminderService>.Instance);
|
||||
}
|
||||
|
||||
private static Mock<ITnsuPortalClient> BuildPortalClientMock(
|
||||
string candidateFio, DateTimeOffset? dismissalDate, string? candidateEmail, string? managerEmail)
|
||||
{
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
|
||||
portalClient
|
||||
.Setup(x => x.GetFullBaseAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync([new EmployeeFullBaseInfo(Guid.NewGuid(), candidateFio, "ТОО TANSU", dismissalDate)]);
|
||||
|
||||
portalClient
|
||||
.Setup(x => x.GetAllEmployeesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync([new EmployeeContactInfo(Guid.NewGuid(), candidateFio, candidateEmail, "ТОО TANSU")]);
|
||||
|
||||
if (candidateEmail is not null)
|
||||
portalClient
|
||||
.Setup(x => x.GetManagerAsync(candidateEmail, "chief", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(managerEmail);
|
||||
|
||||
return portalClient;
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "До конца испытательного срока 3 дня — напоминание с email кандидата и его руководителя")]
|
||||
public async Task ProbationEndingIn3Days_BuildsReminder()
|
||||
{
|
||||
var (listUrl, pageUrl) = await CreateHireTypeAsync("Probation due");
|
||||
// probationEndsAt = Now.Date + 3 дня = 2026-08-15; "2 месяца" от 2026-06-15.
|
||||
var rows = new[] { HireDataRow(1, "Закрыта", "Иванов Иван Иванович", "2026-06-15T00:00:00Z", "2 месяца") };
|
||||
var portalClient = BuildPortalClientMock(
|
||||
"Иванов Иван Иванович", dismissalDate: null, candidateEmail: "candidate@tnsu.kz", managerEmail: "manager@tnsu.kz");
|
||||
|
||||
var service = BuildProbationService(pageUrl, listUrl, rows, portalClient);
|
||||
var result = await service.GetProbationEndingAsync(Now, CancellationToken.None);
|
||||
|
||||
result.IsSuccess.ShouldBeTrue();
|
||||
var reminder = result.Value.ShouldHaveSingleItem();
|
||||
reminder.Category.ShouldBe(ReminderCategory.ProbationEnding);
|
||||
reminder.ManagerEmail.ShouldBe("manager@tnsu.kz");
|
||||
reminder.CandidateEmail.ShouldBe("candidate@tnsu.kz");
|
||||
reminder.Email.ShouldBe("manager@tnsu.kz");
|
||||
reminder.CandidateName.ShouldBe("Иванов Иван Иванович");
|
||||
reminder.DaysUntilProbationEnds.ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Кандидат уже уволен (dismissalDate заполнен) — напоминания нет")]
|
||||
public async Task DismissedCandidate_IsSkipped()
|
||||
{
|
||||
var (listUrl, pageUrl) = await CreateHireTypeAsync("Probation dismissed");
|
||||
var rows = new[] { HireDataRow(1, "Закрыта", "Петров Пётр Петрович", "2026-06-15T00:00:00Z", "2 месяца") };
|
||||
var portalClient = BuildPortalClientMock(
|
||||
"Петров Пётр Петрович", dismissalDate: Now.AddDays(-1), candidateEmail: "petrov@tnsu.kz", managerEmail: "manager@tnsu.kz");
|
||||
|
||||
var service = BuildProbationService(pageUrl, listUrl, rows, portalClient);
|
||||
var result = await service.GetProbationEndingAsync(Now, CancellationToken.None);
|
||||
|
||||
result.Value.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
// Сотрудник холдинга может числиться в нескольких компаниях сразу (см.
|
||||
// /Employee/FullBase — по записи на компанию). Увольнение из одной из них не
|
||||
// означает, что человек ушёл: пока хоть где-то дата увольнения не стоит, он
|
||||
// работает, и напоминание ему по-прежнему нужно.
|
||||
[Fact(DisplayName = "Уволен из одной компании, но работает в другой — напоминание есть")]
|
||||
public async Task DismissedFromOneCompanyOnly_StillBuildsReminder()
|
||||
{
|
||||
var (listUrl, pageUrl) = await CreateHireTypeAsync("Probation partly dismissed");
|
||||
const string fio = "Романов Роман Романович";
|
||||
var rows = new[] { HireDataRow(1, "Закрыта", fio, "2026-06-15T00:00:00Z", "2 месяца") };
|
||||
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetFullBaseAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(
|
||||
[
|
||||
new EmployeeFullBaseInfo(Guid.NewGuid(), fio, "ТОО АСТЕЛ-К", Now.AddDays(-10)),
|
||||
new EmployeeFullBaseInfo(Guid.NewGuid(), fio, "ТОО TANSU Construction", null)
|
||||
]);
|
||||
portalClient
|
||||
.Setup(x => x.GetAllEmployeesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync([new EmployeeContactInfo(Guid.NewGuid(), fio, "romanov@tnsu.kz", "ТОО TANSU Construction")]);
|
||||
portalClient
|
||||
.Setup(x => x.GetManagerAsync("romanov@tnsu.kz", "chief", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync("manager@tnsu.kz");
|
||||
|
||||
var service = BuildProbationService(pageUrl, listUrl, rows, portalClient);
|
||||
var result = await service.GetProbationEndingAsync(Now, CancellationToken.None);
|
||||
|
||||
result.Value.ShouldHaveSingleItem().CandidateEmail.ShouldBe("romanov@tnsu.kz");
|
||||
}
|
||||
|
||||
// ФИО в заявке набирают руками — лишние пробелы и другой регистр не должны
|
||||
// ломать сопоставление со справочником (см. EmployeeDirectorySnapshot.Normalize).
|
||||
[Fact(DisplayName = "ФИО сопоставляется без учёта регистра и лишних пробелов")]
|
||||
public async Task CandidateName_IsMatchedLoosely()
|
||||
{
|
||||
var (listUrl, pageUrl) = await CreateHireTypeAsync("Probation fio normalize");
|
||||
var rows = new[] { HireDataRow(1, "Закрыта", " волков ВОЛК волкович ", "2026-06-15T00:00:00Z", "2 месяца") };
|
||||
var portalClient = BuildPortalClientMock(
|
||||
"Волков Волк Волкович", dismissalDate: null, candidateEmail: "volkov@tnsu.kz", managerEmail: "manager@tnsu.kz");
|
||||
|
||||
var service = BuildProbationService(pageUrl, listUrl, rows, portalClient);
|
||||
var result = await service.GetProbationEndingAsync(Now, CancellationToken.None);
|
||||
|
||||
result.Value.ShouldHaveSingleItem().CandidateEmail.ShouldBe("volkov@tnsu.kz");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Кандидат не найден в /Employee/FullBase — напоминания нет")]
|
||||
public async Task CandidateNotInFullBase_IsSkipped()
|
||||
{
|
||||
var (listUrl, pageUrl) = await CreateHireTypeAsync("Probation unknown");
|
||||
var rows = new[] { HireDataRow(1, "Закрыта", "Сидоров Сидор Сидорович", "2026-06-15T00:00:00Z", "2 месяца") };
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient.Setup(x => x.GetFullBaseAsync(It.IsAny<CancellationToken>())).ReturnsAsync([]);
|
||||
portalClient.Setup(x => x.GetAllEmployeesAsync(It.IsAny<CancellationToken>())).ReturnsAsync([]);
|
||||
|
||||
var service = BuildProbationService(pageUrl, listUrl, rows, portalClient);
|
||||
var result = await service.GetProbationEndingAsync(Now, CancellationToken.None);
|
||||
|
||||
result.Value.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "До конца испытательного срока не ровно 3 дня — напоминания нет")]
|
||||
public async Task NotExactlyThreeDaysLeft_IsSkipped()
|
||||
{
|
||||
var (listUrl, pageUrl) = await CreateHireTypeAsync("Probation not due");
|
||||
// "1 месяц" от 2026-06-15 → срок кончился 2026-07-15, задолго до Now.
|
||||
var rows = new[] { HireDataRow(1, "Закрыта", "Кузнецов Кузьма Кузьмич", "2026-06-15T00:00:00Z", "1 месяц") };
|
||||
var portalClient = BuildPortalClientMock(
|
||||
"Кузнецов Кузьма Кузьмич", dismissalDate: null, candidateEmail: "kuznecov@tnsu.kz", managerEmail: "manager@tnsu.kz");
|
||||
|
||||
var service = BuildProbationService(pageUrl, listUrl, rows, portalClient);
|
||||
var result = await service.GetProbationEndingAsync(Now, CancellationToken.None);
|
||||
|
||||
result.Value.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "«Без испытательного срока» — напоминания нет")]
|
||||
public async Task NoProbationPeriod_IsSkipped()
|
||||
{
|
||||
var (listUrl, pageUrl) = await CreateHireTypeAsync("Probation none");
|
||||
var rows = new[]
|
||||
{
|
||||
HireDataRow(1, "Закрыта", "Смирнов Смирн Смирнович", "2026-06-15T00:00:00Z", "Без испытательного срока")
|
||||
};
|
||||
var portalClient = BuildPortalClientMock(
|
||||
"Смирнов Смирн Смирнович", dismissalDate: null, candidateEmail: "smirnov@tnsu.kz", managerEmail: "manager@tnsu.kz");
|
||||
|
||||
var service = BuildProbationService(pageUrl, listUrl, rows, portalClient);
|
||||
var result = await service.GetProbationEndingAsync(Now, CancellationToken.None);
|
||||
|
||||
result.Value.ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user