1+ import { jest } from '@jest/globals' ;
2+ import { TextEncoder } from 'node:util' ;
3+
14import {
25 ApplicationTags ,
6+ base64UrlEncode ,
37 Configuration ,
48 Context ,
9+ Encoding ,
510 FlagManager ,
611 internal ,
712 LDEmitter ,
@@ -14,15 +19,26 @@ import {
1419} from '@launchdarkly/js-client-sdk-common' ;
1520
1621import BrowserDataManager from '../src/BrowserDataManager' ;
17- import { ValidatedOptions } from '../src/options' ;
22+ import validateOptions , { ValidatedOptions } from '../src/options' ;
23+ import BrowserEncoding from '../src/platform/BrowserEncoding' ;
24+ import BrowserInfo from '../src/platform/BrowserInfo' ;
25+ import LocalStorage from '../src/platform/LocalStorage' ;
26+ import { MockHasher } from './MockHasher' ;
27+
28+ global . TextEncoder = TextEncoder ;
1829
1930function mockResponse ( value : string , statusCode : number ) {
2031 const response : Response = {
2132 headers : {
33+ // @ts -ignore
2234 get : jest . fn ( ) ,
35+ // @ts -ignore
2336 keys : jest . fn ( ) ,
37+ // @ts -ignore
2438 values : jest . fn ( ) ,
39+ // @ts -ignore
2540 entries : jest . fn ( ) ,
41+ // @ts -ignore
2642 has : jest . fn ( ) ,
2743 } ,
2844 status : statusCode ,
@@ -32,8 +48,14 @@ function mockResponse(value: string, statusCode: number) {
3248 return Promise . resolve ( response ) ;
3349}
3450
51+ /**
52+ * Mocks fetch. Returns the fetch jest.Mock object.
53+ * @param remoteJson
54+ * @param statusCode
55+ */
3556function mockFetch ( value : string , statusCode : number = 200 ) {
3657 const f = jest . fn ( ) ;
58+ // @ts -ignore
3759 f . mockResolvedValue ( mockResponse ( value , statusCode ) ) ;
3860 return f ;
3961}
@@ -46,9 +68,8 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
4668 let baseHeaders : LDHeaders ;
4769 let emitter : jest . Mocked < LDEmitter > ;
4870 let diagnosticsManager : jest . Mocked < internal . DiagnosticsManager > ;
49- let browserDataManager : BrowserDataManager ;
71+ let dataManager : BrowserDataManager ;
5072 let logger : LDLogger ;
51-
5273 beforeEach ( ( ) => {
5374 logger = {
5475 error : jest . fn ( ) ,
@@ -79,40 +100,67 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
79100 pollInterval : 1000 ,
80101 userAgentHeaderName : 'user-agent' ,
81102 trackEventModifier : ( event ) => event ,
82- }
103+ } ;
83104 const mockedFetch = mockFetch ( '{"flagA": true}' , 200 ) ;
84105 platform = {
106+ crypto : {
107+ createHash : ( ) => new MockHasher ( ) ,
108+ randomUUID : ( ) => '123' ,
109+ } ,
110+ info : new BrowserInfo ( ) ,
85111 requests : {
112+ createEventSource : jest . fn ( ( streamUri : string = '' , options : any = { } ) => ( {
113+ streamUri,
114+ options,
115+ onclose : jest . fn ( ) ,
116+ addEventListener : jest . fn ( ) ,
117+ close : jest . fn ( ) ,
118+ } ) ) ,
86119 fetch : mockedFetch ,
87- createEventSource : jest . fn ( ) ,
88120 getEventSourceCapabilities : jest . fn ( ) ,
89121 } ,
122+ storage : new LocalStorage ( config . logger ) ,
123+ encoding : new BrowserEncoding ( ) ,
90124 } as unknown as jest . Mocked < Platform > ;
91125
92126 flagManager = {
93127 loadCached : jest . fn ( ) ,
128+ get : jest . fn ( ) ,
129+ getAll : jest . fn ( ) ,
130+ init : jest . fn ( ) ,
131+ upsert : jest . fn ( ) ,
132+ on : jest . fn ( ) ,
133+ off : jest . fn ( ) ,
94134 } as unknown as jest . Mocked < FlagManager > ;
95135
96- browserConfig = { stream : true } as ValidatedOptions ;
136+ browserConfig = validateOptions ( { stream : false } , logger ) ;
97137 baseHeaders = { } ;
98138 emitter = {
99139 emit : jest . fn ( ) ,
100140 } as unknown as jest . Mocked < LDEmitter > ;
101141 diagnosticsManager = { } as unknown as jest . Mocked < internal . DiagnosticsManager > ;
102142
103- browserDataManager = new BrowserDataManager (
143+ dataManager = new BrowserDataManager (
104144 platform ,
105145 flagManager ,
106146 'test-credential' ,
107147 config ,
108148 browserConfig ,
109149 ( ) => ( {
110- pathGet : jest . fn ( ) ,
111- pathReport : jest . fn ( ) ,
150+ pathGet ( encoding : Encoding , _plainContextString : string ) : string {
151+ return `/msdk/evalx/contexts/${ base64UrlEncode ( _plainContextString , encoding ) } ` ;
152+ } ,
153+ pathReport ( _encoding : Encoding , _plainContextString : string ) : string {
154+ return `/msdk/evalx/context` ;
155+ } ,
112156 } ) ,
113157 ( ) => ( {
114- pathGet : jest . fn ( ) ,
115- pathReport : jest . fn ( ) ,
158+ pathGet ( encoding : Encoding , _plainContextString : string ) : string {
159+ return `/meval/${ base64UrlEncode ( _plainContextString , encoding ) } ` ;
160+ } ,
161+ pathReport ( _encoding : Encoding , _plainContextString : string ) : string {
162+ return `/meval` ;
163+ } ,
116164 } ) ,
117165 baseHeaders ,
118166 emitter ,
@@ -124,71 +172,130 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
124172 jest . resetAllMocks ( ) ;
125173 } ) ;
126174
127- it ( 'should load cached flags and continue to initialize via a poll' , async ( ) => {
175+ it ( 'creates an event source when stream is true' , async ( ) => {
176+ dataManager = new BrowserDataManager (
177+ platform ,
178+ flagManager ,
179+ 'test-credential' ,
180+ config ,
181+ validateOptions ( { stream : true } , logger ) ,
182+ ( ) => ( {
183+ pathGet ( encoding : Encoding , _plainContextString : string ) : string {
184+ return `/msdk/evalx/contexts/${ base64UrlEncode ( _plainContextString , encoding ) } ` ;
185+ } ,
186+ pathReport ( _encoding : Encoding , _plainContextString : string ) : string {
187+ return `/msdk/evalx/context` ;
188+ } ,
189+ } ) ,
190+ ( ) => ( {
191+ pathGet ( encoding : Encoding , _plainContextString : string ) : string {
192+ return `/meval/${ base64UrlEncode ( _plainContextString , encoding ) } ` ;
193+ } ,
194+ pathReport ( _encoding : Encoding , _plainContextString : string ) : string {
195+ return `/meval` ;
196+ } ,
197+ } ) ,
198+ baseHeaders ,
199+ emitter ,
200+ diagnosticsManager ,
201+ ) ;
202+
203+ const context = Context . fromLDContext ( { kind : 'user' , key : 'test-user' } ) ;
204+ const identifyOptions : LDIdentifyOptions = { waitForNetworkResults : false } ;
205+ const identifyResolve = jest . fn ( ) ;
206+ const identifyReject = jest . fn ( ) ;
207+
208+ await dataManager . identify ( identifyResolve , identifyReject , context , identifyOptions ) ;
209+
210+ expect ( platform . requests . createEventSource ) . toHaveBeenCalled ( ) ;
211+ } ) ;
212+
213+ it ( 'should load cached flags and continue to poll to complete identify' , async ( ) => {
128214 const context = Context . fromLDContext ( { kind : 'user' , key : 'test-user' } ) ;
129- const identifyOptions : LDIdentifyOptions = { } ;
215+ const identifyOptions : LDIdentifyOptions = { waitForNetworkResults : false } ;
130216 const identifyResolve = jest . fn ( ) ;
131217 const identifyReject = jest . fn ( ) ;
132218
133219 flagManager . loadCached . mockResolvedValue ( true ) ;
134220
135- await browserDataManager . identify ( identifyResolve , identifyReject , context , identifyOptions ) ;
221+ await dataManager . identify ( identifyResolve , identifyReject , context , identifyOptions ) ;
136222
137223 expect ( logger . debug ) . toHaveBeenCalledWith (
138- 'Identify - Flags loaded from cache. Continuing to initialize via a poll.' ,
224+ '[BrowserDataManager] Identify - Flags loaded from cache. Continuing to initialize via a poll.' ,
139225 ) ;
226+
140227 expect ( flagManager . loadCached ) . toHaveBeenCalledWith ( context ) ;
141- expect ( platform . requests . fetch ) . toHaveBeenCalled ( ) ;
228+ expect ( identifyResolve ) . toHaveBeenCalled ( ) ;
229+ expect ( flagManager . init ) . toHaveBeenCalledWith (
230+ expect . anything ( ) ,
231+ expect . objectContaining ( { flagA : { flag : true , version : undefined } } ) ,
232+ ) ;
233+ expect ( platform . requests . createEventSource ) . not . toHaveBeenCalled ( ) ;
142234 } ) ;
143235
144- it ( 'should set up streaming connection if stream is enabled ' , async ( ) => {
236+ it ( 'should identify from polling when there are no cached flags ' , async ( ) => {
145237 const context = Context . fromLDContext ( { kind : 'user' , key : 'test-user' } ) ;
146- const identifyOptions : LDIdentifyOptions = { } ;
238+ const identifyOptions : LDIdentifyOptions = { waitForNetworkResults : false } ;
147239 const identifyResolve = jest . fn ( ) ;
148240 const identifyReject = jest . fn ( ) ;
149241
150- await browserDataManager . identify ( identifyResolve , identifyReject , context , identifyOptions ) ;
242+ flagManager . loadCached . mockResolvedValue ( false ) ;
151243
152- expect ( platform . requests . createEventSource ) . toHaveBeenCalled ( ) ;
244+ await dataManager . identify ( identifyResolve , identifyReject , context , identifyOptions ) ;
245+
246+ expect ( logger . debug ) . not . toHaveBeenCalledWith (
247+ 'Identify - Flags loaded from cache. Continuing to initialize via a poll.' ,
248+ ) ;
249+
250+ expect ( flagManager . loadCached ) . toHaveBeenCalledWith ( context ) ;
251+ expect ( identifyResolve ) . toHaveBeenCalled ( ) ;
252+ expect ( flagManager . init ) . toHaveBeenCalledWith (
253+ expect . anything ( ) ,
254+ expect . objectContaining ( { flagA : { flag : true , version : undefined } } ) ,
255+ ) ;
256+ expect ( platform . requests . createEventSource ) . not . toHaveBeenCalled ( ) ;
153257 } ) ;
154258
155- it ( 'should not set up streaming connection if stream is disabled' , async ( ) => {
156- browserConfig . stream = false ;
259+ it ( 'creates a stream when streaming is enabled after construction' , async ( ) => {
157260 const context = Context . fromLDContext ( { kind : 'user' , key : 'test-user' } ) ;
158- const identifyOptions : LDIdentifyOptions = { } ;
261+ const identifyOptions : LDIdentifyOptions = { waitForNetworkResults : false } ;
159262 const identifyResolve = jest . fn ( ) ;
160263 const identifyReject = jest . fn ( ) ;
161264
162- await browserDataManager . identify ( identifyResolve , identifyReject , context , identifyOptions ) ;
265+ flagManager . loadCached . mockResolvedValue ( false ) ;
266+
267+ await dataManager . identify ( identifyResolve , identifyReject , context , identifyOptions ) ;
163268
164269 expect ( platform . requests . createEventSource ) . not . toHaveBeenCalled ( ) ;
270+ dataManager . startDataSource ( ) ;
271+ expect ( platform . requests . createEventSource ) . toHaveBeenCalled ( ) ;
165272 } ) ;
166273
167- // it('should stop the data source', () => {
168- // const mockClose = jest.fn();
169- // browserDataManager.updateProcessor = { close: mockClose } as any;
170-
171- // browserDataManager.stopDataSource();
172-
173- // expect(mockClose).toHaveBeenCalled();
174- // expect(browserDataManager.updateProcessor).toBeUndefined();
175- // });
176-
177- // it('should start the data source if context exists', () => {
178- // const mockSetupConnection = jest.spyOn(browserDataManager as any, 'setupConnection');
179- // browserDataManager.context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
180-
181- // browserDataManager.startDataSource();
274+ it ( 'does not re-create the stream if it already running' , async ( ) => {
275+ const context = Context . fromLDContext ( { kind : 'user' , key : 'test-user' } ) ;
276+ const identifyOptions : LDIdentifyOptions = { waitForNetworkResults : false } ;
277+ const identifyResolve = jest . fn ( ) ;
278+ const identifyReject = jest . fn ( ) ;
182279
183- // expect(mockSetupConnection).toHaveBeenCalled();
184- // });
280+ flagManager . loadCached . mockResolvedValue ( false ) ;
185281
186- // it('should not start the data source if context does not exist', () => {
187- // const mockSetupConnection = jest.spyOn(browserDataManager as any, 'setupConnection');
188- // browserDataManager.context = undefined;
282+ await dataManager . identify ( identifyResolve , identifyReject , context , identifyOptions ) ;
189283
190- // browserDataManager.startDataSource();
284+ expect ( platform . requests . createEventSource ) . not . toHaveBeenCalled ( ) ;
285+ dataManager . startDataSource ( ) ;
286+ dataManager . startDataSource ( ) ;
287+ expect ( platform . requests . createEventSource ) . toHaveBeenCalledTimes ( 1 ) ;
288+ expect ( logger . debug ) . toHaveBeenCalledWith (
289+ '[BrowserDataManager] Update processor already active. Not changing state.' ,
290+ ) ;
291+ } ) ;
191292
192- // expect(mockSetupConnection).not.toHaveBeenCalled();
193- // });
293+ it ( 'does not start a stream if identify has not been called' , async ( ) => {
294+ expect ( platform . requests . createEventSource ) . not . toHaveBeenCalled ( ) ;
295+ dataManager . startDataSource ( ) ;
296+ expect ( platform . requests . createEventSource ) . not . toHaveBeenCalledTimes ( 1 ) ;
297+ expect ( logger . debug ) . toHaveBeenCalledWith (
298+ '[BrowserDataManager] Context not set, not starting update processor.' ,
299+ ) ;
300+ } ) ;
194301} ) ;
0 commit comments