using FsCheck; using FsCheck.Xunit; using HoneyBox.Admin.Services; using Microsoft.Extensions.Caching.Memory; using Xunit; namespace HoneyBox.Tests.Services; /// /// 验证码服务属性测试 /// Feature: admin-system, Property 10: Captcha code characteristics /// Validates: Requirements 14.2 /// public class CaptchaServicePropertyTests { private readonly IMemoryCache _cache; private readonly CaptchaService _captchaService; public CaptchaServicePropertyTests() { _cache = new MemoryCache(new MemoryCacheOptions()); _captchaService = new CaptchaService(_cache); } /// /// 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 /// [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; } /// /// Property 10: Captcha code characteristics - Code format validation /// The captcha code stored in cache should be 4-6 alphanumeric characters. /// Validates: Requirements 14.2 /// [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; } /// /// 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 /// [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; } /// /// Property 13: Captcha single-use enforcement - Failed validation also removes captcha /// Even when validation fails, the captcha should be removed. /// Validates: Requirements 14.6 /// [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; } /// /// Captcha validation should be case-insensitive /// Validates: Requirements 14.2 /// [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; } /// /// Invalid captcha key should fail validation /// Validates: Requirements 14.5 /// [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; } /// /// Empty or null inputs should fail validation /// Validates: Requirements 14.5 /// [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("", "")); } }