@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
557 lines (513 loc) • 21 kB
text/typescript
import type {
AdapterAccount,
AdapterAuthenticator,
AdapterSession,
AdapterUser,
VerificationToken,
} from '@auth/core/adapters';
import { eq } from 'drizzle-orm/expressions';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { getTestDBInstance } from '@/database/core/dbForTest';
import {
nextauthAccounts,
nextauthAuthenticators,
nextauthSessions,
nextauthVerificationTokens,
users,
} from '@/database/schemas';
import { LobeChatDatabase } from '@/database/type';
import { LobeNextAuthDbAdapter } from '@/libs/next-auth/adapter';
let serverDB = await getTestDBInstance();
let nextAuthAdapter = LobeNextAuthDbAdapter(serverDB);
const userId = 'user-db';
const user: AdapterUser = {
id: userId,
name: 'John Doe',
email: 'john.doe@example.com',
emailVerified: new Date(),
image: 'https://example.com/avatar.jpg',
};
const sessionToken = 'session-token';
beforeAll(async () => {
await serverDB.delete(users);
await serverDB.delete(nextauthAccounts);
await serverDB.delete(nextauthAuthenticators);
await serverDB.delete(nextauthSessions);
await serverDB.delete(nextauthVerificationTokens);
});
beforeEach(async () => {
process.env.KEY_VAULTS_SECRET = 'ofQiJCXLF8mYemwfMWLOHoHimlPu91YmLfU7YZ4lreQ=';
// insert a user
// @ts-expect-error: createUser is defined
await nextAuthAdapter.createUser(user);
});
afterEach(async () => {
await serverDB.delete(users);
});
afterAll(async () => {
await serverDB.delete(users);
await serverDB.delete(nextauthAccounts);
await serverDB.delete(nextauthAuthenticators);
await serverDB.delete(nextauthSessions);
await serverDB.delete(nextauthVerificationTokens);
process.env.KEY_VAULTS_SECRET = undefined;
});
/**
* The tests below only test the database operation functionality,
* the mock values might not represent the actual value
*/
describe('LobeNextAuthDbAdapter', () => {
describe('users', () => {
describe('createUser', () => {
it('should create a new user', async () => {
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.createUser).toBeDefined();
const anotherUserId = 'user-db-2';
const anotherUserName = 'John Doe 2';
const anotherEmail = 'john.doe.2@example.com';
// @ts-expect-error: createUser is defined
const createdUser = await nextAuthAdapter.createUser({
...user,
id: anotherUserId,
name: anotherUserName,
email: anotherEmail,
});
expect(createdUser).toBeDefined();
expect(createdUser.id).toBe(anotherUserId);
expect(createdUser.name).toBe(anotherUserName);
expect(createdUser.email).toBe(anotherEmail);
expect(createdUser.emailVerified).toBe(user.emailVerified);
expect(createdUser.image).toBe(user.image);
});
it('should not create a user if it already exists', async () => {
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.createUser).toBeDefined();
// @ts-expect-error: createUser is defined
await nextAuthAdapter.createUser(user);
// Should not create a new user if it already exists
expect(
await serverDB.query.users.findMany({ where: eq(users.email, user.email) }),
).toHaveLength(1);
});
it('should not create a user if email exist', async () => {
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.createUser).toBeDefined();
const anotherUserId = 'user-db-2';
const anotherUserName = 'John Doe 2';
// @ts-expect-error: createUser is defined
await nextAuthAdapter.createUser({
...user,
id: anotherUserId,
name: anotherUserName,
});
// Should not create a new user if email already exists
expect(
await serverDB.query.users.findMany({ where: eq(users.email, user.email) }),
).toHaveLength(1);
});
it('should create a user if id not exist and email is null', async () => {
// In previous version, it will link the account to the existing user if the email is null
// issue: https://github.com/lobehub/lobe-chat/issues/4918
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.createUser).toBeDefined();
const existUserId = 'user-db-1';
const existUserName = 'John Doe 1';
// @ts-expect-error: createUser is defined
await nextAuthAdapter.createUser({
...user,
id: existUserId,
name: existUserName,
email: '',
});
const anotherUserId = 'user-db-2';
const anotherUserName = 'John Doe 2';
// @ts-expect-error: createUser is defined
await nextAuthAdapter.createUser({
...user,
id: anotherUserId,
name: anotherUserName,
email: '',
});
// Should create a new user if id not exists and email is null
expect(
await serverDB.query.users.findMany({ where: eq(users.id, anotherUserId) }),
).toHaveLength(1);
});
it('should create a user if id not exist even email is invalid type', async () => {
// In previous version, it will link the account to the existing user if the email is null
// issue: https://github.com/lobehub/lobe-chat/issues/4918
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.createUser).toBeDefined();
const existUserId = 'user-db-1';
const existUserName = 'John Doe 1';
// @ts-expect-error: createUser is defined
await nextAuthAdapter.createUser({
...user,
id: existUserId,
name: existUserName,
email: Object({}), // assign a non-string value
});
const anotherUserId = 'user-db-2';
const anotherUserName = 'John Doe 2';
// @ts-expect-error: createUser is defined
await nextAuthAdapter.createUser({
...user,
id: anotherUserId,
name: anotherUserName,
// @ts-expect-error: try to assign undefined value
email: undefined,
});
// Should create a new user if id not exists and email is null
expect(
await serverDB.query.users.findMany({ where: eq(users.id, anotherUserId) }),
).toHaveLength(1);
});
});
describe('deleteUser', () => {
it('should delete a user', async () => {
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.deleteUser).toBeDefined();
// @ts-expect-error: deleteUser is defined
await nextAuthAdapter.deleteUser(userId);
const deletedUser = await serverDB.query.users.findFirst({ where: eq(users.id, userId) });
expect(deletedUser).toBeUndefined();
});
});
describe('getUser', () => {
it('should get a user', async () => {
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.getUser).toBeDefined();
// @ts-expect-error: getUser is defined
const fetchedUser = await nextAuthAdapter.getUser(userId);
expect(fetchedUser).toBeDefined();
expect(fetchedUser?.id).toBe(user.id);
expect(fetchedUser?.name).toBe(user.name);
expect(fetchedUser?.email).toBe(user.email);
expect(fetchedUser?.emailVerified).toEqual(user.emailVerified);
expect(fetchedUser?.image).toBe(user.image);
});
});
describe('getUserByEmail', () => {
it('should get a user by email', async () => {
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.getUserByEmail).toBeDefined();
// @ts-expect-error: getUserByEmail is defined
const fetchedUser = await nextAuthAdapter.getUserByEmail(user.email);
expect(fetchedUser).toBeDefined();
expect(fetchedUser?.id).toBe(user.id);
expect(fetchedUser?.name).toBe(user.name);
expect(fetchedUser?.email).toBe(user.email);
expect(fetchedUser?.emailVerified).toEqual(user.emailVerified);
expect(fetchedUser?.image).toBe(user.image);
});
});
describe('getUserByAccount', () => {
it('should get a user by account', async () => {
const account: AdapterAccount = {
providerAccountId: 'provider-account-id',
userId: userId,
provider: 'auth0',
type: 'email',
};
// @ts-expect-error: linkAccount is defined
await nextAuthAdapter.linkAccount(account);
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.getUserByAccount).toBeDefined();
// @ts-expect-error: getUserByAccount is defined
const fetchedUser = await nextAuthAdapter.getUserByAccount(account);
expect(fetchedUser).toBeDefined();
expect(fetchedUser?.id).toBe(user.id);
expect(fetchedUser?.name).toBe(user.name);
expect(fetchedUser?.email).toBe(user.email);
expect(fetchedUser?.emailVerified).toEqual(user.emailVerified);
expect(fetchedUser?.image).toBe(user.image);
});
});
describe('updateUser', () => {
it('should update a user', async () => {
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.updateUser).toBeDefined();
const updatedName = 'updated' + user.name;
const updatedEmail = 'updated' + user.email;
// @ts-expect-error: updateUser is defined
const updatedUser = await nextAuthAdapter.updateUser({
id: userId,
name: updatedName,
email: updatedEmail,
});
expect(updatedUser).toBeDefined();
expect(updatedUser.id).toBe(userId);
expect(updatedUser.name).toBe(updatedName);
expect(updatedUser.email).toBe(updatedEmail);
});
});
});
describe('authenticators', () => {
describe('createAuthenticator', () => {
it('should create a new authenticator', async () => {
// Create an authenticator and link to the exists user
const params: AdapterAuthenticator = {
credentialBackedUp: false,
credentialDeviceType: 'type',
credentialID: 'some-id',
credentialPublicKey: 'some-key',
userId: userId,
providerAccountId: 'provider-account-id',
counter: 1,
};
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.createAuthenticator).toBeDefined();
// @ts-expect-error: createAuthenticator is defined
const authenticator = await nextAuthAdapter.createAuthenticator(params);
expect(authenticator).toBeDefined();
expect(authenticator.userId).toBe(params.userId);
expect(authenticator.providerAccountId).toBe(params.providerAccountId);
});
});
describe('getAuthenticator', () => {
it('should get an authenticator', async () => {
// Create an authenticator and link to the exists user
const params: AdapterAuthenticator = {
credentialBackedUp: false,
credentialDeviceType: 'type',
credentialID: 'some-id',
credentialPublicKey: 'some-key',
userId: userId,
providerAccountId: 'provider-account-id',
counter: 1,
};
// @ts-expect-error: createAuthenticator is defined
await nextAuthAdapter.createAuthenticator(params);
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.getAuthenticator).toBeDefined();
// @ts-expect-error: getAuthenticator is defined
const fetchedAuthenticator = await nextAuthAdapter.getAuthenticator(params.credentialID);
expect(fetchedAuthenticator).toBeDefined();
expect(fetchedAuthenticator?.userId).toBe(params.userId);
expect(fetchedAuthenticator?.providerAccountId).toBe(params.providerAccountId);
});
});
describe('updateAuthenticatorCounter', () => {
it('should update an authenticator counter', async () => {
// Create an authenticator and link to the exists user
const params: AdapterAuthenticator = {
credentialBackedUp: false,
credentialDeviceType: 'type',
credentialID: 'some-id',
credentialPublicKey: 'some-key',
userId: userId,
providerAccountId: 'provider-account-id',
counter: 1,
};
// @ts-expect-error: createAuthenticator is defined
await nextAuthAdapter.createAuthenticator(params);
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.updateAuthenticatorCounter).toBeDefined();
// @ts-expect-error: updateAuthenticatorCounter is defined
const updatedAuthenticator = await nextAuthAdapter.updateAuthenticatorCounter(
params.credentialID,
2,
);
expect(updatedAuthenticator).toBeDefined();
expect(updatedAuthenticator.counter).toBe(2);
});
});
describe('listAuthenticatorsByUserId', () => {
it('should list authenticators by user id', async () => {
// Create an authenticator and link to the exists user
const params: AdapterAuthenticator = {
credentialBackedUp: false,
credentialDeviceType: 'type',
credentialID: 'some-id',
credentialPublicKey: 'some-key',
userId: userId,
providerAccountId: 'provider-account-id',
counter: 1,
};
// @ts-expect-error: createAuthenticator is defined
await nextAuthAdapter.createAuthenticator(params);
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.listAuthenticatorsByUserId).toBeDefined();
// @ts-expect-error: listAuthenticatorsByUserId is defined
const authenticators = await nextAuthAdapter.listAuthenticatorsByUserId(userId);
expect(authenticators).toBeDefined();
expect(authenticators.length).toBeGreaterThan(0);
expect(authenticators[0].userId).toBe(params.userId);
expect(authenticators[0].providerAccountId).toBe(params.providerAccountId);
});
});
});
describe('session', () => {
describe('createSession', () => {
it('should create a new session', async () => {
const data: AdapterSession = {
sessionToken,
userId: userId,
expires: new Date(),
};
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.createSession).toBeDefined();
// @ts-expect-error: createSession is defined
const session = await nextAuthAdapter.createSession(data);
expect(session).toBeDefined();
expect(session.sessionToken).toBe(data.sessionToken);
expect(session.userId).toBe(data.userId);
expect(session.expires).toEqual(data.expires);
});
});
describe('updateSession', () => {
it('should update a session', async () => {
const data: AdapterSession = {
sessionToken,
userId: userId,
expires: new Date(),
};
// @ts-expect-error: createSession is defined
await nextAuthAdapter.createSession(data);
const updatedExpires = new Date();
expect(nextAuthAdapter).toBeDefined();
expect(nextAuthAdapter.updateSession).toBeDefined();
// @ts-expect-error: updateSession is defined
const updatedSession = await nextAuthAdapter.updateSession({
sessionToken,
expires: updatedExpires,
});
expect(updatedSession).toBeDefined();
expect(updatedSession?.sessionToken).toBe(data.sessionToken);
expect(updatedSession?.expires).toEqual(updatedExpires);
});
});
describe('getSessionAndUser', () => {
it('should get a session and user', async () => {
// create session
const data = {
sessionToken,
userId: userId,
expires: new Date(),
};
// @ts-expect-error: createSession is defined
await nextAuthAdapter.createSession(data);
// @ts-expect-error: getSessionAndUser is defined
const sessionAndUser = await nextAuthAdapter.getSessionAndUser(sessionToken);
expect(sessionAndUser?.session.sessionToken).toBe(sessionToken);
expect(sessionAndUser?.user.id).toBe(user.id);
});
});
describe('deleteSession', () => {
it('should delete a session', async () => {
const data = {
sessionToken,
userId: userId,
expires: new Date(),
};
// @ts-expect-error: createSession is defined
await nextAuthAdapter.createSession(data);
// @ts-expect-error: deleteSession is defined
await nextAuthAdapter.deleteSession(sessionToken);
const session = await serverDB.query.nextauthSessions.findFirst({
where: eq(nextauthSessions.sessionToken, sessionToken),
});
expect(session).toBeUndefined();
});
});
});
describe('verificationToken', () => {
describe('createVerificationToken', () => {
it('should create a new verification token', async () => {
const token: VerificationToken = {
identifier: 'identifier',
expires: new Date(),
token: 'token',
};
// @ts-expect-error: createVerificationToken is defined
const createdToken = await nextAuthAdapter.createVerificationToken(token);
expect(createdToken).toBeDefined();
expect(createdToken?.identifier).toBe(token.identifier);
expect(createdToken?.expires).toEqual(token.expires);
expect(createdToken?.token).toBe(token.token);
});
});
describe('useVerificationToken', () => {
it('should use a verification token if exist', async () => {
const token: VerificationToken = {
identifier: 'identifier',
expires: new Date(),
token: 'token2',
};
// @ts-expect-error: createVerificationToken is defined
await nextAuthAdapter.createVerificationToken(token);
// @ts-expect-error: useVerificationToken is defined
await nextAuthAdapter.useVerificationToken(token.token);
});
it('should return null if the token does not exist', async () => {
const token: VerificationToken = {
identifier: 'identifier',
expires: new Date(),
token: 'token-not-exist',
};
// @ts-expect-error: useVerificationToken is defined
const result = await nextAuthAdapter.useVerificationToken(token.token);
expect(result).toBeNull();
});
});
});
describe('accounts', () => {
describe('linkAccount', () => {
it('should link an account', async () => {
const account: AdapterAccount = {
providerAccountId: 'provider-account-id',
userId: userId,
provider: 'auth0',
type: 'email',
};
// @ts-expect-error: linkAccount is defined
const insertedAccount = await nextAuthAdapter.linkAccount(account);
expect(insertedAccount).toBeDefined();
expect(insertedAccount?.providerAccountId).toBe(account.providerAccountId);
expect(insertedAccount?.userId).toBe(userId);
expect(insertedAccount?.provider).toBe(account.provider);
expect(insertedAccount?.type).toBe(account.type);
});
});
describe('getAccount', () => {
it('should get an account', async () => {
const account: AdapterAccount = {
providerAccountId: 'provider-account-id',
userId: userId,
provider: 'auth0',
type: 'email',
};
// @ts-expect-error: linkAccount is defined
await nextAuthAdapter.linkAccount(account);
// @ts-expect-error: getAccount is defined
const fetchedAccount = await nextAuthAdapter.getAccount(
account.providerAccountId,
account.provider,
);
expect(fetchedAccount).toBeDefined();
expect(fetchedAccount?.providerAccountId).toBe(account.providerAccountId);
expect(fetchedAccount?.userId).toBe(userId);
expect(fetchedAccount?.provider).toBe(account.provider);
expect(fetchedAccount?.type).toBe(account.type);
});
});
describe('unlinkAccount', () => {
it('should unlink an account', async () => {
const account: AdapterAccount = {
providerAccountId: 'provider-account-id',
userId: userId,
provider: 'auth0',
type: 'email',
};
// @ts-expect-error: linkAccount is defined
await nextAuthAdapter.linkAccount(account);
// @ts-expect-error: unlinkAccount is defined
await nextAuthAdapter.unlinkAccount(account);
const fetchedAccount = await serverDB.query.nextauthAccounts.findFirst({
where: eq(nextauthAccounts.providerAccountId, account.providerAccountId),
});
expect(fetchedAccount).toBeUndefined();
});
});
});
});