using System.Text.Json; using ClosedXML.Excel; 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.Persistence; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Tests; public sealed class StatisticsControllerTests { [Fact] public async Task Course_summary_cache_is_scope_isolated_and_export_is_fresh() { 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 firstCollege = new College { Code = "C01", Name = "第一学院" }; var secondCollege = new College { Code = "C02", Name = "第二学院" }; var category = new CourseCategory { Code = "CAT", Name = "测试分类" }; db.AddRange( firstCollege, secondCollege, category, CreateCourse("C001", firstCollege, category), CreateCourse("C002", secondCollege, category)); await db.SaveChangesAsync(); var cache = new RecordingCache(); var firstController = new StatisticsController( db, new CollegeDataScope(firstCollege.Id), cache); var secondController = new StatisticsController( db, new CollegeDataScope(secondCollege.Id), cache); var first = await firstController.GetCourseSummary( null, null, null, CancellationToken.None); Assert.Equal(1, TotalCourses(first)); db.Courses.Add(CreateCourse("C003", firstCollege, category)); await db.SaveChangesAsync(); var cached = await firstController.GetCourseSummary( null, null, null, CancellationToken.None); Assert.Equal(1, TotalCourses(cached)); var otherCollege = await secondController.GetCourseSummary( null, null, null, CancellationToken.None); Assert.Equal(1, TotalCourses(otherCollege)); Assert.Equal(2, cache.SourceCalls); Assert.Equal(2, cache.Keys.Count); var export = await firstController.ExportCourses( null, null, null, CancellationToken.None); var file = Assert.IsType(export); using var stream = new MemoryStream(file.FileContents); using var workbook = new XLWorkbook(stream); Assert.Equal( 2, workbook.Worksheet("课程性质").Cell(1, 2).GetValue()); Assert.Equal(2, cache.SourceCalls); } private static int TotalCourses(ActionResult result) { var json = Assert.IsType(result.Value); return json.GetProperty("totals").GetProperty("totalCourses").GetInt32(); } private static Course CreateCourse( string code, College college, CourseCategory category) => new() { Code = code, Name = $"课程 {code}", CollegeId = college.Id, CourseCategoryId = category.Id, Nature = CourseNature.MajorRequired, Credits = 2, TotalHours = 32, LectureHours = 32, AssessmentMethod = AssessmentMethod.Examination }; private sealed class RecordingCache : IAppCache { private readonly Dictionary values = []; public int SourceCalls { get; private set; } public IReadOnlyCollection Keys => values.Keys; public async Task GetOrCreateAsync( string key, Func> factory, AppCacheProfile profile, IReadOnlyCollection tags, CancellationToken cancellationToken) { if (values.TryGetValue(key, out var value)) return (T)value; SourceCalls++; var loaded = await factory(cancellationToken); values[key] = loaded!; return loaded; } public ValueTask RemoveByTagAsync( string tag, CancellationToken cancellationToken = default) => ValueTask.CompletedTask; } private sealed class CollegeDataScope(Guid collegeId) : ICurrentUserDataScope { public CurrentUserScope Current { get; } = new( Guid.NewGuid(), "学院管理员", collegeId, DataScope.College, new HashSet { SystemRoles.CollegeAdmin }); } }