HaniBlindBox/server/HoneyBox/tests/HoneyBox.Tests/Services/CaptchaServicePropertyTests.cs
2026-01-05 23:22:20 +08:00

215 lines
7.3 KiB
C#

using FsCheck;
using FsCheck.Xunit;
using HoneyBox.Admin.Services;
using Microsoft.Extensions.Caching.Memory;
using Xunit;
namespace HoneyBox.Tests.Services;
/// <summary>
/// 验证码服务属性测试
/// Feature: admin-system, Property 10: Captcha code characteristics
/// Validates: Requirements 14.2
/// </summary>
public class CaptchaServicePropertyTests
{
private readonly IMemoryCache _cache;
private readonly CaptchaService _captchaService;
public CaptchaServicePropertyTests()
{
_cache = new MemoryCache(new MemoryCacheOptions());
_captchaService = new CaptchaService(_cache);
}
/// <summary>
/// Property 10: Captcha code characteristics
/// For any generated captcha, the code SHALL be alphanumeric (letters and digits only)
/// and have a length between 4 and 6 characters inclusive.
/// Validates: Requirements 14.2
/// </summary>
[Property(MaxTest = 100)]
public bool GeneratedCaptchaShouldHaveValidFormat()
{
var result = _captchaService.Generate();
// CaptchaKey should not be empty
if (string.IsNullOrWhiteSpace(result.CaptchaKey))
return false;
// CaptchaImage should be a valid base64 PNG image
if (!result.CaptchaImage.StartsWith("data:image/png;base64,"))
return false;
// Verify the base64 part is valid
var base64Part = result.CaptchaImage.Substring("data:image/png;base64,".Length);
try
{
var bytes = Convert.FromBase64String(base64Part);
if (bytes.Length == 0)
return false;
}
catch
{
return false;
}
return true;
}
/// <summary>
/// Property 10: Captcha code characteristics - Code format validation
/// The captcha code stored in cache should be 4-6 alphanumeric characters.
/// Validates: Requirements 14.2
/// </summary>
[Property(MaxTest = 100)]
public bool CaptchaCodeShouldBeAlphanumericAndCorrectLength()
{
var result = _captchaService.Generate();
// Get the code from cache to verify its format
var cacheKey = "captcha:" + result.CaptchaKey;
if (!_cache.TryGetValue(cacheKey, out string? code))
return false;
if (string.IsNullOrEmpty(code))
return false;
// Length should be between 4 and 6
if (code.Length < 4 || code.Length > 6)
return false;
// All characters should be alphanumeric
foreach (var c in code)
{
if (!char.IsLetterOrDigit(c))
return false;
}
return true;
}
/// <summary>
/// Property 13: Captcha single-use enforcement
/// For any captcha code, after ONE validation attempt (whether successful or failed),
/// the captcha SHALL be removed from cache and subsequent validation attempts
/// with the same captcha key SHALL fail.
/// Validates: Requirements 14.6
/// </summary>
[Property(MaxTest = 100)]
public bool CaptchaShouldBeRemovedAfterValidation()
{
var result = _captchaService.Generate();
var cacheKey = "captcha:" + result.CaptchaKey;
// Get the actual code from cache
_cache.TryGetValue(cacheKey, out string? actualCode);
// First validation (with correct code) should succeed
var firstValidation = _captchaService.Validate(result.CaptchaKey, actualCode ?? "wrong");
// Second validation with same key should always fail (captcha removed)
var secondValidation = _captchaService.Validate(result.CaptchaKey, actualCode ?? "wrong");
// The captcha should be removed from cache after first validation
var stillInCache = _cache.TryGetValue(cacheKey, out _);
return !secondValidation && !stillInCache;
}
/// <summary>
/// Property 13: Captcha single-use enforcement - Failed validation also removes captcha
/// Even when validation fails, the captcha should be removed.
/// Validates: Requirements 14.6
/// </summary>
[Property(MaxTest = 100)]
public bool FailedValidationShouldAlsoRemoveCaptcha()
{
var result = _captchaService.Generate();
var cacheKey = "captcha:" + result.CaptchaKey;
// Validate with wrong code
var validation = _captchaService.Validate(result.CaptchaKey, "WRONGCODE");
// Validation should fail
if (validation)
return false;
// Captcha should be removed from cache
var stillInCache = _cache.TryGetValue(cacheKey, out _);
return !stillInCache;
}
/// <summary>
/// Captcha validation should be case-insensitive
/// Validates: Requirements 14.2
/// </summary>
[Property(MaxTest = 100)]
public bool CaptchaValidationShouldBeCaseInsensitive()
{
var result = _captchaService.Generate();
var cacheKey = "captcha:" + result.CaptchaKey;
// Get the actual code from cache
if (!_cache.TryGetValue(cacheKey, out string? actualCode) || string.IsNullOrEmpty(actualCode))
return false;
// Generate a new captcha for testing case insensitivity
var result2 = _captchaService.Generate();
var cacheKey2 = "captcha:" + result2.CaptchaKey;
if (!_cache.TryGetValue(cacheKey2, out string? actualCode2) || string.IsNullOrEmpty(actualCode2))
return false;
// Test with uppercase version
var upperValidation = _captchaService.Validate(result2.CaptchaKey, actualCode2.ToUpper());
// Generate another captcha for lowercase test
var result3 = _captchaService.Generate();
var cacheKey3 = "captcha:" + result3.CaptchaKey;
if (!_cache.TryGetValue(cacheKey3, out string? actualCode3) || string.IsNullOrEmpty(actualCode3))
return false;
// Test with lowercase version
var lowerValidation = _captchaService.Validate(result3.CaptchaKey, actualCode3.ToLower());
// Both should succeed (case insensitive)
return upperValidation && lowerValidation;
}
/// <summary>
/// Invalid captcha key should fail validation
/// Validates: Requirements 14.5
/// </summary>
[Property(MaxTest = 100)]
public bool InvalidCaptchaKeyShouldFailValidation(NonEmptyString randomKey, NonEmptyString randomCode)
{
// Random key that doesn't exist should fail
var validation = _captchaService.Validate(randomKey.Item, randomCode.Item);
return !validation;
}
/// <summary>
/// Empty or null inputs should fail validation
/// Validates: Requirements 14.5
/// </summary>
[Fact]
public void EmptyInputsShouldFailValidation()
{
var result = _captchaService.Generate();
// Empty key should fail
Assert.False(_captchaService.Validate("", "code"));
Assert.False(_captchaService.Validate(" ", "code"));
// Empty code should fail
Assert.False(_captchaService.Validate(result.CaptchaKey, ""));
Assert.False(_captchaService.Validate(result.CaptchaKey, " "));
// Both empty should fail
Assert.False(_captchaService.Validate("", ""));
}
}