Skip to content

Commit 1826dfc

Browse files
sam-gcavolkovi
authored andcommitted
Add popup strategy implementation, (sign in, link, reauth) (#3363)
* Popup strategy implementation * Popup strategy + tests * Formatting * Updated comment * Fix IdpTask types * Refactor popup code to have abstract base * WIP * Early feedback * Feedback, tests * Formatting * Formatting * Formatting
1 parent 9b685fd commit 1826dfc

File tree

12 files changed

+1236
-33
lines changed

12 files changed

+1236
-33
lines changed

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

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,26 @@ import {
5050
signInWithCustomToken,
5151
signInWithEmailAndPassword,
5252
unlink,
53+
<<<<<<< HEAD
5354
updateEmail,
5455
updatePassword,
5556
updateProfile,
5657
verifyPasswordResetCode
5758
} from '@firebase/auth-exp';
59+
=======
60+
getMultiFactorResolver,
61+
multiFactor,
62+
PhoneMultiFactorGenerator,
63+
OAuthProvider,
64+
signInWithPopup,
65+
linkWithPopup,
66+
reauthenticateWithPopup,
67+
BrowserPopupRedirectResolver
68+
} from '@firebase/auth-exp';
69+
70+
const browserPopupRedirectResolver = new BrowserPopupRedirectResolver();
71+
72+
>>>>>>> ad6dfaac7... Popup strategy implementation
5873
import { config } from './config';
5974
import {
6075
alertError,
@@ -1182,10 +1197,9 @@ function onPopupRedirectAddCustomParam(_event) {
11821197
* Performs the corresponding popup/redirect action for a generic provider.
11831198
*/
11841199
function onPopupRedirectGenericProviderClick() {
1185-
alertNotImplemented();
1186-
// var providerId = $('#popup-redirect-generic-providerid').val();
1187-
// var provider = new OAuthProvider(providerId);
1188-
// signInWithPopupRedirect(provider);
1200+
var providerId = $('#popup-redirect-generic-providerid').val();
1201+
var provider = new OAuthProvider(providerId);
1202+
signInWithPopupRedirect(provider);
11891203
}
11901204

11911205
/**
@@ -1231,11 +1245,16 @@ function onPopupRedirectProviderClick(_event) {
12311245
* sign in.
12321246
*/
12331247
function signInWithPopupRedirect(provider) {
1234-
const action = $('input[name=popup-redirect-action]:checked').val();
1235-
const type = $('input[name=popup-redirect-type]:checked').val();
1248+
const glob = {
1249+
signInWithPopup,
1250+
linkWithPopup,
1251+
reauthenticateWithPopup,
1252+
}
1253+
let action = $('input[name=popup-redirect-action]:checked').val();
1254+
let type = $('input[name=popup-redirect-type]:checked').val();
12361255
let method = null;
12371256
let inst = null;
1238-
if (action === 'link' || action === 'reauthenticate') {
1257+
if (action == 'link' || action == 'reauthenticate') {
12391258
if (!activeUser()) {
12401259
alertError('No user logged in.');
12411260
return;
@@ -1285,21 +1304,23 @@ function signInWithPopupRedirect(provider) {
12851304
}
12861305
console.log('Provider:');
12871306
console.log(provider);
1288-
if (type === 'popup') {
1289-
inst[method](provider).then(response => {
1307+
if (type == 'popup') {
1308+
glob[method](inst, provider, browserPopupRedirectResolver).then(response => {
12901309
console.log('Popup response:');
12911310
console.log(response);
12921311
alertSuccess(action + ' with ' + provider['providerId'] + ' successful!');
12931312
logAdditionalUserInfo(response);
12941313
onAuthSuccess(activeUser());
1295-
}, onAuthError);
1314+
},
1315+
onAuthError);
12961316
} else {
1297-
try {
1298-
inst[method](provider).catch(onAuthError);
1299-
} catch (error) {
1300-
console.log('Error while calling ' + method);
1301-
console.error(error);
1302-
}
1317+
alertNotImplemented();
1318+
// try {
1319+
// inst[method](provider).catch(onAuthError);
1320+
// } catch (error) {
1321+
// console.log('Error while calling ' + method);
1322+
// console.error(error);
1323+
// }
13031324
}
13041325
}
13051326

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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,
33+
AuthEventType,
34+
EventManager,
35+
PopupRedirectResolver
36+
} from '../../model/popup_redirect';
37+
import { AuthEventManager } from '../auth/auth_event_manager';
38+
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors';
39+
import { UserCredentialImpl } from '../user/user_credential_impl';
40+
import { _getInstance } from '../util/instantiator';
41+
import { AbstractPopupRedirectOperation } from './abstract_popup_redirect_operation';
42+
import * as idp from './idp';
43+
44+
use(sinonChai);
45+
use(chaiAsPromised);
46+
47+
const ERROR = AUTH_ERROR_FACTORY.create(AuthErrorCode.INTERNAL_ERROR, {
48+
appName: 'test'
49+
});
50+
51+
/**
52+
* A real class is needed to instantiate the action
53+
*/
54+
class WrapperOperation extends AbstractPopupRedirectOperation {
55+
eventId = '100';
56+
onExecution = sinon.stub().returns(Promise.resolve());
57+
cleanUp = sinon.stub();
58+
}
59+
60+
describe('src/core/strategies/abstract_popup_redirect_operation', () => {
61+
let auth: Auth;
62+
let resolver: PopupRedirectResolver;
63+
let eventManager: EventManager;
64+
let idpStubs: sinon.SinonStubbedInstance<typeof idp>;
65+
66+
beforeEach(async () => {
67+
auth = await testAuth();
68+
eventManager = new AuthEventManager(auth.name);
69+
resolver = _getInstance(makeMockPopupRedirectResolver(eventManager));
70+
idpStubs = sinon.stub(idp);
71+
});
72+
73+
afterEach(() => {
74+
sinon.restore();
75+
});
76+
77+
context('#execute', () => {
78+
let operation: WrapperOperation;
79+
80+
beforeEach(() => {
81+
operation = new WrapperOperation(
82+
auth,
83+
AuthEventType.LINK_VIA_POPUP,
84+
resolver
85+
);
86+
idpStubs._signIn.returns(
87+
Promise.resolve(
88+
new UserCredentialImpl(
89+
testUser(auth, 'uid'),
90+
null,
91+
OperationType.SIGN_IN
92+
)
93+
)
94+
);
95+
});
96+
97+
/** Finishes out the promise */
98+
function finishPromise(outcome: AuthEvent | FirebaseError): void {
99+
delay((): void => {
100+
if (outcome instanceof FirebaseError) {
101+
operation.onError(outcome);
102+
} else {
103+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
104+
operation.onAuthEvent(outcome);
105+
}
106+
});
107+
}
108+
109+
it('initializes the resolver', async () => {
110+
sinon.spy(resolver, '_initialize');
111+
const promise = operation.execute();
112+
finishPromise(authEvent());
113+
await promise;
114+
expect(resolver._initialize).to.have.been.calledWith(auth);
115+
});
116+
117+
it('calls subclass onExecution', async () => {
118+
finishPromise(authEvent());
119+
await operation.execute();
120+
expect(operation.onExecution).to.have.been.called;
121+
});
122+
123+
it('registers and unregisters itself with the event manager', async () => {
124+
sinon.spy(eventManager, 'registerConsumer');
125+
sinon.spy(eventManager, 'unregisterConsumer');
126+
finishPromise(authEvent());
127+
await operation.execute();
128+
expect(eventManager.registerConsumer).to.have.been.calledWith(operation);
129+
expect(eventManager.unregisterConsumer).to.have.been.calledWith(
130+
operation
131+
);
132+
});
133+
134+
it('unregisters itself in case of error', async () => {
135+
sinon.spy(eventManager, 'unregisterConsumer');
136+
finishPromise(ERROR);
137+
try {
138+
await operation.execute();
139+
} catch {}
140+
expect(eventManager.unregisterConsumer).to.have.been.calledWith(
141+
operation
142+
);
143+
});
144+
145+
it('emits the user credential returned from idp task', async () => {
146+
finishPromise(authEvent());
147+
const cred = await operation.execute();
148+
expect(cred.user.uid).to.eq('uid');
149+
expect(cred.credential).to.be.null;
150+
expect(cred.operationType).to.eq(OperationType.SIGN_IN);
151+
});
152+
153+
it('bubbles up any error', done => {
154+
finishPromise(ERROR);
155+
operation.execute().catch(e => {
156+
expect(e).to.eq(ERROR);
157+
done();
158+
});
159+
});
160+
161+
it('calls cleanUp on error', async () => {
162+
finishPromise(ERROR);
163+
try {
164+
await operation.execute();
165+
} catch {}
166+
expect(operation.cleanUp).to.have.been.called;
167+
});
168+
169+
it('calls cleanUp on success', async () => {
170+
finishPromise(authEvent());
171+
await operation.execute();
172+
expect(operation.cleanUp).to.have.been.called;
173+
});
174+
175+
context('idp tasks', () => {
176+
function updateFilter(type: AuthEventType): void {
177+
((operation as unknown) as Record<string, unknown>).filter = type;
178+
}
179+
180+
function expectedIdpTaskParams(): idp.IdpTaskParams {
181+
return {
182+
auth,
183+
requestUri: BASE_AUTH_EVENT.urlResponse!,
184+
sessionId: BASE_AUTH_EVENT.sessionId!,
185+
tenantId: BASE_AUTH_EVENT.tenantId || undefined,
186+
postBody: BASE_AUTH_EVENT.postBody || undefined,
187+
user: undefined
188+
};
189+
}
190+
191+
it('routes SIGN_IN_VIA_POPUP', async () => {
192+
const type = AuthEventType.SIGN_IN_VIA_POPUP;
193+
updateFilter(type);
194+
finishPromise(authEvent({ type }));
195+
await operation.execute();
196+
expect(idp._signIn).to.have.been.calledWith(expectedIdpTaskParams());
197+
});
198+
199+
it('routes SIGN_IN_VIA_REDIRECT', async () => {
200+
const type = AuthEventType.SIGN_IN_VIA_REDIRECT;
201+
updateFilter(type);
202+
finishPromise(authEvent({ type }));
203+
await operation.execute();
204+
expect(idp._signIn).to.have.been.calledWith(expectedIdpTaskParams());
205+
});
206+
207+
it('routes LINK_VIA_POPUP', async () => {
208+
const type = AuthEventType.LINK_VIA_POPUP;
209+
updateFilter(type);
210+
finishPromise(authEvent({ type }));
211+
await operation.execute();
212+
expect(idp._link).to.have.been.calledWith(expectedIdpTaskParams());
213+
});
214+
215+
it('routes LINK_VIA_REDIRECT', async () => {
216+
const type = AuthEventType.LINK_VIA_REDIRECT;
217+
updateFilter(type);
218+
finishPromise(authEvent({ type }));
219+
await operation.execute();
220+
expect(idp._link).to.have.been.calledWith(expectedIdpTaskParams());
221+
});
222+
223+
it('routes REAUTH_VIA_POPUP', async () => {
224+
const type = AuthEventType.REAUTH_VIA_POPUP;
225+
updateFilter(type);
226+
finishPromise(authEvent({ type }));
227+
await operation.execute();
228+
expect(idp._reauth).to.have.been.calledWith(expectedIdpTaskParams());
229+
});
230+
231+
it('routes REAUTH_VIA_REDIRECT', async () => {
232+
const type = AuthEventType.REAUTH_VIA_REDIRECT;
233+
updateFilter(type);
234+
finishPromise(authEvent({ type }));
235+
await operation.execute();
236+
expect(idp._reauth).to.have.been.calledWith(expectedIdpTaskParams());
237+
});
238+
});
239+
});
240+
});

0 commit comments

Comments
 (0)