using Jiaowu.Api.Controllers; using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Tests; public sealed class TeachingTasksControllerTests { [Fact] public async Task Delete_AllowsArchivedTask_ButRejectsPublishedTask() { await using var database = await TestDatabase.CreateAsync(); var archived = await database.AddTaskAsync("ARCHIVED", TeachingTaskStatus.Closed); var published = await database.AddTaskAsync("PUBLISHED", TeachingTaskStatus.Published); var deleteArchived = await database.Controller.Delete( archived.Id, CancellationToken.None); var deletePublished = await database.Controller.Delete( published.Id, CancellationToken.None); Assert.IsType(deleteArchived); Assert.IsType(deletePublished); Assert.False(await database.Db.TeachingTasks.AnyAsync(x => x.Id == archived.Id)); Assert.True(await database.Db.TeachingTasks.AnyAsync(x => x.Id == published.Id)); } [Fact] public async Task Unarchive_RestoresArchivedTaskToPublished() { await using var database = await TestDatabase.CreateAsync(); var task = await database.AddTaskAsync("RESTORE", TeachingTaskStatus.Closed); var result = await database.Controller.Unarchive(task.Id, CancellationToken.None); Assert.IsType(result); await database.Db.Entry(task).ReloadAsync(); Assert.Equal(TeachingTaskStatus.Published, task.Status); } [Fact] public async Task Batch_UnarchivesOnlyArchivedTasks() { await using var database = await TestDatabase.CreateAsync(); var first = await database.AddTaskAsync("FIRST", TeachingTaskStatus.Closed); var second = await database.AddTaskAsync("SECOND", TeachingTaskStatus.Closed); var result = await database.Controller.Batch( new TeachingTaskBatchRequest( TeachingTaskBatchOperation.Unarchive, [first.Id, second.Id]), CancellationToken.None); Assert.IsType(result); Assert.All( await database.Db.TeachingTasks .Where(x => x.Id == first.Id || x.Id == second.Id) .ToListAsync(), task => Assert.Equal(TeachingTaskStatus.Published, task.Status)); } [Fact] public async Task Options_ReturnsAllPublishedTasksWithoutListPageTruncation() { await using var database = await TestDatabase.CreateAsync(); for (var index = 1; index <= 125; index++) { await database.AddTaskAsync( $"OPTION-{index:D3}", TeachingTaskStatus.Published); } await database.AddTaskAsync("DRAFT", TeachingTaskStatus.Draft); var result = await database.Controller.GetOptions( database.TermId, TeachingTaskStatus.Published, CancellationToken.None); var ok = Assert.IsType(result); var items = Assert.IsAssignableFrom(ok.Value); Assert.Equal(125, items.Cast().Count()); } [Fact] public async Task Options_PreservesCollegeDataScope() { await using var database = await TestDatabase.CreateAsync(collegeScoped: true); await database.AddTaskAsync("IN-SCOPE", TeachingTaskStatus.Published); var otherCollege = new College { Code = "OTHER", Name = "其他学院" }; var otherCourse = new Course { Code = "OTHER101", Name = "范围外课程", College = otherCollege, Nature = CourseNature.MajorRequired, TotalHours = 32, LectureHours = 32 }; database.Db.Add(new TeachingTask { TaskNumber = "TASK-OUT-OF-SCOPE", Name = "范围外教学任务", AcademicTermId = database.TermId, Course = otherCourse, Capacity = 40, StartWeek = 1, EndWeek = 16, WeeklyHours = 2, Status = TeachingTaskStatus.Published, PublishedAt = DateTime.UtcNow }); await database.Db.SaveChangesAsync(); var result = await database.Controller.GetOptions( database.TermId, TeachingTaskStatus.Published, CancellationToken.None); var ok = Assert.IsType(result); var items = Assert.IsAssignableFrom(ok.Value); Assert.Single(items.Cast()); } private sealed class TestDatabase : IAsyncDisposable { private readonly SqliteConnection connection; private readonly AcademicTerm term; private readonly Course course; private TestDatabase( SqliteConnection connection, AppDbContext db, AcademicTerm term, Course course, ICurrentUserDataScope dataScope) { this.connection = connection; Db = db; this.term = term; this.course = course; Controller = new TeachingTasksController( db, dataScope); } public AppDbContext Db { get; } public TeachingTasksController Controller { get; } public Guid TermId => term.Id; public static async Task CreateAsync(bool collegeScoped = false) { var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; var db = new AppDbContext(options); await db.Database.EnsureCreatedAsync(); var college = new College { Code = "TEST", Name = "测试学院" }; var term = new AcademicTerm { Code = "2026-1", Name = "2026—2027 学年第一学期", AcademicYear = "2026-2027", Season = TermSeason.Autumn, StartDate = new DateOnly(2026, 9, 1), EndDate = new DateOnly(2027, 1, 15) }; var course = new Course { Code = "TEST101", Name = "测试课程", CollegeId = college.Id, Nature = CourseNature.MajorRequired, TotalHours = 32, LectureHours = 32 }; db.AddRange(college, term, course); await db.SaveChangesAsync(); return new TestDatabase( connection, db, term, course, new TestDataScope(collegeScoped ? college.Id : null)); } public async Task AddTaskAsync( string suffix, TeachingTaskStatus status) { var task = new TeachingTask { TaskNumber = $"TASK-{suffix}", Name = $"教学任务 {suffix}", AcademicTermId = term.Id, CourseId = course.Id, Capacity = 40, StartWeek = 1, EndWeek = 16, WeeklyHours = 2, Status = status, PublishedAt = status == TeachingTaskStatus.Draft ? null : DateTime.UtcNow }; Db.TeachingTasks.Add(task); await Db.SaveChangesAsync(); return task; } public async ValueTask DisposeAsync() { await Db.DisposeAsync(); await connection.DisposeAsync(); } } private sealed class TestDataScope : ICurrentUserDataScope { public TestDataScope(Guid? restrictedCollegeId = null) { Current = new CurrentUserScope( Guid.NewGuid(), "测试管理员", restrictedCollegeId, restrictedCollegeId.HasValue ? DataScope.College : DataScope.All, new HashSet( restrictedCollegeId.HasValue ? [SystemRoles.CollegeAdmin] : [SystemRoles.SuperAdmin])); } public CurrentUserScope Current { get; } } }