@@ -10,6 +10,7 @@ public abstract class QueryEntryTestsBase : MiniLcmTestBase
1010 private readonly string Peach = "Peach" ;
1111 private readonly string Banana = "Banana" ;
1212 private readonly string Kiwi = "Kiwi" ;
13+ private readonly string Null_LexemeForm = string . Empty ; // nulls get normalized to empty strings
1314
1415 private static readonly AutoFaker Faker = new ( AutoFakerDefault . Config ) ;
1516
@@ -87,13 +88,15 @@ await Api.CreateEntry(new Entry()
8788 }
8889 ]
8990 } ) ;
91+ // null / missing key - exposes potential NPEs
92+ await Api . CreateEntry ( new Entry ( ) ) ;
9093 }
9194
9295 [ Fact ]
9396 public async Task CanFilterToMissingSenses ( )
9497 {
9598 var results = await Api . GetEntries ( new ( Filter : new ( ) { GridifyFilter = "Senses=null" } ) ) . ToArrayAsync ( ) ;
96- results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeEquivalentTo ( Apple ) ;
99+ results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeEquivalentTo ( Apple , Null_LexemeForm ) ;
97100 }
98101
99102 [ Fact ]
@@ -106,7 +109,7 @@ public async Task CanFilterToNotMissingSenses()
106109 [ Fact ]
107110 public async Task CanFilterToMissingPartOfSpeech ( )
108111 {
109- var results = await Api . GetEntries ( new ( Filter : new ( ) { GridifyFilter = "Senses.PartOfSpeechId=null " } ) ) . ToArrayAsync ( ) ;
112+ var results = await Api . GetEntries ( new ( Filter : new ( ) { GridifyFilter = "Senses.PartOfSpeechId=" } ) ) . ToArrayAsync ( ) ;
110113 //does not include entries with no senses
111114 results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeEquivalentTo ( Peach ) ;
112115 }
@@ -145,14 +148,14 @@ public async Task CanFilterSemanticDomainCodeContains()
145148 public async Task CanFilterToMissingComplexFormTypes ( )
146149 {
147150 var results = await Api . GetEntries ( new ( Filter : new ( ) { GridifyFilter = "ComplexFormTypes=null" } ) ) . ToArrayAsync ( ) ;
148- results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeEquivalentTo ( Apple , Banana , Kiwi ) ;
151+ results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeEquivalentTo ( Apple , Banana , Kiwi , Null_LexemeForm ) ;
149152 }
150153
151154 [ Fact ]
152155 public async Task CanFilterToMissingComplexFormTypesWithEmptyArray ( )
153156 {
154157 var results = await Api . GetEntries ( new ( Filter : new ( ) { GridifyFilter = "ComplexFormTypes=[]" } ) ) . ToArrayAsync ( ) ;
155- results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeEquivalentTo ( Apple , Banana , Kiwi ) ;
158+ results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeEquivalentTo ( Apple , Banana , Kiwi , Null_LexemeForm ) ;
156159 }
157160
158161 [ Fact ]
@@ -187,11 +190,13 @@ public async Task CanFilterLexemeFormContains()
187190 public async Task CanFilterGlossNull ( )
188191 {
189192 var results = await Api . GetEntries ( new ( Filter : new ( ) { GridifyFilter = "Senses.Gloss[en]=null" } ) ) . ToArrayAsync ( ) ;
190- results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeEquivalentTo ( Peach ) ;
193+ /// No entries have a gloss of "null"
194+ /// <see cref="Filtering.EntryFilter.NewMapper"/> and <see cref="NullAndEmptyQueryEntryTestsBase"/>
195+ results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeEmpty ( ) ;
191196 }
192197
193198 [ Fact ]
194- public async Task CanFilterGlossEmpty ( )
199+ public async Task CanFilterGlossEmptyOrNull ( )
195200 {
196201 var results = await Api . GetEntries ( new ( Filter : new ( ) { GridifyFilter = "Senses.Gloss[en]=" } ) ) . ToArrayAsync ( ) ;
197202 results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeEquivalentTo ( Peach ) ;
@@ -317,3 +322,64 @@ public async Task HeadwordOrder(string searchTerm, string wordsAndGlosses, strin
317322 string . Join ( "," , result ) . Should ( ) . Be ( expectedOrder ) ;
318323 }
319324}
325+
326+ // A seperate class to preserve the readability of the results in the main test class
327+ public abstract class NullAndEmptyQueryEntryTestsBase : MiniLcmTestBase
328+ {
329+ private readonly string Apple = "Apple" ;
330+ private readonly string Null = string . Empty ; // nulls get normalized to empty strings
331+ private readonly string EmptyString = string . Empty ;
332+ private readonly string NullString = "null" ;
333+
334+ public override async Task InitializeAsync ( )
335+ {
336+ await base . InitializeAsync ( ) ;
337+ await Api . CreateEntry ( new Entry ( ) { LexemeForm = { { "en" , Apple } } } ) ;
338+ // null / missing key
339+ await Api . CreateEntry ( new Entry ( ) ) ;
340+ // blank
341+ await Api . CreateEntry ( new Entry ( ) { LexemeForm = { [ "en" ] = EmptyString } } ) ;
342+ // null string
343+ await Api . CreateEntry ( new Entry ( ) { LexemeForm = { [ "en" ] = NullString } } ) ;
344+ }
345+
346+ [ Fact ]
347+ public async Task CanFilterIsNullOrEmpty ( )
348+ {
349+ var results = await Api . GetEntries ( new ( Filter : new ( ) { GridifyFilter = "LexemeForm[en]=" } ) ) . ToArrayAsync ( ) ;
350+ results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeEquivalentTo ( Null , EmptyString ) ;
351+ }
352+
353+ [ Fact ]
354+ public async Task CanFilterIsNotNullOrEmpty ( )
355+ {
356+ var results = await Api . GetEntries ( new ( Filter : new ( ) { GridifyFilter = "LexemeForm[en]!=" } ) ) . ToArrayAsync ( ) ;
357+ results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeEquivalentTo ( Apple , NullString ) ;
358+ }
359+
360+ [ Fact ]
361+ public async Task CanFilterEqualsNullString ( )
362+ {
363+ var results = await Api . GetEntries ( new ( Filter : new ( ) { GridifyFilter = "LexemeForm[en]=null" } ) ) . ToArrayAsync ( ) ;
364+ results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeEquivalentTo ( NullString ) ;
365+ }
366+
367+ [ Fact ]
368+ public async Task CanFilterNotEqualsNullString ( )
369+ {
370+ var results = await Api . GetEntries ( new ( Filter : new ( ) { GridifyFilter = "LexemeForm[en]!=null" } ) ) . ToArrayAsync ( ) ;
371+ // Sadly the != operator isn't consistent, but it's an edge case that probably isn't crucial:
372+ // crdt logic: key exists (and/or value is not null, I'm not sure exactly) && LexemeForm[en] != "null"
373+ // fwdata logic: LexemeForm[en] != "null"
374+ // i.e. the entry that doesn't have LexemeForm[en] at all, is only included in the fwdata results
375+ results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeSubsetOf ( [ Apple , Null , EmptyString ] ) ;
376+ results . Count ( ) . Should ( ) . BeInRange ( 2 , 3 ) ;
377+ }
378+
379+ [ Fact ]
380+ public async Task CanFilterContainsNullString ( )
381+ {
382+ var results = await Api . GetEntries ( new ( Filter : new ( ) { GridifyFilter = "LexemeForm[en]=*null" } ) ) . ToArrayAsync ( ) ;
383+ results . Select ( e => e . LexemeForm [ "en" ] ) . Should ( ) . BeEquivalentTo ( NullString ) ;
384+ }
385+ }
0 commit comments