🍒 feat(Test): 增加测试项目

This commit is contained in:
喵你个汪呀 2025-08-30 00:02:41 +08:00
parent 4e2b80a261
commit 85af7edee2
5 changed files with 337 additions and 0 deletions

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<NoWarn>1701;1702;1591;8632</NoWarn>
<DocumentationFile></DocumentationFile>
<ImplicitUsings>enable</ImplicitUsings>
<PreserveCompilationContext>true</PreserveCompilationContext>
<Nullable>disable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<Copyright>Admin.NET</Copyright>
<Description>Admin.NET 通用权限开发平台</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Furion.Xunit" Version="4.9.7.114" />
<PackageReference Include="Furion.Pure" Version="4.9.7.114">
<ExcludeAssets>compile</ExcludeAssets>
</PackageReference>
<PackageReference Include="xunit.assert" Version="2.9.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Admin.NET.Web.Core\Admin.NET.Web.Core.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,214 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Test.Attribute;
public class RequiredIFTest
{
#region
private static ValidationResult ValidateProperty(object instance, string propertyName, object value)
{
var context = new ValidationContext(instance) { MemberName = propertyName };
var property = instance.GetType().GetProperty(propertyName);
return property?.GetCustomAttribute<ValidationAttribute>()?.GetValidationResult(value, context);
}
private static bool IsValid(object instance) => Validator.TryValidateObject(instance, new ValidationContext(instance), null, true);
#endregion
#region 1 [RequiredIF(nameof(TarProperty), 1)]
[Theory]
[InlineData(1, null, false)] // TarProperty=1, SomeProperty=null → 失败
[InlineData(1, "", false)] // TarProperty=1, SomeProperty="" → 失败
[InlineData(1, " ", false)] // TarProperty=1, SomeProperty=" " → 失败
[InlineData(1, "a", true)] // TarProperty=1, SomeProperty="a" → 成功
[InlineData(2, null, true)] // TarProperty≠1, SomeProperty=null → 成功(不触发)
public void RequiredIF_Equal_WhenTargetIsValue_ShouldRequire(int tarProperty, string someProperty, bool expectedValid)
{
var model = new RequiredIfTest1Input { TarProperty = tarProperty, SomeProperty = someProperty };
var result = ValidateProperty(model, nameof(model.SomeProperty), someProperty);
Assert.Equal(expectedValid, result == ValidationResult.Success);
}
private class RequiredIfTest1Input
{
public int TarProperty { get; set; }
[RequiredIF(nameof(TarProperty), 1, ErrorMessage = "TarProperty为1时SomeProperty不能为空")]
public string SomeProperty { get; set; }
}
#endregion
#region 2 [RequiredIF(nameof(TarProperty), 1, Operator.NotEqual)]
[Theory]
[InlineData(1, "a", true)] // TarProperty=1 → 不触发必填
[InlineData(2, null, false)] // TarProperty≠1, SomeProperty=null → 必填失败
[InlineData(0, "", false)] // TarProperty≠1, SomeProperty="" → 失败
[InlineData(3, "ok", true)] // TarProperty≠1, SomeProperty=ok → 成功
public void RequiredIF_NotEqual_WhenTargetIsNotValue_ShouldRequire(int tarProperty, string someProperty, bool expectedValid)
{
var model = new RequiredIfTest2Input { TarProperty = tarProperty, SomeProperty = someProperty };
var result = ValidateProperty(model, nameof(model.SomeProperty), someProperty);
Assert.Equal(expectedValid, result == ValidationResult.Success);
}
private class RequiredIfTest2Input
{
public int TarProperty { get; set; }
[RequiredIF(nameof(TarProperty), 1, Operator.NotEqual, ErrorMessage = "TarProperty不为1时SomeProperty不能为空")]
public string SomeProperty { get; set; }
}
#endregion
#region 3 [RequiredIF(nameof(TarProperty), new[]{1,2}, Operator.Contains)]
[Theory]
[InlineData(new int[] { 1 }, null, false)]
[InlineData(new int[] { 2 }, "", false)]
[InlineData(new int[] { 1, 3 }, "x", true)]
[InlineData(new int[] { 3, 4 }, null, true)] // 不包含1或2 → 不必填
public void RequiredIF_Contains_WhenTargetContainsValue_ShouldRequire(int[] tarProperty, string someProperty, bool expectedValid)
{
var model = new RequiredIfTest3Input { TarProperty = tarProperty, SomeProperty = someProperty };
var result = ValidateProperty(model, nameof(model.SomeProperty), someProperty);
Assert.Equal(expectedValid, result == ValidationResult.Success);
}
private class RequiredIfTest3Input
{
public int[] TarProperty { get; set; }
[RequiredIF(nameof(TarProperty), new object[] { 1, 2 }, Operator.Contains, ErrorMessage = "TarProperty包含1或2时SomeProperty不能为空")]
public string SomeProperty { get; set; }
}
#endregion
#region 4 [RequiredIF(nameof(TarProperty), new[]{1,2}, Operator.NotContains)]
[Theory]
[InlineData(new[] { 1 }, null, true)] // 包含1 → 不必填
[InlineData(new[] { 2 }, "", true)] // 包含2 → 不必填
[InlineData(new[] { 3, 4 }, null, false)] // 不包含1,2 → 必填
[InlineData(new[] { 3, 4 }, "ok", true)] // 不包含1,2 → 必填
public void RequiredIF_NotContains_WhenTargetNotContainsValue_ShouldRequire(int[] tarProperty, string someProperty, bool expectedValid)
{
var result = IsValid(new RequiredIfTest4Input { TarProperty = tarProperty, SomeProperty = someProperty });
Assert.Equal(expectedValid, result);
}
private class RequiredIfTest4Input
{
public int[] TarProperty { get; set; }
[RequiredIF(nameof(TarProperty), new object[] { 1, 2 }, Operator.NotContains, ErrorMessage = "TarProperty不包含1和2时SomeProperty不能为空")]
public string SomeProperty { get; set; }
}
#endregion
#region longstringIList
[Theory]
[InlineData(0L, null, false)] // long=0 → 必填,无值 → 失败
[InlineData(1L, null, true)] // long=1 ≠ 0 → 不必填
[InlineData(0L, "a", true)] // long=0 → 必填,有值 → 成功
public void RequiredIF_WhenTargetIsLong_ShouldTreatZeroAsEmpty(long tarProperty, string someProperty, bool expectedValid)
{
var model = new RequiredIfTestLongInput { TarProperty = tarProperty, SomeProperty = someProperty };
var result = ValidateProperty(model, nameof(model.SomeProperty), someProperty);
Assert.Equal(expectedValid, result == ValidationResult.Success);
}
private class RequiredIfTestLongInput
{
public long TarProperty { get; set; }
[RequiredIF(nameof(TarProperty), ErrorMessage = "TarProperty为0时SomeProperty不能为空")]
public string SomeProperty { get; set; }
}
[Theory]
[InlineData(new string[] { }, null, false)] // 空集合 → 必填
[InlineData(new[] { "a" }, null, true)] // 非空集合 → 非必填
public void RequiredIF_WhenTargetIsList_ShouldTreatEmptyAsNull(string[] tarProperty, string someProperty, bool expectedValid)
{
var model = new RequiredIfTestListInput { TarProperty = tarProperty, SomeProperty = someProperty };
var result = ValidateProperty(model, nameof(model.SomeProperty), someProperty);
Assert.Equal(expectedValid, result == ValidationResult.Success);
}
private class RequiredIfTestListInput
{
public string[] TarProperty { get; set; }
[RequiredIF(nameof(TarProperty), ErrorMessage = "TarProperty为空时SomeProperty不能为空")]
public string SomeProperty { get; set; }
}
#endregion
#region null比较IComparable
[Fact]
public void RequiredIF_WhenTargetPropertyNotFound_ShouldReturnError()
{
var model = new RequiredIfTestInvalidProperty { SomeProperty = null };
var result = ValidateProperty(model, nameof(model.SomeProperty), null);
Assert.IsType<ValidationResult>(result);
Assert.Contains("找不到属性", result.ErrorMessage);
}
private class RequiredIfTestInvalidProperty
{
[RequiredIF("NonExistentProperty", "test")]
public string SomeProperty { get; set; }
}
[Theory]
[InlineData(null, null, Operator.Equal, false)]
[InlineData(null, "a", Operator.Equal, false)]
[InlineData("a", null, Operator.NotEqual, true)]
[InlineData(null, null, Operator.NotEqual, true)]
public void RequiredIF_NullComparison_ShouldWork(object source, object target, Operator op, bool expectedSuccess)
{
var model = new RequiredIfTestNullInput { Source = source, TargetValue = target };
var attr = new RequiredIFAttribute(nameof(RequiredIfTestNullInput.TargetValue), target, op);
var result = attr.GetValidationResult(source, new ValidationContext(model) { MemberName = nameof(model.Source) });
Assert.Equal(expectedSuccess, result == ValidationResult.Success);
}
private class RequiredIfTestNullInput
{
public object Source { get; set; }
public object TargetValue { get; set; }
}
[Theory]
[InlineData(null, 4, Operator.GreaterThan, false)] // [RequiredIF(nameof(TargetValue), 3, Operator.GreaterThan))]
[InlineData(1, 4, Operator.GreaterThan, true)] // [RequiredIF(nameof(TargetValue), 3, Operator.GreaterThan))]
[InlineData(null, 3, Operator.GreaterThanOrEqual, false)] // [RequiredIF(nameof(TargetValue), 3, Operator.GreaterThanOrEqual))]
[InlineData(null, 2, Operator.LessThan, false)] // [RequiredIF(nameof(TargetValue), 3, Operator.LessThan))]
public void RequiredIF_ComparableTypes_ShouldSupportComparison(int? source, int target, Operator op, bool expectedSuccess)
{
var model = new RequiredIfTestComparable { Source = source, TargetValue = target };
var attr = new RequiredIFAttribute(nameof(RequiredIfTestComparable.TargetValue), 3, op);
var result = attr.GetValidationResult(source, new ValidationContext(model) { MemberName = nameof(model.Source) });
Assert.Equal(expectedSuccess, result == ValidationResult.Success);
}
private class RequiredIfTestComparable
{
public int? Source { get; set; }
public int TargetValue { get; set; }
}
#endregion
}

