mi-assessment/server/MiAssessment/tests/MiAssessment.Tests/Services/AssessmentServicePropertyTests.cs
2026-02-09 14:45:06 +08:00

2078 lines
76 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}