2078 lines
76 KiB
C#
2078 lines
76 KiB
C#
using FsCheck;
|
||
using FsCheck.Xunit;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
using MiAssessment.Core.Services;
|
||
using MiAssessment.Model.Data;
|
||
using MiAssessment.Model.Entities;
|
||
using Moq;
|
||
using Xunit;
|
||
|
||
namespace MiAssessment.Tests.Services;
|
||
|
||
/// <summary>
|
||
/// AssessmentService 属性测试
|
||
/// 验证测评服务的列表查询排序正确性和分页查询一致性
|
||
/// </summary>
|
||
public class AssessmentServicePropertyTests
|
||
{
|
||
private readonly Mock<ILogger<AssessmentService>> _mockLogger = new();
|
||
|
||
#region Property 2: 列表查询排序正确性 - Question
|
||
|
||
/// <summary>
|
||
/// Property 2: 题目列表按QuestionNo升序排列
|
||
/// *For any* Question list query, the returned items SHALL be ordered by QuestionNo
|
||
/// field in ascending order.
|
||
///
|
||
/// **Feature: miniapp-api, Property 2: 列表查询排序正确性**
|
||
/// **Validates: Requirements 3.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool QuestionListSortedByQuestionNoAscending(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var random = new Random(seed.Get);
|
||
var typeId = (long)seed.Get;
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建具有不同QuestionNo值的题目(随机顺序插入)
|
||
var questionNos = new List<int>();
|
||
for (int i = 0; i < 10; i++)
|
||
{
|
||
var questionNo = random.Next(1, 1000);
|
||
questionNos.Add(questionNo);
|
||
dbContext.Questions.Add(CreateQuestion(seed.Get + i, typeId, questionNo, status: 1, isDeleted: false));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.GetQuestionListAsync(typeId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 验证列表按QuestionNo升序排列
|
||
if (result.Count < 2) return true;
|
||
|
||
for (int i = 0; i < result.Count - 1; i++)
|
||
{
|
||
if (result[i].QuestionNo > result[i + 1].QuestionNo)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 2: 题目列表只返回启用状态的记录
|
||
/// *For any* Question list query, the returned items SHALL only include records
|
||
/// with Status=1 and IsDeleted=false.
|
||
///
|
||
/// **Feature: miniapp-api, Property 2: 列表查询排序正确性**
|
||
/// **Validates: Requirements 3.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool QuestionListOnlyReturnsEnabledRecords(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var typeId = (long)seed.Get;
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建启用的题目
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
dbContext.Questions.Add(CreateQuestion(seed.Get + i, typeId, i + 1, status: 1, isDeleted: false));
|
||
}
|
||
|
||
// 创建禁用的题目
|
||
for (int i = 0; i < 2; i++)
|
||
{
|
||
dbContext.Questions.Add(CreateQuestion(seed.Get + 100 + i, typeId, 100 + i, status: 0, isDeleted: false));
|
||
}
|
||
|
||
// 创建已删除的题目
|
||
for (int i = 0; i < 2; i++)
|
||
{
|
||
dbContext.Questions.Add(CreateQuestion(seed.Get + 200 + i, typeId, 200 + i, status: 1, isDeleted: true));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.GetQuestionListAsync(typeId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 验证返回结果只包含Status=1且未删除的记录
|
||
// 1. 返回的记录数应该等于启用且未删除的记录数
|
||
if (result.Count != 3) return false;
|
||
|
||
// 2. 验证数据库中确实存在被过滤掉的记录
|
||
var allQuestions = dbContext.Questions.Where(q => q.AssessmentTypeId == typeId).ToList();
|
||
var disabledQuestions = allQuestions.Where(q => q.Status == 0 && !q.IsDeleted).ToList();
|
||
var deletedQuestions = allQuestions.Where(q => q.IsDeleted).ToList();
|
||
|
||
if (disabledQuestions.Count != 2 || deletedQuestions.Count != 2) return false;
|
||
|
||
// 3. 验证返回的记录中不包含禁用或已删除的记录
|
||
var returnedIds = result.Select(q => q.Id).ToHashSet();
|
||
var disabledIds = disabledQuestions.Select(q => q.Id).ToHashSet();
|
||
var deletedIds = deletedQuestions.Select(q => q.Id).ToHashSet();
|
||
|
||
return !returnedIds.Intersect(disabledIds).Any() && !returnedIds.Intersect(deletedIds).Any();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 2: 题目列表只返回指定测评类型的记录
|
||
/// *For any* Question list query with typeId, the returned items SHALL only include
|
||
/// records with matching AssessmentTypeId.
|
||
///
|
||
/// **Feature: miniapp-api, Property 2: 列表查询排序正确性**
|
||
/// **Validates: Requirements 3.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool QuestionListOnlyReturnsMatchingTypeRecords(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var typeId1 = (long)seed.Get;
|
||
var typeId2 = (long)(seed.Get + 1000);
|
||
|
||
// 创建两个测评类型
|
||
dbContext.AssessmentTypes.Add(CreateAssessmentType(typeId1));
|
||
dbContext.AssessmentTypes.Add(CreateAssessmentType(typeId2));
|
||
|
||
// 为类型1创建题目
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
dbContext.Questions.Add(CreateQuestion(seed.Get + i, typeId1, i + 1, status: 1, isDeleted: false));
|
||
}
|
||
|
||
// 为类型2创建题目
|
||
for (int i = 0; i < 2; i++)
|
||
{
|
||
dbContext.Questions.Add(CreateQuestion(seed.Get + 100 + i, typeId2, i + 1, status: 1, isDeleted: false));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.GetQuestionListAsync(typeId1).GetAwaiter().GetResult();
|
||
|
||
// Assert: 只返回类型1的题目
|
||
if (result.Count != 3) return false;
|
||
|
||
// 验证所有返回的题目都属于类型1
|
||
var returnedIds = result.Select(q => q.Id).ToHashSet();
|
||
var type1Questions = dbContext.Questions.Where(q => q.AssessmentTypeId == typeId1 && q.Status == 1 && !q.IsDeleted).ToList();
|
||
var type1Ids = type1Questions.Select(q => q.Id).ToHashSet();
|
||
|
||
return returnedIds.SetEquals(type1Ids);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Property 3: 分页查询一致性 - AssessmentHistory
|
||
|
||
/// <summary>
|
||
/// Property 3: 分页查询返回的记录数不超过pageSize
|
||
/// *For any* paginated query, the returned items count SHALL not exceed pageSize.
|
||
///
|
||
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
|
||
/// **Validates: Requirements 6.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool PaginationReturnsCorrectCount(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var pageSize = Math.Max(1, seed.Get % 20 + 1); // 1-20
|
||
|
||
// 创建测评类型
|
||
var typeId = (long)seed.Get;
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建多条测评记录(超过pageSize)
|
||
var recordCount = pageSize + 10;
|
||
for (int i = 0; i < recordCount; i++)
|
||
{
|
||
var order = CreateOrder(seed.Get + i, userId);
|
||
dbContext.Orders.Add(order);
|
||
dbContext.AssessmentRecords.Add(CreateAssessmentRecord(seed.Get + i, userId, order.Id, typeId));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.GetHistoryListAsync(userId, 1, pageSize).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 6.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool PaginationTotalEqualsMatchingRecordsCount(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var otherUserId = (long)(seed.Get + 10000);
|
||
|
||
// 创建测评类型
|
||
var typeId = (long)seed.Get;
|
||
dbContext.AssessmentTypes.Add(CreateAssessmentType(typeId));
|
||
|
||
// 为当前用户创建测评记录
|
||
var userRecordCount = Math.Max(1, seed.Get % 15 + 1); // 1-15
|
||
for (int i = 0; i < userRecordCount; i++)
|
||
{
|
||
var order = CreateOrder(seed.Get + i, userId);
|
||
dbContext.Orders.Add(order);
|
||
dbContext.AssessmentRecords.Add(CreateAssessmentRecord(seed.Get + i, userId, order.Id, typeId));
|
||
}
|
||
|
||
// 为其他用户创建测评记录(不应计入Total)
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
var order = CreateOrder(seed.Get + 100 + i, otherUserId);
|
||
dbContext.Orders.Add(order);
|
||
dbContext.AssessmentRecords.Add(CreateAssessmentRecord(seed.Get + 100 + i, otherUserId, order.Id, typeId));
|
||
}
|
||
|
||
// 创建已删除的记录(不应计入Total)
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
var order = CreateOrder(seed.Get + 200 + i, userId);
|
||
dbContext.Orders.Add(order);
|
||
var deletedRecord = CreateAssessmentRecord(seed.Get + 200 + i, userId, order.Id, typeId);
|
||
deletedRecord.IsDeleted = true;
|
||
dbContext.AssessmentRecords.Add(deletedRecord);
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.GetHistoryListAsync(userId, 1, 20).GetAwaiter().GetResult();
|
||
|
||
// Assert: Total等于当前用户未删除的记录数
|
||
return result.Total == userRecordCount;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 3: 遍历所有页面能获取所有满足条件的记录
|
||
/// *For any* paginated query, traversing all pages SHALL return all matching records.
|
||
///
|
||
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
|
||
/// **Validates: Requirements 6.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool PaginationTraversalReturnsAllRecords(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 typeId = (long)seed.Get;
|
||
dbContext.AssessmentTypes.Add(CreateAssessmentType(typeId));
|
||
|
||
// 创建测评记录
|
||
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);
|
||
dbContext.Orders.Add(order);
|
||
var record = CreateAssessmentRecord(seed.Get + i, userId, order.Id, typeId);
|
||
dbContext.AssessmentRecords.Add(record);
|
||
expectedIds.Add(record.Id);
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 遍历所有页面
|
||
var allRetrievedIds = new HashSet<long>();
|
||
var page = 1;
|
||
var maxPages = (totalRecords / pageSize) + 2; // 防止无限循环
|
||
|
||
while (page <= maxPages)
|
||
{
|
||
var result = service.GetHistoryListAsync(userId, page, pageSize).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 6.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool PaginationTotalPagesCalculatedCorrectly(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var pageSize = Math.Max(1, seed.Get % 20 + 1); // 1-20
|
||
|
||
// 创建测评类型
|
||
var typeId = (long)seed.Get;
|
||
dbContext.AssessmentTypes.Add(CreateAssessmentType(typeId));
|
||
|
||
// 创建测评记录
|
||
var totalRecords = Math.Max(1, seed.Get % 50 + 1); // 1-50
|
||
for (int i = 0; i < totalRecords; i++)
|
||
{
|
||
var order = CreateOrder(seed.Get + i, userId);
|
||
dbContext.Orders.Add(order);
|
||
dbContext.AssessmentRecords.Add(CreateAssessmentRecord(seed.Get + i, userId, order.Id, typeId));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.GetHistoryListAsync(userId, 1, pageSize).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 6.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool PaginationPagesReturnNonOverlappingRecords(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var pageSize = 3; // 固定pageSize以确保多页
|
||
|
||
// 创建测评类型
|
||
var typeId = (long)seed.Get;
|
||
dbContext.AssessmentTypes.Add(CreateAssessmentType(typeId));
|
||
|
||
// 创建足够多的测评记录以产生多页
|
||
var totalRecords = 10;
|
||
for (int i = 0; i < totalRecords; i++)
|
||
{
|
||
var order = CreateOrder(seed.Get + i, userId);
|
||
dbContext.Orders.Add(order);
|
||
dbContext.AssessmentRecords.Add(CreateAssessmentRecord(seed.Get + i, userId, order.Id, typeId));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 获取前两页
|
||
var page1 = service.GetHistoryListAsync(userId, 1, pageSize).GetAwaiter().GetResult();
|
||
var page2 = service.GetHistoryListAsync(userId, 2, pageSize).GetAwaiter().GetResult();
|
||
|
||
// Assert: 两页的记录ID不重叠
|
||
var page1Ids = page1.List.Select(r => r.Id).ToHashSet();
|
||
var page2Ids = page2.List.Select(r => r.Id).ToHashSet();
|
||
|
||
return !page1Ids.Intersect(page2Ids).Any();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Property 6: 邀请码使用一次性
|
||
|
||
/// <summary>
|
||
/// Property 6: 已分配的邀请码验证通过
|
||
/// *For any* assigned invite code (Status=2), the verification SHALL return IsValid=true
|
||
/// and the correct InviteCodeId.
|
||
///
|
||
/// **Feature: miniapp-api, Property 6: 邀请码使用一次性**
|
||
/// **Validates: Requirements 5.1, 5.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool AssignedInviteCodeVerificationReturnsValid(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var inviteCode = CreateInviteCode(seed.Get, status: 2); // 已分配状态
|
||
dbContext.InviteCodes.Add(inviteCode);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.VerifyInviteCodeAsync(inviteCode.Code).GetAwaiter().GetResult();
|
||
|
||
// Assert: 已分配的邀请码验证应该通过
|
||
return result.IsValid &&
|
||
result.InviteCodeId == inviteCode.Id &&
|
||
string.IsNullOrEmpty(result.ErrorMessage);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 6: 已使用的邀请码验证失败
|
||
/// *For any* used invite code (Status=3), the verification SHALL return IsValid=false
|
||
/// and ErrorMessage="邀请码已被使用".
|
||
///
|
||
/// **Feature: miniapp-api, Property 6: 邀请码使用一次性**
|
||
/// **Validates: Requirements 5.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool UsedInviteCodeVerificationReturnsAlreadyUsedError(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var inviteCode = CreateInviteCode(seed.Get, status: 3); // 已使用状态
|
||
inviteCode.UseUserId = seed.Get + 1000;
|
||
inviteCode.UseOrderId = seed.Get + 2000;
|
||
inviteCode.UseTime = DateTime.Now.AddDays(-1);
|
||
dbContext.InviteCodes.Add(inviteCode);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.VerifyInviteCodeAsync(inviteCode.Code).GetAwaiter().GetResult();
|
||
|
||
// Assert: 已使用的邀请码验证应该失败,返回"邀请码已被使用"
|
||
return !result.IsValid &&
|
||
result.ErrorMessage == "邀请码已被使用" &&
|
||
result.InviteCodeId == null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 6: 未分配的邀请码验证失败
|
||
/// *For any* unassigned invite code (Status=1), the verification SHALL return IsValid=false
|
||
/// and ErrorMessage="邀请码有误,请重新输入".
|
||
///
|
||
/// **Feature: miniapp-api, Property 6: 邀请码使用一次性**
|
||
/// **Validates: Requirements 5.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool UnassignedInviteCodeVerificationReturnsInvalidError(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var inviteCode = CreateInviteCode(seed.Get, status: 1); // 未分配状态
|
||
dbContext.InviteCodes.Add(inviteCode);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.VerifyInviteCodeAsync(inviteCode.Code).GetAwaiter().GetResult();
|
||
|
||
// Assert: 未分配的邀请码验证应该失败,返回"邀请码有误,请重新输入"
|
||
return !result.IsValid &&
|
||
result.ErrorMessage == "邀请码有误,请重新输入" &&
|
||
result.InviteCodeId == null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 6: 不存在的邀请码验证失败
|
||
/// *For any* non-existent invite code, the verification SHALL return IsValid=false
|
||
/// and ErrorMessage="邀请码有误,请重新输入".
|
||
///
|
||
/// **Feature: miniapp-api, Property 6: 邀请码使用一次性**
|
||
/// **Validates: Requirements 5.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool NonExistentInviteCodeVerificationReturnsInvalidError(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
// 不添加任何邀请码到数据库
|
||
var nonExistentCode = GenerateRandomInviteCode(seed.Get);
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.VerifyInviteCodeAsync(nonExistentCode).GetAwaiter().GetResult();
|
||
|
||
// Assert: 不存在的邀请码验证应该失败
|
||
return !result.IsValid &&
|
||
result.ErrorMessage == "邀请码有误,请重新输入" &&
|
||
result.InviteCodeId == null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 6: 邀请码验证不区分大小写
|
||
/// *For any* valid invite code, verification with different case variations SHALL return
|
||
/// the same result.
|
||
///
|
||
/// **Feature: miniapp-api, Property 6: 邀请码使用一次性**
|
||
/// **Validates: Requirements 5.1, 5.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool InviteCodeVerificationIsCaseInsensitive(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var inviteCode = CreateInviteCode(seed.Get, status: 2); // 已分配状态
|
||
dbContext.InviteCodes.Add(inviteCode);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 使用小写验证
|
||
var lowerCaseResult = service.VerifyInviteCodeAsync(inviteCode.Code.ToLower()).GetAwaiter().GetResult();
|
||
// Act: 使用大写验证
|
||
var upperCaseResult = service.VerifyInviteCodeAsync(inviteCode.Code.ToUpper()).GetAwaiter().GetResult();
|
||
|
||
// Assert: 大小写不同的验证结果应该相同
|
||
return lowerCaseResult.IsValid == upperCaseResult.IsValid &&
|
||
lowerCaseResult.InviteCodeId == upperCaseResult.InviteCodeId;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 6: 已删除的邀请码验证失败
|
||
/// *For any* deleted invite code (IsDeleted=true), the verification SHALL return IsValid=false
|
||
/// regardless of its status.
|
||
///
|
||
/// **Feature: miniapp-api, Property 6: 邀请码使用一次性**
|
||
/// **Validates: Requirements 5.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool DeletedInviteCodeVerificationReturnsInvalidError(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var inviteCode = CreateInviteCode(seed.Get, status: 2); // 已分配状态
|
||
inviteCode.IsDeleted = true; // 但已被软删除
|
||
dbContext.InviteCodes.Add(inviteCode);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.VerifyInviteCodeAsync(inviteCode.Code).GetAwaiter().GetResult();
|
||
|
||
// Assert: 已删除的邀请码验证应该失败
|
||
return !result.IsValid &&
|
||
result.ErrorMessage == "邀请码有误,请重新输入" &&
|
||
result.InviteCodeId == null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 6: 邀请码状态互斥性
|
||
/// *For any* invite code, only Status=2 (已分配) SHALL pass verification;
|
||
/// Status=1 (未分配) and Status=3 (已使用) SHALL fail.
|
||
///
|
||
/// **Feature: miniapp-api, Property 6: 邀请码使用一次性**
|
||
/// **Validates: Requirements 5.1, 5.3, 5.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool InviteCodeStatusDeterminesVerificationResult(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
|
||
// 创建三种状态的邀请码
|
||
var unassignedCode = CreateInviteCode(seed.Get, status: 1);
|
||
var assignedCode = CreateInviteCode(seed.Get + 1000, status: 2);
|
||
var usedCode = CreateInviteCode(seed.Get + 2000, status: 3);
|
||
usedCode.UseUserId = seed.Get + 3000;
|
||
usedCode.UseTime = DateTime.Now.AddDays(-1);
|
||
|
||
dbContext.InviteCodes.AddRange(unassignedCode, assignedCode, usedCode);
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var unassignedResult = service.VerifyInviteCodeAsync(unassignedCode.Code).GetAwaiter().GetResult();
|
||
var assignedResult = service.VerifyInviteCodeAsync(assignedCode.Code).GetAwaiter().GetResult();
|
||
var usedResult = service.VerifyInviteCodeAsync(usedCode.Code).GetAwaiter().GetResult();
|
||
|
||
// Assert: 只有已分配状态的邀请码验证通过
|
||
return !unassignedResult.IsValid &&
|
||
assignedResult.IsValid &&
|
||
!usedResult.IsValid &&
|
||
assignedResult.InviteCodeId == assignedCode.Id;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 6: 邀请码验证返回正确的邀请码ID
|
||
/// *For any* valid invite code verification, the returned InviteCodeId SHALL match
|
||
/// the actual invite code's Id in the database.
|
||
///
|
||
/// **Feature: miniapp-api, Property 6: 邀请码使用一次性**
|
||
/// **Validates: Requirements 5.4**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool ValidInviteCodeVerificationReturnsCorrectId(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
|
||
// 创建多个已分配的邀请码
|
||
var codes = new List<InviteCode>();
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
var code = CreateInviteCode(seed.Get + i * 100, status: 2);
|
||
codes.Add(code);
|
||
dbContext.InviteCodes.Add(code);
|
||
}
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act & Assert: 验证每个邀请码返回正确的ID
|
||
foreach (var code in codes)
|
||
{
|
||
var result = service.VerifyInviteCodeAsync(code.Code).GetAwaiter().GetResult();
|
||
if (!result.IsValid || result.InviteCodeId != code.Id)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Property 9: 测评答案提交状态变更
|
||
|
||
/// <summary>
|
||
/// Property 9: 提交答案后测评记录状态变为"生成中"
|
||
/// *For any* valid answer submission, the assessment record status SHALL change
|
||
/// from "待测评"(1) or "测评中"(2) to "生成中"(3).
|
||
///
|
||
/// **Feature: miniapp-api, Property 9: 测评答案提交状态变更**
|
||
/// **Validates: Requirements 4.1, 4.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool SubmitAnswersChangesStatusToGenerating(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var typeId = (long)seed.Get;
|
||
var recordId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
var questionCount = Math.Max(1, seed.Get % 10 + 1); // 1-10 questions
|
||
var initialStatus = (seed.Get % 2) + 1; // 1 or 2 (待测评 or 测评中)
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
assessmentType.QuestionCount = questionCount;
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建订单
|
||
var order = CreateOrder(orderId, userId);
|
||
dbContext.Orders.Add(order);
|
||
|
||
// 创建测评记录(初始状态为待测评或测评中)
|
||
var record = CreateAssessmentRecord(recordId, userId, orderId, typeId);
|
||
record.Status = initialStatus;
|
||
dbContext.AssessmentRecords.Add(record);
|
||
|
||
// 创建题目
|
||
var questionIds = new List<long>();
|
||
for (int i = 0; i < questionCount; i++)
|
||
{
|
||
var questionId = seed.Get + i + 1000;
|
||
questionIds.Add(questionId);
|
||
dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// 创建答案请求
|
||
var request = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest
|
||
{
|
||
RecordId = recordId,
|
||
Answers = questionIds.Select((qId, index) => new MiAssessment.Model.Models.Assessment.AnswerItem
|
||
{
|
||
QuestionId = qId,
|
||
AnswerValue = (index % 10) + 1 // 1-10
|
||
}).ToList()
|
||
};
|
||
|
||
// Act
|
||
var result = service.SubmitAnswersAsync(userId, request).GetAwaiter().GetResult();
|
||
|
||
// Assert: 验证状态变为"生成中"(3)
|
||
var updatedRecord = dbContext.AssessmentRecords.Find(recordId);
|
||
return result.Success && updatedRecord != null && updatedRecord.Status == 3;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 9: 提交答案后答案数量等于题目数量
|
||
/// *For any* valid answer submission, the saved answer count SHALL equal the question count.
|
||
///
|
||
/// **Feature: miniapp-api, Property 9: 测评答案提交状态变更**
|
||
/// **Validates: Requirements 4.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool SubmitAnswersSavesCorrectAnswerCount(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var typeId = (long)seed.Get;
|
||
var recordId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
var questionCount = Math.Max(1, seed.Get % 15 + 1); // 1-15 questions
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
assessmentType.QuestionCount = questionCount;
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建订单
|
||
var order = CreateOrder(orderId, userId);
|
||
dbContext.Orders.Add(order);
|
||
|
||
// 创建测评记录(初始状态为待测评)
|
||
var record = CreateAssessmentRecord(recordId, userId, orderId, typeId);
|
||
record.Status = 1; // 待测评
|
||
dbContext.AssessmentRecords.Add(record);
|
||
|
||
// 创建题目
|
||
var questionIds = new List<long>();
|
||
for (int i = 0; i < questionCount; i++)
|
||
{
|
||
var questionId = seed.Get + i + 1000;
|
||
questionIds.Add(questionId);
|
||
dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// 创建答案请求
|
||
var request = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest
|
||
{
|
||
RecordId = recordId,
|
||
Answers = questionIds.Select((qId, index) => new MiAssessment.Model.Models.Assessment.AnswerItem
|
||
{
|
||
QuestionId = qId,
|
||
AnswerValue = (index % 10) + 1
|
||
}).ToList()
|
||
};
|
||
|
||
// Act
|
||
service.SubmitAnswersAsync(userId, request).GetAwaiter().GetResult();
|
||
|
||
// Assert: 验证保存的答案数量等于题目数量
|
||
var savedAnswerCount = dbContext.AssessmentAnswers.Count(a => a.RecordId == recordId);
|
||
return savedAnswerCount == questionCount;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 9: 只有待测评或测评中状态才能提交答案
|
||
/// *For any* assessment record with status other than "待测评"(1) or "测评中"(2),
|
||
/// answer submission SHALL fail.
|
||
///
|
||
/// **Feature: miniapp-api, Property 9: 测评答案提交状态变更**
|
||
/// **Validates: Requirements 4.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool SubmitAnswersFailsForInvalidStatus(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var typeId = (long)seed.Get;
|
||
var recordId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
var questionCount = 5;
|
||
// 使用无效状态:3(生成中) 或 4(已完成)
|
||
var invalidStatus = (seed.Get % 2) + 3; // 3 or 4
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
assessmentType.QuestionCount = questionCount;
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建订单
|
||
var order = CreateOrder(orderId, userId);
|
||
dbContext.Orders.Add(order);
|
||
|
||
// 创建测评记录(无效状态)
|
||
var record = CreateAssessmentRecord(recordId, userId, orderId, typeId);
|
||
record.Status = invalidStatus;
|
||
dbContext.AssessmentRecords.Add(record);
|
||
|
||
// 创建题目
|
||
var questionIds = new List<long>();
|
||
for (int i = 0; i < questionCount; i++)
|
||
{
|
||
var questionId = seed.Get + i + 1000;
|
||
questionIds.Add(questionId);
|
||
dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// 创建答案请求
|
||
var request = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest
|
||
{
|
||
RecordId = recordId,
|
||
Answers = questionIds.Select((qId, index) => new MiAssessment.Model.Models.Assessment.AnswerItem
|
||
{
|
||
QuestionId = qId,
|
||
AnswerValue = (index % 10) + 1
|
||
}).ToList()
|
||
};
|
||
|
||
// Act & Assert: 验证提交失败
|
||
try
|
||
{
|
||
service.SubmitAnswersAsync(userId, request).GetAwaiter().GetResult();
|
||
return false; // 应该抛出异常
|
||
}
|
||
catch (InvalidOperationException ex)
|
||
{
|
||
return ex.Message.Contains("当前测评状态不允许提交答案");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 9: 答案数量不匹配时提交失败
|
||
/// *For any* answer submission where answer count does not match question count,
|
||
/// the submission SHALL fail.
|
||
///
|
||
/// **Feature: miniapp-api, Property 9: 测评答案提交状态变更**
|
||
/// **Validates: Requirements 4.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool SubmitAnswersFailsWhenAnswerCountMismatch(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var typeId = (long)seed.Get;
|
||
var recordId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
var questionCount = Math.Max(3, seed.Get % 10 + 3); // 3-12 questions
|
||
var answerCount = questionCount - 1; // 少一个答案
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
assessmentType.QuestionCount = questionCount;
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建订单
|
||
var order = CreateOrder(orderId, userId);
|
||
dbContext.Orders.Add(order);
|
||
|
||
// 创建测评记录
|
||
var record = CreateAssessmentRecord(recordId, userId, orderId, typeId);
|
||
record.Status = 1; // 待测评
|
||
dbContext.AssessmentRecords.Add(record);
|
||
|
||
// 创建题目
|
||
var questionIds = new List<long>();
|
||
for (int i = 0; i < questionCount; i++)
|
||
{
|
||
var questionId = seed.Get + i + 1000;
|
||
questionIds.Add(questionId);
|
||
dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// 创建答案请求(答案数量少于题目数量)
|
||
var request = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest
|
||
{
|
||
RecordId = recordId,
|
||
Answers = questionIds.Take(answerCount).Select((qId, index) => new MiAssessment.Model.Models.Assessment.AnswerItem
|
||
{
|
||
QuestionId = qId,
|
||
AnswerValue = (index % 10) + 1
|
||
}).ToList()
|
||
};
|
||
|
||
// Act & Assert: 验证提交失败
|
||
try
|
||
{
|
||
service.SubmitAnswersAsync(userId, request).GetAwaiter().GetResult();
|
||
return false; // 应该抛出异常
|
||
}
|
||
catch (InvalidOperationException ex)
|
||
{
|
||
return ex.Message.Contains("答案数量") && ex.Message.Contains("题目数量") && ex.Message.Contains("不匹配");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 9: 非本人记录提交答案失败
|
||
/// *For any* answer submission to a record not belonging to the current user,
|
||
/// the submission SHALL fail with unauthorized error.
|
||
///
|
||
/// **Feature: miniapp-api, Property 9: 测评答案提交状态变更**
|
||
/// **Validates: Requirements 4.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool SubmitAnswersFailsForOtherUserRecord(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var otherUserId = (long)(seed.Get + 10000);
|
||
var typeId = (long)seed.Get;
|
||
var recordId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
var questionCount = 5;
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
assessmentType.QuestionCount = questionCount;
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建订单(属于其他用户)
|
||
var order = CreateOrder(orderId, otherUserId);
|
||
dbContext.Orders.Add(order);
|
||
|
||
// 创建测评记录(属于其他用户)
|
||
var record = CreateAssessmentRecord(recordId, otherUserId, orderId, typeId);
|
||
record.Status = 1; // 待测评
|
||
dbContext.AssessmentRecords.Add(record);
|
||
|
||
// 创建题目
|
||
var questionIds = new List<long>();
|
||
for (int i = 0; i < questionCount; i++)
|
||
{
|
||
var questionId = seed.Get + i + 1000;
|
||
questionIds.Add(questionId);
|
||
dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// 创建答案请求
|
||
var request = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest
|
||
{
|
||
RecordId = recordId,
|
||
Answers = questionIds.Select((qId, index) => new MiAssessment.Model.Models.Assessment.AnswerItem
|
||
{
|
||
QuestionId = qId,
|
||
AnswerValue = (index % 10) + 1
|
||
}).ToList()
|
||
};
|
||
|
||
// Act & Assert: 验证当前用户提交其他用户的记录失败
|
||
try
|
||
{
|
||
service.SubmitAnswersAsync(userId, request).GetAwaiter().GetResult();
|
||
return false; // 应该抛出异常
|
||
}
|
||
catch (UnauthorizedAccessException ex)
|
||
{
|
||
return ex.Message.Contains("无权限");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 9: 提交答案后SubmitTime被设置
|
||
/// *For any* valid answer submission, the assessment record's SubmitTime SHALL be set.
|
||
///
|
||
/// **Feature: miniapp-api, Property 9: 测评答案提交状态变更**
|
||
/// **Validates: Requirements 4.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool SubmitAnswersSetsSubmitTime(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var typeId = (long)seed.Get;
|
||
var recordId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
var questionCount = Math.Max(1, seed.Get % 5 + 1); // 1-5 questions
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
assessmentType.QuestionCount = questionCount;
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建订单
|
||
var order = CreateOrder(orderId, userId);
|
||
dbContext.Orders.Add(order);
|
||
|
||
// 创建测评记录(初始SubmitTime为null)
|
||
var record = CreateAssessmentRecord(recordId, userId, orderId, typeId);
|
||
record.Status = 1; // 待测评
|
||
record.SubmitTime = null;
|
||
dbContext.AssessmentRecords.Add(record);
|
||
|
||
// 创建题目
|
||
var questionIds = new List<long>();
|
||
for (int i = 0; i < questionCount; i++)
|
||
{
|
||
var questionId = seed.Get + i + 1000;
|
||
questionIds.Add(questionId);
|
||
dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var beforeSubmit = DateTime.Now.AddSeconds(-1);
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// 创建答案请求
|
||
var request = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest
|
||
{
|
||
RecordId = recordId,
|
||
Answers = questionIds.Select((qId, index) => new MiAssessment.Model.Models.Assessment.AnswerItem
|
||
{
|
||
QuestionId = qId,
|
||
AnswerValue = (index % 10) + 1
|
||
}).ToList()
|
||
};
|
||
|
||
// Act
|
||
service.SubmitAnswersAsync(userId, request).GetAwaiter().GetResult();
|
||
var afterSubmit = DateTime.Now.AddSeconds(1);
|
||
|
||
// Assert: 验证SubmitTime被设置且在合理范围内
|
||
var updatedRecord = dbContext.AssessmentRecords.Find(recordId);
|
||
return updatedRecord != null &&
|
||
updatedRecord.SubmitTime != null &&
|
||
updatedRecord.SubmitTime >= beforeSubmit &&
|
||
updatedRecord.SubmitTime <= afterSubmit;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 9: 重复提交答案会覆盖之前的答案
|
||
/// *For any* re-submission of answers, the previous answers SHALL be replaced.
|
||
///
|
||
/// **Feature: miniapp-api, Property 9: 测评答案提交状态变更**
|
||
/// **Validates: Requirements 4.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool ResubmitAnswersReplacesExistingAnswers(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var typeId = (long)seed.Get;
|
||
var recordId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
var questionCount = 3;
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
assessmentType.QuestionCount = questionCount;
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建订单
|
||
var order = CreateOrder(orderId, userId);
|
||
dbContext.Orders.Add(order);
|
||
|
||
// 创建测评记录
|
||
var record = CreateAssessmentRecord(recordId, userId, orderId, typeId);
|
||
record.Status = 1; // 待测评
|
||
dbContext.AssessmentRecords.Add(record);
|
||
|
||
// 创建题目
|
||
var questionIds = new List<long>();
|
||
for (int i = 0; i < questionCount; i++)
|
||
{
|
||
var questionId = seed.Get + i + 1000;
|
||
questionIds.Add(questionId);
|
||
dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// 第一次提交(答案值都是1)
|
||
var request1 = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest
|
||
{
|
||
RecordId = recordId,
|
||
Answers = questionIds.Select(qId => new MiAssessment.Model.Models.Assessment.AnswerItem
|
||
{
|
||
QuestionId = qId,
|
||
AnswerValue = 1
|
||
}).ToList()
|
||
};
|
||
service.SubmitAnswersAsync(userId, request1).GetAwaiter().GetResult();
|
||
|
||
// 重置状态以允许重新提交
|
||
record.Status = 1;
|
||
dbContext.SaveChanges();
|
||
|
||
// 第二次提交(答案值都是5)
|
||
var request2 = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest
|
||
{
|
||
RecordId = recordId,
|
||
Answers = questionIds.Select(qId => new MiAssessment.Model.Models.Assessment.AnswerItem
|
||
{
|
||
QuestionId = qId,
|
||
AnswerValue = 5
|
||
}).ToList()
|
||
};
|
||
service.SubmitAnswersAsync(userId, request2).GetAwaiter().GetResult();
|
||
|
||
// Assert: 验证答案被覆盖
|
||
var savedAnswers = dbContext.AssessmentAnswers.Where(a => a.RecordId == recordId).ToList();
|
||
return savedAnswers.Count == questionCount && savedAnswers.All(a => a.AnswerValue == 5);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 9: 提交答案后可以通过GetResultStatus查询到状态变更
|
||
/// *For any* valid answer submission, GetResultStatus SHALL return Status=3 (生成中).
|
||
///
|
||
/// **Feature: miniapp-api, Property 9: 测评答案提交状态变更**
|
||
/// **Validates: Requirements 4.2**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool GetResultStatusReturnsGeneratingAfterSubmit(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var typeId = (long)seed.Get;
|
||
var recordId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
var questionCount = Math.Max(1, seed.Get % 5 + 1);
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
assessmentType.QuestionCount = questionCount;
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建订单
|
||
var order = CreateOrder(orderId, userId);
|
||
dbContext.Orders.Add(order);
|
||
|
||
// 创建测评记录
|
||
var record = CreateAssessmentRecord(recordId, userId, orderId, typeId);
|
||
record.Status = 1; // 待测评
|
||
dbContext.AssessmentRecords.Add(record);
|
||
|
||
// 创建题目
|
||
var questionIds = new List<long>();
|
||
for (int i = 0; i < questionCount; i++)
|
||
{
|
||
var questionId = seed.Get + i + 1000;
|
||
questionIds.Add(questionId);
|
||
dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// 提交答案
|
||
var request = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest
|
||
{
|
||
RecordId = recordId,
|
||
Answers = questionIds.Select((qId, index) => new MiAssessment.Model.Models.Assessment.AnswerItem
|
||
{
|
||
QuestionId = qId,
|
||
AnswerValue = (index % 10) + 1
|
||
}).ToList()
|
||
};
|
||
service.SubmitAnswersAsync(userId, request).GetAwaiter().GetResult();
|
||
|
||
// Act: 查询状态
|
||
var status = service.GetResultStatusAsync(userId, recordId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 验证状态为"生成中"(3)且未完成
|
||
return status != null && status.Status == 3 && !status.IsCompleted;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Property 4: 用户数据隔离 - GetResult
|
||
|
||
/// <summary>
|
||
/// Property 4: 用户只能获取自己的测评结果
|
||
/// *For any* GetResult request, the returned data SHALL only belong to the current
|
||
/// logged-in user. Requests for other users' records SHALL return null.
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 4.3, 4.5**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool GetResultOnlyReturnsOwnRecords(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var otherUserId = (long)(seed.Get + 10000);
|
||
var typeId = (long)seed.Get;
|
||
var recordId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建订单(属于当前用户)
|
||
var order = CreateOrder(orderId, userId);
|
||
dbContext.Orders.Add(order);
|
||
|
||
// 创建测评记录(属于当前用户,已完成状态)
|
||
var record = CreateAssessmentRecord(recordId, userId, orderId, typeId);
|
||
record.Status = 4; // 已完成
|
||
record.CompleteTime = DateTime.Now.AddMinutes(-10);
|
||
dbContext.AssessmentRecords.Add(record);
|
||
|
||
// 创建报告分类
|
||
var category = CreateReportCategory(seed.Get, typeId, categoryType: 1);
|
||
dbContext.ReportCategories.Add(category);
|
||
|
||
// 创建测评结果
|
||
var result = CreateAssessmentResult(seed.Get, recordId, category.Id);
|
||
dbContext.AssessmentResults.Add(result);
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 当前用户获取自己的记录
|
||
var ownResult = service.GetResultAsync(userId, recordId).GetAwaiter().GetResult();
|
||
|
||
// Act: 其他用户尝试获取该记录
|
||
var otherResult = service.GetResultAsync(otherUserId, recordId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 当前用户可以获取,其他用户不能获取
|
||
return ownResult != null &&
|
||
ownResult.Id == recordId &&
|
||
otherResult == null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4: 用户无法获取其他用户的测评结果
|
||
/// *For any* GetResult request with a recordId belonging to another user,
|
||
/// the result SHALL be null (no data returned).
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 4.5**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool GetResultReturnsNullForOtherUserRecords(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var otherUserId = (long)(seed.Get + 10000);
|
||
var typeId = (long)seed.Get;
|
||
var recordId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建订单(属于其他用户)
|
||
var order = CreateOrder(orderId, otherUserId);
|
||
dbContext.Orders.Add(order);
|
||
|
||
// 创建测评记录(属于其他用户,已完成状态)
|
||
var record = CreateAssessmentRecord(recordId, otherUserId, orderId, typeId);
|
||
record.Status = 4; // 已完成
|
||
record.CompleteTime = DateTime.Now.AddMinutes(-10);
|
||
dbContext.AssessmentRecords.Add(record);
|
||
|
||
// 创建报告分类
|
||
var category = CreateReportCategory(seed.Get, typeId, categoryType: 1);
|
||
dbContext.ReportCategories.Add(category);
|
||
|
||
// 创建测评结果
|
||
var result = CreateAssessmentResult(seed.Get, recordId, category.Id);
|
||
dbContext.AssessmentResults.Add(result);
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 当前用户尝试获取其他用户的记录
|
||
var resultDto = service.GetResultAsync(userId, recordId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 应该返回null
|
||
return resultDto == null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4: 多用户场景下数据隔离正确
|
||
/// *For any* multi-user scenario, each user SHALL only see their own assessment results,
|
||
/// and the total count of accessible records SHALL match their own record count.
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 4.3, 4.5**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool MultiUserDataIsolationIsCorrect(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 typeId = (long)seed.Get;
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建报告分类
|
||
var category = CreateReportCategory(seed.Get, typeId, categoryType: 1);
|
||
dbContext.ReportCategories.Add(category);
|
||
|
||
// 为每个用户创建不同数量的测评记录
|
||
var user1RecordCount = Math.Max(1, seed.Get % 3 + 1); // 1-3
|
||
var user2RecordCount = Math.Max(1, (seed.Get + 1) % 3 + 1); // 1-3
|
||
var user3RecordCount = Math.Max(1, (seed.Get + 2) % 3 + 1); // 1-3
|
||
|
||
var user1RecordIds = new List<long>();
|
||
var user2RecordIds = new List<long>();
|
||
var user3RecordIds = new List<long>();
|
||
|
||
// 创建用户1的记录
|
||
for (int i = 0; i < user1RecordCount; i++)
|
||
{
|
||
var recordId = seed.Get + i;
|
||
var orderId = seed.Get + i;
|
||
var order = CreateOrder(orderId, user1Id);
|
||
dbContext.Orders.Add(order);
|
||
var record = CreateAssessmentRecord(recordId, user1Id, orderId, typeId);
|
||
record.Status = 4;
|
||
record.CompleteTime = DateTime.Now.AddMinutes(-i);
|
||
dbContext.AssessmentRecords.Add(record);
|
||
dbContext.AssessmentResults.Add(CreateAssessmentResult(seed.Get + i, recordId, category.Id));
|
||
user1RecordIds.Add(recordId);
|
||
}
|
||
|
||
// 创建用户2的记录
|
||
for (int i = 0; i < user2RecordCount; i++)
|
||
{
|
||
var recordId = seed.Get + 1000 + i;
|
||
var orderId = seed.Get + 1000 + i;
|
||
var order = CreateOrder(orderId, user2Id);
|
||
dbContext.Orders.Add(order);
|
||
var record = CreateAssessmentRecord(recordId, user2Id, orderId, typeId);
|
||
record.Status = 4;
|
||
record.CompleteTime = DateTime.Now.AddMinutes(-i);
|
||
dbContext.AssessmentRecords.Add(record);
|
||
dbContext.AssessmentResults.Add(CreateAssessmentResult(seed.Get + 1000 + i, recordId, category.Id));
|
||
user2RecordIds.Add(recordId);
|
||
}
|
||
|
||
// 创建用户3的记录
|
||
for (int i = 0; i < user3RecordCount; i++)
|
||
{
|
||
var recordId = seed.Get + 2000 + i;
|
||
var orderId = seed.Get + 2000 + i;
|
||
var order = CreateOrder(orderId, user3Id);
|
||
dbContext.Orders.Add(order);
|
||
var record = CreateAssessmentRecord(recordId, user3Id, orderId, typeId);
|
||
record.Status = 4;
|
||
record.CompleteTime = DateTime.Now.AddMinutes(-i);
|
||
dbContext.AssessmentRecords.Add(record);
|
||
dbContext.AssessmentResults.Add(CreateAssessmentResult(seed.Get + 2000 + i, recordId, category.Id));
|
||
user3RecordIds.Add(recordId);
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act & Assert: 验证每个用户只能访问自己的记录
|
||
// 用户1可以访问自己的记录
|
||
foreach (var recordId in user1RecordIds)
|
||
{
|
||
var result = service.GetResultAsync(user1Id, recordId).GetAwaiter().GetResult();
|
||
if (result == null || result.Id != recordId) return false;
|
||
}
|
||
|
||
// 用户1不能访问用户2的记录
|
||
foreach (var recordId in user2RecordIds)
|
||
{
|
||
var result = service.GetResultAsync(user1Id, recordId).GetAwaiter().GetResult();
|
||
if (result != null) return false;
|
||
}
|
||
|
||
// 用户1不能访问用户3的记录
|
||
foreach (var recordId in user3RecordIds)
|
||
{
|
||
var result = service.GetResultAsync(user1Id, recordId).GetAwaiter().GetResult();
|
||
if (result != null) return false;
|
||
}
|
||
|
||
// 用户2可以访问自己的记录
|
||
foreach (var recordId in user2RecordIds)
|
||
{
|
||
var result = service.GetResultAsync(user2Id, recordId).GetAwaiter().GetResult();
|
||
if (result == null || result.Id != recordId) return false;
|
||
}
|
||
|
||
// 用户2不能访问用户1的记录
|
||
foreach (var recordId in user1RecordIds)
|
||
{
|
||
var result = service.GetResultAsync(user2Id, recordId).GetAwaiter().GetResult();
|
||
if (result != null) return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4: 未完成的测评记录不返回结果
|
||
/// *For any* GetResult request for an incomplete record (Status != 4),
|
||
/// the result SHALL be null even if the record belongs to the current user.
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 4.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool GetResultReturnsNullForIncompleteRecords(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var typeId = (long)seed.Get;
|
||
var recordId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
// 使用非完成状态:1(待测评), 2(测评中), 3(生成中)
|
||
var incompleteStatus = (seed.Get % 3) + 1; // 1, 2, or 3
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建订单
|
||
var order = CreateOrder(orderId, userId);
|
||
dbContext.Orders.Add(order);
|
||
|
||
// 创建测评记录(未完成状态)
|
||
var record = CreateAssessmentRecord(recordId, userId, orderId, typeId);
|
||
record.Status = incompleteStatus;
|
||
dbContext.AssessmentRecords.Add(record);
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 获取未完成的记录
|
||
var result = service.GetResultAsync(userId, recordId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 应该返回null
|
||
return result == null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4: 已删除的测评记录不返回结果
|
||
/// *For any* GetResult request for a deleted record (IsDeleted=true),
|
||
/// the result SHALL be null even if the record belongs to the current user.
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 4.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool GetResultReturnsNullForDeletedRecords(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var typeId = (long)seed.Get;
|
||
var recordId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建订单
|
||
var order = CreateOrder(orderId, userId);
|
||
dbContext.Orders.Add(order);
|
||
|
||
// 创建测评记录(已完成但已删除)
|
||
var record = CreateAssessmentRecord(recordId, userId, orderId, typeId);
|
||
record.Status = 4; // 已完成
|
||
record.IsDeleted = true; // 已删除
|
||
record.CompleteTime = DateTime.Now.AddMinutes(-10);
|
||
dbContext.AssessmentRecords.Add(record);
|
||
|
||
// 创建报告分类
|
||
var category = CreateReportCategory(seed.Get, typeId, categoryType: 1);
|
||
dbContext.ReportCategories.Add(category);
|
||
|
||
// 创建测评结果
|
||
var result = CreateAssessmentResult(seed.Get, recordId, category.Id);
|
||
dbContext.AssessmentResults.Add(result);
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 获取已删除的记录
|
||
var resultDto = service.GetResultAsync(userId, recordId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 应该返回null
|
||
return resultDto == null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4: 返回的测评结果数据完整性
|
||
/// *For any* valid GetResult request, the returned data SHALL contain complete
|
||
/// report structure including intelligences, traits, abilities, etc.
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 4.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool GetResultReturnsCompleteDataStructure(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var typeId = (long)seed.Get;
|
||
var recordId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建订单
|
||
var order = CreateOrder(orderId, userId);
|
||
dbContext.Orders.Add(order);
|
||
|
||
// 创建测评记录(已完成状态)
|
||
var record = CreateAssessmentRecord(recordId, userId, orderId, typeId);
|
||
record.Status = 4;
|
||
record.CompleteTime = DateTime.Now.AddMinutes(-10);
|
||
dbContext.AssessmentRecords.Add(record);
|
||
|
||
// 创建多种类型的报告分类和结果
|
||
var categoryTypes = new[] { 1, 2, 3, 4, 5, 6, 7, 8 }; // 八种分类类型
|
||
for (int i = 0; i < categoryTypes.Length; i++)
|
||
{
|
||
var category = CreateReportCategory(seed.Get + i * 100, typeId, categoryTypes[i]);
|
||
dbContext.ReportCategories.Add(category);
|
||
var result = CreateAssessmentResult(seed.Get + i * 100, recordId, category.Id);
|
||
dbContext.AssessmentResults.Add(result);
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var resultDto = service.GetResultAsync(userId, recordId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 验证返回的数据结构完整
|
||
if (resultDto == null) return false;
|
||
|
||
// 验证基本信息
|
||
if (resultDto.Id != recordId) return false;
|
||
if (resultDto.TypeId != typeId) return false;
|
||
if (string.IsNullOrEmpty(resultDto.AssessmentName)) return false;
|
||
if (string.IsNullOrEmpty(resultDto.Name)) return false;
|
||
if (string.IsNullOrEmpty(resultDto.TestDate)) return false;
|
||
|
||
// 验证各分类结果列表已初始化
|
||
if (resultDto.Intelligences == null) return false;
|
||
if (resultDto.Traits == null) return false;
|
||
if (resultDto.Abilities == null) return false;
|
||
if (resultDto.InnateLearningTypes == null) return false;
|
||
if (resultDto.KeyLearningAbilities == null) return false;
|
||
if (resultDto.BrainTypes == null) return false;
|
||
if (resultDto.PersonalityTypes == null) return false;
|
||
if (resultDto.FutureDevelopmentAbilities == null) return false;
|
||
|
||
// 验证各分类结果数量(每种类型各1个)
|
||
if (resultDto.Intelligences.Count != 1) return false;
|
||
if (resultDto.Traits.Count != 1) return false;
|
||
if (resultDto.Abilities.Count != 1) return false;
|
||
if (resultDto.InnateLearningTypes.Count != 1) return false;
|
||
if (resultDto.KeyLearningAbilities.Count != 1) return false;
|
||
if (resultDto.BrainTypes.Count != 1) return false;
|
||
if (resultDto.PersonalityTypes.Count != 1) return false;
|
||
if (resultDto.FutureDevelopmentAbilities.Count != 1) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4: 不存在的记录ID返回null
|
||
/// *For any* GetResult request with a non-existent recordId,
|
||
/// the result SHALL be null.
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 4.3**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool GetResultReturnsNullForNonExistentRecords(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var nonExistentRecordId = (long)(seed.Get + 999999);
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 获取不存在的记录
|
||
var result = service.GetResultAsync(userId, nonExistentRecordId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 应该返回null
|
||
return result == null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4: 用户数据隔离在GetResultStatus中也生效
|
||
/// *For any* GetResultStatus request, the returned data SHALL only belong to
|
||
/// the current logged-in user.
|
||
///
|
||
/// **Feature: miniapp-api, Property 4: 用户数据隔离**
|
||
/// **Validates: Requirements 4.3, 4.5**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool GetResultStatusOnlyReturnsOwnRecords(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = (long)seed.Get;
|
||
var otherUserId = (long)(seed.Get + 10000);
|
||
var typeId = (long)seed.Get;
|
||
var recordId = (long)seed.Get;
|
||
var orderId = (long)seed.Get;
|
||
|
||
// 创建测评类型
|
||
var assessmentType = CreateAssessmentType(typeId);
|
||
dbContext.AssessmentTypes.Add(assessmentType);
|
||
|
||
// 创建订单(属于当前用户)
|
||
var order = CreateOrder(orderId, userId);
|
||
dbContext.Orders.Add(order);
|
||
|
||
// 创建测评记录(属于当前用户)
|
||
var record = CreateAssessmentRecord(recordId, userId, orderId, typeId);
|
||
record.Status = 3; // 生成中
|
||
dbContext.AssessmentRecords.Add(record);
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 当前用户获取自己的记录状态
|
||
var ownStatus = service.GetResultStatusAsync(userId, recordId).GetAwaiter().GetResult();
|
||
|
||
// Act: 其他用户尝试获取该记录状态
|
||
var otherStatus = service.GetResultStatusAsync(otherUserId, recordId).GetAwaiter().GetResult();
|
||
|
||
// Assert: 当前用户可以获取,其他用户不能获取
|
||
return ownStatus != null &&
|
||
ownStatus.Status == 3 &&
|
||
otherStatus == null;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 边界条件测试
|
||
|
||
/// <summary>
|
||
/// Property 2: 空数据库返回空题目列表
|
||
///
|
||
/// **Feature: miniapp-api, Property 2: 列表查询排序正确性**
|
||
/// **Validates: Requirements 3.2**
|
||
/// </summary>
|
||
[Fact]
|
||
public void EmptyDatabaseReturnsEmptyQuestionList()
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.GetQuestionListAsync(1).GetAwaiter().GetResult();
|
||
|
||
// Assert
|
||
Assert.Empty(result);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 3: 空数据库返回空分页结果
|
||
///
|
||
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
|
||
/// **Validates: Requirements 6.1**
|
||
/// </summary>
|
||
[Fact]
|
||
public void EmptyDatabaseReturnsEmptyPagedResult()
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.GetHistoryListAsync(1, 1, 20).GetAwaiter().GetResult();
|
||
|
||
// Assert
|
||
Assert.Empty(result.List);
|
||
Assert.Equal(0, result.Total);
|
||
Assert.Equal(0, result.TotalPages);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 3: 分页参数边界值处理
|
||
///
|
||
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
|
||
/// **Validates: Requirements 6.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 PaginationHandlesBoundaryValues(int page, int pageSize)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateTestDbContext();
|
||
var userId = 1L;
|
||
|
||
// 创建测评类型
|
||
var typeId = 1L;
|
||
dbContext.AssessmentTypes.Add(CreateAssessmentType(typeId));
|
||
|
||
// 创建测评记录
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
var order = CreateOrder(i + 1, userId);
|
||
dbContext.Orders.Add(order);
|
||
dbContext.AssessmentRecords.Add(CreateAssessmentRecord(i + 1, userId, order.Id, typeId));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new AssessmentService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.GetHistoryListAsync(userId, page, pageSize).GetAwaiter().GetResult();
|
||
|
||
// Assert: 服务应该处理边界值,不抛出异常
|
||
Assert.NotNull(result);
|
||
Assert.True(result.Page >= 1);
|
||
Assert.True(result.PageSize >= 1 && result.PageSize <= 100);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 辅助方法
|
||
|
||
/// <summary>
|
||
/// 创建测试用内存数据库上下文
|
||
/// 使用自定义配置忽略外键关系验证
|
||
/// </summary>
|
||
private TestAssessmentDbContext CreateTestDbContext()
|
||
{
|
||
var options = new DbContextOptionsBuilder<TestAssessmentDbContext>()
|
||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||
.Options;
|
||
|
||
return new TestAssessmentDbContext(options);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试测评类型
|
||
/// </summary>
|
||
private AssessmentType CreateAssessmentType(long id)
|
||
{
|
||
return new AssessmentType
|
||
{
|
||
Id = id,
|
||
Name = $"Test Assessment Type {id}",
|
||
Code = $"TEST_{id}",
|
||
ImageUrl = $"https://example.com/type_{id}.jpg",
|
||
IntroContent = $"<p>Introduction for type {id}</p>",
|
||
Price = 99.00m,
|
||
QuestionCount = 80,
|
||
Sort = (int)(id % 100),
|
||
Status = 1,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试题目
|
||
/// </summary>
|
||
private Question CreateQuestion(long id, long typeId, int questionNo, int status, bool isDeleted)
|
||
{
|
||
return new Question
|
||
{
|
||
Id = id,
|
||
AssessmentTypeId = typeId,
|
||
QuestionNo = questionNo,
|
||
Content = $"Question {questionNo} content for type {typeId}",
|
||
Sort = questionNo,
|
||
Status = status,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = isDeleted
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试订单
|
||
/// </summary>
|
||
private Order CreateOrder(long id, long userId)
|
||
{
|
||
return new Order
|
||
{
|
||
Id = id,
|
||
OrderNo = $"ORD{id:D10}",
|
||
UserId = userId,
|
||
OrderType = 1,
|
||
ProductId = 1,
|
||
ProductName = "Test Assessment",
|
||
Amount = 99.00m,
|
||
PayAmount = 99.00m,
|
||
Status = 2, // 已支付
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试测评记录
|
||
/// </summary>
|
||
private AssessmentRecord CreateAssessmentRecord(long id, long userId, long orderId, long typeId)
|
||
{
|
||
return new AssessmentRecord
|
||
{
|
||
Id = id,
|
||
UserId = userId,
|
||
OrderId = orderId,
|
||
AssessmentTypeId = typeId,
|
||
Name = $"TestUser{userId}",
|
||
Phone = "13800138000",
|
||
Gender = 1,
|
||
Age = 18,
|
||
EducationStage = 3,
|
||
Province = "北京市",
|
||
City = "北京市",
|
||
District = "朝阳区",
|
||
Status = 4, // 已完成
|
||
CreateTime = DateTime.Now.AddMinutes(-id), // 不同的创建时间以测试排序
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试邀请码
|
||
/// </summary>
|
||
/// <param name="seed">种子值,用于生成唯一ID和Code</param>
|
||
/// <param name="status">状态:1未分配 2已分配 3已使用</param>
|
||
private InviteCode CreateInviteCode(long seed, int status)
|
||
{
|
||
return new InviteCode
|
||
{
|
||
Id = seed,
|
||
Code = GenerateRandomInviteCode(seed),
|
||
BatchNo = $"BATCH{seed / 100:D6}",
|
||
AssignUserId = status >= 2 ? seed + 500 : null,
|
||
AssignTime = status >= 2 ? DateTime.Now.AddDays(-7) : null,
|
||
UseUserId = status == 3 ? seed + 600 : null,
|
||
UseOrderId = status == 3 ? seed + 700 : null,
|
||
UseTime = status == 3 ? DateTime.Now.AddDays(-1) : null,
|
||
Status = status,
|
||
CreateTime = DateTime.Now.AddDays(-10),
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试报告分类
|
||
/// </summary>
|
||
/// <param name="id">分类ID</param>
|
||
/// <param name="typeId">测评类型ID</param>
|
||
/// <param name="categoryType">分类类型:1八大智能 2个人特质 3细分能力 4先天学习 5学习能力 6大脑类型 7性格类型 8未来能力</param>
|
||
private ReportCategory CreateReportCategory(long id, long typeId, int categoryType)
|
||
{
|
||
var categoryNames = new Dictionary<int, string>
|
||
{
|
||
{ 1, "语言智能" },
|
||
{ 2, "领导力" },
|
||
{ 3, "逻辑推理" },
|
||
{ 4, "视觉学习" },
|
||
{ 5, "专注力" },
|
||
{ 6, "左脑型" },
|
||
{ 7, "外向型" },
|
||
{ 8, "创新能力" }
|
||
};
|
||
|
||
var categoryCodes = new Dictionary<int, string>
|
||
{
|
||
{ 1, "LINGUISTIC" },
|
||
{ 2, "LEADERSHIP" },
|
||
{ 3, "LOGICAL" },
|
||
{ 4, "VISUAL" },
|
||
{ 5, "FOCUS" },
|
||
{ 6, "LEFT_BRAIN" },
|
||
{ 7, "EXTROVERT" },
|
||
{ 8, "INNOVATION" }
|
||
};
|
||
|
||
return new ReportCategory
|
||
{
|
||
Id = id,
|
||
AssessmentTypeId = typeId,
|
||
ParentId = 0,
|
||
Name = categoryNames.GetValueOrDefault(categoryType, $"Category_{categoryType}"),
|
||
Code = categoryCodes.GetValueOrDefault(categoryType, $"CAT_{categoryType}"),
|
||
CategoryType = categoryType,
|
||
ScoreRule = 1,
|
||
Sort = categoryType,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建测试测评结果
|
||
/// </summary>
|
||
/// <param name="id">结果ID</param>
|
||
/// <param name="recordId">测评记录ID</param>
|
||
/// <param name="categoryId">分类ID</param>
|
||
private AssessmentResult CreateAssessmentResult(long id, long recordId, long categoryId)
|
||
{
|
||
var random = new Random((int)(id % int.MaxValue));
|
||
var score = (decimal)(random.Next(60, 100));
|
||
var maxScore = 100m;
|
||
var percentage = score / maxScore * 100;
|
||
var starLevel = score >= 90 ? 5 : score >= 80 ? 4 : score >= 70 ? 3 : score >= 60 ? 2 : 1;
|
||
|
||
return new AssessmentResult
|
||
{
|
||
Id = id,
|
||
RecordId = recordId,
|
||
CategoryId = categoryId,
|
||
Score = score,
|
||
MaxScore = maxScore,
|
||
Percentage = percentage,
|
||
Rank = 1,
|
||
StarLevel = starLevel,
|
||
CreateTime = DateTime.Now
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成随机邀请码(5位大写字母)
|
||
/// </summary>
|
||
/// <param name="seed">种子值</param>
|
||
private static string GenerateRandomInviteCode(long seed)
|
||
{
|
||
const string charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||
var random = new Random((int)(seed % int.MaxValue));
|
||
var chars = new char[5];
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
chars[i] = charset[random.Next(charset.Length)];
|
||
}
|
||
return new string(chars);
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
|
||
/// <summary>
|
||
/// 测试用DbContext,继承自MiAssessmentDbContext但忽略外键关系验证
|
||
/// </summary>
|
||
public class TestAssessmentDbContext : MiAssessmentDbContext
|
||
{
|
||
public TestAssessmentDbContext(DbContextOptions<TestAssessmentDbContext> options)
|
||
: base(CreateBaseOptions(options))
|
||
{
|
||
}
|
||
|
||
private static DbContextOptions<MiAssessmentDbContext> CreateBaseOptions(DbContextOptions<TestAssessmentDbContext> 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,避免外键关系验证
|
||
// 只配置必要的表映射
|
||
modelBuilder.Entity<AssessmentType>().ToTable("assessment_types");
|
||
modelBuilder.Entity<Question>().ToTable("questions");
|
||
modelBuilder.Entity<AssessmentRecord>().ToTable("assessment_records");
|
||
modelBuilder.Entity<AssessmentAnswer>().ToTable("assessment_answers");
|
||
modelBuilder.Entity<Order>().ToTable("orders");
|
||
modelBuilder.Entity<InviteCode>().ToTable("invite_codes");
|
||
modelBuilder.Entity<ReportCategory>().ToTable("report_categories");
|
||
modelBuilder.Entity<AssessmentResult>().ToTable("assessment_results");
|
||
|
||
// 忽略导航属性
|
||
modelBuilder.Entity<AssessmentRecord>().Ignore(e => e.AssessmentType);
|
||
modelBuilder.Entity<Question>().Ignore(e => e.AssessmentType);
|
||
modelBuilder.Entity<AssessmentAnswer>().Ignore(e => e.Record);
|
||
modelBuilder.Entity<AssessmentAnswer>().Ignore(e => e.Question);
|
||
modelBuilder.Entity<ReportCategory>().Ignore(e => e.AssessmentType);
|
||
modelBuilder.Entity<AssessmentResult>().Ignore(e => e.Record);
|
||
modelBuilder.Entity<AssessmentResult>().Ignore(e => e.Category);
|
||
}
|
||
}
|