using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Hua.Todo.Core.Entities;
using Xunit;
using System.Data.Common;
using Hua.Todo.Application.Repositories;
namespace Hua.Todo.Maui.Tests;
///
/// 数据库初始化集成测试,覆盖 MauiProgram.InitializeDatabase 的 Migrate → EnsureCreated 回退链路。
/// 对应日志中 "no such table: T_Tasks" 错误场景,验证迁移链完整性与恢复能力。
///
public class DatabaseInitializationTests : IDisposable
{
private readonly SqliteConnection _connection;
private readonly DbContextOptions _options;
public DatabaseInitializationTests()
{
_connection = new SqliteConnection("Data Source=:memory:");
_connection.Open();
_options = new DbContextOptionsBuilder()
.UseSqlite(_connection)
.Options;
}
public void Dispose()
{
_connection.Dispose();
}
private TodoDbContext CreateContext()
{
return new TodoDbContext(_options);
}
///
/// 模拟 MauiProgram.InitializeDatabase 的 Migrate() 路径:
/// 从零开始执行全部迁移,验证 T_Tasks 表存在。
/// 对应日志第 1-21 行:Migrate() 失败场景。
///
[Fact]
public void InitializeDatabase_Migrate_FromScratch_CreatesTTasksTable()
{
// Arrange - 模拟 MauiProgram.InitializeDatabase 中的 Migrate() 调用
using var context = CreateContext();
// Act: 执行迁移(等同于 Migrate())
context.Database.Migrate();
// Assert: T_Tasks 表存在
var tables = GetTableNames(context);
Assert.Contains("T_Tasks", tables);
}
///
/// 验证迁移后所有核心表存在。
/// 对应日志第 23-27 行:查询 T_Tasks 失败场景。
///
[Fact]
public void InitializeDatabase_Migrate_AllCoreTablesExist()
{
using var context = CreateContext();
context.Database.Migrate();
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 表包含日志中查询的所有列。
/// 对应日志第 24-26 行 SELECT 查询中引用的所有字段。
///
[Fact]
public void InitializeDatabase_TTasks_HasAllColumnsFromQuery()
{
using var context = CreateContext();
context.Database.Migrate();
var columns = GetColumnNames(context, "T_Tasks");
// 日志第 24 行 SELECT 查询中引用的字段,必须全部存在
Assert.Contains("Id", columns);
Assert.Contains("AudioDuration", columns);
Assert.Contains("Code", columns);
Assert.Contains("ConcurrencyStamp", columns);
Assert.Contains("CreationTime", columns);
Assert.Contains("CreatorId", columns);
Assert.Contains("DeleterId", columns);
Assert.Contains("DeletionTime", columns);
Assert.Contains("Description", columns);
Assert.Contains("ExtraProperties", columns);
Assert.Contains("IsCompleted", columns);
Assert.Contains("IsDeleted", columns);
Assert.Contains("LastModificationTime", columns);
Assert.Contains("LastModifierId", columns);
Assert.Contains("MeetingNotes", columns);
Assert.Contains("ParentTaskId", columns);
Assert.Contains("Priority", columns);
Assert.Contains("TaskType", columns);
Assert.Contains("Title", columns);
Assert.Contains("UserId", columns);
}
///
/// 模拟 MauiProgram.InitializeDatabase 的 fallback 路径:
/// Migrate 失败 → 删除数据库 → EnsureCreated 重建。
/// 对应日志第 1 行 "Database initialization failed" 后的恢复流程。
///
[Fact]
public void InitializeDatabase_Fallback_EnsureCreated_AfterFailure()
{
// 使用文件数据库模拟 fallback 中的文件删除操作
var tempDbPath = Path.Combine(Path.GetTempPath(), $"hua_todo_test_{Guid.NewGuid()}.db");
try
{
var connectionString = $"Data Source={tempDbPath}";
// 先创建一个损坏的数据库(有迁移历史但表被删除)
SqliteConnection? conn1 = null;
try
{
conn1 = new SqliteConnection(connectionString);
conn1.Open();
var options1 = new DbContextOptionsBuilder()
.UseSqlite(conn1)
.Options;
using var context1 = new TodoDbContext(options1);
context1.Database.Migrate();
// 手动删除 T_Tasks 模拟损坏
context1.Database.ExecuteSqlRaw("DROP TABLE IF EXISTS T_Tasks;");
}
finally
{
// 显式关闭连接并清空连接池,确保文件句柄释放
conn1?.Close();
conn1?.Dispose();
SqliteConnection.ClearAllPools();
}
// 模拟 InitializeDatabase fallback: 删除 DB 文件,重新创建
if (File.Exists(tempDbPath))
{
File.Delete(tempDbPath);
var walPath = tempDbPath + "-wal";
var shmPath = tempDbPath + "-shm";
if (File.Exists(walPath)) File.Delete(walPath);
if (File.Exists(shmPath)) File.Delete(shmPath);
}
// EnsureCreated: 从当前模型创建 schema
SqliteConnection? conn2 = null;
try
{
conn2 = new SqliteConnection(connectionString);
conn2.Open();
var options2 = new DbContextOptionsBuilder()
.UseSqlite(conn2)
.Options;
using var context2 = new TodoDbContext(options2);
// Act: EnsureCreated(fallback 路径)
context2.Database.EnsureCreated();
// Assert: T_Tasks 已重建
var tables = GetTableNames(context2);
Assert.Contains("T_Tasks", tables);
Assert.Contains("Attachments", tables);
}
finally
{
conn2?.Close();
conn2?.Dispose();
SqliteConnection.ClearAllPools();
}
}
finally
{
// 清理:确保所有连接池已释放
SqliteConnection.ClearAllPools();
GC.Collect();
GC.WaitForPendingFinalizers();
if (File.Exists(tempDbPath)) File.Delete(tempDbPath);
var w = tempDbPath + "-wal";
var s = tempDbPath + "-shm";
if (File.Exists(w)) File.Delete(w);
if (File.Exists(s)) File.Delete(s);
}
}
///
/// 验证 Migrate 是幂等的:第二次调用不会报错。
/// 模拟多次启动 MauiProgram 的场景。
///
[Fact]
public void InitializeDatabase_Migrate_Idempotent()
{
using var context = CreateContext();
context.Database.Migrate();
// 第二次 Migrate 不应抛出异常
var exception = Record.Exception(() => context.Database.Migrate());
Assert.Null(exception);
}
///
/// 验证迁移后可以执行日志中失败的查询。
/// 对应日志第 23-27 行 SELECT 查询。
///
[Fact]
public void InitializeDatabase_CanExecuteQueryFromLog()
{
using var context = CreateContext();
context.Database.Migrate();
// 创建测试用户
var userId = Guid.NewGuid();
context.Users.Add(new UserEntity
{
Id = userId,
UserName = "test",
PasswordHash = "hash",
PasswordSalt = "salt",
Role = "User"
});
context.SaveChanges();
// 插入测试任务
var taskId = Guid.NewGuid();
context.Tasks.Add(new TaskEntity
{
Id = taskId,
UserId = userId,
Title = "测试",
Priority = TaskPriority.Medium,
Code = "T001",
CreationTime = DateTime.UtcNow,
CreatorId = userId
});
context.SaveChanges();
// 执行日志中的查询:LEFT JOIN T_Tasks ON ParentTaskId
var tasks = context.Tasks
.Include(t => t.ParentTask)
.OrderBy(t => t.Id)
.ToList();
// 验证查询不抛异常且返回结果
Assert.NotNull(tasks);
Assert.NotEmpty(tasks);
Assert.Contains(tasks, t => t.Title == "测试");
}
///
/// 验证迁移顺序正确:MakeTaskEntityAbpCompatible 在 AddMeetingAndDescriptionFields 之前执行。
/// 这是日志中 "no such table: T_Tasks" 错误的根因验证。
///
[Fact]
public void InitializeDatabase_MigrationOrder_Correct()
{
using var context = CreateContext();
context.Database.Migrate();
var history = context.GetService();
var appliedMigrations = history.GetAppliedMigrations()
.Select(m => m.MigrationId)
.ToList();
// 找到两个迁移的索引位置
var abpCompatIndex = appliedMigrations.FindIndex(m => m.Contains("MakeTaskEntityAbpCompatible"));
var meetingFieldsIndex = appliedMigrations.FindIndex(m => m.Contains("AddMeetingAndDescriptionFields"));
Assert.True(abpCompatIndex >= 0, "MakeTaskEntityAbpCompatible should be applied");
Assert.True(meetingFieldsIndex >= 0, "AddMeetingAndDescriptionFields should be applied");
// MakeTaskEntityAbpCompatible 必须在 AddMeetingAndDescriptionFields 之前
Assert.True(abpCompatIndex < meetingFieldsIndex,
$"MakeTaskEntityAbpCompatible (index {abpCompatIndex}) must be before AddMeetingAndDescriptionFields (index {meetingFieldsIndex})");
}
// ========== 辅助方法 ==========
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();
}
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())
{
columns.Add(reader.GetString(1)); // name 列是第 2 列 (index 1)
}
return columns;
}
}