215 lines
7.3 KiB
C#
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("", ""));
|
|
}
|
|
}
|