Use EmailAddressRequest and validate email

This commit is contained in:
roschaefer 2019-09-27 01:12:01 +02:00
parent 707cf741de
commit e116d52992
7 changed files with 87 additions and 23 deletions

View File

@ -0,0 +1,12 @@
module.exports = {
email: { type: 'string', primary: true, lowercase: true, email: true },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
nonce: { type: 'string', token: true },
belongsTo: {
type: 'relationship',
relationship: 'BELONGS_TO',
target: 'User',
direction: 'out',
eager: true,
},
}

View File

@ -5,6 +5,7 @@ export default {
User: require('./User.js'),
InvitationCode: require('./InvitationCode.js'),
EmailAddress: require('./EmailAddress.js'),
EmailAddressRequest: require('./EmailAddressRequest.js'),
SocialMedia: require('./SocialMedia.js'),
Post: require('./Post.js'),
Comment: require('./Comment.js'),

View File

@ -2,10 +2,18 @@ import generateNonce from './helpers/generateNonce'
import Resolver from './helpers/Resolver'
import existingEmailAddress from './helpers/existingEmailAddress'
import { UserInputError } from 'apollo-server'
import Validator from 'neode/build/Services/Validator.js'
export default {
Mutation: {
AddEmailAddress: async (_parent, args, context, _resolveInfo) => {
try {
const { neode } = context
await new Validator(neode, neode.model('EmailAddressRequest'), args)
} catch (e) {
throw new UserInputError('must be a valid email')
}
let response = await existingEmailAddress(_parent, args, context)
if (response) return response
@ -19,7 +27,7 @@ export default {
const result = await txc.run(
`
MATCH (user:User {id: $userId})
MERGE (user)<-[:BELONGS_TO]-(email:EmailAddress {email: $email, nonce: $nonce})
MERGE (user)<-[:BELONGS_TO]-(email:EmailAddressRequest {email: $email, nonce: $nonce})
SET email.createdAt = toString(datetime())
RETURN email
`,
@ -46,9 +54,11 @@ export default {
const result = await txc.run(
`
MATCH (user:User {id: $userId})-[previous:PRIMARY_EMAIL]->(:EmailAddress)
MATCH (user)<-[:BELONGS_TO]-(email:EmailAddress {email: $email, nonce: $nonce})
MATCH (user)<-[:BELONGS_TO]-(email:EmailAddressRequest {email: $email, nonce: $nonce})
MERGE (user)-[:PRIMARY_EMAIL]->(email)
SET email:EmailAddress
SET email.verifiedAt = toString(datetime())
REMOVE email:EmailAddressRequest
DELETE previous
RETURN email
`,
@ -59,6 +69,10 @@ export default {
try {
const txResult = await writeTxResultPromise
response = txResult[0]
} catch (e) {
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
throw new UserInputError('A user account with this email already exists.')
throw new Error(e)
} finally {
session.close()
}

View File

@ -68,7 +68,16 @@ describe('AddEmailAddress', () => {
})
describe('email attribute is not a valid email', () => {
it.todo('throws UserInputError')
beforeEach(() => {
variables = { ...variables, email: 'foobar' }
})
it('throws UserInputError', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { AddEmailAddress: null },
errors: [{ message: 'must be a valid email' }],
})
})
})
describe('email attribute is a valid email', () => {
@ -85,24 +94,23 @@ describe('AddEmailAddress', () => {
})
})
it('connects `EmailAddress` to the authenticated user', async () => {
it('connects `EmailAddressRequest` to the authenticated user', async () => {
await mutate({ mutation, variables })
const result = await neode.cypher(`
MATCH(u:User)-[:PRIMARY_EMAIL]->(:EmailAddress {email: "user@example.org"})
MATCH(u:User)<-[:BELONGS_TO]-(e:EmailAddress {email: "new-email@example.org"})
MATCH(u:User)<-[:BELONGS_TO]-(e:EmailAddressRequest {email: "new-email@example.org"})
RETURN e
`)
const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddressRequest'))
await expect(email.toJson()).resolves.toMatchObject({
email: 'new-email@example.org',
nonce: expect.any(String),
})
})
describe('if a lone `EmailAddress` node already exists with that email', () => {
it('returns this `EmailAddress` node', async () => {
await factory.create('EmailAddress', {
verifiedAt: null,
describe('if another `EmailAddressRequest` node already exists with that email', () => {
it('throws no unique constraint violation error', async () => {
await factory.create('EmailAddressRequest', {
createdAt: '2019-09-24T14:00:01.565Z',
email: 'new-email@example.org',
})
@ -111,7 +119,6 @@ describe('AddEmailAddress', () => {
AddEmailAddress: {
email: 'new-email@example.org',
verifiedAt: null,
createdAt: '2019-09-24T14:00:01.565Z', // this is to make sure it's the one above
},
},
errors: undefined,
@ -175,10 +182,10 @@ describe('VerifyEmailAddress', () => {
})
})
describe('given an unverified `EmailAddress`', () => {
describe('given a `EmailAddressRequest`', () => {
let emailAddress
beforeEach(async () => {
emailAddress = await factory.create('EmailAddress', {
emailAddress = await factory.create('EmailAddressRequest', {
nonce: 'abcdef',
verifiedAt: null,
createdAt: new Date().toISOString(),
@ -196,7 +203,7 @@ describe('VerifyEmailAddress', () => {
})
})
describe('given valid nonce for unverified `EmailAddress` node', () => {
describe('given valid nonce for `EmailAddressRequest` node', () => {
beforeEach(() => {
variables = { ...variables, nonce: 'abcdef' }
})
@ -210,7 +217,7 @@ describe('VerifyEmailAddress', () => {
})
})
describe('and the `EmailAddress` belongs to the authenticated user', () => {
describe('and the `EmailAddressRequest` belongs to the authenticated user', () => {
beforeEach(async () => {
await emailAddress.relateTo(user, 'belongsTo')
})
@ -256,6 +263,19 @@ describe('VerifyEmailAddress', () => {
email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
await expect(email).toBe(false)
})
describe('Edge case: In the meantime someone created an `EmailAddress` node with the given email', () => {
beforeEach(async () => {
await factory.create('EmailAddress', { email: 'to-be-verified@example.org' })
})
it('throws UserInputError because of unique constraints', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { VerifyEmailAddress: null },
errors: [{ message: 'A user account with this email already exists.' }],
})
})
})
})
})
})

View File

@ -0,0 +1,10 @@
import { defaults } from './emailAddresses.js'
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
args = defaults({ args })
return neodeInstance.create('EmailAddressRequest', args)
},
}
}

View File

@ -1,16 +1,21 @@
import faker from 'faker'
export function defaults({ args }) {
const defaults = {
email: faker.internet.email(),
verifiedAt: new Date().toISOString(),
}
args = {
...defaults,
...args,
}
return args
}
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
const defaults = {
email: faker.internet.email(),
verifiedAt: new Date().toISOString(),
}
args = {
...defaults,
...args,
}
args = defaults({ args })
return neodeInstance.create('EmailAddress', args)
},
}

View File

@ -9,6 +9,7 @@ import createTag from './tags.js'
import createSocialMedia from './socialMedia.js'
import createLocation from './locations.js'
import createEmailAddress from './emailAddresses.js'
import createEmailAddressRequests from './emailAddressRequests.js'
export const seedServerHost = 'http://127.0.0.1:4001'
@ -32,6 +33,7 @@ const factories = {
SocialMedia: createSocialMedia,
Location: createLocation,
EmailAddress: createEmailAddress,
EmailAddressRequest: createEmailAddressRequests,
}
export const cleanDatabase = async (options = {}) => {