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+ }
0 commit comments