using Hua.Todo.Application.Repositories; using Hua.Todo.Application.Services.CloudSync.Models; using Hua.Todo.Application.Services.CloudSync.Services; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging.Abstractions; using Hua.Todo.Core.Entities; using Xunit; namespace Hua.Todo.Host.Tests; /// /// CloudTaskSyncService 单元测试。 /// public class CloudTaskSyncServiceTests : IDisposable { private readonly TodoDbContext _dbContext; private readonly CloudTaskSyncService _service; private readonly Guid _testUserId = Guid.NewGuid(); public CloudTaskSyncServiceTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)) .Options; _dbContext = new TodoDbContext(options); _service = new CloudTaskSyncService(_dbContext, NullLogger.Instance); } public void Dispose() { _dbContext.Dispose(); } /// /// 测试创建新任务。 /// [Fact] public async Task SyncAsync_NewTask_CreatesTask() { // Arrange var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = Guid.NewGuid(), Title = "Test Task", Priority = TaskPriority.High, IsCompleted = false, LastModificationTime = DateTime.UtcNow } } }; // Act var response = await _service.SyncAsync(_testUserId, request, CancellationToken.None); // Assert Assert.Single(response.Tasks); var task = response.Tasks.First(); Assert.Equal("Test Task", task.Title); Assert.Equal(TaskPriority.High, task.Priority); Assert.False(task.IsCompleted); Assert.NotEqual(Guid.Empty, task.Id); } /// /// 测试更新已有任务。 /// [Fact] public async Task SyncAsync_ExistingTask_UpdatesTask() { // Arrange var taskId = Guid.NewGuid(); var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "Updated Task", Priority = TaskPriority.Low, IsCompleted = true, LastModificationTime = DateTime.UtcNow } } }; // Act await _service.SyncAsync(_testUserId, request, CancellationToken.None); var response = await _service.GetTasksAsync(_testUserId, CancellationToken.None); // Assert Assert.Single(response); var task = response.First(); Assert.Equal(taskId, task.Id); Assert.Equal("Updated Task", task.Title); Assert.Equal(TaskPriority.Low, task.Priority); Assert.True(task.IsCompleted); } /// /// 测试删除任务(Tombstone)。 /// [Fact] public async Task SyncAsync_DeleteTask_MarksAsDeleted() { // Arrange var taskId = Guid.NewGuid(); var createRequest = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "Task to Delete", LastModificationTime = DateTime.UtcNow } } }; var createResponse = await _service.SyncAsync(_testUserId, createRequest, CancellationToken.None); Assert.Single(createResponse.Tasks); var deleteRequest = new SyncRequest { Deletes = new List { taskId } }; // Act var deleteResponse = await _service.SyncAsync(_testUserId, deleteRequest, CancellationToken.None); // Assert - 直接检查返回的任务 var deletedTask = deleteResponse.Tasks.FirstOrDefault(t => t.Id == taskId); Assert.NotNull(deletedTask); Assert.True(deletedTask.IsDeleted); Assert.NotNull(deletedTask.DeletionTime); } /// /// 测试 LWW 冲突解决:客户端更新时接受较新版本。 /// [Fact] public async Task SyncAsync_LWW_ClientNewer_AcceptsClientVersion() { // Arrange var taskId = Guid.NewGuid(); var olderTime = DateTime.UtcNow.AddMinutes(-10); var newerTime = DateTime.UtcNow; // 先创建旧版本 var createRequest = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "Original", LastModificationTime = olderTime } } }; await _service.SyncAsync(_testUserId, createRequest, CancellationToken.None); // 再提交新版本 var updateRequest = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "Updated by Client", LastModificationTime = newerTime } } }; // Act await _service.SyncAsync(_testUserId, updateRequest, CancellationToken.None); var response = await _service.GetTasksAsync(_testUserId, CancellationToken.None); // Assert Assert.Single(response); var task = response.First(); Assert.Equal("Updated by Client", task.Title); } /// /// 测试 LWW 冲突解决:服务端更新时拒绝旧版本。 /// [Fact] public async Task SyncAsync_LWW_ServerNewer_KeepsServerVersion() { // Arrange var taskId = Guid.NewGuid(); var serverTime = DateTime.UtcNow; var clientOldTime = DateTime.UtcNow.AddMinutes(-10); // 先创建服务端版本 var createRequest = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "Server Version", LastModificationTime = serverTime } } }; await _service.SyncAsync(_testUserId, createRequest, CancellationToken.None); // 再提交客户端旧版本 var updateRequest = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "Client Old Version", LastModificationTime = clientOldTime } } }; // Act await _service.SyncAsync(_testUserId, updateRequest, CancellationToken.None); var response = await _service.GetTasksAsync(_testUserId, CancellationToken.None); // Assert Assert.Single(response); var task = response.First(); Assert.Equal("Server Version", task.Title); } /// /// 测试获取任务全量(含 Tombstone)。 /// [Fact] public async Task GetTasksAsync_ReturnsAllTasksIncludingDeleted() { // Arrange var taskId = Guid.NewGuid(); var createRequest = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "Task", LastModificationTime = DateTime.UtcNow } } }; await _service.SyncAsync(_testUserId, createRequest, CancellationToken.None); var deleteRequest = new SyncRequest { Deletes = new List { taskId } }; await _service.SyncAsync(_testUserId, deleteRequest, CancellationToken.None); // Act var response = await _service.GetTasksAsync(_testUserId, CancellationToken.None); // Assert Assert.Single(response); // GetTasksAsync returns all including deleted Assert.True(response.First().IsDeleted); } /// /// 测试父子任务关系。 /// [Fact] public async Task SyncAsync_ParentChildTask_CreatesRelationship() { // Arrange var parentId = Guid.NewGuid(); var childId = Guid.NewGuid(); var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = parentId, Title = "Parent Task", LastModificationTime = DateTime.UtcNow }, new CloudTaskUpsert { Id = childId, Title = "Child Task", ParentTaskId = parentId, LastModificationTime = DateTime.UtcNow } } }; // Act await _service.SyncAsync(_testUserId, request, CancellationToken.None); var response = await _service.GetTasksAsync(_testUserId, CancellationToken.None); // Assert Assert.Equal(2, response.Count); var child = response.First(t => t.Title == "Child Task"); Assert.Equal(parentId, child.ParentTaskId); } /// /// 测试软删除时递归删除子任务。 /// [Fact] public async Task SyncAsync_DeleteParentTask_RecursiveDeletesChildren() { // Arrange var parentId = Guid.NewGuid(); var childId = Guid.NewGuid(); var createRequest = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = parentId, Title = "Parent", LastModificationTime = DateTime.UtcNow }, new CloudTaskUpsert { Id = childId, Title = "Child", ParentTaskId = parentId, LastModificationTime = DateTime.UtcNow } } }; await _service.SyncAsync(_testUserId, createRequest, CancellationToken.None); var deleteRequest = new SyncRequest { Deletes = new List { parentId } }; // Act await _service.SyncAsync(_testUserId, deleteRequest, CancellationToken.None); var response = await _service.GetTasksAsync(_testUserId, CancellationToken.None); // Assert Assert.Equal(2, response.Count); Assert.All(response, t => Assert.True(t.IsDeleted)); } /// /// 测试 Code 字段在创建和返回时正确映射。 /// [Fact] public async Task SyncAsync_Code_IsStoredAndReturned() { // Arrange var taskId = Guid.NewGuid(); var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "Task with Code", Code = "5", LastModificationTime = DateTime.UtcNow } } }; // Act var response = await _service.SyncAsync(_testUserId, request, CancellationToken.None); // Assert Assert.Single(response.Tasks); Assert.Equal("5", response.Tasks.First().Code); } /// /// 测试 Code 字段在更新时正确覆盖。 /// [Fact] public async Task SyncAsync_Code_IsUpdated() { // Arrange var taskId = Guid.NewGuid(); var createRequest = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "Original", Code = "1", LastModificationTime = DateTime.UtcNow } } }; await _service.SyncAsync(_testUserId, createRequest, CancellationToken.None); var updateRequest = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "Updated", Code = "99", LastModificationTime = DateTime.UtcNow.AddMinutes(1) } } }; // Act var response = await _service.SyncAsync(_testUserId, updateRequest, CancellationToken.None); // Assert Assert.Single(response.Tasks); Assert.Equal("99", response.Tasks.First().Code); } /// /// 测试新任务 Code 为空字符串时的处理。 /// [Fact] public async Task SyncAsync_Code_EmptyString_StoredAsEmpty() { // Arrange var taskId = Guid.NewGuid(); var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "No Code", Code = string.Empty, LastModificationTime = DateTime.UtcNow } } }; // Act var response = await _service.SyncAsync(_testUserId, request, CancellationToken.None); // Assert Assert.Single(response.Tasks); Assert.Equal(string.Empty, response.Tasks.First().Code); } /// /// 测试同一批次中包含子任务引用新创建的父任务(父子任务 Id 均已知)。 /// 验证第二遍处理子任务时能正确找到已创建父任务的 Id。 /// [Fact] public async Task SyncAsync_ParentChild_SameBatch_ChildFindsNewParent() { // Arrange var parentId = Guid.NewGuid(); var childId = Guid.NewGuid(); var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = parentId, Title = "Parent", LastModificationTime = DateTime.UtcNow }, new CloudTaskUpsert { Id = childId, Title = "Child", ParentTaskId = parentId, LastModificationTime = DateTime.UtcNow } } }; // Act await _service.SyncAsync(_testUserId, request, CancellationToken.None); var response = await _service.GetTasksAsync(_testUserId, CancellationToken.None); // Assert Assert.Equal(2, response.Count); var child = response.First(t => t.Title == "Child"); Assert.Equal(parentId, child.ParentTaskId); var parent = response.First(t => t.Title == "Parent"); Assert.Null(parent.ParentTaskId); } /// /// 测试去重:同一批次中相同 Id 出现多次,只保留最后一条。 /// [Fact] public async Task SyncAsync_DuplicateId_LastWins() { // Arrange var taskId = Guid.NewGuid(); var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "First Version", Code = "1", LastModificationTime = DateTime.UtcNow.AddMinutes(-5) }, new CloudTaskUpsert { Id = taskId, Title = "Last Version", Code = "2", LastModificationTime = DateTime.UtcNow } } }; // Act var response = await _service.SyncAsync(_testUserId, request, CancellationToken.None); // Assert Assert.Single(response.Tasks); Assert.Equal("Last Version", response.Tasks.First().Title); Assert.Equal("2", response.Tasks.First().Code); } /// /// 测试更新已有任务时不会触发 UNIQUE 约束冲突。 /// [Fact] public async Task SyncAsync_UpdateExistingTask_NoUniqueConstraintViolation() { // Arrange - 先创建一个任务 var taskId = Guid.NewGuid(); var createRequest = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "Original", Priority = TaskPriority.High, IsCompleted = false, LastModificationTime = DateTime.UtcNow.AddMinutes(-5) } } }; await _service.SyncAsync(_testUserId, createRequest, CancellationToken.None); // Act - 用相同的 Id 再次同步(更新),模拟客户端重发 var updateRequest = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = taskId, Title = "Updated", Priority = TaskPriority.Low, IsCompleted = true, LastModificationTime = DateTime.UtcNow // 较新时间 } } }; // Assert - 不应抛出 DbUpdateException var response = await _service.SyncAsync(_testUserId, updateRequest, CancellationToken.None); Assert.Single(response.Tasks); Assert.Equal("Updated", response.Tasks.First().Title); } /// /// 测试同一批次中父子任务(均有 Id),先处理父再处理子时无 UNIQUE 冲突。 /// [Fact] public async Task SyncAsync_ParentChildSameBatch_NoUniqueConstraintViolation() { // Arrange - 父子任务均在同一批次中,模拟客户端新建父子任务后同步 var parentId = Guid.NewGuid(); var childId = Guid.NewGuid(); var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = parentId, Title = "Parent Task", Priority = TaskPriority.High, IsCompleted = false, Code = "1", ParentTaskId = null, LastModificationTime = null // 与实际错误场景一致 }, new CloudTaskUpsert { Id = childId, Title = "Child Task", Priority = TaskPriority.Medium, IsCompleted = false, Code = "2", ParentTaskId = parentId, LastModificationTime = null } } }; // Act & Assert - 不应抛出 UNIQUE constraint failed var response = await _service.SyncAsync(_testUserId, request, CancellationToken.None); Assert.Equal(2, response.Tasks.Count); } /// /// 测试 Title 为空或仅空白的 upsert 被过滤掉。 /// [Fact] public async Task SyncAsync_EmptyTitle_FilteredOut() { // Arrange var request = new SyncRequest { Upserts = new List { new CloudTaskUpsert { Id = Guid.NewGuid(), Title = " ", LastModificationTime = DateTime.UtcNow }, new CloudTaskUpsert { Id = Guid.NewGuid(), Title = "", LastModificationTime = DateTime.UtcNow }, new CloudTaskUpsert { Id = Guid.NewGuid(), Title = "Valid Task", LastModificationTime = DateTime.UtcNow } } }; // Act var response = await _service.SyncAsync(_testUserId, request, CancellationToken.None); // Assert Assert.Single(response.Tasks); Assert.Equal("Valid Task", response.Tasks.First().Title); } }