已完成教学任务归档纠错功能:
已归档任务新增“撤销归档”和“删除”,支持单条及批量操作。 撤销后恢复为“已发布”状态。 已发布任务仍不可直接删除;被排课、选课、成绩或考试引用的归档任务也会拒绝删除并明确提示。 状态与操作文案统一为“归档”
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
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<NoContentResult>(deleteArchived);
|
||||
Assert.IsType<ConflictObjectResult>(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<NoContentResult>(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<OkObjectResult>(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));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
this.connection = connection;
|
||||
Db = db;
|
||||
this.term = term;
|
||||
this.course = course;
|
||||
Controller = new TeachingTasksController(
|
||||
db,
|
||||
new TestDataScope());
|
||||
}
|
||||
|
||||
public AppDbContext Db { get; }
|
||||
public TeachingTasksController Controller { get; }
|
||||
|
||||
public static async Task<TestDatabase> 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 = "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);
|
||||
}
|
||||
|
||||
public async Task<TeachingTask> 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 CurrentUserScope Current { get; } = new(
|
||||
Guid.NewGuid(),
|
||||
"测试管理员",
|
||||
null,
|
||||
DataScope.All,
|
||||
new HashSet<string>([SystemRoles.SuperAdmin]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user