Added backend support for a user's profile header image: added type in gql schema, resolvers, models and database seed. Also added tests everywhere a user's profile avatar is also tested.

This commit is contained in:
Dries Cruyskens 2020-06-12 18:18:33 +02:00
parent c113fde014
commit a551959b79
11 changed files with 78 additions and 18 deletions

View File

@ -91,14 +91,22 @@ Factory.define('user')
url: faker.internet.avatar(),
}),
)
/* Defaults to false since a lot of existing tests are based on finding a certain
amount of images without further checks causing them to fail when finding these
extra profile header images. A couple of users are given profile headers
explicitly in seed.js.
*/
.option('profileHeader', () => false)
.after(async (buildObject, options) => {
const [user, email, avatar] = await Promise.all([
const [user, email, avatar, profileHeader] = await Promise.all([
neode.create('User', buildObject),
neode.create('EmailAddress', { email: options.email }),
options.avatar,
options.profileHeader,
])
await Promise.all([user.relateTo(email, 'primaryEmail'), email.relateTo(user, 'belongsTo')])
if (avatar) await user.relateTo(avatar, 'avatar')
if (profileHeader) await user.relateTo(profileHeader, 'profileHeader')
return user
})

View File

@ -168,6 +168,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
{
email: 'moderator@example.org',
profileHeader: Factory.build('image'),
},
),
Factory.build(
@ -180,6 +181,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
{
email: 'user@example.org',
profileHeader: Factory.build('image'),
},
),
Factory.build(
@ -192,6 +194,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
{
email: 'huey@example.org',
profileHeader: Factory.build('image'),
},
),
Factory.build(

View File

@ -18,6 +18,7 @@ const obfuscate = async (resolve, root, args, context, info) => {
root.title = 'UNAVAILABLE'
root.slug = 'UNAVAILABLE'
root.avatar = null
root.profileHeader = null
root.about = 'UNAVAILABLE'
root.name = 'UNAVAILABLE'
root.image = null

View File

@ -41,6 +41,9 @@ beforeAll(async () => {
avatar: Factory.build('image', {
url: '/some/offensive/avatar.jpg',
}),
profileHeader: Factory.build('image', {
url: '/some/offensive/profileHeader.jpg',
}),
},
),
neode.create('Category', {
@ -225,6 +228,9 @@ describe('softDeleteMiddleware', () => {
avatar {
url
}
profileHeader {
url
}
}
}
}
@ -270,6 +276,10 @@ describe('softDeleteMiddleware', () => {
expect(subject.avatar).toEqual({
url: expect.stringContaining('/some/offensive/avatar.jpg'),
}))
it('display profile header', () =>
expect(subject.profileHeader).toEqual({
url: expect.stringContaining('/some/offensive/profileHeader.jpg'),
}))
})
describe('Post', () => {
@ -308,6 +318,7 @@ describe('softDeleteMiddleware', () => {
it('obfuscates slug', () => expect(subject.slug).toEqual('UNAVAILABLE'))
it('obfuscates about', () => expect(subject.about).toEqual('UNAVAILABLE'))
it('obfuscates avatar', () => expect(subject.avatar).toEqual(null))
it('obfuscates profile header', () => expect(subject.profileHeader).toEqual(null))
})
describe('Post', () => {

View File

@ -12,6 +12,12 @@ export default {
target: 'Image',
direction: 'out',
},
profileHeader: {
type: 'relationship',
relationship: 'PROFILE_HEADER_IMAGE',
target: 'Image',
direction: 'out',
},
deleted: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false },
role: { type: 'string', default: 'user' },

View File

@ -105,7 +105,7 @@ const uploadImageFile = async (upload, uploadCallback) => {
const sanitizeRelationshipType = (relationshipType) => {
// Cypher query language does not allow to parameterize relationship types
// See: https://github.com/neo4j/neo4j/issues/340
if (!['HERO_IMAGE', 'AVATAR_IMAGE'].includes(relationshipType)) {
if (!['HERO_IMAGE', 'AVATAR_IMAGE', 'PROFILE_HEADER_IMAGE'].includes(relationshipType)) {
throw new Error(`Unknown relationship type ${relationshipType}`)
}
}

View File

@ -21,7 +21,9 @@ const statisticsQuery = gql`
}
}
`
beforeAll(() => {
beforeAll(async () => {
// Clean the database so no artifacts from other test files are interfering.
await cleanDatabase()
authenticatedUser = undefined
const { server } = createServer({
context: () => {

View File

@ -109,6 +109,9 @@ describe('currentUser', () => {
avatar {
url
}
profileHeader {
url
}
email
role
}
@ -142,6 +145,9 @@ describe('currentUser', () => {
avatar: Factory.build('image', {
url: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
}),
profileHeader: Factory.build('image', {
url: 'https://s3.amazonaws.com/uifaces/faces/twitter/hellofeverrrr/128.jpg',
}),
},
)
const userBearerToken = encode({ id: 'u3' })
@ -156,6 +162,9 @@ describe('currentUser', () => {
avatar: Factory.build('image', {
url: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
}),
profileHeader: Factory.build('image', {
url: 'https://s3.amazonaws.com/uifaces/faces/twitter/hellofeverrrr/128.jpg',
}),
email: 'test@example.org',
name: 'Matilde Hermiston',
slug: 'matilde-hermiston',

View File

@ -140,8 +140,9 @@ export default {
},
UpdateUser: async (_parent, params, context, _resolveInfo) => {
const { termsAndConditionsAgreedVersion } = params
const { avatar: avatarInput } = params
const { avatar: avatarInput, profileHeader: profileHeaderInput } = params
delete params.avatar
delete params.profileHeader
if (termsAndConditionsAgreedVersion) {
const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g)
if (!regEx.test(termsAndConditionsAgreedVersion)) {
@ -165,6 +166,9 @@ export default {
if (avatarInput) {
await mergeImage(user, 'AVATAR_IMAGE', avatarInput, { transaction })
}
if (profileHeaderInput) {
await mergeImage(user, 'PROFILE_HEADER_IMAGE', profileHeaderInput, { transaction })
}
return user
})
try {
@ -235,6 +239,7 @@ export default {
log(deleteUserTransactionResponse)
const [user] = deleteUserTransactionResponse.records.map((record) => record.get('user'))
await deleteImage(user, 'AVATAR_IMAGE', { transaction })
await deleteImage(user, 'PROFILE_HEADER_IMAGE', { transaction })
return user
})
try {
@ -291,6 +296,7 @@ export default {
},
hasOne: {
avatar: '-[:AVATAR_IMAGE]->(related:Image)',
profileHeader: '-[:PROFILE_HEADER_IMAGE]->(related:Image)',
invitedBy: '<-[:INVITED]-(related:User)',
location: '-[:IS_IN]->(related:Location)',
},

View File

@ -341,11 +341,17 @@ describe('DeleteUser', () => {
beforeEach(async () => {
variables = { id: ' u343', resource: [] }
user = await Factory.build('user', {
name: 'My name should be deleted',
about: 'along with my about',
id: 'u343',
})
user = await Factory.build(
'user',
{
name: 'My name should be deleted',
about: 'along with my about',
id: 'u343',
},
{
profileHeader: Factory.build('image'),
},
)
})
describe('authenticated as Admin', () => {
@ -496,8 +502,8 @@ describe('DeleteUser', () => {
).resolves.toMatchObject(expectedResponse)
})
it('deletes user avatar and post hero images', async () => {
await expect(neode.all('Image')).resolves.toHaveLength(22)
it('deletes user avatar, profile header and post hero images', async () => {
await expect(neode.all('Image')).resolves.toHaveLength(23)
await mutate({ mutation: deleteUserMutation, variables })
await expect(neode.all('Image')).resolves.toHaveLength(20)
})
@ -627,11 +633,17 @@ describe('DeleteUser', () => {
beforeEach(async () => {
variables = { id: 'u343', resource: [] }
user = await Factory.build('user', {
name: 'My name should be deleted',
about: 'along with my about',
id: 'u343',
})
user = await Factory.build(
'user',
{
name: 'My name should be deleted',
about: 'along with my about',
id: 'u343',
},
{
profileHeader: Factory.build('image'),
},
)
await Factory.build(
'user',
{
@ -792,8 +804,8 @@ describe('DeleteUser', () => {
).resolves.toMatchObject(expectedResponse)
})
it('deletes user avatar and post hero images', async () => {
await expect(neode.all('Image')).resolves.toHaveLength(22)
it('deletes user avatar, profile header and post hero images', async () => {
await expect(neode.all('Image')).resolves.toHaveLength(23)
await mutate({ mutation: deleteUserMutation, variables })
await expect(neode.all('Image')).resolves.toHaveLength(20)
})

View File

@ -26,6 +26,7 @@ type User {
email: String! @cypher(statement: "MATCH (this)-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e.email")
slug: String!
avatar: Image @relation(name: "AVATAR_IMAGE", direction: "OUT")
profileHeader: Image @relation(name: "PROFILE_HEADER_IMAGE", direction: "OUT")
deleted: Boolean
disabled: Boolean
role: UserGroup!
@ -192,6 +193,7 @@ type Mutation {
email: String
slug: String
avatar: ImageInput
profileHeader: ImageInput
locationName: String
about: String
termsAndConditionsAgreedVersion: String