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' ,
@@ -151,4 +155,127 @@ describe('Remote Config API', () => {
151155 await ensureInitialized ( rc ) ;
152156 expect ( getString ( rc , 'foobar' ) ) . to . equal ( 'hello world' ) ;
153157 } ) ;
158+
159+ describe ( 'onConfigUpdate' , ( ) => {
160+ let capturedObserver : ConfigUpdateObserver | undefined ;
161+ let rc : RemoteConfigImpl ;
162+ let addObserverStub : sinon . SinonStub ;
163+ let removeObserverStub : sinon . SinonStub ;
164+
165+ beforeEach ( ( ) => {
166+ rc = getRemoteConfig ( app ) as RemoteConfigImpl ;
167+
168+ addObserverStub = sinon
169+ . stub ( rc . _realtimeHandler , 'addObserver' )
170+ . resolves ( ) ;
171+ removeObserverStub = sinon
172+ . stub ( rc . _realtimeHandler , 'removeObserver' )
173+ . resolves ( ) ;
174+
175+ addObserverStub . callsFake ( async ( observer : ConfigUpdateObserver ) => {
176+ capturedObserver = observer ;
177+ } ) ;
178+ } ) ;
179+
180+ afterEach ( ( ) => {
181+ capturedObserver = undefined ;
182+ addObserverStub . restore ( ) ;
183+ removeObserverStub . restore ( ) ;
184+ } ) ;
185+
186+ it ( 'should call addObserver on the internal realtimeHandler' , async ( ) => {
187+ const mockObserver = {
188+ next : sinon . stub ( ) ,
189+ error : sinon . stub ( ) ,
190+ complete : sinon . stub ( )
191+ } ;
192+ await onConfigUpdate ( rc , mockObserver ) ;
193+
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 mockObserver = {
200+ next : sinon . stub ( ) ,
201+ error : sinon . stub ( ) ,
202+ complete : sinon . stub ( )
203+ } ;
204+ const unsubscribe = await onConfigUpdate ( rc , mockObserver ) ;
205+
206+ expect ( unsubscribe ) . to . be . a ( 'function' ) ;
207+ } ) ;
208+
209+ it ( 'returned unsubscribe function should call removeObserver' , async ( ) => {
210+ const mockObserver = {
211+ next : sinon . stub ( ) ,
212+ error : sinon . stub ( ) ,
213+ complete : sinon . stub ( )
214+ } ;
215+ const unsubscribe = await onConfigUpdate ( rc , mockObserver ) ;
216+
217+ unsubscribe ( ) ;
218+
219+ expect ( removeObserverStub ) . to . have . been . calledOnce ;
220+ expect ( removeObserverStub ) . to . have . been . calledWith ( mockObserver ) ;
221+ } ) ;
222+
223+ it ( 'observer.next should be called when realtimeHandler propagates an update' , async ( ) => {
224+ const mockObserver = {
225+ next : sinon . stub ( ) ,
226+ error : sinon . stub ( ) ,
227+ complete : sinon . stub ( )
228+ } ;
229+ await onConfigUpdate ( rc , mockObserver ) ;
230+
231+ if ( capturedObserver && capturedObserver . next ) {
232+ const mockConfigUpdate = { getUpdatedKeys : ( ) => new Set ( [ 'new_key' ] ) } ;
233+ capturedObserver . next ( mockConfigUpdate ) ;
234+ } else {
235+ expect . fail ( 'Observer was not captured or next method is missing.' ) ;
236+ }
237+
238+ expect ( mockObserver . next ) . to . have . been . calledOnce ;
239+ expect ( mockObserver . next ) . to . have . been . calledWithMatch ( {
240+ getUpdatedKeys : sinon . match . func
241+ } ) ;
242+ expect (
243+ mockObserver . next . getCall ( 0 ) . args [ 0 ] . getUpdatedKeys ( )
244+ ) . to . deep . equal ( new Set ( [ 'new_key' ] ) ) ;
245+ } ) ;
246+
247+ it ( 'observer.error should be called when realtimeHandler propagates an error' , async ( ) => {
248+ const mockObserver = {
249+ next : sinon . stub ( ) ,
250+ error : sinon . stub ( ) ,
251+ complete : sinon . stub ( )
252+ } ;
253+ await onConfigUpdate ( rc , mockObserver ) ;
254+
255+ if ( capturedObserver && capturedObserver . error ) {
256+ const expectedOriginalErrorMessage = 'Realtime stream error' ;
257+ const mockError = ERROR_FACTORY . create (
258+ ErrorCode . CONFIG_UPDATE_STREAM_ERROR ,
259+ {
260+ originalErrorMessage : expectedOriginalErrorMessage
261+ }
262+ ) ;
263+ capturedObserver . error ( mockError ) ;
264+ } else {
265+ expect . fail ( 'Observer was not captured or error method is missing.' ) ;
266+ }
267+
268+ expect ( mockObserver . error ) . to . have . been . calledOnce ;
269+ const receivedError = mockObserver . error . getCall ( 0 ) . args [ 0 ] ;
270+
271+ expect ( receivedError . message ) . to . equal (
272+ 'Remote Config: The stream was not able to connect to the backend. (remoteconfig/stream-error).'
273+ ) ;
274+ expect ( receivedError ) . to . have . nested . property (
275+ 'customData.originalErrorMessage' ,
276+ 'Realtime stream error'
277+ ) ;
278+ expect ( ( receivedError as any ) . code ) . to . equal ( 'remoteconfig/stream-error' ) ; // Updated to expect the full prefixed code
279+ } ) ;
280+ } ) ;
154281} ) ;
0 commit comments