Skip to content

Commit 40780f4

Browse files
committed
Tests
1 parent 90a8320 commit 40780f4

File tree

8 files changed

+168
-63
lines changed

8 files changed

+168
-63
lines changed

packages-exp/auth-exp/demo/src/index.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,26 +50,17 @@ import {
5050
signInWithCustomToken,
5151
signInWithEmailAndPassword,
5252
unlink,
53-
<<<<<<< HEAD
5453
updateEmail,
5554
updatePassword,
5655
updateProfile,
57-
verifyPasswordResetCode
58-
} from '@firebase/auth-exp';
59-
=======
60-
getMultiFactorResolver,
61-
multiFactor,
62-
PhoneMultiFactorGenerator,
56+
verifyPasswordResetCode,
6357
OAuthProvider,
6458
signInWithPopup,
6559
linkWithPopup,
6660
reauthenticateWithPopup,
67-
BrowserPopupRedirectResolver
61+
browserPopupRedirectResolver
6862
} from '@firebase/auth-exp';
6963

70-
const browserPopupRedirectResolver = new BrowserPopupRedirectResolver();
71-
72-
>>>>>>> ad6dfaac7... Popup strategy implementation
7364
import { config } from './config';
7465
import {
7566
alertError,

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

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@ import * as sinon from 'sinon';
2020
import * as sinonChai from 'sinon-chai';
2121

2222
import {
23-
AuthEvent,
24-
AuthEventConsumer,
25-
AuthEventError,
26-
AuthEventType
23+
AuthEvent, AuthEventConsumer, AuthEventError, AuthEventType
2724
} from '../../model/popup_redirect';
2825
import { AuthErrorCode } from '../errors';
2926
import { AuthEventManager } from './auth_event_manager';
@@ -34,18 +31,15 @@ describe('src/core/auth/auth_event_manager', () => {
3431
let manager: AuthEventManager;
3532

3633
function makeConsumer(
37-
filter: AuthEventType
34+
filter: AuthEventType|AuthEventType[]
3835
): sinon.SinonStubbedInstance<AuthEventConsumer> {
3936
const stub = sinon.stub({
40-
filter,
41-
isMatchingEvent: () => true,
37+
filter: Array.isArray(filter) ? filter : [filter],
4238
onAuthEvent: () => {},
43-
onError: () => {}
39+
onError: () => {},
40+
eventId: null,
4441
});
4542

46-
// Make isMatchingEvent call through by default
47-
stub.isMatchingEvent.returns(true);
48-
4943
return stub;
5044
}
5145

@@ -94,21 +88,22 @@ describe('src/core/auth/auth_event_manager', () => {
9488
expect(consumer.onAuthEvent).not.to.have.been.called;
9589
});
9690

97-
it('calls isMatchingEvent with the event id', () => {
91+
it('does not call through if eventId does not match', () => {
9892
const consumer = makeConsumer(AuthEventType.REAUTH_VIA_POPUP);
93+
consumer.eventId = 'not-event-id';
9994
manager.registerConsumer(consumer);
95+
10096
manager.onEvent(makeEvent(AuthEventType.REAUTH_VIA_POPUP, 'event-id'));
101-
expect(consumer.isMatchingEvent).to.have.been.calledWith('event-id');
97+
expect(consumer.onAuthEvent).not.to.have.been.called;
10298
});
10399

104-
it('does not call through if isMatchingEvent is false', () => {
100+
it('does call through if eventId is null', () => {
105101
const consumer = makeConsumer(AuthEventType.REAUTH_VIA_POPUP);
102+
consumer.eventId = null;
106103
manager.registerConsumer(consumer);
107-
consumer.isMatchingEvent.returns(false);
108104

109-
manager.onEvent(makeEvent(AuthEventType.REAUTH_VIA_POPUP));
110-
expect(consumer.onAuthEvent).not.to.have.been.called;
111-
expect(consumer.isMatchingEvent).to.have.been.called;
105+
manager.onEvent(makeEvent(AuthEventType.REAUTH_VIA_POPUP, 'event-id'));
106+
expect(consumer.onAuthEvent).to.have.been.called;
112107
});
113108

114109
it('converts errors into FirebaseError if the type matches', () => {
@@ -139,4 +134,54 @@ describe('src/core/auth/auth_event_manager', () => {
139134
const error = consumer.onError.getCall(0).args[0];
140135
expect(error.code).to.eq(`auth/${AuthErrorCode.INTERNAL_ERROR}`);
141136
});
137+
138+
context('redirect consumers', () => {
139+
let consumer: AuthEventConsumer;
140+
141+
beforeEach(() => {
142+
consumer = makeConsumer([AuthEventType.SIGN_IN_VIA_REDIRECT, AuthEventType.LINK_VIA_REDIRECT, AuthEventType.REAUTH_VIA_REDIRECT]);
143+
});
144+
145+
it('redirect events are queued until the future', () => {
146+
const event = makeEvent(AuthEventType.REAUTH_VIA_REDIRECT);
147+
expect(manager.onEvent(event)).to.be.true;
148+
149+
manager.registerConsumer(consumer);
150+
expect(consumer.onAuthEvent).to.have.been.calledWith(event);
151+
});
152+
153+
it('queued redirects only work for the first new consumer', () => {
154+
const event = makeEvent(AuthEventType.REAUTH_VIA_REDIRECT);
155+
expect(manager.onEvent(event)).to.be.true;
156+
157+
manager.registerConsumer(consumer);
158+
expect(consumer.onAuthEvent).to.have.been.calledWith(event);
159+
160+
const consumerB = makeConsumer(AuthEventType.REAUTH_VIA_REDIRECT);
161+
manager.registerConsumer(consumerB);
162+
expect(consumerB.onAuthEvent).not.to.have.been.called;
163+
});
164+
165+
it('does not queue a redirect event if it was handled immediately', () => {
166+
const event = makeEvent(AuthEventType.REAUTH_VIA_REDIRECT);
167+
manager.registerConsumer(consumer);
168+
169+
expect(manager.onEvent(event)).to.be.true;
170+
expect(consumer.onAuthEvent).to.have.been.calledWith(event);
171+
172+
const consumerB = makeConsumer(AuthEventType.REAUTH_VIA_REDIRECT);
173+
manager.registerConsumer(consumerB);
174+
expect(consumerB.onAuthEvent).not.to.have.been.called;
175+
});
176+
177+
it('unknown auth error prevents consumption of future redirect events', () => {
178+
const event = makeEvent(AuthEventType.UNKNOWN);
179+
event.error = {code: 'auth/no-auth-event'} as AuthEventError;
180+
expect(manager.onEvent(event)).to.be.true;
181+
expect(manager.onEvent(makeEvent(AuthEventType.SIGN_IN_VIA_REDIRECT))).to.be.false;
182+
183+
manager.registerConsumer(consumer);
184+
expect(consumer.onAuthEvent).not.to.have.been.called;
185+
});
186+
});
142187
});

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

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ export class AuthEventManager implements EventManager {
4141
}
4242

4343
onEvent(event: AuthEvent): boolean {
44+
if (isNullRedirectEvent(event)) {
45+
// A null redirect event comes through as the first event when no pending
46+
// redirect actions are in the auth domain
47+
this.hasHandledPotentialRedirect = true;
48+
return true;
49+
}
50+
4451
let handled = false;
4552
this.consumers.forEach(consumer => {
4653
if (this.isEventForConsumer(event, consumer)) {
@@ -49,24 +56,20 @@ export class AuthEventManager implements EventManager {
4956
}
5057
});
5158

52-
// The redirect event is always available immediately, unlike popup
53-
// events (which happen in the normal app lifetime). Since the user
54-
// may open the iframe (through a popup method) before getRedirectResult()
55-
// is called, we need to queue up the redirect event so the user has access
56-
// to it later. On the other hand, if we get a "unknown" auth event with
57-
// the message "no-auth-event", we know there will never be a redirect event
58-
// for this session.
59-
if (event.type === AuthEventType.UNKNOWN && event.error?.code === `auth/${AuthErrorCode.NO_AUTH_EVENT}`) {
60-
this.hasHandledPotentialRedirect = true;
61-
return true;
59+
if (this.hasHandledPotentialRedirect || !isRedirectEvent(event.type)) {
60+
// If we've already seen a redirect before, or this is a popup event,
61+
// bail now
62+
return handled;
63+
}
64+
65+
this.hasHandledPotentialRedirect = true;
6266

63-
} else if (!handled && isRedirectEvent(event.type) && !this.hasHandledPotentialRedirect) {
67+
// If the redirect wasn't handled, hang on to it
68+
if (!handled) {
6469
this.queuedRedirectEvent = event;
6570
handled = true;
6671
}
6772

68-
this.hasHandledPotentialRedirect = this.hasHandledPotentialRedirect || isRedirectEvent(event.type);
69-
7073
return handled;
7174
}
7275

@@ -92,6 +95,10 @@ export class AuthEventManager implements EventManager {
9295
}
9396
}
9497

98+
function isNullRedirectEvent({type, error}: AuthEvent): boolean {
99+
return type === AuthEventType.UNKNOWN && error?.code === `auth/${AuthErrorCode.NO_AUTH_EVENT}`;
100+
}
101+
95102
function isRedirectEvent(type: AuthEventType): boolean {
96103
switch (type) {
97104
case AuthEventType.SIGN_IN_VIA_REDIRECT:

packages-exp/auth-exp/src/core/strategies/abstract_popup_redirect_operation.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,6 @@ export abstract class AbstractPopupRedirectOperation
9595
this.reject(error);
9696
}
9797

98-
isMatchingEvent(eventId: string | null): boolean {
99-
return !!eventId && this.eventId === eventId;
100-
}
101-
10298
private getIdpTask(type: AuthEventType): IdpTask {
10399
switch (type) {
104100
case AuthEventType.SIGN_IN_VIA_POPUP:

packages-exp/auth-exp/src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,4 @@ export { PhoneMultiFactorGenerator } from './mfa/assertions/phone';
128128
export { getMultiFactorResolver } from './mfa/mfa_resolver';
129129
export { multiFactor } from './mfa/mfa_user';
130130

131-
// TODO(samhorlbeck): This should be exported as a single const
132-
export { BrowserPopupRedirectResolver } from './platform_browser/popup_redirect';
131+
export { browserPopupRedirectResolver } from './platform_browser/popup_redirect';

packages-exp/auth-exp/src/platform_browser/auth_window.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,7 @@ export type AuthWindow = {
3838
};
3939

4040
export const AUTH_WINDOW = (window as unknown) as AuthWindow;
41+
42+
export function _setWindowLocation(url: string): void {
43+
AUTH_WINDOW.location.href = url;
44+
}

packages-exp/auth-exp/src/platform_browser/popup_redirect.test.ts

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,26 @@ import { FirebaseError } from '@firebase/util';
2727
import { TEST_AUTH_DOMAIN, TEST_KEY, testAuth } from '../../test/mock_auth';
2828
import { AuthEventManager } from '../core/auth/auth_event_manager';
2929
import { OAuthProvider } from '../core/providers/oauth';
30+
import { SingletonInstantiator } from '../core/util/instantiator';
3031
import { Auth } from '../model/auth';
3132
import {
32-
AuthEvent,
33-
AuthEventType,
34-
GapiAuthEvent
33+
AuthEvent, AuthEventType, GapiAuthEvent, PopupRedirectResolver
3534
} from '../model/popup_redirect';
35+
import * as authWindow from './auth_window';
3636
import * as gapiLoader from './iframe/gapi';
37-
import { BrowserPopupRedirectResolver } from './popup_redirect';
37+
import { browserPopupRedirectResolver } from './popup_redirect';
3838

3939
use(chaiAsPromised);
4040
use(sinonChai);
4141

4242
describe('src/platform_browser/popup_redirect', () => {
43-
let resolver: BrowserPopupRedirectResolver;
43+
let resolver: PopupRedirectResolver;
4444
let auth: Auth;
4545
let onIframeMessage: (event: GapiAuthEvent) => Promise<void>;
4646

4747
beforeEach(async () => {
4848
auth = await testAuth();
49-
resolver = new BrowserPopupRedirectResolver();
49+
resolver = new (browserPopupRedirectResolver as SingletonInstantiator<PopupRedirectResolver>)();
5050

5151
sinon.stub(gapiLoader, '_loadGapi').returns(
5252
Promise.resolve(({
@@ -65,7 +65,7 @@ describe('src/platform_browser/popup_redirect', () => {
6565
sinon.restore();
6666
});
6767

68-
context('#openPopup', () => {
68+
context('#_openPopup', () => {
6969
let popupUrl: string | undefined;
7070
let provider: OAuthProvider;
7171
const event = AuthEventType.LINK_VIA_POPUP;
@@ -115,15 +115,66 @@ describe('src/platform_browser/popup_redirect', () => {
115115
});
116116
});
117117

118-
context('#initialize', () => {
118+
context('#_openRedirect', () => {
119+
let newWindowLocation: string;
120+
let provider: OAuthProvider;
121+
const event = AuthEventType.LINK_VIA_POPUP;
122+
123+
beforeEach(async () => {
124+
provider = new OAuthProvider(ProviderId.GOOGLE);
125+
await resolver._initialize(auth);
126+
sinon.stub(authWindow, '_setWindowLocation').callsFake(url => {
127+
newWindowLocation = url;
128+
});
129+
});
130+
131+
it('builds the correct url', () => {
132+
provider.addScope('some-scope-a');
133+
provider.addScope('some-scope-b');
134+
provider.setCustomParameters({ foo: 'bar' });
135+
136+
// This promise will never resolve on purpose
137+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
138+
resolver._openRedirect(auth, provider, event);
139+
expect(newWindowLocation).to.include(
140+
`https://${TEST_AUTH_DOMAIN}/__/auth/handler`
141+
);
142+
expect(newWindowLocation).to.include(`apiKey=${TEST_KEY}`);
143+
expect(newWindowLocation).to.include('appName=test-app');
144+
expect(newWindowLocation).to.include(`authType=${AuthEventType.LINK_VIA_POPUP}`);
145+
expect(newWindowLocation).to.include(`v=${SDK_VERSION}`);
146+
expect(newWindowLocation).to.include('scopes=some-scope-a%2Csome-scope-b');
147+
expect(newWindowLocation).to.include(
148+
'customParameters=%7B%22foo%22%3A%22bar%22%7D'
149+
);
150+
});
151+
152+
it('throws an error if authDomain is unspecified', async () => {
153+
delete auth.config.authDomain;
154+
155+
await expect(
156+
resolver._openRedirect(auth, provider, event)
157+
).to.be.rejectedWith(FirebaseError, 'auth/auth-domain-config-required');
158+
});
159+
160+
it('throws an error if apiKey is unspecified', async () => {
161+
delete auth.config.apiKey;
162+
163+
await expect(
164+
resolver._openRedirect(auth, provider, event)
165+
).to.be.rejectedWith(FirebaseError, 'auth/invalid-api-key');
166+
});
167+
});
168+
169+
context('#_initialize', () => {
119170
it('only registers once, returns same event manager', async () => {
120171
const manager = await resolver._initialize(auth);
121172
expect(await resolver._initialize(auth)).to.eq(manager);
122173
});
123174

124175
it('iframe event goes through to the manager', async () => {
125176
const manager = (await resolver._initialize(auth)) as AuthEventManager;
126-
sinon.spy(manager, 'onEvent');
177+
sinon.stub(manager, 'onEvent').returns(true);
127178
const response = await onIframeMessage({
128179
type: 'authEvent',
129180
authEvent: { type: AuthEventType.LINK_VIA_POPUP } as AuthEvent
@@ -136,5 +187,21 @@ describe('src/platform_browser/popup_redirect', () => {
136187
status: 'ACK'
137188
});
138189
});
190+
191+
it('returns error to the iframe if the event was not handled', async () => {
192+
const manager = (await resolver._initialize(auth)) as AuthEventManager;
193+
sinon.stub(manager, 'onEvent').returns(false);
194+
const response = await onIframeMessage({
195+
type: 'authEvent',
196+
authEvent: { type: AuthEventType.LINK_VIA_POPUP } as AuthEvent
197+
});
198+
199+
expect(manager.onEvent).to.have.been.calledWith({
200+
type: AuthEventType.LINK_VIA_POPUP
201+
});
202+
expect(response).to.eql({
203+
status: 'ERROR'
204+
});
205+
});
139206
});
140207
});

0 commit comments

Comments
 (0)