using Jiaowu.Api.Controllers; using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Tests; public sealed class BaseDataControllerTests : IAsyncDisposable { private readonly SqliteConnection connection = new("Data Source=:memory:"); private readonly AppDbContext db; private readonly BaseDataController controller; public BaseDataControllerTests() { connection.Open(); var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; db = new AppDbContext(options); db.Database.EnsureCreated(); controller = new BaseDataController(db); } [Fact] public async Task SetCurrentTerm_clears_previous_current_in_one_save() { var previous = CreateTerm("2025-2026-2", true); var next = CreateTerm("2026-2027-1", false); db.AcademicTerms.AddRange(previous, next); await db.SaveChangesAsync(); var result = await controller.SetCurrentTerm(next.Id, CancellationToken.None); Assert.Null(result.Result); db.ChangeTracker.Clear(); Assert.False((await db.AcademicTerms.FindAsync(previous.Id))!.IsCurrent); Assert.True((await db.AcademicTerms.FindAsync(next.Id))!.IsCurrent); Assert.Equal(1, await db.AcademicTerms.CountAsync(x => x.IsCurrent)); } [Fact] public async Task Archive_requires_switching_current_and_is_reversible() { var current = CreateTerm("2026-2027-1", true); var history = CreateTerm("2025-2026-2", false); db.AcademicTerms.AddRange(current, history); await db.SaveChangesAsync(); var currentResult = await controller.ArchiveTerm( current.Id, CancellationToken.None); Assert.IsType(currentResult.Result); var archiveResult = await controller.ArchiveTerm( history.Id, CancellationToken.None); Assert.Null(archiveResult.Result); Assert.True(history.IsArchived); Assert.NotNull(history.ArchivedAt); Assert.True(history.IsEnabled); var unarchiveResult = await controller.UnarchiveTerm( history.Id, CancellationToken.None); Assert.Null(unarchiveResult.Result); Assert.False(history.IsArchived); Assert.Null(history.ArchivedAt); } [Fact] public async Task Archived_term_must_be_unarchived_before_becoming_current() { var current = CreateTerm("2026-2027-1", true); var archived = CreateTerm("2025-2026-2", false); archived.IsArchived = true; archived.ArchivedAt = DateTime.UtcNow; db.AcademicTerms.AddRange(current, archived); await db.SaveChangesAsync(); var result = await controller.SetCurrentTerm( archived.Id, CancellationToken.None); Assert.IsType(result.Result); Assert.True(current.IsCurrent); Assert.False(archived.IsCurrent); } [Fact] public async Task Current_term_cannot_be_cleared_without_a_replacement() { var current = CreateTerm("2026-2027-1", true); db.AcademicTerms.Add(current); await db.SaveChangesAsync(); var request = new TermRequest( current.Code, current.Name, true, current.AcademicYear, current.Season, current.StartDate, current.EndDate, false); var result = await controller.UpdateTerm( current.Id, request, CancellationToken.None); Assert.IsType(result.Result); Assert.True(current.IsCurrent); } private static AcademicTerm CreateTerm(string code, bool isCurrent) => new() { Code = code, Name = code, AcademicYear = code[..9], Season = TermSeason.Autumn, StartDate = new DateOnly(2026, 9, 1), EndDate = new DateOnly(2027, 1, 20), IsCurrent = isCurrent, IsEnabled = true }; public async ValueTask DisposeAsync() { await db.DisposeAsync(); await connection.DisposeAsync(); } }