Skip to content

Commit 4f6ee70

Browse files
authored
[Auth] Add integration tests (headless & webdriver) for middleware (#6161)
* Add integration tests for auth middleware * Webdriver tests * Lint * Formatting
1 parent 91406c5 commit 4f6ee70

13 files changed

+366
-1
lines changed

packages/auth/test/integration/flows/anonymous.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
getTestInstance,
3838
randomEmail
3939
} from '../../helpers/integration/helpers';
40+
import { generateMiddlewareTests } from './middleware_test_generator';
4041

4142
use(chaiAsPromised);
4243

@@ -128,4 +129,8 @@ describe('Integration test: anonymous auth', () => {
128129
);
129130
});
130131
});
132+
133+
generateMiddlewareTests(() => auth, () => {
134+
return signInAnonymously(auth);
135+
});
131136
});

packages/auth/test/integration/flows/custom.local.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
getTestInstance,
4040
randomEmail
4141
} from '../../helpers/integration/helpers';
42+
import { generateMiddlewareTests } from './middleware_test_generator';
4243

4344
use(chaiAsPromised);
4445

@@ -225,4 +226,8 @@ describe('Integration test: custom auth', () => {
225226
);
226227
});
227228
});
229+
230+
generateMiddlewareTests(() => auth, () => {
231+
return signInWithCustomToken(auth, customToken);
232+
});
228233
});

packages/auth/test/integration/flows/email.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
getTestInstance,
3939
randomEmail
4040
} from '../../helpers/integration/helpers';
41+
import { generateMiddlewareTests } from './middleware_test_generator';
4142

4243
use(chaiAsPromised);
4344

@@ -168,5 +169,9 @@ describe('Integration test: email/password auth', () => {
168169
);
169170
expect(userA.uid).to.eq(userB.uid);
170171
});
172+
173+
generateMiddlewareTests(() => auth, () => {
174+
return signInWithEmailAndPassword(auth, email, 'password');
175+
});
171176
});
172177
});

packages/auth/test/integration/flows/idp.local.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
getTestInstance,
4242
randomEmail
4343
} from '../../helpers/integration/helpers';
44+
import { generateMiddlewareTests } from './middleware_test_generator';
4445

4546
use(chaiAsPromised);
4647

