Skip to content

Commit c3a73b2

Browse files
committed
Tests
1 parent 79d4a5c commit c3a73b2

File tree

5 files changed

+265
-11
lines changed

5 files changed

+265
-11
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ export type AuthWindow = {
2828
} & {
2929
// Any known / named properties we want to add
3030
grecaptcha?: Recaptcha;
31+
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
3132
___jsl?: Record<string, any>;
33+
gapi: typeof gapi;
3234
} & {
3335
// A final catch-all for callbacks (which will have random names) that
3436
// we will stick on the window.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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 { FirebaseError } from '@firebase/util';
24+
25+
import { testAuth } from '../../../test/mock_auth';
26+
import { Auth } from '../../model/auth';
27+
import { AUTH_WINDOW } from '../auth_window';
28+
import { _loadGapi, _resetLoader } from './gapi';
29+
30+
use(sinonChai);
31+
use(chaiAsPromised);
32+
33+
describe('src/platform_browser/iframe/gapi', () => {
34+
let library: unknown;
35+
let tag: HTMLElement;
36+
let auth: Auth;
37+
function onJsLoad(): void {
38+
AUTH_WINDOW.gapi = library as typeof gapi;
39+
AUTH_WINDOW[(tag as HTMLScriptElement).src.split('onload=')[1]]();
40+
}
41+
42+
beforeEach(async () => {
43+
44+
const head = document.createElement('div');
45+
tag = document.createElement('script');
46+
47+
sinon.stub(document, 'createElement').returns(tag);
48+
sinon.stub(document, 'getElementsByTagName').returns([head] as unknown as HTMLCollection);
49+
sinon.stub(head, 'appendChild').callsFake(() => {
50+
onJsLoad();
51+
return head;
52+
});
53+
54+
auth = await testAuth();
55+
});
56+
57+
function makeGapi(result: unknown, timesout = false): Record<string, unknown> {
58+
const callbackFn = timesout === false ? 'callback' : 'ontimeout';
59+
return {
60+
load: sinon.stub().callsFake((_name: string, params: Record<string, Function>) => params[callbackFn]()),
61+
iframes: {
62+
getContext: () => result,
63+
}
64+
};
65+
}
66+
67+
afterEach(() => {
68+
sinon.restore();
69+
delete AUTH_WINDOW.gapi;
70+
_resetLoader();
71+
});
72+
73+
it('calls gapi.load once it is ready', async () => {
74+
const gapi = makeGapi('context!');
75+
76+
library = gapi;
77+
expect(await _loadGapi(auth)).to.eq('context!');
78+
expect(gapi.load).to.have.been.called;
79+
});
80+
81+
it('resets the gapi.load state', async () => {
82+
AUTH_WINDOW.___jsl = {
83+
H: {
84+
something: {
85+
r: ['requested'],
86+
L: ['loaded', 'test'],
87+
}
88+
},
89+
CP: [1, 2, 3, 4]
90+
};
91+
92+
library = makeGapi('iframes');
93+
94+
await _loadGapi(auth);
95+
96+
// Expect deep equality, but *not* pointer equality
97+
expect(AUTH_WINDOW.___jsl.H.something.r).to.eql(
98+
AUTH_WINDOW.___jsl.H.something.L
99+
);
100+
expect(AUTH_WINDOW.___jsl.H.something.r).not.to.eq(
101+
AUTH_WINDOW.___jsl.H.something.L
102+
);
103+
expect(AUTH_WINDOW.___jsl.CP).to.eql([null, null, null, null]);
104+
});
105+
106+
it('returns the cached object without reloading', async () => {
107+
library = makeGapi('test');
108+
109+
expect(await _loadGapi(auth)).to.eq('test');
110+
expect(await _loadGapi(auth)).to.eq('test');
111+
112+
expect(document.createElement).to.have.been.calledOnce;
113+
});
114+
115+
it('rejects with a network error if load fails', async () => {
116+
library = {};
117+
await expect(_loadGapi(auth)).to.be.rejectedWith(FirebaseError, 'auth/network-request-failed');
118+
});
119+
120+
it('rejects with a network error if ontimeout called', async () => {
121+
library = makeGapi(undefined, /* timesout */ true);
122+
await expect(_loadGapi(auth)).to.be.rejectedWith(FirebaseError, 'auth/network-request-failed');
123+
});
124+
125+
it('resets the load promise if the load errors', async () => {
126+
library = {};
127+
const firstAttempt = _loadGapi(auth);
128+
await expect(firstAttempt).to.be.rejectedWith(FirebaseError, 'auth/network-request-failed');
129+
expect(_loadGapi(auth)).not.to.eq(firstAttempt);
130+
});
131+
});

packages-exp/auth-exp/src/platform_browser/iframe/gapi.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ function resetUnloadedGapiModules(): void {
3232
// Clear last failed gapi.load state to force next gapi.load to first
3333
// load the failed gapi.iframes module.
3434
// Get gapix.beacon context.
35-
const beacon = AUTH_WINDOW['___jsl'];
35+
console.warn(AUTH_WINDOW.___jsl);
36+
const beacon = AUTH_WINDOW.___jsl;
3637
// Get current hint.
3738
if (beacon?.H) {
3839
// Get gapi hint.
39-
for (const hint of beacon.H) {
40+
for (const hint of Object.keys(beacon.H)) {
4041
// Requested modules.
4142
beacon.H[hint].r = beacon.H[hint].r || [];
4243
// Loaded modules.
@@ -83,10 +84,11 @@ function loadGapi(auth: Auth): Promise<gapi.iframes.Context> {
8384
});
8485
};
8586

86-
if (gapi?.iframes?.Iframe) {
87+
if (AUTH_WINDOW.gapi?.iframes?.Iframe) {
8788
// If gapi.iframes.Iframe available, resolve.
8889
resolve(gapi.iframes.getContext());
89-
} else if (!!gapi?.load) {
90+
} else if (!!AUTH_WINDOW.gapi?.load) {
91+
console.error('here');
9092
// Gapi loader ready, load gapi.iframes.
9193
loadGapiIframe();
9294
} else {
@@ -125,3 +127,7 @@ export function _loadGapi(auth: Auth): Promise<gapi.iframes.Context> {
125127
cachedGApiLoader = cachedGApiLoader || loadGapi(auth);
126128
return cachedGApiLoader;
127129
}
130+
131+
export function _resetLoader(): void {
132+
cachedGApiLoader = null;
133+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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 { FirebaseError } from '@firebase/util';
25+
26+
import { TEST_AUTH_DOMAIN, TEST_KEY, testAuth } from '../../../test/mock_auth';
27+
import { stubSingleTimeout } from '../../../test/timeout_stub';
28+
import { Auth } from '../../model/auth';
29+
import { AUTH_WINDOW } from '../auth_window';
30+
import * as gapiLoader from './gapi';
31+
import { _openIframe } from './iframe';
32+
33+
use(sinonChai);
34+
use(chaiAsPromised);
35+
36+
type IframesCallback = (iframesLib: unknown) => Promise<unknown>;
37+
38+
describe('src/platform_browser/iframe/iframe', () => {
39+
let auth: Auth;
40+
let iframeSettings: Record<string, unknown>;
41+
let libraryLoadedCallback: IframesCallback;
42+
43+
beforeEach(async () => {
44+
AUTH_WINDOW.gapi = {iframes: {
45+
CROSS_ORIGIN_IFRAMES_FILTER: 'cross-origin-filter',
46+
}} as unknown as typeof gapi;
47+
auth = await testAuth();
48+
49+
sinon.stub(gapiLoader, '_loadGapi').returns(Promise.resolve({
50+
open: sinon.stub().callsFake((settings: Record<string, unknown>, cb: IframesCallback) => {
51+
iframeSettings = settings;
52+
libraryLoadedCallback = cb;
53+
})
54+
}) as unknown as Promise<gapi.iframes.Context>);
55+
});
56+
57+
afterEach(() => {
58+
sinon.restore();
59+
});
60+
61+
it('sets all the correct settings', async () => {
62+
await _openIframe(auth);
63+
console.warn(iframeSettings);
64+
65+
expect(iframeSettings.where).to.eql(document.body);
66+
expect(iframeSettings.url).to.eq(`https://${TEST_AUTH_DOMAIN}/__/auth/iframe?apiKey=${TEST_KEY}&appName=test-app&v=${SDK_VERSION}`);
67+
expect(iframeSettings.messageHandlersFilter).to.eq('cross-origin-filter');
68+
expect(iframeSettings.attributes).to.eql( {
69+
style: {
70+
position: 'absolute',
71+
top: '-100px',
72+
width: '1px',
73+
height: '1px'
74+
}
75+
});
76+
expect(iframeSettings.dontclear).to.be.true;
77+
});
78+
79+
context('on load callback', () => {
80+
let iframe: sinon.SinonStubbedInstance<gapi.iframes.Iframe>;
81+
let clearTimeoutStub: sinon.SinonStub;
82+
83+
beforeEach(() => {
84+
iframe = sinon.stub({
85+
restyle: () => {},
86+
ping: () => {}
87+
} as unknown as gapi.iframes.Iframe);
88+
clearTimeoutStub = sinon.stub(AUTH_WINDOW, 'clearTimeout');
89+
});
90+
91+
it('restyles the iframe to prevent hideOnLeave', async () => {
92+
stubSingleTimeout();
93+
iframe.ping.returns(Promise.resolve([iframe]));
94+
await libraryLoadedCallback(iframe);
95+
expect(iframe.restyle).to.have.been.calledWith({
96+
setHideOnLeave: false
97+
});
98+
});
99+
100+
it('rejects if the iframe ping promise rejects', async () => {
101+
stubSingleTimeout();
102+
iframe.ping.returns(Promise.reject('no'));
103+
await expect(libraryLoadedCallback(iframe)).to.be.rejectedWith(FirebaseError, 'auth/network-request-failed');
104+
});
105+
106+
it('clears the rejection timeout on success', async () => {
107+
stubSingleTimeout(123);
108+
iframe.ping.returns(Promise.resolve([iframe]));
109+
await libraryLoadedCallback(iframe);
110+
expect(clearTimeoutStub).to.have.been.calledWith(123);
111+
});
112+
});
113+
});

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import { querystring } from '@firebase/util';
2121
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../../core/errors';
2222
import { Delay } from '../../core/util/delay';
2323
import { Auth } from '../../model/auth';
24-
import { _loadGapi } from './gapi';
24+
import { AUTH_WINDOW } from '../auth_window';
25+
import * as gapiLoader from './gapi';
2526

2627
const PING_TIMEOUT = new Delay(5000, 15000);
2728
const IFRAME_ATTRIBUTES = {
@@ -40,20 +41,20 @@ function getIframeUrl(auth: Auth): string {
4041
apiKey: auth.config.apiKey,
4142
appName: auth.name,
4243
v: SDK_VERSION
43-
}
44+
};
4445
// Can pass 'eid' as one of 'p' (production), 's' (staging), or 't' (test)
4546
// TODO: do we care about frameworks? pass them as fw=
4647

47-
return `${url}?${querystring(params)}`;
48+
return `${url}?${querystring(params).slice(1)}`;
4849
}
4950

5051
export async function _openIframe(auth: Auth): Promise<gapi.iframes.Iframe> {
51-
const context = await _loadGapi(auth);
52+
const context = await gapiLoader._loadGapi(auth);
5253
return context.open(
5354
{
5455
where: document.body,
5556
url: getIframeUrl(auth),
56-
messageHandlersFilter: gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER,
57+
messageHandlersFilter: AUTH_WINDOW.gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER,
5758
attributes: IFRAME_ATTRIBUTES,
5859
dontclear: true
5960
},
@@ -64,7 +65,8 @@ export async function _openIframe(auth: Auth): Promise<gapi.iframes.Iframe> {
6465
});
6566
// Confirm iframe is correctly loaded.
6667
// To fallback on failure, set a timeout.
67-
const networkErrorTimer = setTimeout(() => {
68+
console.warn('setting timeout');
69+
const networkErrorTimer = AUTH_WINDOW.setTimeout(() => {
6870
reject(
6971
AUTH_ERROR_FACTORY.create(AuthErrorCode.NETWORK_REQUEST_FAILED, {
7072
appName: auth.name
@@ -73,7 +75,7 @@ export async function _openIframe(auth: Auth): Promise<gapi.iframes.Iframe> {
7375
}, PING_TIMEOUT.get());
7476
// Clear timer and resolve pending iframe ready promise.
7577
function clearTimerAndResolve(): void {
76-
clearTimeout(networkErrorTimer);
78+
AUTH_WINDOW.clearTimeout(networkErrorTimer);
7779
resolve(iframe);
7880
};
7981
// This returns an IThenable. However the reject part does not call

0 commit comments

Comments
 (0)