Files
Academic-Affairs-System/tests/Jiaowu.Api.Tests/ExamDeletionControllerTests.cs
T
biss 64decdab7e 草稿计划支持选择场次后“批量移除已选场次”,一次最多 100 个。
批量操作会完整校验:包含不存在、其他计划或已发布计划的场次时整批拒绝,不会部分删除。[ExamsController.cs (line 384)](E:/jiaowu/src/Jiaowu.Api/Controllers/ExamsController.cs:384)
草稿计划新增“删除草稿”按钮,删除前显示计划名称及场次数量确认。[ExamsView.vue (line 292)](E:/jiaowu/web/src/views/ExamsView.vue:292)
删除草稿计划会级联清理场次和监考关联;已发布计划不能删除。[ExamsController.cs (line 81)](E:/jiaowu/src/Jiaowu.Api/Controllers/ExamsController.cs:81)
删除后会自动切换到其他计划;没有剩余计划时正确显示空状态。
新增草稿、已发布、跨计划混选及级联删除测试。[ExamDeletionControllerTests.cs (line 17)](E:/jiaowu/tests/Jiaowu.Api.Tests/ExamDeletionControllerTests.cs:17)
2026-07-27 18:31:52 +08:00

274 lines
9.1 KiB
C#

using Jiaowu.Api.Controllers;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Exams;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace Jiaowu.Api.Tests;
public sealed class ExamDeletionControllerTests
{
[Fact]
public async Task RemoveSessionsBatch_ValidatesCompleteSelectionBeforeRemoving()
{
await using var fixture = await Fixture.CreateAsync();
var controller = fixture.CreateController();
var selectedIds = fixture.DraftPlan.Sessions
.Select(x => x.Id)
.ToArray();
var otherPlanSessionId = fixture.PublishedPlan.Sessions.Single().Id;
var invalidResult = await controller.RemoveSessionsBatch(
fixture.DraftPlan.Id,
new RemoveExamSessionsBatchRequest(
[selectedIds[0], otherPlanSessionId]),
CancellationToken.None);
Assert.IsType<ConflictObjectResult>(invalidResult);
Assert.Equal(2, await fixture.Db.ExamSessions.CountAsync(
x => x.ExamPlanId == fixture.DraftPlan.Id));
var result = Assert.IsType<OkObjectResult>(
await controller.RemoveSessionsBatch(
fixture.DraftPlan.Id,
new RemoveExamSessionsBatchRequest(selectedIds),
CancellationToken.None));
Assert.Equal(2, ReadIntProperty(result.Value!, "removedCount"));
Assert.False(await fixture.Db.ExamSessions.AnyAsync(
x => x.ExamPlanId == fixture.DraftPlan.Id));
Assert.False(await fixture.Db.ExamSessionInvigilators.AnyAsync(
x => selectedIds.Contains(x.ExamSessionId)));
Assert.True(await fixture.Db.ExamPlans.AnyAsync(
x => x.Id == fixture.DraftPlan.Id));
}
[Fact]
public async Task RemoveSessionsBatch_RejectsPublishedPlan()
{
await using var fixture = await Fixture.CreateAsync();
var controller = fixture.CreateController();
var sessionId = fixture.PublishedPlan.Sessions.Single().Id;
var result = await controller.RemoveSessionsBatch(
fixture.PublishedPlan.Id,
new RemoveExamSessionsBatchRequest([sessionId]),
CancellationToken.None);
Assert.IsType<ConflictObjectResult>(result);
Assert.True(await fixture.Db.ExamSessions.AnyAsync(
x => x.Id == sessionId));
}
[Fact]
public async Task DeletePlan_DeletesDraftWithSessions_AndRejectsPublishedPlan()
{
await using var fixture = await Fixture.CreateAsync();
var controller = fixture.CreateController();
var draftPlanId = fixture.DraftPlan.Id;
var publishedPlanId = fixture.PublishedPlan.Id;
var draftSessionIds = fixture.DraftPlan.Sessions
.Select(x => x.Id)
.ToArray();
var publishedResult = await controller.DeletePlan(
publishedPlanId,
CancellationToken.None);
Assert.IsType<ConflictObjectResult>(publishedResult);
Assert.True(await fixture.Db.ExamPlans.AnyAsync(
x => x.Id == publishedPlanId));
var draftResult = await controller.DeletePlan(
draftPlanId,
CancellationToken.None);
Assert.IsType<NoContentResult>(draftResult);
Assert.False(await fixture.Db.ExamPlans.AnyAsync(
x => x.Id == draftPlanId));
Assert.False(await fixture.Db.ExamSessions.AnyAsync(
x => x.ExamPlanId == draftPlanId));
Assert.False(await fixture.Db.ExamSessionInvigilators.AnyAsync(
x => draftSessionIds.Contains(x.ExamSessionId)));
Assert.True(await fixture.Db.ExamPlans.AnyAsync(
x => x.Id == publishedPlanId));
}
private static int ReadIntProperty(object value, string name) =>
(int)value.GetType().GetProperty(name)!.GetValue(value)!;
private sealed class Fixture : IAsyncDisposable
{
private readonly SqliteConnection connection;
private Fixture(
SqliteConnection connection,
AppDbContext db,
ExamPlan draftPlan,
ExamPlan publishedPlan)
{
this.connection = connection;
Db = db;
DraftPlan = draftPlan;
PublishedPlan = publishedPlan;
}
public AppDbContext Db { get; }
public ExamPlan DraftPlan { get; }
public ExamPlan PublishedPlan { get; }
public static async Task<Fixture> CreateAsync()
{
var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(connection)
.Options;
var db = new AppDbContext(options);
await db.Database.EnsureCreatedAsync();
var college = new College
{
Code = "CS",
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, 20)
};
var course = new Course
{
Code = "CS101",
Name = "程序设计基础",
CollegeId = college.Id,
Credits = 4,
TotalHours = 64,
LectureHours = 48,
PracticeHours = 16,
Nature = CourseNature.MajorRequired,
AssessmentMethod = AssessmentMethod.Examination
};
var firstTask = CreateTask(
term.Id,
course.Id,
"2026-1-CS101-01");
var secondTask = CreateTask(
term.Id,
course.Id,
"2026-1-CS101-02");
var invigilator = new Teacher
{
TeacherNumber = "T001",
Name = "张老师",
CollegeId = college.Id
};
var firstDraftSession = CreateSession(
firstTask.Id,
new DateOnly(2027, 1, 8));
firstDraftSession.Invigilators =
[
new ExamSessionInvigilator
{
TeacherId = invigilator.Id
}
];
var draftPlan = new ExamPlan
{
AcademicTermId = term.Id,
Name = "期末考试草稿",
Sessions =
[
firstDraftSession,
CreateSession(secondTask.Id, new DateOnly(2027, 1, 9))
]
};
var publishedPlan = new ExamPlan
{
AcademicTermId = term.Id,
Name = "已发布期末考试",
Status = ExamPlanStatus.Published,
PublishedAt = DateTime.UtcNow,
Sessions =
[
CreateSession(firstTask.Id, new DateOnly(2027, 1, 10))
]
};
db.AddRange(
college,
term,
course,
firstTask,
secondTask,
invigilator,
draftPlan,
publishedPlan);
await db.SaveChangesAsync();
return new Fixture(
connection,
db,
draftPlan,
publishedPlan);
}
public ExamsController CreateController() => new(
Db,
new ManagerDataScope(),
new ExamArrangementService(Db),
NoOpAppCache.Instance);
public async ValueTask DisposeAsync()
{
await Db.DisposeAsync();
await connection.DisposeAsync();
}
private static TeachingTask CreateTask(
Guid termId,
Guid courseId,
string number) => new()
{
AcademicTermId = termId,
CourseId = courseId,
TaskNumber = number,
Name = $"教学班 {number}",
Capacity = 60,
Status = TeachingTaskStatus.Published
};
private static ExamSession CreateSession(
Guid taskId,
DateOnly examDate) => new()
{
TeachingTaskId = taskId,
ExamDate = examDate,
StartPeriod = 1,
PeriodCount = 2,
StartsAt = examDate.ToDateTime(new TimeOnly(8, 0)),
EndsAt = examDate.ToDateTime(new TimeOnly(9, 50)),
RequiredInvigilatorCount = 2
};
}
private sealed class ManagerDataScope : ICurrentUserDataScope
{
public CurrentUserScope Current { get; } = new(
Guid.NewGuid(),
"考试管理员",
null,
DataScope.All,
new HashSet<string>([SystemRoles.AcademicAdmin]));
}
}