@@ -285,4 +286,11 @@ describe('Integration test: headless IdP', () => {
285286
'github.com'
286287
]);
287288
});
289+
290+
generateMiddlewareTests(() => auth, () => {
291+
return signInWithCredential(
292+
auth,
293+
GoogleAuthProvider.credential(oauthIdToken)
294+
);
295+
});
288296
});
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/**
2+
* @license
3+
* Copyright 2022 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 chaiAsPromised from 'chai-as-promised';
20+
import * as sinon from 'sinon';
21+
import sinonChai from 'sinon-chai';
22+
23+
// eslint-disable-next-line import/no-extraneous-dependencies
24+
import {Auth, createUserWithEmailAndPassword, User} from '@firebase/auth';
25+
import { randomEmail } from '../../helpers/integration/helpers';
26+
27+
use(chaiAsPromised);
28+
use(sinonChai);
29+
30+
export function generateMiddlewareTests(authGetter: () => Auth, signIn: () => Promise<unknown>): void {
31+
context('middleware', () => {
32+
let auth: Auth;
33+
let unsubscribes: Array<() => void>;
34+
35+
beforeEach(() => {
36+
auth = authGetter();
37+
unsubscribes = [];
38+
});
39+
40+
afterEach(() => {
41+
for (const u of unsubscribes) {
42+
u();
43+
}
44+
});
45+
46+
/**
47+
* Helper function for adding beforeAuthStateChanged that will
48+
* automatically unsubscribe after every test (since some tests may
49+
* perform cleanup after that would be affected by the middleware)
50+
*/
51+
function beforeAuthStateChanged(callback: (user: User | null) => void | Promise<void>): void {
52+
unsubscribes.push(auth.beforeAuthStateChanged(callback));
53+
}
54+
55+
it('can prevent user sign in', async () => {
56+
beforeAuthStateChanged(() => {
57+
throw new Error('stop sign in');
58+
});
59+
60+
await expect(signIn()).to.be.rejectedWith('auth/login-blocked');
61+
expect(auth.currentUser).to.be.null;
62+
});
63+
64+
it('can prevent user sign in as a promise', async () => {
65+
beforeAuthStateChanged(() => {
66+
return Promise.reject('stop sign in');
67+
});
68+
69+
await expect(signIn()).to.be.rejectedWith('auth/login-blocked');
70+
expect(auth.currentUser).to.be.null;
71+
});
72+
73+
it('keeps previously-logged in user if blocked', async () => {
74+
// Use a random email/password sign in for the base user
75+
const {user: baseUser} = await createUserWithEmailAndPassword(auth, randomEmail(), 'password');
76+
77+
beforeAuthStateChanged(() => {
78+
throw new Error('stop sign in');
79+
});
80+
81+
await expect(signIn()).to.be.rejectedWith('auth/login-blocked');
82+
expect(auth.currentUser).to.eq(baseUser);
83+
});
84+
85+
it('can allow sign in', async () => {
86+
beforeAuthStateChanged(() => {
87+
// Pass
88+
});
89+
90+
await expect(signIn()).not.to.be.rejected;
91+
expect(auth.currentUser).not.to.be.null;
92+
});
93+
94+
it('can allow sign in as a promise', async () => {
95+
beforeAuthStateChanged(() => {
96+
return Promise.resolve();
97+
});
98+
99+
await expect(signIn()).not.to.be.rejected;
100+
expect(auth.currentUser).not.to.be.null;
101+
});
102+
103+
it('overrides previous user if allowed', async () => {
104+
// Use a random email/password sign in for the base user
105+
const {user: baseUser} = await createUserWithEmailAndPassword(auth, randomEmail(), 'password');
106+
107+
beforeAuthStateChanged(() => {
108+
// Pass
109+
});
110+
111+
await expect(signIn()).not.to.be.rejected;
112+
expect(auth.currentUser).not.to.eq(baseUser);
113+
});
114+
115+
it('will reject if one callback fails', async () => {
116+
// Also check that the function is called multiple
117+
// times
118+
const spy = sinon.spy();
119+
120+
beforeAuthStateChanged(spy);
121+
beforeAuthStateChanged(spy);
122+
beforeAuthStateChanged(spy);
123+
beforeAuthStateChanged(() => {
124+
throw new Error('stop sign in');
125+
});
126+
127+
await expect(signIn()).to.be.rejectedWith('auth/login-blocked');
128+
expect(auth.currentUser).to.be.null;
129+
expect(spy).to.have.been.calledThrice;
130+
});
131+
132+
it('keeps previously-logged in user if one rejects', async () => {
133+
// Use a random email/password sign in for the base user
134+
const {user: baseUser} = await createUserWithEmailAndPassword(auth, randomEmail(), 'password');
135+
136+
// Also check that the function is called multiple
137+
// times
138+
const spy = sinon.spy();
139+
140+
beforeAuthStateChanged(spy);
141+
beforeAuthStateChanged(spy);
142+
beforeAuthStateChanged(spy);
143+
beforeAuthStateChanged(() => {
144+
throw new Error('stop sign in');
145+
});
146+
147+
await expect(signIn()).to.be.rejectedWith('auth/login-blocked');
148+
expect(auth.currentUser).to.eq(baseUser);
149+
expect(spy).to.have.been.calledThrice;
150+
});
151+
152+
it('allows sign in with multiple callbacks all pass', async () => {
153+
// Use a random email/password sign in for the base user
154+
const {user: baseUser} = await createUserWithEmailAndPassword(auth, randomEmail(), 'password');
155+
156+
// Also check that the function is called multiple
157+
// times
158+
const spy = sinon.spy();
159+
160+
beforeAuthStateChanged(spy);
161+
beforeAuthStateChanged(spy);
162+
beforeAuthStateChanged(spy);
163+
164+
await expect(signIn()).not.to.be.rejected;
165+
expect(auth.currentUser).not.to.eq(baseUser);
166+
expect(spy).to.have.been.calledThrice;
167+
});
168+
169+
it('does not call subsequent callbacks after rejection', async () => {
170+
const firstSpy = sinon.spy();
171+
const secondSpy = sinon.spy();
172+
173+
beforeAuthStateChanged(firstSpy);
174+
beforeAuthStateChanged(() => {
175+
throw new Error('stop sign in');
176+
});
177+
beforeAuthStateChanged(secondSpy);
178+
179+
await expect(signIn()).to.be.rejectedWith('auth/login-blocked');
180+
expect(firstSpy).to.have.been.calledOnce;
181+
expect(secondSpy).not.to.have.been.called;
182+
});
183+
184+
it('can prevent sign-out', async () => {
185+
await signIn();
186+
const user = auth.currentUser;
187+
188+
beforeAuthStateChanged(() => {
189+
throw new Error('block sign out');
190+
});
191+
192+
await expect(auth.signOut()).to.be.rejectedWith('auth/login-blocked');
193+
expect(auth.currentUser).to.eq(user);
194+
});
195+
});
196+
}

