Skip to content

Commit b0e670d

Browse files
committed
Implement onIdTokenChanged and update onAuthStateChanged
Firebase SDK [Version 4.0.0](https://firebase.google.com/support/release-notes/js#version_500_-_may_8_2018) changed the behavior of `onAuthStateChanged` and added a new event handler called `onIdTokenChanged`. This patch mocks that change. onIdTokenChanged now implements the former behavior of onAuthStateChanged. onAuthStateChanged now signals only when the actual user changes, not when a new ID token is set for the same user.
1 parent 6469bd9 commit b0e670d

File tree

4 files changed

+200
-21
lines changed

4 files changed

+200
-21
lines changed

API.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,9 @@ Authentication methods for simulating changes to the auth state of a Firebase re
175175
Changes the active authentication credentials to the `authData` object.
176176
Before changing the authentication state, `changeAuthState` checks the
177177
`user` object against the current authentication data.
178-
`onAuthStateChanged` listeners will only be triggered if the data is not
179-
deeply equal.
178+
`onIdTokenChanged` listeners will be triggered if the data is not
179+
deeply equal. `onAuthStateChanged` listeners will be triggered if the
180+
data is deeply equal but with different ID token validity.
180181

181182
`user` should be a `MockUser` object or an object with the same fields
182183
as `MockUser`. To simulate no user being authenticated, pass `null` for
@@ -225,9 +226,13 @@ Finds a user previously created with [`createUser`](https://www.firebase.com/doc
225226
##### `updateUser(user)` -> `Promise<MockUser>`
226227

227228
Replace the existing user with a new one, by matching uid. Throws an
228-
error if no user exists whose uid matches the given user's uid. Resolves
229-
with the updated user when complete. This operation is queued until the
230-
next flush.
229+
error if no user exists whose uid matches the given user's uid.
230+
Appropriate `onAuthStateChanged` and `onIdTokenChanged` listeners will
231+
be triggered if the new user has the same `uid` as the current
232+
authenticated user.
233+
234+
Resolves with the updated user when complete. This operation is queued
235+
until the next flush.
231236

232237
## Server Timestamps
233238

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88
### Added
99
- Changelog
10+
- mock `auth.Auth.onIdTokenChanged()` method, matching the previous
11+
behavior of `onAuthStateChanged()` (see below)
1012

1113
### Changed
14+
- (Breaking) Consistent with Firebase SDK
15+
[version 4.0.0](https://firebase.google.com/support/release-notes/js#version_500_-_may_8_2018) and later,
16+
and later, `onAuthStateChanged` no longer issues an event when a new
17+
ID token is issued for the same user. The `onIdTokenChanged` method is
18+
now mocked, keeping the previous behavior.
1219
- `MockStorageFile.download()` now allows omitting the destination arg;
1320
in that case, it simply resolves the `Promise` with the file contents
1421
and does not write it anywhere else.

src/firebase-auth.js

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ function FirebaseAuth () {
99
this.currentUser = null;
1010
this._auth = {
1111
listeners: [],
12+
idTokenListeners: [],
1213
completionListeners: [],
1314
users: [],
1415
uidCounter: 1
@@ -17,10 +18,10 @@ function FirebaseAuth () {
1718

1819
FirebaseAuth.prototype.changeAuthState = function (userData) {
1920
this._defer('changeAuthState', _.toArray(arguments), function() {
20-
if (!_.isEqual(this.currentUser, userData)) {
21-
this.currentUser = _.isObject(userData) ? userData : null;
22-
this._triggerAuthEvent();
23-
}
21+
userData = _.isObject(userData) ? userData : null;
22+
const oldUser = _.cloneDeep(this.currentUser);
23+
this.currentUser = userData;
24+
this._notify_state_listeners(oldUser);
2425
});
2526
};
2627

@@ -47,6 +48,13 @@ FirebaseAuth.prototype.onAuthStateChanged = function (callback) {
4748
}
4849
};
4950

51+
FirebaseAuth.prototype.onIdTokenChanged = function (callback) {
52+
const currentUser = this.currentUser;
53+
this._auth.idTokenListeners.push({fn: callback});
54+
callback.call(null, _.cloneDeep(currentUser));
55+
return () => this.offAuth(callback);
56+
};
57+
5058
FirebaseAuth.prototype.getUserByEmail = function (email, onComplete) {
5159
var err = this._nextErr('getUserByEmail');
5260
var self = this;
@@ -100,6 +108,11 @@ FirebaseAuth.prototype.updateUser = function (newUser) {
100108
reject(new Error('Tried to update a nonexistent user'));
101109
} else {
102110
self._auth.users[i] = newUser.clone();
111+
if (this.currentUser && this.currentUser.uid === newUser.uid) {
112+
const oldUser = this.currentUser.clone();
113+
this.currentUser = self._auth.users[i];
114+
this._notify_state_listeners(oldUser);
115+
}
103116
resolve(newUser);
104117
}
105118
});
@@ -175,7 +188,6 @@ Object.keys(signinMethods)
175188
self._triggerAuthEvent();
176189
}, true);
177190
});
178-
return promise;
179191
};
180192
});
181193