View File

@ -0,0 +1,65 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
global using Admin.NET.Core.Service;
global using Admin.NET.Core;
global using Furion;
global using Furion.ConfigurableOptions;
global using Furion.DatabaseAccessor;
global using Furion.DataEncryption;
global using Furion.DataValidation;
global using Furion.DependencyInjection;
global using Furion.DynamicApiController;
global using Furion.EventBus;
global using Furion.FriendlyException;
global using Furion.HttpRemote;
global using Furion.JsonSerialization;
global using Furion.Logging;
global using Furion.Logging.Extensions;
global using Furion.Schedule;
global using Furion.UnifyResult;
global using Furion.ViewEngine;
global using Magicodes.ExporterAndImporter.Core;
global using Magicodes.ExporterAndImporter.Core.Extension;
global using Magicodes.ExporterAndImporter.Excel;
global using Mapster;
global using Microsoft.AspNetCore.Authorization;
global using Microsoft.AspNetCore.Http;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.AspNetCore.Mvc.Filters;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.Options;
global using Microsoft.SemanticKernel;
global using NewLife;
global using NewLife.Caching;
global using Newtonsoft.Json.Linq;
global using SKIT.FlurlHttpClient;
global using SKIT.FlurlHttpClient.Wechat.Api;
global using SKIT.FlurlHttpClient.Wechat.Api.Models;
global using SKIT.FlurlHttpClient.Wechat.TenpayV3;
global using SKIT.FlurlHttpClient.Wechat.TenpayV3.Events;
global using SKIT.FlurlHttpClient.Wechat.TenpayV3.Models;
global using SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings;
global using SqlSugar;
global using System.Collections;
global using System.Collections.Concurrent;
global using System.ComponentModel;
global using System.ComponentModel.DataAnnotations;
global using System.Data;
global using System.Diagnostics;
global using System.Linq.Dynamic.Core;
global using System.Linq.Expressions;
global using System.Reflection;
global using System.Runtime.InteropServices;
global using System.Text;
global using System.Text.RegularExpressions;
global using System.Web;
global using UAParser;
global using Xunit;
global using Yitter.IdGenerator;

