File: Utils\SearchTextParserTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Aspire.Cli.Tests.Utils;
 
public class SearchTextParserTests
{
    #region ParseSearch - Basic
 
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void ParseSearch_EmptyOrNull_ReturnsEmpty(string? search)
    {
        var filter = SearchTextParser.ParseSearch(search);
        Assert.True(filter.IsEmpty);
    }
 
    [Fact]
    public void ParseSearch_SingleWord_ReturnsTextFragment()
    {
        var filter = SearchTextParser.ParseSearch("hello");
        Assert.Single(filter.TextFragments, "hello");
        Assert.Empty(filter.Qualifiers);
        Assert.Empty(filter.NegatedQualifiers);
    }
 
    [Fact]
    public void ParseSearch_MultipleWords_SplitsIntoFragments()
    {
        var filter = SearchTextParser.ParseSearch("hello world foo");
        Assert.Equal(["hello", "world", "foo"], filter.TextFragments);
    }
 
    [Fact]
    public void ParseSearch_QuotedText_TreatedAsSingleFragment()
    {
        var filter = SearchTextParser.ParseSearch("\"hello world\"");
        Assert.Single(filter.TextFragments, "hello world");
    }
 
    [Fact]
    public void ParseSearch_MixedQuotedAndUnquoted()
    {
        var filter = SearchTextParser.ParseSearch("error \"connection failed\" timeout");
        Assert.Equal(["error", "connection failed", "timeout"], filter.TextFragments);
    }
 
    #endregion
 
    #region ParseSearch - Qualifiers
 
    [Fact]
    public void ParseSearch_SimpleQualifier()
    {
        var filter = SearchTextParser.ParseSearch("severity:error");
        Assert.Empty(filter.TextFragments);
        Assert.Single(filter.Qualifiers);
        Assert.Equal("severity", filter.Qualifiers[0].Key);
        Assert.Equal("error", filter.Qualifiers[0].Value);
        Assert.Equal(ComparisonOperator.Contains, filter.Qualifiers[0].Operator);
    }
 
    [Fact]
    public void ParseSearch_QualifierKeyIsLowercased()
    {
        var filter = SearchTextParser.ParseSearch("Severity:Error");
        Assert.Equal("severity", filter.Qualifiers[0].Key);
        Assert.Equal("Error", filter.Qualifiers[0].Value);
    }
 
    [Fact]
    public void ParseSearch_QualifierWithQuotedValue()
    {
        var filter = SearchTextParser.ParseSearch("message:\"connection timed out\"");
        Assert.Single(filter.Qualifiers);
        Assert.Equal("message", filter.Qualifiers[0].Key);
        Assert.Equal("connection timed out", filter.Qualifiers[0].Value);
    }
 
    [Fact]
    public void ParseSearch_DottedKey()
    {
        var filter = SearchTextParser.ParseSearch("http.method:GET");
        Assert.Single(filter.Qualifiers);
        Assert.Equal("http.method", filter.Qualifiers[0].Key);
        Assert.Equal("GET", filter.Qualifiers[0].Value);
    }
 
    [Fact]
    public void ParseSearch_QuotedQualifierSyntax_TreatedAsTextFragment()
    {
        var filter = SearchTextParser.ParseSearch("\"http.method:GET\"");
        Assert.Single(filter.TextFragments, "http.method:GET");
        Assert.Empty(filter.Qualifiers);
    }
 
    [Fact]
    public void ParseSearch_MultipleQualifiers()
    {
        var filter = SearchTextParser.ParseSearch("severity:error resource:api");
        Assert.Equal(2, filter.Qualifiers.Length);
        Assert.Equal("severity", filter.Qualifiers[0].Key);
        Assert.Equal("error", filter.Qualifiers[0].Value);
        Assert.Equal("resource", filter.Qualifiers[1].Key);
        Assert.Equal("api", filter.Qualifiers[1].Value);
    }
 
    [Fact]
    public void ParseSearch_QualifierWithEmptyValue_TreatedAsText()
    {
        var filter = SearchTextParser.ParseSearch("key:");
        Assert.Single(filter.TextFragments, "key:");
        Assert.Empty(filter.Qualifiers);
    }
 
    [Fact]
    public void ParseSearch_QualifierWithEmptyQuotedValue_TreatedAsText()
    {
        var filter = SearchTextParser.ParseSearch("key:\"\"");
        Assert.Single(filter.TextFragments, "key:\"\"");
        Assert.Empty(filter.Qualifiers);
    }
 
    #endregion
 
    #region ParseSearch - Negation
 
