294 lines
11 KiB
C#
294 lines
11 KiB
C#
using FsCheck;
|
|
using FsCheck.Xunit;
|
|
using NSubstitute;
|
|
using Microsoft.Extensions.Logging;
|
|
using XiangYi.Application.Services;
|
|
using XiangYi.Core.Entities.Admin;
|
|
using XiangYi.Core.Interfaces;
|
|
|
|
namespace XiangYi.Application.Tests.Services;
|
|
|
|
/// <summary>
|
|
/// AdminLogService属性测试
|
|
/// </summary>
|
|
public class AdminLogServicePropertyTests
|
|
{
|
|
/// <summary>
|
|
/// **Feature: backend-api, Property 22: 操作日志记录**
|
|
/// **Validates: Requirements 13.5**
|
|
///
|
|
/// *For any* 管理员执行的敏感操作(增删改), 应记录操作日志包含操作人、操作类型、操作时间
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public Property LogOperation_ShouldContainRequiredFields()
|
|
{
|
|
return Prop.ForAll(
|
|
Arb.Default.PositiveInt(),
|
|
adminIdArb =>
|
|
{
|
|
var adminUserId = (long)adminIdArb.Get;
|
|
var adminUsername = $"admin_{adminUserId}";
|
|
var module = "TestModule";
|
|
var action = "Create";
|
|
|
|
// Arrange
|
|
AdminOperationLog? capturedLog = null;
|
|
var mockRepository = Substitute.For<IRepository<AdminOperationLog>>();
|
|
mockRepository.AddAsync(Arg.Any<AdminOperationLog>())
|
|
.Returns(callInfo =>
|
|
{
|
|
capturedLog = callInfo.Arg<AdminOperationLog>();
|
|
capturedLog.Id = 1; // Simulate ID assignment
|
|
return Task.FromResult(capturedLog);
|
|
});
|
|
|
|
var mockLogger = Substitute.For<ILogger<AdminLogService>>();
|
|
var service = new AdminLogService(mockRepository, mockLogger);
|
|
|
|
// Act
|
|
var result = service.LogOperationAsync(
|
|
adminUserId,
|
|
adminUsername,
|
|
module,
|
|
action,
|
|
description: "Test operation",
|
|
status: 1
|
|
).Result;
|
|
|
|
// Assert
|
|
if (capturedLog == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// 1. 操作人ID必须正确记录
|
|
var hasCorrectAdminId = capturedLog.AdminUserId == adminUserId;
|
|
|
|
// 2. 操作人用户名必须正确记录
|
|
var hasCorrectUsername = capturedLog.AdminUsername == adminUsername;
|
|
|
|
// 3. 操作模块必须正确记录
|
|
var hasCorrectModule = capturedLog.Module == module;
|
|
|
|
// 4. 操作类型必须正确记录
|
|
var hasCorrectAction = capturedLog.Action == action;
|
|
|
|
// 5. 状态必须正确记录
|
|
var hasCorrectStatus = capturedLog.Status == 1;
|
|
|
|
return hasCorrectAdminId && hasCorrectUsername && hasCorrectModule && hasCorrectAction && hasCorrectStatus;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 操作日志应记录所有可选字段
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public Property LogOperation_ShouldRecordOptionalFields()
|
|
{
|
|
return Prop.ForAll(
|
|
Arb.Default.PositiveInt(),
|
|
adminIdArb =>
|
|
{
|
|
var adminUserId = (long)adminIdArb.Get;
|
|
var requestUrl = $"/api/admin/test/{adminUserId}";
|
|
var ip = "192.168.1.1";
|
|
|
|
// Arrange
|
|
AdminOperationLog? capturedLog = null;
|
|
var mockRepository = Substitute.For<IRepository<AdminOperationLog>>();
|
|
mockRepository.AddAsync(Arg.Any<AdminOperationLog>())
|
|
.Returns(callInfo =>
|
|
{
|
|
capturedLog = callInfo.Arg<AdminOperationLog>();
|
|
capturedLog.Id = 1;
|
|
return Task.FromResult(capturedLog);
|
|
});
|
|
|
|
var mockLogger = Substitute.For<ILogger<AdminLogService>>();
|
|
var service = new AdminLogService(mockRepository, mockLogger);
|
|
|
|
// Act
|
|
var result = service.LogOperationAsync(
|
|
adminUserId,
|
|
"testuser",
|
|
"TestModule",
|
|
"Create",
|
|
description: "Test description",
|
|
requestUrl: requestUrl,
|
|
requestMethod: "POST",
|
|
requestParams: "{\"test\":1}",
|
|
responseResult: "{\"success\":true}",
|
|
targetId: "123",
|
|
ip: ip,
|
|
userAgent: "TestAgent",
|
|
executeTime: 100,
|
|
status: 1
|
|
).Result;
|
|
|
|
// Assert
|
|
if (capturedLog == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// 验证所有可选字段都被正确记录
|
|
var hasDescription = capturedLog.Description == "Test description";
|
|
var hasRequestUrl = capturedLog.RequestUrl == requestUrl;
|
|
var hasRequestMethod = capturedLog.RequestMethod == "POST";
|
|
var hasRequestParams = capturedLog.RequestParams == "{\"test\":1}";
|
|
var hasResponseResult = capturedLog.ResponseResult == "{\"success\":true}";
|
|
var hasTargetId = capturedLog.TargetId == "123";
|
|
var hasIp = capturedLog.Ip == ip;
|
|
var hasUserAgent = capturedLog.UserAgent == "TestAgent";
|
|
var hasExecuteTime = capturedLog.ExecuteTime == 100;
|
|
|
|
return hasDescription && hasRequestUrl && hasRequestMethod &&
|
|
hasRequestParams && hasResponseResult && hasTargetId &&
|
|
hasIp && hasUserAgent && hasExecuteTime;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 失败操作应正确记录错误信息
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public Property LogOperation_ShouldRecordErrorInfoForFailedOperations()
|
|
{
|
|
return Prop.ForAll(
|
|
Arb.Default.PositiveInt(),
|
|
adminIdArb =>
|
|
{
|
|
var adminUserId = (long)adminIdArb.Get;
|
|
var errorMsg = $"Error occurred for admin {adminUserId}";
|
|
|
|
// Arrange
|
|
AdminOperationLog? capturedLog = null;
|
|
var mockRepository = Substitute.For<IRepository<AdminOperationLog>>();
|
|
mockRepository.AddAsync(Arg.Any<AdminOperationLog>())
|
|
.Returns(callInfo =>
|
|
{
|
|
capturedLog = callInfo.Arg<AdminOperationLog>();
|
|
capturedLog.Id = 1;
|
|
return Task.FromResult(capturedLog);
|
|
});
|
|
|
|
var mockLogger = Substitute.For<ILogger<AdminLogService>>();
|
|
var service = new AdminLogService(mockRepository, mockLogger);
|
|
|
|
// Act - 记录失败操作
|
|
var result = service.LogOperationAsync(
|
|
adminUserId,
|
|
"testuser",
|
|
"TestModule",
|
|
"Delete",
|
|
description: "Failed operation",
|
|
status: 2, // 失败状态
|
|
errorMsg: errorMsg
|
|
).Result;
|
|
|
|
// Assert
|
|
if (capturedLog == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// 1. 状态应为失败(2)
|
|
var hasFailedStatus = capturedLog.Status == 2;
|
|
|
|
// 2. 错误信息应正确记录
|
|
var hasErrorMsg = capturedLog.ErrorMsg == errorMsg;
|
|
|
|
return hasFailedStatus && hasErrorMsg;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 日志记录应返回有效的日志ID
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public Property LogOperation_ShouldReturnValidLogId()
|
|
{
|
|
return Prop.ForAll(
|
|
Arb.Default.PositiveInt(),
|
|
adminIdArb =>
|
|
{
|
|
var adminUserId = (long)adminIdArb.Get;
|
|
var expectedLogId = adminUserId + 1000; // 模拟生成的日志ID
|
|
|
|
// Arrange
|
|
var mockRepository = Substitute.For<IRepository<AdminOperationLog>>();
|
|
mockRepository.AddAsync(Arg.Any<AdminOperationLog>())
|
|
.Returns(callInfo =>
|
|
{
|
|
var log = callInfo.Arg<AdminOperationLog>();
|
|
log.Id = expectedLogId;
|
|
return Task.FromResult(log);
|
|
});
|
|
|
|
var mockLogger = Substitute.For<ILogger<AdminLogService>>();
|
|
var service = new AdminLogService(mockRepository, mockLogger);
|
|
|
|
// Act
|
|
var result = service.LogOperationAsync(
|
|
adminUserId,
|
|
"testuser",
|
|
"TestModule",
|
|
"Update"
|
|
).Result;
|
|
|
|
// Assert - 返回的日志ID应该是有效的正数
|
|
return result == expectedLogId && result > 0;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 敏感操作类型(增删改)应被正确记录
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public Property LogOperation_ShouldRecordSensitiveOperationTypes()
|
|
{
|
|
var sensitiveActions = new[] { "Create", "Update", "Delete", "Add", "Modify", "Remove" };
|
|
|
|
return Prop.ForAll(
|
|
Arb.Default.PositiveInt(),
|
|
adminIdArb =>
|
|
{
|
|
var adminUserId = (long)adminIdArb.Get;
|
|
var actionIndex = (int)(adminUserId % sensitiveActions.Length);
|
|
var action = sensitiveActions[actionIndex];
|
|
|
|
// Arrange
|
|
AdminOperationLog? capturedLog = null;
|
|
var mockRepository = Substitute.For<IRepository<AdminOperationLog>>();
|
|
mockRepository.AddAsync(Arg.Any<AdminOperationLog>())
|
|
.Returns(callInfo =>
|
|
{
|
|
capturedLog = callInfo.Arg<AdminOperationLog>();
|
|
capturedLog.Id = 1;
|
|
return Task.FromResult(capturedLog);
|
|
});
|
|
|
|
var mockLogger = Substitute.For<ILogger<AdminLogService>>();
|
|
var service = new AdminLogService(mockRepository, mockLogger);
|
|
|
|
// Act
|
|
var result = service.LogOperationAsync(
|
|
adminUserId,
|
|
"testuser",
|
|
"UserManagement",
|
|
action
|
|
).Result;
|
|
|
|
// Assert
|
|
if (capturedLog == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// 敏感操作类型应被正确记录
|
|
return capturedLog.Action == action && sensitiveActions.Contains(capturedLog.Action);
|
|
});
|
|
}
|
|
}
|