@@ -12,6 +12,7 @@ import {
1212 signInWithProvider ,
1313 signInWithCustomToken ,
1414 generateTotpQrCode ,
15+ completeEmailLinkSignIn ,
1516} from "./auth" ;
1617
1718vi . mock ( "firebase/auth" , ( ) => ( {
@@ -1226,3 +1227,207 @@ describe("generateTotpQrCode", () => {
12261227 expect ( mockSecret . generateQrCodeUrl ) . not . toHaveBeenCalled ( ) ;
12271228 } ) ;
12281229} ) ;
1230+
1231+ describe ( "completeEmailLinkSignIn" , ( ) => {
1232+ beforeEach ( ( ) => {
1233+ vi . clearAllMocks ( ) ;
1234+ Object . defineProperty ( window , "location" , {
1235+ value : { href : "https://example.com/auth?oobCode=abc123" } ,
1236+ writable : true ,
1237+ } ) ;
1238+ } ) ;
1239+
1240+ afterEach ( ( ) => {
1241+ window . localStorage . clear ( ) ;
1242+ } ) ;
1243+
1244+ it ( "should return null when URL is not an email link" , async ( ) => {
1245+ const mockUI = createMockUI ( ) ;
1246+ const currentUrl = "https://example.com/not-email-link" ;
1247+
1248+ vi . mocked ( _isSignInWithEmailLink ) . mockReturnValue ( false ) ;
1249+
1250+ const result = await completeEmailLinkSignIn ( mockUI , currentUrl ) ;
1251+
1252+ expect ( _isSignInWithEmailLink ) . toHaveBeenCalledWith ( mockUI . auth , currentUrl ) ;
1253+ expect ( result ) . toBeNull ( ) ;
1254+
1255+ expect ( vi . mocked ( mockUI . setState ) . mock . calls ) . toEqual ( [ [ "idle" ] ] ) ;
1256+ } ) ;
1257+
1258+ it ( "should return null when no email is stored in localStorage" , async ( ) => {
1259+ const mockUI = createMockUI ( ) ;
1260+ const currentUrl = "https://example.com/auth?oobCode=abc123" ;
1261+
1262+ vi . mocked ( _isSignInWithEmailLink ) . mockReturnValue ( true ) ;
1263+
1264+ const result = await completeEmailLinkSignIn ( mockUI , currentUrl ) ;
1265+
1266+ expect ( _isSignInWithEmailLink ) . toHaveBeenCalledWith ( mockUI . auth , currentUrl ) ;
1267+ expect ( result ) . toBeNull ( ) ;
1268+ expect ( vi . mocked ( mockUI . setState ) . mock . calls ) . toEqual ( [ [ "idle" ] ] ) ;
1269+ } ) ;
1270+
1271+ it ( "should complete email link sign-in with no behavior" , async ( ) => {
1272+ const mockUI = createMockUI ( ) ;
1273+ const currentUrl = "https://example.com/auth?oobCode=abc123" ;
1274+ const email = "test@example.com" ;
1275+ const mockCredential = { providerId : "emailLink" } as UserCredential ;
1276+ const emailLinkCredential = { providerId : "emailLink" } as any ;
1277+
1278+ vi . mocked ( _isSignInWithEmailLink ) . mockReturnValue ( true ) ;
1279+ window . localStorage . setItem ( "emailForSignIn" , email ) ;
1280+ vi . mocked ( hasBehavior ) . mockReturnValue ( false ) ;
1281+ vi . mocked ( EmailAuthProvider . credentialWithLink ) . mockReturnValue ( emailLinkCredential ) ;
1282+ vi . mocked ( _signInWithCredential ) . mockResolvedValue ( mockCredential ) ;
1283+
1284+ const result = await completeEmailLinkSignIn ( mockUI , currentUrl ) ;
1285+
1286+ expect ( _isSignInWithEmailLink ) . toHaveBeenCalledWith ( mockUI . auth , currentUrl ) ;
1287+ expect ( hasBehavior ) . toHaveBeenCalledWith ( mockUI , "autoUpgradeAnonymousCredential" ) ;
1288+ expect ( EmailAuthProvider . credentialWithLink ) . toHaveBeenCalledWith ( email , currentUrl ) ;
1289+ expect ( _signInWithCredential ) . toHaveBeenCalledWith ( mockUI . auth , emailLinkCredential ) ;
1290+ expect ( result ) . toBe ( mockCredential ) ;
1291+ expect ( vi . mocked ( mockUI . setState ) . mock . calls ) . toEqual ( [ [ "pending" ] , [ "pending" ] , [ "idle" ] , [ "idle" ] ] ) ;
1292+ expect ( window . localStorage . getItem ( "emailForSignIn" ) ) . toBeNull ( ) ;
1293+ } ) ;
1294+
1295+ it ( "should call autoUpgradeAnonymousCredential behavior when enabled and return result" , async ( ) => {
1296+ const mockUI = createMockUI ( ) ;
1297+ const currentUrl = "https://example.com/auth?oobCode=abc123" ;
1298+ const email = "test@example.com" ;
1299+ const emailLinkCredential = { providerId : "emailLink" } as any ;
1300+ const mockResult = { providerId : "upgraded" } as UserCredential ;
1301+
1302+ vi . mocked ( _isSignInWithEmailLink ) . mockReturnValue ( true ) ;
1303+ window . localStorage . setItem ( "emailForSignIn" , email ) ;
1304+ vi . mocked ( hasBehavior ) . mockReturnValue ( true ) ;
1305+ vi . mocked ( EmailAuthProvider . credentialWithLink ) . mockReturnValue ( emailLinkCredential ) ;
1306+ const mockBehavior = vi . fn ( ) . mockResolvedValue ( mockResult ) ;
1307+ vi . mocked ( getBehavior ) . mockReturnValue ( mockBehavior ) ;
1308+
1309+ const result = await completeEmailLinkSignIn ( mockUI , currentUrl ) ;
1310+
1311+ expect ( _isSignInWithEmailLink ) . toHaveBeenCalledWith ( mockUI . auth , currentUrl ) ;
1312+ expect ( hasBehavior ) . toHaveBeenCalledWith ( mockUI , "autoUpgradeAnonymousCredential" ) ;
1313+ expect ( EmailAuthProvider . credentialWithLink ) . toHaveBeenCalledWith ( email , currentUrl ) ;
1314+ expect ( getBehavior ) . toHaveBeenCalledWith ( mockUI , "autoUpgradeAnonymousCredential" ) ;
1315+ expect ( mockBehavior ) . toHaveBeenCalledWith ( mockUI , emailLinkCredential ) ;
1316+ expect ( result ) . toBe ( mockResult ) ;
1317+ expect ( vi . mocked ( mockUI . setState ) . mock . calls ) . toEqual ( [ [ "pending" ] , [ "idle" ] ] ) ;
1318+ expect ( window . localStorage . getItem ( "emailForSignIn" ) ) . toBeNull ( ) ;
1319+ } ) ;
1320+
1321+ it ( "should fall back to signInWithEmailLink when autoUpgradeAnonymousCredential behavior returns undefined" , async ( ) => {
1322+ const mockUI = createMockUI ( ) ;
1323+ const currentUrl = "https://example.com/auth?oobCode=abc123" ;
1324+ const email = "test@example.com" ;
1325+ const emailLinkCredential = { providerId : "emailLink" } as any ;
1326+ const mockResult = { providerId : "emailLink" } as UserCredential ;
1327+
1328+ vi . mocked ( _isSignInWithEmailLink ) . mockReturnValue ( true ) ;
1329+ window . localStorage . setItem ( "emailForSignIn" , email ) ;
1330+ vi . mocked ( hasBehavior ) . mockReturnValue ( true ) ;
1331+ vi . mocked ( EmailAuthProvider . credentialWithLink ) . mockReturnValue ( emailLinkCredential ) ;
1332+ const mockBehavior = vi . fn ( ) . mockResolvedValue ( undefined ) ;
1333+ vi . mocked ( getBehavior ) . mockReturnValue ( mockBehavior ) ;
1334+ vi . mocked ( _signInWithCredential ) . mockResolvedValue ( mockResult ) ;
1335+
1336+ const result = await completeEmailLinkSignIn ( mockUI , currentUrl ) ;
1337+
1338+ expect ( _isSignInWithEmailLink ) . toHaveBeenCalledWith ( mockUI . auth , currentUrl ) ;
1339+ expect ( hasBehavior ) . toHaveBeenCalledWith ( mockUI , "autoUpgradeAnonymousCredential" ) ;
1340+ expect ( EmailAuthProvider . credentialWithLink ) . toHaveBeenCalledWith ( email , currentUrl ) ;
1341+ expect ( getBehavior ) . toHaveBeenCalledWith ( mockUI , "autoUpgradeAnonymousCredential" ) ;
1342+ expect ( mockBehavior ) . toHaveBeenCalledWith ( mockUI , emailLinkCredential ) ;
1343+ expect ( _signInWithCredential ) . toHaveBeenCalledWith ( mockUI . auth , emailLinkCredential ) ;
1344+ expect ( result ) . toBe ( mockResult ) ;
1345+ expect ( vi . mocked ( mockUI . setState ) . mock . calls ) . toEqual ( [ [ "pending" ] , [ "pending" ] , [ "idle" ] , [ "idle" ] ] ) ;
1346+ expect ( window . localStorage . getItem ( "emailForSignIn" ) ) . toBeNull ( ) ;
1347+ } ) ;
1348+
1349+ it ( "should call handleFirebaseError if an error is thrown" , async ( ) => {
1350+ const mockUI = createMockUI ( ) ;
1351+ const currentUrl = "https://example.com/auth?oobCode=abc123" ;
1352+ const email = "test@example.com" ;
1353+ const error = new FirebaseError ( "auth/invalid-action-code" , "Invalid action code" ) ;
1354+ const emailLinkCredential = { providerId : "emailLink" } as any ;
1355+
1356+ vi . mocked ( _isSignInWithEmailLink ) . mockReturnValue ( true ) ;
1357+ window . localStorage . setItem ( "emailForSignIn" , email ) ;
1358+ vi . mocked ( hasBehavior ) . mockReturnValue ( false ) ;
1359+ vi . mocked ( EmailAuthProvider . credentialWithLink ) . mockReturnValue ( emailLinkCredential ) ;
1360+ vi . mocked ( _signInWithCredential ) . mockRejectedValue ( error ) ;
1361+
1362+ const result = await completeEmailLinkSignIn ( mockUI , currentUrl ) ;
1363+
1364+ expect ( handleFirebaseError ) . toHaveBeenCalledWith ( mockUI , error ) ;
1365+ expect ( vi . mocked ( mockUI . setState ) . mock . calls ) . toEqual ( [ [ "pending" ] , [ "pending" ] , [ "idle" ] , [ "idle" ] ] ) ;
1366+ expect ( window . localStorage . getItem ( "emailForSignIn" ) ) . toBeNull ( ) ;
1367+ expect ( result ) . toBeUndefined ( ) ;
1368+ } ) ;
1369+
1370+ it ( "should call handleFirebaseError if autoUpgradeAnonymousCredential behavior throws error" , async ( ) => {
1371+ const mockUI = createMockUI ( ) ;
1372+ const currentUrl = "https://example.com/auth?oobCode=abc123" ;
1373+ const email = "test@example.com" ;
1374+ const emailLinkCredential = { providerId : "emailLink" } as any ;
1375+ const error = new Error ( "Behavior error" ) ;
1376+
1377+ vi . mocked ( _isSignInWithEmailLink ) . mockReturnValue ( true ) ;
1378+ window . localStorage . setItem ( "emailForSignIn" , email ) ;
1379+ vi . mocked ( hasBehavior ) . mockReturnValue ( true ) ;
1380+ vi . mocked ( EmailAuthProvider . credentialWithLink ) . mockReturnValue ( emailLinkCredential ) ;
1381+ const mockBehavior = vi . fn ( ) . mockRejectedValue ( error ) ;
1382+ vi . mocked ( getBehavior ) . mockReturnValue ( mockBehavior ) ;
1383+
1384+ const result = await completeEmailLinkSignIn ( mockUI , currentUrl ) ;
1385+
1386+ expect ( handleFirebaseError ) . toHaveBeenCalledWith ( mockUI , error ) ;
1387+ expect ( vi . mocked ( mockUI . setState ) . mock . calls ) . toEqual ( [ [ "pending" ] , [ "idle" ] ] ) ;
1388+ expect ( window . localStorage . getItem ( "emailForSignIn" ) ) . toBeNull ( ) ;
1389+ expect ( result ) . toBeUndefined ( ) ;
1390+ } ) ;
1391+
1392+ it ( "should clear email from localStorage even when error occurs" , async ( ) => {
1393+ const mockUI = createMockUI ( ) ;
1394+ const currentUrl = "https://example.com/auth?oobCode=abc123" ;
1395+ const email = "test@example.com" ;
1396+ const error = new FirebaseError ( "auth/invalid-action-code" , "Invalid action code" ) ;
1397+ const emailLinkCredential = { providerId : "emailLink" } as any ;
1398+
1399+ vi . mocked ( _isSignInWithEmailLink ) . mockReturnValue ( true ) ;
1400+ window . localStorage . setItem ( "emailForSignIn" , email ) ;
1401+ vi . mocked ( hasBehavior ) . mockReturnValue ( false ) ;
1402+ vi . mocked ( EmailAuthProvider . credentialWithLink ) . mockReturnValue ( emailLinkCredential ) ;
1403+ vi . mocked ( _signInWithCredential ) . mockRejectedValue ( error ) ;
1404+
1405+ await completeEmailLinkSignIn ( mockUI , currentUrl ) ;
1406+
1407+ expect ( window . localStorage . getItem ( "emailForSignIn" ) ) . toBeNull ( ) ;
1408+ } ) ;
1409+
1410+ it ( "should clear email from localStorage even when URL is not an email link" , async ( ) => {
1411+ const mockUI = createMockUI ( ) ;
1412+ const currentUrl = "https://example.com/not-email-link" ;
1413+ const email = "test@example.com" ;
1414+
1415+ vi . mocked ( _isSignInWithEmailLink ) . mockReturnValue ( false ) ;
1416+ window . localStorage . setItem ( "emailForSignIn" , email ) ;
1417+
1418+ await completeEmailLinkSignIn ( mockUI , currentUrl ) ;
1419+
1420+ expect ( window . localStorage . getItem ( "emailForSignIn" ) ) . toBeNull ( ) ;
1421+ } ) ;
1422+
1423+ it ( "should not clear email from localStorage when no email is stored" , async ( ) => {
1424+ const mockUI = createMockUI ( ) ;
1425+ const currentUrl = "https://example.com/auth?oobCode=abc123" ;
1426+
1427+ vi . mocked ( _isSignInWithEmailLink ) . mockReturnValue ( true ) ;
1428+
1429+ await completeEmailLinkSignIn ( mockUI , currentUrl ) ;
1430+
1431+ expect ( window . localStorage . getItem ( "emailForSignIn" ) ) . toBeNull ( ) ;
1432+ } ) ;
1433+ } ) ;
0 commit comments