Skip to content

Commit 686741e

Browse files
committed
WIP
1 parent e7ada09 commit 686741e

File tree

6 files changed

+253
-24
lines changed

6 files changed

+253
-24
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect, use } from 'chai';
19+
import * as chaiAsPromised from 'chai-as-promised';
20+
import * as sinon from 'sinon';
21+
import * as sinonChai from 'sinon-chai';
22+
23+
import { OperationType } from '@firebase/auth-types-exp';
24+
import { FirebaseError } from '@firebase/util';
25+
26+
import { delay } from '../../../test/delay';
27+
import { authEvent, BASE_AUTH_EVENT } from '../../../test/iframe_event';
28+
import { testAuth, testUser } from '../../../test/mock_auth';
29+
import { makeMockPopupRedirectResolver } from '../../../test/mock_popup_redirect_resolver';
30+
import { Auth } from '../../model/auth';
31+
import {
32+
AuthEvent, AuthEventType, EventManager, PopupRedirectResolver
33+
} from '../../model/popup_redirect';
34+
import { AuthEventManager } from '../auth/auth_event_manager';
35+
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors';
36+
import { UserCredentialImpl } from '../user/user_credential_impl';
37+
import { _getInstance } from '../util/instantiator';
38+
import { AbstractPopupRedirectAction } from './abstract_popup_redirect_action';
39+
import * as idp from './idp';
40+
41+
use(sinonChai);
42+
use(chaiAsPromised);
43+
44+
const ERROR = AUTH_ERROR_FACTORY.create(AuthErrorCode.INTERNAL_ERROR, {
45+
appName: 'test'
46+
});
47+
48+
/**
49+
* A real class is needed to instantiate the action
50+
*/
51+
class WrapperAction extends AbstractPopupRedirectAction {
52+
eventId = '100';
53+
onExecution = sinon.stub().returns(Promise.resolve());
54+
cleanUp = sinon.stub();
55+
}
56+
57+
describe('src/core/strategies/abstract_popup_redirect_action', () => {
58+
let auth: Auth;
59+
let resolver: PopupRedirectResolver;
60+
let eventManager: EventManager;
61+
let idpStubs: sinon.SinonStubbedInstance<typeof idp>;
62+
63+
beforeEach(async () => {
64+
auth = await testAuth();
65+
eventManager = new AuthEventManager(auth.name);
66+
resolver = _getInstance(makeMockPopupRedirectResolver(eventManager));
67+
idpStubs = sinon.stub(idp);
68+
});
69+
70+
afterEach(() => {
71+
sinon.restore();
72+
});
73+
74+
context('#execute', () => {
75+
let action: WrapperAction;
76+
77+
beforeEach(() => {
78+
action = new WrapperAction(auth, AuthEventType.LINK_VIA_POPUP, resolver);
79+
idpStubs._signIn.returns(Promise.resolve(new UserCredentialImpl(testUser(auth, 'uid'), null, OperationType.SIGN_IN)));
80+
});
81+
82+
/** Finishes out the promise */
83+
function finishPromise(outcome: AuthEvent|FirebaseError): void {
84+
delay((): void => {
85+
if (outcome instanceof FirebaseError) {
86+
action.onError(outcome);
87+
} else {
88+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
89+
action.onAuthEvent(outcome);
90+
}
91+
});
92+
}
93+
94+
it('initializes the resolver', async () => {
95+
sinon.spy(resolver, '_initialize');
96+
const promise = action.execute();
97+
finishPromise(authEvent());
98+
await promise;
99+
expect(resolver._initialize).to.have.been.calledWith(auth);
100+
});
101+
102+
it('calls subclass onExecution', async () => {
103+
finishPromise(authEvent());
104+
await action.execute();
105+
expect(action.onExecution).to.have.been.called;
106+
});
107+
108+
it('registers and unregisters itself with the event manager', async () => {
109+
sinon.spy(eventManager, 'registerConsumer');
110+
sinon.spy(eventManager, 'unregisterConsumer');
111+
finishPromise(authEvent());
112+
await action.execute();
113+
expect(eventManager.registerConsumer).to.have.been.calledWith(action);
114+
expect(eventManager.unregisterConsumer).to.have.been.calledWith(action);
115+
});
116+
117+
it('unregisters itself in case of error', async () => {
118+
sinon.spy(eventManager, 'unregisterConsumer');
119+
finishPromise(ERROR);
120+
try { await action.execute(); } catch {}
121+
expect(eventManager.unregisterConsumer).to.have.been.calledWith(action);
122+
});
123+
124+
it('emits the user credential returned from idp task', async () => {
125+
finishPromise(authEvent());
126+
const cred = await action.execute();
127+
expect(cred.user.uid).to.eq('uid');
128+
expect(cred.credential).to.be.null;
129+
expect(cred.operationType).to.eq(OperationType.SIGN_IN);
130+
});
131+
132+
it('bubbles up any error', async (done) => {
133+
finishPromise(ERROR);
134+
try {
135+
await action.execute();
136+
} catch (e) {
137+
expect(e).to.eq(ERROR);
138+
done();
139+
}
140+
});
141+
142+
context('idp tasks', () => {
143+
function updateFilter(type: AuthEventType) {
144+
(action as unknown as Record<string, unknown>).filter = type;
145+
}
146+
147+
const expectedIdpTaskParams: idp.IdpTaskParams = {
148+
auth,
149+
requestUri: BASE_AUTH_EVENT.urlResponse!,
150+
sessionId: BASE_AUTH_EVENT.sessionId!,
151+
tenantId: BASE_AUTH_EVENT.tenantId || undefined,
152+
postBody: BASE_AUTH_EVENT.postBody || undefined,
153+
};
154+
155+
it('routes signInWithPopup', async () => {
156+
const type = AuthEventType.SIGN_IN_VIA_POPUP;
157+
updateFilter(type);
158+
finishPromise(authEvent({type}));
159+
await action.execute();
160+
expect(idp._signIn).to.have.been.calledWith(expectedIdpTaskParams);
161+
});
162+
});
163+
});
164+
});

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
* limitations under the License.
1616
*/
1717