View File

@ -0,0 +1,24 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
// 配置启动类类型,第一个参数是 TestProgram 类完整限定名,第二个参数是当前项目程序集名称
using Furion.Xunit;
using Xunit.Abstractions;
[assembly: TestFramework("Admin.NET.Test.TestProgram", "Admin.NET.Test")]
namespace Admin.NET.Test;
/// <summary>
/// 单元测试启动类
/// </summary>
public class TestProgram : TestStartup
{
public TestProgram(IMessageSink messageSink) : base(messageSink)
{
Serve.RunNative();
}
}

View File

@ -32,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.PaddleOCR"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.WorkWeixin", "Plugins\Admin.NET.Plugin.WorkWeixin\Admin.NET.Plugin.WorkWeixin.csproj", "{12998618-A875-4580-B5B1-0CC50CE85F27}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Test", "Admin.NET.Test\Admin.NET.Test.csproj", "{8F4A19E9-EEBC-483A-A78B-06F1CE852C7A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -82,6 +84,10 @@ Global
{12998618-A875-4580-B5B1-0CC50CE85F27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12998618-A875-4580-B5B1-0CC50CE85F27}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12998618-A875-4580-B5B1-0CC50CE85F27}.Release|Any CPU.Build.0 = Release|Any CPU
{8F4A19E9-EEBC-483A-A78B-06F1CE852C7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8F4A19E9-EEBC-483A-A78B-06F1CE852C7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F4A19E9-EEBC-483A-A78B-06F1CE852C7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F4A19E9-EEBC-483A-A78B-06F1CE852C7A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE