diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index b5ce4fe07..71a44f225 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -178,6 +178,7 @@ export default shield( GenerateInviteCode: isAuthenticated, switchUserRole: isAdmin, markTeaserAsViewed: allow, + saveCategorySettings: isAuthenticated, }, User: { email: or(isMyOwn, isAdmin), diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index 5dc78c5e1..23a39a2a1 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -269,6 +269,45 @@ export default { session.close() } }, + saveCategorySettings: async (object, args, context, resolveInfo) => { + const { activeCategories } = args + const { + user: { id }, + } = context + + const session = context.driver.session() + await session.writeTransaction((transaction) => { + return transaction.run( + ` + MATCH (user:User { id: $id })-[previousCategories:NOT_INTERESTED_IN]->(category:Category) + DELETE previousCategories + RETURN user, category + `, + { id }, + ) + }) + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const saveCategorySettingsResponse = await transaction.run( + ` + MATCH (category:Category) WHERE NOT category.id IN $activeCategories + MATCH (user:User { id: $id }) + MERGE (user)-[r:NOT_INTERESTED_IN]->(category) + RETURN user, r, category + `, + { id, activeCategories }, + ) + const [user] = await saveCategorySettingsResponse.records.map((record) => + record.get('user'), + ) + return user + }) + try { + await writeTxResultPromise + return true + } finally { + session.close() + } + }, }, User: { email: async (parent, params, context, resolveInfo) => { diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index 920ef52ea..7116d2201 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -3,6 +3,7 @@ import { gql } from '../../helpers/jest' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' +import { categories } from '../../constants/categories' const categoryIds = ['cat9'] let user @@ -56,6 +57,12 @@ const switchUserRoleMutation = gql` } ` +const saveCategorySettings = gql` + mutation ($activeCategories: [String]) { + saveCategorySettings(activeCategories: $activeCategories) + } +` + beforeAll(async () => { await cleanDatabase() @@ -544,3 +551,146 @@ describe('switch user role', () => { }) }) }) + +describe('save category settings', () => { + beforeEach(async () => { + await Promise.all( + categories.map(({ icon, name }, index) => { + Factory.build('category', { + id: `cat${index + 1}`, + slug: name, + name, + icon, + }) + }), + ) + }) + + beforeEach(async () => { + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + variables = { + activeCategories: ['cat1', 'cat3', 'cat5'], + } + }) + + describe('not authenticated', () => { + beforeEach(async () => { + authenticatedUser = undefined + }) + + it('throws an error', async () => { + await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + const userQuery = gql` + query ($id: ID) { + User(id: $id) { + activeCategories { + id + } + } + } + ` + + describe('no categories saved', () => { + it('returns true for active categories mutation', async () => { + await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual( + expect.objectContaining({ + data: { saveCategorySettings: true }, + }), + ) + }) + + describe('query for user', () => { + beforeEach(async () => { + await mutate({ mutation: saveCategorySettings, variables }) + }) + + it('returns the active categories when user is queried', async () => { + await expect( + query({ query: userQuery, variables: { id: authenticatedUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + activeCategories: expect.arrayContaining([ + { id: 'cat1' }, + { id: 'cat3' }, + { id: 'cat5' }, + ]), + }, + ], + }, + }), + ) + }) + }) + }) + + describe('categories already saved', () => { + beforeEach(async () => { + variables = { + activeCategories: ['cat1', 'cat3', 'cat5'], + } + await mutate({ mutation: saveCategorySettings, variables }) + variables = { + activeCategories: ['cat10', 'cat11', 'cat12', 'cat8', 'cat9'], + } + }) + + it('returns true', async () => { + await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual( + expect.objectContaining({ + data: { saveCategorySettings: true }, + }), + ) + }) + + describe('query for user', () => { + beforeEach(async () => { + await mutate({ mutation: saveCategorySettings, variables }) + }) + + it('returns the new active categories when user is queried', async () => { + await expect( + query({ query: userQuery, variables: { id: authenticatedUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + activeCategories: expect.arrayContaining([ + { id: 'cat10' }, + { id: 'cat11' }, + { id: 'cat12' }, + { id: 'cat8' }, + { id: 'cat9' }, + ]), + }, + ], + }, + }), + ) + }) + }) + }) + }) +}) diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 871e73ad8..d2ac79037 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -114,6 +114,14 @@ type User { badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") emotions: [EMOTED] + + activeCategories: [Category] @cypher( + statement: """ + MATCH (category:Category) + WHERE NOT ((this)-[:NOT_INTERESTED_IN]->(category)) + RETURN category + """ + ) } @@ -220,4 +228,6 @@ type Mutation { unblockUser(id: ID!): User switchUserRole(role: UserRole!, id: ID!): User + + saveCategorySettings(activeCategories: [String]): Boolean }