    [Fact]
    public void ParseSearch_NegatedQualifier()
    {
        var filter = SearchTextParser.ParseSearch("-severity:debug");
        Assert.Empty(filter.TextFragments);
        Assert.Empty(filter.Qualifiers);
        Assert.Single(filter.NegatedQualifiers);
        Assert.Equal("severity", filter.NegatedQualifiers[0].Key);
        Assert.Equal("debug", filter.NegatedQualifiers[0].Value);
    }
 
    [Fact]
    public void ParseSearch_NegatedQualifierWithQuotedValue()
    {
        var filter = SearchTextParser.ParseSearch("-message:\"not important\"");
        Assert.Single(filter.NegatedQualifiers);
        Assert.Equal("message", filter.NegatedQualifiers[0].Key);
        Assert.Equal("not important", filter.NegatedQualifiers[0].Value);
    }
 
    [Fact]
    public void ParseSearch_MixedPositiveAndNegated()
    {
        var filter = SearchTextParser.ParseSearch("severity:error -resource:debug hello");
        Assert.Single(filter.TextFragments, "hello");
        Assert.Single(filter.Qualifiers);
        Assert.Equal("severity", filter.Qualifiers[0].Key);
        Assert.Single(filter.NegatedQualifiers);
        Assert.Equal("resource", filter.NegatedQualifiers[0].Key);
    }
 
    #endregion
 
    #region ParseSearch - Attribute Prefix (@)
 
    [Fact]
    public void ParseSearch_AttributeQualifier()
    {
        var filter = SearchTextParser.ParseSearch("@http.method:GET");
        Assert.Single(filter.Qualifiers);
        Assert.Equal("http.method", filter.Qualifiers[0].Key);
        Assert.Equal("GET", filter.Qualifiers[0].Value);
        Assert.True(filter.Qualifiers[0].IsAttribute);
    }
 
    [Fact]
    public void ParseSearch_NegatedAttributeQualifier()
    {
        var filter = SearchTextParser.ParseSearch("-@db.system:redis");
        Assert.Single(filter.NegatedQualifiers);
        Assert.Equal("db.system", filter.NegatedQualifiers[0].Key);
        Assert.Equal("redis", filter.NegatedQualifiers[0].Value);
        Assert.True(filter.NegatedQualifiers[0].IsAttribute);
    }
 
    [Fact]
    public void ParseSearch_BareQualifierIsNotAttribute()
    {
        var filter = SearchTextParser.ParseSearch("status:error");
        Assert.Single(filter.Qualifiers);
        Assert.False(filter.Qualifiers[0].IsAttribute);
    }
 
    [Fact]
    public void ParseSearch_AttributeWithQuotedValue()
    {
        var filter = SearchTextParser.ParseSearch("@user.name:\"John Doe\"");
        Assert.Single(filter.Qualifiers);
        Assert.Equal("user.name", filter.Qualifiers[0].Key);
        Assert.Equal("John Doe", filter.Qualifiers[0].Value);
        Assert.True(filter.Qualifiers[0].IsAttribute);
    }
 
    [Fact]
    public void ParseSearch_AttributeWithComparisonOperator()
    {
        var filter = SearchTextParser.ParseSearch("@response.time:>500");
        Assert.Single(filter.Qualifiers);
        Assert.Equal("response.time", filter.Qualifiers[0].Key);
        Assert.Equal("500", filter.Qualifiers[0].Value);
        Assert.Equal(ComparisonOperator.GreaterThan, filter.Qualifiers[0].Operator);
        Assert.True(filter.Qualifiers[0].IsAttribute);
    }
 
    #endregion
 
    #region ParseSearch - Comparison Operators
 
    [Fact]
    public void ParseSearch_GreaterThan()
    {
        var filter = SearchTextParser.ParseSearch("duration:>100");
        Assert.Single(filter.Qualifiers);
        Assert.Equal("duration", filter.Qualifiers[0].Key);
        Assert.Equal("100", filter.Qualifiers[0].Value);
        Assert.Equal(ComparisonOperator.GreaterThan, filter.Qualifiers[0].Operator);
    }
 
    [Fact]
    public void ParseSearch_GreaterThanOrEqual()
    {
        var filter = SearchTextParser.ParseSearch("duration:>=500");
        Assert.Single(filter.Qualifiers);
        Assert.Equal("duration", filter.Qualifiers[0].Key);
        Assert.Equal("500", filter.Qualifiers[0].Value);
        Assert.Equal(ComparisonOperator.GreaterThanOrEqual, filter.Qualifiers[0].Operator);
    }
 
