Skip to content

Commit 4cdd12f

Browse files
committed
Add tests for user state change
1 parent 7e467a0 commit 4cdd12f

File tree

4 files changed

+112
-13
lines changed

4 files changed

+112
-13
lines changed

packages-exp/auth-exp/src/core/auth/auth_impl.test.ts

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,13 @@ import { FirebaseError } from '@firebase/util';
2424

2525
import { testUser } from '../../../test/mock_auth';
2626
import { Auth } from '../../model/auth';
27+
import { User } from '../../model/user';
2728
import { Persistence } from '../persistence';
2829
import { browserLocalPersistence } from '../persistence/browser';
2930
import { inMemoryPersistence } from '../persistence/in_memory';
3031
import { PersistenceUserManager } from '../persistence/persistence_user_manager';
3132
import { ClientPlatform, getClientVersion } from '../util/version';
32-
import {
33-
DEFAULT_API_HOST,
34-
DEFAULT_API_SCHEME,
35-
initializeAuth
36-
} from './auth_impl';
33+
import { DEFAULT_API_HOST, DEFAULT_API_SCHEME, initializeAuth } from './auth_impl';
3734

3835
use(sinonChai);
3936

@@ -117,6 +114,102 @@ describe('AuthImpl', () => {
117114
);
118115
});
119116
});
117+
118+
describe('change listeners', () => {
119+
// // Helpers to convert auth state change results to promise
120+
// function onAuthStateChange(callback: NextFn<User|null>)
121+
122+
it('immediately calls authStateChange if initialization finished', (done) => {
123+
const user = testUser('uid');
124+
auth.currentUser = user;
125+
auth._isInitialized = true;
126+
auth.onAuthStateChanged(user => {
127+
expect(user).to.eq(user);
128+
done();
129+
});
130+
});
131+
132+
it('immediately calls idTokenChange if initialization finished', (done) => {
133+
const user = testUser('uid');
134+
auth.currentUser = user;
135+
auth._isInitialized = true;
136+
auth.onIdTokenChange(user => {
137+
expect(user).to.eq(user);
138+
done();
139+
});
140+
});
141+
142+
it('immediate callback is done async', () => {
143+
auth._isInitialized = true;
144+
let callbackCalled = false;
145+
auth.onIdTokenChange(() => {
146+
callbackCalled = true;
147+
});
148+
149+
expect(callbackCalled).to.be.false;
150+
});
151+
152+
describe('user logs in/out, tokens refresh', () => {
153+
let user: User;
154+
let callback: sinon.SinonSpy;
155+
156+
beforeEach(() => {
157+
user = testUser('uid');
158+
callback = sinon.spy();
159+
});
160+
161+
it('onAuthStateChange triggers on log in', async () => {
162+
await auth.updateCurrentUser(null);
163+
auth.onAuthStateChanged(callback);
164+
await auth.updateCurrentUser(user);
165+
expect(callback).to.have.been.calledTwice;
166+
});
167+
168+
it('onAuthStateChange triggers on log out', async () => {
169+
await auth.updateCurrentUser(user);
170+
auth.onAuthStateChanged(callback);
171+
await auth.updateCurrentUser(null);
172+
expect(callback).to.have.been.calledTwice;
173+
});
174+
175+
it('onIdTokenChange triggers on log in', async () => {
176+
await auth.updateCurrentUser(null);
177+
auth.onIdTokenChange(callback);
178+
await auth.updateCurrentUser(user);
179+
expect(callback).to.have.been.calledTwice;
180+
});
181+
182+
it('onIdTokenChange triggers on log out', async () => {
183+
await auth.updateCurrentUser(user);
184+
auth.onIdTokenChange(callback);
185+
await auth.updateCurrentUser(null);
186+
expect(callback).to.have.been.calledTwice;
187+
});
188+
189+
it('onAuthStateChange does not trigger for user props change', async () => {
190+
await auth.updateCurrentUser(user);
191+
auth.onAuthStateChanged(callback);
192+
user.refreshToken = 'hey look I changed';
193+
await auth.updateCurrentUser(user);
194+
expect(callback).to.have.been.calledOnce;
195+
});
196+
197+
it('onIdTokenChange triggers for user props change', async () => {
198+
await auth.updateCurrentUser(user);
199+
auth.onIdTokenChange(callback);
200+
user.refreshToken = 'hey look I changed';
201+
await auth.updateCurrentUser(user);
202+
expect(callback).to.have.been.calledTwice;
203+
});
204+
205+
it('onAuthStateChange triggers if uid changes', async () => {
206+
await auth.updateCurrentUser(user);
207+
auth.onAuthStateChanged(callback);
208+
await auth.updateCurrentUser(testUser('different-uid'));
209+
expect(callback).to.have.been.calledTwice;
210+
});
211+
});
212+
});
120213
});
121214

