Files
UniVerse/backend/UniVerse.Api.Tests/Sync/ScheduleSyncServiceTests.cs
serega404 09d3d2778d
Backend CI / build-and-test (push) Successful in 53s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 7s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 6m26s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 14s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 6s
feat: подтянул занятость из Modeus
2026-05-30 14:08:49 +03:00

426 lines
18 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using UniVerse.Application.DTOs.Sync;
using UniVerse.Application.Interfaces;
using UniVerse.Domain.Enums;
using UniVerse.Infrastructure.Data;
using UniVerse.Infrastructure.Services;
using Xunit;
namespace UniVerse.Api.Tests.Sync;
public class ScheduleSyncServiceTests
{
private const string EventId = "48102128-2224-4cb9-ae8f-a91d0b7c512a";
private const string CourseId = "73aa6226-adbb-4e15-b264-e16fee19fd73";
private const string PersonId = "b5a5cad8-60c2-4d94-9972-8a0c2e981440";
private const string FullName = "Иванов Иван Иванович";
[Fact]
public async Task SyncScheduleAsync_UsesRoomWorkingCapacityForLectureSeats()
{
await using var db = CreateDbContext();
var modeus = Substitute.For<IModeusApiClient>();
modeus.SearchEventsAsync(Arg.Any<SyncScheduleRequest>())
.Returns(new ModeusEventsResponse
{
Embedded = new ModeusEventsEmbedded
{
Events =
[
new ModeusEvent
{
Id = "event-1",
Name = "Open lecture",
StartsAt = new DateTime(2026, 5, 13, 9, 0, 0, DateTimeKind.Utc),
EndsAt = new DateTime(2026, 5, 13, 10, 30, 0, DateTimeKind.Utc)
}
],
EventRooms =
[
new ModeusEventRoom
{
Links = new ModeusEventRoomLinks
{
Event = new ModeusHrefLink("/events/event-1"),
Room = new ModeusHrefLink("/rooms/room-1")
}
}
],
Rooms =
[
new ModeusRoom("room-1", "Room 101", "101", null, TotalCapacity: 60, WorkingCapacity: 42)
],
EventTeams =
[
new ModeusEventTeam("event-1", 15)
]
}
});
var service = new ScheduleSyncService(db, modeus, NullLogger<ScheduleSyncService>.Instance);
var result = await service.SyncScheduleAsync(new SyncScheduleRequest(null, null, null, null));
var lecture = await db.Lectures.SingleAsync();
Assert.Null(result.Error);
Assert.Equal(1, result.Created);
Assert.Equal(42, lecture.MaxEnrollments);
}
[Fact]
public async Task SyncScheduleAsync_LoadsRoomCapacityWhenEventRoomHasNoCapacity()
{
await using var db = CreateDbContext();
var modeus = Substitute.For<IModeusApiClient>();
modeus.SearchEventsAsync(Arg.Any<SyncScheduleRequest>())
.Returns(new ModeusEventsResponse
{
Embedded = new ModeusEventsEmbedded
{
Events =
[
new ModeusEvent
{
Id = "event-1",
Name = "Open lecture",
StartsAt = new DateTime(2026, 5, 13, 9, 0, 0, DateTimeKind.Utc),
EndsAt = new DateTime(2026, 5, 13, 10, 30, 0, DateTimeKind.Utc)
}
],
EventRooms =
[
new ModeusEventRoom
{
Links = new ModeusEventRoomLinks
{
Event = new ModeusHrefLink("/events/event-1"),
Room = new ModeusHrefLink("/rooms/room-1")
}
}
],
Rooms =
[
new ModeusRoom("room-1", "Room 101", "101", null, TotalCapacity: null, WorkingCapacity: null)
],
EventTeams =
[
new ModeusEventTeam("event-1", 15)
]
}
});
modeus.SearchRoomsAsync()
.Returns(new ModeusRoomsResponse
{
Rooms =
[
new ModeusRoom("room-1", "Room 101", "101", null, TotalCapacity: 60, WorkingCapacity: 48)
]
});
var service = new ScheduleSyncService(db, modeus, NullLogger<ScheduleSyncService>.Instance);
var result = await service.SyncScheduleAsync(new SyncScheduleRequest(null, null, null, null));
var lecture = await db.Lectures.SingleAsync();
Assert.Null(result.Error);
Assert.Equal(1, result.Created);
Assert.Equal(48, lecture.MaxEnrollments);
}
[Fact]
public async Task SyncScheduleAsync_SavesMandatoryAttendeesFromIctisStats()
{
await using var db = CreateDbContext();
var modeus = Substitute.For<IModeusApiClient>();
modeus.SearchEventsAsync(Arg.Any<SyncScheduleRequest>())
.Returns(new ModeusEventsResponse
{
Embedded = new ModeusEventsEmbedded
{
Events =
[
new ModeusEvent
{
Id = "event-1",
Name = "Open lecture",
StartsAt = new DateTime(2026, 5, 13, 9, 0, 0, DateTimeKind.Utc),
EndsAt = new DateTime(2026, 5, 13, 10, 30, 0, DateTimeKind.Utc),
IctisStats = new ModeusIctisStats(StudentCount: 30, TeacherCount: 1)
}
],
EventRooms =
[
new ModeusEventRoom
{
Links = new ModeusEventRoomLinks
{
Event = new ModeusHrefLink("/events/event-1"),
Room = new ModeusHrefLink("/rooms/room-1")
}
}
],
Rooms =
[
new ModeusRoom("room-1", "Room 101", "101", null, TotalCapacity: 120, WorkingCapacity: 120)
]
}
});
var service = new ScheduleSyncService(db, modeus, NullLogger<ScheduleSyncService>.Instance);
var result = await service.SyncScheduleAsync(new SyncScheduleRequest(null, null, null, null));
var lecture = await db.Lectures.SingleAsync();
Assert.Null(result.Error);
Assert.Equal(1, result.Created);
Assert.Equal(120, lecture.MaxEnrollments);
Assert.Equal(31, lecture.MandatoryAttendeesCount);
}
[Fact]
public async Task SyncScheduleAsync_UsesModeusEventAttendeeTeacher()
{
await using var db = CreateDbContext();
var modeus = new FakeModeusApiClient(BuildEventsResponse());
var service = new ScheduleSyncService(db, modeus, NullLogger<ScheduleSyncService>.Instance);
var result = await service.SyncScheduleAsync(new SyncScheduleRequest(null, null, null, null));
Assert.Null(result.Error);
Assert.Equal(1, result.Created);
var lecture = await db.Lectures.Include(item => item.Teacher).SingleAsync();
Assert.Equal("Иванов Иван Иванович", lecture.Teacher?.DisplayName);
Assert.Equal("modeus-b5a5cad8-60c2-4d94-9972-8a0c2e981440@modeus.local", lecture.Teacher?.Email);
var teacherProfile = await db.TeacherProfiles.Include(item => item.User).SingleAsync();
Assert.Equal("b5a5cad8-60c2-4d94-9972-8a0c2e981440", teacherProfile.ModeusId);
Assert.Equal(teacherProfile.UserId, lecture.TeacherId);
var teacherRole = await db.UserRoles.SingleAsync();
Assert.Equal(lecture.TeacherId, teacherRole.UserId);
Assert.Equal(UserRole.Teacher, teacherRole.Role);
}
[Fact]
public async Task SyncScheduleAsync_SavesResolvedTeacherSubId()
{
await using var db = CreateDbContext();
var modeus = new FakeModeusApiClient(BuildEventsResponse(), subId: "sso-sub-1");
var service = new ScheduleSyncService(db, modeus, NullLogger<ScheduleSyncService>.Instance);
var result = await service.SyncScheduleAsync(new SyncScheduleRequest(null, null, null, null));
Assert.Null(result.Error);
var teacher = await db.Users.Include(user => user.TeacherProfile).SingleAsync();
Assert.Equal("sso-sub-1", teacher.MicrosoftId);
Assert.Equal($"modeus-{PersonId}@modeus.local", teacher.Email);
Assert.Equal(PersonId, teacher.TeacherProfile?.ModeusId);
}
[Fact]
public async Task SyncScheduleAsync_UsesPlaceholderWhenSubLookupFails()
{
await using var db = CreateDbContext();
var modeus = new FakeModeusApiClient(BuildEventsResponse(), throwOnSubLookup: true);
var service = new ScheduleSyncService(db, modeus, NullLogger<ScheduleSyncService>.Instance);
var result = await service.SyncScheduleAsync(new SyncScheduleRequest(null, null, null, null));
Assert.Null(result.Error);
var teacher = await db.Users.Include(user => user.TeacherProfile).SingleAsync();
Assert.Null(teacher.MicrosoftId);
Assert.Equal($"modeus-{PersonId}@modeus.local", teacher.Email);
Assert.Equal(PersonId, teacher.TeacherProfile?.ModeusId);
}
[Fact]
public async Task SyncScheduleAsync_AttachesTeacherProfileToExistingSsoUser()
{
await using var db = CreateDbContext();
db.Users.Add(new UniVerse.Domain.Entities.User
{
Id = 77,
Email = "teacher@sfedu.ru",
DisplayName = "Old Name",
MicrosoftId = "sso-sub-1",
Roles = [new UniVerse.Domain.Entities.UserRoleAssignment { UserId = 77, Role = UserRole.Student }]
});
await db.SaveChangesAsync();
var modeus = new FakeModeusApiClient(BuildEventsResponse(), subId: "sso-sub-1");
var service = new ScheduleSyncService(db, modeus, NullLogger<ScheduleSyncService>.Instance);
var result = await service.SyncScheduleAsync(new SyncScheduleRequest(null, null, null, null));
Assert.Null(result.Error);
Assert.Single(await db.Users.ToListAsync());
var teacher = await db.Users.Include(user => user.Roles).Include(user => user.TeacherProfile).SingleAsync();
Assert.Equal(77, teacher.Id);
Assert.Equal("teacher@sfedu.ru", teacher.Email);
Assert.Contains(teacher.Roles, role => role.Role == UserRole.Student);
Assert.Contains(teacher.Roles, role => role.Role == UserRole.Teacher);
Assert.Equal(PersonId, teacher.TeacherProfile?.ModeusId);
Assert.True(await db.Lectures.AnyAsync(lecture => lecture.TeacherId == 77));
}
[Fact]
public async Task SyncScheduleAsync_MergesPlaceholderIntoExistingSsoUserOnRetry()
{
await using var db = CreateDbContext();
var placeholder = new UniVerse.Domain.Entities.User
{
Id = 10,
Email = $"modeus-{PersonId}@modeus.local",
DisplayName = FullName,
Roles = [new UniVerse.Domain.Entities.UserRoleAssignment { UserId = 10, Role = UserRole.Teacher }],
TeacherProfile = new UniVerse.Domain.Entities.TeacherProfile { UserId = 10, ModeusId = PersonId }
};
db.Users.Add(placeholder);
db.Users.Add(new UniVerse.Domain.Entities.User
{
Id = 20,
Email = "teacher@sfedu.ru",
DisplayName = FullName,
MicrosoftId = "sso-sub-1",
Roles = [new UniVerse.Domain.Entities.UserRoleAssignment { UserId = 20, Role = UserRole.Student }]
});
db.Courses.Add(new UniVerse.Domain.Entities.Course { Id = 1, Name = "Course", ExternalId = CourseId, IsSynced = true });
db.Lectures.Add(new UniVerse.Domain.Entities.Lecture
{
Id = 1,
CourseId = 1,
TeacherId = 10,
ExternalId = EventId,
Title = "Old",
StartsAt = DateTime.UtcNow,
EndsAt = DateTime.UtcNow.AddHours(1)
});
await db.SaveChangesAsync();
var modeus = new FakeModeusApiClient(BuildEventsResponse(), subId: "sso-sub-1");
var service = new ScheduleSyncService(db, modeus, NullLogger<ScheduleSyncService>.Instance);
var result = await service.SyncScheduleAsync(new SyncScheduleRequest(null, null, null, null));
Assert.Null(result.Error);
Assert.Single(await db.Users.ToListAsync());
var realUser = await db.Users.Include(user => user.Roles).Include(user => user.TeacherProfile).SingleAsync();
Assert.Equal(20, realUser.Id);
Assert.Equal(PersonId, realUser.TeacherProfile?.ModeusId);
Assert.Contains(realUser.Roles, role => role.Role == UserRole.Teacher);
Assert.True(await db.Lectures.AllAsync(lecture => lecture.TeacherId == 20));
}
[Fact]
public async Task SyncScheduleAsync_DoesNotLookupSubWhenTeacherAlreadyHasMicrosoftId()
{
await using var db = CreateDbContext();
db.Users.Add(new UniVerse.Domain.Entities.User
{
Id = 10,
Email = "teacher@sfedu.ru",
DisplayName = FullName,
MicrosoftId = "sso-sub-1",
Roles = [new UniVerse.Domain.Entities.UserRoleAssignment { UserId = 10, Role = UserRole.Teacher }],
TeacherProfile = new UniVerse.Domain.Entities.TeacherProfile { UserId = 10, ModeusId = PersonId }
});
await db.SaveChangesAsync();
var modeus = Substitute.For<IModeusApiClient>();
modeus.SearchEventsAsync(Arg.Any<SyncScheduleRequest>()).Returns(BuildEventsResponse());
var service = new ScheduleSyncService(db, modeus, NullLogger<ScheduleSyncService>.Instance);
var result = await service.SyncScheduleAsync(new SyncScheduleRequest(null, null, null, null));
Assert.Null(result.Error);
await modeus.DidNotReceive().GetSubIdByFullNameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
}
private static AppDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase($"ScheduleSyncTests_{Guid.NewGuid()}")
.Options;
return new AppDbContext(options);
}
private static ModeusEventsResponse BuildEventsResponse()
{
const string attendeeId = "a894db4e-833f-4f52-a153-fdd7c7d32ca7";
return new ModeusEventsResponse
{
Embedded = new ModeusEventsEmbedded
{
Events =
[
new ModeusEvent
{
Id = EventId,
Name = "Тема 20. Управление ресурсами проекта. Часть 2.",
TypeId = "LAB",
StartsAt = new DateTime(2026, 4, 14, 5, 0, 0, DateTimeKind.Utc),
EndsAt = new DateTime(2026, 4, 14, 6, 35, 0, DateTimeKind.Utc),
Links = new ModeusEventLinks
{
CourseUnitRealization = new ModeusHrefLink($"/{CourseId}")
}
}
],
CourseUnitRealizations =
[
new ModeusCourseUnitRealization(
CourseId,
"Управление проектами разработки программного обеспечения",
"УПРПО")
],
EventTeams = [new ModeusEventTeam(EventId, 25)],
EventAttendees =
[
new ModeusEventAttendee
{
Id = attendeeId,
RoleId = "TEACH",
RoleName = "Преподаватель",
Links = new ModeusEventAttendeeLinks
{
Event = new ModeusHrefLink($"/{EventId}"),
Person = new ModeusHrefLink($"/{PersonId}")
}
}
],
Persons =
[
new ModeusPerson(
PersonId,
"Иванов",
"Иван",
"Иванович",
FullName)
]
}
};
}
private sealed class FakeModeusApiClient(
ModeusEventsResponse events,
string? subId = null,
bool throwOnSubLookup = false) : IModeusApiClient
{
public Task<ModeusEventsResponse> SearchEventsAsync(SyncScheduleRequest request) => Task.FromResult(events);
public Task<ModeusRoomsResponse> SearchRoomsAsync() => Task.FromResult(new ModeusRoomsResponse());
public Task<List<ModeusEmployee>> SearchEmployeeAsync(string fullname) => Task.FromResult(new List<ModeusEmployee>());
public Task<string?> GetSubIdByFullNameAsync(string fullname, CancellationToken cancellationToken = default)
{
if (throwOnSubLookup)
throw new HttpRequestException("lookup failed");
return Task.FromResult(subId);
}
}
}