From e51124f316414641405eda8d1591c6df367adebe Mon Sep 17 00:00:00 2001 From: roschaefer Date: Tue, 24 Sep 2019 17:09:15 +0200 Subject: [PATCH] Resolvers for EmailAddress implemented --- backend/src/schema/resolvers/emails.js | 32 +++- backend/src/schema/resolvers/emails.spec.js | 190 ++++++++++++++------ 2 files changed, 169 insertions(+), 53 deletions(-) diff --git a/backend/src/schema/resolvers/emails.js b/backend/src/schema/resolvers/emails.js index 664df2803..d2f76ba39 100644 --- a/backend/src/schema/resolvers/emails.js +++ b/backend/src/schema/resolvers/emails.js @@ -1,6 +1,7 @@ import generateNonce from './helpers/generateNonce' import Resolver from './helpers/Resolver' import existingEmailAddress from './helpers/existingEmailAddress' +import { UserInputError } from 'apollo-server' export default { Mutation: { @@ -34,7 +35,36 @@ export default { } return response }, - VerifyEmailAddress: async (_parent, args, context, _resolveInfo) => {}, + VerifyEmailAddress: async (_parent, args, context, _resolveInfo) => { + let response + const { + user: { id: userId }, + } = context + const { nonce, email } = args + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async txc => { + const result = await txc.run( + ` + MATCH (user:User {id: $userId})-[previous:PRIMARY_EMAIL]->(:EmailAddress) + MATCH (user)<-[:BELONGS_TO]-(email:EmailAddress {email: $email, nonce: $nonce}) + MERGE (user)-[:PRIMARY_EMAIL]->(email) + SET email.verifiedAt = toString(datetime()) + DELETE previous + RETURN email + `, + { userId, email, nonce }, + ) + return result.records.map(record => record.get('email').properties) + }) + try { + const txResult = await writeTxResultPromise + response = txResult[0] + } finally { + session.close() + } + if (!response) throw new UserInputError('Invalid nonce or no email address found.') + return response + }, }, EmailAddress: { ...Resolver('EmailAddress', { diff --git a/backend/src/schema/resolvers/emails.spec.js b/backend/src/schema/resolvers/emails.spec.js index 5029622ae..6ec66ca65 100644 --- a/backend/src/schema/resolvers/emails.spec.js +++ b/backend/src/schema/resolvers/emails.spec.js @@ -67,59 +67,65 @@ describe('AddEmailAddress', () => { authenticatedUser = await user.toJson() }) - it('creates a new unverified `EmailAddress` node', async () => { - await expect(mutate({ mutation, variables })).resolves.toMatchObject({ - data: { - AddEmailAddress: { - email: 'new-email@example.org', - verifiedAt: null, - createdAt: expect.any(String), - }, - }, - errors: undefined, - }) + describe('email attribute is not a valid email', () => { + it.todo('throws UserInputError') }) - it('connects `EmailAddress` to the authenticated user', async () => { - await mutate({ mutation, variables }) - const result = await neode.cypher(` - MATCH(u:User)-[:PRIMARY_EMAIL]->(p:EmailAddress {email: "user@example.org"}) - MATCH(u:User)<-[:BELONGS_TO]-(e:EmailAddress {email: "new-email@example.org"}) - RETURN e - `) - const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) - 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, - createdAt: '2019-09-24T14:00:01.565Z', - email: 'new-email@example.org', - }) + describe('email attribute is a valid email', () => { + it('creates a new unverified `EmailAddress` node', async () => { await expect(mutate({ mutation, variables })).resolves.toMatchObject({ data: { 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 + createdAt: expect.any(String), }, }, errors: undefined, }) }) - }) - describe('but if another user owns an `EmailAddress` already with that email', () => { - it('throws UserInputError because of unique constraints', async () => { - await factory.create('User', { email: 'new-email@example.org' }) - await expect(mutate({ mutation, variables })).resolves.toMatchObject({ - data: { AddEmailAddress: null }, - errors: [{ message: 'A user account with this email already exists.' }], + it('connects `EmailAddress` 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"}) + RETURN e + `) + const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) + 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, + createdAt: '2019-09-24T14:00:01.565Z', + email: 'new-email@example.org', + }) + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { + 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, + }) + }) + }) + + describe('but if another user owns an `EmailAddress` already with that email', () => { + it('throws UserInputError because of unique constraints', async () => { + await factory.create('User', { email: 'new-email@example.org' }) + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { AddEmailAddress: null }, + errors: [{ message: 'A user account with this email already exists.' }], + }) }) }) }) @@ -132,6 +138,7 @@ describe('VerifyEmailAddress', () => { VerifyEmailAddress(email: $email, nonce: $nonce) { email createdAt + verifiedAt } } ` @@ -154,23 +161,102 @@ describe('VerifyEmailAddress', () => { }) describe('authenticated', () => { + beforeEach(async () => { + user = await factory.create('User', { email: 'user@example.org' }) + authenticatedUser = await user.toJson() + }) + describe('if no unverified `EmailAddress` node exists', () => { - it.todo('throws UserInputError') + it('throws UserInputError', async () => { + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { VerifyEmailAddress: null }, + errors: [{ message: 'Invalid nonce or no email address found.' }], + }) + }) }) - describe('given invalid nonce', () => { - it.todo('throws UserInputError') - }) - - describe('given valid nonce for unverified `EmailAddress` node', () => { - describe('but the address does not belong to the authenticated user', () => { - it.todo('throws UserInputError') + describe('given an unverified `EmailAddress`', () => { + let emailAddress + beforeEach(async () => { + emailAddress = await factory.create('EmailAddress', { + nonce: 'abcdef', + verifiedAt: null, + createdAt: new Date().toISOString(), + email: 'to-be-verified@example.org', + }) }) - describe('and the `EmailAddress` belongs to the authenticated user', () => { - it.todo('adds `verifiedAt`') - it.todo('connects the new `EmailAddress` as PRIMARY') - it.todo('removes previous PRIMARY relationship') + describe('given invalid nonce', () => { + it('throws UserInputError', async () => { + variables.nonce = 'asdfgh' + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { VerifyEmailAddress: null }, + errors: [{ message: 'Invalid nonce or no email address found.' }], + }) + }) + }) + + describe('given valid nonce for unverified `EmailAddress` node', () => { + beforeEach(() => { + variables = { ...variables, nonce: 'abcdef' } + }) + + describe('but the address does not belong to the authenticated user', () => { + it('throws UserInputError', async () => { + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { VerifyEmailAddress: null }, + errors: [{ message: 'Invalid nonce or no email address found.' }], + }) + }) + }) + + describe('and the `EmailAddress` belongs to the authenticated user', () => { + beforeEach(async () => { + await emailAddress.relateTo(user, 'belongsTo') + }) + + it('adds `verifiedAt`', async () => { + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { + VerifyEmailAddress: { + email: 'to-be-verified@example.org', + verifiedAt: expect.any(String), + createdAt: expect.any(String), + }, + }, + errors: undefined, + }) + }) + + it('connects the new `EmailAddress` as PRIMARY', async () => { + await mutate({ mutation, variables }) + const result = await neode.cypher(` + MATCH(u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "to-be-verified@example.org"}) + MATCH(u:User)<-[:BELONGS_TO]-(:EmailAddress {email: "user@example.org"}) + RETURN e + `) + const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) + await expect(email.toJson()).resolves.toMatchObject({ + email: 'to-be-verified@example.org', + }) + }) + + it('removes previous PRIMARY relationship', async () => { + const cypherStatement = ` + MATCH(u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "user@example.org"}) + RETURN e + ` + let result = await neode.cypher(cypherStatement) + let email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) + await expect(email.toJson()).resolves.toMatchObject({ + email: 'user@example.org', + }) + await mutate({ mutation, variables }) + result = await neode.cypher(cypherStatement) + email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) + await expect(email).toBe(false) + }) + }) }) }) })