Files
UniVerse/backend/UniVerse.Api.Tests/Lectures/LectureServiceTests.cs
T
serega404 2e7ce6c2e8
Backend CI / build-and-test (push) Failing after 32s
Frontend CI / build-and-check (push) Failing after 5m5s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 6s
🚀 Create and publish a Docker image / Build & publish backend image (push) Failing after 1m28s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Failing after 19s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Has been skipped
feat: добавил ограничение записи на лекции
2026-05-21 19:34:08 +03:00

209 lines
8.6 KiB
C#

using Microsoft.EntityFrameworkCore;
using NSubstitute;
using UniVerse.Application.DTOs.Lectures;
using UniVerse.Application.DTOs.Notifications;
using UniVerse.Application.Interfaces;
using UniVerse.Domain.Entities;
using UniVerse.Domain.Exceptions;
using UniVerse.Infrastructure.Data;
using UniVerse.Infrastructure.Services;
using Xunit;
namespace UniVerse.Api.Tests.Lectures;
public class LectureServiceTests
{
[Fact]
public async Task GetAllAsync_MarksLecturesEnrolledByCurrentUser()
{
await using var db = CreateDbContext();
var service = new LectureService(db, Substitute.For<IGamificationService>(), Substitute.For<INotificationScheduler>());
var startsAt = DateTime.UtcNow.AddDays(1);
db.Users.Add(new User { Id = 1, Email = "student@test.local" });
db.Courses.Add(new Course { Id = 1, Name = "Course" });
db.Lectures.AddRange(
Lecture(1, startsAt),
Lecture(2, startsAt.AddDays(1)));
db.LectureEnrollments.Add(new LectureEnrollment { LectureId = 1, UserId = 1 });
await db.SaveChangesAsync();
var result = await service.GetAllAsync(new LectureFilterRequest(null, null, null, null, null, null, null, null), 1);
Assert.True(result.Items.Single(item => item.Id == 1).IsEnrolled);
Assert.False(result.Items.Single(item => item.Id == 2).IsEnrolled);
}
[Fact]
public async Task EnrollAsync_SchedulesLectureReminders()
{
await using var db = CreateDbContext();
var scheduler = Substitute.For<INotificationScheduler>();
var service = new LectureService(db, Substitute.For<IGamificationService>(), scheduler);
var startsAt = DateTime.UtcNow.AddHours(4);
db.Users.Add(new User { Id = 1, Email = "student@test.local", DisplayName = "Student" });
db.Courses.Add(new Course { Id = 1, Name = "Course" });
db.Lectures.Add(Lecture(1, startsAt));
await db.SaveChangesAsync();
await service.EnrollAsync(1, 1);
await scheduler.Received(1).ScheduleAsync(
Arg.Is<NotificationMessage>(m => m.Recipient == "student@test.local" && m.Subject.Contains("через 3 часа")),
Arg.Is<DateTimeOffset>(d => d == new DateTimeOffset(startsAt.AddHours(-3))),
"lecture-1-user-1-starts-in-3-hours",
Arg.Any<CancellationToken>());
await scheduler.Received(1).ScheduleAsync(
Arg.Is<NotificationMessage>(m => m.Recipient == "student@test.local" && m.Subject.Contains("через 1 час")),
Arg.Is<DateTimeOffset>(d => d == new DateTimeOffset(startsAt.AddHours(-1))),
"lecture-1-user-1-starts-in-1-hour",
Arg.Any<CancellationToken>());
await scheduler.Received(1).ScheduleAsync(
Arg.Is<NotificationMessage>(m => m.Recipient == "student@test.local" && m.Subject.Contains("Оцените")),
Arg.Is<DateTimeOffset>(d => d == new DateTimeOffset(startsAt.AddHours(2))),
"lecture-1-user-1-ended",
Arg.Any<CancellationToken>());
}
[Fact]
public async Task EnrollAsync_SkipsPastLectureReminders()
{
await using var db = CreateDbContext();
var scheduler = Substitute.For<INotificationScheduler>();
var service = new LectureService(db, Substitute.For<IGamificationService>(), scheduler);
var startsAt = DateTime.UtcNow.AddMinutes(90);
db.Users.Add(new User { Id = 1, Email = "student@test.local" });
db.Courses.Add(new Course { Id = 1, Name = "Course" });
db.Lectures.Add(Lecture(1, startsAt));
await db.SaveChangesAsync();
await service.EnrollAsync(1, 1);
await scheduler.DidNotReceive().ScheduleAsync(
Arg.Any<NotificationMessage>(),
Arg.Any<DateTimeOffset>(),
"lecture-1-user-1-starts-in-3-hours",
Arg.Any<CancellationToken>());
await scheduler.Received(2).ScheduleAsync(
Arg.Any<NotificationMessage>(),
Arg.Any<DateTimeOffset>(),
Arg.Any<string>(),
Arg.Any<CancellationToken>());
}
[Theory]
[InlineData(1, 3)]
[InlineData(2, 3)]
[InlineData(3, 5)]
[InlineData(4, 7)]
[InlineData(5, 7)]
public async Task EnrollAsync_ThrowsWhenActiveEnrollmentLimitReached(int level, int activeEnrollments)
{
await using var db = CreateDbContext();
var gamification = Substitute.For<IGamificationService>();
gamification.CalculateLevelAsync(Arg.Any<int>()).Returns(level);
var service = new LectureService(db, gamification, Substitute.For<INotificationScheduler>());
var startsAt = DateTime.UtcNow.AddDays(1);
db.Users.Add(new User { Id = 1, Email = "student@test.local" });
db.Courses.Add(new Course { Id = 1, Name = "Course" });
db.Lectures.Add(Lecture(100, startsAt.AddDays(100)));
for (var i = 1; i <= activeEnrollments; i++)
{
db.Lectures.Add(Lecture(i, startsAt.AddDays(i)));
db.LectureEnrollments.Add(new LectureEnrollment { LectureId = i, UserId = 1 });
}
await db.SaveChangesAsync();
await Assert.ThrowsAsync<ConflictException>(() => service.EnrollAsync(100, 1));
}
[Fact]
public async Task EnrollAsync_ThrowsWhenPastUnattendedEnrollmentsReachLimit()
{
await using var db = CreateDbContext();
var gamification = Substitute.For<IGamificationService>();
gamification.CalculateLevelAsync(Arg.Any<int>()).Returns(1);
var service = new LectureService(db, gamification, Substitute.For<INotificationScheduler>());
var now = DateTime.UtcNow;
db.Users.Add(new User { Id = 1, Email = "student@test.local" });
db.Courses.Add(new Course { Id = 1, Name = "Course" });
db.Lectures.Add(Lecture(100, now.AddDays(1)));
for (var i = 1; i <= 3; i++)
{
db.Lectures.Add(Lecture(i, now.AddDays(-i)));
db.LectureEnrollments.Add(new LectureEnrollment { LectureId = i, UserId = 1 });
}
await db.SaveChangesAsync();
await Assert.ThrowsAsync<ConflictException>(() => service.EnrollAsync(100, 1));
}
[Fact]
public async Task EnrollAsync_DoesNotCountAttendedEnrollmentsTowardLimit()
{
await using var db = CreateDbContext();
var gamification = Substitute.For<IGamificationService>();
gamification.CalculateLevelAsync(Arg.Any<int>()).Returns(1);
var service = new LectureService(db, gamification, Substitute.For<INotificationScheduler>());
var now = DateTime.UtcNow;
db.Users.Add(new User { Id = 1, Email = "student@test.local" });
db.Courses.Add(new Course { Id = 1, Name = "Course" });
db.Lectures.Add(Lecture(100, now.AddDays(1)));
for (var i = 1; i <= 3; i++)
{
db.Lectures.Add(Lecture(i, now.AddDays(-i)));
db.LectureEnrollments.Add(new LectureEnrollment { LectureId = i, UserId = 1, Attended = true });
}
await db.SaveChangesAsync();
await service.EnrollAsync(100, 1);
Assert.True(await db.LectureEnrollments.AnyAsync(e => e.LectureId == 100 && e.UserId == 1));
}
[Fact]
public async Task UnenrollAsync_CancelsLectureReminders()
{
await using var db = CreateDbContext();
var scheduler = Substitute.For<INotificationScheduler>();
var service = new LectureService(db, Substitute.For<IGamificationService>(), scheduler);
var startsAt = DateTime.UtcNow.AddHours(4);
db.Users.Add(new User { Id = 1, Email = "student@test.local" });
db.Courses.Add(new Course { Id = 1, Name = "Course" });
db.Lectures.Add(Lecture(1, startsAt));
db.LectureEnrollments.Add(new LectureEnrollment { LectureId = 1, UserId = 1 });
await db.SaveChangesAsync();
await service.UnenrollAsync(1, 1);
await scheduler.Received(1).CancelAsync("lecture-1-user-1-starts-in-3-hours", Arg.Any<CancellationToken>());
await scheduler.Received(1).CancelAsync("lecture-1-user-1-starts-in-1-hour", Arg.Any<CancellationToken>());
await scheduler.Received(1).CancelAsync("lecture-1-user-1-ended", Arg.Any<CancellationToken>());
}
private static AppDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase($"LectureServiceTests_{Guid.NewGuid()}")
.Options;
return new AppDbContext(options);
}
private static Lecture Lecture(int id, DateTime startsAt) => new()
{
Id = id,
CourseId = 1,
Title = $"Lecture {id}",
StartsAt = startsAt,
EndsAt = startsAt.AddHours(2),
IsOpen = true,
MaxEnrollments = 30
};
}