Skip to content

Commit 2607c18

Browse files
committed
Add unit tests for generic ITextSearch<BingWebPage> methods
- Added 9 semantic verification tests for LINQ filter translation - Tests verify correct Bing API query parameter generation - Fixed inequality operator bug discovered during testing - Addresses reviewer feedback on PR #13188
1 parent 1995595 commit 2607c18

2 files changed

Lines changed: 244 additions & 5 deletions

File tree

dotnet/src/Plugins/Plugins.UnitTests/Web/Bing/BingTextSearchTests.cs

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,237 @@ public async Task DoesNotBuildsUriForInvalidQueryParameterAsync()
233233
Assert.Equal("Unknown equality filter clause field name 'fooBar', must be one of answerCount,cc,freshness,mkt,promote,responseFilter,safeSearch,setLang,textDecorations,textFormat,contains,ext,filetype,inanchor,inbody,intitle,ip,language,loc,location,prefer,site,feed,hasfeed,url (Parameter 'searchOptions')", e.Message);
234234
}
235235

236+
#region Generic ITextSearch<BingWebPage> Interface Tests
237+
238+
[Fact]
239+
public async Task GenericSearchAsyncWithLanguageEqualityFilterProducesCorrectBingQueryAsync()
240+
{
241+
// Arrange
242+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
243+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
244+
245+
// Act
246+
var searchOptions = new TextSearchOptions<BingWebPage>
247+
{
248+
Top = 4,
249+
Skip = 0,
250+
Filter = page => page.Language == "en"
251+
};
252+
KernelSearchResults<string> result = await textSearch.SearchAsync("What is the Semantic Kernel?", searchOptions);
253+
254+
// Assert - Verify LINQ expression converted to Bing's language: operator
255+
var requestUris = this._messageHandlerStub.RequestUris;
256+
Assert.Single(requestUris);
257+
Assert.NotNull(requestUris[0]);
258+
Assert.Contains("language%3Aen", requestUris[0]!.AbsoluteUri);
259+
Assert.Contains("count=4", requestUris[0]!.AbsoluteUri);
260+
Assert.Contains("offset=0", requestUris[0]!.AbsoluteUri);
261+
}
262+
263+
[Fact]
264+
public async Task GenericSearchAsyncWithLanguageInequalityFilterProducesCorrectBingQueryAsync()
265+
{
266+
// Arrange
267+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
268+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
269+
270+
// Act
271+
var searchOptions = new TextSearchOptions<BingWebPage>
272+
{
273+
Top = 4,
274+
Skip = 0,
275+
Filter = page => page.Language != "fr"
276+
};
277+
KernelSearchResults<string> result = await textSearch.SearchAsync("What is the Semantic Kernel?", searchOptions);
278+
279+
// Assert - Verify LINQ inequality expression converted to Bing's negation syntax (-language:fr)
280+
var requestUris = this._messageHandlerStub.RequestUris;
281+
Assert.Single(requestUris);
282+
Assert.NotNull(requestUris[0]);
283+
Assert.Contains("-language%3Afr", requestUris[0]!.AbsoluteUri);
284+
}
285+
286+
[Fact]
287+
public async Task GenericSearchAsyncWithContainsFilterProducesCorrectBingQueryAsync()
288+
{
289+
// Arrange
290+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
291+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
292+
293+
// Act
294+
var searchOptions = new TextSearchOptions<BingWebPage>
295+
{
296+
Top = 4,
297+
Skip = 0,
298+
Filter = page => page.Name.Contains("Microsoft")
299+
};
300+
KernelSearchResults<string> result = await textSearch.SearchAsync("What is the Semantic Kernel?", searchOptions);
301+
302+
// Assert - Verify LINQ Contains() converted to Bing's intitle: operator
303+
var requestUris = this._messageHandlerStub.RequestUris;
304+
Assert.Single(requestUris);
305+
Assert.NotNull(requestUris[0]);
306+
Assert.Contains("intitle%3AMicrosoft", requestUris[0]!.AbsoluteUri);
307+
}
308+
309+
[Fact]
310+
public async Task GenericSearchAsyncWithComplexAndFilterProducesCorrectBingQueryAsync()
311+
{
312+
// Arrange
313+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
314+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
315+
316+
// Act
317+
var searchOptions = new TextSearchOptions<BingWebPage>
318+
{
319+
Top = 4,
320+
Skip = 0,
321+
Filter = page => page.Language == "en" && page.Name.Contains("AI")
322+
};
323+
KernelSearchResults<string> result = await textSearch.SearchAsync("What is the Semantic Kernel?", searchOptions);
324+
325+
// Assert - Verify LINQ AND expression produces both Bing operators
326+
var requestUris = this._messageHandlerStub.RequestUris;
327+
Assert.Single(requestUris);
328+
Assert.NotNull(requestUris[0]);
329+
Assert.Contains("language%3Aen", requestUris[0]!.AbsoluteUri);
330+
Assert.Contains("intitle%3AAI", requestUris[0]!.AbsoluteUri);
331+
}
332+
333+
[Fact]
334+
public async Task GenericGetTextSearchResultsAsyncWithUrlFilterProducesCorrectBingQueryAsync()
335+
{
336+
// Arrange
337+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
338+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
339+
340+
// Act
341+
var searchOptions = new TextSearchOptions<BingWebPage>
342+
{
343+
Top = 4,
344+
Skip = 0,
345+
Filter = page => page.Url.Contains("microsoft.com")
346+
};
347+
KernelSearchResults<TextSearchResult> result = await textSearch.GetTextSearchResultsAsync("What is the Semantic Kernel?", searchOptions);
348+
349+
// Assert - Verify LINQ Url.Contains() converted to Bing's url: operator
350+
var requestUris = this._messageHandlerStub.RequestUris;
351+
Assert.Single(requestUris);
352+
Assert.NotNull(requestUris[0]);
353+
Assert.Contains("url%3Amicrosoft.com", requestUris[0]!.AbsoluteUri);
354+
355+
// Also verify result structure
356+
Assert.NotNull(result);
357+
Assert.NotNull(result.Results);
358+
}
359+
360+
[Fact]
361+
public async Task GenericGetSearchResultsAsyncWithSnippetContainsFilterProducesCorrectBingQueryAsync()
362+
{
363+
// Arrange
364+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
365+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
366+
367+
// Act
368+
var searchOptions = new TextSearchOptions<BingWebPage>
369+
{
370+
Top = 4,
371+
Skip = 0,
372+
Filter = page => page.Snippet.Contains("semantic")
373+
};
374+
KernelSearchResults<object> result = await textSearch.GetSearchResultsAsync("What is the Semantic Kernel?", searchOptions);
375+
376+
// Assert - Verify LINQ Snippet.Contains() converted to Bing's inbody: operator
377+
var requestUris = this._messageHandlerStub.RequestUris;
378+
Assert.Single(requestUris);
379+
Assert.NotNull(requestUris[0]);
380+
Assert.Contains("inbody%3Asemantic", requestUris[0]!.AbsoluteUri);
381+
382+
// Verify result structure
383+
Assert.NotNull(result);
384+
Assert.NotNull(result.Results);
385+
}
386+
387+
[Fact]
388+
public async Task GenericSearchAsyncWithDisplayUrlEqualityFilterProducesCorrectBingQueryAsync()
389+
{
390+
// Arrange
391+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(SiteFilterDevBlogsResponseJson));
392+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
393+
394+
// Act
395+
var searchOptions = new TextSearchOptions<BingWebPage>
396+
{
397+
Top = 4,
398+
Skip = 0,
399+
Filter = page => page.DisplayUrl == "devblogs.microsoft.com"
400+
};
401+
KernelSearchResults<string> result = await textSearch.SearchAsync("What is the Semantic Kernel?", searchOptions);
402+
403+
// Assert - Verify LINQ DisplayUrl equality converted to Bing's site: operator
404+
var requestUris = this._messageHandlerStub.RequestUris;
405+
Assert.Single(requestUris);
406+
Assert.NotNull(requestUris[0]);
407+
Assert.Contains("site%3Adevblogs.microsoft.com", requestUris[0]!.AbsoluteUri);
408+
}
409+
410+
[Fact]
411+
public async Task GenericSearchAsyncWithMultipleAndConditionsProducesCorrectBingQueryAsync()
412+
{
413+
// Arrange
414+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
415+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
416+
417+
// Act
418+
var searchOptions = new TextSearchOptions<BingWebPage>
419+
{
420+
Top = 4,
421+
Skip = 0,
422+
Filter = page => page.Language == "en" && page.DisplayUrl.Contains("microsoft.com") && page.Name.Contains("Semantic")
423+
};
424+
KernelSearchResults<string> result = await textSearch.SearchAsync("What is the Semantic Kernel?", searchOptions);
425+
426+
// Assert - Verify all LINQ conditions converted correctly
427+
var requestUris = this._messageHandlerStub.RequestUris;
428+
Assert.Single(requestUris);
429+
Assert.NotNull(requestUris[0]);
430+
string uri = requestUris[0]!.AbsoluteUri;
431+
Assert.Contains("language%3Aen", uri);
432+
Assert.Contains("site%3Amicrosoft.com", uri); // DisplayUrl.Contains() → site: operator
433+
Assert.Contains("intitle%3ASemantic", uri);
434+
}
435+
436+
[Fact]
437+
public async Task GenericSearchAsyncWithNoFilterReturnsResultsSuccessfullyAsync()
438+
{
439+
// Arrange
440+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
441+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
442+
443+
// Act - No filter specified
444+
var searchOptions = new TextSearchOptions<BingWebPage>
445+
{
446+
Top = 10,
447+
Skip = 0
448+
};
449+
KernelSearchResults<string> result = await textSearch.SearchAsync("What is the Semantic Kernel?", searchOptions);
450+
451+
// Assert - Verify basic query without filter operators
452+
var requestUris = this._messageHandlerStub.RequestUris;
453+
Assert.Single(requestUris);
454+
Assert.NotNull(requestUris[0]);
455+
Assert.DoesNotContain("language%3A", requestUris[0]!.AbsoluteUri);
456+
Assert.DoesNotContain("intitle%3A", requestUris[0]!.AbsoluteUri);
457+
458+
// Verify results
459+
Assert.NotNull(result);
460+
Assert.NotNull(result.Results);
461+
var resultList = await result.Results.ToListAsync();
462+
Assert.Equal(10, resultList.Count);
463+
}
464+
465+
#endregion
466+
236467
/// <inheritdoc/>
237468
public void Dispose()
238469
{

dotnet/src/Plugins/Plugins.Web/Bing/BingTextSearch.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,10 @@ private static void ProcessEqualityExpression(BinaryExpression binaryExpr, TextS
214214
{
215215
if (isNegated)
216216
{
217-
// For inequality, use Bing's negation syntax by prepending '-' to the filter name
218-
// Example: -language:en excludes pages in English
219-
filter.Equality($"-{bingFilterName}", value);
217+
// For inequality, wrap the value with a negation marker
218+
// This will be processed in BuildQuery to prepend '-' to the advanced search operator
219+
// Example: language:en becomes -language:en (excludes pages in English)
220+
filter.Equality(bingFilterName, $"-{value}");
220221
}
221222
else
222223
{
@@ -506,14 +507,21 @@ private static string BuildQuery(string query, TextSearchOptions searchOptions)
506507
{
507508
if (filterClause is EqualToFilterClause equalityFilterClause)
508509
{
510+
// Check if value starts with '-' indicating negation (for inequality != operator)
511+
string? valueStr = equalityFilterClause.Value?.ToString();
512+
bool isNegated = valueStr?.StartsWith("-", StringComparison.Ordinal) == true;
513+
string actualValue = isNegated && valueStr != null ? valueStr.Substring(1) : valueStr ?? string.Empty;
514+
509515
if (s_advancedSearchKeywords.Contains(equalityFilterClause.FieldName, StringComparer.OrdinalIgnoreCase) && equalityFilterClause.Value is not null)
510516
{
511-
fullQuery.Append($"+{equalityFilterClause.FieldName}%3A").Append(Uri.EscapeDataString(equalityFilterClause.Value.ToString()!));
517+
// For advanced search keywords, prepend '-' if negated to exclude results
518+
string prefix = isNegated ? "-" : "";
519+
fullQuery.Append($"+{prefix}{equalityFilterClause.FieldName}%3A").Append(Uri.EscapeDataString(actualValue));
512520
}
513521
else if (s_queryParameters.Contains(equalityFilterClause.FieldName, StringComparer.OrdinalIgnoreCase) && equalityFilterClause.Value is not null)
514522
{
515523
string? queryParam = s_queryParameters.FirstOrDefault(s => s.Equals(equalityFilterClause.FieldName, StringComparison.OrdinalIgnoreCase));
516-
queryParams.Append('&').Append(queryParam!).Append('=').Append(Uri.EscapeDataString(equalityFilterClause.Value.ToString()!));
524+
queryParams.Append('&').Append(queryParam!).Append('=').Append(Uri.EscapeDataString(actualValue));
517525
}
518526
else
519527
{

0 commit comments

Comments
 (0)