diff --git a/package-lock.json b/package-lock.json index 3e5a0dbca8..facfdf2e7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1863,6 +1863,11 @@ "integrity": "sha512-S/yKGU1syOMzO86+dGpg2qGoDL0zvzcb262G+gqEy6TgP6rt6z6qxSFX/8X6vLC91P7G7C3nLs0+bvDzmvBA3Q==", "dev": true }, + "btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=" + }, "buffer": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", @@ -2291,6 +2296,14 @@ "parse-json": "^4.0.0" } }, + "cross-fetch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.6.tgz", + "integrity": "sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ==", + "requires": { + "node-fetch": "2.6.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2682,8 +2695,7 @@ "dotenv": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", - "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", - "dev": true + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" }, "ecc-jsbn": { "version": "0.1.2", @@ -3498,6 +3510,21 @@ "reusify": "^1.0.4" } }, + "faunadb": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/faunadb/-/faunadb-3.0.1.tgz", + "integrity": "sha512-WlfPjC0V9xHs4NTunOWmYZtJfbJ45Z1VAIKKka6+mRrmijWOFQzJVDY9CqS6X9kvepM36EjmtNkIvV0OJ1wTEA==", + "requires": { + "base64-js": "^1.2.0", + "btoa-lite": "^1.0.0", + "cross-fetch": "^3.0.4", + "dotenv": "^8.2.0", + "fn-annotate": "^1.1.3", + "object-assign": "^4.1.0", + "url-parse": "^1.4.7", + "util-deprecate": "^1.0.2" + } + }, "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -3596,6 +3623,11 @@ "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", "dev": true }, + "fn-annotate": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fn-annotate/-/fn-annotate-1.2.0.tgz", + "integrity": "sha1-KNoAARfephhC/mHzU/Qc9Mk6en4=" + }, "follow-redirects": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", @@ -5876,6 +5908,17 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "node-releases": { "version": "1.1.53", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.53.tgz", @@ -7563,6 +7606,11 @@ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -7901,6 +7949,11 @@ } } }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, "resolve": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", @@ -9224,6 +9277,15 @@ "dev": true, "optional": true }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -9234,8 +9296,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.0.1", diff --git a/package.json b/package.json index a431cf2670..12267f5246 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,12 @@ "test:app:rebuild": "npm run build && docker-compose -f test/docker/app.yml up -d --build", "test:app:stop": "docker-compose -f test/docker/app.yml down", "test": "npm run test:app:rebuild && npm run test:integration && npm run test:app:stop", - "test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb && npm run test:db:mssql", + "test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb && npm run test:db:mssql && npm run test:db:fauna", "test:db:mysql": "node test/mysql.js", "test:db:postgres": "node test/postgres.js", "test:db:mongodb": "node test/mongodb.js", "test:db:mssql": "node test/mssql.js", + "test:db:fauna": "node test/fauna.js", "test:integration": "mocha test/integration", "db:start": "docker-compose -f test/docker/databases.yml up -d", "db:stop": "docker-compose -f test/docker/databases.yml down", @@ -42,6 +43,7 @@ "license": "ISC", "dependencies": { "crypto-js": "^4.0.0", + "faunadb": "^3.0.1", "futoin-hkdf": "^1.3.2", "jose": "^1.27.2", "jsonwebtoken": "^8.5.1", diff --git a/src/adapters/fauna/index.js b/src/adapters/fauna/index.js new file mode 100644 index 0000000000..f86b875cfb --- /dev/null +++ b/src/adapters/fauna/index.js @@ -0,0 +1,505 @@ +import { query as q } from 'faunadb' +import { createHash, randomBytes } from 'crypto' +import logger from '../../lib/logger' + +const Adapter = (config, options = {}) => { + const { faunaClient } = config + + async function getAdapter (appOptions) { + function _debug (debugCode, ...args) { + logger.debug(`fauna_${debugCode}`, ...args) + } + + const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000 + const sessionMaxAge = (appOptions && appOptions.session && appOptions.session.maxAge) + ? appOptions.session.maxAge * 1000 + : defaultSessionMaxAge + const sessionUpdateAge = (appOptions && appOptions.session && appOptions.session.updateAge) + ? appOptions.session.updateAge * 1000 + : 0 + + async function createUser (profile) { + _debug('createUser', profile) + + const timestamp = new Date().toISOString() + const FQL = q.Create( + q.Collection('user'), { + data: { + name: profile.name, + email: profile.email, + image: profile.image, + emailVerified: profile.emailVerified + ? profile.emailVerified + : false, + createdAt: q.Time(timestamp), + updatedAt: q.Time(timestamp) + } + }) + + try { + const newUser = await faunaClient.query(FQL) + newUser.data.id = newUser.ref.id + + return newUser.data + } catch (error) { + console.error('CREATE_USER', error) + return Promise.reject(new Error('CREATE_USER')) + } + } + + async function getUser (id) { + _debug('getUser', id) + + const FQL = q.Get( + q.Ref(q.Collection('user'), id) + ) + + try { + const user = await faunaClient.query(FQL) + user.data.id = user.ref.id + + return user.data + } catch (error) { + console.error('GET_USER', error) + return Promise.reject(new Error('GET_USER')) + } + } + + async function getUserByEmail (email) { + _debug('getUserByEmail', email) + + if (!email) { + return null + } + + const FQL = q.Let( + { + ref: q.Match(q.Index('user_by_email'), email) + }, + q.If( + q.Exists(q.Var('ref')), + q.Get(q.Var('ref')), + null + ) + ) + + try { + const user = await faunaClient.query(FQL) + + if (user == null) { + return null + } + + user.data.id = user.ref.id + return user.data + } catch (error) { + console.error('GET_USER_BY_EMAIL', error) + return Promise.reject(new Error('GET_USER_BY_EMAIL')) + } + } + + async function getUserByProviderAccountId (providerId, providerAccountId) { + _debug('getUserByProviderAccountId', providerId, providerAccountId) + + const FQL = q.Let( + { + ref: q.Match( + q.Index('account_by_provider_account_id'), + [providerId, providerAccountId] + ) + }, + q.If( + q.Exists(q.Var('ref')), + q.Get( + q.Ref( + q.Collection('user'), + q.Select(['data', 'userId'], + q.Get(q.Var('ref')) + ) + ) + ), + null + ) + ) + + try { + const user = await faunaClient.query(FQL) + + if (user == null) { + return null + } + + user.data.id = user.ref.id + + return user.data + } catch (error) { + console.error('GET_USER_BY_PROVIDER_ACCOUNT_ID', error) + return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID')) + } + } + + async function updateUser (user) { + _debug('updateUser', user) + + const timestamp = new Date().toISOString() + const FQL = q.Update( + q.Ref(q.Collection('user'), user.id), + { + data: { + name: user.name, + email: user.email, + image: user.image, + emailVerified: user.emailVerified ? user.emailVerified : false, + updatedAt: q.Time(timestamp) + } + } + ) + + try { + const user = await faunaClient.query(FQL) + user.data.id = user.ref.id + + return user.data + } catch (error) { + console.error('UPDATE_USER_ERROR', error) + return Promise.reject(new Error('UPDATE_USER_ERROR')) + } + } + + async function deleteUser (userId) { + _debug('deleteUser', userId) + + const FQL = q.Delete( + q.Ref(q.Collection('user'), userId) + ) + + try { + await faunaClient.query(FQL) + } catch (error) { + console.error('DELETE_USER_ERROR', error) + return Promise.reject(new Error('DELETE_USER_ERROR')) + } + } + + async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) { + _debug('linkAccount', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) + + try { + const timestamp = new Date().toISOString() + const account = await faunaClient.query( + q.Create(q.Collection('account'), { + data: { + userId: userId, + providerId: providerId, + providerType: providerType, + providerAccountId: providerAccountId, + refreshToken: refreshToken, + accessToken: accessToken, + accessTokenExpires: accessTokenExpires, + createdAt: q.Time(timestamp), + updatedAt: q.Time(timestamp) + } + }) + ) + + return account.data + } catch (error) { + console.error('LINK_ACCOUNT_ERROR', error) + return Promise.reject(new Error('LINK_ACCOUNT_ERROR')) + } + } + + async function unlinkAccount (userId, providerId, providerAccountId) { + _debug('unlinkAccount', userId, providerId, providerAccountId) + + const FQL = q.Delete( + q.Select('ref', + q.Get( + q.Match( + q.Index('account_by_provider_account_id'), + [providerId, providerAccountId] + ) + ) + ) + ) + + try { + await faunaClient.query(FQL) + } catch (error) { + console.error('UNLINK_ACCOUNT_ERROR', error) + return Promise.reject(new Error('UNLINK_ACCOUNT_ERROR')) + } + } + + async function createSession (user) { + _debug('createSession', user) + + let expires = null + if (sessionMaxAge) { + const dateExpires = new Date() + dateExpires.setTime(dateExpires.getTime() + sessionMaxAge) + expires = dateExpires.toISOString() + } + + const timestamp = new Date().toISOString() + const FQL = + q.Create(q.Collection('session'), { + data: { + userId: user.id, + expires: q.Time(expires), + sessionToken: randomBytes(32).toString('hex'), + accessToken: randomBytes(32).toString('hex'), + createdAt: q.Time(timestamp), + updatedAt: q.Time(timestamp) + } + }) + + try { + const session = await faunaClient.query(FQL) + + session.data.id = session.ref.id + + return session.data + } catch (error) { + console.error('CREATE_SESSION_ERROR', error) + return Promise.reject(new Error('CREATE_SESSION_ERROR')) + } + } + + async function getSession (sessionToken) { + _debug('getSession', sessionToken) + + try { + var session = await faunaClient.query( + q.Get( + q.Match( + q.Index('session_by_token'), + sessionToken + ) + ) + ) + + // Check session has not expired (do not return it if it has) + if (session && session.expires && new Date() > session.expires) { + await _deleteSession(sessionToken) + return null + } + + session.data.id = session.ref.id + + return session.data + } catch (error) { + console.error('GET_SESSION_ERROR', error) + return Promise.reject(new Error('GET_SESSION_ERROR')) + } + } + + async function updateSession (session, force) { + _debug('updateSession', session) + + try { + const shouldUpdate = sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires + if (!shouldUpdate && !force) { + return null + } + + // Calculate last updated date, to throttle write updates to database + // Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge + // e.g. ({expiry date} - 30 days) + 1 hour + // + // Default for sessionMaxAge is 30 days. + // Default for sessionUpdateAge is 1 hour. + const dateSessionIsDueToBeUpdated = new Date(session.expires) + dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge) + dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge) + + // Trigger update of session expiry date and write to database, only + // if the session was last updated more than {sessionUpdateAge} ago + const currentDate = new Date() + if (currentDate < dateSessionIsDueToBeUpdated && !force) { + return null + } + + const newExpiryDate = new Date() + newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge) + + const updatedSession = await faunaClient.query( + q.Update( + q.Ref(q.Collection('session'), session.id), + { + data: { + expires: q.Time(newExpiryDate.toISOString()), + updatedAt: q.Time(new Date().toISOString()) + } + } + ) + ) + + updatedSession.data.id = updatedSession.ref.id + + return updatedSession.data + } catch (error) { + console.error('UPDATE_SESSION_ERROR', error) + return Promise.reject(new Error('UPDATE_SESSION_ERROR')) + } + } + + async function _deleteSession (sessionToken) { + const FQL = q.Delete( + q.Select('ref', + q.Get( + q.Match( + q.Index('session_by_token'), + sessionToken + ) + ) + ) + ) + + return faunaClient.query(FQL) + } + + async function deleteSession (sessionToken) { + _debug('deleteSession', sessionToken) + + try { + return await _deleteSession(sessionToken) + } catch (error) { + console.error('DELETE_SESSION_ERROR', error) + return Promise.reject(new Error('DELETE_SESSION_ERROR')) + } + } + + async function createVerificationRequest (identifier, url, token, secret, provider) { + _debug('createVerificationRequest', identifier) + + const { baseUrl } = appOptions + const { sendVerificationRequest, maxAge } = provider + + // Store hashed token (using secret as salt) so that tokens cannot be exploited + // even if the contents of the database is compromised + // @TODO Use bcrypt function here instead of simple salted hash + const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex') + + let expires = null + if (maxAge) { + const dateExpires = new Date() + dateExpires.setTime(dateExpires.getTime() + (maxAge * 1000)) + + expires = dateExpires.toISOString() + } + + const timestamp = new Date().toISOString() + const FQL = q.Create( + q.Collection('verification_request'), { + data: { + identifier: identifier, + token: hashedToken, + expires: expires === null ? null : q.Time(expires), + createdAt: q.Time(timestamp), + updatedAt: q.Time(timestamp) + } + } + ) + + try { + const verificationRequest = await faunaClient.query(FQL) + + // With the verificationCallback on a provider, you can send an email, or queue + // an email to be sent, or perform some other action (e.g. send a text message) + await sendVerificationRequest({ identifier, url, token, baseUrl, provider }) + + return verificationRequest.data + } catch (error) { + console.error('CREATE_VERIFICATION_REQUEST_ERROR', error) + return Promise.reject(new Error('CREATE_VERIFICATION_REQUEST_ERROR')) + } + } + + async function getVerificationRequest (identifier, token, secret, provider) { + _debug('getVerificationRequest', identifier, token) + + const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex') + const FQL = q.Let( + { + ref: q.Match(q.Index('vertification_request_by_token'), hashedToken) + }, + q.If( + q.Exists(q.Var('ref')), + { + ref: q.Var('ref'), + request: q.Select('data', q.Get(q.Var('ref'))) + }, + null + ) + ) + + try { + const { ref, request: verificationRequest } = await faunaClient.query(FQL) + const nowDate = Date.now() + + if (verificationRequest && verificationRequest.expires && verificationRequest.expires < nowDate) { + // Delete the expired request so it cannot be used + await faunaClient.query( + q.Delete(ref) + ) + + return null + } + + return verificationRequest + } catch (error) { + console.error('GET_VERIFICATION_REQUEST_ERROR', error) + return Promise.reject(new Error('GET_VERIFICATION_REQUEST_ERROR')) + } + } + + async function deleteVerificationRequest (identifier, token, secret, provider) { + _debug('deleteVerification', identifier, token) + + const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex') + const FQL = q.Delete( + q.Select('ref', + q.Get( + q.Match( + q.Index('vertification_request_by_token'), hashedToken + ) + ) + ) + ) + + try { + await faunaClient.query(FQL) + } catch (error) { + console.error('DELETE_VERIFICATION_REQUEST_ERROR', error) + return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR')) + } + } + + return Promise.resolve({ + createUser, + getUser, + getUserByEmail, + getUserByProviderAccountId, + updateUser, + deleteUser, + linkAccount, + unlinkAccount, + createSession, + getSession, + updateSession, + deleteSession, + createVerificationRequest, + getVerificationRequest, + deleteVerificationRequest + }) + } + + return { + getAdapter + } +} + +export default { + Adapter +} diff --git a/src/adapters/index.js b/src/adapters/index.js index 38cbe0b109..e6bdd5d425 100644 --- a/src/adapters/index.js +++ b/src/adapters/index.js @@ -1,8 +1,10 @@ import TypeORM from './typeorm' import Prisma from './prisma' +import Fauna from './fauna' export default { Default: TypeORM.Adapter, TypeORM, - Prisma + Prisma, + Fauna } diff --git a/test/docker/databases.yml b/test/docker/databases.yml index db47f6ccd6..f4c1f54e4e 100644 --- a/test/docker/databases.yml +++ b/test/docker/databases.yml @@ -34,3 +34,10 @@ services: service: postgres ports: - "5432:5432" + + fauna: + extends: + file: databases/fauna.yml + service: fauna + ports: + - 8443:8443 diff --git a/test/docker/databases/fauna.yml b/test/docker/databases/fauna.yml new file mode 100644 index 0000000000..64380469f3 --- /dev/null +++ b/test/docker/databases/fauna.yml @@ -0,0 +1,7 @@ +version: '2' + +services: + + fauna: + image: fauna/faunadb + restart: always \ No newline at end of file diff --git a/test/fauna.js b/test/fauna.js new file mode 100644 index 0000000000..ca21a54fae --- /dev/null +++ b/test/fauna.js @@ -0,0 +1,200 @@ +/* eslint-disable */ +const Adapters = require('../adapters'); +const assert = require('assert'); +const fauna = require('faunadb'); +const q = fauna.query; + +const adminClient = new fauna.Client({ + secret: 'secret', + domain: 'localhost', + port: '8443', + scheme: 'http' +}); + +// Authenticated client against the new DB used for tests +let client = null; + +const InitialiseDb = async () => { + await adminClient.query( + q.CreateDatabase({name: 'nextauth'}) + ); + + const key = await adminClient.query( + q.CreateKey({ + database: q.Database('nextauth'), + role: 'server' + }) + ); + + client = new fauna.Client({ + secret: key.secret, + domain: 'localhost', + port: '8443', + scheme: 'http' + }); + + await client.query(q.CreateCollection({name: 'account'})); + await client.query(q.CreateCollection({name: 'session'})); + await client.query(q.CreateCollection({name: 'user'})); + await client.query(q.CreateCollection({name: 'verification_request'})); + + await client.query(q.CreateIndex({ + name: 'account_by_provider_account_id', + source: q.Collection('account'), + unique: true, + terms: [ + { field: ['data', 'providerId'] }, + { field: ['data', 'providerAccountId'] } + ] + })); + + await client.query(q.CreateIndex({ + name: 'session_by_token', + source: q.Collection('session'), + unique: true, + terms: [ + { field: ['data', 'sessionToken'] } + ] + })); + + await client.query(q.CreateIndex({ + name: 'user_by_email', + source: q.Collection('user'), + unique: true, + terms: [ + { field: ['data', 'email'] } + ] + })); + + await client.query(q.CreateIndex({ + name: 'vertification_request_by_token', + source: q.Collection('verification_request'), + unique: true, + terms: [ + { field: ['data', 'token'] } + ] + })); +} + +const RunTests = async (adapter) => { + // createUser + const newUserResult = await adapter.createUser({ + name: 'test user', + email: 'user@name.test', + image: 'https://www.gravatar.com/avatar/0' + }); + + assert.strictEqual(newUserResult.name, 'test user'); + assert(newUserResult.createdAt !== null); + + const userId = newUserResult.id; + + // getUser + const user = await adapter.getUser(newUserResult.id); + assert.strictEqual(user.id, userId); + + // getUserByEmail + const userByEmaiil = await adapter.getUserByEmail('user@name.test'); + assert.strictEqual(userByEmaiil.id, userId); + + // updateUser + const update = { + ...user, + name: 'updated name' + }; + const updatedUser = await adapter.updateUser(update); + assert.strictEqual(updatedUser.name, 'updated name'); + assert.strictEqual(updatedUser.id, userId); + + // linkAccount + const account = await adapter.linkAccount( + userId, + 'github', + 'oauth', + 756832, + undefined, + 'b7e3b00f2c596abc445f11abc445f1104c1b2b', + null + ); + assert.strictEqual(account.userId, userId); + assert.strictEqual(account.providerId, 'github'); + assert(account.createdAt !== null); + + // getUserByProviderAccountId + const userByProviderAccountId = await adapter.getUserByProviderAccountId('github', 756832); + assert.strictEqual(userByProviderAccountId.email, user.email); + + // createSession + const newSession = await adapter.createSession(user); + assert(newSession.sessionToken !== null); + assert(newSession.createdAt !== null); + assert(newSession.expires !== null); + + // getSession + const session = await adapter.getSession(newSession.sessionToken); + assert.strictEqual(session.sessionToken, newSession.sessionToken); + + // updateSession + const updatedSession = await adapter.updateSession(session); + assert(updatedSession.expires !== session.expires); + + // deleteSession + await adapter.deleteSession(session.sessionToken); + + // unlinkAccount + await adapter.unlinkAccount(userId, 'github', 756832); + + // deleteUser + await adapter.deleteUser(userId); + + // createVerificationRequest + let requestSent = false; + const newVerificationRequest = await adapter.createVerificationRequest( + 'user@test.test', + 'http://localhost/callback/email?email=test@test.test&token=123', + '123', + 'abc', + { + sendVerificationRequest: ({}) => { + requestSent = true; + } + } + ); + assert.strictEqual(newVerificationRequest.identifier, 'user@test.test'); + assert(newVerificationRequest.token !== null && newVerificationRequest.token !== ''); + assert(requestSent === true); + + // getVerificationRequest + const verificationRequest = await adapter.getVerificationRequest('user@test.test', '123', 'abc'); + assert.strictEqual(verificationRequest.identifier, 'user@test.test'); + assert.strictEqual(verificationRequest.token, newVerificationRequest.token); + + // deleteVerificationRequest + await adapter.deleteVerificationRequest('user@test.test', '123', 'abc'); +} + +;(async () => { + let error = false; + + try { + // Initialise collections and create indexes + await InitialiseDb(); + + const adapterFactory = Adapters.Fauna.Adapter({faunaClient: client}); + const adapter = await adapterFactory.getAdapter({baseUrl: 'http://localhost'}); + + await RunTests(adapter); + console.log('FaunaDB loaded ok'); + } catch (error) { + console.error('FaunaDB error', error); + error = true; + } finally { + // Clean up the DB + await adminClient.query( + q.Delete(q.Database('nextauth')) + ); + } + + const retCode = error ? 1 : 0; + process.exit(retCode); +})();