17
17
18
18
import { expect } from 'chai' ;
19
19
import {
20
+ ConfigUpdateObserver ,
20
21
ensureInitialized ,
21
22
fetchAndActivate ,
22
23
FetchResponse ,
23
24
getRemoteConfig ,
24
- getString
25
+ getString ,
26
+ onConfigUpdate
25
27
} from '../src' ;
26
28
import '../test/setup' ;
27
29
import {
@@ -34,6 +36,8 @@ import * as sinon from 'sinon';
34
36
import { Component , ComponentType } from '@firebase/component' ;
35
37
import { FirebaseInstallations } from '@firebase/installations-types' ;
36
38
import { 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' ;
37
41
38
42
const fakeFirebaseConfig = {
39
43
apiKey : 'api-key' ,
@@ -45,6 +49,12 @@ const fakeFirebaseConfig = {
45
49
appId : '1:111:web:a1234'
46
50
} ;
47
51
52
+ const mockObserver = {
53
+ next : sinon . stub ( ) ,
54
+ error : sinon . stub ( ) ,
55
+ complete : sinon . stub ( )
56
+ } ;
57
+
48
58
async function clearDatabase ( ) : Promise < void > {
49
59
const db = await openDatabase ( ) ;
50
60
db . transaction ( [ APP_NAMESPACE_STORE ] , 'readwrite' )
@@ -151,4 +161,99 @@ describe('Remote Config API', () => {
151
161
await ensureInitialized ( rc ) ;
152
162
expect ( getString ( rc , 'foobar' ) ) . to . equal ( 'hello world' ) ;
153
163
} ) ;
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
+ } ) ;
154
259
} ) ;
0 commit comments