Skip to content

Commit 3eb667b

Browse files
committed
Address PR feedback, add a test helper for stubbing timeouts
1 parent 1612b51 commit 3eb667b

File tree

8 files changed

+134
-114
lines changed

8 files changed

+134
-114
lines changed

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

Lines changed: 0 additions & 22 deletions
This file was deleted.
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;

packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_loader.test.ts

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,15 @@ import * as sinonChai from 'sinon-chai';
2323
import { FirebaseError } from '@firebase/util';
2424

2525
import { testAuth } from '../../../test/mock_auth';
26+
import { stubSingleTimeout } from '../../../test/timeout_stub';
2627
import { Auth } from '../../model/auth';
27-
import { AuthWindow } from '../auth_window';
28+
import { AUTH_WINDOW } from '../auth_window';
2829
import * as jsHelpers from '../load_js';
2930
import {
30-
_JSLOAD_CALLBACK,
31-
MOCK_RECAPTCHA_LOADER,
32-
ReCaptchaLoader,
33-
ReCaptchaLoaderImpl
31+
_JSLOAD_CALLBACK, MOCK_RECAPTCHA_LOADER, ReCaptchaLoader, ReCaptchaLoaderImpl
3432
} from './recaptcha_loader';
3533
import { MockReCaptcha } from './recaptcha_mock';
3634

37-
const WINDOW: AuthWindow = window;
38-
3935
use(chaiAsPromised);
4036
use(sinonChai);
4137