    [Fact]
    public void ParseSearch_LessThan()
    {
        var filter = SearchTextParser.ParseSearch("duration:<50");
        Assert.Single(filter.Qualifiers);
        Assert.Equal("duration", filter.Qualifiers[0].Key);
        Assert.Equal("50", filter.Qualifiers[0].Value);
        Assert.Equal(ComparisonOperator.LessThan, filter.Qualifiers[0].Operator);
    }
 
    [Fact]
    public void ParseSearch_LessThanOrEqual()
    {
        var filter = SearchTextParser.ParseSearch("duration:<=200");
        Assert.Single(filter.Qualifiers);
        Assert.Equal("duration", filter.Qualifiers[0].Key);
        Assert.Equal("200", filter.Qualifiers[0].Value);
        Assert.Equal(ComparisonOperator.LessThanOrEqual, filter.Qualifiers[0].Operator);
    }
 
    [Fact]
    public void ParseSearch_NegatedComparisonOperator()
    {
        var filter = SearchTextParser.ParseSearch("-duration:>1000");
        Assert.Single(filter.NegatedQualifiers);
        Assert.Equal("duration", filter.NegatedQualifiers[0].Key);
        Assert.Equal("1000", filter.NegatedQualifiers[0].Value);
        Assert.Equal(ComparisonOperator.GreaterThan, filter.NegatedQualifiers[0].Operator);
    }
 
    #endregion
 
    #region ParseSearch - Complex / Mixed
 
    [Fact]
    public void ParseSearch_ComplexMixed()
    {
        var filter = SearchTextParser.ParseSearch("error severity:warning -resource:test duration:>100 \"connection reset\"");
        Assert.Equal(["error", "connection reset"], filter.TextFragments);
        Assert.Equal(2, filter.Qualifiers.Length);
        Assert.Equal("severity", filter.Qualifiers[0].Key);
        Assert.Equal("warning", filter.Qualifiers[0].Value);
        Assert.Equal("duration", filter.Qualifiers[1].Key);
        Assert.Equal("100", filter.Qualifiers[1].Value);
        Assert.Equal(ComparisonOperator.GreaterThan, filter.Qualifiers[1].Operator);
        Assert.Single(filter.NegatedQualifiers);
        Assert.Equal("resource", filter.NegatedQualifiers[0].Key);
    }
 
    #endregion
 
    #region ParseFragments - Backward Compatibility
 
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void ParseFragments_EmptyOrNull_ReturnsEmpty(string? search)
    {
        var result = SearchTextParser.ParseFragments(search);
        Assert.Empty(result);
    }
 
    [Fact]
    public void ParseFragments_TextOnly()
    {
        var result = SearchTextParser.ParseFragments("hello world");
        Assert.Equal(["hello", "world"], result);
    }
 
    [Fact]
    public void ParseFragments_IncludesQualifierValues()
    {
        var result = SearchTextParser.ParseFragments("severity:error hello");
        Assert.Contains("hello", result);
        Assert.Contains("error", result);
    }
 
    [Fact]
    public void ParseFragments_IncludesNegatedQualifierValues()
    {
        var result = SearchTextParser.ParseFragments("-severity:debug");
        Assert.Contains("debug", result);
    }
 
    #endregion
 
    #region MatchesAllFragments
 
    [Fact]
    public void MatchesAllFragments_EmptyFragments_ReturnsTrue()
    {
        Assert.True(SearchTextParser.MatchesAllFragments([], "anything", static (state, fragment) =>
            state.Contains(fragment, StringComparisons.FullTextSearch)));
    }
 
    [Fact]
    public void MatchesAllFragments_SingleMatch()
    {
        Assert.True(SearchTextParser.MatchesAllFragments(["hello"], "hello world", static (state, fragment) =>
            state.Contains(fragment, StringComparisons.FullTextSearch)));
    }
 
    [Fact]
    public void MatchesAllFragments_CaseInsensitive()
    {
        Assert.True(SearchTextParser.MatchesAllFragments(["HELLO"], "hello world", static (state, fragment) =>
            state.Contains(fragment, StringComparisons.FullTextSearch)));
    }
 
    [Fact]
    public void MatchesAllFragments_AllMustMatch()
    {
        Assert.False(SearchTextParser.MatchesAllFragments(["hello", "missing"], "hello world", static (state, fragment) =>
            state.Contains(fragment, StringComparisons.FullTextSearch)));
    }
 
    [Fact]
    public void MatchesAllFragments_DifferentCandidates()
    {
        Assert.True(SearchTextParser.MatchesAllFragments(["hello", "world"], ("hello", "world"), static (state, fragment) =>
            state.Item1.Contains(fragment, StringComparisons.FullTextSearch) ||
            state.Item2.Contains(fragment, StringComparisons.FullTextSearch)));
    }
 
    #endregion
}