Skip to content

Commit

Permalink
feat(auth): implement user linking (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavohenke committed Sep 27, 2020
1 parent ecb462a commit 4bac607
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 11 deletions.
22 changes: 22 additions & 0 deletions auth/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,28 @@ describe("#createUserWithEmailAndPassword()", () => {
});
});

describe("#fetchSignInMethodsForEmail()", () => {
it("returns list of sign in methods", async () => {
const auth = new MockAuth(app);
await auth.createUserWithEmailAndPassword("foo@bar.com", "baz");

// sign in with a different method, to make sure things don't mix up
const provider = new firebase.auth.FacebookAuthProvider();
auth.mockSocialSignIn(provider).respondWithUser("John", "john@doe.com");
auth.signInWithPopup(provider);

return expect(auth.fetchSignInMethodsForEmail("foo@bar.com")).resolves.toEqual([
firebase.auth.EmailAuthProvider.PROVIDER_ID,
]);
});

it("returns no sign in methods if email doesn't exist", async () => {
const auth = new MockAuth(app);

return expect(auth.fetchSignInMethodsForEmail("foo@bar.com")).resolves.toEqual([]);
});
});

describe("#onAuthStateChange()", () => {
it("invokes listener right away with current status", async () => {
const auth = new MockAuth(app);
Expand Down
8 changes: 3 additions & 5 deletions auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,10 @@ export class MockAuth implements firebase.auth.Auth {
return this.signIn(user, { isNewUser: true });
}

fetchProvidersForEmail(email: string): Promise<any> {
throw new Error("Method not implemented.");
}

fetchSignInMethodsForEmail(email: string): Promise<string[]> {
throw new Error("Method not implemented.");
const user = this.store.findByEmail(email);
const providers = user ? user.providerData : [];
return Promise.resolve(providers.map((info) => info.providerId));
}

getRedirectResult(): Promise<any> {
Expand Down
59 changes: 58 additions & 1 deletion auth/user.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,51 @@
import * as firebase from "firebase";
import createMockInstance from "jest-create-mock-instance";
import { User } from "./user";
import { User, UserInfo } from "./user";
import { UserStore } from "./user-store";

let store: jest.Mocked<UserStore>;
beforeEach(() => {
store = createMockInstance(UserStore);
});

describe("#linkWithPopup()", () => {
const provider = new firebase.auth.GoogleAuthProvider();
const response = { displayName: "foo", email: "foo@baz.com" };
beforeEach(() => {
store.consumeSocialMock.mockResolvedValue(response);
});

it("doesn't link if provider is already linked", async () => {
const info = new UserInfo("1", provider.providerId, {});
const user = new User({ uid: "1", providerData: [info] }, store);
return expect(user.linkWithPopup(provider)).rejects.toThrowError(
"auth/provider-already-linked"
);
});

it("links provider if not linked", async () => {
const user = new User({ uid: "1", email: "foo@bar.com" }, store);
const credential = await user.linkWithPopup(provider);
expect(credential.operationType).toBe("link");
expect(credential.user).toBe(user);
expect(credential.additionalUserInfo).toEqual({
providerId: provider.providerId,
profile: null,
isNewUser: false,
username: user.email,
});
});

it("updates providerData", async () => {
const user = new User({ uid: "1" }, store);
await user.linkWithPopup(provider);
expect(user.providerData).toContainEqual(new UserInfo(user.uid, provider.providerId, response));
expect(store.update).toHaveBeenCalledWith(user.uid, {
providerData: user.providerData,
});
});
});

describe("#updateEmail()", () => {
it("doesn't update anything if email didn't change", async () => {
const user = new User({ uid: "1", email: "foo@bar.com" }, store);
Expand Down Expand Up @@ -55,3 +94,21 @@ describe("#updateProfile()", () => {
});
});
});

describe("#unlink()", () => {
const providerId = firebase.auth.GoogleAuthProvider.PROVIDER_ID;

it("unlinks and updates providerData", async () => {
const user = new User({ uid: "1", providerData: [new UserInfo("1", providerId, {})] }, store);
await user.unlink(providerId);
expect(user.providerData).toHaveLength(0);
expect(store.update).toHaveBeenCalledWith(user.uid, {
providerData: user.providerData,
});
});

it("throws if provider isn't linked", () => {
const user = new User({ uid: "1" }, store);
return expect(user.unlink(providerId)).rejects.toThrowError("auth/no-such-provider");
});
});
34 changes: 29 additions & 5 deletions auth/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as firebase from "firebase";
import { UserCredential } from "./user-credential";
import { UserStore } from "./user-store";

export interface UserSchema {
Expand Down Expand Up @@ -99,11 +100,25 @@ export class User implements firebase.User, UserSchema {
throw new Error("Method not implemented.");
}

linkWithPopup(provider: firebase.auth.AuthProvider): Promise<any> {
throw new Error("Method not implemented.");
private async linkWithSocial(provider: firebase.auth.AuthProvider) {
if (this.providerData.some((info) => info.providerId === provider.providerId)) {
throw new Error("auth/provider-already-linked");
}

const data = await this.store.consumeSocialMock(provider);
this.providerData = [...this.providerData, new UserInfo(this.uid, provider.providerId, data)];
this.store.update(this.uid, {
providerData: this.providerData,
});
this.providerId = provider.providerId;
return new UserCredential(this, "link", { isNewUser: false });
}

linkWithPopup(provider: firebase.auth.AuthProvider): Promise<UserCredential> {
return this.linkWithSocial(provider);
}

linkWithRedirect(provider: firebase.auth.AuthProvider): Promise<any> {
async linkWithRedirect(provider: firebase.auth.AuthProvider): Promise<void> {
throw new Error("Method not implemented.");
}

Expand Down Expand Up @@ -147,8 +162,17 @@ export class User implements firebase.User, UserSchema {
return { ...self };
}

unlink(providerId: string): Promise<any> {
throw new Error("Method not implemented.");
async unlink(providerId: string): Promise<User> {
const index = this.providerData.findIndex((info) => info.providerId === providerId);
if (index === -1) {
throw new Error("auth/no-such-provider");
}

this.providerData.splice(index, 1);
this.store.update(this.uid, {
providerData: this.providerData,
});
return this;
}

updateEmail(newEmail: string): Promise<void> {
Expand Down

0 comments on commit 4bac607

Please sign in to comment.