using System; using Server; using Server.Engines.AdvancedSearch; using Xunit; namespace UOContent.Tests; public class AdvancedSearchUtilitiesTests { [Theory] [InlineData("abc")] // not a number -> was FormatException [InlineData("99999999999")] // overflows int -> was OverflowException [InlineData("0xZZ")] // bad hex -> was FormatException public void CompareValues_BadNumeric_ReturnsFalse_DoesNotThrow(string value) { var ex = Record.Exception(() => { var result = AdvancedSearchUtilities.CompareValues(typeof(int), 5, value, ">"); Assert.False(result); }); Assert.Null(ex); } [Theory] [InlineData("Bogus")] // not a member -> was ArgumentException [InlineData("onehandedxyz")] // not a member, even case-insensitively -> was ArgumentException public void CompareValues_BadEnum_ReturnsFalse_DoesNotThrow(string value) { var ex = Record.Exception(() => { var result = AdvancedSearchUtilities.CompareValues(typeof(Layer), (byte)Layer.OneHanded, value, "="); Assert.False(result); }); Assert.Null(ex); } [Fact] public void CompareValues_ValidEnum_IgnoresCase() { Assert.True(AdvancedSearchUtilities.CompareValues(typeof(Layer), (byte)Layer.OneHanded, "onehanded", "=")); } [Theory] // leaf value is "T"/"F"; evalLeaf returns leaf=="T" [InlineData("T", true)] [InlineData("F", false)] [InlineData("F@F|T", true)] // (F&&F)||T = T (buggy code gave F&&(F||T)=F) [InlineData("T|F@F", true)] // T||(F&&F) = T (buggy code gave (T||F)&&F=F) [InlineData("T@F", false)] [InlineData("T@T", true)] [InlineData("F|F", false)] public void EvaluateBoolean_Precedence(string expr, bool expected) { // State is unused here; the leaf evaluator just checks the span equals "T". var result = AdvancedSearchUtilities.EvaluateBoolean(expr, 0, static (_, leaf) => leaf.SequenceEqual("T")); Assert.Equal(expected, result); } [Fact] public void CompareValues_ReferenceType_EqualityByString_NoThrow() { // A reference-typed property (e.g. RootParent name-ish) compared with "=" should not throw, // and ordering operators must return false rather than throwing. var ex = Record.Exception(() => { Assert.False(AdvancedSearchUtilities.CompareValues(typeof(object), new object(), "whatever", ">")); }); Assert.Null(ex); } [Fact] public void CompareValues_TimeSpan_ParsesViaSpanParsable() { // TimeSpan is not IConvertible, so the old Convert.ChangeType fallback threw and silently // returned no-match. ISpanParsable parses it correctly. var prop = TimeSpan.FromMinutes(5); Assert.True(AdvancedSearchUtilities.CompareValues(typeof(TimeSpan), prop, "00:05:00", "=")); Assert.False(AdvancedSearchUtilities.CompareValues(typeof(TimeSpan), prop, "00:10:00", "=")); Assert.True(AdvancedSearchUtilities.CompareValues(typeof(TimeSpan), prop, "00:01:00", ">")); } [Fact] public void CompareValues_TimeSpan_BadInput_ReturnsFalse_NoThrow() { var ex = Record.Exception(() => Assert.False(AdvancedSearchUtilities.CompareValues(typeof(TimeSpan), TimeSpan.Zero, "notaspan", "="))); Assert.Null(ex); } [Fact] public void CompareValues_Guid_ValueTypeParsedViaTypes() { // Guid is a value type not named by the hot paths; it's parsed via Types (IParsable) and // compared by value. var g = Guid.Parse("00000000-0000-0000-0000-000000000001"); Assert.True(AdvancedSearchUtilities.CompareValues(typeof(Guid), g, "00000000-0000-0000-0000-000000000001", "=")); Assert.False(AdvancedSearchUtilities.CompareValues(typeof(Guid), g, "00000000-0000-0000-0000-000000000002", "=")); } // A reference type with a legacy RunUO-style static Parse(string) and NO IParsable<> interface — // the Faction/Town shape. Types must still discover its Parse by reflection. private sealed class LegacyParseType { public string Value { get; private init; } public static LegacyParseType Parse(string s) => new() { Value = s }; public override bool Equals(object obj) => obj is LegacyParseType o && o.Value == Value; public override int GetHashCode() => Value?.GetHashCode() ?? 0; } [Fact] public void CompareValues_LegacyParseString_ParsedViaTypes() { // Pre-IParsable types (only a static Parse(string)) must still be searchable: Types binds the // legacy Parse by reflection, so we compare against a real parsed instance, not the raw text. var prop = LegacyParseType.Parse("alpha"); Assert.True(AdvancedSearchUtilities.CompareValues(typeof(LegacyParseType), prop, "alpha", "=")); Assert.False(AdvancedSearchUtilities.CompareValues(typeof(LegacyParseType), prop, "beta", "=")); } }