Skip to content

Commit 53506d0

Browse files
committed
Add browser popup implementation, some utils
1 parent bd554bf commit 53506d0

File tree

5 files changed

+236
-141
lines changed

5 files changed

+236
-141
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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 sinon from 'sinon';
20+
import * as sinonChai from 'sinon-chai';
21+
22+
import { AuthEvent, AuthEventConsumer, AuthEventType } from '../../model/popup_redirect';
23+
import { AuthEventManager } from './auth_event_manager';
24+
25+
use(sinonChai);
26+
27+
describe('src/core/auth/auth_event_manager', () => {
28+
let manager: AuthEventManager;
29+
30+
function makeConsumer(filter: AuthEventType ): sinon.SinonStubbedInstance<AuthEventConsumer> {
31+
const stub = sinon.stub({
32+
filter,
33+
isMatchingEvent: () => true,
34+
onAuthEvent: () => {}
35+
});
36+
37+
// Make isMatchingEvent call through by default
38+
stub.isMatchingEvent.returns(true);
39+
40+
return stub;
41+
}
42+
43+
function makeEvent(type: AuthEventType, eventId = 'event'): AuthEvent {
44+
return {
45+
type,
46+
eventId,
47+
} as AuthEvent;
48+
}
49+
50+
beforeEach(() => {
51+
manager = new AuthEventManager();
52+
});
53+
54+
it('multiple consumers may be registered for one event type', () => {
55+
const a = makeConsumer(AuthEventType.LINK_VIA_POPUP);
56+
const b = makeConsumer(AuthEventType.LINK_VIA_POPUP);
57+
58+
const evt = makeEvent(AuthEventType.LINK_VIA_POPUP);
59+
manager.registerConsumer(a);
60+
manager.registerConsumer(b);
61+
manager.onEvent(evt);
62+
63+
expect(a.onAuthEvent).to.have.been.calledWith(evt);
64+
expect(b.onAuthEvent).to.have.been.calledWith(evt);
65+
});
66+
67+
it('can unregister listeners', () => {
68+
const a = makeConsumer(AuthEventType.LINK_VIA_POPUP);
69+
const b = makeConsumer(AuthEventType.LINK_VIA_POPUP);
70+
71+
const evt = makeEvent(AuthEventType.LINK_VIA_POPUP);
72+
manager.registerConsumer(a);
73+
manager.registerConsumer(b);
74+
manager.unregisterConsumer(a);
75+
manager.onEvent(evt);
76+
77+
expect(a.onAuthEvent).not.to.have.been.calledWith(evt);
78+
expect(b.onAuthEvent).to.have.been.calledWith(evt);
79+
});
80+
81+
it('does not call the consumer if filter does not match', () => {
82+
const consumer = makeConsumer(AuthEventType.REAUTH_VIA_POPUP);
83+
manager.registerConsumer(consumer);
84+
manager.onEvent(makeEvent(AuthEventType.REAUTH_VIA_REDIRECT));
85+
expect(consumer.onAuthEvent).not.to.have.been.called;
86+
});
87+
88+
it('calls isMatchingEvent with the event id', () => {
89+
const consumer = makeConsumer(AuthEventType.REAUTH_VIA_POPUP);
90+
manager.registerConsumer(consumer);
91+
manager.onEvent(makeEvent(AuthEventType.REAUTH_VIA_POPUP, 'event-id'));
92+
expect(consumer.isMatchingEvent).to.have.been.calledWith('event-id');
93+
});
94+
95+
it('does not call through if isMatchingEvent is false', () => {
96+
const consumer = makeConsumer(AuthEventType.REAUTH_VIA_POPUP);
97+
manager.registerConsumer(consumer);
98+
consumer.isMatchingEvent.returns(false);
99+
100+
manager.onEvent(makeEvent(AuthEventType.REAUTH_VIA_POPUP));
101+
expect(consumer.onAuthEvent).not.to.have.been.called;
102+
expect(consumer.isMatchingEvent).to.have.been.called;
103+
});
104+
});

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