@@ -48,7 +44,7 @@ describe('platform-browser/recaptcha/recaptcha_loader', () => {
4844

4945
afterEach(() => {
5046
sinon.restore();
51-
delete WINDOW.grecaptcha;
47+
delete AUTH_WINDOW.grecaptcha;
5248
});
5349

5450
describe('MockLoader', () => {
@@ -66,13 +62,7 @@ describe('platform-browser/recaptcha/recaptcha_loader', () => {
6662
const networkTimeoutId = 123;
6763

6864
beforeEach(() => {
69-
sinon.stub(window, 'setTimeout').callsFake(cb => {
70-
triggerNetworkTimeout = () => cb();
71-
// For some bizarre reason setTimeout always get shoehorned into NodeJS.Timeout,
72-
// which is flat-wrong. This is the easiest way to fix it.
73-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
74-
return networkTimeoutId as any;
75-
});
65+
triggerNetworkTimeout = stubSingleTimeout(networkTimeoutId);
7666

7767
sinon.stub(jsHelpers, '_loadJS').callsFake(() => {
7868
return new Promise((resolve, reject) => {
@@ -105,15 +95,15 @@ describe('platform-browser/recaptcha/recaptcha_loader', () => {
10595

10696
context('on js load callback', () => {
10797
function spoofJsLoad(): void {
108-
WINDOW[_JSLOAD_CALLBACK]();
98+
AUTH_WINDOW[_JSLOAD_CALLBACK]();
10999
}
110100

111101
it('clears the network timeout', () => {
112-
sinon.spy(WINDOW, 'clearTimeout');
102+
sinon.spy(AUTH_WINDOW, 'clearTimeout');
113103
// eslint-disable-next-line @typescript-eslint/no-floating-promises
114104
loader.load(auth);
115105
spoofJsLoad();
116-
expect(WINDOW.clearTimeout).to.have.been.calledWith(networkTimeoutId);
106+
expect(AUTH_WINDOW.clearTimeout).to.have.been.calledWith(networkTimeoutId);
117107
});
118108

119109
it('rejects if the grecaptcha object is not on the window', async () => {
@@ -127,25 +117,26 @@ describe('platform-browser/recaptcha/recaptcha_loader', () => {
127117

128118
it('overwrites the render method', async () => {
129119
const promise = loader.load(auth);
130-
const oldRenderMethod = (): string => 'foo';
131-
WINDOW.grecaptcha = { render: oldRenderMethod };
120+
const mockRecaptcha = new MockReCaptcha(auth);
121+
const oldRenderMethod = mockRecaptcha.render;
122+
AUTH_WINDOW.grecaptcha = mockRecaptcha;
132123
spoofJsLoad();
133124
expect((await promise).render).not.to.eq(oldRenderMethod);
134125
});
135126

136127
it('returns immediately if the new language code matches the old', async () => {
137128
const promise = loader.load(auth);
138-
WINDOW.grecaptcha = { render: (): string => 'foo' };
129+
AUTH_WINDOW.grecaptcha = new MockReCaptcha(auth);
139130
spoofJsLoad();
140131
await promise;
141132
// Notice no call to spoofJsLoad..
142-
expect(await loader.load(auth)).to.eq(WINDOW.grecaptcha);
133+
expect(await loader.load(auth)).to.eq(AUTH_WINDOW.grecaptcha);
143134
});
144135

145136
it('returns immediately if grecaptcha is already set on window', async () => {
146-
WINDOW.grecaptcha = { render: (): string => 'foo' };
137+
AUTH_WINDOW.grecaptcha = new MockReCaptcha(auth);
147138
const loader = new ReCaptchaLoaderImpl();
148-
expect(await loader.load(auth)).to.eq(WINDOW.grecaptcha);
139+
expect(await loader.load(auth)).to.eq(AUTH_WINDOW.grecaptcha);
149140
});
150141
});
151142
});

packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_loader.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { querystring } from '@firebase/util';
2020
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../../core/errors';
2121
import { Delay } from '../../core/util/delay';
2222
import { Auth } from '../../model/auth';
23-
import { AuthWindow } from '../auth_window';
23+
import { AUTH_WINDOW } from '../auth_window';
2424
import * as jsHelpers from '../load_js';
2525
import { Recaptcha } from './recaptcha';
2626
import { MockReCaptcha } from './recaptcha_mock';
@@ -36,38 +36,32 @@ export interface ReCaptchaLoader {
3636
clearedOneInstance(): void;
3737
}
3838

39-
function pullGrecaptcha(): Recaptcha | undefined {
40-
return (window as AuthWindow).grecaptcha;
41-
}
42-
43-
const WINDOW = window as AuthWindow;
44-
4539
/**
4640
* Loader for the GReCaptcha library. There should only ever be one of this.
4741
*/
4842
export class ReCaptchaLoaderImpl implements ReCaptchaLoader {
49-
private hl = '';
43+
private hostLanguage = '';
5044
private counter = 0;
51-
private readonly librarySeparatelyLoaded = !!pullGrecaptcha();
45+
private readonly librarySeparatelyLoaded = !!AUTH_WINDOW.grecaptcha;
5246

5347
load(auth: Auth, hl = ''): Promise<Recaptcha> {
5448
if (this.shouldResolveImmediately(hl)) {
55-
return Promise.resolve(pullGrecaptcha()!);
49+
return Promise.resolve(AUTH_WINDOW.grecaptcha!);
5650
}
5751
return new Promise<Recaptcha>((resolve, reject) => {
58-
const networkTimeout = WINDOW.setTimeout(() => {
52+
const networkTimeout = AUTH_WINDOW.setTimeout(() => {
5953
reject(
6054
AUTH_ERROR_FACTORY.create(AuthErrorCode.NETWORK_REQUEST_FAILED, {
6155
appName: auth.name
6256
})
6357
);
6458
}, NETWORK_TIMEOUT_DELAY.get());
6559

66-
WINDOW[_JSLOAD_CALLBACK] = () => {
67-
WINDOW.clearTimeout(networkTimeout);
68-
delete WINDOW[_JSLOAD_CALLBACK];
60+
AUTH_WINDOW[_JSLOAD_CALLBACK] = () => {
61+
AUTH_WINDOW.clearTimeout(networkTimeout);
62+
delete AUTH_WINDOW[_JSLOAD_CALLBACK];
6963

70-
const recaptcha = pullGrecaptcha();
64+
const recaptcha = AUTH_WINDOW.grecaptcha;
7165

7266
if (!recaptcha) {
7367
reject(
@@ -87,7 +81,7 @@ export class ReCaptchaLoaderImpl implements ReCaptchaLoader {
8781
return widgetId;
8882
};
8983

90-
this.hl = hl;
84+
this.hostLanguage = hl;
9185
resolve(recaptcha);
9286
};
9387

@@ -121,8 +115,8 @@ export class ReCaptchaLoaderImpl implements ReCaptchaLoader {
121115
// In cases (2) and (3), we _can't_ reload as it would break the recaptchas
122116
// that are already in the page
123117
return (
124-
!!pullGrecaptcha() &&
125-
(hl === this.hl || this.counter > 0 || this.librarySeparatelyLoaded)
118+
!!AUTH_WINDOW.grecaptcha &&
119+
(hl === this.hostLanguage || this.counter > 0 || this.librarySeparatelyLoaded)
126120
);
127121
}
128122
}

packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_mock.test.ts

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,10 @@ import * as sinonChai from 'sinon-chai';
2323
import { FirebaseError } from '@firebase/util';
2424

2525
import { testAuth } from '../../../test/mock_auth';
26+
import { stubTimeouts, TimerMap } from '../../../test/timeout_stub';
2627
import { Auth } from '../../model/auth';
2728
import {
28-
_EXPIRATION_TIME_MS,
29-
_SOLVE_TIME_MS,
30-
_WIDGET_ID_START,
31-
MockReCaptcha,
32-
MockWidget,
33-
Widget
29+
_EXPIRATION_TIME_MS, _SOLVE_TIME_MS, _WIDGET_ID_START, MockReCaptcha, MockWidget, Widget
3430
} from './recaptcha_mock';
3531

3632
use(sinonChai);
@@ -150,45 +146,26 @@ describe('platform-browser/recaptcha/recaptcha_mock', () => {
150146

151147
context('#execute', () => {
152148
// Stub out a bunch of stuff on setTimer
153-
let tripSolveTimer: () => void;
154-
let tripExpireTimer: () => void;
149+
let pendingTimers: TimerMap;
155150
let callbacks: { [key: string]: sinon.SinonSpy };
156151
let widget: MockWidget;
157152
let timeoutStub: sinon.SinonStub;
158153

159-
const solveTimer = 1;
160-
const expireTimer = 2;
161-
162154
beforeEach(() => {
163155
callbacks = {
164156
'callback': sinon.spy(),
165157
'expired-callback': sinon.spy()
166158
};
167-
timeoutStub = sinon
168-
.stub(window, 'setTimeout')
169-
.callsFake((cb, duration) => {
170-
switch (duration) {
171-
case _SOLVE_TIME_MS:
172-
tripSolveTimer = cb;
173-
// For some bizarre reason setTimeout always get shoehorned into NodeJS.Timeout,
174-
// which is flat-wrong. This is the easiest way to fix it.
175-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
176-
return solveTimer as any;
177-
case _EXPIRATION_TIME_MS:
178-
tripExpireTimer = cb;
179-
return expireTimer;
180-
default:
181-
throw new Error('Requested unknown callback duration');
182-
}
183-
});
159+
pendingTimers = stubTimeouts();
160+
timeoutStub = window.setTimeout as unknown as sinon.SinonStub;
184161
widget = new MockWidget(container, auth.name, callbacks);
185162
});
186163

187164
it('keeps re-executing with new tokens if expiring', () => {
188-
tripSolveTimer();
189-
tripExpireTimer();
190-
tripSolveTimer();
191-
tripExpireTimer();
165+
pendingTimers[_SOLVE_TIME_MS]();
166+
pendingTimers[_EXPIRATION_TIME_MS]();
167+
pendingTimers[_SOLVE_TIME_MS]();
168+
pendingTimers[_EXPIRATION_TIME_MS]();
192169

193170
expect(callbacks['callback']).to.have.been.calledTwice;
194171
expect(callbacks['callback'].getCall(0).args[0]).not.to.eq(
@@ -197,21 +174,21 @@ describe('platform-browser/recaptcha/recaptcha_mock', () => {
197174
});
198175

199176
it('posts callback with a random alphanumeric code', () => {
200-
tripSolveTimer();
177+
pendingTimers[_SOLVE_TIME_MS]();
201178
const arg: string = callbacks['callback'].getCall(0).args[0];
202179
expect(arg).to.be.a('string');
203180
expect(arg.length).to.equal(50);
204181
});
205182

206183
it('expired callback does execute if just solve trips', () => {
207-
tripSolveTimer();
184+
pendingTimers[_SOLVE_TIME_MS]();
208185
expect(callbacks['callback']).to.have.been.called;
209186
expect(callbacks['expired-callback']).not.to.have.been.called;
210187
});
211188

212189
it('expired callback executes if just expiration timer trips', () => {
213-
tripSolveTimer();
214-
tripExpireTimer();
190+
pendingTimers[_SOLVE_TIME_MS]();
191+
pendingTimers[_EXPIRATION_TIME_MS]();
215192
expect(callbacks['callback']).to.have.been.called;
216193
expect(callbacks['expired-callback']).to.have.been.called;
217194
});

packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_verifier.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { testAuth } from '../../../test/mock_auth';
2727
import * as fetch from '../../../test/mock_fetch';
2828
import { Endpoint } from '../../api';
2929
import { Auth } from '../../model/auth';
30-
import { AuthWindow } from '../auth_window';
30+
import { AUTH_WINDOW } from '../auth_window';
3131
import { Parameters, Recaptcha } from './recaptcha';
3232
import { _JSLOAD_CALLBACK, MOCK_RECAPTCHA_LOADER } from './recaptcha_loader';
3333
import { MockReCaptcha } from './recaptcha_mock';
@@ -144,8 +144,8 @@ describe('platform_browser/recaptcha/recaptcha_verifier.ts', () => {
144144

145145
it('calls existing global function if on the window', async () => {
146146
let token = '';
147-
(window as AuthWindow).callbackOnWindowObject = (t: string): void => {
148-
token = t;
147+
AUTH_WINDOW.callbackOnWindowObject = (t: unknown): void => {
148+
token = t as string;
149149
};
150150

151151
parameters = {
@@ -156,7 +156,7 @@ describe('platform_browser/recaptcha/recaptcha_verifier.ts', () => {
156156
const expected = await verifier.verify();
157157
expect(token).to.eq(expected);
158158

159-
delete (window as AuthWindow).callbackOnWindowObject;
159+
delete AUTH_WINDOW.callbackOnWindowObject;
160160
});
161161
});
162162

packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_verifier.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,9 @@ import { AuthErrorCode } from '../../core/errors';
2323
import { assert } from '../../core/util/assert';
2424
import { _isHttpOrHttps } from '../../core/util/location';
2525
import { Auth } from '../../model/auth';
26-
import { AuthWindow } from '../auth_window';
26+
import { AUTH_WINDOW } from '../auth_window';
2727
import { Parameters, Recaptcha } from './recaptcha';
28-
import {
29-
MOCK_RECAPTCHA_LOADER,
30-
RECAPTCHA_LOADER,
31-
ReCaptchaLoader
32-
} from './recaptcha_loader';
28+
import { MOCK_RECAPTCHA_LOADER, RECAPTCHA_LOADER, ReCaptchaLoader } from './recaptcha_loader';
3329

3430
const DEFAULT_PARAMS: Parameters = {
3531
theme: 'light',
@@ -169,7 +165,7 @@ export class RecaptchaVerifier implements ApplicationVerifier {
169165
if (typeof existing === 'function') {
170166
existing(token);
171167
} else if (typeof existing === 'string') {
172-
const globalFunc = (window as AuthWindow)[existing];
168+
const globalFunc = AUTH_WINDOW[existing];
173169
if (typeof globalFunc === 'function') {
174170
globalFunc(token);
175171
}
@@ -218,10 +214,9 @@ export class RecaptchaVerifier implements ApplicationVerifier {
218214
}
219215

220216
function isWorker(): boolean {
221-
const win: AuthWindow = window;
222217
return (
223-
typeof win['WorkerGlobalScope'] !== 'undefined' &&
224-
typeof win['importScripts'] === 'function'
218+
typeof AUTH_WINDOW['WorkerGlobalScope'] !== 'undefined' &&
219+
typeof AUTH_WINDOW['importScripts'] === 'function'
225220
);
226221
}
227222

0 commit comments

Comments
 (0)