@@ -48,6 +48,12 @@ vi.mock("firebase/auth", () => ({
4848 PhoneAuthProvider : Object . assign ( vi . fn ( ) , {
4949 credential : vi . fn ( ) ,
5050 } ) ,
51+ OAuthProvider : {
52+ credentialFromJSON : vi . fn ( ) ,
53+ } ,
54+ SAMLAuthProvider : {
55+ credentialFromJSON : vi . fn ( ) ,
56+ } ,
5157 linkWithCredential : vi . fn ( ) ,
5258} ) ) ;
5359
@@ -64,6 +70,9 @@ import {
6470 signInWithCredential as _signInWithCredential ,
6571 EmailAuthProvider ,
6672 PhoneAuthProvider ,
73+ OAuthProvider ,
74+ SAMLAuthProvider ,
75+ linkWithCredential as _linkWithCredential ,
6776 createUserWithEmailAndPassword as _createUserWithEmailAndPassword ,
6877 sendPasswordResetEmail as _sendPasswordResetEmail ,
6978 sendSignInLinkToEmail as _sendSignInLinkToEmail ,
@@ -81,8 +90,6 @@ import { FirebaseError } from "firebase/app";
8190
8291import { createMockUI } from "~/tests/utils" ;
8392
84- // TODO(ehesp): Add tests for handlePendingCredential.
85-
8693describe ( "signInWithEmailAndPassword" , ( ) => {
8794 beforeEach ( ( ) => {
8895 vi . clearAllMocks ( ) ;
@@ -1007,6 +1014,131 @@ describe("signInAnonymously", () => {
10071014 } ) ;
10081015} ) ;
10091016
1017+ describe ( "handlePendingCredential" , ( ) => {
1018+ beforeEach ( ( ) => {
1019+ vi . clearAllMocks ( ) ;
1020+ window . sessionStorage . clear ( ) ;
1021+ } ) ;
1022+
1023+ afterEach ( ( ) => {
1024+ window . sessionStorage . clear ( ) ;
1025+ } ) ;
1026+
1027+ const mockUserCredential = {
1028+ user : { uid : "anonymous-uid" } ,
1029+ providerId : "anonymous" ,
1030+ operationType : "signIn" ,
1031+ } as UserCredential ;
1032+
1033+ it ( "should return the user unchanged when there is no pending credential" , async ( ) => {
1034+ const mockUI = createMockUI ( ) ;
1035+ vi . mocked ( _signInAnonymously ) . mockResolvedValue ( mockUserCredential ) ;
1036+
1037+ const result = await signInAnonymously ( mockUI ) ;
1038+
1039+ expect ( OAuthProvider . credentialFromJSON ) . not . toHaveBeenCalled ( ) ;
1040+ expect ( _linkWithCredential ) . not . toHaveBeenCalled ( ) ;
1041+ expect ( result ) . toBe ( mockUserCredential ) ;
1042+ } ) ;
1043+
1044+ it ( "should rehydrate an OAuth credential via OAuthProvider.credentialFromJSON and link it" , async ( ) => {
1045+ const mockUI = createMockUI ( ) ;
1046+ const storedJSON = { providerId : "google.com" , signInMethod : "google.com" , idToken : "fake-id-token" } ;
1047+ window . sessionStorage . setItem ( "pendingCred" , JSON . stringify ( storedJSON ) ) ;
1048+
1049+ const rehydratedCredential = { providerId : "google.com" } as any ;
1050+ const linkedUserCredential = { ...mockUserCredential , providerId : "google.com" } as UserCredential ;
1051+
1052+ vi . mocked ( _signInAnonymously ) . mockResolvedValue ( mockUserCredential ) ;
1053+ vi . mocked ( OAuthProvider . credentialFromJSON ) . mockReturnValue ( rehydratedCredential ) ;
1054+ vi . mocked ( _linkWithCredential ) . mockResolvedValue ( linkedUserCredential ) ;
1055+
1056+ const result = await signInAnonymously ( mockUI ) ;
1057+
1058+ expect ( OAuthProvider . credentialFromJSON ) . toHaveBeenCalledWith ( storedJSON ) ;
1059+ expect ( _linkWithCredential ) . toHaveBeenCalledWith ( mockUserCredential . user , rehydratedCredential ) ;
1060+ expect ( result ) . toBe ( linkedUserCredential ) ;
1061+ expect ( window . sessionStorage . getItem ( "pendingCred" ) ) . toBeNull ( ) ;
1062+ } ) ;
1063+
1064+ it ( "should fall back to SAMLAuthProvider when OAuthProvider cannot rehydrate the credential" , async ( ) => {
1065+ const mockUI = createMockUI ( ) ;
1066+ const storedJSON = { providerId : "saml.my-provider" , signInMethod : "saml.my-provider" , pendingToken : "abc" } ;
1067+ window . sessionStorage . setItem ( "pendingCred" , JSON . stringify ( storedJSON ) ) ;
1068+
1069+ const rehydratedCredential = { providerId : "saml.my-provider" } as any ;
1070+ const linkedUserCredential = { ...mockUserCredential , providerId : "saml.my-provider" } as UserCredential ;
1071+
1072+ vi . mocked ( _signInAnonymously ) . mockResolvedValue ( mockUserCredential ) ;
1073+ vi . mocked ( OAuthProvider . credentialFromJSON ) . mockImplementation ( ( ) => {
1074+ throw new Error ( "not an OAuth credential" ) ;
1075+ } ) ;
1076+ vi . mocked ( SAMLAuthProvider . credentialFromJSON ) . mockReturnValue ( rehydratedCredential ) ;
1077+ vi . mocked ( _linkWithCredential ) . mockResolvedValue ( linkedUserCredential ) ;
1078+
1079+ const result = await signInAnonymously ( mockUI ) ;
1080+
1081+ expect ( SAMLAuthProvider . credentialFromJSON ) . toHaveBeenCalledWith ( storedJSON ) ;
1082+ expect ( _linkWithCredential ) . toHaveBeenCalledWith ( mockUserCredential . user , rehydratedCredential ) ;
1083+ expect ( result ) . toBe ( linkedUserCredential ) ;
1084+ expect ( window . sessionStorage . getItem ( "pendingCred" ) ) . toBeNull ( ) ;
1085+ } ) ;
1086+
1087+ it ( "should return the original user and clear storage when the credential cannot be rehydrated" , async ( ) => {
1088+ const mockUI = createMockUI ( ) ;
1089+ const storedJSON = { providerId : "unknown" , signInMethod : "unknown" } ;
1090+ window . sessionStorage . setItem ( "pendingCred" , JSON . stringify ( storedJSON ) ) ;
1091+
1092+ vi . mocked ( _signInAnonymously ) . mockResolvedValue ( mockUserCredential ) ;
1093+ vi . mocked ( OAuthProvider . credentialFromJSON ) . mockImplementation ( ( ) => {
1094+ throw new Error ( "not an OAuth credential" ) ;
1095+ } ) ;
1096+ vi . mocked ( SAMLAuthProvider . credentialFromJSON ) . mockImplementation ( ( ) => {
1097+ throw new Error ( "not a SAML credential" ) ;
1098+ } ) ;
1099+
1100+ const result = await signInAnonymously ( mockUI ) ;
1101+
1102+ expect ( _linkWithCredential ) . not . toHaveBeenCalled ( ) ;
1103+ expect ( result ) . toBe ( mockUserCredential ) ;
1104+ expect ( window . sessionStorage . getItem ( "pendingCred" ) ) . toBeNull ( ) ;
1105+ } ) ;
1106+
1107+ it ( "should return the original user and clear storage when the stored credential is invalid JSON" , async ( ) => {
1108+ const mockUI = createMockUI ( ) ;
1109+ window . sessionStorage . setItem ( "pendingCred" , "{invalid-json" ) ;
1110+
1111+ vi . mocked ( _signInAnonymously ) . mockResolvedValue ( mockUserCredential ) ;
1112+
1113+ const result = await signInAnonymously ( mockUI ) ;
1114+
1115+ expect ( OAuthProvider . credentialFromJSON ) . not . toHaveBeenCalled ( ) ;
1116+ expect ( _linkWithCredential ) . not . toHaveBeenCalled ( ) ;
1117+ expect ( result ) . toBe ( mockUserCredential ) ;
1118+ expect ( window . sessionStorage . getItem ( "pendingCred" ) ) . toBeNull ( ) ;
1119+ } ) ;
1120+
1121+ it ( "should return the original user and clear storage when linkWithCredential fails" , async ( ) => {
1122+ const mockUI = createMockUI ( ) ;
1123+ const storedJSON = { providerId : "google.com" , signInMethod : "google.com" , idToken : "fake-id-token" } ;
1124+ window . sessionStorage . setItem ( "pendingCred" , JSON . stringify ( storedJSON ) ) ;
1125+
1126+ const rehydratedCredential = { providerId : "google.com" } as any ;
1127+
1128+ vi . mocked ( _signInAnonymously ) . mockResolvedValue ( mockUserCredential ) ;
1129+ vi . mocked ( OAuthProvider . credentialFromJSON ) . mockReturnValue ( rehydratedCredential ) ;
1130+ vi . mocked ( _linkWithCredential ) . mockRejectedValue (
1131+ new FirebaseError ( "auth/credential-already-in-use" , "Credential already in use" )
1132+ ) ;
1133+
1134+ const result = await signInAnonymously ( mockUI ) ;
1135+
1136+ expect ( _linkWithCredential ) . toHaveBeenCalledWith ( mockUserCredential . user , rehydratedCredential ) ;
1137+ expect ( result ) . toBe ( mockUserCredential ) ;
1138+ expect ( window . sessionStorage . getItem ( "pendingCred" ) ) . toBeNull ( ) ;
1139+ } ) ;
1140+ } ) ;
1141+
10101142describe ( "signInWithCustomToken" , ( ) => {
10111143 beforeEach ( ( ) => {
10121144 vi . clearAllMocks ( ) ;
0 commit comments