diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js index 1ebf063ff..4b3e1d922 100644 --- a/backend/src/db/factories.js +++ b/backend/src/db/factories.js @@ -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 }) diff --git a/backend/src/db/seed.js b/backend/src/db/seed.js index 685b5ef0e..a614a0be8 100644 --- a/backend/src/db/seed.js +++ b/backend/src/db/seed.js @@ -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( diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.js b/backend/src/middleware/softDelete/softDeleteMiddleware.js index 2e1f60251..d2b93bac5 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.js +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.js @@ -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 diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js index 63569ddb0..c2e82af90 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js @@ -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', () => { diff --git a/backend/src/models/User.js b/backend/src/models/User.js index ae7e1ae8c..048dddea2 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -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' }, diff --git a/backend/src/schema/resolvers/images/images.js b/backend/src/schema/resolvers/images/images.js index 18a3569b6..1af24bf76 100644 --- a/backend/src/schema/resolvers/images/images.js +++ b/backend/src/schema/resolvers/images/images.js @@ -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}`) } } diff --git a/backend/src/schema/resolvers/statistics.spec.js b/backend/src/schema/resolvers/statistics.spec.js index c5bb5f88b..7de771629 100644 --- a/backend/src/schema/resolvers/statistics.spec.js +++ b/backend/src/schema/resolvers/statistics.spec.js @@ -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: () => { diff --git a/backend/src/schema/resolvers/user_management.spec.js b/backend/src/schema/resolvers/user_management.spec.js index b434ea628..5fb7f6c60 100644 --- a/backend/src/schema/resolvers/user_management.spec.js +++ b/backend/src/schema/resolvers/user_management.spec.js @@ -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', diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index e276968e5..d887d7e8d 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -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)', }, diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index cb9012133..758f044d3 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -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) }) diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index af525396b..4cb5b60e4 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -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