Skip to content

Commit 3455c28

Browse files
authored
Merge 6558439 into ab5b7d3
2 parents ab5b7d3 + 6558439 commit 3455c28

File tree

13 files changed

+1360
-0
lines changed

13 files changed

+1360
-0
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,6 @@ export { reload } from './core/user/reload';
5454

5555
// model
5656
export { Operation as ActionCodeOperationType } from './model/action_code_info';
57+
58+
// platform-browser/recaptcha
59+
export { RecaptchaVerifier } from './platform_browser/recaptcha/recaptcha_verifier';
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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 { Recaptcha } from './recaptcha/recaptcha';
19+
20+
/**
21+
* A specialized window type that melds the normal window type plus the
22+
* various bits we need. The three different blocks that are &'d together
23+
* cant be defined in the same block together.
24+
*/
25+
export type AuthWindow = {
26+
// Standard window types
27+
[T in keyof Window]: Window[T];
28+
} & {
29+
// Any known / named properties we want to add
30+
grecaptcha?: Recaptcha;
31+
} & {
32+
// A final catch-all for callbacks (which will have random names) that
33+
// we will stick on the window.
34+
[callback: string]: (...args: unknown[]) => void;
35+
};
36+
37+
export const AUTH_WINDOW = (window as unknown) as AuthWindow;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 { _generateCallbackName, _loadJS } from './load_js';
23+
24+
use(sinonChai);
25+
26+
describe('platform-browser/load_js', () => {
27+
afterEach(() => sinon.restore());
28+
29+
describe('_generateCallbackName', () => {
30+
it('generates a callback with a prefix and a number', () => {
31+
expect(_generateCallbackName('foo')).to.match(/__foo\d+/);
32+
});
33+
});
34+
35+
describe('_loadJS', () => {
36+
it('sets the appropriate properties', () => {
37+
const el = document.createElement('script');
38+
sinon.stub(el); // Prevent actually setting the src attribute
39+
sinon.stub(document, 'createElement').returns(el);
40+
41+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
42+
_loadJS('http://localhost/url');
43+
expect(el.setAttribute).to.have.been.calledWith(
44+
'src',
45+
'http://localhost/url'
46+
);
47+
expect(el.type).to.eq('text/javascript');
48+
expect(el.charset).to.eq('UTF-8');
49+
});
50+
});
51+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
function getScriptParentElement(): HTMLDocument | HTMLHeadElement {
19+
return document.getElementsByTagName('head')?.[0] ?? document;
20+
}
21+
22+
export function _loadJS(url: string): Promise<Event> {
23+
// TODO: consider adding timeout support & cancellation
24+
return new Promise((resolve, reject) => {
25+
const el = document.createElement('script');
26+
el.setAttribute('src', url);
27+
el.onload = resolve;
28+
el.onerror = reject;
29+
el.type = 'text/javascript';
30+
el.charset = 'UTF-8';
31+
getScriptParentElement().appendChild(el);
32+
});
33+
}
34+
35+
export function _generateCallbackName(prefix: string): string {
36+
return `__${prefix}${Math.floor(Math.random() * 1000000)}`;
37+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
export interface Parameters {
19+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
20+
[key: string]: any;
21+
}
22+
23+
export interface Recaptcha {
24+
render: (container: HTMLElement, parameters: Parameters) => number;
25+
getResponse: (id: number) => string;
26+
execute: (id: number) => unknown;
27+
reset: (id: number) => unknown;
28+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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 { stubSingleTimeout } from '../../../test/timeout_stub';
27+
import { Auth } from '../../model/auth';
28+
import { AUTH_WINDOW } from '../auth_window';
29+
import * as jsHelpers from '../load_js';
30+
import {
31+
_JSLOAD_CALLBACK,
32+
MOCK_RECAPTCHA_LOADER,
33+
ReCaptchaLoader,
34+
ReCaptchaLoaderImpl
35+
} from './recaptcha_loader';
36+
import { MockReCaptcha } from './recaptcha_mock';
37+
38+
use(chaiAsPromised);
39+
use(sinonChai);
40+
41+
describe('platform-browser/recaptcha/recaptcha_loader', () => {
42+
let auth: Auth;
43+
44+
beforeEach(async () => {
45+
auth = await testAuth();
46+
});
47+
48+
afterEach(() => {
49+
sinon.restore();
50+
delete AUTH_WINDOW.grecaptcha;
51+
});
52+
53+
describe('MockLoader', () => {
54+
it('returns a MockRecaptcha instance', async () => {
55+
expect(await MOCK_RECAPTCHA_LOADER.load(auth)).to.be.instanceOf(
56+
MockReCaptcha
57+
);
58+
});
59+
});
60+
61+
describe('RealLoader', () => {
62+
let triggerNetworkTimeout: () => void;
63+
let jsLoader: { resolve: () => void; reject: () => void };
64+
let loader: ReCaptchaLoader;
65+
const networkTimeoutId = 123;
66+
67+
beforeEach(() => {
68+
triggerNetworkTimeout = stubSingleTimeout(networkTimeoutId);
69+
70+
sinon.stub(jsHelpers, '_loadJS').callsFake(() => {
71+
return new Promise((resolve, reject) => {
72+
jsLoader = { resolve, reject };
73+
});
74+
});
75+
76+
loader = new ReCaptchaLoaderImpl();
77+
});
78+
79+
context('network timeout / errors', () => {
80+
it('rejects if the network times out', async () => {
81+
const promise = loader.load(auth);
82+
triggerNetworkTimeout();
83+
await expect(promise).to.be.rejectedWith(
84+
FirebaseError,
85+
'Firebase: A network AuthError (such as timeout, interrupted connection or unreachable host) has occurred. (auth/network-request-failed).'
86+
);
87+
});
88+
89+
it('rejects with an internal error if the loadJS call fails', async () => {
90+
const promise = loader.load(auth);
91+
jsLoader.reject();
92+
await expect(promise).to.be.rejectedWith(
93+
FirebaseError,
94+
'Firebase: An internal AuthError has occurred. (auth/internal-error).'
95+
);
96+
});
97+
});
98+
99+
context('on js load callback', () => {
100+
function spoofJsLoad(): void {
101+
AUTH_WINDOW[_JSLOAD_CALLBACK]();
102+
}
103+
104+
it('clears the network timeout', () => {
105+
sinon.spy(AUTH_WINDOW, 'clearTimeout');
106+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
107+
loader.load(auth);
108+
spoofJsLoad();
109+
expect(AUTH_WINDOW.clearTimeout).to.have.been.calledWith(
110+
networkTimeoutId
111+
);
112+
});
113+
114+
it('rejects if the grecaptcha object is not on the window', async () => {
115+
const promise = loader.load(auth);
116+
spoofJsLoad();
117+
await expect(promise).to.be.rejectedWith(
118+
FirebaseError,
119+
'Firebase: An internal AuthError has occurred. (auth/internal-error).'
120+
);
121+
});
122+
123+
it('overwrites the render method', async () => {
124+
const promise = loader.load(auth);
125+
const mockRecaptcha = new MockReCaptcha(auth);
126+
const oldRenderMethod = mockRecaptcha.render;
127+
AUTH_WINDOW.grecaptcha = mockRecaptcha;
128+
spoofJsLoad();
129+
expect((await promise).render).not.to.eq(oldRenderMethod);
130+
});
131+
132+
it('returns immediately if the new language code matches the old', async () => {
133+
const promise = loader.load(auth);
134+
AUTH_WINDOW.grecaptcha = new MockReCaptcha(auth);
135+
spoofJsLoad();
136+
await promise;
137+
// Notice no call to spoofJsLoad..
138+
expect(await loader.load(auth)).to.eq(AUTH_WINDOW.grecaptcha);
139+
});
140+
141+
it('returns immediately if grecaptcha is already set on window', async () => {
142+
AUTH_WINDOW.grecaptcha = new MockReCaptcha(auth);
143+
const loader = new ReCaptchaLoaderImpl();
144+
expect(await loader.load(auth)).to.eq(AUTH_WINDOW.grecaptcha);
145+
});
146+
});
147+
});
148+
});

0 commit comments

Comments
 (0)