1+ import { AttributeReference } from '@launchdarkly/js-sdk-common' ;
2+
13import { Flag } from '../../src/evaluation/data/Flag' ;
24import { Segment } from '../../src/evaluation/data/Segment' ;
35import {
46 deserializeAll ,
57 deserializeDelete ,
68 deserializePatch ,
9+ nullReplacer ,
710 replacer ,
8- reviver ,
11+ serializeFlag ,
912 serializeSegment ,
1013} from '../../src/store/serialization' ;
1114
@@ -152,6 +155,38 @@ const segmentWithBucketBy = {
152155 deleted : false ,
153156} ;
154157
158+ const flagWithNullInJsonVariation = {
159+ key : 'flagName' ,
160+ on : true ,
161+ fallthrough : { variation : 1 } ,
162+ variations : [ [ true , null , 'potato' ] , [ null , null ] , { null : null } , { arr : [ null ] } ] ,
163+ version : 1 ,
164+ } ;
165+
166+ const flagWithManyNulls = {
167+ key : 'test-after-value1' ,
168+ on : true ,
169+ rules : [
170+ {
171+ variation : 0 ,
172+ id : 'ruleid' ,
173+ clauses : [
174+ {
175+ attribute : 'attrname' ,
176+ op : 'after' ,
177+ values : [ 'not valid' ] ,
178+ negate : null ,
179+ } ,
180+ ] ,
181+ trackEvents : null ,
182+ } ,
183+ ] ,
184+ offVariation : null ,
185+ fallthrough : { variation : 1 } ,
186+ variations : [ true , false ] ,
187+ version : 1 ,
188+ } ;
189+
155190function makeAllData ( flag ?: any , segment ?: any ) : any {
156191 const allData : any = {
157192 data : {
@@ -239,6 +274,42 @@ describe('when deserializing all data', () => {
239274 const ref = parsed ?. data . flags . flagName . rules ?. [ 0 ] . rollout ?. bucketByAttributeReference ;
240275 expect ( ref ?. isValid ) . toBeTruthy ( ) ;
241276 } ) ;
277+
278+ it ( 'does not replace null in Objects or array JSON variations' , ( ) => {
279+ const jsonString = makeSerializedAllData ( flagWithNullInJsonVariation ) ;
280+ const parsed = deserializeAll ( jsonString ) ;
281+
282+ expect ( parsed ?. data . flags . flagName . variations ) . toStrictEqual (
283+ flagWithNullInJsonVariation . variations ,
284+ ) ;
285+ } ) ;
286+
287+ it ( 'removes null values outside variations' , ( ) => {
288+ const jsonString = makeSerializedAllData ( flagWithManyNulls ) ;
289+ const parsed = deserializeAll ( jsonString ) ;
290+
291+ expect ( parsed ?. data . flags . flagName ) . toStrictEqual ( {
292+ key : 'test-after-value1' ,
293+ on : true ,
294+ rules : [
295+ {
296+ variation : 0 ,
297+ id : 'ruleid' ,
298+ clauses : [
299+ {
300+ attribute : 'attrname' ,
301+ attributeReference : new AttributeReference ( 'attrname' ) ,
302+ op : 'after' ,
303+ values : [ 'not valid' ] ,
304+ } ,
305+ ] ,
306+ } ,
307+ ] ,
308+ fallthrough : { variation : 1 } ,
309+ variations : [ true , false ] ,
310+ version : 1 ,
311+ } ) ;
312+ } ) ;
242313} ) ;
243314
244315describe ( 'when deserializing patch data' , ( ) => {
@@ -290,9 +361,45 @@ describe('when deserializing patch data', () => {
290361 const ref = ( parsed ?. data as Flag ) . rules ?. [ 0 ] . rollout ?. bucketByAttributeReference ;
291362 expect ( ref ?. isValid ) . toBeTruthy ( ) ;
292363 } ) ;
364+
365+ it ( 'does not replace null in Objects or array JSON variations' , ( ) => {
366+ const jsonString = makeSerializedPatchData ( flagWithNullInJsonVariation ) ;
367+ const parsed = deserializePatch ( jsonString ) ;
368+
369+ expect ( ( parsed ?. data as Flag ) ?. variations ) . toStrictEqual (
370+ flagWithNullInJsonVariation . variations ,
371+ ) ;
372+ } ) ;
373+
374+ it ( 'removes null values outside variations' , ( ) => {
375+ const jsonString = makeSerializedPatchData ( flagWithManyNulls ) ;
376+ const parsed = deserializePatch ( jsonString ) ;
377+
378+ expect ( parsed ?. data as Flag ) . toStrictEqual ( {
379+ key : 'test-after-value1' ,
380+ on : true ,
381+ rules : [
382+ {
383+ variation : 0 ,
384+ id : 'ruleid' ,
385+ clauses : [
386+ {
387+ attribute : 'attrname' ,
388+ attributeReference : new AttributeReference ( 'attrname' ) ,
389+ op : 'after' ,
390+ values : [ 'not valid' ] ,
391+ } ,
392+ ] ,
393+ } ,
394+ ] ,
395+ fallthrough : { variation : 1 } ,
396+ variations : [ true , false ] ,
397+ version : 1 ,
398+ } ) ;
399+ } ) ;
293400} ) ;
294401
295- it ( 'removes null elements' , ( ) => {
402+ it ( 'removes null elements that are not part of arrays ' , ( ) => {
296403 const baseData = {
297404 a : 'b' ,
298405 b : 'c' ,
@@ -306,10 +413,49 @@ it('removes null elements', () => {
306413 polluted . c . f = null ;
307414
308415 const stringPolluted = JSON . stringify ( polluted ) ;
309- const parsed = JSON . parse ( stringPolluted , reviver ) ;
416+ const parsed = JSON . parse ( stringPolluted ) ;
417+ nullReplacer ( parsed ) ;
310418 expect ( parsed ) . toStrictEqual ( baseData ) ;
311419} ) ;
312420
421+ it ( 'does not remove null in arrays' , ( ) => {
422+ const data = {
423+ a : [ 'b' , null , { arr : [ null ] } ] ,
424+ c : {
425+ d : [ 'e' , null , { arr : [ null ] } ] ,
426+ } ,
427+ } ;
428+
429+ const parsed = JSON . parse ( JSON . stringify ( data ) ) ;
430+ nullReplacer ( parsed ) ;
431+ expect ( parsed ) . toStrictEqual ( data ) ;
432+ } ) ;
433+
434+ it ( 'does remove null from objects that are inside of arrays' , ( ) => {
435+ const data = {
436+ a : [ 'b' , null , { null : null , notNull : true } ] ,
437+ c : {
438+ d : [ 'e' , null , { null : null , notNull : true } ] ,
439+ } ,
440+ } ;
441+
442+ const parsed = JSON . parse ( JSON . stringify ( data ) ) ;
443+ nullReplacer ( parsed ) ;
444+ expect ( parsed ) . toStrictEqual ( {
445+ a : [ 'b' , null , { notNull : true } ] ,
446+ c : {
447+ d : [ 'e' , null , { notNull : true } ] ,
448+ } ,
449+ } ) ;
450+ } ) ;
451+
452+ it ( 'can handle attempting to replace nulls for an undefined or null value' , ( ) => {
453+ expect ( ( ) => {
454+ nullReplacer ( null ) ;
455+ nullReplacer ( undefined ) ;
456+ } ) . not . toThrow ( ) ;
457+ } ) ;
458+
313459it . each ( [
314460 [ flagWithAttributeNameInClause , undefined ] ,
315461 [ flagWithAttributeReferenceInClause , undefined ] ,
@@ -450,3 +596,11 @@ it('serialization converts sets back to arrays for includedContexts/excludedCont
450596 expect ( jsonDeserialized . includedContexts [ 0 ] . generated_valuesSet ) . toBeUndefined ( ) ;
451597 expect ( jsonDeserialized . excludedContexts [ 0 ] . generated_valuesSet ) . toBeUndefined ( ) ;
452598} ) ;
599+
600+ it ( 'serializes null values without issue' , ( ) => {
601+ const jsonString = makeSerializedAllData ( flagWithNullInJsonVariation ) ;
602+ const parsed = deserializeAll ( jsonString ) ;
603+ const serialized = serializeFlag ( parsed ! . data . flags . flagName ) ;
604+ // After serialization nulls should still be there, and any memo generated items should be gone.
605+ expect ( JSON . parse ( serialized ) ) . toEqual ( flagWithNullInJsonVariation ) ;
606+ } ) ;
0 commit comments