forked from firebase/firebase-admin-node
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfirebase-app.spec.ts
More file actions
1057 lines (874 loc) · 42.8 KB
/
firebase-app.spec.ts
File metadata and controls
1057 lines (874 loc) · 42.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*!
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
// Use untyped import syntax for Node built-ins
import https = require('https');
import * as _ from 'lodash';
import * as chai from 'chai';
import * as sinon from 'sinon';
import * as sinonChai from 'sinon-chai';
import * as chaiAsPromised from 'chai-as-promised';
import * as utils from './utils';
import * as mocks from '../resources/mocks';
import {ApplicationDefaultCredential, CertCredential, GoogleOAuthAccessToken} from '../../src/auth/credential';
import {FirebaseServiceInterface} from '../../src/firebase-service';
import {FirebaseApp, FirebaseAccessToken} from '../../src/firebase-app';
import {FirebaseNamespace, FirebaseNamespaceInternals, FIREBASE_CONFIG_VAR} from '../../src/firebase-namespace';
import {Auth} from '../../src/auth/auth';
import {Messaging} from '../../src/messaging/messaging';
import {Storage} from '../../src/storage/storage';
import {Firestore} from '@google-cloud/firestore';
import {Database} from '@firebase/database';
import {InstanceId} from '../../src/instance-id/instance-id';
import {ProjectManagement} from '../../src/project-management/project-management';
import { SecurityRules } from '../../src/security-rules/security-rules';
import { FirebaseAppError, AppErrorCodes } from '../../src/utils/error';
chai.should();
chai.use(sinonChai);
chai.use(chaiAsPromised);
const expect = chai.expect;
const ONE_HOUR_IN_SECONDS = 60 * 60;
const ONE_MINUTE_IN_MILLISECONDS = 60 * 1000;
const deleteSpy = sinon.spy();
function mockServiceFactory(app: FirebaseApp): FirebaseServiceInterface {
return {
app,
INTERNAL: {
delete: deleteSpy.bind(null, app.name),
},
};
}
describe('FirebaseApp', () => {
let mockApp: FirebaseApp;
let clock: sinon.SinonFakeTimers;
let getTokenStub: sinon.SinonStub;
let firebaseNamespace: FirebaseNamespace;
let firebaseNamespaceInternals: FirebaseNamespaceInternals;
let firebaseConfigVar: string;
beforeEach(() => {
getTokenStub = sinon.stub(CertCredential.prototype, 'getAccessToken').resolves({
access_token: 'mock-access-token',
expires_in: 3600,
});
clock = sinon.useFakeTimers(1000);
mockApp = mocks.app();
firebaseConfigVar = process.env[FIREBASE_CONFIG_VAR];
delete process.env[FIREBASE_CONFIG_VAR];
firebaseNamespace = new FirebaseNamespace();
firebaseNamespaceInternals = firebaseNamespace.INTERNAL;
sinon.stub(firebaseNamespaceInternals, 'removeApp');
mockApp = new FirebaseApp(mocks.appOptions, mocks.appName, firebaseNamespaceInternals);
});
afterEach(() => {
getTokenStub.restore();
clock.restore();
if (firebaseConfigVar) {
process.env[FIREBASE_CONFIG_VAR] = firebaseConfigVar;
} else {
delete process.env[FIREBASE_CONFIG_VAR];
}
deleteSpy.resetHistory();
(firebaseNamespaceInternals.removeApp as any).restore();
});
describe('#name', () => {
it('should throw if the app has already been deleted', () => {
return mockApp.delete().then(() => {
expect(() => {
return mockApp.name;
}).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`);
});
});
it('should return the app\'s name', () => {
expect(mockApp.name).to.equal(mocks.appName);
});
it('should be case sensitive', () => {
const newMockAppName = mocks.appName.toUpperCase();
mockApp = new FirebaseApp(mocks.appOptions, newMockAppName, firebaseNamespaceInternals);
expect(mockApp.name).to.not.equal(mocks.appName);
expect(mockApp.name).to.equal(newMockAppName);
});
it('should respect leading and trailing whitespace', () => {
const newMockAppName = ' ' + mocks.appName + ' ';
mockApp = new FirebaseApp(mocks.appOptions, newMockAppName, firebaseNamespaceInternals);
expect(mockApp.name).to.not.equal(mocks.appName);
expect(mockApp.name).to.equal(newMockAppName);
});
it('should be read-only', () => {
expect(() => {
(mockApp as any).name = 'foo';
}).to.throw(`Cannot set property name of #<FirebaseApp> which has only a getter`);
});
});
describe('#options', () => {
it('should throw if the app has already been deleted', () => {
return mockApp.delete().then(() => {
expect(() => {
return mockApp.options;
}).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`);
});
});
it('should return the app\'s options', () => {
expect(mockApp.options).to.deep.equal(mocks.appOptions);
});
it('should be read-only', () => {
expect(() => {
(mockApp as any).options = {};
}).to.throw(`Cannot set property options of #<FirebaseApp> which has only a getter`);
});
it('should not return an object which can mutate the underlying options', () => {
const original = _.clone(mockApp.options);
(mockApp.options as any).foo = 'changed';
expect(mockApp.options).to.deep.equal(original);
});
it('should ignore the config file when options is not null', () => {
process.env[FIREBASE_CONFIG_VAR] = './test/resources/firebase_config.json';
const app = firebaseNamespace.initializeApp(mocks.appOptionsNoDatabaseUrl, mocks.appName);
expect(app.options.databaseAuthVariableOverride).to.be.undefined;
expect(app.options.databaseURL).to.undefined;
expect(app.options.projectId).to.be.undefined;
expect(app.options.storageBucket).to.undefined;
});
it('should throw when the environment variable points to non existing file', () => {
process.env[FIREBASE_CONFIG_VAR] = './test/resources/non_existant.json';
expect(() => {
firebaseNamespace.initializeApp();
}).to.throw(`Failed to parse app options file: Error: ENOENT: no such file or directory`);
});
it('should throw when the environment variable contains bad json', () => {
process.env[FIREBASE_CONFIG_VAR] = '{,,';
expect(() => {
firebaseNamespace.initializeApp();
}).to.throw(`Failed to parse app options file: SyntaxError: Unexpected token ,`);
});
it('should throw when the environment variable points to an empty file', () => {
process.env[FIREBASE_CONFIG_VAR] = './test/resources/firebase_config_empty.json';
expect(() => {
firebaseNamespace.initializeApp();
}).to.throw(`Failed to parse app options file`);
});
it('should throw when the environment variable points to bad json', () => {
process.env[FIREBASE_CONFIG_VAR] = './test/resources/firebase_config_bad.json';
expect(() => {
firebaseNamespace.initializeApp();
}).to.throw(`Failed to parse app options file`);
});
it('should ignore a bad config key in the config file', () => {
process.env[FIREBASE_CONFIG_VAR] = './test/resources/firebase_config_bad_key.json';
const app = firebaseNamespace.initializeApp();
expect(app.options.projectId).to.equal('hipster-chat-mock');
expect(app.options.databaseAuthVariableOverride).to.be.undefined;
expect(app.options.databaseURL).to.undefined;
expect(app.options.storageBucket).to.undefined;
});
it('should ignore a bad config key in the json string', () => {
process.env[FIREBASE_CONFIG_VAR] =
`{
"notAValidKeyValue": "The key value here is not valid.",
"projectId": "hipster-chat-mock"
}`;
const app = firebaseNamespace.initializeApp();
expect(app.options.projectId).to.equal('hipster-chat-mock');
expect(app.options.databaseAuthVariableOverride).to.be.undefined;
expect(app.options.databaseURL).to.undefined;
expect(app.options.storageBucket).to.undefined;
});
it('should not throw when the config file has a bad key and the config file is unused', () => {
process.env[FIREBASE_CONFIG_VAR] = './test/resources/firebase_config_bad_key.json';
const app = firebaseNamespace.initializeApp(mocks.appOptionsWithOverride, mocks.appName);
expect(app.options.projectId).to.equal('project_id');
expect(app.options.databaseAuthVariableOverride).to.deep.equal({ 'some#string': 'some#val' });
expect(app.options.databaseURL).to.equal('https://databaseName.firebaseio.com');
expect(app.options.storageBucket).to.equal('bucketName.appspot.com');
});
it('should not throw when the config json has a bad key and the config json is unused', () => {
process.env[FIREBASE_CONFIG_VAR] =
`{
"notAValidKeyValue": "The key value here is not valid.",
"projectId": "hipster-chat-mock"
}`;
const app = firebaseNamespace.initializeApp(mocks.appOptionsWithOverride, mocks.appName);
expect(app.options.projectId).to.equal('project_id');
expect(app.options.databaseAuthVariableOverride).to.deep.equal({ 'some#string': 'some#val' });
expect(app.options.databaseURL).to.equal('https://databaseName.firebaseio.com');
expect(app.options.storageBucket).to.equal('bucketName.appspot.com');
});
it('should use explicitly specified options when available and ignore the config file', () => {
process.env[FIREBASE_CONFIG_VAR] = './test/resources/firebase_config.json';
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
expect(app.options.credential).to.be.instanceOf(CertCredential);
expect(app.options.databaseAuthVariableOverride).to.be.undefined;
expect(app.options.databaseURL).to.equal('https://databaseName.firebaseio.com');
expect(app.options.projectId).to.be.undefined;
expect(app.options.storageBucket).to.equal('bucketName.appspot.com');
});
it('should not throw if some fields are missing', () => {
process.env[FIREBASE_CONFIG_VAR] = './test/resources/firebase_config_partial.json';
const app = firebaseNamespace.initializeApp(mocks.appOptionsAuthDB, mocks.appName);
expect(app.options.databaseURL).to.equal('https://databaseName.firebaseio.com');
expect(app.options.projectId).to.be.undefined;
expect(app.options.storageBucket).to.be.undefined;
});
it('should not throw when the config environment variable is not set, and some options are present', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptionsNoDatabaseUrl, mocks.appName);
expect(app.options.credential).to.be.instanceOf(CertCredential);
expect(app.options.databaseURL).to.be.undefined;
expect(app.options.projectId).to.be.undefined;
expect(app.options.storageBucket).to.be.undefined;
});
it('should init with application default creds when no options provided and env variable is not set', () => {
const app = firebaseNamespace.initializeApp();
expect(app.options.credential).to.be.instanceOf(ApplicationDefaultCredential);
expect(app.options.databaseURL).to.be.undefined;
expect(app.options.projectId).to.be.undefined;
expect(app.options.storageBucket).to.be.undefined;
});
it('should init with application default creds when no options provided and env variable is an empty json', () => {
process.env[FIREBASE_CONFIG_VAR] = '{}';
const app = firebaseNamespace.initializeApp();
expect(app.options.credential).to.be.instanceOf(ApplicationDefaultCredential);
expect(app.options.databaseURL).to.be.undefined;
expect(app.options.projectId).to.be.undefined;
expect(app.options.storageBucket).to.be.undefined;
});
it('should init when no init arguments are provided and config var points to a file', () => {
process.env[FIREBASE_CONFIG_VAR] = './test/resources/firebase_config.json';
const app = firebaseNamespace.initializeApp();
expect(app.options.credential).to.be.instanceOf(ApplicationDefaultCredential);
expect(app.options.databaseAuthVariableOverride).to.deep.equal({ 'some#key': 'some#val' });
expect(app.options.databaseURL).to.equal('https://hipster-chat.firebaseio.mock');
expect(app.options.projectId).to.equal('hipster-chat-mock');
expect(app.options.storageBucket).to.equal('hipster-chat.appspot.mock');
});
it('should init when no init arguments are provided and config var is json', () => {
process.env[FIREBASE_CONFIG_VAR] = `{
"databaseAuthVariableOverride": { "some#key": "some#val" },
"databaseURL": "https://hipster-chat.firebaseio.mock",
"projectId": "hipster-chat-mock",
"storageBucket": "hipster-chat.appspot.mock"
}`;
const app = firebaseNamespace.initializeApp();
expect(app.options.credential).to.be.instanceOf(ApplicationDefaultCredential);
expect(app.options.databaseAuthVariableOverride).to.deep.equal({ 'some#key': 'some#val' });
expect(app.options.databaseURL).to.equal('https://hipster-chat.firebaseio.mock');
expect(app.options.projectId).to.equal('hipster-chat-mock');
expect(app.options.storageBucket).to.equal('hipster-chat.appspot.mock');
});
});
describe('#delete()', () => {
it('should throw if the app has already been deleted', () => {
return mockApp.delete().then(() => {
expect(() => {
return mockApp.delete();
}).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`);
});
});
it('should call removeApp() on the Firebase namespace internals', () => {
return mockApp.delete().then(() => {
expect(firebaseNamespaceInternals.removeApp)
.to.have.been.calledOnce
.and.calledWith(mocks.appName);
});
});
it('should call delete() on each service\'s internals', () => {
firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mockServiceFactory);
firebaseNamespace.INTERNAL.registerService(mocks.serviceName + '2', mockServiceFactory);
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
(app as {[key: string]: any})[mocks.serviceName]();
(app as {[key: string]: any})[mocks.serviceName + '2']();
return app.delete().then(() => {
expect(deleteSpy).to.have.been.calledTwice;
expect(deleteSpy.firstCall.args).to.deep.equal([mocks.appName]);
expect(deleteSpy.secondCall.args).to.deep.equal([mocks.appName]);
});
});
});
describe('auth()', () => {
it('should throw if the app has already been deleted', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
return app.delete().then(() => {
expect(() => {
return app.auth();
}).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`);
});
});
it('should return the Auth namespace', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const authNamespace: Auth = app.auth();
expect(authNamespace).not.be.null;
});
it('should return a cached version of Auth on subsequent calls', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const serviceNamespace1: Auth = app.auth();
const serviceNamespace2: Auth = app.auth();
expect(serviceNamespace1).to.deep.equal(serviceNamespace2);
});
});
describe('messaging()', () => {
it('should throw if the app has already been deleted', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
return app.delete().then(() => {
expect(() => {
return app.messaging();
}).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`);
});
});
it('should return the Messaging namespace', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const fcmNamespace: Messaging = app.messaging();
expect(fcmNamespace).not.be.null;
});
it('should return a cached version of Messaging on subsequent calls', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const serviceNamespace1: Messaging = app.messaging();
const serviceNamespace2: Messaging = app.messaging();
expect(serviceNamespace1).to.deep.equal(serviceNamespace2);
});
});
describe('database()', () => {
afterEach(() => {
try {
firebaseNamespace.app(mocks.appName).delete();
} catch (e) {
// ignore
}
});
it('should throw if the app has already been deleted', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
return app.delete().then(() => {
expect(() => {
return app.database();
}).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`);
});
});
it('should return the Database', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const db: Database = app.database();
expect(db).not.be.null;
});
it('should return the Database for different apps', () => {
const app1 = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const app2 = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName + '-other');
const db1: Database = app1.database();
const db2: Database = app2.database();
expect(db1).to.not.deep.equal(db2);
expect(db1.ref().toString()).to.equal('https://databasename.firebaseio.com/');
expect(db2.ref().toString()).to.equal('https://databasename.firebaseio.com/');
return app2.delete();
});
it('should throw when databaseURL is not set', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptionsNoDatabaseUrl, mocks.appName);
expect(() => {
app.database();
}).to.throw('Can\'t determine Firebase Database URL.');
});
it('should return a cached version of Database on subsequent calls', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const db1: Database = app.database();
const db2: Database = app.database();
const db3: Database = app.database(mocks.appOptions.databaseURL);
expect(db1).to.equal(db2);
expect(db1).to.equal(db3);
expect(db1.ref().toString()).to.equal('https://databasename.firebaseio.com/');
});
it('should return a Database instance for the specified URL', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const db1: Database = app.database();
const db2: Database = app.database('https://other-database.firebaseio.com');
expect(db1.ref().toString()).to.equal('https://databasename.firebaseio.com/');
expect(db2.ref().toString()).to.equal('https://other-database.firebaseio.com/');
});
const invalidArgs = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop];
invalidArgs.forEach((url) => {
it(`should throw given invalid URL argument: ${JSON.stringify(url)}`, () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
expect(() => {
(app as any).database(url);
}).to.throw('Database URL must be a valid, non-empty URL string.');
});
});
const invalidUrls = ['a', 'foo', 'google.com'];
invalidUrls.forEach((url) => {
it(`should throw given invalid URL string: '${url}'`, () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
expect(() => {
app.database(url);
}).to.throw('FIREBASE FATAL ERROR: Cannot parse Firebase url. ' +
'Please use https://<YOUR FIREBASE>.firebaseio.com');
});
});
});
describe('storage()', () => {
it('should throw if the app has already been deleted', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
return app.delete().then(() => {
expect(() => {
return app.storage();
}).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`);
});
});
it('should return the Storage namespace', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const gcsNamespace: Storage = app.storage();
expect(gcsNamespace).not.be.null;
});
it('should return a cached version of Messaging on subsequent calls', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const serviceNamespace1: Storage = app.storage();
const serviceNamespace2: Storage = app.storage();
expect(serviceNamespace1).to.deep.equal(serviceNamespace2);
});
});
describe('firestore()', () => {
it('should throw if the app has already been deleted', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
return app.delete().then(() => {
expect(() => {
return app.firestore();
}).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`);
});
});
it('should return the Firestore client', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const fs: Firestore = app.firestore();
expect(fs).not.be.null;
});
it('should return a cached version of Firestore on subsequent calls', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const service1: Firestore = app.firestore();
const service2: Firestore = app.firestore();
expect(service1).to.deep.equal(service2);
});
});
describe('instanceId()', () => {
it('should throw if the app has already been deleted', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
return app.delete().then(() => {
expect(() => {
return app.instanceId();
}).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`);
});
});
it('should return the InstanceId client', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const iid: InstanceId = app.instanceId();
expect(iid).not.be.null;
});
it('should return a cached version of InstanceId on subsequent calls', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const service1: InstanceId = app.instanceId();
const service2: InstanceId = app.instanceId();
expect(service1).to.equal(service2);
});
});
describe('projectManagement()', () => {
it('should throw if the app has already been deleted', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
return app.delete().then(() => {
expect(() => {
return app.projectManagement();
}).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`);
});
});
it('should return the projectManagement client', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const projectManagement: ProjectManagement = app.projectManagement();
expect(projectManagement).to.not.be.null;
});
it('should return a cached version of ProjectManagement on subsequent calls', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const service1: ProjectManagement = app.projectManagement();
const service2: ProjectManagement = app.projectManagement();
expect(service1).to.equal(service2);
});
});
describe('securityRules()', () => {
it('should throw if the app has already been deleted', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
return app.delete().then(() => {
expect(() => {
return app.securityRules();
}).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`);
});
});
it('should return the securityRules client', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const securityRules: SecurityRules = app.securityRules();
expect(securityRules).to.not.be.null;
});
it('should return a cached version of SecurityRules on subsequent calls', () => {
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const service1: SecurityRules = app.securityRules();
const service2: SecurityRules = app.securityRules();
expect(service1).to.equal(service2);
});
});
describe('#[service]()', () => {
it('should throw if the app has already been deleted', () => {
firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mockServiceFactory);
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
return app.delete().then(() => {
expect(() => {
return (app as {[key: string]: any})[mocks.serviceName]();
}).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`);
});
});
it('should return the service namespace', () => {
firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mockServiceFactory);
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
const serviceNamespace = (app as {[key: string]: any})[mocks.serviceName]();
expect(serviceNamespace).to.have.keys(['app', 'INTERNAL']);
});
it('should return a cached version of the service on subsequent calls', () => {
const createServiceSpy = sinon.spy();
firebaseNamespace.INTERNAL.registerService(mocks.serviceName, createServiceSpy);
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
expect(createServiceSpy).to.not.have.been.called;
const serviceNamespace1 = (app as {[key: string]: any})[mocks.serviceName]();
expect(createServiceSpy).to.have.been.calledOnce;
const serviceNamespace2 = (app as {[key: string]: any})[mocks.serviceName]();
expect(createServiceSpy).to.have.been.calledOnce;
expect(serviceNamespace1).to.deep.equal(serviceNamespace2);
});
});
describe('INTERNAL.getToken()', () => {
it('throws a custom credential implementation which returns invalid access tokens', () => {
const credential = {
getAccessToken: () => 5,
};
const app = utils.createAppWithOptions({
credential: credential as any,
});
return app.INTERNAL.getToken().then(() => {
throw new Error('Unexpected success');
}, (err) => {
expect(err.toString()).to.include('Invalid access token generated');
});
});
it('returns a valid token given a well-formed custom credential implementation', () => {
const oracle: GoogleOAuthAccessToken = {
access_token: 'This is a custom token',
expires_in: ONE_HOUR_IN_SECONDS,
};
const credential = {
getAccessToken: () => Promise.resolve(oracle),
};
const app = utils.createAppWithOptions({credential});
return app.INTERNAL.getToken().then((token) => {
expect(token.accessToken).to.equal(oracle.access_token);
expect(+token.expirationTime).to.equal((ONE_HOUR_IN_SECONDS + 1) * 1000);
});
});
it('returns a valid token given no arguments', () => {
return mockApp.INTERNAL.getToken().then((token) => {
expect(token).to.have.keys(['accessToken', 'expirationTime']);
expect(token.accessToken).to.be.a('string').and.to.not.be.empty;
expect(token.expirationTime).to.be.a('number');
});
});
it('returns a valid token with force refresh', () => {
return mockApp.INTERNAL.getToken(true).then((token) => {
expect(token).to.have.keys(['accessToken', 'expirationTime']);
expect(token.accessToken).to.be.a('string').and.to.not.be.empty;
expect(token.expirationTime).to.be.a('number');
});
});
it('returns the cached token given no arguments', () => {
return mockApp.INTERNAL.getToken(true).then((token1) => {
clock.tick(1000);
return mockApp.INTERNAL.getToken().then((token2) => {
expect(token1).to.deep.equal(token2);
expect(getTokenStub).to.have.been.calledOnce;
});
});
});
it('returns a new token with force refresh', () => {
return mockApp.INTERNAL.getToken(true).then((token1) => {
clock.tick(1000);
return mockApp.INTERNAL.getToken(true).then((token2) => {
expect(token1).to.not.deep.equal(token2);
expect(getTokenStub).to.have.been.calledTwice;
});
});
});
it('proactively refreshes the token five minutes before it expires', () => {
// Force a token refresh.
return mockApp.INTERNAL.getToken(true).then((token1) => {
// Forward the clock to five minutes and one second before expiry.
const expiryInMilliseconds = token1.expirationTime - Date.now();
clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS) - 1000);
return mockApp.INTERNAL.getToken().then((token2) => {
// Ensure the token has not been proactively refreshed.
expect(token1).to.deep.equal(token2);
expect(getTokenStub).to.have.been.calledOnce;
// Forward the clock to exactly five minutes before expiry.
clock.tick(1000);
return mockApp.INTERNAL.getToken().then((token3) => {
// Ensure the token was proactively refreshed.
expect(token1).to.not.deep.equal(token3);
expect(getTokenStub).to.have.been.calledTwice;
});
});
});
});
it('retries to proactively refresh the token if a proactive refresh attempt fails', () => {
// Force a token refresh.
return mockApp.INTERNAL.getToken(true).then((token1) => {
// Stub the getToken() method to return a rejected promise.
getTokenStub.restore();
getTokenStub = sinon.stub(mockApp.options.credential, 'getAccessToken')
.rejects(new Error('Intentionally rejected'));
// Forward the clock to exactly five minutes before expiry.
const expiryInMilliseconds = token1.expirationTime - Date.now();
clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS));
// Forward the clock to exactly four minutes before expiry.
clock.tick(60 * 1000);
// Restore the stubbed getAccessToken() method.
getTokenStub.restore();
getTokenStub = sinon.stub(CertCredential.prototype, 'getAccessToken').resolves({
access_token: 'mock-access-token',
expires_in: 3600,
});
return mockApp.INTERNAL.getToken().then((token2) => {
// Ensure the token has not been proactively refreshed.
expect(token1).to.deep.equal(token2);
expect(getTokenStub).to.have.not.been.called;
// Forward the clock to exactly three minutes before expiry.
clock.tick(60 * 1000);
return mockApp.INTERNAL.getToken().then((token3) => {
// Ensure the token was proactively refreshed.
expect(token1).to.not.deep.equal(token3);
expect(getTokenStub).to.have.been.calledOnce;
});
});
});
});
it('stops retrying to proactively refresh the token after five attempts', () => {
// Force a token refresh.
let originalToken: FirebaseAccessToken;
return mockApp.INTERNAL.getToken(true).then((token) => {
originalToken = token;
// Stub the credential's getAccessToken() method to always return a rejected promise.
getTokenStub.restore();
getTokenStub = sinon.stub(mockApp.options.credential, 'getAccessToken')
.rejects(new Error('Intentionally rejected'));
// Expect the call count to initially be zero.
expect(getTokenStub.callCount).to.equal(0);
// Forward the clock to exactly five minutes before expiry.
const expiryInMilliseconds = token.expirationTime - Date.now();
clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS));
// Due to synchronous timing issues when the timer is mocked, make a call to getToken()
// without forcing a refresh to ensure there is enough time for the underlying token refresh
// timeout to fire and complete.
return mockApp.INTERNAL.getToken();
}).then((token) => {
// Ensure the token was attempted to be proactively refreshed one time.
expect(getTokenStub.callCount).to.equal(1);
// Ensure the proactive refresh failed.
expect(token).to.deep.equal(originalToken);
// Forward the clock to four minutes before expiry.
clock.tick(ONE_MINUTE_IN_MILLISECONDS);
// See note above about calling getToken().
return mockApp.INTERNAL.getToken();
}).then((token) => {
// Ensure the token was attempted to be proactively refreshed two times.
expect(getTokenStub.callCount).to.equal(2);
// Ensure the proactive refresh failed.
expect(token).to.deep.equal(originalToken);
// Forward the clock to three minutes before expiry.
clock.tick(ONE_MINUTE_IN_MILLISECONDS);
// See note above about calling getToken().
return mockApp.INTERNAL.getToken();
}).then((token) => {
// Ensure the token was attempted to be proactively refreshed three times.
expect(getTokenStub.callCount).to.equal(3);
// Ensure the proactive refresh failed.
expect(token).to.deep.equal(originalToken);
// Forward the clock to two minutes before expiry.
clock.tick(ONE_MINUTE_IN_MILLISECONDS);
// See note above about calling getToken().
return mockApp.INTERNAL.getToken();
}).then((token) => {
// Ensure the token was attempted to be proactively refreshed four times.
expect(getTokenStub.callCount).to.equal(4);
// Ensure the proactive refresh failed.
expect(token).to.deep.equal(originalToken);
// Forward the clock to one minute before expiry.
clock.tick(ONE_MINUTE_IN_MILLISECONDS);
// See note above about calling getToken().
return mockApp.INTERNAL.getToken();
}).then((token) => {
// Ensure the token was attempted to be proactively refreshed five times.
expect(getTokenStub.callCount).to.equal(5);
// Ensure the proactive refresh failed.
expect(token).to.deep.equal(originalToken);
// Forward the clock to expiry.
clock.tick(ONE_MINUTE_IN_MILLISECONDS);
// See note above about calling getToken().
return mockApp.INTERNAL.getToken();
}).then((token) => {
// Ensure the token was not attempted to be proactively refreshed a sixth time.
expect(getTokenStub.callCount).to.equal(5);
// Ensure the token has never been refresh.
expect(token).to.deep.equal(originalToken);
});
});
it('resets the proactive refresh timeout upon a force refresh', () => {
// Force a token refresh.
return mockApp.INTERNAL.getToken(true).then((token1) => {
// Forward the clock to five minutes and one second before expiry.
let expiryInMilliseconds = token1.expirationTime - Date.now();
clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS) - 1000);
// Force a token refresh.
return mockApp.INTERNAL.getToken(true).then((token2) => {
// Ensure the token was force refreshed.
expect(token1).to.not.deep.equal(token2);
expect(getTokenStub).to.have.been.calledTwice;
// Forward the clock to exactly five minutes before the original token's expiry.
clock.tick(1000);
return mockApp.INTERNAL.getToken().then((token3) => {
// Ensure the token hasn't changed, meaning the proactive refresh was canceled.
expect(token2).to.deep.equal(token3);
expect(getTokenStub).to.have.been.calledTwice;
// Forward the clock to exactly five minutes before the refreshed token's expiry.
expiryInMilliseconds = token3.expirationTime - Date.now();
clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS));
return mockApp.INTERNAL.getToken().then((token4) => {
// Ensure the token was proactively refreshed.
expect(token3).to.not.deep.equal(token4);
expect(getTokenStub).to.have.been.calledThrice;
});
});
});
});
});
it('proactively refreshes the token at the next full minute if it expires in five minutes or less', () => {
// Turn off default mocking of one hour access tokens and replace it with a short-lived token.
getTokenStub.restore();
getTokenStub = sinon.stub(mockApp.options.credential, 'getAccessToken').resolves({
access_token: utils.generateRandomAccessToken(),
expires_in: 3 * 60 + 10,
});
// Expect the call count to initially be zero.
expect(getTokenStub.callCount).to.equal(0);
// Force a token refresh.
return mockApp.INTERNAL.getToken(true).then((token1) => {
// Move the clock forward to three minutes and one second before expiry.
clock.tick(9 * 1000);
expect(getTokenStub.callCount).to.equal(1);
// Move the clock forward to exactly three minutes before expiry.
clock.tick(1000);
// Expect the underlying getAccessToken() method to have been called once.
expect(getTokenStub.callCount).to.equal(2);
return mockApp.INTERNAL.getToken().then((token2) => {
// Ensure the token was proactively refreshed.
expect(token1).to.not.deep.equal(token2);
});
});
});
it('Includes the original error in exception', () => {
getTokenStub.restore();
const mockError = new FirebaseAppError(
AppErrorCodes.INVALID_CREDENTIAL, 'Something went wrong');
getTokenStub = sinon.stub(CertCredential.prototype, 'getAccessToken').rejects(mockError);
const detailedMessage = 'Credential implementation provided to initializeApp() via the "credential" property'
+ ' failed to fetch a valid Google OAuth2 access token with the following error: "Something went wrong".';
expect(mockApp.INTERNAL.getToken(true)).to.be.rejectedWith(detailedMessage);
});
it('Returns a detailed message when an error is due to an invalid_grant', () => {
getTokenStub.restore();
const mockError = new FirebaseAppError(
AppErrorCodes.INVALID_CREDENTIAL, 'Failed to get credentials: invalid_grant (reason)');
getTokenStub = sinon.stub(CertCredential.prototype, 'getAccessToken').rejects(mockError);
const detailedMessage = 'Credential implementation provided to initializeApp() via the "credential" property'
+ ' failed to fetch a valid Google OAuth2 access token with the following error: "Failed to get credentials:'
+ ' invalid_grant (reason)". There are two likely causes: (1) your server time is not properly synced or (2)'
+ ' your certificate key file has been revoked. To solve (1), re-sync the time on your server. To solve (2),'
+ ' make sure the key ID for your key file is still present at '
+ 'https://console.firebase.google.com/iam-admin/serviceaccounts/project. If not, generate a new key file '
+ 'at https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk.';
expect(mockApp.INTERNAL.getToken(true)).to.be.rejectedWith(detailedMessage);
});
});
describe('INTERNAL.addAuthTokenListener()', () => {
let addAuthTokenListenerSpy: sinon.SinonSpy;
before(() => {
addAuthTokenListenerSpy = sinon.spy();
});
afterEach(() => {
addAuthTokenListenerSpy.resetHistory();
});
it('is notified when the token changes', () => {
mockApp.INTERNAL.addAuthTokenListener(addAuthTokenListenerSpy);
return mockApp.INTERNAL.getToken().then((token: FirebaseAccessToken) => {
expect(addAuthTokenListenerSpy).to.have.been.calledOnce.and.calledWith(token.accessToken);
});
});
it('can be called twice', () => {
mockApp.INTERNAL.addAuthTokenListener(addAuthTokenListenerSpy);
mockApp.INTERNAL.addAuthTokenListener(addAuthTokenListenerSpy);
return mockApp.INTERNAL.getToken().then((token: FirebaseAccessToken) => {
expect(addAuthTokenListenerSpy).to.have.been.calledTwice;
expect(addAuthTokenListenerSpy.firstCall).to.have.been.calledWith(token.accessToken);
expect(addAuthTokenListenerSpy.secondCall).to.have.been.calledWith(token.accessToken);
});
});
it('will be called on token refresh', () => {
mockApp.INTERNAL.addAuthTokenListener(addAuthTokenListenerSpy);