1717
1818import { expect } from 'chai' ;
1919import {
20+ ConfigUpdateObserver ,
2021 ensureInitialized ,
2122 fetchAndActivate ,
2223 FetchResponse ,
2324 getRemoteConfig ,
24- getString
25+ getString ,
26+ onConfigUpdate
2527} from '../src' ;
2628import '../test/setup' ;
2729import {
@@ -34,6 +36,8 @@ import * as sinon from 'sinon';
3436import { Component , ComponentType } from '@firebase/component' ;
3537import { FirebaseInstallations } from '@firebase/installations-types' ;
3638import { openDatabase , APP_NAMESPACE_STORE } from '../src/storage/storage' ;
39+ import { ERROR_FACTORY , ErrorCode } from '../src/errors' ;
40+ import { RemoteConfig as RemoteConfigImpl } from '../src/remote_config' ;
3741
3842const fakeFirebaseConfig = {
3943 apiKey : 'api-key' ,
@@ -45,6 +49,12 @@ const fakeFirebaseConfig = {
4549 appId : '1:111:web:a1234'
4650} ;
4751
52+ const mockObserver = {
53+ next : sinon . stub ( ) ,
54+ error : sinon . stub ( ) ,
55+ complete : sinon . stub ( )
56+ } ;
57+
4858async function clearDatabase ( ) : Promise < void > {
4959 const db = await openDatabase ( ) ;
5060 db . transaction ( [ APP_NAMESPACE_STORE ] , 'readwrite' )
@@ -151,4 +161,99 @@ describe('Remote Config API', () => {
151161 await ensureInitialized ( rc ) ;
152162 expect ( getString ( rc , 'foobar' ) ) . to . equal ( 'hello world' ) ;
153163 } ) ;
164+
165+ describe ( 'onConfigUpdate' , ( ) => {
166+ let capturedObserver : ConfigUpdateObserver | undefined ;
167+ let rc : RemoteConfigImpl ;
168+ let addObserverStub : sinon . SinonStub ;
169+ let removeObserverStub : sinon . SinonStub ;
170+
171+ beforeEach ( ( ) => {
172+ rc = getRemoteConfig ( app ) as RemoteConfigImpl ;
173+
174+ addObserverStub = sinon
175+ . stub ( rc . _realtimeHandler , 'addObserver' )
176+ . resolves ( ) ;
177+ removeObserverStub = sinon
178+ . stub ( rc . _realtimeHandler , 'removeObserver' )
179+ . resolves ( ) ;
180+
181+ addObserverStub . callsFake ( async ( observer : ConfigUpdateObserver ) => {
182+ capturedObserver = observer ;
183+ } ) ;
184+ } ) ;
185+
186+ afterEach ( ( ) => {
187+ capturedObserver = undefined ;
188+ addObserverStub . restore ( ) ;
189+ removeObserverStub . restore ( ) ;
190+ } ) ;
191+
192+ it ( 'should call addObserver on the internal realtimeHandler' , async ( ) => {
193+ await onConfigUpdate ( rc , mockObserver ) ;
194+ expect ( addObserverStub ) . to . have . been . calledOnce ;
195+ expect ( addObserverStub ) . to . have . been . calledWith ( mockObserver ) ;
196+ } ) ;
197+
198+ it ( 'should return an unsubscribe function' , async ( ) => {
199+ const unsubscribe = await onConfigUpdate ( rc , mockObserver ) ;
200+ expect ( unsubscribe ) . to . be . a ( 'function' ) ;
201+ } ) ;
202+
203+ it ( 'returned unsubscribe function should call removeObserver' , async ( ) => {
204+ const unsubscribe = await onConfigUpdate ( rc , mockObserver ) ;
205+
206+ unsubscribe ( ) ;
207+ expect ( removeObserverStub ) . to . have . been . calledOnce ;
208+ expect ( removeObserverStub ) . to . have . been . calledWith ( mockObserver ) ;
209+ } ) ;
210+
211+ it ( 'observer.next should be called when realtimeHandler propagates an update' , async ( ) => {
212+ await onConfigUpdate ( rc , mockObserver ) ;
213+
214+ if ( capturedObserver && capturedObserver . next ) {
215+ const mockConfigUpdate = { getUpdatedKeys : ( ) => new Set ( [ 'new_key' ] ) } ;
216+ capturedObserver . next ( mockConfigUpdate ) ;
217+ } else {
218+ expect . fail ( 'Observer was not captured or next method is missing.' ) ;
219+ }
220+
221+ expect ( mockObserver . next ) . to . have . been . calledOnce ;
222+ expect ( mockObserver . next ) . to . have . been . calledWithMatch ( {
223+ getUpdatedKeys : sinon . match . func
224+ } ) ;
225+ expect (
226+ mockObserver . next . getCall ( 0 ) . args [ 0 ] . getUpdatedKeys ( )
227+ ) . to . deep . equal ( new Set ( [ 'new_key' ] ) ) ;
228+ } ) ;
229+
230+ it ( 'observer.error should be called when realtimeHandler propagates an error' , async ( ) => {
231+ await onConfigUpdate ( rc , mockObserver ) ;
232+
233+ if ( capturedObserver && capturedObserver . error ) {
234+ const expectedOriginalErrorMessage = 'Realtime stream error' ;
235+ const mockError = ERROR_FACTORY . create (
236+ ErrorCode . CONFIG_UPDATE_STREAM_ERROR ,
237+ {
238+ originalErrorMessage : expectedOriginalErrorMessage
239+ }
240+ ) ;
241+ capturedObserver . error ( mockError ) ;
242+ } else {
243+ expect . fail ( 'Observer was not captured or error method is missing.' ) ;
244+ }
245+
246+ expect ( mockObserver . error ) . to . have . been . calledOnce ;
247+ const receivedError = mockObserver . error . getCall ( 0 ) . args [ 0 ] ;
248+
249+ expect ( receivedError . message ) . to . equal (
250+ 'Remote Config: The stream was not able to connect to the backend: Realtime stream error. (remoteconfig/stream-error).'
251+ ) ;
252+ expect ( receivedError ) . to . have . nested . property (
253+ 'customData.originalErrorMessage' ,
254+ 'Realtime stream error'
255+ ) ;
256+ expect ( ( receivedError as any ) . code ) . to . equal ( 'remoteconfig/stream-error' ) ;
257+ } ) ;
258+ } ) ;
154259} ) ;
0 commit comments