packages/auth/test/integration/flows/oob.local.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
getTestInstance,
5454
randomEmail
5555
} from '../../helpers/integration/helpers';
56+
import { generateMiddlewareTests } from './middleware_test_generator';
5657

5758
use(chaiAsPromised);
5859

@@ -267,6 +268,14 @@ describe('Integration test: oob codes', () => {
267268
signInWithEmailLink(auth, email, otherSession.oobLink)
268269
).to.be.rejectedWith(FirebaseError, 'auth/invalid-email');
269270
});
271+
272+
generateMiddlewareTests(() => auth, () => {
273+
return signInWithEmailLink(
274+
auth,
275+
email,
276+
oobSession.oobLink
277+
);
278+
});
270279
});
271280

272281
it('can be used to verify email', async () => {

packages/auth/test/integration/flows/phone.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
getTestInstance
4343
} from '../../helpers/integration/helpers';
4444
import { getPhoneVerificationCodes } from '../../helpers/integration/emulator_rest_helpers';
45+
import { generateMiddlewareTests } from './middleware_test_generator';
4546

4647
use(chaiAsPromised);
4748

@@ -306,4 +307,9 @@ describe('Integration test: phone auth', () => {
306307
expect(errorUserCred.user.uid).to.eq(signUpCred.user.uid);
307308
});
308309
});
310+
311+
generateMiddlewareTests(() => auth, async () => {
312+
const cr = await signInWithPhoneNumber(auth, PHONE_A.phoneNumber, verifier);
313+
await cr.confirm(await code(cr, PHONE_A.code));
314+
});
309315
});

packages/auth/test/integration/webdriver/persistence.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,22 @@
1717

1818
// eslint-disable-next-line import/no-extraneous-dependencies
1919
import { UserCredential } from '@firebase/auth';
20-
import { expect } from 'chai';
20+
import { expect, use } from 'chai';
21+
import chaiAsPromised from 'chai-as-promised';
2122
import { createAnonAccount } from '../../helpers/integration/emulator_rest_helpers';
2223
import { API_KEY } from '../../helpers/integration/settings';
2324
import { START_FUNCTION } from './util/auth_driver';
2425
import {
2526
AnonFunction,
2627
CoreFunction,
28+
MiddlewareFunction,
2729
PersistenceFunction
2830
} from './util/functions';
2931
import { JsLoadCondition } from './util/js_load_condition';
3032
import { browserDescribe } from './util/test_runner';
3133

34+
use(chaiAsPromised);
35+
3236
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
3337
async function testPersistedUser() {
3438
const account = await createAnonAccount();
@@ -458,6 +462,39 @@ browserDescribe('WebDriver persistence test', (driver, browser) => {
458462
expect(await driver.getUserSnapshot()).to.contain({ uid: uid2 });
459463
});
460464

465+
it('middleware does not block tab sync', async () => {
466+
if (driver.isCompatLayer()) {
467+
// Compat layer is skipped because it doesn't support middleware
468+
console.warn('Skipping middleware tabs in compat test');
469+
return;
470+
}
471+
472+
// Blocking middleware in main page
473+
await driver.call(MiddlewareFunction.ATTACH_BLOCKING_MIDDLEWARE);
474+
475+
// Check that it blocks basic sign in
476+
await expect(driver.call(
477+
AnonFunction.SIGN_IN_ANONYMOUSLY
478+
)).to.be.rejectedWith('auth/login-blocked');
479+
const userInPopup = await driver.getUserSnapshot();
480+
expect(userInPopup).to.be.null;
481+
482+
// Now sign in in new page
483+
await driver.webDriver.executeScript('window.open(".");');
484+
await driver.selectPopupWindow();
485+
await driver.webDriver.wait(new JsLoadCondition(START_FUNCTION));
486+
await driver.injectConfigAndInitAuth();
487+
await driver.waitForAuthInit();
488+
const cred: UserCredential = await driver.call(
489+
AnonFunction.SIGN_IN_ANONYMOUSLY
490+
);
491+
492+
// And make sure it was updated in main window
493+
await driver.selectMainWindow({ noWait: true });
494+
await driver.pause(700);
495+
expect((await driver.getUserSnapshot()).uid).to.eq(cred.user.uid);
496+
});
497+
461498
it('sync current user across windows with localStorage', async () => {
462499
await driver.webDriver.navigate().refresh();
463500
// Simulate browsers that do not support indexedDB.

0 commit comments

Comments
 (0)