@@ -218,6 +230,15 @@ FirebaseAuth.prototype._triggerAuthEvent = function () {
218230
listeners.forEach(function (parts) {
219231
parts.fn.call(parts.context, _.cloneDeep(user));
220232
});
233+
this._triggerIdTokenEvent();
234+
};
235+
236+
FirebaseAuth.prototype._triggerIdTokenEvent = function () {
237+
var user = this.currentUser;
238+
var listeners = _.cloneDeep(this._auth.idTokenListeners);
239+
listeners.forEach(function (parts) {
240+
parts.fn.call(parts.context, _.cloneDeep(user));
241+
});
221242
};
222243

223244
FirebaseAuth.prototype._getUser = function (uid) {
@@ -239,12 +260,11 @@ FirebaseAuth.prototype.onAuth = function (onComplete, context) {
239260
};
240261

241262
FirebaseAuth.prototype.offAuth = function (onComplete, context) {
242-
var index = _.findIndex(this._auth.listeners, function (listener) {
263+
function shouldRemove(listener) {
243264
return listener.fn === onComplete && listener.context === context;
244-
});
245-
if (index > -1) {
246-
this._auth.listeners.splice(index, 1);
247265
}
266+
[this._auth.listeners, this._auth.idTokenListeners]
267+
.forEach(event => _.remove(event, shouldRemove));
248268
};
249269

250270
FirebaseAuth.prototype.unauth = function () {
@@ -502,6 +522,46 @@ FirebaseAuth.prototype.setCustomUserClaims = function (uid, claims) {
502522
});
503523
};
504524

525+
FirebaseAuth.prototype._notify_state_listeners = function (previousUser) {
526+
const difference = scanDifference(previousUser, this.currentUser);
527+
if (difference === 'different_user') {
528+
this._triggerAuthEvent();
529+
} else if(difference === 'different_token') {
530+
this._triggerIdTokenEvent();
531+
} else if (difference === 'same') {
532+
// do nothing
533+
} else {
534+
throw new Error('Unexpected result from scanDifference');
535+
}
536+
537+
function scanDifference(oldUser, newUser) {
538+
if (_.isObject(oldUser)) {
539+
if (_.isObject(newUser)) {
540+
if (_.isEqual(oldUser, newUser)) {
541+
return 'same';
542+
} else {
543+
return equalExceptToken(oldUser, newUser) ?
544+
'different_token' : 'different_user';
545+
}
546+
} else {
547+
return 'different_user';
548+
}
549+
} else {
550+
return _.isObject(newUser) ? 'different_user' : 'same';
551+
}
552+
}
553+
554+
function equalExceptToken(user1, user2) {
555+
const u1 = user1.clone();
556+
const u2 = user2.clone();
557+
delete u1._idtoken;
558+
delete u1._tokenValidity;
559+
delete u2._idtoken;
560+
delete u2._tokenValidity;
561+
return _.isEqual(u1, u2);
562+
}
563+
};
564+
505565
FirebaseAuth.prototype._nextUid = function () {
506566
return 'simplelogin:' + (this._auth.uidCounter++);
507567
};

test/unit/auth.js

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -315,13 +315,9 @@ describe('Auth', function () {
315315

316316
});
317317

318-
describe('#onAuthStateChanged', function () {
318+
function onAuthChangeSharedProperties(it, unsubscribe) {
319319

320-
beforeEach(() => {
321-
ref.onAuthStateChanged(spy);
322-
});
323-
324-
it('is triggered when changeAuthState modifies data', function () {
320+
it('is triggered when changeAuthState modifies the user', function () {
325321
ref.changeAuthState({
326322
uid: 'kato'
327323
});
@@ -331,7 +327,7 @@ describe('Auth', function () {
331327
});
332328
});
333329

334-
it('is not triggered if auth state does not change', function () {
330+
it('is not triggered when auth state does not change', function () {
335331
ref.changeAuthState({
336332
uid: 'kato'
337333
});
@@ -347,6 +343,117 @@ describe('Auth', function () {
347343
it('synchronously triggers the callback with the current auth data', function () {
348344
expect(spy).to.have.been.calledWith(null);
349345
});
346+
347+
it('does not trigger after unsubscribe', function () {
348+
unsubscribe();
349+
spy.reset();
350+
ref.changeAuthState(new User(ref, {
351+
uid: 'kato',
352+
}));
353+
ref.flush();
354+
expect(spy.callCount).to.equal(0);
355+
});
356+
}
357+
358+
describe('#onIdTokenChanged', () => {
359+
360+
let unsubscribe;
361+
362+
beforeEach(() => {
363+
unsubscribe = ref.onIdTokenChanged(spy);
364+
});
365+
366+
onAuthChangeSharedProperties(it, () => unsubscribe());
367+
368+
it('is triggered if only ID token changes', () => {
369+
const user = new User(ref, {
370+
uid: 'kato',
371+
372+
});
373+
ref.createUser(user);
374+
ref.changeAuthState(user);
375+
ref.flush();
376+
spy.reset();
377+
const userWithNewToken = user.clone();
378+
userWithNewToken._idtoken = Math.random().toString();
379+
ref.changeAuthState(userWithNewToken);
380+
ref.flush();
381+
expect(spy.called).to.equal(true);
382+
});
383+
384+
it('is triggered on updating current user', () => {
385+
const user = new User(ref, {
386+
uid: 'kato',
387+
388+
});
389+
ref.createUser(user);
390+
ref.changeAuthState(user);
391+
ref.flush();
392+
spy.reset();
393+
const userWithNewToken = user.clone();
394+
userWithNewToken._idtoken = Math.random().toString();
395+
ref.updateUser(userWithNewToken);
396+
ref.flush();
397+
expect(spy.called).to.equal(true);
398+
});
399+
400+
it('is not triggered on updating non-current user', () => {
401+
const currentUser = new User(ref, {
402+
uid: 'green hornet',
403+
404+
});
405+
const user = new User(ref, {
406+
uid: 'kato',
407+
408+
});
409+
ref.createUser(user);
410+
ref.createUser(currentUser);
411+
ref.changeAuthState(currentUser);
412+
ref.flush();
413+
spy.reset();
414+
const userWithNewToken = user.clone();
415+
userWithNewToken._idtoken = Math.random().toString();
416+
ref.updateUser(userWithNewToken);
417+
ref.flush();
418+
expect(spy.called).to.equal(false);
419+
});
420+
421+
it('is not triggered on updating current user with the same info', () => {
422+
const user = new User(ref, {
423+
uid: 'kato',
424+
425+
});
426+
ref.createUser(user);
427+
ref.changeAuthState(user);
428+
ref.flush();
429+
spy.reset();
430+
ref.updateUser(user.clone());
431+
ref.flush();
432+
expect(spy.called).to.equal(false);
433+
});
434+
});
435+
436+
describe('#onAuthStateChanged', function () {
437+
438+
let unsubscribe;
439+
440+
beforeEach(() => {
441+
unsubscribe = ref.onAuthStateChanged(spy);
442+
});
443+
444+
onAuthChangeSharedProperties(it, () => unsubscribe());
445+
446+
it('is not triggered if only ID token changes', () => {
447+
const user = new User(ref, {
448+
uid: 'kato'
449+
});
450+
ref.changeAuthState(user);
451+
ref.flush();
452+
spy.reset();
453+
ref.autoFlush();
454+
return user.getIdToken(true)
455+
.then(() => expect(spy.called).to.equal(false));
456+
});
350457
});
351458

352459
describe('#createUser', function () {

0 commit comments

Comments
 (0)