Lines changed: 0 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -36,138 +36,3 @@ export class AuthEventManager implements EventManager {
3636
});
3737
}
3838
}
39-
40-
// export abstract class Abstract
41-
42-
// export abstract class AbstractAuthEventManager {
43-
// // private readonly redirectOutcomeHandler = new RedirectManager();
44-
// private readonly popupOutcomeHandler = new PopupResultManager();
45-
46-
// constructor() {}
47-
48-
// abstract openPopup(
49-
// provider: AuthProvider,
50-
// authType: AuthEventType,
51-
// eventId?: string
52-
// ): Promise<AuthPopup>;
53-
54-
// abstract processRedirect(
55-
// auth: Auth,
56-
// provider: AuthProvider,
57-
// authType: AuthEventType
58-
// ): Promise<never>;
59-
60-
// abstract initializeAndWait(auth: Auth): Promise<void>;
61-
// abstract isInitialized(): boolean;
62-
63-
// async onEvent(event: AuthEvent): Promise<boolean> {
64-
// if (event.error && event.type !== AuthEventType.UNKNOWN) {
65-
// this.getOutcomeHandler(event.type).broadcastResult(null, event.error);
66-
// return true;
67-
// }
68-
69-
// const potentialUser = this.userForEvent(event.eventId);
70-
71-
// switch (event.type) {
72-
// case AuthEventType.SIGN_IN_VIA_POPUP:
73-
// if (!this.popupOutcomeHandler.isMatchingEvent(event.eventId)) {
74-
// break;
75-
// }
76-
// // Fallthrough
77-
// case AuthEventType.SIGN_IN_VIA_REDIRECT:
78-
// this.execIdpTask(event, idp.signIn);
79-
// break;
80-
// case AuthEventType.LINK_VIA_POPUP:
81-
// case AuthEventType.LINK_VIA_REDIRECT:
82-
// if (potentialUser) {
83-
// this.execIdpTask(event, idp.link, potentialUser);
84-
// }
85-
// break;
86-
// case AuthEventType.REAUTH_VIA_POPUP:
87-
// case AuthEventType.REAUTH_VIA_REDIRECT:
88-
// if (potentialUser) {
89-
// this.execIdpTask(event, idp.reauth, potentialUser);
90-
// }
91-
// break;
92-
// }
93-
94-
// // Always resolve with the iframe
95-
// return true;
96-
// }
97-
98-
// processPopup(
99-
// auth: Auth,
100-
// provider: AuthProvider,
101-
// authType: AuthEventType,
102-
// eventId?: string
103-
// ): Promise<UserCredential | null> {
104-
// // TODO: Fix the dirty hack
105-
// this.auth = auth;
106-
107-
// return this.popupOutcomeHandler.getNewPendingPromise(async () => {
108-
// if (!this.isInitialized()) {
109-
// await this.initializeAndWait(auth);
110-
// }
111-
112-
// const win = await this.openPopup(auth, provider, authType, eventId);
113-
// win.associatedEvent = eventId || null;
114-
// return win;
115-
// });
116-
// }
117-
118-
// getRedirectResult(auth: Auth): Promise<UserCredential | null> {
119-
// // TODO: Fix this dirty hack
120-
// this.auth = auth;
121-
// return this.redirectOutcomeHandler.getRedirectPromiseOrInit(() => {
122-
// if (!this.isInitialized()) {
123-
// this.initializeAndWait(auth);
124-
// }
125-
// });
126-
// }
127-
128-
// private userForEvent(id: string | null): User | undefined {
129-
// return this.auth
130-
// .getPotentialRedirectUsers_()
131-
// .find(u => u.redirectEventId_ === id);
132-
// }
133-
134-
// private getOutcomeHandler(
135-
// eventType: AuthEventType
136-
// ): PopupRedirectOutcomeHandler {
137-
// switch (eventType) {
138-
// case AuthEventType.SIGN_IN_VIA_POPUP:
139-
// case AuthEventType.LINK_VIA_POPUP:
140-
// case AuthEventType.REAUTH_VIA_POPUP:
141-
// return this.popupOutcomeHandler;
142-
// case AuthEventType.SIGN_IN_VIA_REDIRECT:
143-
// case AuthEventType.LINK_VIA_REDIRECT:
144-
// case AuthEventType.REAUTH_VIA_REDIRECT:
145-
// return this.redirectOutcomeHandler;
146-
// default:
147-
// throw AUTH_ERROR_FACTORY.create(AuthErrorCode.INTERNAL_ERROR, {
148-
// appName: 'TODO'
149-
// });
150-
// }
151-
// }
152-
153-
// private async execIdpTask(event: AuthEvent, task: idp.IdpTask, user?: User) {
154-
// const { urlResponse, sessionId, postBody, tenantId } = event;
155-
// const params: idp.IdpTaskParams = {
156-
// requestUri: urlResponse!,
157-
// sessionId: sessionId!,
158-
// auth: this.auth,
159-
// tenantId: tenantId || undefined,
160-
// postBody: postBody || undefined,
161-
// user
162-
// };
163-
164-
// const outcomeHandler = this.getOutcomeHandler(event.type);
165-
166-
// try {
167-
// const cred = await task(params);
168-
// outcomeHandler.broadcastResult(cred);
169-
// } catch (e) {
170-
// outcomeHandler.broadcastResult(null, e);
171-
// }
172-
// }
173-
// }

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import * as externs from '@firebase/auth-types-exp';
1919

2020
import { AuthPopup } from '../core/util/popup';
2121
import { Auth } from './auth';
22-
import { UserCredential } from './user';
2322

