using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.System; using Jiaowu.Api.Infrastructure.Grades; using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; namespace Jiaowu.Api.Tests; public sealed class CourseGradeStatisticsRefreshSchedulerTests { [Fact] public async Task EnqueueDueAsync_UsesPersistentScheduleAndHonorsInterval() { await using var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; await using var db = new AppDbContext(options); await db.Database.EnsureCreatedAsync(); var scheduler = new CourseGradeStatisticsRefreshScheduler( db, NullLogger.Instance); var now = new DateTime(2026, 8, 9, 12, 0, 0, DateTimeKind.Utc); await scheduler.EnqueueDueAsync(now, CancellationToken.None); var setting = Assert.Single(await db.CourseGradeStatisticsRefreshSettings.ToListAsync()); Assert.True(setting.IsEnabled); Assert.Equal(now, setting.LastRunAt); await scheduler.EnqueueDueAsync(now.AddMinutes(1), CancellationToken.None); Assert.Equal(now, setting.LastRunAt); } [Fact] public async Task EnqueueStaleAsync_GroupsByCourseTerm_AndSkipsActiveTarget() { await using var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; await using var db = new AppDbContext(options); await db.Database.EnsureCreatedAsync(); var college = new College { Code = "SCHEDULE", Name = "定时任务学院" }; var major = new Major { Code = "SCHEDULE-M", Name = "定时任务专业", College = college, DegreeType = "本科" }; var administrativeClass = new AdministrativeClass { Code = "SCHEDULE-C", Name = "定时任务一班", Major = major, Grade = 2026 }; var student = new Student { StudentNumber = "SCHEDULE-001", Name = "定时任务学生", AdministrativeClass = administrativeClass, EnrollmentYear = 2026, EnrollmentDate = new DateOnly(2026, 9, 1) }; var course = new Course { Code = "SCHEDULE-COURSE", Name = "定时任务课程", College = college, Credits = 2, TotalHours = 32, LectureHours = 32 }; var term = new AcademicTerm { Code = "2026-A", Name = "2026-2027 学年第一学期", AcademicYear = "2026-2027", Season = TermSeason.Autumn, StartDate = new DateOnly(2026, 9, 1), EndDate = new DateOnly(2027, 1, 15) }; var firstTask = new TeachingTask { TaskNumber = "SCHEDULE-T1", Name = "定时任务教学班一", Course = course, AcademicTerm = term, Capacity = 30 }; var secondTask = new TeachingTask { TaskNumber = "SCHEDULE-T2", Name = "定时任务教学班二", Course = course, AcademicTerm = term, Capacity = 30 }; var firstSheet = PublishedSheet(firstTask, student, 80m); var secondSheet = PublishedSheet(secondTask, student, 90m); db.GradeSheets.AddRange(firstSheet, secondSheet); await db.SaveChangesAsync(); var scheduler = new CourseGradeStatisticsRefreshScheduler( db, NullLogger.Instance); var queued = await scheduler.EnqueueStaleAsync(100, CancellationToken.None); var queuedAgain = await scheduler.EnqueueStaleAsync(100, CancellationToken.None); Assert.Equal(1, queued); Assert.Equal(0, queuedAgain); var job = Assert.Single(await db.CourseGradeStatisticsRefreshJobs.ToListAsync()); var outbox = Assert.Single(await db.BackgroundJobOutboxMessages.ToListAsync()); Assert.Equal(BackgroundJobKind.CourseGradeStatisticsRefresh, outbox.JobKind); job.Status = CourseGradeStatisticsRefreshJobStatus.Succeeded; db.TeachingTaskGradeStatistics.AddRange( FreshStatistic(firstSheet, course, term), FreshStatistic(secondSheet, course, term)); await db.SaveChangesAsync(); var queuedAfterFreshStatistics = await scheduler.EnqueueStaleAsync( 100, CancellationToken.None); Assert.Equal(0, queuedAfterFreshStatistics); Assert.Single(await db.CourseGradeStatisticsRefreshJobs.ToListAsync()); } private static GradeSheet PublishedSheet( TeachingTask task, Student student, decimal score) { var sheet = new GradeSheet { TeachingTask = task, Status = GradeSheetStatus.Published, PublishedAt = DateTime.UtcNow }; sheet.Records.Add(new GradeRecord { GradeSheet = sheet, Student = student, TotalScore = score, GradePoint = 3m }); return sheet; } private static TeachingTaskGradeStatistic FreshStatistic( GradeSheet sheet, Course course, AcademicTerm term) => new() { GradeSheetId = sheet.Id, TeachingTaskId = sheet.TeachingTaskId, CourseId = course.Id, AcademicTermId = term.Id, StudentCount = 1, PassedCount = 1, HighestScore = 80m, AverageScore = 80m, MedianScore = 80m, LowestScore = 80m, PassRate = 100m, CalculatedAt = DateTime.UtcNow.AddMinutes(1) }; }