using Hua.Todo.Application.Repositories; using Hua.Todo.Application.Services.CloudSync.Models; using Hua.Todo.Application.Services.CloudSync.Services; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Hua.Todo.Core.Entities; using Xunit; namespace Hua.Todo.Host.Tests; /// /// CloudTaskSyncService SQLite 集成测试(验证 UNIQUE 约束不会在生产环境中触发)。 /// public class CloudTaskSyncServiceSqliteTests : IDisposable { private readonly SqliteConnection _connection; private readonly TodoDbContext _dbContext; private readonly CloudTaskSyncService _service; private readonly Guid _testUserId; public CloudTaskSyncServiceSqliteTests() { _testUserId = Guid.NewGuid(); // 使用共享缓存的 SQLite 内存数据库 _connection = new SqliteConnection("Data Source=CloudTaskSyncTests;Mode=Memory;Cache=Shared"); _connection.Open(); var options = new DbContextOptionsBuilder() .UseSqlite(_connection) .Options; _dbContext = new TodoDbContext(options); _dbContext.Database.EnsureCreated(); // 创建测试用户(满足 UserId 外键约束) _dbContext.Users.Add(new UserEntity { Id = _testUserId, UserName = "test_user", PasswordHash = "hash", PasswordSalt = "salt", Role = "User" }); _dbContext.SaveChanges(); _service = new CloudTaskSyncService(_dbContext, NullLogger.Instance); } public void Dispose() { _dbContext.Dispose(); _connection.Dispose(); } /// /// 复现用户报告的 Bug:同一批次中同步父子任务(均有已知 Id,lastModificationTime 为 null)。 /// 期望:首次同步成功创建两个任务。 /// [Fact] public async Task SyncAsync_ParentChildWithNullTime_FirstSync_Succeeds() { var parentId = Guid.NewGuid(); var childId = Guid.NewGuid(); var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = childId, Title = "子任务", Priority = TaskPriority.Medium, Code = "2", ParentTaskId = parentId, LastModificationTime = null }, new CloudTaskUpsert { Id = parentId, Title = "父任务", Priority = TaskPriority.High, Code = "1", ParentTaskId = null, LastModificationTime = null } } }; var response = await _service.SyncAsync(_testUserId, request, CancellationToken.None); Assert.Equal(2, response.Tasks.Count); var parent = response.Tasks.First(t => t.Title == "父任务"); Assert.Null(parent.ParentTaskId); var child = response.Tasks.First(t => t.Title == "子任务"); Assert.Equal(parentId, child.ParentTaskId); } /// /// 复现 Bug 场景:首次同步成功后再用相同数据重新同步(模拟客户端重试)。 /// lastModificationTime 为 null 时 LWW 应跳过更新,不应抛出 UNIQUE 约束。 /// [Fact] public async Task SyncAsync_ParentChildWithNullTime_Resync_NoUniqueViolation() { var parentId = Guid.NewGuid(); var childId = Guid.NewGuid(); var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = childId, Title = "子任务", Priority = TaskPriority.Medium, Code = "2", ParentTaskId = parentId, LastModificationTime = null }, new CloudTaskUpsert { Id = parentId, Title = "父任务", Priority = TaskPriority.High, Code = "1", ParentTaskId = null, LastModificationTime = null } } }; // 首次同步 var firstResponse = await _service.SyncAsync(_testUserId, request, CancellationToken.None); Assert.Equal(2, firstResponse.Tasks.Count); // 重新同步(同一 DbContext,模拟客户端重试) var resyncResponse = await _service.SyncAsync(_testUserId, request, CancellationToken.None); Assert.Equal(2, resyncResponse.Tasks.Count); } /// /// 测试多次重试同步不引发 UNIQUE 约束。 /// [Fact] public async Task SyncAsync_ParentChildWithNullTime_MultipleResyncs_NoUniqueViolation() { var parentId = Guid.NewGuid(); var childId = Guid.NewGuid(); var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = childId, Title = "子任务", Priority = TaskPriority.Medium, Code = "2", ParentTaskId = parentId, LastModificationTime = null }, new CloudTaskUpsert { Id = parentId, Title = "父任务", Priority = TaskPriority.High, Code = "1", ParentTaskId = null, LastModificationTime = null } } }; for (int i = 0; i < 5; i++) { var response = await _service.SyncAsync(_testUserId, request, CancellationToken.None); Assert.Equal(2, response.Tasks.Count); } } /// /// 测试已有 DbContext 跟踪实体的场景下同步不会引发 UNIQUE 冲突。 /// 模拟场景:任务已通过其他 API 创建并仍被跟踪,然后通过 CloudSync 同步同一数据。 /// [Fact] public async Task SyncAsync_TaskExistsThenSynced_NoUniqueViolation() { var taskId = Guid.NewGuid(); var existingTask = new TaskEntity { Id = taskId, UserId = _testUserId, Title = "已有任务", Priority = TaskPriority.Medium, CreationTime = DateTime.UtcNow, CreatorId = _testUserId, LastModificationTime = DateTime.UtcNow, LastModifierId = _testUserId }; _dbContext.Tasks.Add(existingTask); await _dbContext.SaveChangesAsync(); var syncRequest = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "已有任务", LastModificationTime = null } } }; var response = await _service.SyncAsync(_testUserId, syncRequest, CancellationToken.None); Assert.Single(response.Tasks); Assert.Equal("已有任务", response.Tasks.First().Title); } /// /// 复现用户 Bug 的核心场景:使用独立 DbContext 模拟跨请求重试。 /// Context A 创建任务并提交,Context B 再次同步相同数据时不应触发 UNIQUE 约束。 /// [Fact] public async Task SyncAsync_CrossContextResync_NoUniqueViolation() { var parentId = Guid.NewGuid(); var childId = Guid.NewGuid(); var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = childId, Title = "子任务", Priority = TaskPriority.Medium, Code = "2", ParentTaskId = parentId, LastModificationTime = null }, new CloudTaskUpsert { Id = parentId, Title = "父任务", Priority = TaskPriority.High, Code = "1", ParentTaskId = null, LastModificationTime = null } } }; // Context A:首次同步 var firstResponse = await _service.SyncAsync(_testUserId, request, CancellationToken.None); Assert.Equal(2, firstResponse.Tasks.Count); // Context B:使用全新 DbContext 重新同步(模拟另一个请求) var options = new DbContextOptionsBuilder() .UseSqlite(_connection) .Options; using var dbContextB = new TodoDbContext(options); var serviceB = new CloudTaskSyncService(dbContextB, NullLogger.Instance); var resyncResponse = await serviceB.SyncAsync(_testUserId, request, CancellationToken.None); Assert.Equal(2, resyncResponse.Tasks.Count); } /// /// 使用报错场景中的精确 UUID 复现 Bug: /// 父子任务在同一批次中同步,lastModificationTime 均为 null。 /// [Fact] public async Task SyncAsync_ExactBugReportUuids_NoUniqueViolation() { // 使用报错日志中的精确 UUID var parentId = Guid.Parse("6afeea6f-296f-4da1-a1e1-279e818f932c"); var childId = Guid.Parse("412290d0-01b7-4348-af2b-a6abb55dd580"); var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = childId, Title = "测试", Priority = (TaskPriority)1, IsCompleted = false, Code = "2", ParentTaskId = parentId, LastModificationTime = null }, new CloudTaskUpsert { Id = parentId, Title = "测试", Priority = (TaskPriority)1, IsCompleted = false, Code = "1", ParentTaskId = null, LastModificationTime = null } } }; // 首次同步 var response1 = await _service.SyncAsync(_testUserId, request, CancellationToken.None); Assert.Equal(2, response1.Tasks.Count); // 使用全新 DbContext 重新同步(模拟跨请求重试) var options = new DbContextOptionsBuilder() .UseSqlite(_connection) .Options; using var dbContextB = new TodoDbContext(options); var serviceB = new CloudTaskSyncService(dbContextB, NullLogger.Instance); // 重试同步 - 不应抛出 UNIQUE 约束 var response2 = await serviceB.SyncAsync(_testUserId, request, CancellationToken.None); Assert.Equal(2, response2.Tasks.Count); } /// /// 测试同一次同步请求中,父任务已存在于 DB 但 lastModificationTime 为 null 的重同步场景。 /// [Fact] public async Task SyncAsync_ExistingTaskWithNullTime_NoUniqueViolation() { // 使用精确 UUID var parentId = Guid.Parse("6afeea6f-296f-4da1-a1e1-279e818f932c"); var childId = Guid.Parse("412290d0-01b7-4348-af2b-a6abb55dd580"); // 先创建一个已存在的父任务(模拟前置同步已完成) var existingParent = new TaskEntity { Id = parentId, UserId = _testUserId, Title = "测试", Priority = (TaskPriority)1, IsCompleted = false, Code = "1", ParentTaskId = null, CreationTime = DateTime.UtcNow, CreatorId = _testUserId, LastModificationTime = DateTime.UtcNow, LastModifierId = _testUserId }; _dbContext.Tasks.Add(existingParent); await _dbContext.SaveChangesAsync(); // 同步请求(lastModificationTime 为 null,应被 LWW 跳过) var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = parentId, Title = "测试", Priority = (TaskPriority)1, IsCompleted = false, Code = "1", ParentTaskId = null, LastModificationTime = null // 🔑 关键:null 时间戳 } } }; // 不应抛出 UNIQUE 约束 var response = await _service.SyncAsync(_testUserId, request, CancellationToken.None); Assert.Single(response.Tasks); } /// /// 验证跨 UserId 同步相同 Id 不再触发 UNIQUE 约束。 /// 任务已存在于 DB(通过其他 UserId 创建),用不同 UserId 同步相同 Id, /// 应被 ProcessUpsertAsync 的 DB 级别二次查重发现并安全跳过。 /// [Fact] public async Task SyncAsync_TaskExistsWithDifferentUserId_HandlesGracefully() { // 创建另一个用户的 DbContext var otherUserId = Guid.NewGuid(); _dbContext.Users.Add(new UserEntity { Id = otherUserId, UserName = "other_user", PasswordHash = "hash", PasswordSalt = "salt", Role = "User" }); await _dbContext.SaveChangesAsync(); // 使用报错 UUID 创建任务,但属于 otherUserId var parentId = Guid.Parse("6afeea6f-296f-4da1-a1e1-279e818f932c"); _dbContext.Tasks.Add(new TaskEntity { Id = parentId, UserId = otherUserId, Title = "other user task", Priority = (TaskPriority)1, CreationTime = DateTime.UtcNow, CreatorId = otherUserId, LastModificationTime = DateTime.UtcNow, LastModifierId = otherUserId }); await _dbContext.SaveChangesAsync(); // 使用独立 DbContext 同步相同 Id(模拟跨请求) var options = new DbContextOptionsBuilder() .UseSqlite(_connection) .Options; using var dbContextB = new TodoDbContext(options); var serviceB = new CloudTaskSyncService(dbContextB, NullLogger.Instance); var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = parentId, Title = "测试", Priority = (TaskPriority)1, IsCompleted = false, Code = "1", ParentTaskId = null, LastModificationTime = null } } }; // 应正常完成(不抛出 DbUpdateException),DB 级别二次查重发现已存在实体并跳过 // 注意:response.Tasks 为空,因为该任务属于 otherUserId,不属于 _testUserId var response = await serviceB.SyncAsync(_testUserId, request, CancellationToken.None); Assert.Empty(response.Tasks); } }