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'), User: require('./User.js'),
InvitationCode: require('./InvitationCode.js'), InvitationCode: require('./InvitationCode.js'),
EmailAddress: require('./EmailAddress.js'), EmailAddress: require('./EmailAddress.js'),
EmailAddressRequest: require('./EmailAddressRequest.js'),
SocialMedia: require('./SocialMedia.js'), SocialMedia: require('./SocialMedia.js'),
Post: require('./Post.js'), Post: require('./Post.js'),
Comment: require('./Comment.js'), Comment: require('./Comment.js'),

View File

@ -2,10 +2,18 @@ import generateNonce from './helpers/generateNonce'
import Resolver from './helpers/Resolver' import Resolver from './helpers/Resolver'
import existingEmailAddress from './helpers/existingEmailAddress' import existingEmailAddress from './helpers/existingEmailAddress'
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
import Validator from 'neode/build/Services/Validator.js'
export default { export default {
Mutation: { Mutation: {
AddEmailAddress: async (_parent, args, context, _resolveInfo) => { 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) let response = await existingEmailAddress(_parent, args, context)
if (response) return response if (response) return response
@ -19,7 +27,7 @@ export default {
const result = await txc.run( const result = await txc.run(
` `
MATCH (user:User {id: $userId}) 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()) SET email.createdAt = toString(datetime())
RETURN email RETURN email
`, `,
@ -46,9 +54,11 @@ export default {
const result = await txc.run( const result = await txc.run(
` `
MATCH (user:User {id: $userId})-[previous:PRIMARY_EMAIL]->(:EmailAddress) 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) MERGE (user)-[:PRIMARY_EMAIL]->(email)
SET email:EmailAddress
SET email.verifiedAt = toString(datetime()) SET email.verifiedAt = toString(datetime())
REMOVE email:EmailAddressRequest
DELETE previous DELETE previous
RETURN email RETURN email
`, `,
@ -59,6 +69,10 @@ export default {
try { try {
const txResult = await writeTxResultPromise const txResult = await writeTxResultPromise
response = txResult[0] 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 { } finally {
session.close() session.close()
} }

View File

@ -68,7 +68,16 @@ describe('AddEmailAddress', () => {
}) })
describe('email attribute is not a valid email', () => { 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', () => { 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 }) await mutate({ mutation, variables })
const result = await neode.cypher(` const result = await neode.cypher(`
MATCH(u:User)-[:PRIMARY_EMAIL]->(:EmailAddress {email: "user@example.org"}) 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 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({ await expect(email.toJson()).resolves.toMatchObject({
email: 'new-email@example.org', email: 'new-email@example.org',
nonce: expect.any(String), nonce: expect.any(String),
}) })
}) })
describe('if a lone `EmailAddress` node already exists with that email', () => { describe('if another `EmailAddressRequest` node already exists with that email', () => {
it('returns this `EmailAddress` node', async () => { it('throws no unique constraint violation error', async () => {
await factory.create('EmailAddress', { await factory.create('EmailAddressRequest', {
verifiedAt: null,
createdAt: '2019-09-24T14:00:01.565Z', createdAt: '2019-09-24T14:00:01.565Z',
email: 'new-email@example.org', email: 'new-email@example.org',
}) })
@ -111,7 +119,6 @@ describe('AddEmailAddress', () => {
AddEmailAddress: { AddEmailAddress: {
email: 'new-email@example.org', email: 'new-email@example.org',
verifiedAt: null, verifiedAt: null,
createdAt: '2019-09-24T14:00:01.565Z', // this is to make sure it's the one above
}, },
}, },
errors: undefined, errors: undefined,
@ -175,10 +182,10 @@ describe('VerifyEmailAddress', () => {
}) })
}) })
describe('given an unverified `EmailAddress`', () => { describe('given a `EmailAddressRequest`', () => {
let emailAddress let emailAddress
beforeEach(async () => { beforeEach(async () => {
emailAddress = await factory.create('EmailAddress', { emailAddress = await factory.create('EmailAddressRequest', {
nonce: 'abcdef', nonce: 'abcdef',
verifiedAt: null, verifiedAt: null,
createdAt: new Date().toISOString(), 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(() => { beforeEach(() => {
variables = { ...variables, nonce: 'abcdef' } 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 () => { beforeEach(async () => {
await emailAddress.relateTo(user, 'belongsTo') await emailAddress.relateTo(user, 'belongsTo')
}) })
@ -256,6 +263,19 @@ describe('VerifyEmailAddress', () => {
email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
await expect(email).toBe(false) 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' 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() { export default function create() {
return { return {
factory: async ({ args, neodeInstance }) => { factory: async ({ args, neodeInstance }) => {
const defaults = { args = defaults({ args })
email: faker.internet.email(),
verifiedAt: new Date().toISOString(),
}
args = {
...defaults,
...args,
}
return neodeInstance.create('EmailAddress', args) return neodeInstance.create('EmailAddress', args)
}, },
} }

View File

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