99d25adbb1
Backend CI / build-and-test (push) Failing after 13m11s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Failing after 10m12s
Frontend CI / build-and-check (push) Failing after 16m9s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Failing after 14m6s
🚀 Create and publish a Docker image / Build & publish backend image (push) Failing after 14m58s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Failing after 14m58s
242 lines
10 KiB
C#
242 lines
10 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>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_TeacherCannotUpdateAnotherTeachersLecture()
|
|
{
|
|
await using var db = CreateDbContext();
|
|
var service = new LectureService(db, Substitute.For<IGamificationService>(), Substitute.For<INotificationScheduler>());
|
|
db.Courses.Add(new Course { Id = 1, Name = "Course" });
|
|
var lecture = Lecture(1, DateTime.UtcNow.AddDays(1));
|
|
lecture.TeacherId = 2;
|
|
db.Lectures.Add(lecture);
|
|
await db.SaveChangesAsync();
|
|
|
|
var request = new UpdateLectureRequest(null, null, "Updated", null, Domain.Enums.LectureFormat.Offline,
|
|
DateTime.UtcNow.AddDays(1), DateTime.UtcNow.AddDays(1).AddHours(2), true, 30, null);
|
|
|
|
await Assert.ThrowsAsync<ForbiddenException>(() => service.UpdateAsync(1, request, currentUserId: 1));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetEnrollmentsAsync_AdminCanReadAnyLecture()
|
|
{
|
|
await using var db = CreateDbContext();
|
|
var service = new LectureService(db, Substitute.For<IGamificationService>(), Substitute.For<INotificationScheduler>());
|
|
db.Courses.Add(new Course { Id = 1, Name = "Course" });
|
|
var lecture = Lecture(1, DateTime.UtcNow.AddDays(1));
|
|
lecture.TeacherId = 2;
|
|
db.Lectures.Add(lecture);
|
|
await db.SaveChangesAsync();
|
|
|
|
var result = await service.GetEnrollmentsAsync(1, new UniVerse.Application.DTOs.Common.PaginationRequest(), currentUserId: 1, isAdmin: true);
|
|
|
|
Assert.Empty(result.Items);
|
|
}
|
|
|
|
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
|
|
};
|
|
}
|