feat: 完成云同步、语音控制与多平台扩展基础架构搭建
本次提交完成了项目核心基础架构升级: 1. 新增动态API中间件与权限控制系统,支持匿名/鉴权接口分离 2. 搭建云同步服务体系,包含认证、任务同步、安全策略等核心模块 3. 实现语音控制全链路,从STT/意图解析到命令执行 4. 新增任务类型、附件实体与相关仓储接口 5. 重构前端配置与代理规则,统一后端端口为5057 6. 新增多平台测试项目与CI脚本优化 7. 完善项目文档与代码注释规范 移除了旧版迁移文件与冗余代理配置,调整项目结构适配跨平台部署需求。
This commit is contained in:
@@ -0,0 +1,679 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// CloudTaskSyncService 单元测试。
|
||||
/// </summary>
|
||||
public class CloudTaskSyncServiceTests : IDisposable
|
||||
{
|
||||
private readonly TodoDbContext _dbContext;
|
||||
private readonly CloudTaskSyncService _service;
|
||||
private readonly Guid _testUserId = Guid.NewGuid();
|
||||
|
||||
public CloudTaskSyncServiceTests()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<TodoDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning))
|
||||
.Options;
|
||||
|
||||
_dbContext = new TodoDbContext(options);
|
||||
_service = new CloudTaskSyncService(_dbContext, NullLogger<CloudTaskSyncService>.Instance);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_dbContext.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试创建新任务。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SyncAsync_NewTask_CreatesTask()
|
||||
{
|
||||
// Arrange
|
||||
var request = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试更新已有任务。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SyncAsync_ExistingTask_UpdatesTask()
|
||||
{
|
||||
// Arrange
|
||||
var taskId = Guid.NewGuid();
|
||||
var request = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试删除任务(Tombstone)。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SyncAsync_DeleteTask_MarksAsDeleted()
|
||||
{
|
||||
// Arrange
|
||||
var taskId = Guid.NewGuid();
|
||||
var createRequest = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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<Guid> { 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 LWW 冲突解决:客户端更新时接受较新版本。
|
||||
/// </summary>
|
||||
[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<CloudTaskUpsert>
|
||||
{
|
||||
new CloudTaskUpsert
|
||||
{
|
||||
Id = taskId,
|
||||
Title = "Original",
|
||||
LastModificationTime = olderTime
|
||||
}
|
||||
}
|
||||
};
|
||||
await _service.SyncAsync(_testUserId, createRequest, CancellationToken.None);
|
||||
|
||||
// 再提交新版本
|
||||
var updateRequest = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 LWW 冲突解决:服务端更新时拒绝旧版本。
|
||||
/// </summary>
|
||||
[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<CloudTaskUpsert>
|
||||
{
|
||||
new CloudTaskUpsert
|
||||
{
|
||||
Id = taskId,
|
||||
Title = "Server Version",
|
||||
LastModificationTime = serverTime
|
||||
}
|
||||
}
|
||||
};
|
||||
await _service.SyncAsync(_testUserId, createRequest, CancellationToken.None);
|
||||
|
||||
// 再提交客户端旧版本
|
||||
var updateRequest = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试获取任务全量(含 Tombstone)。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetTasksAsync_ReturnsAllTasksIncludingDeleted()
|
||||
{
|
||||
// Arrange
|
||||
var taskId = Guid.NewGuid();
|
||||
var createRequest = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
new CloudTaskUpsert
|
||||
{
|
||||
Id = taskId,
|
||||
Title = "Task",
|
||||
LastModificationTime = DateTime.UtcNow
|
||||
}
|
||||
}
|
||||
};
|
||||
await _service.SyncAsync(_testUserId, createRequest, CancellationToken.None);
|
||||
|
||||
var deleteRequest = new SyncRequest
|
||||
{
|
||||
Deletes = new List<Guid> { 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试父子任务关系。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SyncAsync_ParentChildTask_CreatesRelationship()
|
||||
{
|
||||
// Arrange
|
||||
var parentId = Guid.NewGuid();
|
||||
var childId = Guid.NewGuid();
|
||||
|
||||
var request = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试软删除时递归删除子任务。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SyncAsync_DeleteParentTask_RecursiveDeletesChildren()
|
||||
{
|
||||
// Arrange
|
||||
var parentId = Guid.NewGuid();
|
||||
var childId = Guid.NewGuid();
|
||||
|
||||
var createRequest = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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<Guid> { 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 Code 字段在创建和返回时正确映射。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SyncAsync_Code_IsStoredAndReturned()
|
||||
{
|
||||
// Arrange
|
||||
var taskId = Guid.NewGuid();
|
||||
var request = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 Code 字段在更新时正确覆盖。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SyncAsync_Code_IsUpdated()
|
||||
{
|
||||
// Arrange
|
||||
var taskId = Guid.NewGuid();
|
||||
var createRequest = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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<CloudTaskUpsert>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试新任务 Code 为空字符串时的处理。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SyncAsync_Code_EmptyString_StoredAsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var taskId = Guid.NewGuid();
|
||||
var request = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试同一批次中包含子任务引用新创建的父任务(父子任务 Id 均已知)。
|
||||
/// 验证第二遍处理子任务时能正确找到已创建父任务的 Id。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SyncAsync_ParentChild_SameBatch_ChildFindsNewParent()
|
||||
{
|
||||
// Arrange
|
||||
var parentId = Guid.NewGuid();
|
||||
var childId = Guid.NewGuid();
|
||||
|
||||
var request = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试去重:同一批次中相同 Id 出现多次,只保留最后一条。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SyncAsync_DuplicateId_LastWins()
|
||||
{
|
||||
// Arrange
|
||||
var taskId = Guid.NewGuid();
|
||||
var request = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试更新已有任务时不会触发 UNIQUE 约束冲突。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SyncAsync_UpdateExistingTask_NoUniqueConstraintViolation()
|
||||
{
|
||||
// Arrange - 先创建一个任务
|
||||
var taskId = Guid.NewGuid();
|
||||
var createRequest = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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<CloudTaskUpsert>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试同一批次中父子任务(均有 Id),先处理父再处理子时无 UNIQUE 冲突。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SyncAsync_ParentChildSameBatch_NoUniqueConstraintViolation()
|
||||
{
|
||||
// Arrange - 父子任务均在同一批次中,模拟客户端新建父子任务后同步
|
||||
var parentId = Guid.NewGuid();
|
||||
var childId = Guid.NewGuid();
|
||||
|
||||
var request = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 Title 为空或仅空白的 upsert 被过滤掉。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SyncAsync_EmptyTitle_FilteredOut()
|
||||
{
|
||||
// Arrange
|
||||
var request = new SyncRequest
|
||||
{
|
||||
Upserts = new List<CloudTaskUpsert>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user