Skip to content

Commit 96f49e0

Browse files
fix(test): improve adapter test suite (#11904)
* chore(test): improve adapter test suite * update kysely * tweak invite check * more tweaks * mention adapter tests in top section and fix link
1 parent 538dd77 commit 96f49e0

File tree

4 files changed

+36
-13
lines changed

4 files changed

+36
-13
lines changed

docs/pages/guides/creating-a-database-adapter.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Auth.js adapters are very flexible, and you can implement only the methods you n
88

99
An Auth.js adapter is a function that receives an ORM/database client and returns an object with methods (based on the [`Adapter` interface](/reference/core/adapters#adapter)) that interact with the database. The same database Adapter will be compatible with any Auth.js library.
1010

11+
Optionally, you can run our [Adapter tests](https://github.com/nextauthjs/next-auth/blob/main/packages/utils/adapter.ts) on your adapter to ensure it is compliant with the Auth.js.
12+
1113
## User management
1214

1315
Auth.js differentiates between users and accounts. A user can have multiple accounts. An account is created for each provider type the user signs in with for the first time. For example, if a user signs in with Google and then with Facebook, they will have two accounts, one for each provider. The first provider the user signs in with will also be used to create the user object. See the [`profile()` provider method](/reference/core/providers#profile).
@@ -146,7 +148,7 @@ See also: [Verification Token](/concepts/database-models#verification-token) mod
146148
If you created an adapter and want us to distribute it as an official package, please make sure it meets the following requirements. Check out this [existing adapter](https://github.com/nextauthjs/next-auth/tree/main/packages/adapter-prisma) to learn about the package structure, required files, test setup, config, etc.
147149

148150
1. The Adapter _must_ implement all methods of the [`Adapter` interface](/reference/core/adapters#adapter)
149-
1. [Adapter tests](https://github.com/nextauthjs/next-auth/tree/main/packages/utils/adapter/index.ts) _must_ be included and _must_ pass. Docker is favored over services, to make CI resilient to network errors and to reduce the number of GitHub Action Secrets (which also lets us run these tests in fork PRs)
151+
1. [Adapter tests](https://github.com/nextauthjs/next-auth/blob/main/packages/utils/adapter.ts) _must_ be included and _must_ pass. Docker is favored over services, to make CI resilient to network errors and to reduce the number of GitHub Action Secrets (which also lets us run these tests in fork PRs)
150152
1. The Adapter _must_ follow these coding styles
151153

152154
- Written in TypeScript

packages/adapter-kysely/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ export function KyselyAdapter(db: Kysely<Database>): Adapter {
186186
.selectFrom("VerificationToken")
187187
.selectAll()
188188
.where("token", "=", token)
189+
.where("identifier", "=", identifier)
189190
.executeTakeFirst()
190191
.then(async (res) => {
191192
await query.executeTakeFirst()

packages/core/src/lib/actions/callback/index.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -198,13 +198,13 @@ export async function callback(
198198

199199
return { redirect: callbackUrl, cookies }
200200
} else if (provider.type === "email") {
201-
const token = query?.token as string | undefined
202-
const identifier = query?.email as string | undefined
201+
const paramToken = query?.token as string | undefined
202+
const paramIdentifier = query?.email as string | undefined
203203

204-
if (!token || !identifier) {
204+
if (!paramToken || !paramIdentifier) {
205205
const e = new TypeError(
206206
"Missing token or email. The sign-in URL was manually opened without token/identifier or the link was not sent correctly in the email.",
207-
{ cause: { hasToken: !!token, hasEmail: !!identifier } }
207+
{ cause: { hasToken: !!paramToken, hasEmail: !!paramIdentifier } }
208208
)
209209
e.name = "Configuration"
210210
throw e
@@ -213,16 +213,18 @@ export async function callback(
213213
const secret = provider.secret ?? options.secret
214214
// @ts-expect-error -- Verified in `assertConfig`.
215215
const invite = await adapter.useVerificationToken({
216-
identifier,
217-
token: await createHash(`${token}${secret}`),
216+
identifier: paramIdentifier, // TODO: Drop this requirement for lookup
217+
token: await createHash(`${paramToken}${secret}`),
218218
})
219219

220220
const hasInvite = !!invite
221-
const expired = invite ? invite.expires.valueOf() < Date.now() : undefined
222-
const invalidInvite = !hasInvite || expired
221+
const expired = hasInvite && invite.expires.valueOf() < Date.now()
222+
const invalidInvite =
223+
!hasInvite || expired || invite.identifier !== paramIdentifier
223224
if (invalidInvite) throw new Verification({ hasInvite, expired })
224225

225-
const user = (await adapter!.getUserByEmail(identifier)) ?? {
226+
const { identifier } = invite
227+
const user = (await adapter!.getUserByEmail(paramIdentifier)) ?? {
226228
id: crypto.randomUUID(),
227229
email: identifier,
228230
emailVerified: null,

packages/utils/adapter.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { afterAll, beforeAll, expect, test } from "vitest"
22

3-
import type { Adapter } from "@auth/core/adapters"
3+
import type { Adapter, VerificationToken } from "@auth/core/adapters"
44
import { createHash, randomInt, randomUUID } from "crypto"
55

66
export interface TestOptions {
@@ -287,7 +287,7 @@ export async function runBasicTests(options: TestOptions) {
287287
identifier,
288288
expires:
289289
options.fixtures?.verificationTokenExpires ?? FIFTEEN_MINUTES_FROM_NOW,
290-
}
290+
} satisfies VerificationToken
291291
await adapter.createVerificationToken?.(verificationToken)
292292

293293
const dbVerificationToken1 = await adapter.useVerificationToken?.({
@@ -301,8 +301,26 @@ export async function runBasicTests(options: TestOptions) {
301301

302302
expect(dbVerificationToken1).toEqual(verificationToken)
303303

304-
const dbVerificationToken2 = await adapter.useVerificationToken?.({
304+
const dbVerificationTokenSecondTry = await adapter.useVerificationToken?.({
305+
identifier,
306+
token: hashedToken,
307+
})
308+
309+
expect(dbVerificationTokenSecondTry).toBeNull()
310+
311+
// Should only return if the identifier matches
312+
313+
const verificationToken2 = {
314+
token: hashedToken,
305315
identifier,
316+
expires:
317+
options.fixtures?.verificationTokenExpires ?? FIFTEEN_MINUTES_FROM_NOW,
318+
} satisfies VerificationToken
319+
320+
await adapter.createVerificationToken?.(verificationToken2)
321+
322+
const dbVerificationToken2 = await adapter.useVerificationToken?.({
323+
identifier: "[email protected]",
306324
token: hashedToken,
307325
})
308326

0 commit comments

Comments
 (0)