1561 lines
56 KiB
C#
1561 lines
56 KiB
C#
using FsCheck;
|
||
using FsCheck.Xunit;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
using MiAssessment.Core.Interfaces;
|
||
using MiAssessment.Core.Services;
|
||
using MiAssessment.Model.Data;
|
||
using MiAssessment.Model.Entities;
|
||
using MiAssessment.Model.Models.Order;
|
||
using Moq;
|
||
using Xunit;
|
||
|
||
namespace MiAssessment.Tests.Services;
|
||
|
||
/// <summary>
|
||
/// 小程序API OrderService 属性测试
|
||
/// 验证订单服务的分页查询一致性和用户数据隔离
|
||
/// </summary>
|
||
public class ApiOrderServicePropertyTests
|
||
{
|
||
private readonly Mock<ILogger<OrderService>> _mockLogger = new();
|
||
private readonly Mock<IWechatPayService> _mockWechatPayService = new();
|
||
|
||
#region Property 3: 分页查询一致性- Order List
|
||
|
||
/// <summary>
|
||
/// Property 3: 分页查询返回的记录数不超过pageSize
|
||
/// *For any* paginated query, the returned items count SHALL not exceed pageSize.
|
||
///
|
||
/// **Feature: miniapp-api, Property 3: 分页查询一致性*
|
||
/// **Validates: Requirements 7.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool OrderPaginationReturnsCorrectCount(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var pageSize = Math.Max(1, seed.Get % 20 + 1); // 1-20
|
||
|
||
// 创建多条订单记录(超过pageSize<7A>?
|
||
var orderCount = pageSize + 10;
|
||
for (int i = 0; i < orderCount; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2));
|
||
}
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act
|
||
var result = service.GetListAsync(userId, 1, pageSize, null).GetAwaiter().GetResult();
|
||
|
||
// Assert: 返回的记录数不超过pageSize
|
||
return result.List.Count <= pageSize;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Property 3: 分页查询的Total等于满足条件的总记录数
|
||
/// *For any* paginated query, Total SHALL equal the count of all matching records.
|
||
///
|
||
/// **Feature: miniapp-api, Property 3: 分页查询一致性*
|
||
/// **Validates: Requirements 7.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool OrderPaginationTotalEqualsMatchingRecordsCount(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var otherUserId = (long)(seed.Get + 10000);
|
||
|
||
// 为当前用户创建订单记录
|
||
var userOrderCount = Math.Max(1, seed.Get % 15 + 1); // 1-15
|
||
for (int i = 0; i < userOrderCount; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2));
|
||
}
|
||
|
||
// 为其他用户创建订单记录(不应计入Total<61>?
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + 100 + i, otherUserId, orderType: 1, status: 2));
|
||
}
|
||
|
||
// 创建已删除的记录(不应计入Total<61>?
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
var deletedOrder = CreateOrder(seed.Get + 200 + i, userId, orderType: 1, status: 2);
|
||
deletedOrder.IsDeleted = true;
|
||
dbContext.Orders.Add(deletedOrder);
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act
|
||
var result = service.GetListAsync(userId, 1, 20, null).GetAwaiter().GetResult();
|
||
|
||
// Assert: Total等于当前用户未删除的订单<E8AEA2>?
|
||
return result.Total == userOrderCount;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 3: 遍历所有页面能获取所有满足条件的记录
|
||
/// *For any* paginated query, traversing all pages SHALL return all matching records.
|
||
///
|
||
/// **Feature: miniapp-api, Property 3: 分页查询一致性*
|
||
/// **Validates: Requirements 7.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool OrderPaginationTraversalReturnsAllRecords(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var pageSize = Math.Max(1, seed.Get % 5 + 1); // 1-5 (小pageSize以测试多页
|
||
|
||
// 创建订单记录
|
||
var totalRecords = Math.Max(1, seed.Get % 12 + 1); // 1-12
|
||
var expectedIds = new HashSet<long>();
|
||
for (int i = 0; i < totalRecords; i++)
|
||
{
|
||
var order = CreateOrder(seed.Get + i, userId, orderType: 1, status: 2);
|
||
dbContext.Orders.Add(order);
|
||
expectedIds.Add(order.Id);
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act: 遍历所有页<E69C89>?
|
||
var allRetrievedIds = new HashSet<long>();
|
||
var page = 1;
|
||
var maxPages = (totalRecords / pageSize) + 2; // 防止无限循环
|
||
|
||
while (page <= maxPages)
|
||
{
|
||
var result = service.GetListAsync(userId, page, pageSize, null).GetAwaiter().GetResult();
|
||
if (result.List.Count == 0) break;
|
||
|
||
foreach (var item in result.List)
|
||
{
|
||
allRetrievedIds.Add(item.Id);
|
||
}
|
||
|
||
if (page >= result.TotalPages) break;
|
||
page++;
|
||
}
|
||
|
||
// Assert: 遍历所有页面后获取的记录ID集合应等于预期的ID集合
|
||
return allRetrievedIds.SetEquals(expectedIds);
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Property 3: 分页查询的TotalPages计算正确
|
||
/// *For any* paginated query, TotalPages SHALL equal ceil(Total / PageSize).
|
||
///
|
||
/// **Feature: miniapp-api, Property 3: 分页查询一致性*
|
||
/// **Validates: Requirements 7.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool OrderPaginationTotalPagesCalculatedCorrectly(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var pageSize = Math.Max(1, seed.Get % 20 + 1); // 1-20
|
||
|
||
// 创建订单记录
|
||
var totalRecords = Math.Max(1, seed.Get % 50 + 1); // 1-50
|
||
for (int i = 0; i < totalRecords; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act
|
||
var result = service.GetListAsync(userId, 1, pageSize, null).GetAwaiter().GetResult();
|
||
|
||
// Assert: TotalPages = ceil(Total / PageSize)
|
||
var expectedTotalPages = (int)Math.Ceiling((double)result.Total / pageSize);
|
||
return result.TotalPages == expectedTotalPages;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 3: 分页查询不同页面返回不重复的记录
|
||
/// *For any* paginated query, different pages SHALL return non-overlapping records.
|
||
///
|
||
/// **Feature: miniapp-api, Property 3: 分页查询一致性*
|
||
/// **Validates: Requirements 7.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool OrderPaginationPagesReturnNonOverlappingRecords(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var pageSize = 3; // 固定pageSize以确保多页
|
||
|
||
// 创建足够多的订单记录以产生多页
|
||
var totalRecords = 10;
|
||
for (int i = 0; i < totalRecords; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act: 获取前两<E5898D>?
|
||
var page1 = service.GetListAsync(userId, 1, pageSize, null).GetAwaiter().GetResult();
|
||
var page2 = service.GetListAsync(userId, 2, pageSize, null).GetAwaiter().GetResult();
|
||
|
||
// Assert: 两页的记录ID不重叠
|
||
var page1Ids = page1.List.Select(o => o.Id).ToHashSet();
|
||
var page2Ids = page2.List.Select(o => o.Id).ToHashSet();
|
||
|
||
return !page1Ids.Intersect(page2Ids).Any();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 3: 按订单类型筛选时分页查询一致性
|
||
/// *For any* paginated query with orderType filter, Total SHALL equal the count
|
||
/// of all matching records with that orderType.
|
||
///
|
||
/// **Feature: miniapp-api, Property 3: 分页查询一致性*
|
||
/// **Validates: Requirements 7.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool OrderPaginationWithTypeFilterReturnsCorrectTotal(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var filterType = (seed.Get % 2) + 1; // 1 or 2
|
||
|
||
// 创建测评订单(类型<E7B1BB>?
|
||
var type1Count = Math.Max(1, seed.Get % 10 + 1); // 1-10
|
||
for (int i = 0; i < type1Count; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2));
|
||
}
|
||
|
||
// 创建规划订单(类型<E7B1BB>?
|
||
var type2Count = Math.Max(1, (seed.Get + 5) % 10 + 1); // 1-10
|
||
for (int i = 0; i < type2Count; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + 100 + i, userId, orderType: 2, status: 2));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act
|
||
var result = service.GetListAsync(userId, 1, 20, filterType).GetAwaiter().GetResult();
|
||
|
||
// Assert: Total等于指定类型的订单数
|
||
var expectedCount = filterType == 1 ? type1Count : type2Count;
|
||
return result.Total == expectedCount;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Property 3: 按订单类型筛选时只返回匹配类型的记录
|
||
/// *For any* paginated query with orderType filter, all returned records SHALL
|
||
/// have the specified orderType.
|
||
///
|
||
/// **Feature: miniapp-api, Property 3: 分页查询一致性*
|
||
/// **Validates: Requirements 7.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool OrderPaginationWithTypeFilterReturnsOnlyMatchingType(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var filterType = (seed.Get % 2) + 1; // 1 or 2
|
||
|
||
// 创建测评订单(类型<E7B1BB>?
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2));
|
||
}
|
||
|
||
// 创建规划订单(类型<E7B1BB>?
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + 100 + i, userId, orderType: 2, status: 2));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act
|
||
var result = service.GetListAsync(userId, 1, 20, filterType).GetAwaiter().GetResult();
|
||
|
||
// Assert: 所有返回的记录都是指定类型
|
||
return result.List.All(o => o.OrderType == filterType);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Property 4: 用户数据隔离 - Order List
|
||
|
||
/// <summary>
|
||
/// Property 4: 订单列表只返回当前用户的订单
|
||
/// *For any* order list query, the returned data SHALL only belong to the current
|
||
/// logged-in user and SHALL NOT contain data from other users.
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 7.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool OrderListOnlyReturnsCurrentUserOrders(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var otherUserId = (long)(seed.Get + 10000);
|
||
|
||
// 为当前用户创建订单
|
||
var userOrderCount = Math.Max(1, seed.Get % 10 + 1); // 1-10
|
||
for (int i = 0; i < userOrderCount; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2));
|
||
}
|
||
|
||
// 为其他用户创建订单
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + 100 + i, otherUserId, orderType: 1, status: 2));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act: 当前用户查询订单列表
|
||
var result = service.GetListAsync(userId, 1, 100, null).GetAwaiter().GetResult();
|
||
|
||
// Assert: 返回的订单数等于当前用户的订单数,且不包含其他用户的订单
|
||
if (result.Total != userOrderCount) return false;
|
||
if (result.List.Count != userOrderCount) return false;
|
||
|
||
// 验证数据库中确实存在其他用户的订单
|
||
var otherUserOrders = dbContext.Orders.Where(o => o.UserId == otherUserId && !o.IsDeleted).ToList();
|
||
if (otherUserOrders.Count != 5) return false;
|
||
|
||
// 验证返回的订单中不包含其他用户的订单ID
|
||
var returnedIds = result.List.Select(o => o.Id).ToHashSet();
|
||
var otherUserIds = otherUserOrders.Select(o => o.Id).ToHashSet();
|
||
|
||
return !returnedIds.Intersect(otherUserIds).Any();
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Property 4: 多用户场景下订单列表数据隔离正确
|
||
/// *For any* multi-user scenario, each user SHALL only see their own orders,
|
||
/// and the total count of accessible orders SHALL match their own order count.
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 7.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool MultiUserOrderListDataIsolationIsCorrect(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var user1Id = (long)seed.Get;
|
||
var user2Id = (long)(seed.Get + 10000);
|
||
var user3Id = (long)(seed.Get + 20000);
|
||
|
||
// 为每个用户创建不同数量的订单
|
||
var user1OrderCount = Math.Max(1, seed.Get % 5 + 1); // 1-5
|
||
var user2OrderCount = Math.Max(1, (seed.Get + 1) % 5 + 1); // 1-5
|
||
var user3OrderCount = Math.Max(1, (seed.Get + 2) % 5 + 1); // 1-5
|
||
|
||
// 创建用户1的订单
|
||
for (int i = 0; i < user1OrderCount; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + i, user1Id, orderType: 1, status: 2));
|
||
}
|
||
|
||
// 创建用户2的订单
|
||
for (int i = 0; i < user2OrderCount; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + 1000 + i, user2Id, orderType: 1, status: 2));
|
||
}
|
||
|
||
// 创建用户3的订单
|
||
for (int i = 0; i < user3OrderCount; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + 2000 + i, user3Id, orderType: 1, status: 2));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act & Assert: 验证每个用户只能看到自己的订单
|
||
var user1Result = service.GetListAsync(user1Id, 1, 100, null).GetAwaiter().GetResult();
|
||
var user2Result = service.GetListAsync(user2Id, 1, 100, null).GetAwaiter().GetResult();
|
||
var user3Result = service.GetListAsync(user3Id, 1, 100, null).GetAwaiter().GetResult();
|
||
|
||
// 验证每个用户的订单数量正确
|
||
if (user1Result.Total != user1OrderCount) return false;
|
||
if (user2Result.Total != user2OrderCount) return false;
|
||
if (user3Result.Total != user3OrderCount) return false;
|
||
|
||
// 验证订单ID不重叠
|
||
var user1Ids = user1Result.List.Select(o => o.Id).ToHashSet();
|
||
var user2Ids = user2Result.List.Select(o => o.Id).ToHashSet();
|
||
var user3Ids = user3Result.List.Select(o => o.Id).ToHashSet();
|
||
|
||
if (user1Ids.Intersect(user2Ids).Any()) return false;
|
||
if (user1Ids.Intersect(user3Ids).Any()) return false;
|
||
if (user2Ids.Intersect(user3Ids).Any()) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Property 4: 用户数据隔离 - Order Detail
|
||
|
||
/// <summary>
|
||
/// Property 4: 用户只能获取自己的订单详情
|
||
/// *For any* GetDetail request, the returned data SHALL only belong to the current
|
||
/// logged-in user. Requests for other users' orders SHALL return null.
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 7.2, 7.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool OrderDetailOnlyReturnsOwnOrders(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var otherUserId = (long)(seed.Get + 10000);
|
||
var orderId = (long)seed.Get;
|
||
|
||
// 创建当前用户的订单
|
||
var order = CreateOrder(orderId, userId, orderType: 1, status: 2);
|
||
dbContext.Orders.Add(order);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act: 当前用户获取自己的订单
|
||
var ownResult = service.GetDetailAsync(userId, orderId).GetAwaiter().GetResult();
|
||
|
||
// Act: 其他用户尝试获取该订单
|
||
var otherResult = service.GetDetailAsync(otherUserId, orderId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 当前用户可以获取,其他用户不能获取
|
||
return ownResult != null &&
|
||
ownResult.Id == orderId &&
|
||
otherResult == null;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Property 4: 用户无法获取其他用户的订单详情
|
||
/// *For any* GetDetail request with an orderId belonging to another user,
|
||
/// the result SHALL be null (no data returned).
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 7.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool OrderDetailReturnsNullForOtherUserOrders(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var otherUserId = (long)(seed.Get + 10000);
|
||
var orderId = (long)seed.Get;
|
||
|
||
// 创建其他用户的订单
|
||
var order = CreateOrder(orderId, otherUserId, orderType: 1, status: 2);
|
||
dbContext.Orders.Add(order);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act: 当前用户尝试获取其他用户的订单
|
||
var result = service.GetDetailAsync(userId, orderId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 应该返回null
|
||
return result == null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4: 多用户场景下订单详情数据隔离正确
|
||
/// *For any* multi-user scenario, each user SHALL only access their own order details,
|
||
/// and attempts to access other users' orders SHALL return null.
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 7.2, 7.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool MultiUserOrderDetailDataIsolationIsCorrect(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var user1Id = (long)seed.Get;
|
||
var user2Id = (long)(seed.Get + 10000);
|
||
var user3Id = (long)(seed.Get + 20000);
|
||
|
||
// 为每个用户创建订单
|
||
var user1OrderIds = new List<long>();
|
||
var user2OrderIds = new List<long>();
|
||
var user3OrderIds = new List<long>();
|
||
|
||
// 创建用户1的订单
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
var orderId = seed.Get + i;
|
||
dbContext.Orders.Add(CreateOrder(orderId, user1Id, orderType: 1, status: 2));
|
||
user1OrderIds.Add(orderId);
|
||
}
|
||
|
||
// 创建用户2的订单
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
var orderId = seed.Get + 1000 + i;
|
||
dbContext.Orders.Add(CreateOrder(orderId, user2Id, orderType: 1, status: 2));
|
||
user2OrderIds.Add(orderId);
|
||
}
|
||
|
||
// 创建用户3的订单
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
var orderId = seed.Get + 2000 + i;
|
||
dbContext.Orders.Add(CreateOrder(orderId, user3Id, orderType: 1, status: 2));
|
||
user3OrderIds.Add(orderId);
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act & Assert: 验证每个用户只能访问自己的订单详情
|
||
// 用户1可以访问自己的订单
|
||
foreach (var orderId in user1OrderIds)
|
||
{
|
||
var result = service.GetDetailAsync(user1Id, orderId).GetAwaiter().GetResult();
|
||
if (result == null || result.Id != orderId) return false;
|
||
}
|
||
|
||
// 用户1不能访问用户2的订单
|
||
foreach (var orderId in user2OrderIds)
|
||
{
|
||
var result = service.GetDetailAsync(user1Id, orderId).GetAwaiter().GetResult();
|
||
if (result != null) return false;
|
||
}
|
||
|
||
// 用户1不能访问用户3的订单
|
||
foreach (var orderId in user3OrderIds)
|
||
{
|
||
var result = service.GetDetailAsync(user1Id, orderId).GetAwaiter().GetResult();
|
||
if (result != null) return false;
|
||
}
|
||
|
||
// 用户2可以访问自己的订单
|
||
foreach (var orderId in user2OrderIds)
|
||
{
|
||
var result = service.GetDetailAsync(user2Id, orderId).GetAwaiter().GetResult();
|
||
if (result == null || result.Id != orderId) return false;
|
||
}
|
||
|
||
// 用户2不能访问用户1的订单
|
||
foreach (var orderId in user1OrderIds)
|
||
{
|
||
var result = service.GetDetailAsync(user2Id, orderId).GetAwaiter().GetResult();
|
||
if (result != null) return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
|
||
#endregion
|
||
|
||
#region Property 4: 用户数据隔离 - Pay Result
|
||
|
||
/// <summary>
|
||
/// Property 4: 用户只能查询自己订单的支付结<E4BB98>?
|
||
/// *For any* GetPayResult request, the returned data SHALL only belong to the current
|
||
/// logged-in user. Requests for other users' orders SHALL return null.
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 7.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool PayResultOnlyReturnsOwnOrders(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var otherUserId = (long)(seed.Get + 10000);
|
||
var orderId = (long)seed.Get;
|
||
|
||
// 创建当前用户的订单
|
||
var order = CreateOrder(orderId, userId, orderType: 1, status: 2);
|
||
dbContext.Orders.Add(order);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act: 当前用户查询自己订单的支付结<E4BB98>?
|
||
var ownResult = service.GetPayResultAsync(userId, orderId).GetAwaiter().GetResult();
|
||
|
||
// Act: 其他用户尝试查询该订单的支付结果
|
||
var otherResult = service.GetPayResultAsync(otherUserId, orderId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 当前用户可以查询,其他用户不能查<E883BD>?
|
||
return ownResult != null && otherResult == null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4: 用户无法查询其他用户订单的支付结<E4BB98>?
|
||
/// *For any* GetPayResult request with an orderId belonging to another user,
|
||
/// the result SHALL be null (no data returned).
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 7.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool PayResultReturnsNullForOtherUserOrders(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var otherUserId = (long)(seed.Get + 10000);
|
||
var orderId = (long)seed.Get;
|
||
|
||
// 创建其他用户的订单
|
||
var order = CreateOrder(orderId, otherUserId, orderType: 1, status: 2);
|
||
dbContext.Orders.Add(order);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act: 当前用户尝试查询其他用户订单的支付结<E4BB98>?
|
||
var result = service.GetPayResultAsync(userId, orderId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 应该返回null
|
||
return result == null;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 边界条件测试
|
||
|
||
/// <summary>
|
||
/// Property 3: 空数据库返回空分页结<E9A1B5>?
|
||
///
|
||
/// **Feature: miniapp-api, Property 3: 分页查询一致性*
|
||
/// **Validates: Requirements 7.1**
|
||
/// </summary>
|
||
[Fact]
|
||
public void EmptyDatabaseReturnsEmptyOrderPagedResult()
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act
|
||
var result = service.GetListAsync(1, 1, 20, null).GetAwaiter().GetResult();
|
||
|
||
// Assert
|
||
Assert.Empty(result.List);
|
||
Assert.Equal(0, result.Total);
|
||
Assert.Equal(0, result.TotalPages);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 3: 分页参数边界值处<E580BC>?
|
||
///
|
||
/// **Feature: miniapp-api, Property 3: 分页查询一致性*
|
||
/// **Validates: Requirements 7.1**
|
||
/// </summary>
|
||
[Theory]
|
||
[InlineData(0, 20)] // page < 1
|
||
[InlineData(-1, 20)] // page < 1
|
||
[InlineData(1, 0)] // pageSize < 1
|
||
[InlineData(1, -1)] // pageSize < 1
|
||
[InlineData(1, 200)] // pageSize > 100
|
||
public void OrderPaginationHandlesBoundaryValues(int page, int pageSize)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = 1L;
|
||
|
||
// 创建订单记录
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(i + 1, userId, orderType: 1, status: 2));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act
|
||
var result = service.GetListAsync(userId, page, pageSize, null).GetAwaiter().GetResult();
|
||
|
||
// Assert: 服务应该处理边界值,不抛出异<E587BA>?
|
||
Assert.NotNull(result);
|
||
Assert.True(result.Page >= 1);
|
||
Assert.True(result.PageSize >= 1 && result.PageSize <= 100);
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Property 4: 不存在的订单ID返回null
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 7.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool OrderDetailReturnsNullForNonExistentOrders(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var nonExistentOrderId = (long)(seed.Get + 999999);
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act: 获取不存在的订单
|
||
var result = service.GetDetailAsync(userId, nonExistentOrderId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 应该返回null
|
||
return result == null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4: 已删除的订单返回null
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 7.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool OrderDetailReturnsNullForDeletedOrders(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
|
||
// 创建已删除的订单
|
||
var order = CreateOrder(orderId, userId, orderType: 1, status: 2);
|
||
order.IsDeleted = true;
|
||
dbContext.Orders.Add(order);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act: 获取已删除的订单
|
||
var result = service.GetDetailAsync(userId, orderId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 应该返回null
|
||
return result == null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4: 已删除的订单不出现在列表<E58897>?
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 7.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool OrderListExcludesDeletedOrders(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
|
||
// 创建正常订单
|
||
var normalOrderCount = Math.Max(1, seed.Get % 5 + 1); // 1-5
|
||
for (int i = 0; i < normalOrderCount; i++)
|
||
{
|
||
dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2));
|
||
}
|
||
|
||
// 创建已删除的订单
|
||
var deletedOrderCount = Math.Max(1, (seed.Get + 3) % 5 + 1); // 1-5
|
||
var deletedOrderIds = new HashSet<long>();
|
||
for (int i = 0; i < deletedOrderCount; i++)
|
||
{
|
||
var orderId = seed.Get + 100 + i;
|
||
var order = CreateOrder(orderId, userId, orderType: 1, status: 2);
|
||
order.IsDeleted = true;
|
||
dbContext.Orders.Add(order);
|
||
deletedOrderIds.Add(orderId);
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// Act
|
||
var result = service.GetListAsync(userId, 1, 100, null).GetAwaiter().GetResult();
|
||
|
||
// Assert: 返回的订单数等于正常订单数,且不包含已删除的订单
|
||
if (result.Total != normalOrderCount) return false;
|
||
|
||
var returnedIds = result.List.Select(o => o.Id).ToHashSet();
|
||
return !returnedIds.Intersect(deletedOrderIds).Any();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Property 5: 订单创建完整<EFBFBD>?
|
||
|
||
/// <summary>
|
||
/// Property 5: 测评订单创建后同时存在订单记录和测评记录
|
||
/// *For any* assessment order creation request, after successful creation there SHALL exist
|
||
/// both an order record and an assessment record, and the assessment record's OrderId
|
||
/// SHALL point to the newly created order.
|
||
///
|
||
/// **Feature: miniapp-api, Property 5: 订单创建完整<E5AE8C>?*
|
||
/// **Validates: Requirements 8.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool AssessmentOrderCreationCreatesOrderAndAssessmentRecord(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var productId = (long)(seed.Get % 100 + 1);
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(productId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
var request = new CreateOrderRequest
|
||
{
|
||
OrderType = 1, // 测评订单
|
||
ProductId = productId,
|
||
AssessmentInfo = CreateAssessmentInfo(seed.Get)
|
||
};
|
||
|
||
// Act
|
||
var result = service.CreateAsync(userId, request).GetAwaiter().GetResult();
|
||
|
||
// Assert: 验证订单记录存在
|
||
var order = dbContext.Orders.FirstOrDefault(o => o.Id == result.OrderId);
|
||
if (order == null) return false;
|
||
|
||
// Assert: 验证测评记录存在
|
||
var assessmentRecord = dbContext.AssessmentRecords.FirstOrDefault(r => r.OrderId == result.OrderId);
|
||
if (assessmentRecord == null) return false;
|
||
|
||
// Assert: 验证测评记录的OrderId指向新创建的订单
|
||
if (assessmentRecord.OrderId != order.Id) return false;
|
||
|
||
// Assert: 验证返回的AssessmentRecordId正确
|
||
if (result.AssessmentRecordId != assessmentRecord.Id) return false;
|
||
|
||
// Assert: 验证订单和测评记录的用户ID一<44>?
|
||
if (order.UserId != userId || assessmentRecord.UserId != userId) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 5: 规划订单创建后同时存在订单记录和规划预约记录
|
||
/// *For any* planner order creation request, after successful creation there SHALL exist
|
||
/// both an order record and a planner booking record, and the booking record's OrderId
|
||
/// SHALL point to the newly created order.
|
||
///
|
||
/// **Feature: miniapp-api, Property 5: 订单创建完整<E5AE8C>?*
|
||
/// **Validates: Requirements 8.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool PlannerOrderCreationCreatesOrderAndBookingRecord(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var productId = (long)(seed.Get % 100 + 1);
|
||
|
||
// 创建规划<E8A784>?
|
||
var planner = CreatePlanner(productId);
|
||
dbContext.Planners.Add(planner);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
var request = new CreateOrderRequest
|
||
{
|
||
OrderType = 2, // 规划订单
|
||
ProductId = productId,
|
||
PlannerInfo = CreatePlannerInfo(seed.Get)
|
||
};
|
||
|
||
// Act
|
||
var result = service.CreateAsync(userId, request).GetAwaiter().GetResult();
|
||
|
||
// Assert: 验证订单记录存在
|
||
var order = dbContext.Orders.FirstOrDefault(o => o.Id == result.OrderId);
|
||
if (order == null) return false;
|
||
|
||
// Assert: 验证规划预约记录存在
|
||
var plannerBooking = dbContext.PlannerBookings.FirstOrDefault(b => b.OrderId == result.OrderId);
|
||
if (plannerBooking == null) return false;
|
||
|
||
// Assert: 验证规划预约记录的OrderId指向新创建的订单
|
||
if (plannerBooking.OrderId != order.Id) return false;
|
||
|
||
// Assert: 验证订单和规划预约记录的用户ID一<44>?
|
||
if (order.UserId != userId || plannerBooking.UserId != userId) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 5: 使用邀请码创建订单时金额设<E9A29D>?并标记邀请码已使<E5B7B2>?
|
||
/// *For any* order creation with invite code, the order amount SHALL be set to 0
|
||
/// and the invite code SHALL be marked as used.
|
||
///
|
||
/// **Feature: miniapp-api, Property 5: 订单创建完整<E5AE8C>?*
|
||
/// **Validates: Requirements 8.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool OrderCreationWithInviteCodeSetsAmountToZeroAndMarksCodeUsed(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var productId = (long)(seed.Get % 100 + 1);
|
||
var inviteCodeId = (long)(seed.Get + 1000);
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(productId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建已分配的邀请码(状<EFBC88>?=已分配)
|
||
var inviteCode = CreateInviteCode(inviteCodeId, status: 2);
|
||
dbContext.InviteCodes.Add(inviteCode);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
var request = new CreateOrderRequest
|
||
{
|
||
OrderType = 1,
|
||
ProductId = productId,
|
||
AssessmentInfo = CreateAssessmentInfo(seed.Get),
|
||
InviteCodeId = inviteCodeId
|
||
};
|
||
|
||
// Act
|
||
var result = service.CreateAsync(userId, request).GetAwaiter().GetResult();
|
||
|
||
// Assert: 验证支付金额<E98791>?
|
||
if (result.PayAmount != 0) return false;
|
||
|
||
// Assert: 验证不需要支<E8A681>?
|
||
if (result.NeedPay) return false;
|
||
|
||
// Assert: 验证订单记录的PayAmount<6E>?
|
||
var order = dbContext.Orders.FirstOrDefault(o => o.Id == result.OrderId);
|
||
if (order == null || order.PayAmount != 0) return false;
|
||
|
||
// Assert: 验证邀请码状态变为已使用<E4BDBF>?<3F>?
|
||
var updatedInviteCode = dbContext.InviteCodes.FirstOrDefault(ic => ic.Id == inviteCodeId);
|
||
if (updatedInviteCode == null || updatedInviteCode.Status != 3) return false;
|
||
|
||
// Assert: 验证邀请码记录了使用者和订单信息
|
||
if (updatedInviteCode.UseUserId != userId) return false;
|
||
if (updatedInviteCode.UseOrderId != result.OrderId) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 5: 订单创建后关联记录的数据完整<E5AE8C>?
|
||
/// *For any* order creation, the associated record (assessment or booking) SHALL contain
|
||
/// all the information provided in the request.
|
||
///
|
||
/// **Feature: miniapp-api, Property 5: 订单创建完整<E5AE8C>?*
|
||
/// **Validates: Requirements 8.1, 8.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool OrderCreationPreservesAllRequestData(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var productId = (long)(seed.Get % 100 + 1);
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(productId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
var assessmentInfo = CreateAssessmentInfo(seed.Get);
|
||
var request = new CreateOrderRequest
|
||
{
|
||
OrderType = 1,
|
||
ProductId = productId,
|
||
AssessmentInfo = assessmentInfo
|
||
};
|
||
|
||
// Act
|
||
var result = service.CreateAsync(userId, request).GetAwaiter().GetResult();
|
||
|
||
// Assert: 验证测评记录包含所有请求数<E6B182>?
|
||
var assessmentRecord = dbContext.AssessmentRecords.FirstOrDefault(r => r.OrderId == result.OrderId);
|
||
if (assessmentRecord == null) return false;
|
||
|
||
// 验证所有字<E69C89>?
|
||
if (assessmentRecord.Name != assessmentInfo.Name) return false;
|
||
if (assessmentRecord.Phone != assessmentInfo.Phone) return false;
|
||
if (assessmentRecord.Gender != assessmentInfo.Gender) return false;
|
||
if (assessmentRecord.Age != assessmentInfo.Age) return false;
|
||
if (assessmentRecord.EducationStage != assessmentInfo.EducationStage) return false;
|
||
if (assessmentRecord.Province != assessmentInfo.Province) return false;
|
||
if (assessmentRecord.City != assessmentInfo.City) return false;
|
||
if (assessmentRecord.District != assessmentInfo.District) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Property 10: 订单事务回滚
|
||
|
||
/// <summary>
|
||
/// Property 10: 无效的测评类型ID导致订单创建失败时无残留数据
|
||
/// *For any* order creation with invalid product ID, there SHALL be no partially created
|
||
/// order or associated records in the database.
|
||
///
|
||
/// **Feature: miniapp-api, Property 10: 订单事务回滚**
|
||
/// **Validates: Requirements 8.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool InvalidProductIdCausesNoPartialData(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var invalidProductId = (long)(seed.Get + 999999); // 不存在的产品ID
|
||
|
||
// 记录创建前的记录<E8AEB0>?
|
||
var orderCountBefore = dbContext.Orders.Count();
|
||
var assessmentRecordCountBefore = dbContext.AssessmentRecords.Count();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
var request = new CreateOrderRequest
|
||
{
|
||
OrderType = 1,
|
||
ProductId = invalidProductId,
|
||
AssessmentInfo = CreateAssessmentInfo(seed.Get)
|
||
};
|
||
|
||
// Act & Assert
|
||
try
|
||
{
|
||
service.CreateAsync(userId, request).GetAwaiter().GetResult();
|
||
// 如果没有抛出异常,测试失<E8AF95>?
|
||
return false;
|
||
}
|
||
catch (ArgumentException)
|
||
{
|
||
// 预期的异<E79A84>?
|
||
}
|
||
|
||
// Assert: 验证没有创建任何订单记录
|
||
var orderCountAfter = dbContext.Orders.Count();
|
||
if (orderCountAfter != orderCountBefore) return false;
|
||
|
||
// Assert: 验证没有创建任何测评记录
|
||
var assessmentRecordCountAfter = dbContext.AssessmentRecords.Count();
|
||
if (assessmentRecordCountAfter != assessmentRecordCountBefore) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 10: 无效的规划师ID导致订单创建失败时无残留数据
|
||
/// *For any* planner order creation with invalid planner ID, there SHALL be no partially
|
||
/// created order or booking records in the database.
|
||
///
|
||
/// **Feature: miniapp-api, Property 10: 订单事务回滚**
|
||
/// **Validates: Requirements 8.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool InvalidPlannerIdCausesNoPartialData(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var invalidPlannerId = (long)(seed.Get + 999999); // 不存在的规划师ID
|
||
|
||
// 记录创建前的记录<E8AEB0>?
|
||
var orderCountBefore = dbContext.Orders.Count();
|
||
var bookingCountBefore = dbContext.PlannerBookings.Count();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
var request = new CreateOrderRequest
|
||
{
|
||
OrderType = 2,
|
||
ProductId = invalidPlannerId,
|
||
PlannerInfo = CreatePlannerInfo(seed.Get)
|
||
};
|
||
|
||
// Act & Assert
|
||
try
|
||
{
|
||
service.CreateAsync(userId, request).GetAwaiter().GetResult();
|
||
// 如果没有抛出异常,测试失<E8AF95>?
|
||
return false;
|
||
}
|
||
catch (ArgumentException)
|
||
{
|
||
// 预期的异<E79A84>?
|
||
}
|
||
|
||
// Assert: 验证没有创建任何订单记录
|
||
var orderCountAfter = dbContext.Orders.Count();
|
||
if (orderCountAfter != orderCountBefore) return false;
|
||
|
||
// Assert: 验证没有创建任何规划预约记录
|
||
var bookingCountAfter = dbContext.PlannerBookings.Count();
|
||
if (bookingCountAfter != bookingCountBefore) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 10: 已使用的邀请码导致订单创建失败时无残留数据
|
||
/// *For any* order creation with already used invite code, there SHALL be no partially
|
||
/// created order or associated records in the database.
|
||
///
|
||
/// **Feature: miniapp-api, Property 10: 订单事务回滚**
|
||
/// **Validates: Requirements 8.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool UsedInviteCodeCausesNoPartialData(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var productId = (long)(seed.Get % 100 + 1);
|
||
var inviteCodeId = (long)(seed.Get + 1000);
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(productId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建已使用的邀请码(状<EFBC88>?=已使用)
|
||
var inviteCode = CreateInviteCode(inviteCodeId, status: 3);
|
||
dbContext.InviteCodes.Add(inviteCode);
|
||
dbContext.SaveChanges();
|
||
|
||
// 记录创建前的记录<E8AEB0>?
|
||
var orderCountBefore = dbContext.Orders.Count();
|
||
var assessmentRecordCountBefore = dbContext.AssessmentRecords.Count();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
var request = new CreateOrderRequest
|
||
{
|
||
OrderType = 1,
|
||
ProductId = productId,
|
||
AssessmentInfo = CreateAssessmentInfo(seed.Get),
|
||
InviteCodeId = inviteCodeId
|
||
};
|
||
|
||
// Act & Assert
|
||
try
|
||
{
|
||
service.CreateAsync(userId, request).GetAwaiter().GetResult();
|
||
// 如果没有抛出异常,测试失<E8AF95>?
|
||
return false;
|
||
}
|
||
catch (ArgumentException)
|
||
{
|
||
// 预期的异<E79A84>?
|
||
}
|
||
|
||
// Assert: 验证没有创建任何订单记录
|
||
var orderCountAfter = dbContext.Orders.Count();
|
||
if (orderCountAfter != orderCountBefore) return false;
|
||
|
||
// Assert: 验证没有创建任何测评记录
|
||
var assessmentRecordCountAfter = dbContext.AssessmentRecords.Count();
|
||
if (assessmentRecordCountAfter != assessmentRecordCountBefore) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 10: 不存在的邀请码ID导致订单创建失败时无残留数据
|
||
/// *For any* order creation with non-existent invite code ID, there SHALL be no partially
|
||
/// created order or associated records in the database.
|
||
///
|
||
/// **Feature: miniapp-api, Property 10: 订单事务回滚**
|
||
/// **Validates: Requirements 8.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool NonExistentInviteCodeCausesNoPartialData(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var productId = (long)(seed.Get % 100 + 1);
|
||
var nonExistentInviteCodeId = (long)(seed.Get + 999999);
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(productId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
dbContext.SaveChanges();
|
||
|
||
// 记录创建前的记录<E8AEB0>?
|
||
var orderCountBefore = dbContext.Orders.Count();
|
||
var assessmentRecordCountBefore = dbContext.AssessmentRecords.Count();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
var request = new CreateOrderRequest
|
||
{
|
||
OrderType = 1,
|
||
ProductId = productId,
|
||
AssessmentInfo = CreateAssessmentInfo(seed.Get),
|
||
InviteCodeId = nonExistentInviteCodeId
|
||
};
|
||
|
||
// Act & Assert
|
||
try
|
||
{
|
||
service.CreateAsync(userId, request).GetAwaiter().GetResult();
|
||
// 如果没有抛出异常,测试失<E8AF95>?
|
||
return false;
|
||
}
|
||
catch (ArgumentException)
|
||
{
|
||
// 预期的异<E79A84>?
|
||
}
|
||
|
||
// Assert: 验证没有创建任何订单记录
|
||
var orderCountAfter = dbContext.Orders.Count();
|
||
if (orderCountAfter != orderCountBefore) return false;
|
||
|
||
// Assert: 验证没有创建任何测评记录
|
||
var assessmentRecordCountAfter = dbContext.AssessmentRecords.Count();
|
||
if (assessmentRecordCountAfter != assessmentRecordCountBefore) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 10: 缺少必要信息导致订单创建失败时无残留数据
|
||
/// *For any* order creation with missing required info (e.g., assessment info for assessment order),
|
||
/// there SHALL be no partially created order or associated records in the database.
|
||
///
|
||
/// **Feature: miniapp-api, Property 10: 订单事务回滚**
|
||
/// **Validates: Requirements 8.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool MissingRequiredInfoCausesNoPartialData(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var productId = (long)(seed.Get % 100 + 1);
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(productId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
dbContext.SaveChanges();
|
||
|
||
// 记录创建前的记录<E8AEB0>?
|
||
var orderCountBefore = dbContext.Orders.Count();
|
||
var assessmentRecordCountBefore = dbContext.AssessmentRecords.Count();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
// 测评订单但不提供AssessmentInfo
|
||
var request = new CreateOrderRequest
|
||
{
|
||
OrderType = 1,
|
||
ProductId = productId,
|
||
AssessmentInfo = null // 缺少必要信息
|
||
};
|
||
|
||
// Act & Assert
|
||
try
|
||
{
|
||
service.CreateAsync(userId, request).GetAwaiter().GetResult();
|
||
// 如果没有抛出异常,测试失<E8AF95>?
|
||
return false;
|
||
}
|
||
catch (ArgumentException)
|
||
{
|
||
// 预期的异<E79A84>?
|
||
}
|
||
|
||
// Assert: 验证没有创建任何订单记录
|
||
var orderCountAfter = dbContext.Orders.Count();
|
||
if (orderCountAfter != orderCountBefore) return false;
|
||
|
||
// Assert: 验证没有创建任何测评记录
|
||
var assessmentRecordCountAfter = dbContext.AssessmentRecords.Count();
|
||
if (assessmentRecordCountAfter != assessmentRecordCountBefore) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 10: 无效的订单类型导致订单创建失败时无残留数<E79599>?
|
||
/// *For any* order creation with invalid order type, there SHALL be no partially
|
||
/// created order or associated records in the database.
|
||
///
|
||
/// **Feature: miniapp-api, Property 10: 订单事务回滚**
|
||
/// **Validates: Requirements 8.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool InvalidOrderTypeCausesNoPartialData(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var productId = (long)(seed.Get % 100 + 1);
|
||
var invalidOrderType = (seed.Get % 10) + 3; // 3-12,都是无效的订单类型
|
||
|
||
// 创建测评类型(以防万一<E4B887>?
|
||
var assessmentType = CreateAssessmentType(productId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
dbContext.SaveChanges();
|
||
|
||
// 记录创建前的记录<E8AEB0>?
|
||
var orderCountBefore = dbContext.Orders.Count();
|
||
var assessmentRecordCountBefore = dbContext.AssessmentRecords.Count();
|
||
var bookingCountBefore = dbContext.PlannerBookings.Count();
|
||
|
||
var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object);
|
||
|
||
var request = new CreateOrderRequest
|
||
{
|
||
OrderType = invalidOrderType,
|
||
ProductId = productId,
|
||
AssessmentInfo = CreateAssessmentInfo(seed.Get)
|
||
};
|
||
|
||
// Act & Assert
|
||
try
|
||
{
|
||
service.CreateAsync(userId, request).GetAwaiter().GetResult();
|
||
// 如果没有抛出异常,测试失<E8AF95>?
|
||
return false;
|
||
}
|
||
catch (ArgumentException)
|
||
{
|
||
// 预期的异<E79A84>?
|
||
}
|
||
|
||
// Assert: 验证没有创建任何订单记录
|
||
var orderCountAfter = dbContext.Orders.Count();
|
||
if (orderCountAfter != orderCountBefore) return false;
|
||
|
||
// Assert: 验证没有创建任何测评记录
|
||
var assessmentRecordCountAfter = dbContext.AssessmentRecords.Count();
|
||
if (assessmentRecordCountAfter != assessmentRecordCountBefore) return false;
|
||
|
||
// Assert: 验证没有创建任何规划预约记录
|
||
var bookingCountAfter = dbContext.PlannerBookings.Count();
|
||
if (bookingCountAfter != bookingCountBefore) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 辅助方法
|
||
|
||
/// <summary>
|
||
/// 创建测试用内存数据库上下<E4B88A>?
|
||
/// </summary>
|
||
private TestOrderDbContext CreateTestDbContext()
|
||
{
|
||
var options = new DbContextOptionsBuilder<TestOrderDbContext>()
|
||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||
.Options;
|
||
|
||
return new TestOrderDbContext(options);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试订单
|
||
/// </summary>
|
||
private Order CreateOrder(long id, long userId, int orderType, int status)
|
||
{
|
||
return new Order
|
||
{
|
||
Id = id,
|
||
OrderNo = $"ORD{id:D10}",
|
||
UserId = userId,
|
||
OrderType = orderType,
|
||
ProductId = 1,
|
||
ProductName = orderType == 1 ? "多元智能测评" : "学业规划服务",
|
||
Amount = 99.00m,
|
||
PayAmount = 99.00m,
|
||
Status = status,
|
||
PayTime = status >= 2 ? DateTime.Now.AddMinutes(-10) : null,
|
||
CreateTime = DateTime.Now.AddMinutes(-id), // 不同的创建时间以测试排序
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试用测评类型
|
||
/// </summary>
|
||
private AssessmentType CreateAssessmentType(long id)
|
||
{
|
||
return new AssessmentType
|
||
{
|
||
Id = id,
|
||
Name = $"测评类型{id}",
|
||
Code = $"TYPE{id}",
|
||
Price = 99.00m,
|
||
Status = 1, // 已上<E5B7B2>?
|
||
QuestionCount = 80,
|
||
Sort = 1,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试用规划师
|
||
/// </summary>
|
||
private Planner CreatePlanner(long id)
|
||
{
|
||
return new Planner
|
||
{
|
||
Id = id,
|
||
Name = $"规划师{id}",
|
||
Avatar = $"https://example.com/avatar{id}.jpg",
|
||
Introduction = $"规划师{id}的简介",
|
||
Price = 199.00m,
|
||
Status = 1, // 启用
|
||
Sort = 1,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试用邀请码
|
||
/// </summary>
|
||
private InviteCode CreateInviteCode(long id, int status)
|
||
{
|
||
return new InviteCode
|
||
{
|
||
Id = id,
|
||
Code = $"CODE{id % 100000:D5}",
|
||
Status = status,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试用测评信息
|
||
/// </summary>
|
||
private AssessmentInfoDto CreateAssessmentInfo(int seed)
|
||
{
|
||
return new AssessmentInfoDto
|
||
{
|
||
Name = $"测试用户{seed}",
|
||
Phone = $"138{seed % 100000000:D8}",
|
||
Gender = (seed % 2) + 1, // 1或2
|
||
Age = (seed % 50) + 6, // 6-55岁
|
||
EducationStage = (seed % 6) + 1, // 1-6
|
||
Province = "北京市",
|
||
City = "北京市",
|
||
District = "海淀区"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试用规划预约信息
|
||
/// </summary>
|
||
private PlannerInfoDto CreatePlannerInfo(int seed)
|
||
{
|
||
return new PlannerInfoDto
|
||
{
|
||
Name = $"预约用户{seed}",
|
||
Phone = $"139{seed % 100000000:D8}",
|
||
Remark = $"备注{seed}"
|
||
};
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 测试用DbContext,继承自MiAssessmentDbContext但忽略外键关系验证
|
||
/// </summary>
|
||
public class TestOrderDbContext : MiAssessmentDbContext
|
||
{
|
||
public TestOrderDbContext(DbContextOptions<TestOrderDbContext> options)
|
||
: base(CreateBaseOptions(options))
|
||
{
|
||
}
|
||
|
||
private static DbContextOptions<MiAssessmentDbContext> CreateBaseOptions(DbContextOptions<TestOrderDbContext> options)
|
||
{
|
||
var builder = new DbContextOptionsBuilder<MiAssessmentDbContext>();
|
||
builder.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||
.ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning));
|
||
return builder.Options;
|
||
}
|
||
|
||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||
{
|
||
// 不调用基类的OnModelCreating,避免外键关系验证
|
||
// 只配置必要的表映<E8A1A8>?
|
||
modelBuilder.Entity<Order>().ToTable("orders");
|
||
modelBuilder.Entity<AssessmentRecord>().ToTable("assessment_records");
|
||
modelBuilder.Entity<AssessmentType>().ToTable("assessment_types");
|
||
modelBuilder.Entity<Planner>().ToTable("planners");
|
||
modelBuilder.Entity<PlannerBooking>().ToTable("planner_bookings");
|
||
modelBuilder.Entity<InviteCode>().ToTable("invite_codes");
|
||
|
||
// 忽略导航属<E888AA>?
|
||
modelBuilder.Entity<AssessmentRecord>().Ignore(e => e.AssessmentType);
|
||
}
|
||
}
|