diff --git a/lib/Auth.ts b/lib/Auth.ts index d0a3e5f7..813f972d 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -27,11 +27,67 @@ export const AuthenticationOptions: AuthOptions = { clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET, allowDangerousEmailAccountLinking: true, + profile: async (profile) => { + logger.debug("Google profile:", profile); + + const existingUser = await ( + await cachedDb + ).findObject(CollectionId.Users, { email: profile.email }); + if (existingUser) { + existingUser.id = profile.sub; + return existingUser; + } + + const user = new User( + profile.name, + profile.email, + profile.picture, + false, + await GenerateSlug(await cachedDb, CollectionId.Users, profile.name), + [], + [], + ); + + user.id = profile.sub; + + return user; + }, }), SlackProvider({ clientId: process.env.NEXT_PUBLIC_SLACK_CLIENT_ID as string, clientSecret: process.env.SLACK_CLIENT_SECRET as string, allowDangerousEmailAccountLinking: true, + profile: async (profile) => { + logger.debug("Slack profile:", profile); + + const existing = await ( + await cachedDb + ).findObject(CollectionId.Users, { email: profile.email }); + + if (existing) { + existing.slackId = profile.sub; + existing.id = profile.sub; + console.log("Found existing user:", existing); + return existing; + } + + const user = new User( + profile.name, + profile.email, + profile.picture, + false, + await GenerateSlug(await cachedDb, CollectionId.Users, profile.name), + [], + [], + profile.sub, + 10, + 1, + ); + + user.id = profile.sub; + + return user; + }, }), Email({ server: { diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index ee3a0a7c..b2ca5758 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -1,4 +1,4 @@ -import { _id, format, MongoDBAdapter } from "@next-auth/mongodb-adapter"; +import { format, MongoDBAdapter } from "@next-auth/mongodb-adapter"; import { Adapter, AdapterAccount, @@ -13,50 +13,8 @@ import { GenerateSlug } from "./Utils"; import { ObjectId } from "bson"; import Logger from "./client/Logger"; import { RollbarInterface } from "./client/RollbarUtils"; -import { Profile } from "next-auth"; - -function formatTo(obj: T | undefined) { - if (!obj) return undefined; - - const formatted = format.to(obj); - - if ("_id" in obj) { - formatted._id = obj._id as ObjectId; - } - - if ("id" in obj) { - (formatted as any).id = obj.id; - } - - if ("userId" in obj) { - (formatted as any).userId = new ObjectId((obj.userId as any).toString()); - } - - return formatted; -} - -function formatFrom(obj: T | undefined) { - console.log("formatFrom", obj); - if (!obj) return undefined; - - const formatted = format.from(obj); - if ("_id" in obj) { - (formatted as any)._id = obj._id as ObjectId; - } - if ("id" in obj) { - (formatted as any).id = obj.id; - } - - if ("userId" in obj) { - (formatted as any).userId = obj.userId; - } - - return formatted; -} /** - * Should match the MongoDB adapter as closely as possible - * (https://github.com/nextauthjs/next-auth/blob/main/packages/adapter-mongodb/src/index.ts). * @tested_by tests/lib/DbInterfaceAuthAdapter.test.ts */ export default function DbInterfaceAuthAdapter( @@ -69,98 +27,264 @@ export default function DbInterfaceAuthAdapter( new Logger(["AUTH"], false); const adapter: Adapter = { - /** - * @param data returns from the profile callback of the auth provider - */ createUser: async (data: Record) => { - const profile = formatTo(data)!; + const db = await dbPromise; + + const adapterUser = format.to(data); + adapterUser._id = data["_id"] as any; + + logger.debug("Creating user:", adapterUser.name); + + // Check if user already exists + const existingUser = await db.findObject(CollectionId.Users, { + email: adapterUser.email, + }); + + if (existingUser) { + // If user exists, return existing user + logger.warn("User already exists:", existingUser.name); + rollbar.warn("User already exists when creating user", { + existingUser, + data, + }); + return format.from(existingUser); + } + + logger.debug("Creating user:", adapterUser); const user = new User( - profile.name ?? profile.email ?? "Unknown User", - profile.email, - profile.image, + adapterUser.name ?? "Unknown", + adapterUser.email, + adapterUser.image ?? process.env.DEFAULT_IMAGE, false, await GenerateSlug( - await dbPromise, + db, CollectionId.Users, - profile.name ?? profile.email ?? "Unknown User", + adapterUser.name ?? "Unknown", ), [], [], - profile.sub, - 10, + adapterUser.id, + 0, 1, ); - user._id = new ObjectId() as any; + user._id = new ObjectId(adapterUser._id) as any; - // We need the 'id' field to avoid the error "Profile id is missing in OAuth profile response" - user.id = user._id!.toString(); - - await (await dbPromise).addObject(CollectionId.Users, user); - return formatFrom(user); + const dbUser = await db.addObject(CollectionId.Users, user); + logger.info("Created user:", dbUser._id!.toString()); + return format.from(dbUser); }, getUser: async (id: string) => { - const user = await ( - await dbPromise - ).findObjectById(CollectionId.Users, _id(id)); - if (!user) return null; - return formatFrom(user) as AdapterUser; + const db = await dbPromise; + + if (id.length !== 24) return null; + + logger.debug("Getting user:", id); + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(id), + ); + + if (!user) { + logger.warn("User not found:", id); + return null; + } + user.id = user._id!.toString()!; + return format.from(user); }, getUserByEmail: async (email: string) => { - const user = await ( - await dbPromise - ).findObject(CollectionId.Users, { - email, - }); - if (!user) return null; - return formatFrom(user) as AdapterUser; + const db = await dbPromise; + + logger.debug("Getting user by email:", email); + + const user = await db.findObject(CollectionId.Users, { email }); + + if (!user) { + logger.warn("User not found by email:", email); + return null; + } + + user.id = user._id!.toString()!; + return format.from(user); }, getUserByAccount: async ( providerAccountId: Pick, ) => { const db = await dbPromise; - const account = await db.findObject( - CollectionId.Accounts, - providerAccountId, - ); - if (!account) return null; + + logger.debug("Getting user by account:", providerAccountId); + + const account = await db.findObject(CollectionId.Accounts, { + providerAccountId: providerAccountId.providerAccountId, + }); + + if (!account) { + logger.warn( + "Account not found by providerAccountId:", + providerAccountId.providerAccountId, + ); + return null; + } + const user = await db.findObjectById( CollectionId.Users, - new ObjectId(account.userId), + account.userId as any as ObjectId, + ); + + if (!user) { + logger.warn("User not found:", account.userId); + return null; + } + + logger.debug( + "Found user by account: Account", + providerAccountId.providerAccountId, + "=> User", + user._id, + user.name, ); - if (!user) return null; - return formatFrom(user) as AdapterUser; + + user.id = user._id!.toString()!; + return format.from(user); }, updateUser: async ( data: Partial & Pick, ) => { - const { _id, ...user } = formatTo(data as any)!; const db = await dbPromise; + const { _id, ...user } = format.to(data); + + if (!_id) { + logger.error("User ID not found when updating user:", user); + rollbar.error("User ID not found when updating user", { + user, + }); + + return format.from(user); + } + + logger.debug("Updating user:", _id); - const result = await db.findObjectAndUpdate( + const existing = await db.findObjectById( CollectionId.Users, - _id, - user as unknown as Partial, + new ObjectId(_id), ); - return formatFrom(result!) as AdapterUser; + if (!existing) { + logger.error("User not found:", _id); + rollbar.error("User not found when updating user", { + _id, + user, + }); + return format.from(user); + } + + user.id = existing._id!.toString()!; + + await db.updateObjectById( + CollectionId.Users, + new ObjectId(_id), + user as Partial, + ); + + return format.from({ ...existing, ...user, _id: _id }); }, deleteUser: async (id: string) => { - const userId = _id(id); const db = await dbPromise; - await Promise.all([ - db.deleteObjects(CollectionId.Accounts, { userId: userId as any }), - db.deleteObjects(CollectionId.Sessions, { userId: userId as any }), - db.deleteObjectById(CollectionId.Users, userId), - ]); + + logger.log("Deleting user:", id); + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(id), + ); + if (!user) { + logger.error("User not found:", id); + rollbar.error("User not found when deleting user", { + id, + }); + return null; + } + + const account = await db.findObject(CollectionId.Accounts, { + userId: user._id, + }); + + const sessions = await db.findObjects(CollectionId.Sessions, { + userId: user._id, + }); + + const promises: Promise[] = [ + db.deleteObjectById(CollectionId.Users, user._id as any as ObjectId), + ]; + + if (account) { + promises.push( + db.deleteObjectById(CollectionId.Accounts, new ObjectId(account._id)), + ); + } + + if (sessions.length) { + promises.push( + Promise.all( + sessions.map((session) => + db.deleteObjectById( + CollectionId.Sessions, + new ObjectId(session._id), + ), + ), + ), + ); + } + + await Promise.all(promises); + + return format.from(user); }, /** * Creates an account */ linkAccount: async (data: Record) => { - const account = formatTo(data as any)!; - await (await dbPromise).addObject(CollectionId.Accounts, account); + const db = await dbPromise; + + const account = format.to(data); + account.userId = data["userId"] as any; // userId gets overwritten for some reason + + logger.debug( + "Linking account: providerAccountId", + account.providerAccountId, + "userId:", + account.userId, + ); + + const existingAccount = await db.findObject(CollectionId.Accounts, { + providerAccountId: account.providerAccountId, + }); + + if (existingAccount) { + logger.warn( + "Account already exists:", + existingAccount.providerAccountId, + ); + rollbar.warn("Account already exists when linking account", { + account, + }); + + let formattedAccount: AdapterAccount; + + // Sometimes gives an error about not finding toHexString. + try { + formattedAccount = format.from(existingAccount); + } catch (e) { + account.userId = new ObjectId(account.userId) as any; + formattedAccount = format.from(account); + } + return formattedAccount; + } + + await db.addObject(CollectionId.Accounts, account); + return account; }, /** @@ -169,79 +293,211 @@ export default function DbInterfaceAuthAdapter( unlinkAccount: async ( providerAccountId: Pick, ) => { - const account = await ( - await dbPromise - ).findObjectAndDelete(CollectionId.Accounts, providerAccountId); - return formatFrom(account!) ?? null; + const db = await dbPromise; + + logger.debug("Unlinking account:", providerAccountId.providerAccountId); + + const account = await db.findObject(CollectionId.Accounts, { + providerAccountId: providerAccountId.providerAccountId, + }); + + if (!account) { + logger.warn( + "Account not found by providerAccountId:", + providerAccountId.providerAccountId, + ); + rollbar.warn("Account not found when unlinking account", { + providerAccountId, + }); + return null; + } + + await db.deleteObjectById( + CollectionId.Accounts, + new ObjectId(account._id), + ); + + return format.from(account); }, getSessionAndUser: async (sessionToken: string) => { const db = await dbPromise; + const session = await db.findObject(CollectionId.Sessions, { sessionToken, }); - if (!session) return null; + + if (!session) { + // Weirdly, this is ok. + logger.warn("Session not found:", sessionToken); + return null; + } const user = await db.findObjectById( CollectionId.Users, new ObjectId(session.userId), ); - if (!user) return null; + + if (!user) { + logger.warn("User not found:", session.userId); + return null; + } + + logger.debug( + "Got session and user. Session Token:", + sessionToken, + "User:", + user._id, + user.name, + ); return { - user: formatFrom(user) as any as AdapterUser, - session: formatFrom(session) as any as AdapterSession, + session: format.from(session), + user: { + ...format.from(user), + _id: user._id, + }, }; }, createSession: async (data: Record) => { - const session = formatTo(data as any)!; - await (await dbPromise).addObject(CollectionId.Sessions, session as any); - return formatFrom(session)!; + const db = await dbPromise; + + const session = format.to(data); + session.userId = data["userId"] as any; // userId gets overwritten for some reason + + if (!session.userId) { + logger.error("User ID not found in session:", session); + rollbar.error("User ID not found in session when creating session", { + session, + }); + + const dbSession = await db.addObject( + CollectionId.Sessions, + session as unknown as Session, + ); + + return format.from(dbSession); + } + + logger.debug( + "Creating session:", + session._id, + "with user", + session.userId, + ); + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(session.userId), + ); + + if (!user) { + logger.warn("User not found:", session.userId); + rollbar.warn("User not found", { + session, + }); + } else session.userId = user._id as any; + + const dbSession = await db.addObject( + CollectionId.Sessions, + session as unknown as Session, + ); + + return format.from(dbSession); }, updateSession: async ( data: Partial & Pick, ) => { - const { _id, ...session } = formatTo(data as any)!; - const db = await dbPromise; + const { _id, ...session } = format.to(data); + session.userId = data["userId"] as any; // userId gets overwritten for some reason + + logger.debug("Updating session:", session.sessionToken); + const existing = await db.findObject(CollectionId.Sessions, { sessionToken: session.sessionToken, }); + if (!existing) { + logger.error("Session not found:", session.sessionToken); + rollbar.error("Session not found when updating session", { + session, + }); + return null; + } + + if (session.userId) { + session.userId = new ObjectId(session.userId) as any; + } + await db.updateObjectById( CollectionId.Sessions, - existing?._id as any, - session as any, - ); - return formatFrom({ - _id, - ...existing, - ...session, - }) as any as AdapterSession; + new ObjectId(existing._id), + session as unknown as Partial, + ); + + return format.from({ ...existing, ...data }); }, deleteSession: async (sessionToken: string) => { - const session = await ( - await dbPromise - ).findObjectAndDelete(CollectionId.Sessions, { + const db = await dbPromise; + + logger.debug("Deleting session:", sessionToken); + + const session = await db.findObject(CollectionId.Sessions, { sessionToken, }); - return formatFrom(session!) as any as AdapterSession; + + if (!session) { + logger.warn("Session not found:", sessionToken); + rollbar.warn("Session not found when deleting session", { + sessionToken, + }); + return null; + } + + await db.deleteObjectById( + CollectionId.Sessions, + new ObjectId(session._id), + ); + + return format.from(session); }, createVerificationToken: async (token: VerificationToken) => { - await ( - await dbPromise - ).addObject(CollectionId.VerificationTokens, format.to(token) as any); + const db = await dbPromise; + + logger.debug("Creating verification token:", token.identifier); + + await db.addObject( + CollectionId.VerificationTokens, + format.to(token) as VerificationToken, + ); return token; }, useVerificationToken: async (token: { identifier: string; token: string; }) => { - const verificationToken = await ( - await dbPromise - ).findObjectAndDelete(CollectionId.VerificationTokens, token); - if (!verificationToken) return null; - const { _id, ...rest } = verificationToken; - return rest; + const db = await dbPromise; + + logger.info("Using verification token:", token.identifier); + + const existing = await db.findObject(CollectionId.VerificationTokens, { + token: token.token, + }); + + if (!existing) { + logger.warn("Verification token not found:", token.token); + rollbar.warn("Verification token not found when using token", { + token, + }); + return null; + } + + await db.deleteObjectById( + CollectionId.VerificationTokens, + new ObjectId(existing._id), + ); + + return format.from(existing); }, }; diff --git a/lib/Types.ts b/lib/Types.ts index a25a741e..8127ae02 100644 --- a/lib/Types.ts +++ b/lib/Types.ts @@ -54,7 +54,6 @@ export class User implements NextAuthUser { onboardingComplete: boolean = false; resendContactId: string | undefined = undefined; lastSignInDateTime: Date | undefined = undefined; - emailVerified: Date | undefined = undefined; constructor( name: string | undefined, diff --git a/lib/Utils.ts b/lib/Utils.ts index e794fa8b..09503f38 100644 --- a/lib/Utils.ts +++ b/lib/Utils.ts @@ -113,7 +113,6 @@ export async function populateMissingUserFields( level: user.level ?? 0, resendContactId: user.resendContactId ?? undefined, lastSignInDateTime: user.lastSignInDateTime ?? undefined, - emailVerified: user.emailVerified ?? undefined, }; if (user._id) (filled as User)._id = user._id as unknown as string; diff --git a/lib/client/dbinterfaces/CachedDbInterface.ts b/lib/client/dbinterfaces/CachedDbInterface.ts index 0903e9f5..2ae214e5 100644 --- a/lib/client/dbinterfaces/CachedDbInterface.ts +++ b/lib/client/dbinterfaces/CachedDbInterface.ts @@ -74,33 +74,4 @@ export default class CachedDbInterface >(collection: TId, slug: string): Promise { return findObjectBySlugLookUp(this, collection, slug); } - - addOrUpdateObject( - collection: TId, - object: CollectionIdToType, - ): Promise> { - return super.addOrUpdateObject(collection, object); - } - - findObjectAndUpdate( - collection: TId, - id: ObjectId, - newValues: Partial>, - ): Promise | undefined> { - return super.findObjectAndUpdate(collection, id, newValues); - } - - deleteObjects( - collection: TId, - query: object, - ): Promise { - return super.deleteObjects(collection, query); - } - - findObjectAndDelete( - collection: TId, - query: object, - ): Promise | undefined> { - return super.findObjectAndDelete(collection, query); - } } diff --git a/lib/client/dbinterfaces/DbInterface.ts b/lib/client/dbinterfaces/DbInterface.ts index 29bc3172..698a8a63 100644 --- a/lib/client/dbinterfaces/DbInterface.ts +++ b/lib/client/dbinterfaces/DbInterface.ts @@ -4,6 +4,7 @@ import CollectionId, { SluggedCollectionId, } from "../CollectionId"; import { default as BaseDbInterface } from "mongo-anywhere/DbInterface"; +import slugToId from "@/lib/slugToId"; export type WithStringOrObjectIdId = Omit & { _id?: ObjectId | string; @@ -62,25 +63,4 @@ export default interface DbInterface collection: CollectionId, query: object, ): Promise; - - addOrUpdateObject( - collection: TId, - object: CollectionIdToType, - ): Promise>; - - findObjectAndUpdate( - collection: TId, - id: ObjectId, - newValues: Partial>, - ): Promise | undefined>; - - deleteObjects( - collection: TId, - query: object, - ): Promise; - - findObjectAndDelete( - collection: TId, - query: object, - ): Promise | undefined>; } diff --git a/lib/client/dbinterfaces/InMemoryDbInterface.ts b/lib/client/dbinterfaces/InMemoryDbInterface.ts index 306a7204..779f931f 100644 --- a/lib/client/dbinterfaces/InMemoryDbInterface.ts +++ b/lib/client/dbinterfaces/InMemoryDbInterface.ts @@ -65,33 +65,4 @@ export default class InMemoryDbInterface >(collection: TId, slug: string): Promise { return findObjectBySlugLookUp(this, collection, slug); } - - addOrUpdateObject( - collection: TId, - object: CollectionIdToType, - ): Promise> { - return super.addOrUpdateObject(collection, object); - } - - findObjectAndUpdate( - collection: TId, - id: ObjectId, - newValues: Partial>, - ): Promise | undefined> { - return super.findObjectAndUpdate(collection, id, newValues); - } - - deleteObjects( - collection: TId, - query: object, - ): Promise { - return super.deleteObjects(collection, query); - } - - findObjectAndDelete( - collection: TId, - query: object, - ): Promise | undefined> { - return super.findObjectAndDelete(collection, query); - } } diff --git a/lib/client/dbinterfaces/LocalStorageDbInterface.ts b/lib/client/dbinterfaces/LocalStorageDbInterface.ts index a5f962a2..b7975a2d 100644 --- a/lib/client/dbinterfaces/LocalStorageDbInterface.ts +++ b/lib/client/dbinterfaces/LocalStorageDbInterface.ts @@ -65,33 +65,4 @@ export default class LocalStorageDbInterface >(collection: TId, slug: string): Promise { return findObjectBySlugLookUp(this, collection, slug); } - - addOrUpdateObject( - collection: TId, - object: CollectionIdToType, - ): Promise> { - return super.addOrUpdateObject(collection, object); - } - - findObjectAndUpdate( - collection: TId, - id: ObjectId, - newValues: Partial>, - ): Promise | undefined> { - return super.findObjectAndUpdate(collection, id, newValues); - } - - deleteObjects( - collection: TId, - query: object, - ): Promise { - return super.deleteObjects(collection, query); - } - - findObjectAndDelete( - collection: TId, - query: object, - ): Promise | undefined> { - return super.findObjectAndDelete(collection, query); - } } diff --git a/package-lock.json b/package-lock.json index 78c99b46..e1d3d706 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "jose": "^6.0.10", "levenary": "^1.1.1", "minimongo": "^7.0.0", - "mongo-anywhere": "^1.2.0", + "mongo-anywhere": "^1.1.15", "mongodb": "^5.0.0", "next": "^15.2.4", "next-auth": "^4.24.11", @@ -8234,9 +8234,9 @@ } }, "node_modules/mongo-anywhere": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mongo-anywhere/-/mongo-anywhere-1.2.0.tgz", - "integrity": "sha512-0D7eooYwPY1Z4ZhTmPExKGOOfQHSEU8GpmFPqSQKlJx90vHaY0P/ABwiycjRfDRxrhg1XI8JrNdwJoWFvvsuxA==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/mongo-anywhere/-/mongo-anywhere-1.1.15.tgz", + "integrity": "sha512-wN6E/jN0lae5EqAeaAaE5fdUdb+ZchZKib3FWGOOOQUYZvTv2ino9Aii3GK+uIMxNdnm6N//B0bEGEeCNbBy1g==", "dependencies": { "bson": "^5.0.0", "minimongo": "^6.19.0", diff --git a/package.json b/package.json index 0e85e912..93953def 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "jose": "^6.0.10", "levenary": "^1.1.1", "minimongo": "^7.0.0", - "mongo-anywhere": "^1.2.0", + "mongo-anywhere": "^1.1.15", "mongodb": "^5.0.0", "next": "^15.2.4", "next-auth": "^4.24.11", diff --git a/tests/unit/lib/DbInterfaceAuthAdapter.test.ts b/tests/unit/lib/DbInterfaceAuthAdapter.test.ts index c53e8517..d1cf93be 100644 --- a/tests/unit/lib/DbInterfaceAuthAdapter.test.ts +++ b/tests/unit/lib/DbInterfaceAuthAdapter.test.ts @@ -84,6 +84,21 @@ describe(prototype.createUser.name, () => { expect(foundUser?.name).toBeDefined(); expect(foundUser?.image).toBeDefined(); }); + + test("Does not create a new user if one already exists", async () => { + const { db, adapter } = await getDeps(); + + const user = { + email: "test@gmail.com", + }; + + await adapter.createUser(user); + await adapter.createUser(user); + + expect( + await db.countObjects(CollectionId.Users, { email: user.email }), + ).toBe(1); + }); }); describe(prototype.getUser!.name, () => { @@ -192,10 +207,7 @@ describe(prototype.getUserByAccount!.name, () => { await db.addObject(CollectionId.Accounts, account); - const foundUser = await adapter.getUserByAccount!({ - provider: account.provider, - providerAccountId: account.providerAccountId, - }); + const foundUser = await adapter.getUserByAccount!(account); expect(foundUser).toMatchObject(addedUser); }); @@ -210,10 +222,7 @@ describe(prototype.getUserByAccount!.name, () => { userId: new ObjectId() as any, }; - const user = await adapter.getUserByAccount!({ - provider: account.provider, - providerAccountId: account.providerAccountId, - }); + const user = await adapter.getUserByAccount!(account); expect(user).toBeNull(); }); @@ -230,10 +239,7 @@ describe(prototype.getUserByAccount!.name, () => { await db.addObject(CollectionId.Accounts, account); - const user = await adapter.getUserByAccount!({ - provider: account.provider, - providerAccountId: account.providerAccountId, - }); + const user = await adapter.getUserByAccount!(account); expect(user).toBeNull(); }); @@ -263,12 +269,35 @@ describe(prototype.updateUser!.name, () => { email: user.email, }); - // Not sure how id behaves, don't use it - foundUser!.id = foundUser!._id!.toString(); - expect(foundUser).toMatchObject(updatedUser); }); + test("Errors if not given an _id", async () => { + const { adapter, rollbar } = await getDeps(); + + const user = { + name: "Test User", + email: "test@gmail.com", + }; + + await adapter.updateUser!(user as any); + + expect(rollbar.error).toHaveBeenCalled(); + }); + + test("Errors if the user doesn't exist", async () => { + const { adapter, rollbar } = await getDeps(); + + const user = { + name: "Test User", + email: "test@gmail.com", + }; + + await adapter.updateUser!(user as any); + + expect(rollbar.error).toHaveBeenCalled(); + }); + test("Returns the updated user without their _id", async () => { const { db, adapter } = await getDeps(); @@ -292,6 +321,21 @@ describe(prototype.updateUser!.name, () => { expect(returnedUser).toMatchObject(expectedUser); }); + + test("Errors if no _id is provided", async () => { + const { adapter, db, rollbar } = await getDeps(); + + const user = { + name: "Test User", + email: "test@gmail.com", + }; + + await db.addObject(CollectionId.Users, user as any); + + await adapter.updateUser!({ name: "Test User 2" } as any); + + expect(rollbar.error).toHaveBeenCalled(); + }); }); describe(prototype.deleteUser!.name, () => { @@ -315,6 +359,15 @@ describe(prototype.deleteUser!.name, () => { expect(foundUser).toBeUndefined(); }); + test("Errors but returns null if the user doesn't exist", async () => { + const { adapter, rollbar } = await getDeps(); + + const user = await adapter.deleteUser!(new ObjectId().toString()); + + expect(user).toBeNull(); + expect(rollbar.error).toHaveBeenCalled(); + }); + test("Deletes the user's account", async () => { const { db, adapter } = await getDeps(); @@ -407,6 +460,24 @@ describe(prototype.linkAccount!.name, () => { expect(foundAccount).toEqual(account); }); + test("Warns if the account already exists", async () => { + const { adapter, db, rollbar } = await getDeps(); + + const account: Account = { + _id: new ObjectId(), + provider: "test", + type: "oauth", + providerAccountId: "1234567890", + userId: new ObjectId() as any, + }; + + await db.addObject(CollectionId.Accounts, account); + + await adapter.linkAccount!(account); + + expect(rollbar.warn).toHaveBeenCalled(); + }); + test("Does not create another account if one already exists", async () => { const { db, adapter } = await getDeps(); @@ -473,6 +544,21 @@ describe(prototype.unlinkAccount!.name, () => { expect(foundAccount).toBeUndefined(); }); + test("Warns if the account doesn't exist", async () => { + const { adapter, rollbar } = await getDeps(); + + const account: Account = { + provider: "test", + type: "oauth", + providerAccountId: "1234567890", + userId: new ObjectId() as any, + }; + + await adapter.unlinkAccount!(account); + + expect(rollbar.warn).toHaveBeenCalled(); + }); + test("Returns null if the account doesn't exist", async () => { const { adapter } = await getDeps(); @@ -603,6 +689,33 @@ describe(prototype.createSession!.name, () => { expect(foundSession?.userId).toEqual(session.userId); }); + + test("Errors if not given a userId", async () => { + const { adapter, rollbar } = await getDeps(); + + const session: AdapterSession = { + sessionToken: "1234567890", + userId: undefined as any, + expires: new Date(), + }; + + await adapter.createSession!(session); + expect(rollbar.error).toHaveBeenCalled(); + }); + + test("Warns if the user doesn't exist", async () => { + const { adapter, rollbar } = await getDeps(); + + const session: AdapterSession = { + sessionToken: "1234567890", + userId: new ObjectId() as any, + expires: new Date(), + }; + + await adapter.createSession!(session); + + expect(rollbar.warn).toHaveBeenCalled(); + }); }); describe(prototype.updateSession!.name, () => { @@ -614,17 +727,21 @@ describe(prototype.updateSession!.name, () => { name: "Test User", email: "test@gmail.com", }; - const { _id: userId } = await db.addObject(CollectionId.Users, user as any); + const { _id: userId } = await db.addObject(CollectionId.Users, user as any); const session: AdapterSession = { sessionToken: "1234567890", userId: userId as any, expires: new Date(), }; - await db.addObject(CollectionId.Sessions, session as any); + + const { _id: sessionId } = await db.addObject( + CollectionId.Sessions, + session as any, + ); const updatedSession = { - sessionToken: session.sessionToken, + sessionToken: "1234567890", userId: new ObjectId() as any, }; @@ -636,6 +753,34 @@ describe(prototype.updateSession!.name, () => { expect(foundSession?.userId).toEqual(updatedSession.userId); }); + + test("Errors if not given a sessionToken", async () => { + const { adapter, rollbar } = await getDeps(); + + const session: AdapterSession = { + sessionToken: undefined as any, + userId: new ObjectId() as any, + expires: new Date(), + }; + + await adapter.updateSession!(session); + + expect(rollbar.error).toHaveBeenCalled(); + }); + + test("Errors if the session doesn't exist", async () => { + const { adapter, rollbar } = await getDeps(); + + const session: AdapterSession = { + sessionToken: "1234567890", + userId: new ObjectId() as any, + expires: new Date(), + }; + + await adapter.updateSession!(session); + + expect(rollbar.error).toHaveBeenCalled(); + }); }); describe(prototype.deleteSession!.name, () => { @@ -656,7 +801,10 @@ describe(prototype.deleteSession!.name, () => { expires: new Date(), }; - await db.addObject(CollectionId.Sessions, session as any); + const { _id: sessionId } = await db.addObject( + CollectionId.Sessions, + session as any, + ); await adapter.deleteSession!(session.sessionToken); @@ -667,6 +815,14 @@ describe(prototype.deleteSession!.name, () => { expect(foundSession).toBeUndefined(); }); + test("Warns if the session doesn't exist", async () => { + const { adapter, rollbar } = await getDeps(); + + await adapter.deleteSession!("1234567890"); + + expect(rollbar.warn).toHaveBeenCalled(); + }); + test("Does not delete the user", async () => { const { db, adapter } = await getDeps(); @@ -726,39 +882,32 @@ describe(prototype.createVerificationToken!.name, () => { describe(prototype.useVerificationToken!.name, () => { test("Returns token", async () => { - const { adapter, db } = await getDeps(); - const testToken = { identifier: "hi", expires: new Date(), token: "hello", }; - await db.addObject(CollectionId.VerificationTokens, testToken); - const foundToken = await adapter.useVerificationToken!({ - identifier: testToken.identifier, - token: testToken.token, - }); + const { adapter, db } = await getDeps(); + + await db.addObject(CollectionId.VerificationTokens, testToken); + const foundToken = await adapter.useVerificationToken!(testToken); expect(foundToken?.identifier).toBe(testToken.identifier); expect(foundToken?.token).toBe(testToken.token); }); test("Token is removed from database", async () => { - const { adapter, db } = await getDeps(); - const testToken = { identifier: "hi", expires: new Date(), token: "hello", }; - await db.addObject(CollectionId.VerificationTokens, testToken); - await adapter.useVerificationToken!({ - identifier: testToken.identifier, - token: testToken.token, - }); + const { adapter, db } = await getDeps(); + await db.addObject(CollectionId.VerificationTokens, testToken); + await adapter.useVerificationToken!(testToken); const foundToken = await db.findObject(CollectionId.VerificationTokens, { identifier: testToken.identifier, token: testToken.token, @@ -766,4 +915,17 @@ describe(prototype.useVerificationToken!.name, () => { expect(foundToken).toBeUndefined(); }); + + test("Warns if token doesn't exist", async () => { + const testToken = { + identifier: "hi", + expires: new Date(), + token: "hello", + }; + const { adapter, rollbar } = await getDeps(); + + await adapter.useVerificationToken!(testToken); + + expect(rollbar.warn).toHaveBeenCalled(); + }); });