122215
describe('initializeAuth', () => {

packages-exp/auth-exp/src/core/auth/auth_impl.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class AuthImpl implements Auth {
4242
private persistenceManager?: PersistenceUserManager;
4343
private authStateSubscription = new Subscription<User>(this);
4444
private idTokenSubscription = new Subscription<User>(this);
45-
private isInitialized = false;
45+
_isInitialized = false;
4646

4747
// Tracks the last notified UID for state change listeners to prevent
4848
// repeated calls to the callbacks
@@ -68,7 +68,7 @@ class AuthImpl implements Auth {
6868
await this.directlySetCurrentUser(storedUser);
6969
}
7070

71-
this.isInitialized = true;
71+
this._isInitialized = true;
7272
this._notifyStateListeners();
7373
});
7474
}
@@ -100,7 +100,7 @@ class AuthImpl implements Auth {
100100
}
101101

102102
_notifyStateListeners(): void {
103-
if (!this.isInitialized) {
103+
if (!this._isInitialized) {
104104
return;
105105
}
106106

@@ -115,9 +115,10 @@ class AuthImpl implements Auth {
115115
private registerStateListener(subscription: Subscription<User>, nextOrObserver: NextOrObserver<User>,
116116
error?: ErrorFn,
117117
completed?: CompleteFn): Unsubscribe {
118-
if (this.isInitialized) {
118+
if (this._isInitialized) {
119119
const cb = typeof nextOrObserver === 'function' ? nextOrObserver : nextOrObserver.next;
120-
// The callback needs to be called asynchronously per the spec
120+
// The callback needs to be called asynchronously per the spec.
121+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
121122
Promise.resolve().then(() => cb(this.currentUser));
122123
}
123124

@@ -187,6 +188,7 @@ class Subscription<T> {
187188
constructor(readonly auth: Auth) {}
188189

189190
get next(): NextFn<T|null> {
190-
return assert(this.observer, this.auth.name).next;
191+
const observer = assert(this.observer, this.auth.name);
192+
return observer.next.bind(observer);
191193
}
192194
}

packages-exp/auth-exp/src/model/auth.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ export interface Auth {
3737
currentUser: User | null;
3838
readonly name: AppName;
3939
readonly config: Config;
40+
_isInitialized: boolean;
4041

4142
setPersistence(persistence: Persistence): Promise<void>;
4243
updateCurrentUser(user: User | null): Promise<void>;
4344
signOut(): Promise<void>;
4445
onAuthStateChanged(nextOrObserver: NextOrObserver<User>, error?: ErrorFn, completed?: CompleteFn): Unsubscribe;
4546
onIdTokenChange(nextOrObserver: NextOrObserver<User>, error?: ErrorFn, completed?: CompleteFn): Unsubscribe;
47+
_notifyStateListeners(): void;
4648
}
4749

4850
export interface Dependencies {

packages-exp/auth-exp/test/mock_auth.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ export const mockAuth: Auth = {
3232
apiScheme: TEST_SCHEME,
3333
sdkClientVersion: 'testSDK/0.0.0'
3434
},
35+
_isInitialized: true,
3536
currentUser: null,
3637
async setPersistence() {},
3738
async updateCurrentUser() {},
3839
async signOut() {},
39-
onAuthStateChanged() {return () => {}},
40-
onIdTokenChange() { return () => {}},
40+
onAuthStateChanged() {return () => {};},
41+
onIdTokenChange() { return () => {};},
42+
_notifyStateListeners() {},
4143
};
4244

4345
export function testUser(uid: string, email?: string): User {

0 commit comments

Comments
 (0)