Skip to content

Commit

Permalink
fix(ngx-sherfire): ensure FirebaseUser$ updates even when User object…
Browse files Browse the repository at this point in the history
… is reused by Firebase SDK (#17)
  • Loading branch information
pavadeli authored Feb 16, 2021
1 parent 84f85c1 commit 18f4073
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 14 deletions.
9 changes: 5 additions & 4 deletions libs/ngx-sherfire/src/lib/auth/create-firebase-user$.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import type { FirebaseAuth } from './firebase-auth.service';
import type { FirebaseUser$ } from './firebase-user$.service';

export function createFirebaseUser$(zone: NgZone, auth: FirebaseAuth): FirebaseUser$ {
return fromEventPattern<firebase.User | null>(value$ =>
// Wrapped in an object to allow Firebase to reuse the User object (which they do).
return fromEventPattern<{ user: firebase.User | null }>(value$ =>
auth.onIdTokenChanged(
user => zone.run(() => value$.set(user)),
user => zone.run(() => value$.set({ user })),
err => zone.run(() => value$.setFinal(error(err))),
() => zone.run(() => value$.makeFinal()),
),
)
.fallbackTo(() => auth.currentUser ?? unresolved)
.flatMap(user => user && fromPromise(user.getIdTokenResult().then(idtoken => ({ user, idtoken }))));
.fallbackTo(() => (auth.currentUser ? { user: auth.currentUser } : unresolved))
.flatMap(({ user }) => user && fromPromise(user.getIdTokenResult().then(idtoken => ({ user, idtoken }))));
}
52 changes: 42 additions & 10 deletions libs/ngx-sherfire/src/lib/ngx-sherfire.module.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ describe(NgxSherfireModule, () => {
expect(isDerivable(firebaseUser$)).toBeTrue();

expect(onIdTokenChanged).not.toBeCalled();
let user: Unwrap<typeof firebaseUser$> | undefined;
const stop = firebaseUser$.react(u => (user = u));
let userInfo: Unwrap<typeof firebaseUser$> | undefined;
const stop = firebaseUser$.react(u => (userInfo = u));
expect(onIdTokenChanged).toBeCalledTimes(1);

const [[cb]] = onIdTokenChanged.mock.calls;
Expand All @@ -76,28 +76,28 @@ describe(NgxSherfireModule, () => {
}

// reactor has not fired because we don't know yet whether we are logged in or not.
expect(user).toBeUndefined();
expect(userInfo).toBeUndefined();
expect(firebaseUser$.resolved).toBeFalse();

// Firebase responds saying that the user is not logged in.
cb(null);

expect(user).toBeNull();
expect(userInfo).toBeNull();
expect(firebaseUser$.resolved).toBeTrue();

// Apparently, the user now logged in...
sampleUser.getIdTokenResult.mockResolvedValue({ token: 'the token' } as any);
cb(sampleUser);

// UI should not respond yet, we are still in the process of receiving more info
expect(user).toBeNull();
expect(userInfo).toBeNull();
expect(firebaseUser$.resolved).toBeFalse();

expect(sampleUser.getIdTokenResult).toBeCalledTimes(1);

await new Promise(resolve => setTimeout(resolve, 0));

expect(user).toEqual({ user: sampleUser, idtoken: { token: 'the token' } });
expect(userInfo).toEqual({ user: sampleUser, idtoken: { token: 'the token' } });

expect(unsubscribe).not.toBeCalled();
expect(firebaseUser$.resolved).toBeTrue();
Expand All @@ -111,20 +111,52 @@ describe(NgxSherfireModule, () => {
sampleUser.getIdTokenResult.mockResolvedValue({ token: 'other token' } as any);
firebaseAuth.currentUser = sampleUser;

let user: Unwrap<typeof firebaseUser$> | undefined;
firebaseUser$.react(u => (user = u));
let userInfo: Unwrap<typeof firebaseUser$> | undefined;
firebaseUser$.react(u => (userInfo = u));
expect(onIdTokenChanged).toBeCalledTimes(1);

// reactor has not fired because we don't know yet whether we are logged in or not...
expect(user).toBeUndefined();
expect(userInfo).toBeUndefined();
expect(firebaseUser$.resolved).toBeFalse();

// but we have a synchronous currentUser that is being checked.
expect(sampleUser.getIdTokenResult).toBeCalledTimes(1);

await new Promise(resolve => setTimeout(resolve, 0));

expect(user).toEqual({ user: sampleUser, idtoken: { token: 'other token' } });
expect(userInfo).toEqual({ user: sampleUser, idtoken: { token: 'other token' } });
});

test('on idtoken change', async () => {
sampleUser.getIdTokenResult.mockResolvedValueOnce({ token: 'the first token' } as any);
firebaseAuth.currentUser = sampleUser;

let userInfo: Unwrap<typeof firebaseUser$> | undefined;
firebaseUser$.react(u => (userInfo = u));
expect(onIdTokenChanged).toBeCalledTimes(1);
const [[cb]] = onIdTokenChanged.mock.calls;
if (typeof cb !== 'function') {
fail('expected callback to onIdTokenChanged to be a function');
}

// Resolve the getIdTokenResult promise
await new Promise(resolve => setTimeout(resolve, 0));

expect(userInfo).toEqual({ user: sampleUser, idtoken: { token: 'the first token' } });
expect(firebaseUser$.resolved).toBeTrue();

sampleUser.getIdTokenResult.mockResolvedValueOnce({ token: 'the second token' } as any);
cb(sampleUser);

// UI should not respond yet, we are still in the process of receiving more info
expect(userInfo).toEqual({ user: sampleUser, idtoken: { token: 'the first token' } });
expect(firebaseUser$.resolved).toBeFalse();

expect(sampleUser.getIdTokenResult).toBeCalledTimes(2);

await new Promise(resolve => setTimeout(resolve, 0));

expect(userInfo).toEqual({ user: sampleUser, idtoken: { token: 'the second token' } });
});

test('on error', () => {
Expand Down

0 comments on commit 18f4073

Please sign in to comment.