493 lines
16 KiB
C#
493 lines
16 KiB
C#
using FsCheck;
|
||
using FsCheck.Xunit;
|
||
using Xunit;
|
||
using XiangYi.Application.Services;
|
||
using XiangYi.Core.Entities.Biz;
|
||
using XiangYi.Core.Enums;
|
||
|
||
namespace XiangYi.Application.Tests.Services;
|
||
|
||
/// <summary>
|
||
/// ChatService属性测试 - 消息发送持久化
|
||
/// </summary>
|
||
public class MessagePersistencePropertyTests
|
||
{
|
||
/// <summary>
|
||
/// **Feature: backend-api, Property 14: 消息发送持久化**
|
||
/// **Validates: Requirements 6.1, 6.2**
|
||
///
|
||
/// *For any* 发送的聊天消息, 应被正确保存到数据库且可通过消息列表查询到
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Message_ShouldBePersisted_AndQueryable()
|
||
{
|
||
// 生成消息ID(1-10000)
|
||
var messageIdArb = Gen.Choose(1, 10000);
|
||
// 生成消息类型(文本或语音)
|
||
var messageTypeArb = Gen.Elements((int)MessageType.Text, (int)MessageType.Voice);
|
||
// 生成非空消息内容
|
||
var contentArb = Gen.Elements("Hello", "你好", "Test message", "测试消息", "Hi there");
|
||
|
||
return Prop.ForAll(
|
||
messageIdArb.ToArbitrary(),
|
||
messageTypeArb.ToArbitrary(),
|
||
contentArb.ToArbitrary(),
|
||
(messageId, messageType, content) =>
|
||
{
|
||
var voiceUrl = "https://example.com/voice.mp3";
|
||
|
||
// Arrange - 创建消息
|
||
var message = new ChatMessage
|
||
{
|
||
Id = messageId,
|
||
SessionId = 1,
|
||
SenderId = 1,
|
||
ReceiverId = 2,
|
||
MessageType = messageType,
|
||
Content = messageType == (int)MessageType.Text ? content : null,
|
||
VoiceUrl = messageType == (int)MessageType.Voice ? voiceUrl : null,
|
||
Status = (int)MessageStatus.Normal,
|
||
IsRead = false,
|
||
CreateTime = DateTime.Now
|
||
};
|
||
|
||
// 模拟消息列表(持久化后的状态)
|
||
var messages = new List<ChatMessage> { message };
|
||
|
||
// Act - 验证消息是否应该被持久化
|
||
var shouldPersist = ChatService.ShouldMessageBePersisted(
|
||
messageType,
|
||
message.Content,
|
||
message.VoiceUrl);
|
||
|
||
// 验证消息是否可以在列表中查询到
|
||
var isInList = ChatService.IsMessageInList(messages, messageId);
|
||
|
||
// Assert - 如果消息应该被持久化,则应该可以在列表中查询到
|
||
return shouldPersist && isInList;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 文本消息 - 有内容时应该被持久化
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property TextMessage_WithContent_ShouldBePersisted()
|
||
{
|
||
var contentArb = Gen.Elements("Hello", "你好", "Test", "消息内容", "Some text");
|
||
|
||
return Prop.ForAll(
|
||
contentArb.ToArbitrary(),
|
||
content =>
|
||
{
|
||
var shouldPersist = ChatService.ShouldMessageBePersisted(
|
||
(int)MessageType.Text,
|
||
content,
|
||
null);
|
||
|
||
return shouldPersist;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 文本消息 - 空内容时不应该被持久化
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property TextMessage_EmptyContent_ShouldNotBePersisted()
|
||
{
|
||
var emptyContentArb = Gen.Elements("", " ", null, "\t", "\n");
|
||
|
||
return Prop.ForAll(
|
||
emptyContentArb.ToArbitrary(),
|
||
emptyContent =>
|
||
{
|
||
var shouldPersist = ChatService.ShouldMessageBePersisted(
|
||
(int)MessageType.Text,
|
||
emptyContent,
|
||
null);
|
||
|
||
return !shouldPersist;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 语音消息 - 有URL时应该被持久化
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property VoiceMessage_WithUrl_ShouldBePersisted()
|
||
{
|
||
var voiceUrlArb = Gen.Elements(
|
||
"https://example.com/voice1.mp3",
|
||
"https://example.com/voice2.mp3",
|
||
"https://storage.example.com/audio/123.wav");
|
||
|
||
return Prop.ForAll(
|
||
voiceUrlArb.ToArbitrary(),
|
||
voiceUrl =>
|
||
{
|
||
var shouldPersist = ChatService.ShouldMessageBePersisted(
|
||
(int)MessageType.Voice,
|
||
null,
|
||
voiceUrl);
|
||
|
||
return shouldPersist;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 语音消息 - 无URL时不应该被持久化
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property VoiceMessage_EmptyUrl_ShouldNotBePersisted()
|
||
{
|
||
var emptyUrlArb = Gen.Elements("", " ", null, "\t");
|
||
|
||
return Prop.ForAll(
|
||
emptyUrlArb.ToArbitrary(),
|
||
emptyUrl =>
|
||
{
|
||
var shouldPersist = ChatService.ShouldMessageBePersisted(
|
||
(int)MessageType.Voice,
|
||
null,
|
||
emptyUrl);
|
||
|
||
return !shouldPersist;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 消息查询 - 正常状态的消息应该可以查询到
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Message_NormalStatus_ShouldBeQueryable()
|
||
{
|
||
var messageIdArb = Gen.Choose(1, 10000);
|
||
|
||
return Prop.ForAll(
|
||
messageIdArb.ToArbitrary(),
|
||
messageId =>
|
||
{
|
||
var messages = new List<ChatMessage>
|
||
{
|
||
new ChatMessage
|
||
{
|
||
Id = messageId,
|
||
Status = (int)MessageStatus.Normal,
|
||
CreateTime = DateTime.Now
|
||
}
|
||
};
|
||
|
||
var isInList = ChatService.IsMessageInList(messages, messageId);
|
||
return isInList;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 消息查询 - 已删除的消息不应该被查询到
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Message_DeletedStatus_ShouldNotBeQueryable()
|
||
{
|
||
var messageIdArb = Gen.Choose(1, 10000);
|
||
|
||
return Prop.ForAll(
|
||
messageIdArb.ToArbitrary(),
|
||
messageId =>
|
||
{
|
||
var messages = new List<ChatMessage>
|
||
{
|
||
new ChatMessage
|
||
{
|
||
Id = messageId,
|
||
Status = (int)MessageStatus.Deleted,
|
||
CreateTime = DateTime.Now
|
||
}
|
||
};
|
||
|
||
var isInList = ChatService.IsMessageInList(messages, messageId);
|
||
return !isInList;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 消息查询 - 已撤回的消息不应该被查询到
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Message_RecalledStatus_ShouldNotBeQueryable()
|
||
{
|
||
var messageIdArb = Gen.Choose(1, 10000);
|
||
|
||
return Prop.ForAll(
|
||
messageIdArb.ToArbitrary(),
|
||
messageId =>
|
||
{
|
||
var messages = new List<ChatMessage>
|
||
{
|
||
new ChatMessage
|
||
{
|
||
Id = messageId,
|
||
Status = (int)MessageStatus.Recalled,
|
||
CreateTime = DateTime.Now
|
||
}
|
||
};
|
||
|
||
var isInList = ChatService.IsMessageInList(messages, messageId);
|
||
return !isInList;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 消息查询 - 不存在的消息ID不应该被查询到
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Message_NonExistentId_ShouldNotBeQueryable()
|
||
{
|
||
var existingIdArb = Gen.Choose(1, 1000);
|
||
var nonExistentIdArb = Gen.Choose(1001, 2000);
|
||
|
||
return Prop.ForAll(
|
||
existingIdArb.ToArbitrary(),
|
||
nonExistentIdArb.ToArbitrary(),
|
||
(existingId, nonExistentId) =>
|
||
{
|
||
var messages = new List<ChatMessage>
|
||
{
|
||
new ChatMessage
|
||
{
|
||
Id = existingId,
|
||
Status = (int)MessageStatus.Normal,
|
||
CreateTime = DateTime.Now
|
||
}
|
||
};
|
||
|
||
var existingInList = ChatService.IsMessageInList(messages, existingId);
|
||
var nonExistentInList = ChatService.IsMessageInList(messages, nonExistentId);
|
||
|
||
return existingInList && !nonExistentInList;
|
||
});
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// ChatService属性测试 - 交换请求状态流转
|
||
/// </summary>
|
||
public class ExchangeStatusTransitionPropertyTests
|
||
{
|
||
/// <summary>
|
||
/// **Feature: backend-api, Property 15: 交换请求状态流转**
|
||
/// **Validates: Requirements 6.3, 6.4, 6.5**
|
||
///
|
||
/// *For any* 交换微信/照片请求, 状态应从"待响应"流转到"已同意"或"已拒绝"
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ExchangeRequest_StatusTransition_ShouldBeValid()
|
||
{
|
||
// 生成是否同意的布尔值
|
||
var isAgreedArb = Gen.Elements(true, false);
|
||
|
||
return Prop.ForAll(
|
||
isAgreedArb.ToArbitrary(),
|
||
isAgreed =>
|
||
{
|
||
// Arrange - 初始状态为待响应(0)
|
||
var initialStatus = 0;
|
||
|
||
// Act - 计算状态流转
|
||
var newStatus = ChatService.CalculateExchangeStatusTransition(initialStatus, isAgreed);
|
||
|
||
// Assert - 验证状态流转是否有效
|
||
var isValidTransition = ChatService.IsValidExchangeStatusTransition(initialStatus, newStatus);
|
||
var isCorrectStatus = ChatService.IsExchangeResponseStatusCorrect(initialStatus, isAgreed, newStatus);
|
||
|
||
return isValidTransition && isCorrectStatus;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 交换请求 - 同意后状态应为1
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ExchangeRequest_Agreed_StatusShouldBeOne()
|
||
{
|
||
return Prop.ForAll(
|
||
Arb.Default.PositiveInt(),
|
||
_ =>
|
||
{
|
||
var initialStatus = 0;
|
||
var newStatus = ChatService.CalculateExchangeStatusTransition(initialStatus, isAgreed: true);
|
||
return newStatus == 1;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 交换请求 - 拒绝后状态应为2
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ExchangeRequest_Rejected_StatusShouldBeTwo()
|
||
{
|
||
return Prop.ForAll(
|
||
Arb.Default.PositiveInt(),
|
||
_ =>
|
||
{
|
||
var initialStatus = 0;
|
||
var newStatus = ChatService.CalculateExchangeStatusTransition(initialStatus, isAgreed: false);
|
||
return newStatus == 2;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 交换请求 - 已响应的请求不能再次流转
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ExchangeRequest_AlreadyResponded_ShouldNotTransition()
|
||
{
|
||
// 生成已响应的状态(1或2)
|
||
var respondedStatusArb = Gen.Elements(1, 2);
|
||
var isAgreedArb = Gen.Elements(true, false);
|
||
|
||
return Prop.ForAll(
|
||
respondedStatusArb.ToArbitrary(),
|
||
isAgreedArb.ToArbitrary(),
|
||
(respondedStatus, isAgreed) =>
|
||
{
|
||
// Act - 尝试再次流转
|
||
var newStatus = ChatService.CalculateExchangeStatusTransition(respondedStatus, isAgreed);
|
||
|
||
// Assert - 应该返回-1表示无法流转
|
||
return newStatus == -1;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 交换请求 - 只有待响应状态可以流转
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ExchangeRequest_OnlyPendingCanTransition()
|
||
{
|
||
// 生成各种状态
|
||
var statusArb = Gen.Choose(-5, 5);
|
||
var isAgreedArb = Gen.Elements(true, false);
|
||
|
||
return Prop.ForAll(
|
||
statusArb.ToArbitrary(),
|
||
isAgreedArb.ToArbitrary(),
|
||
(status, isAgreed) =>
|
||
{
|
||
var newStatus = ChatService.CalculateExchangeStatusTransition(status, isAgreed);
|
||
|
||
// 只有状态为0时才能成功流转
|
||
if (status == 0)
|
||
{
|
||
return newStatus == (isAgreed ? 1 : 2);
|
||
}
|
||
else
|
||
{
|
||
return newStatus == -1;
|
||
}
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 交换请求 - 状态流转验证函数正确性
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ExchangeRequest_ValidationFunction_ShouldBeCorrect()
|
||
{
|
||
// 生成所有可能的状态组合
|
||
var fromStatusArb = Gen.Choose(-2, 5);
|
||
var toStatusArb = Gen.Choose(-2, 5);
|
||
|
||
return Prop.ForAll(
|
||
fromStatusArb.ToArbitrary(),
|
||
toStatusArb.ToArbitrary(),
|
||
(fromStatus, toStatus) =>
|
||
{
|
||
var isValid = ChatService.IsValidExchangeStatusTransition(fromStatus, toStatus);
|
||
|
||
// 只有从0到1或从0到2是有效的
|
||
var expectedValid = fromStatus == 0 && (toStatus == 1 || toStatus == 2);
|
||
|
||
return isValid == expectedValid;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 交换请求 - 响应状态正确性验证
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ExchangeRequest_ResponseStatusCorrectness()
|
||
{
|
||
var isAgreedArb = Gen.Elements(true, false);
|
||
var newStatusArb = Gen.Choose(0, 3);
|
||
|
||
return Prop.ForAll(
|
||
isAgreedArb.ToArbitrary(),
|
||
newStatusArb.ToArbitrary(),
|
||
(isAgreed, newStatus) =>
|
||
{
|
||
// 原始状态为待响应
|
||
var originalStatus = 0;
|
||
var isCorrect = ChatService.IsExchangeResponseStatusCorrect(originalStatus, isAgreed, newStatus);
|
||
|
||
// 验证正确性
|
||
var expectedCorrect = (isAgreed && newStatus == 1) || (!isAgreed && newStatus == 2);
|
||
|
||
return isCorrect == expectedCorrect;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 交换请求 - 非待响应状态的响应应该失败
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ExchangeRequest_NonPendingStatus_ResponseShouldFail()
|
||
{
|
||
// 生成非待响应状态
|
||
var nonPendingStatusArb = Gen.Elements(1, 2, 3, -1);
|
||
var isAgreedArb = Gen.Elements(true, false);
|
||
var newStatusArb = Gen.Choose(0, 3);
|
||
|
||
return Prop.ForAll(
|
||
nonPendingStatusArb.ToArbitrary(),
|
||
isAgreedArb.ToArbitrary(),
|
||
newStatusArb.ToArbitrary(),
|
||
(originalStatus, isAgreed, newStatus) =>
|
||
{
|
||
var isCorrect = ChatService.IsExchangeResponseStatusCorrect(originalStatus, isAgreed, newStatus);
|
||
|
||
// 非待响应状态的响应应该总是失败
|
||
return !isCorrect;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 交换请求 - 状态流转的一致性
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ExchangeRequest_StatusTransition_Consistency()
|
||
{
|
||
var isAgreedArb = Gen.Elements(true, false);
|
||
|
||
return Prop.ForAll(
|
||
isAgreedArb.ToArbitrary(),
|
||
isAgreed =>
|
||
{
|
||
// 从待响应状态开始
|
||
var initialStatus = 0;
|
||
|
||
// 计算新状态
|
||
var newStatus = ChatService.CalculateExchangeStatusTransition(initialStatus, isAgreed);
|
||
|
||
// 验证流转有效性
|
||
var isValidTransition = ChatService.IsValidExchangeStatusTransition(initialStatus, newStatus);
|
||
|
||
// 验证响应正确性
|
||
var isCorrectResponse = ChatService.IsExchangeResponseStatusCorrect(initialStatus, isAgreed, newStatus);
|
||
|
||
// 所有验证应该一致
|
||
return isValidTransition && isCorrectResponse;
|
||
});
|
||
}
|
||
}
|