18-
import * as externs from '@firebase/auth-types-exp';
1918
import { FirebaseError } from '@firebase/util';
2019

2120
import { Auth } from '../../model/auth';
@@ -32,7 +31,7 @@ interface PendingPromise {
3231
reject: (error: Error) => void;
3332
}
3433

35-
/**
34+
/**
3635
* Popup event manager. Handles the popup's entire lifecycle; listens to auth
3736
* events
3837
*/
@@ -44,13 +43,13 @@ export abstract class AbstractPopupRedirectAction implements AuthEventConsumer {
4443

4544
constructor(
4645
protected readonly auth: Auth,
47-
readonly filter: AuthEventType|'redirect',
46+
readonly filter: AuthEventType,
4847
protected readonly resolver: PopupRedirectResolver,
4948
protected user?: User
5049
) {
5150
}
5251

53-
protected abstract onExecution(): Promise<void>;
52+
abstract onExecution(): Promise<void>;
5453

5554
execute(): Promise<UserCredential> {
5655
return new Promise<UserCredential>(async (resolve, reject) => {
@@ -89,6 +88,10 @@ export abstract class AbstractPopupRedirectAction implements AuthEventConsumer {
8988
this.broadcastResult(null, error);
9089
}
9190

91+
isMatchingEvent(eventId: string|null): boolean {
92+
return !!eventId && this.eventId === eventId;
93+
}
94+
9295
private getIdpTask(type: AuthEventType): IdpTask {
9396
switch(type) {
9497
case AuthEventType.SIGN_IN_VIA_POPUP:
@@ -121,5 +124,5 @@ export abstract class AbstractPopupRedirectAction implements AuthEventConsumer {
121124
this.cleanUp();
122125
}
123126

124-
protected abstract cleanUp(): void;
127+
abstract cleanUp(): void;
125128
}

packages-exp/auth-exp/src/core/strategies/popup.test.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@ import * as chaiAsPromised from 'chai-as-promised';
2020
import * as sinon from 'sinon';
2121
import * as sinonChai from 'sinon-chai';
2222

23-
import { OperationType, ProviderId } from '@firebase/auth-types-exp';
23+
import { OperationType, PopupRedirectResolver, ProviderId } from '@firebase/auth-types-exp';
2424
import { FirebaseError } from '@firebase/util';
2525

2626
import { delay } from '../../../test/delay';
27+
import { BASE_AUTH_EVENT } from '../../../test/iframe_event';
2728
import { testAuth, testUser } from '../../../test/mock_auth';
29+
import { makeMockPopupRedirectResolver } from '../../../test/mock_popup_redirect_resolver';
2830
import { stubTimeouts, TimerMap } from '../../../test/timeout_stub';
2931
import { Auth } from '../../model/auth';
30-
import { AuthEvent, AuthEventType, PopupRedirectResolver } from '../../model/popup_redirect';
32+
import { AuthEvent, AuthEventType } from '../../model/popup_redirect';
3133
import { User } from '../../model/user';
3234
import { AuthEventManager } from '../auth/auth_event_manager';
3335
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors';
@@ -47,15 +49,6 @@ use(chaiAsPromised);
4749
const MATCHING_EVENT_ID = 'matching-event-id';
4850
const OTHER_EVENT_ID = 'wrong-id';
4951

50-
const BASE_AUTH_EVENT: AuthEvent = {
51-
urlResponse: 'url-response',
52-
eventId: MATCHING_EVENT_ID,
53-
type: AuthEventType.SIGN_IN_VIA_POPUP,
54-
sessionId: 'session-id',
55-
tenantId: 'tenant-id',
56-
postBody: 'post-body'
57-
};
58-
5952
describe('src/core/strategies/popup', () => {
6053
let resolver: PopupRedirectResolver;
6154
let provider: OAuthProvider;
@@ -72,11 +65,7 @@ describe('src/core/strategies/popup', () => {
7265
underlyingWindow = { closed: false };
7366
authPopup = new AuthPopup(underlyingWindow as Window);
7467
provider = new OAuthProvider(ProviderId.GOOGLE);
75-
resolver = {
76-
_initialize: async () => eventManager,
77-
_openPopup: async () => authPopup,
78-
_openRedirect: () => new Promise(() => {}),
79-
};
68+
resolver = makeMockPopupRedirectResolver(eventManager, authPopup);
8069
idpStubs = sinon.stub(idpTasks);
8170
sinon.stub(eid, '_generateEventId').returns(MATCHING_EVENT_ID);
8271
pendingTimeouts = stubTimeouts();
@@ -92,6 +81,7 @@ describe('src/core/strategies/popup', () => {
9281
delay(() => {
9382
eventManager.onEvent({
9483
...BASE_AUTH_EVENT,
84+
eventId: MATCHING_EVENT_ID,
9585
...event
9686
});
9787
});

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,15 @@ class PopupAction extends AbstractPopupRedirectAction {
102102
resolver: PopupRedirectResolver,
103103
user?: User
104104
) {
105-
super(auth, filter, resolver, user)
105+
super(auth, filter, resolver, user);
106106
if (PopupAction.currentPopupAction) {
107107
PopupAction.currentPopupAction.cancel();
108108
}
109109

110110
PopupAction.currentPopupAction = this;
111111
}
112112

113-
protected async onExecution(): Promise<void> {
113+
async onExecution(): Promise<void> {
114114
const eventId = _generateEventId();
115115
this.authWindow = await this.resolver._openPopup(
116116
this.auth,
@@ -137,7 +137,7 @@ class PopupAction extends AbstractPopupRedirectAction {
137137
);
138138
}
139139

140-
protected cleanUp(): void {
140+
cleanUp(): void {
141141
if (this.authWindow) {
142142
this.authWindow.close();
143143
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { AuthEvent, AuthEventType } from '../src/model/popup_redirect';
19+
20+
export const BASE_AUTH_EVENT: AuthEvent = {
21+
urlResponse: 'url-response',
22+
eventId: 'event-id',
23+
type: AuthEventType.SIGN_IN_VIA_POPUP,
24+
sessionId: 'session-id',
25+
tenantId: 'tenant-id',
26+
postBody: 'post-body'
27+
};
28+
29+
export function authEvent(event: Partial<AuthEvent> = {}): AuthEvent {
30+
return {
31+
...BASE_AUTH_EVENT,
32+
...event
33+
};
34+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { PopupRedirectResolver } from '@firebase/auth-types-exp';
19+
20+
import { AuthEventManager } from '../src/core/auth/auth_event_manager';
21+
import { AuthPopup } from '../src/core/util/popup';
22+
import { EventManager } from '../src/model/popup_redirect';
23+
24+
/**
25+
* Generates a PopupRedirectResolver that can be used by the oauth methods.
26+
* These methods expect a class that can be instantiated.
27+
*/
28+
export function makeMockPopupRedirectResolver(eventManager?: EventManager, authPopup?: AuthPopup): PopupRedirectResolver {
29+
return class implements PopupRedirectResolver {
30+
async _initialize(): Promise<EventManager> {
31+
return eventManager || new AuthEventManager('test-app');
32+
}
33+
34+
async _openPopup(): Promise<AuthPopup> {
35+
return authPopup || new AuthPopup(null);
36+
}
37+
};
38+
}

0 commit comments

Comments
 (0)