using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Xunit; using System.Data.Common; using Hua.Todo.Application.Repositories; namespace Hua.Todo.Host.Tests; /// /// 数据库迁移集成测试,验证迁移链的完整性与幂等性。 /// 覆盖 Maui/Avalonia 中 InitializeDatabase 的 Migrate → EnsureCreated 回退链路。 /// public class DatabaseMigrationTests : IDisposable { private readonly SqliteConnection _connection; private readonly DbContextOptions _options; public DatabaseMigrationTests() { // 使用独立的内存数据库(每次测试独立的连接) _connection = new SqliteConnection("Data Source=:memory:"); _connection.Open(); _options = new DbContextOptionsBuilder() .UseSqlite(_connection) .Options; } public void Dispose() { _connection.Dispose(); } /// /// 创建新的 DbContext 实例。 /// private TodoDbContext CreateContext() { return new TodoDbContext(_options); } /// /// 验证所有迁移可以从头开始成功应用(Migrate 从零到当前模型)。 /// [Fact] public void Migrate_FromScratch_AllMigrationsApplied() { // Arrange using var context = CreateContext(); // Act: 执行全部迁移 context.Database.Migrate(); // Assert: 验证所有核心表已创建 var tables = GetTableNames(context); Assert.Contains("T_Tasks", tables); Assert.Contains("Attachments", tables); Assert.Contains("Users", tables); Assert.Contains("UserSessions", tables); Assert.Contains("SecurityPolicies", tables); Assert.Contains("AuditLogs", tables); } /// /// 验证 Migrate 是幂等的:第二次调用 Migrate 不会报错。 /// [Fact] public void Migrate_Idempotent_SecondCallDoesNotThrow() { // Arrange using var context = CreateContext(); context.Database.Migrate(); // Act & Assert: 第二次 Migrate 不应抛出异常 var exception = Record.Exception(() => context.Database.Migrate()); Assert.Null(exception); } /// /// 验证 T_Tasks 表包含所有期望的列(ABP 审计字段 + 业务字段 + v1.3.0 新增字段)。 /// [Fact] public void T_Tasks_HasAllExpectedColumns() { // Arrange using var context = CreateContext(); context.Database.Migrate(); // Act var columns = GetColumnNames(context, "T_Tasks"); // Assert: ABP 审计字段 Assert.Contains("Id", columns); Assert.Contains("ExtraProperties", columns); Assert.Contains("ConcurrencyStamp", columns); Assert.Contains("CreationTime", columns); Assert.Contains("CreatorId", columns); Assert.Contains("LastModificationTime", columns); Assert.Contains("LastModifierId", columns); Assert.Contains("IsDeleted", columns); Assert.Contains("DeletionTime", columns); Assert.Contains("DeleterId", columns); // Assert: 业务字段 Assert.Contains("UserId", columns); Assert.Contains("Title", columns); Assert.Contains("Priority", columns); Assert.Contains("IsCompleted", columns); Assert.Contains("Code", columns); Assert.Contains("ParentTaskId", columns); // Assert: v1.3.0 新增字段 Assert.Contains("TaskType", columns); Assert.Contains("MeetingNotes", columns); Assert.Contains("AudioDuration", columns); Assert.Contains("Description", columns); } /// /// 验证 Attachments 表存在且包含期望的列。 /// [Fact] public void Attachments_HasAllExpectedColumns() { // Arrange using var context = CreateContext(); context.Database.Migrate(); // Act var columns = GetColumnNames(context, "Attachments"); // Assert Assert.Contains("Id", columns); Assert.Contains("FileName", columns); Assert.Contains("FilePath", columns); Assert.Contains("ContentType", columns); Assert.Contains("FileSize", columns); Assert.Contains("AttachmentType", columns); Assert.Contains("CreatedAt", columns); Assert.Contains("TaskId", columns); } /// /// 验证 EnsureCreated 可以独立创建完整 schema(不依赖迁移历史)。 /// 模拟 MauiProgram.InitializeDatabase 的 fallback 路径。 /// [Fact] public void EnsureCreated_FromScratch_CreatesAllTables() { // Arrange using var context = CreateContext(); // Act: 使用 EnsureCreated 而非 Migrate context.Database.EnsureCreated(); // Assert: 所有核心表存在 var tables = GetTableNames(context); Assert.Contains("T_Tasks", tables); Assert.Contains("Attachments", tables); Assert.Contains("Users", tables); Assert.Contains("UserSessions", tables); Assert.Contains("SecurityPolicies", tables); Assert.Contains("AuditLogs", tables); } /// /// 验证 Migrate 后所有核心表(包括 Attachments)都存在。 /// [Fact] public void Migrate_AllTablesExist_IncludingAttachments() { // Arrange using var context = CreateContext(); // Act: 执行全部迁移 context.Database.Migrate(); // Assert: 所有核心表已创建 var tables = GetTableNames(context); Assert.Contains("T_Tasks", tables); Assert.Contains("Attachments", tables); Assert.Contains("Users", tables); Assert.Contains("UserSessions", tables); Assert.Contains("SecurityPolicies", tables); Assert.Contains("AuditLogs", tables); } /// /// 验证迁移后可以正常读写 T_Tasks 表。 /// [Fact] public void Migrate_CanInsertAndQueryTask() { // Arrange using var context = CreateContext(); context.Database.Migrate(); // 创建测试用户 var userId = Guid.NewGuid(); context.Users.Add(new Core.Entities.UserEntity { Id = userId, UserName = "test", PasswordHash = "hash", PasswordSalt = "salt", Role = "User" }); context.SaveChanges(); // Act: 插入任务 var taskId = Guid.NewGuid(); context.Tasks.Add(new Core.Entities.TaskEntity { Id = taskId, UserId = userId, Title = "测试任务", Priority = Core.Entities.TaskPriority.High, Code = "T001", Description = "测试描述", TaskType = Core.Entities.TaskType.Normal, CreationTime = DateTime.UtcNow, CreatorId = userId }); context.SaveChanges(); // Assert: 查询验证 var task = context.Tasks.Find(taskId); Assert.NotNull(task); Assert.Equal("测试任务", task!.Title); Assert.Equal("测试描述", task.Description); Assert.Equal(Core.Entities.TaskType.Normal, task.TaskType); } /// /// 验证迁移历史表包含所有预期迁移。 /// [Fact] public void Migrate_RecordsAllMigrationsInHistory() { // Arrange using var context = CreateContext(); // Act context.Database.Migrate(); // Assert var history = context.GetService(); var appliedMigrations = history.GetAppliedMigrations().Select(m => m.MigrationId).ToList(); Assert.NotEmpty(appliedMigrations); // 迁移已合并为单一 InitialCreate,仅验证该迁移已记录 Assert.Contains(appliedMigrations, m => m.Contains("InitialCreate")); } // ========== 辅助方法 ========== /// /// 获取数据库中所有表名。 /// private static List GetTableNames(TodoDbContext context) { return context.Database.SqlQueryRaw( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__EF%' ORDER BY name" ).ToList(); } /// /// 获取指定表的列名列表。 /// 使用原始 ADO.NET 查询 PRAGMA table_info 以避免 EF Core 列映射问题。 /// private static List GetColumnNames(TodoDbContext context, string tableName) { var columns = new List(); var connection = context.Database.GetDbConnection(); // 确保连接已打开(内存数据库可能已关闭) if (connection.State != System.Data.ConnectionState.Open) connection.Open(); using var command = connection.CreateCommand(); command.CommandText = $"PRAGMA table_info('{tableName}')"; using var reader = command.ExecuteReader(); while (reader.Read()) { // PRAGMA table_info 返回: cid(0), name(1), type(2), notnull(3), dflt_value(4), pk(5) columns.Add(reader.GetString(1)); } return columns; } }