using Microsoft.AspNetCore.Identity; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Hua.Todo.Application.Repositories; using Hua.Todo.Application.Services.DataSeeder; using Hua.Todo.Core.Entities; using Xunit; namespace Hua.Todo.Host.Tests.DataSeederTests; /// /// 验收测试。 /// 对应研发工单 v1.2.0 工单 10 的验收标准(Given-When-Then), /// 详见 docs/project/研发工单-v1.2.0/10-数据库初始化默认管理员.md 第五章。 /// public sealed class DataSeederTests : IDisposable { private readonly SqliteConnection _connection; private readonly DbContextOptions _options; private readonly IPasswordHasher _passwordHasher; public DataSeederTests() { _connection = new SqliteConnection("Data Source=:memory:"); _connection.Open(); _options = new DbContextOptionsBuilder() .UseSqlite(_connection) .Options; _passwordHasher = new PasswordHasher(); } public void Dispose() { _connection.Dispose(); } /// /// 创建并迁移数据库上下文(每个测试独立实例,避免数据互相污染)。 /// private TodoDbContext CreateContext() { var context = new TodoDbContext(_options); context.Database.Migrate(); return context; } /// /// 创建使用默认配置的 DataSeeder。 /// private static DataSeeder CreateSeeder(TodoDbContext db, IPasswordHasher hasher, AdminBootstrapOptions? options = null) => new(db, hasher, options ?? new AdminBootstrapOptions()); // ========== 5.1 主流程 ========== /// /// Given 全新空数据库(Migrate 完成,Users 表无任何云用户) /// When 调用 IDataSeeder.SeedAsync() /// Then Users 表新增一条记录,Role="admin",UserName 取配置值,MustChangePassword=true /// And SecurityPolicies 表新增对应记录(AllowPersist=true, AllowSync=true) /// And AuditLogs 表新增一条 EventType="SeedBootstrap" 记录 /// [Fact] public async Task SeedAsync_EmptyDatabase_CreatesDefaultAdminWithPolicyAndAuditLog() { using var db = CreateContext(); var options = new AdminBootstrapOptions { UserName = "admin", Password = "Admin@123", Enabled = true }; var seeder = CreateSeeder(db, _passwordHasher, options); await seeder.SeedAsync(); var admin = await db.Users.SingleOrDefaultAsync(u => u.Role == "admin"); Assert.NotNull(admin); Assert.Equal("admin", admin!.UserName); Assert.True(admin.MustChangePassword); var policy = await db.SecurityPolicies.SingleOrDefaultAsync(p => p.UserId == admin.Id); Assert.NotNull(policy); Assert.True(policy!.AllowPersist); Assert.True(policy.AllowSync); var audit = await db.AuditLogs.SingleOrDefaultAsync(a => a.EventType == "SeedBootstrap"); Assert.NotNull(audit); Assert.True(audit!.IsSuccess); } // ========== 5.2 幂等性 ========== /// /// Given 数据库已存在 Role="admin" 的云用户 /// When 调用 SeedAsync() /// Then Users 表不新增记录 /// And AuditLogs 表不新增 SeedBootstrap 记录 /// [Fact] public async Task SeedAsync_ExistingCloudUser_DoesNotCreateDuplicate() { using var db = CreateContext(); var existingAdmin = await SeedExistingAdminAsync(db, "admin"); var seeder = CreateSeeder(db, _passwordHasher); await seeder.SeedAsync(); var admins = await db.Users.Where(u => u.Role == "admin").ToListAsync(); Assert.Single(admins); Assert.Equal(existingAdmin.Id, admins[0].Id); var seedAudits = await db.AuditLogs.Where(a => a.EventType == "SeedBootstrap").ToListAsync(); Assert.Empty(seedAudits); } // ========== 5.3 配置自定义账号 ========== /// /// Given AdminBootstrapOptions.UserName="root" 且 Password="MyStrong#123" /// When 调用 SeedAsync() /// Then 创建的管理员 UserName="root" /// And 使用该密码可通过 IPasswordHasher.VerifyHashedPassword 校验 /// [Fact] public async Task SeedAsync_CustomOptions_UsesConfiguredCredentials() { using var db = CreateContext(); var options = new AdminBootstrapOptions { Enabled = true, UserName = "root", Password = "MyStrong#123" }; var seeder = CreateSeeder(db, _passwordHasher, options); await seeder.SeedAsync(); var admin = await db.Users.SingleOrDefaultAsync(u => u.UserName == "root"); Assert.NotNull(admin); Assert.Equal("admin", admin!.Role); var verify = _passwordHasher.VerifyHashedPassword(admin, admin.PasswordHash, "MyStrong#123"); Assert.Equal(PasswordVerificationResult.Success, verify); } // ========== 5.4 配置禁用 ========== /// /// Given AdminBootstrapOptions.Enabled=false /// When 调用 SeedAsync() /// Then 不执行任何种子逻辑,直接返回 /// [Fact] public async Task SeedAsync_DisabledOption_DoesNothing() { using var db = CreateContext(); var options = new AdminBootstrapOptions { Enabled = false }; var seeder = CreateSeeder(db, _passwordHasher, options); await seeder.SeedAsync(); var userCount = await db.Users.CountAsync(); Assert.Equal(0, userCount); var auditCount = await db.AuditLogs.CountAsync(); Assert.Equal(0, auditCount); } // ========== 5.5 密码哈希安全 ========== /// /// Given 执行 SeedAsync() 成功 /// When 查询 Users 表 /// Then PasswordHash 不等于明文密码 /// And PasswordHash 可通过 IPasswordHasher.VerifyHashedPassword(明文) 校验为 Success /// [Fact] public async Task SeedAsync_PasswordHashed_NotStoredInPlaintext() { using var db = CreateContext(); const string plainPassword = "Admin@123"; var options = new AdminBootstrapOptions { Enabled = true, UserName = "admin", Password = plainPassword }; var seeder = CreateSeeder(db, _passwordHasher, options); await seeder.SeedAsync(); var admin = await db.Users.SingleAsync(u => u.Role == "admin"); Assert.NotEqual(plainPassword, admin.PasswordHash); Assert.NotEmpty(admin.PasswordHash); var verify = _passwordHasher.VerifyHashedPassword(admin, admin.PasswordHash, plainPassword); Assert.Equal(PasswordVerificationResult.Success, verify); } // ========== 5.6 既有 local 用户不影响初始化 ========== /// /// Given Users 表仅存在 Role="local" 的本地用户(嵌入式遗留数据) /// When 调用 SeedAsync() /// Then 仍然创建默认管理员(因为幂等条件是 Role != "local" 的云用户) /// [Fact] public async Task SeedAsync_OnlyLocalUserExists_StillCreatesAdmin() { using var db = CreateContext(); await SeedLocalUserAsync(db, "local"); var seeder = CreateSeeder(db, _passwordHasher); await seeder.SeedAsync(); var localUser = await db.Users.SingleAsync(u => u.Role == "local"); var admin = await db.Users.SingleOrDefaultAsync(u => u.Role == "admin"); Assert.NotNull(admin); Assert.NotEqual(localUser.Id, admin!.Id); Assert.Equal(2, await db.Users.CountAsync()); } // ========== 辅助方法 ========== /// /// 写入一个已存在的云管理员(模拟"系统已初始化"场景)。 /// private async Task SeedExistingAdminAsync(TodoDbContext db, string userName) { var user = new UserEntity { Id = Guid.NewGuid(), UserName = userName, Role = "admin", CreatedAtUtc = DateTime.UtcNow, UpdatedAtUtc = DateTime.UtcNow }; user.PasswordHash = _passwordHasher.HashPassword(user, "Existing#123"); db.Users.Add(user); await db.SaveChangesAsync(); return user; } /// /// 写入一个本地用户(Role="local",嵌入式模式遗留数据)。 /// private async Task SeedLocalUserAsync(TodoDbContext db, string userName) { var user = new UserEntity { Id = Guid.NewGuid(), UserName = userName, Role = "local", CreatedAtUtc = DateTime.UtcNow, UpdatedAtUtc = DateTime.UtcNow }; user.PasswordHash = _passwordHasher.HashPassword(user, "local-dummy"); db.Users.Add(user); await db.SaveChangesAsync(); } }