2423
export const enum EventFilter {
2524
POPUP,
@@ -75,8 +74,6 @@ export interface EventManager {
7574

7675
export interface PopupRedirectResolver extends externs.PopupRedirectResolver {
7776
initialize(auth: Auth): Promise<EventManager>;
78-
// registerConsumer(authEventConsumer: AuthEventConsumer): void;
79-
// unregisterConsumer(authEventConsumer: AuthEventConsumer): void;
8077
openPopup(
8178
auth: Auth,
8279
provider: externs.AuthProvider,
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 { SDK_VERSION } from '@firebase/app-exp';
24+
import { ProviderId } from '@firebase/auth-types-exp';
25+
import { FirebaseError } from '@firebase/util';
26+
27+
import { TEST_AUTH_DOMAIN, TEST_KEY, testAuth } from '../../test/mock_auth';
28+
import { AuthEventManager } from '../core/auth/auth_event_manager';
29+
import { OAuthProvider } from '../core/providers/oauth';
30+
import { Auth } from '../model/auth';
31+
import { AuthEvent, AuthEventType, GapiAuthEvent } from '../model/popup_redirect';
32+
import * as gapiLoader from './iframe/gapi';
33+
import { BrowserPopupRedirectResolver } from './popup_redirect';
34+
35+
use(chaiAsPromised);
36+
use(sinonChai);
37+
38+
describe('src/platform_browser/popup_redirect', () => {
39+
let resolver: BrowserPopupRedirectResolver;
40+
let auth: Auth;
41+
42+
beforeEach(async () => {
43+
auth = await testAuth();
44+
resolver = new BrowserPopupRedirectResolver();
45+
});
46+
47+
afterEach(() => {
48+
sinon.restore();
49+
});
50+
51+
context('#openPopup', () => {
52+
let popupUrl: string|undefined;
53+
let provider: OAuthProvider;
54+
const event = AuthEventType.LINK_VIA_POPUP;
55+
56+
beforeEach(() => {
57+
sinon.stub(window, 'open').callsFake(url => {
58+
popupUrl = url;
59+
return {} as Window;
60+
});
61+
provider = new OAuthProvider(ProviderId.GOOGLE);
62+
});
63+
64+
it('builds the correct url', async () => {
65+
provider.addScope('some-scope-a');
66+
provider.addScope('some-scope-b');
67+
provider.setCustomParameters({foo: 'bar'});
68+
69+
await resolver.openPopup(auth, provider, event);
70+
expect(popupUrl).to.include(`https://${TEST_AUTH_DOMAIN}/__/auth/handler`);
71+
expect(popupUrl).to.include(`apiKey=${TEST_KEY}`);
72+
expect(popupUrl).to.include('appName=test-app');
73+
expect(popupUrl).to.include(`authType=${AuthEventType.LINK_VIA_POPUP}`);
74+
expect(popupUrl).to.include(`v=${SDK_VERSION}`);
75+
expect(popupUrl).to.include('scopes=some-scope-a%2Csome-scope-b');
76+
expect(popupUrl).to.include('customParameters=%7B%22foo%22%3A%22bar%22%7D');
77+
});
78+
79+
it('throws an error if authDomain is unspecified', async () => {
80+
delete auth.config.authDomain;
81+
82+
await expect(resolver.openPopup(auth, provider, event)).to.be.rejectedWith(
83+
FirebaseError, 'auth/auth-domain-config-required',
84+
);
85+
});
86+
87+
it('throws an error if apiKey is unspecified', async () => {
88+
delete auth.config.apiKey;
89+
90+
await expect(resolver.openPopup(auth, provider, event)).to.be.rejectedWith(
91+
FirebaseError, 'auth/invalid-api-key',
92+
);
93+
});
94+
});
95+
96+
context('#initialize', () => {
97+
let onIframeMessage: (event: GapiAuthEvent) => Promise<void>;
98+
beforeEach(() => {
99+
sinon.stub(gapiLoader, '_loadGapi').returns(Promise.resolve({
100+
open: () => Promise.resolve({
101+
register: (_message: string, cb: (event: GapiAuthEvent) => Promise<void>) => onIframeMessage = cb
102+
})
103+
} as unknown as gapi.iframes.Context));
104+
});
105+
106+
it('only registers once, returns same event manager', async () => {
107+
const manager = await resolver.initialize(auth);
108+
expect(await resolver.initialize(auth)).to.eq(manager);
109+
});
110+
111+
it('iframe event goes through to the manager', async () => {
112+
const manager = await resolver.initialize(auth) as AuthEventManager;
113+
sinon.spy(manager, 'onEvent');
114+
const response = await onIframeMessage({
115+
type: 'authEvent',
116+
authEvent: {type: AuthEventType.LINK_VIA_POPUP} as AuthEvent
117+
});
118+
119+
expect(manager.onEvent).to.have.been.calledWith(
120+
{type: AuthEventType.LINK_VIA_POPUP}
121+
);
122+
expect(response).to.eql({
123+
status: 'ACK',
124+
});
125+
});
126+
});
127+
});

0 commit comments

Comments
 (0)