Files
archived-next-auth/packages/adapter-test/index.ts
Nico Domino dc3ad8c408 chore: move adapters to monorepo (#3805)
* feat: move adapters repo to new packages dir

* fix: rm docusaurus build dir

* fix: update .gitignore

* fix: reorganise package directories

* remove package lock files

* fix: folder rename

* remove package lock file

* fix: jest config paths

* update yarn.lock

* ignore dynamodb local bin

* fix: gitignore

* fix: update adapter-test

* change adapter-test package json

* rename prisma adapter package name

* fix paths

* update gitignore

* run tests with one concurrency

* fix: merge conflicts

* gitignore dist folders

* fix: add jest.config.js to tsconfig ignore

* fix: yarn.lock

* fix: ignore pouch in turbo commands

* ignore jest file

* fix: test turbo test cmd

* fix: turbo test cmd

* test: disable mongodb-adapter temporarily

* ignore all dev.db files

* simplify gitignore

* remove unused dependency

* have tsconfig in its own package

* remove unnecessary .gitignore files

* move jest config to preset

* add ts expect error comment

* chore: update .gitignore

* remove babelrc

* don't depend on build for testing in turbo

* fix: cleanup testing npm scripts

* fix: remove jest-config roots

* fix: add fauna jest preset

* fix: rm dev.db from prisma mirgation

* fix prisma

* remove nohoist

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-02-04 22:40:32 +01:00

328 lines
9.4 KiB
TypeScript

import type { Adapter } from "next-auth/adapters"
import { createHash, randomUUID } from "crypto"
export interface TestOptions {
adapter: Adapter
db: {
/** Generates UUID v4 by default. Use it to override how the test suite should generate IDs, like user id. */
id?: () => string
/**
* Manually disconnect database after all tests have been run,
* if your adapter doesn't do it automatically
*/
disconnect?: () => Promise<any>
/**
* Manually establishes a db connection before all tests,
* if your db doesn't do this automatically
*/
connect?: () => Promise<any>
/** A simple query function that returns a session directly from the db. */
session: (sessionToken: string) => any
/** A simple query function that returns a user directly from the db. */
user: (id: string) => any
/** A simple query function that returns an account directly from the db. */
account: (providerAccountId: {
provider: string
providerAccountId: string
}) => any
/**
* A simple query function that returns an verification token directly from the db,
* based on the user identifier and the verification token (hashed).
*/
verificationToken: (params: { identifier: string; token: string }) => any
}
}
/**
* A wrapper to run the most basic tests.
* Run this at the top of your test file.
* You can add additional tests below, if you wish.
*/
export function runBasicTests(options: TestOptions) {
const id = options.db.id ?? randomUUID
// Init
beforeAll(async () => {
await options.db.connect?.()
})
const { adapter, db } = options
afterAll(async () => {
// @ts-expect-error This is only used for the TypeORM adapter
await adapter.__disconnect?.()
await options.db.disconnect?.()
})
let user: any = {
email: "fill@murray.com",
image: "https://www.fillmurray.com/460/300",
name: "Fill Murray",
emailVerified: new Date(),
}
if (process.env.CUSTOM_MODEL === "1") {
user.role = "admin"
user.phone = "00000000000"
}
const session: any = {
sessionToken: randomUUID(),
expires: ONE_WEEK_FROM_NOW,
}
const account: any = {
provider: "github",
providerAccountId: randomUUID(),
type: "oauth",
access_token: randomUUID(),
expires_at: ONE_MONTH,
id_token: randomUUID(),
refresh_token: randomUUID(),
token_type: "bearer",
scope: "user",
session_state: randomUUID(),
}
// All adapters must define these methods
test("Required (User, Account, Session) methods exist", () => {
const requiredMethods = [
"createUser",
"getUser",
"getUserByEmail",
"getUserByAccount",
"updateUser",
"linkAccount",
"createSession",
"getSessionAndUser",
"updateSession",
"deleteSession",
]
requiredMethods.forEach((method) => {
expect(adapter).toHaveProperty(method)
})
})
test("createUser", async () => {
const { id } = await adapter.createUser(user)
const dbUser = await db.user(id)
expect(dbUser).toEqual({ ...user, id })
user = dbUser
session.userId = dbUser.id
account.userId = dbUser.id
})
test("getUser", async () => {
expect(await adapter.getUser(id())).toBeNull()
expect(await adapter.getUser(user.id)).toEqual(user)
})
test("getUserByEmail", async () => {
expect(await adapter.getUserByEmail("non-existent-email")).toBeNull()
expect(await adapter.getUserByEmail(user.email)).toEqual(user)
})
test("createSession", async () => {
const { sessionToken } = await adapter.createSession(session)
const dbSession = await db.session(sessionToken)
expect(dbSession).toEqual({ ...session, id: dbSession.id })
session.userId = dbSession.userId
session.id = dbSession.id
})
test("getSessionAndUser", async () => {
let sessionAndUser = await adapter.getSessionAndUser("invalid-token")
expect(sessionAndUser).toBeNull()
sessionAndUser = await adapter.getSessionAndUser(session.sessionToken)
if (!sessionAndUser) {
throw new Error("Session and User was not found, but they should exist")
}
expect(sessionAndUser).toEqual({
user,
session,
})
})
test("updateUser", async () => {
const newName = "Updated Name"
const returnedUser = await adapter.updateUser({
id: user.id,
name: newName,
})
expect(returnedUser.name).toBe(newName)
const dbUser = await db.user(user.id)
expect(dbUser.name).toBe(newName)
user.name = newName
})
test("updateSession", async () => {
let dbSession = await db.session(session.sessionToken)
expect(dbSession.expires.valueOf()).not.toBe(ONE_MONTH_FROM_NOW.valueOf())
await adapter.updateSession({
sessionToken: session.sessionToken,
expires: ONE_MONTH_FROM_NOW,
})
dbSession = await db.session(session.sessionToken)
expect(dbSession.expires.valueOf()).toBe(ONE_MONTH_FROM_NOW.valueOf())
})
test("linkAccount", async () => {
await adapter.linkAccount(account)
const dbAccount = await db.account({
provider: account.provider,
providerAccountId: account.providerAccountId,
})
expect(dbAccount).toEqual({ ...account, id: dbAccount.id })
})
test("getUserByAccount", async () => {
let userByAccount = await adapter.getUserByAccount({
provider: "invalid-provider",
providerAccountId: "invalid-provider-account-id",
})
expect(userByAccount).toBeNull()
userByAccount = await adapter.getUserByAccount({
provider: account.provider,
providerAccountId: account.providerAccountId,
})
expect(userByAccount).toEqual(user)
})
test("deleteSession", async () => {
await adapter.deleteSession(session.sessionToken)
const dbSession = await db.session(session.sessionToken)
expect(dbSession).toBeNull()
})
// These are optional for custom adapters, but we require them for the official adapters
test("Verification Token methods exist", () => {
const requiredMethods = ["createVerificationToken", "useVerificationToken"]
requiredMethods.forEach((method) => {
expect(adapter).toHaveProperty(method)
})
})
test("createVerificationToken", async () => {
const identifier = "info@example.com"
const token = randomUUID()
const hashedToken = hashToken(token)
const verificationToken = {
token: hashedToken,
identifier,
expires: FIFTEEN_MINUTES_FROM_NOW,
}
await adapter.createVerificationToken?.(verificationToken)
const dbVerificationToken = await db.verificationToken({
token: hashedToken,
identifier,
})
expect(dbVerificationToken).toEqual(verificationToken)
})
test("useVerificationToken", async () => {
const identifier = "info@example.com"
const token = randomUUID()
const hashedToken = hashToken(token)
const verificationToken = {
token: hashedToken,
identifier,
expires: FIFTEEN_MINUTES_FROM_NOW,
}
await adapter.createVerificationToken?.(verificationToken)
const dbVerificationToken1 = await adapter.useVerificationToken?.({
identifier,
token: hashedToken,
})
if (!dbVerificationToken1) {
throw new Error("Verification Token was not found, but it should exist")
}
expect(dbVerificationToken1).toEqual(verificationToken)
const dbVerificationToken2 = await adapter.useVerificationToken?.({
identifier,
token: hashedToken,
})
expect(dbVerificationToken2).toBeNull()
})
// Future methods
// These methods are not yet invoked in the core, but built-in adapters must implement them
test("Future methods exist", () => {
const requiredMethods = ["unlinkAccount", "deleteUser"]
requiredMethods.forEach((method) => {
expect(adapter).toHaveProperty(method)
})
})
test("unlinkAccount", async () => {
let dbAccount = await db.account({
provider: account.provider,
providerAccountId: account.providerAccountId,
})
expect(dbAccount).toEqual({ ...account, id: dbAccount.id })
await adapter.unlinkAccount?.({
provider: account.provider,
providerAccountId: account.providerAccountId,
})
dbAccount = await db.account({
provider: account.provider,
providerAccountId: account.providerAccountId,
})
expect(dbAccount).toBeNull()
})
test("deleteUser", async () => {
let dbUser = await db.user(user.id)
expect(dbUser).toEqual(user)
// Re-populate db with session and account
delete session.id
await adapter.createSession(session)
await adapter.linkAccount(account)
await adapter.deleteUser?.(user.id)
dbUser = await db.user(user.id)
// User should not exist after it is deleted
expect(dbUser).toBeNull()
const dbSession = await db.session(session.sessionToken)
// Session should not exist after user is deleted
expect(dbSession).toBeNull()
const dbAccount = await db.account({
provider: account.provider,
providerAccountId: account.providerAccountId,
})
// Account should not exist after user is deleted
expect(dbAccount).toBeNull()
})
}
// UTILS
export function hashToken(token: string) {
return createHash("sha256").update(`${token}anything`).digest("hex")
}
export { randomUUID }
export const ONE_WEEK_FROM_NOW = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7)
export const FIFTEEN_MINUTES_FROM_NOW = new Date(Date.now() + 15 * 60 * 1000)
export const ONE_MONTH = 1000 * 60 * 60 * 24 * 30
export const ONE_MONTH_FROM_NOW = new Date(Date.now() + ONE_MONTH)