diff --git a/packages-exp/auth-exp/src/api/authentication/email_link.test.ts b/packages-exp/auth-exp/src/api/authentication/email_link.test.ts index e9bd649b170..abd8706f43d 100644 --- a/packages-exp/auth-exp/src/api/authentication/email_link.test.ts +++ b/packages-exp/auth-exp/src/api/authentication/email_link.test.ts @@ -26,16 +26,14 @@ import { testAuth } from '../../../test/mock_auth'; import * as mockFetch from '../../../test/mock_fetch'; import { Auth } from '../../model/auth'; import { ServerError } from '../errors'; -import { signInWithEmailLink } from './email_link'; +import { + signInWithEmailLink, + signInWithEmailLinkForLinking +} from './email_link'; use(chaiAsPromised); -describe('api/authentication/signInWithEmailLink', () => { - const request = { - email: 'foo@bar.com', - oobCode: 'my-code' - }; - +describe('api/authentication/email_link', () => { let auth: Auth; beforeEach(async () => { @@ -45,44 +43,102 @@ describe('api/authentication/signInWithEmailLink', () => { afterEach(mockFetch.tearDown); - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, { - displayName: 'my-name', - email: 'test@foo.com' + context('signInWithEmailLink', () => { + const request = { + email: 'foo@bar.com', + oobCode: 'my-code' + }; + + it('should POST to the correct endpoint', async () => { + const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, { + displayName: 'my-name', + email: 'test@foo.com' + }); + + const response = await signInWithEmailLink(auth, request); + expect(response.displayName).to.eq('my-name'); + expect(response.email).to.eq('test@foo.com'); + expect(mock.calls[0].request).to.eql(request); + expect(mock.calls[0].method).to.eq('POST'); + expect(mock.calls[0].headers).to.eql({ + 'Content-Type': 'application/json', + 'X-Client-Version': 'testSDK/0.0.0' + }); }); - const response = await signInWithEmailLink(auth, request); - expect(response.displayName).to.eq('my-name'); - expect(response.email).to.eq('test@foo.com'); - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers).to.eql({ - 'Content-Type': 'application/json', - 'X-Client-Version': 'testSDK/0.0.0' + it('should handle errors', async () => { + const mock = mockEndpoint( + Endpoint.SIGN_IN_WITH_EMAIL_LINK, + { + error: { + code: 400, + message: ServerError.INVALID_EMAIL, + errors: [ + { + message: ServerError.INVALID_EMAIL + } + ] + } + }, + 400 + ); + + await expect(signInWithEmailLink(auth, request)).to.be.rejectedWith( + FirebaseError, + 'Firebase: The email address is badly formatted. (auth/invalid-email).' + ); + expect(mock.calls[0].request).to.eql(request); }); }); - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.SIGN_IN_WITH_EMAIL_LINK, - { - error: { - code: 400, - message: ServerError.INVALID_EMAIL, - errors: [ - { - message: ServerError.INVALID_EMAIL - } - ] - } - }, - 400 - ); - - await expect(signInWithEmailLink(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The email address is badly formatted. (auth/invalid-email).' - ); - expect(mock.calls[0].request).to.eql(request); + context('signInWithEmailLinkForLinking', () => { + const request = { + email: 'foo@bar.com', + oobCode: 'my-code', + idToken: 'id-token-2' + }; + + it('should POST to the correct endpoint', async () => { + const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, { + displayName: 'my-name', + email: 'test@foo.com' + }); + + const response = await signInWithEmailLinkForLinking(auth, request); + expect(response.displayName).to.eq('my-name'); + expect(response.email).to.eq('test@foo.com'); + expect(mock.calls[0].request).to.eql(request); + expect(mock.calls[0].method).to.eq('POST'); + expect(mock.calls[0].headers).to.eql({ + 'Content-Type': 'application/json', + 'X-Client-Version': 'testSDK/0.0.0' + }); + }); + + it('should handle errors', async () => { + const mock = mockEndpoint( + Endpoint.SIGN_IN_WITH_EMAIL_LINK, + { + error: { + code: 400, + message: ServerError.INVALID_EMAIL, + errors: [ + { + message: ServerError.INVALID_EMAIL + } + ] + } + }, + 400 + ); + + await expect( + signInWithEmailLinkForLinking(auth, request) + ).to.be.rejectedWith( + FirebaseError, + 'Firebase: The email address is badly formatted. (auth/invalid-email).' + ); + expect(mock.calls[0].request).to.eql(request); + }); }); }); diff --git a/packages-exp/auth-exp/src/api/authentication/email_link.ts b/packages-exp/auth-exp/src/api/authentication/email_link.ts index 10a0efdf445..a14c81cb0bd 100644 --- a/packages-exp/auth-exp/src/api/authentication/email_link.ts +++ b/packages-exp/auth-exp/src/api/authentication/email_link.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Endpoint, HttpMethod, _performSignInRequest } from '..'; +import { _performSignInRequest, Endpoint, HttpMethod } from '../'; import { Auth } from '../../model/auth'; import { IdTokenResponse } from '../../model/id_token'; @@ -38,3 +38,18 @@ export async function signInWithEmailLink( SignInWithEmailLinkResponse >(auth, HttpMethod.POST, Endpoint.SIGN_IN_WITH_EMAIL_LINK, request); } + +export interface SignInWithEmailLinkForLinkingRequest + extends SignInWithEmailLinkRequest { + idToken: string; +} + +export async function signInWithEmailLinkForLinking( + auth: Auth, + request: SignInWithEmailLinkForLinkingRequest +): Promise { + return _performSignInRequest< + SignInWithEmailLinkForLinkingRequest, + SignInWithEmailLinkResponse + >(auth, HttpMethod.POST, Endpoint.SIGN_IN_WITH_EMAIL_LINK, request); +} diff --git a/packages-exp/auth-exp/src/core/credentials/email.test.ts b/packages-exp/auth-exp/src/core/credentials/email.test.ts index 323532d2fce..4cc4727d132 100644 --- a/packages-exp/auth-exp/src/core/credentials/email.test.ts +++ b/packages-exp/auth-exp/src/core/credentials/email.test.ts @@ -75,7 +75,7 @@ describe('core/credentials/email', () => { }); describe('#_getIdTokenResponse', () => { - it('call sign in with password', async () => { + it('calls sign in with password', async () => { const idTokenResponse = await credential._getIdTokenResponse(auth); expect(idTokenResponse.idToken).to.eq('id-token'); expect(idTokenResponse.refreshToken).to.eq('refresh-token'); @@ -90,18 +90,43 @@ describe('core/credentials/email', () => { }); describe('#_linkToIdToken', () => { - it('throws', async () => { - await expect( - credential._linkToIdToken(auth, 'id-token') - ).to.be.rejectedWith(Error); + it('calls update email password', async () => { + apiMock = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, { + idToken: 'id-token', + refreshToken: 'refresh-token', + expiresIn: '1234', + localId: serverUser.localId! + }); + + const idTokenResponse = await credential._linkToIdToken( + auth, + 'id-token-2' + ); + expect(idTokenResponse.idToken).to.eq('id-token'); + expect(idTokenResponse.refreshToken).to.eq('refresh-token'); + expect(idTokenResponse.expiresIn).to.eq('1234'); + expect(idTokenResponse.localId).to.eq(serverUser.localId); + expect(apiMock.calls[0].request).to.eql({ + idToken: 'id-token-2', + returnSecureToken: true, + email: 'some-email', + password: 'some-password' + }); }); }); describe('#_getReauthenticationResolver', () => { - it('throws', () => { - expect(() => credential._getReauthenticationResolver(auth)).to.throw( - Error - ); + it('calls sign in with password', async () => { + const idTokenResponse = await credential._getIdTokenResponse(auth); + expect(idTokenResponse.idToken).to.eq('id-token'); + expect(idTokenResponse.refreshToken).to.eq('refresh-token'); + expect(idTokenResponse.expiresIn).to.eq('1234'); + expect(idTokenResponse.localId).to.eq(serverUser.localId); + expect(apiMock.calls[0].request).to.eql({ + returnSecureToken: true, + email: 'some-email', + password: 'some-password' + }); }); }); }); @@ -153,18 +178,34 @@ describe('core/credentials/email', () => { }); describe('#_linkToIdToken', () => { - it('throws', async () => { - await expect( - credential._linkToIdToken(auth, 'id-token') - ).to.be.rejectedWith(Error); + it('calls sign in with the new token', async () => { + const idTokenResponse = await credential._linkToIdToken( + auth, + 'id-token-2' + ); + expect(idTokenResponse.idToken).to.eq('id-token'); + expect(idTokenResponse.refreshToken).to.eq('refresh-token'); + expect(idTokenResponse.expiresIn).to.eq('1234'); + expect(idTokenResponse.localId).to.eq(serverUser.localId); + expect(apiMock.calls[0].request).to.eql({ + idToken: 'id-token-2', + email: 'some-email', + oobCode: 'oob-code' + }); }); }); describe('#_matchIdTokenWithUid', () => { - it('throws', () => { - expect(() => credential._getReauthenticationResolver(auth)).to.throw( - Error - ); + it('call sign in with email link', async () => { + const idTokenResponse = await credential._getIdTokenResponse(auth); + expect(idTokenResponse.idToken).to.eq('id-token'); + expect(idTokenResponse.refreshToken).to.eq('refresh-token'); + expect(idTokenResponse.expiresIn).to.eq('1234'); + expect(idTokenResponse.localId).to.eq(serverUser.localId); + expect(apiMock.calls[0].request).to.eql({ + email: 'some-email', + oobCode: 'oob-code' + }); }); }); }); diff --git a/packages-exp/auth-exp/src/core/credentials/email.ts b/packages-exp/auth-exp/src/core/credentials/email.ts index 741e460b68d..a4dee25afa3 100644 --- a/packages-exp/auth-exp/src/core/credentials/email.ts +++ b/packages-exp/auth-exp/src/core/credentials/email.ts @@ -17,13 +17,17 @@ import * as externs from '@firebase/auth-types-exp'; +import { updateEmailPassword } from '../../api/account_management/email_and_password'; import { signInWithPassword } from '../../api/authentication/email_and_password'; -import { signInWithEmailLink } from '../../api/authentication/email_link'; +import { + signInWithEmailLink, + signInWithEmailLinkForLinking +} from '../../api/authentication/email_link'; import { Auth } from '../../model/auth'; import { IdTokenResponse } from '../../model/id_token'; -import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors'; +import { AuthErrorCode } from '../errors'; import { EmailAuthProvider } from '../providers/email'; -import { debugFail } from '../util/assert'; +import { debugFail, fail } from '../util/assert'; import { AuthCredential } from './'; export class EmailAuthCredential implements AuthCredential { @@ -57,17 +61,31 @@ export class EmailAuthCredential implements AuthCredential { oobCode: this.password }); default: - throw AUTH_ERROR_FACTORY.create(AuthErrorCode.INTERNAL_ERROR, { - appName: auth.name - }); + fail(auth.name, AuthErrorCode.INTERNAL_ERROR); } } - async _linkToIdToken(_auth: Auth, _idToken: string): Promise { - debugFail('Method not implemented.'); + async _linkToIdToken(auth: Auth, idToken: string): Promise { + switch (this.signInMethod) { + case EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD: + return updateEmailPassword(auth, { + idToken, + returnSecureToken: true, + email: this.email, + password: this.password + }); + case EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD: + return signInWithEmailLinkForLinking(auth, { + idToken, + email: this.email, + oobCode: this.password + }); + default: + fail(auth.name, AuthErrorCode.INTERNAL_ERROR); + } } - _getReauthenticationResolver(_auth: Auth): Promise { - debugFail('Method not implemented.'); + _getReauthenticationResolver(auth: Auth): Promise { + return this._getIdTokenResponse(auth); } }